From e2e2181469cf1cfc640cea42baa33c76f23b3471 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 27 Sep 2026 21:42:12 -0400 Subject: [PATCH] Write the composite under a .part name until it is whole The merge writes its outbox record before the DNG, which takes minutes, and a drain that ran in the meantime took whatever lay at the record's name: a sync pass that fired mid-merge uploaded the first part of the composite and cleared the record. The file is now written as x.dng.part and renamed into place once the last strip is in; the drain skips a record whose payload does not exist yet. --- ui/dr-ui/src/merge.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/ui/dr-ui/src/merge.rs b/ui/dr-ui/src/merge.rs index f38e8a9..cae4bf8 100644 --- a/ui/dr-ui/src/merge.rs +++ b/ui/dr-ui/src/merge.rs @@ -617,8 +617,17 @@ fn run_inner( (path, true) } }; + // Written under a name of its own and renamed into place once the last + // strip is in. The record above names `out_path`, and a drain that runs + // while the merge is still writing — a sync pass fires one whenever the + // sweep finishes — takes whatever is at that name: it uploaded the first + // few hundred megabytes of a composite, cleared the record, and the file + // on the server stayed truncated for good. `pending` skips a record whose + // payload does not exist yet, so the rename is what releases it. + let part_path = part_name(&out_path); let cleanup = |path: &Path| { let _ = std::fs::remove_file(path); + let _ = std::fs::remove_file(part_name(path)); if staged { let _ = std::fs::remove_file(crate::export::destination_record(path)); } @@ -798,7 +807,7 @@ fn run_inner( let inscribed_for_writer = inscribed.clone(); let fill_for_writer = fill_cam.is_some(); let file = - std::fs::File::create(&out_path).map_err(|e| format!("{}: {e}", out_path.display()))?; + std::fs::File::create(&part_path).map_err(|e| format!("{}: {e}", part_path.display()))?; let writer = executors::spawn(Executor::Io, "dng-write", move || -> Result<(), String> { let mut file = std::io::BufWriter::new(file); dr_export::write_linear_dng( @@ -910,6 +919,10 @@ fn run_inner( cleanup(&out_path); return Err(e); } + if let Err(e) = std::fs::rename(&part_path, &out_path) { + cleanup(&out_path); + return Err(format!("{}: {e}", out_path.display())); + } log::info!( "merge: {}×{} written to {} in {:?}", out_w, @@ -1471,6 +1484,14 @@ fn stem(p: &Path) -> String { .unwrap_or_else(|| p.display().to_string()) } +/// Where the composite is written until it is whole: `x.dng.part` for +/// `x.dng`. +fn part_name(path: &Path) -> PathBuf { + let mut name = path.as_os_str().to_owned(); + name.push(".part"); + PathBuf::from(name) +} + /// `name` in `dir`, numbered if that name is taken: a merge never /// overwrites (FR-MRG-3). fn unused_name(dir: &Path, name: &str) -> PathBuf {