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

This commit is contained in:
DuProcess
2026-07-16 19:57:57 -04:00
parent 02a7087241
commit 53d31a88e1
+79 -17
View File
@@ -3654,24 +3654,67 @@ fn download_symlink(
#[cfg(unix)]
fn create_local_symlink(path: &Path, target: &str, overwrite: bool) -> Result<()> {
if let Ok(metadata) = fs::symlink_metadata(path) {
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()))?;
}
prepare_local_symlink_destination(path, overwrite)?;
std::os::unix::fs::symlink(target, path)
.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<()> {
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<()> {
file.seek(std::io::SeekFrom::Start(0))?;
let mut remaining = bytes;
@@ -10034,13 +10077,13 @@ mod tests {
clear_cached_credentials, effective_update_artifact_tag, ensure_tui_safe_status_overlay,
expand_ssh_path_tokens, input_contains_focus_in, input_matches_escape,
is_local_status_target, is_resume_response_for_client, latest_release_download_url,
load_first_native_identity_with_prompt, native_proxy_udp_warning,
newest_client_trace_path_from, parse_dynamic_forward, parse_escape_key,
parse_local_forward, parse_remote_forward, parse_single_remote_path, parse_ssh_config,
parse_trace_line, parse_trace_options, parse_trace_report_options, parse_trace_summary,
parse_update_options, post_submit_hold_duration, queue_or_send_stream_data,
queue_pending_user_input, queue_stale_pending_user_input, raw_contains_host_table,
recv_response_until, refresh_live_addr, release_tag_download_url,
load_first_native_identity_with_prompt, local_symlink_target_is_dir,
native_proxy_udp_warning, newest_client_trace_path_from, parse_dynamic_forward,
parse_escape_key, parse_local_forward, parse_remote_forward, parse_single_remote_path,
parse_ssh_config, parse_trace_line, parse_trace_options, parse_trace_report_options,
parse_trace_summary, parse_update_options, post_submit_hold_duration,
queue_or_send_stream_data, queue_pending_user_input, queue_stale_pending_user_input,
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,
render_status_clear, render_status_overlay, requested_env, resolve_forward_agent_endpoint,
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]
fn cleanup_stream_state_removes_all_per_stream_state_only_for_target() {
let stream_id = 42;