Skip to main content

zag_agent/
file_util.rs

1//! Atomic file write utilities.
2//!
3//! Provides helpers that write to a temporary file and then rename,
4//! ensuring the target file is never left in a partially-written state.
5//! Temp files use a unique name per call (PID + counter) so concurrent
6//! writers targeting the same path do not collide.
7
8use anyhow::{Context, Result};
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicU64, Ordering};
11
12/// Monotonic counter to ensure unique temp filenames within a process.
13static TMP_COUNTER: AtomicU64 = AtomicU64::new(0);
14
15/// Build a unique sibling temp path for the given target, e.g.
16/// `logs/index.json` → `logs/.index.json.12345.0.tmp`.
17fn unique_tmp_path(path: &Path) -> PathBuf {
18    let counter = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
19    let pid = std::process::id();
20    let file_name = path
21        .file_name()
22        .and_then(|n| n.to_str())
23        .unwrap_or("zag-atomic");
24    let tmp_name = format!(".{}.{}.{}.tmp", file_name, pid, counter);
25    match path.parent() {
26        Some(parent) => parent.join(tmp_name),
27        None => PathBuf::from(tmp_name),
28    }
29}
30
31/// Write `content` to `path` atomically.
32///
33/// Writes to a uniquely-named sibling temp file first, then renames.
34/// On Unix, `rename()` is atomic within the same filesystem, so the
35/// target file is either the old version or the new one — never a
36/// partial write. The unique temp name prevents concurrent writers
37/// from clobbering each other's temp files.
38pub fn atomic_write(path: &Path, content: &[u8]) -> Result<()> {
39    if let Some(parent) = path.parent() {
40        std::fs::create_dir_all(parent)
41            .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
42    }
43    let tmp_path = unique_tmp_path(path);
44    std::fs::write(&tmp_path, content)
45        .with_context(|| format!("Failed to write temp file: {}", tmp_path.display()))?;
46    std::fs::rename(&tmp_path, path).with_context(|| {
47        // Clean up the temp file on rename failure.
48        let _ = std::fs::remove_file(&tmp_path);
49        format!(
50            "Failed to rename {} -> {}",
51            tmp_path.display(),
52            path.display()
53        )
54    })?;
55    Ok(())
56}
57
58/// Convenience wrapper: atomically write a `&str` to `path`.
59pub fn atomic_write_str(path: &Path, content: &str) -> Result<()> {
60    atomic_write(path, content.as_bytes())
61}
62
63#[cfg(test)]
64#[path = "file_util_tests.rs"]
65mod tests;