1use std::fs;
7use std::io;
8use std::path::Path;
9use std::time::{SystemTime, UNIX_EPOCH};
10
11pub fn atomic_write(path: &Path, content: &[u8]) -> io::Result<()> {
18 if let Some(parent) = path.parent() {
19 fs::create_dir_all(parent)?;
20 }
21
22 let nonce = SystemTime::now()
23 .duration_since(UNIX_EPOCH)
24 .unwrap_or_default()
25 .as_nanos();
26 let tmp = path.with_extension(format!("tmp.{}", nonce));
27
28 if let Err(err) = fs::write(&tmp, content) {
29 let _ = fs::remove_file(&tmp);
31 return Err(err);
32 }
33
34 if let Err(err) = fs::rename(&tmp, path) {
35 let _ = fs::remove_file(&tmp);
36 return Err(err);
37 }
38
39 Ok(())
40}
41
42#[cfg(test)]
43mod tests {
44 use super::*;
45 use tempfile::tempdir;
46
47 #[test]
48 fn atomic_write_creates_file_and_parent_dirs() {
49 let dir = tempdir().unwrap();
50 let target = dir.path().join("sub").join("deep").join("config.json");
51 atomic_write(&target, b"{\"ok\": true}").unwrap();
52 assert_eq!(fs::read_to_string(&target).unwrap(), "{\"ok\": true}");
53 }
54
55 #[test]
56 fn atomic_write_replaces_existing_content() {
57 let dir = tempdir().unwrap();
58 let target = dir.path().join("data.json");
59 fs::write(&target, b"old").unwrap();
60 atomic_write(&target, b"new").unwrap();
61 assert_eq!(fs::read_to_string(&target).unwrap(), "new");
62 }
63
64 #[test]
65 fn atomic_write_leaves_no_temp_files() {
66 let dir = tempdir().unwrap();
67 let target = dir.path().join("clean.json");
68 atomic_write(&target, b"data").unwrap();
69 let siblings: Vec<_> = fs::read_dir(dir.path())
70 .unwrap()
71 .filter_map(|e| e.ok())
72 .collect();
73 assert_eq!(siblings.len(), 1);
74 assert_eq!(siblings[0].file_name().to_string_lossy(), "clean.json");
75 }
76}