use std::fs::{self, File};
use std::io::{self, BufWriter, Write};
use std::path::Path;
pub fn replace(path: &Path, write: impl FnOnce(&mut BufWriter<File>) -> io::Result<()>) -> bool {
let Some(dir) = path.parent() else {
return false;
};
if fs::create_dir_all(dir).is_err() {
return false;
}
let tmp = path.with_extension(format!("{}.tmp", std::process::id()));
if emit(&tmp, write).is_err() || fs::rename(&tmp, path).is_err() {
let _ = fs::remove_file(&tmp);
return false;
}
true
}
fn emit(tmp: &Path, write: impl FnOnce(&mut BufWriter<File>) -> io::Result<()>) -> io::Result<()> {
let mut out = BufWriter::new(File::create(tmp)?);
write(&mut out)?;
out.flush()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_write_replaces_the_file_and_creates_its_directory() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nested").join("file");
assert!(replace(&path, |out| out.write_all(b"first")));
assert_eq!(fs::read(&path).unwrap(), b"first");
assert!(replace(&path, |out| out.write_all(b"second")));
assert_eq!(fs::read(&path).unwrap(), b"second");
let leftovers = fs::read_dir(path.parent().unwrap())
.unwrap()
.flatten()
.filter(|e| e.path().extension().is_some_and(|x| x == "tmp"))
.count();
assert_eq!(leftovers, 0, "temp files must not survive a write");
}
#[test]
fn a_failed_write_keeps_the_previous_file_and_no_temp() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("1");
assert!(replace(&path, |out| out.write_all(b"kept")));
assert!(!replace(&path, |out| {
out.write_all(b"partial")?;
Err(io::Error::other("producer gave up"))
}));
assert_eq!(fs::read(&path).unwrap(), b"kept");
let leftovers = fs::read_dir(dir.path())
.unwrap()
.flatten()
.filter(|e| e.path().extension().is_some_and(|x| x == "tmp"))
.count();
assert_eq!(leftovers, 0, "a failed write leaves no temp behind");
}
#[test]
fn a_directory_that_cannot_be_created_drops_the_write() {
let dir = tempfile::tempdir().unwrap();
let blocker = dir.path().join("blocker");
fs::write(&blocker, b"a file, not a directory").unwrap();
let path = blocker.join("nested").join("file");
assert!(!replace(&path, |out| out.write_all(b"bytes")));
assert!(!path.exists());
}
}