From 039dc641b3d5af30fb0d46e017fcc5ef1ba8bdef Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:10:17 -0400 Subject: [PATCH] Fix resumed copy truncation --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/bin/dosh-client.rs | 8 ++- src/bin/dosh-server.rs | 3 + tests/integration_smoke.rs | 111 +++++++++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2507a9..98925e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,7 +436,7 @@ dependencies = [ [[package]] name = "dosh" -version = "1.0.0-rc9" +version = "1.0.0-rc10" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 965f8ec..f13c09c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dosh" -version = "1.0.0-rc9" +version = "1.0.0-rc10" edition = "2024" license = "MIT" diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index e311d81..d41ce3e 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -2498,11 +2498,15 @@ fn download_file( 0 }; let mut file = if offset > 0 { - fs::OpenOptions::new() + let file = fs::OpenOptions::new() .read(true) + .write(true) .append(true) .open(local) - .with_context(|| format!("open {}", local.display()))? + .with_context(|| format!("open {}", local.display()))?; + file.set_len(offset) + .with_context(|| format!("truncate {}", local.display()))?; + file } else { fs::OpenOptions::new() .create(true) diff --git a/src/bin/dosh-server.rs b/src/bin/dosh-server.rs index b65248b..0bee9cb 100644 --- a/src/bin/dosh-server.rs +++ b/src/bin/dosh-server.rs @@ -2667,10 +2667,13 @@ async fn handle_file_request( let (file, temp_path, written, atomic) = if resume && final_path.exists() { let mut file = fs::OpenOptions::new() .read(true) + .write(true) .append(true) .open(&final_path) .with_context(|| format!("open {}", final_path.display()))?; let written = file.metadata()?.len().min(size); + file.set_len(written) + .with_context(|| format!("truncate {}", final_path.display()))?; if written > 0 { let mut prefix = fs::File::open(&final_path)?; hash_exact_prefix(&mut prefix, written, &mut hasher)?; diff --git a/tests/integration_smoke.rs b/tests/integration_smoke.rs index be40288..4b99628 100644 --- a/tests/integration_smoke.rs +++ b/tests/integration_smoke.rs @@ -1108,6 +1108,117 @@ fn native_file_copy_recursive_round_trip() { let _ = server.wait(); } +#[test] +fn native_file_copy_resume_truncates_oversized_destinations() { + let dir = tempfile::tempdir().unwrap(); + let port = free_udp_port(); + let config = write_server_config(&dir, port); + write_native_client_auth(&dir, &config); + let mut server = start_server(&dir, &config); + let client_bin = env!("CARGO_BIN_EXE_dosh-client"); + + let local_short = dir.path().join("short.txt"); + fs::write(&local_short, b"short\n").unwrap(); + + let seed_remote = Command::new(client_bin) + .arg("--dosh-host") + .arg("127.0.0.1") + .arg("--dosh-port") + .arg(port.to_string()) + .arg("exec") + .arg("local") + .arg("printf 'short\\nSTALE' > \"$HOME/resume-upload.txt\"") + .env("HOME", dir.path()) + .output() + .unwrap(); + assert!( + seed_remote.status.success(), + "seed remote failed: stdout={} stderr={}", + String::from_utf8_lossy(&seed_remote.stdout), + String::from_utf8_lossy(&seed_remote.stderr) + ); + + let resume_upload = Command::new(client_bin) + .arg("--dosh-host") + .arg("127.0.0.1") + .arg("--dosh-port") + .arg(port.to_string()) + .arg("cp") + .arg("--resume") + .arg(&local_short) + .arg("local:resume-upload.txt") + .env("HOME", dir.path()) + .output() + .unwrap(); + assert!( + resume_upload.status.success(), + "resume upload failed: stdout={} stderr={}", + String::from_utf8_lossy(&resume_upload.stdout), + String::from_utf8_lossy(&resume_upload.stderr) + ); + + let cat_upload = Command::new(client_bin) + .arg("--dosh-host") + .arg("127.0.0.1") + .arg("--dosh-port") + .arg(port.to_string()) + .arg("cat") + .arg("local:resume-upload.txt") + .env("HOME", dir.path()) + .output() + .unwrap(); + assert!( + cat_upload.status.success(), + "cat upload failed: stdout={} stderr={}", + String::from_utf8_lossy(&cat_upload.stdout), + String::from_utf8_lossy(&cat_upload.stderr) + ); + assert_eq!(String::from_utf8_lossy(&cat_upload.stdout), "short\n"); + + let seed_download = Command::new(client_bin) + .arg("--dosh-host") + .arg("127.0.0.1") + .arg("--dosh-port") + .arg(port.to_string()) + .arg("exec") + .arg("local") + .arg("printf 'tiny\\n' > \"$HOME/resume-download.txt\"") + .env("HOME", dir.path()) + .output() + .unwrap(); + assert!( + seed_download.status.success(), + "seed download failed: stdout={} stderr={}", + String::from_utf8_lossy(&seed_download.stdout), + String::from_utf8_lossy(&seed_download.stderr) + ); + + let local_download = dir.path().join("resume-download-local.txt"); + fs::write(&local_download, b"tiny\nSTALE").unwrap(); + let resume_download = Command::new(client_bin) + .arg("--dosh-host") + .arg("127.0.0.1") + .arg("--dosh-port") + .arg(port.to_string()) + .arg("cp") + .arg("--resume") + .arg("local:resume-download.txt") + .arg(&local_download) + .env("HOME", dir.path()) + .output() + .unwrap(); + assert!( + resume_download.status.success(), + "resume download failed: stdout={} stderr={}", + String::from_utf8_lossy(&resume_download.stdout), + String::from_utf8_lossy(&resume_download.stderr) + ); + assert_eq!(fs::read_to_string(&local_download).unwrap(), "tiny\n"); + + let _ = server.kill(); + let _ = server.wait(); +} + #[test] fn native_exec_command_smoke() { let dir = tempfile::tempdir().unwrap();