Skip to main content

mimick/
util.rs

1//! Shared filesystem and I/O utilities.
2//!
3//! Provides atomic file writes (write-to-temp then rename) and other
4//! small helpers used across the sync engine and configuration modules.
5
6use std::fs;
7use std::io;
8use std::path::Path;
9use std::time::{SystemTime, UNIX_EPOCH};
10
11/// Atomically replace the contents of `path` with `content`.
12///
13/// Writes to a temporary sibling file first, then renames it into place.
14/// On POSIX systems `rename(2)` is atomic within the same filesystem, so
15/// readers will either see the old content or the new content -- never a
16/// partially written file.
17pub 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        // Best-effort cleanup; the tmp file may not exist yet.
30        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}