From a489d77b0afc797d46f1edd29282a6316f7cbb5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Mon, 7 Sep 2026 13:52:15 +0000 Subject: [PATCH] fix: write the dump to a temporary file first --- src/scripts/postgres.rs | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/scripts/postgres.rs b/src/scripts/postgres.rs index 6c66e73..50ba0d9 100644 --- a/src/scripts/postgres.rs +++ b/src/scripts/postgres.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result, anyhow, bail}; use std::{ - fs::File, + fs::{self, File}, io::{self, Read, Write}, path::{Path, PathBuf}, process::Stdio, @@ -157,6 +157,13 @@ fn restore_cluster(db: &Database, script: &str, file: &Path) -> Result<()> { Ok(()) } +fn suffixed(path: &Path, suffix: &str) -> PathBuf { + let mut out = path.as_os_str().to_owned(); + out.push(suffix); + + PathBuf::from(out) +} + fn read_header(path: &Path) -> Result> { let mut file = File::open(path).with_context(|| format!("opening {}", path.display()))?; let mut header = vec![0; HEADER_LEN]; @@ -343,15 +350,21 @@ pub fn dump(file: &PathBuf, format: Format, gzip: bool) -> Result<()> { eprintln!("dumping to local file {}", file.to_string_lossy()); - let stdout = Stdio::from(File::create(file)?); - if gzip { + // written beside the target and renamed once the dump succeeds, so a failure + // cannot destroy the dump that is already there + let partial = suffixed(file, ".partial"); + let stdout = Stdio::from( + File::create(&partial).with_context(|| format!("creating {}", partial.display()))?, + ); + + let dumped = if gzip { // the whole pipeline has to arrive as one shell argument CommandBuilder::docker() .args("exec") .arg(&db.container) .args("sh -c") .arg(format!("{} | gzip", dump_command(&db, format))) - .exec_redirect_stdout(stdout)?; + .exec_redirect_stdout(stdout) } else { let dumping = match format.flag() { Some(flag) => in_container(&db) @@ -362,9 +375,17 @@ pub fn dump(file: &PathBuf, format: Format, gzip: bool) -> Result<()> { None => in_container(&db).args("pg_dumpall -U").arg(&db.user), }; - dumping.exec_redirect_stdout(stdout)?; + dumping.exec_redirect_stdout(stdout) + }; + + if let Err(e) = dumped { + let _ = fs::remove_file(&partial); + return Err(e); } + fs::rename(&partial, file) + .with_context(|| format!("renaming {} to {}", partial.display(), file.display()))?; + Ok(()) }