concinnity_host/store/
atomic.rs1use std::fs::{self, File};
14use std::io::{self, BufWriter, Write};
15use std::path::Path;
16
17pub fn replace(path: &Path, write: impl FnOnce(&mut BufWriter<File>) -> io::Result<()>) -> bool {
21 let Some(dir) = path.parent() else {
22 return false;
23 };
24 if fs::create_dir_all(dir).is_err() {
25 return false;
26 }
27 let tmp = path.with_extension(format!("{}.tmp", std::process::id()));
28 if emit(&tmp, write).is_err() || fs::rename(&tmp, path).is_err() {
29 let _ = fs::remove_file(&tmp);
30 return false;
31 }
32 true
33}
34
35fn emit(tmp: &Path, write: impl FnOnce(&mut BufWriter<File>) -> io::Result<()>) -> io::Result<()> {
38 let mut out = BufWriter::new(File::create(tmp)?);
39 write(&mut out)?;
40 out.flush()?;
41 Ok(())
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 #[test]
49 fn a_write_replaces_the_file_and_creates_its_directory() {
50 let dir = tempfile::tempdir().unwrap();
51 let path = dir.path().join("cache").join("1");
52
53 assert!(replace(&path, |out| out.write_all(b"first")));
54 assert_eq!(fs::read(&path).unwrap(), b"first");
55
56 assert!(replace(&path, |out| out.write_all(b"second")));
57 assert_eq!(fs::read(&path).unwrap(), b"second");
58
59 let leftovers = fs::read_dir(path.parent().unwrap())
60 .unwrap()
61 .flatten()
62 .filter(|e| e.path().extension().is_some_and(|x| x == "tmp"))
63 .count();
64 assert_eq!(leftovers, 0, "temp files must not survive a write");
65 }
66
67 #[test]
70 fn a_failed_write_keeps_the_previous_file_and_no_temp() {
71 let dir = tempfile::tempdir().unwrap();
72 let path = dir.path().join("1");
73 assert!(replace(&path, |out| out.write_all(b"kept")));
74
75 assert!(!replace(&path, |out| {
76 out.write_all(b"partial")?;
77 Err(io::Error::other("producer gave up"))
78 }));
79 assert_eq!(fs::read(&path).unwrap(), b"kept");
80 let leftovers = fs::read_dir(dir.path())
81 .unwrap()
82 .flatten()
83 .filter(|e| e.path().extension().is_some_and(|x| x == "tmp"))
84 .count();
85 assert_eq!(leftovers, 0, "a failed write leaves no temp behind");
86 }
87
88 #[test]
91 fn a_directory_that_cannot_be_created_drops_the_write() {
92 let dir = tempfile::tempdir().unwrap();
93 let blocker = dir.path().join("blocker");
94 fs::write(&blocker, b"a file, not a directory").unwrap();
95
96 let path = blocker.join("cache").join("1");
97 assert!(!replace(&path, |out| out.write_all(b"bytes")));
98 assert!(!path.exists());
99 }
100}