linsight_core/
atomic_write.rs1use std::fs::OpenOptions;
25use std::io::{Result, Write};
26use std::path::Path;
27use std::sync::atomic::{AtomicU64, Ordering};
28
29static TMP_COUNTER: AtomicU64 = AtomicU64::new(0);
34
35pub fn atomic_write_json<T: serde::Serialize>(path: &Path, value: &T) -> Result<()> {
40 if let Some(parent) = path.parent() {
41 std::fs::create_dir_all(parent)?;
42 }
43 let suffix = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
44 let pid = std::process::id();
45 let tmp = path.with_extension(format!("json.tmp.{pid}.{suffix}"));
46 let body = serde_json::to_string_pretty(value).map_err(std::io::Error::other)?;
47 {
48 let mut f = OpenOptions::new().write(true).create_new(true).open(&tmp)?;
49 f.write_all(body.as_bytes())?;
50 f.sync_all()?;
51 }
52 if let Err(e) = std::fs::rename(&tmp, path) {
53 let _ = std::fs::remove_file(&tmp);
54 return Err(e);
55 }
56 Ok(())
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use serde::Serialize;
63
64 #[derive(Serialize)]
65 struct Sample {
66 name: String,
67 value: u32,
68 }
69
70 #[test]
71 fn writes_pretty_json_to_target_path() {
72 let dir = tempfile::TempDir::new().unwrap();
73 let path = dir.path().join("out.json");
74 let s = Sample { name: "x".into(), value: 7 };
75 atomic_write_json(&path, &s).unwrap();
76 let body = std::fs::read_to_string(&path).unwrap();
77 assert!(body.contains("\"name\""));
78 assert!(body.contains("\"value\": 7"));
79 }
80
81 #[test]
82 fn creates_parent_dir_if_missing() {
83 let dir = tempfile::TempDir::new().unwrap();
84 let nested = dir.path().join("a/b/c/out.json");
85 let s = Sample { name: "x".into(), value: 1 };
86 atomic_write_json(&nested, &s).unwrap();
87 assert!(nested.exists());
88 }
89
90 #[test]
91 fn leaves_no_tmp_sibling_after_success() {
92 let dir = tempfile::TempDir::new().unwrap();
93 let path = dir.path().join("out.json");
94 let s = Sample { name: "x".into(), value: 1 };
95 atomic_write_json(&path, &s).unwrap();
96 let entries: Vec<_> =
97 std::fs::read_dir(dir.path()).unwrap().flatten().map(|e| e.file_name()).collect();
98 assert_eq!(entries.len(), 1, "stray tmp file: {entries:?}");
100 }
101
102 #[test]
103 fn cleans_up_tmp_when_rename_fails() {
104 let dir = tempfile::TempDir::new().unwrap();
106 let path = dir.path().join("out.json");
107 std::fs::create_dir(&path).unwrap();
108 let s = Sample { name: "x".into(), value: 1 };
109 let err = atomic_write_json(&path, &s).expect_err("rename onto a dir must fail");
110 let tmp_count = std::fs::read_dir(dir.path())
112 .unwrap()
113 .flatten()
114 .filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
115 .count();
116 assert_eq!(tmp_count, 0, "leaked tmp file after rename failure: {err}");
117 }
118}