Skip to main content

mermaid_runtime/
atomic.rs

1//! Atomic file writes.
2//!
3//! Plain `fs::write` truncates the target to zero length and then writes the
4//! new contents in place. A crash / kill / disk-full between those two steps
5//! leaves the file empty or half-written — catastrophic for session,
6//! checkpoint, and plugin-lockfile state that is rewritten in full on every
7//! save. [`write_atomic`] writes to a temp sibling, fsyncs it, then renames over
8//! the target, so a reader always sees either the old complete file or the new
9//! complete file.
10
11use std::fs::{self, File};
12use std::io::Write;
13use std::path::Path;
14use std::sync::atomic::{AtomicU64, Ordering};
15
16static COUNTER: AtomicU64 = AtomicU64::new(0);
17
18/// Write `bytes` to `path` atomically: temp file in the same directory →
19/// `sync_all` → rename over the destination. The rename is atomic on the same
20/// filesystem (and replaces an existing target on both Unix and Windows).
21pub fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
22    let parent = path.parent().unwrap_or_else(|| Path::new("."));
23    fs::create_dir_all(parent)?;
24
25    let stem = path.file_name().and_then(|n| n.to_str()).unwrap_or("tmp");
26    let n = COUNTER.fetch_add(1, Ordering::Relaxed);
27    let tmp = parent.join(format!(".{}.{}.{}.tmp", stem, std::process::id(), n));
28
29    {
30        let mut f = File::create(&tmp)?;
31        f.write_all(bytes)?;
32        f.sync_all()?;
33    }
34
35    if let Err(e) = fs::rename(&tmp, path) {
36        let _ = fs::remove_file(&tmp);
37        return Err(e);
38    }
39
40    // Best-effort durability of the rename itself. Opening a directory as a
41    // File is not supported on Windows, so this is a silent no-op there.
42    if let Ok(dir) = File::open(parent) {
43        let _ = dir.sync_all();
44    }
45    Ok(())
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn atomic_write_replaces_existing_and_no_temp_left() {
54        let dir = std::env::temp_dir().join(format!("mermaid_atomic_{}", std::process::id()));
55        let _ = fs::create_dir_all(&dir);
56        let target = dir.join("conv.json");
57        write_atomic(&target, b"first").unwrap();
58        assert_eq!(fs::read_to_string(&target).unwrap(), "first");
59        write_atomic(&target, b"second").unwrap();
60        assert_eq!(fs::read_to_string(&target).unwrap(), "second");
61        // No leftover temp files.
62        let leftovers = fs::read_dir(&dir)
63            .unwrap()
64            .flatten()
65            .filter(|e| e.file_name().to_string_lossy().ends_with(".tmp"))
66            .count();
67        assert_eq!(leftovers, 0);
68        let _ = fs::remove_dir_all(&dir);
69    }
70}