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.
This commit is contained in:
2026-09-28 19:57:37 -04:00
parent ad27369cdc
commit e2e2181469
+22 -1
View File
@@ -617,8 +617,17 @@ fn run_inner(
(path, true) (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 cleanup = |path: &Path| {
let _ = std::fs::remove_file(path); let _ = std::fs::remove_file(path);
let _ = std::fs::remove_file(part_name(path));
if staged { if staged {
let _ = std::fs::remove_file(crate::export::destination_record(path)); let _ = std::fs::remove_file(crate::export::destination_record(path));
} }
@@ -798,7 +807,7 @@ fn run_inner(
let inscribed_for_writer = inscribed.clone(); let inscribed_for_writer = inscribed.clone();
let fill_for_writer = fill_cam.is_some(); let fill_for_writer = fill_cam.is_some();
let file = 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 writer = executors::spawn(Executor::Io, "dng-write", move || -> Result<(), String> {
let mut file = std::io::BufWriter::new(file); let mut file = std::io::BufWriter::new(file);
dr_export::write_linear_dng( dr_export::write_linear_dng(
@@ -910,6 +919,10 @@ fn run_inner(
cleanup(&out_path); cleanup(&out_path);
return Err(e); 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!( log::info!(
"merge: {}×{} written to {} in {:?}", "merge: {}×{} written to {} in {:?}",
out_w, out_w,
@@ -1471,6 +1484,14 @@ fn stem(p: &Path) -> String {
.unwrap_or_else(|| p.display().to_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 /// `name` in `dir`, numbered if that name is taken: a merge never
/// overwrites (FR-MRG-3). /// overwrites (FR-MRG-3).
fn unused_name(dir: &Path, name: &str) -> PathBuf { fn unused_name(dir: &Path, name: &str) -> PathBuf {