Support Windows symlink downloads
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s
This commit is contained in:
+79
-17
@@ -3654,24 +3654,67 @@ fn download_symlink(
|
|||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn create_local_symlink(path: &Path, target: &str, overwrite: bool) -> Result<()> {
|
fn create_local_symlink(path: &Path, target: &str, overwrite: bool) -> Result<()> {
|
||||||
if let Ok(metadata) = fs::symlink_metadata(path) {
|
prepare_local_symlink_destination(path, overwrite)?;
|
||||||
anyhow::ensure!(overwrite, "destination exists: {}", path.display());
|
|
||||||
anyhow::ensure!(
|
|
||||||
!metadata.is_dir(),
|
|
||||||
"destination is a directory: {}",
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
fs::remove_file(path).with_context(|| format!("remove {}", path.display()))?;
|
|
||||||
}
|
|
||||||
std::os::unix::fs::symlink(target, path)
|
std::os::unix::fs::symlink(target, path)
|
||||||
.with_context(|| format!("symlink {} -> {}", path.display(), target))
|
.with_context(|| format!("symlink {} -> {}", path.display(), target))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(windows)]
|
||||||
|
fn create_local_symlink(path: &Path, target: &str, overwrite: bool) -> Result<()> {
|
||||||
|
prepare_local_symlink_destination(path, overwrite)?;
|
||||||
|
if local_symlink_target_is_dir(path, target) {
|
||||||
|
std::os::windows::fs::symlink_dir(target, path)
|
||||||
|
.with_context(|| format!("directory symlink {} -> {}", path.display(), target))
|
||||||
|
} else {
|
||||||
|
std::os::windows::fs::symlink_file(target, path)
|
||||||
|
.with_context(|| format!("file symlink {} -> {}", path.display(), target))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(unix, windows)))]
|
||||||
fn create_local_symlink(_path: &Path, _target: &str, _overwrite: bool) -> Result<()> {
|
fn create_local_symlink(_path: &Path, _target: &str, _overwrite: bool) -> Result<()> {
|
||||||
bail!("symlink creation is not supported on this client platform")
|
bail!("symlink creation is not supported on this client platform")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepare_local_symlink_destination(path: &Path, overwrite: bool) -> Result<()> {
|
||||||
|
let Ok(metadata) = fs::symlink_metadata(path) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
anyhow::ensure!(overwrite, "destination exists: {}", path.display());
|
||||||
|
anyhow::ensure!(
|
||||||
|
!metadata.is_dir(),
|
||||||
|
"destination is a directory: {}",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
match fs::remove_file(path) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(file_err) if metadata.file_type().is_symlink() => {
|
||||||
|
fs::remove_dir(path).map_err(|_| file_err)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
.with_context(|| format!("remove {}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(not(any(test, windows)), allow(dead_code))]
|
||||||
|
fn local_symlink_target_is_dir(link_path: &Path, target: &str) -> bool {
|
||||||
|
if target.ends_with('/') || target.ends_with('\\') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let target_path = Path::new(target);
|
||||||
|
let resolved = if target_path.is_absolute() {
|
||||||
|
target_path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
link_path
|
||||||
|
.parent()
|
||||||
|
.filter(|parent| !parent.as_os_str().is_empty())
|
||||||
|
.unwrap_or_else(|| Path::new("."))
|
||||||
|
.join(target_path)
|
||||||
|
};
|
||||||
|
fs::metadata(resolved).is_ok_and(|metadata| metadata.is_dir())
|
||||||
|
}
|
||||||
|
|
||||||
fn hash_prefix(file: &mut fs::File, bytes: u64, hasher: &mut Sha256) -> Result<()> {
|
fn hash_prefix(file: &mut fs::File, bytes: u64, hasher: &mut Sha256) -> Result<()> {
|
||||||
file.seek(std::io::SeekFrom::Start(0))?;
|
file.seek(std::io::SeekFrom::Start(0))?;
|
||||||
let mut remaining = bytes;
|
let mut remaining = bytes;
|
||||||
@@ -10034,13 +10077,13 @@ mod tests {
|
|||||||
clear_cached_credentials, effective_update_artifact_tag, ensure_tui_safe_status_overlay,
|
clear_cached_credentials, effective_update_artifact_tag, ensure_tui_safe_status_overlay,
|
||||||
expand_ssh_path_tokens, input_contains_focus_in, input_matches_escape,
|
expand_ssh_path_tokens, input_contains_focus_in, input_matches_escape,
|
||||||
is_local_status_target, is_resume_response_for_client, latest_release_download_url,
|
is_local_status_target, is_resume_response_for_client, latest_release_download_url,
|
||||||
load_first_native_identity_with_prompt, native_proxy_udp_warning,
|
load_first_native_identity_with_prompt, local_symlink_target_is_dir,
|
||||||
newest_client_trace_path_from, parse_dynamic_forward, parse_escape_key,
|
native_proxy_udp_warning, newest_client_trace_path_from, parse_dynamic_forward,
|
||||||
parse_local_forward, parse_remote_forward, parse_single_remote_path, parse_ssh_config,
|
parse_escape_key, parse_local_forward, parse_remote_forward, parse_single_remote_path,
|
||||||
parse_trace_line, parse_trace_options, parse_trace_report_options, parse_trace_summary,
|
parse_ssh_config, parse_trace_line, parse_trace_options, parse_trace_report_options,
|
||||||
parse_update_options, post_submit_hold_duration, queue_or_send_stream_data,
|
parse_trace_summary, parse_update_options, post_submit_hold_duration,
|
||||||
queue_pending_user_input, queue_stale_pending_user_input, raw_contains_host_table,
|
queue_or_send_stream_data, queue_pending_user_input, queue_stale_pending_user_input,
|
||||||
recv_response_until, refresh_live_addr, release_tag_download_url,
|
raw_contains_host_table, recv_response_until, refresh_live_addr, release_tag_download_url,
|
||||||
release_tag_from_effective_url, release_version_from_tag, render_frame_bytes,
|
release_tag_from_effective_url, release_version_from_tag, render_frame_bytes,
|
||||||
render_status_clear, render_status_overlay, requested_env, resolve_forward_agent_endpoint,
|
render_status_clear, render_status_overlay, requested_env, resolve_forward_agent_endpoint,
|
||||||
resolved_startup_command, retire_stream_state, retransmit_stream_closes,
|
resolved_startup_command, retire_stream_state, retransmit_stream_closes,
|
||||||
@@ -10124,6 +10167,25 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn symlink_target_dir_inference_uses_existing_relative_target() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
fs::create_dir(dir.path().join("target-dir")).unwrap();
|
||||||
|
let link = dir.path().join("link");
|
||||||
|
|
||||||
|
assert!(local_symlink_target_is_dir(&link, "target-dir"));
|
||||||
|
assert!(!local_symlink_target_is_dir(&link, "missing-file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn symlink_target_dir_inference_honors_trailing_separator() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let link = dir.path().join("link");
|
||||||
|
|
||||||
|
assert!(local_symlink_target_is_dir(&link, "missing-dir/"));
|
||||||
|
assert!(local_symlink_target_is_dir(&link, "missing-dir\\"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cleanup_stream_state_removes_all_per_stream_state_only_for_target() {
|
fn cleanup_stream_state_removes_all_per_stream_state_only_for_target() {
|
||||||
let stream_id = 42;
|
let stream_id = 42;
|
||||||
|
|||||||
Reference in New Issue
Block a user