Skip to main content

ralph/fsutil/
atomic.rs

1//! Purpose: Atomic file-write helpers for local filesystem persistence.
2//!
3//! Responsibilities:
4//! - Persist file contents atomically through same-directory temp files.
5//! - Flush and sync temp files before replacement.
6//! - Best-effort sync parent directories after successful replacement.
7//!
8//! Scope:
9//! - Local atomic write orchestration only; temp cleanup policy and safeguard dumps live elsewhere.
10//!
11//! Usage:
12//! - Used by queue, config, session, undo, migration, and runtime persistence paths.
13//!
14//! Invariants/Assumptions:
15//! - Atomic writes require a parent directory.
16//! - Temp files are created in the destination directory so `persist` remains local.
17//! - Directory syncing is best-effort and Unix-only.
18//! - Persist failures must drop the temp file handle to avoid leaving temp files behind.
19
20use anyhow::{Context, Result};
21use std::fs;
22use std::io::Write;
23use std::path::Path;
24
25pub fn write_atomic(path: &Path, contents: &[u8]) -> Result<()> {
26    log::debug!("atomic write: {}", path.display());
27    let dir = path
28        .parent()
29        .context("atomic write requires a parent directory")?;
30    fs::create_dir_all(dir).with_context(|| format!("create directory {}", dir.display()))?;
31
32    let mut tmp = tempfile::NamedTempFile::new_in(dir)
33        .with_context(|| format!("create temp file in {}", dir.display()))?;
34    tmp.write_all(contents).context("write temp file")?;
35    tmp.flush().context("flush temp file")?;
36    tmp.as_file().sync_all().context("sync temp file")?;
37
38    match tmp.persist(path) {
39        Ok(_) => {}
40        Err(err) => {
41            // Explicitly drop the temp file to ensure cleanup on persist failure.
42            // PersistError contains both the error and the NamedTempFile handle;
43            // we must extract and drop the file handle to prevent temp file leaks.
44            let _temp_file = err.file;
45            drop(_temp_file);
46            return Err(err.error).with_context(|| format!("persist {}", path.display()));
47        }
48    }
49
50    sync_dir_best_effort(dir);
51    Ok(())
52}
53
54pub(crate) fn sync_dir_best_effort(dir: &Path) {
55    #[cfg(unix)]
56    {
57        log::debug!("syncing directory: {}", dir.display());
58        if let Ok(file) = fs::File::open(dir) {
59            let _ = file.sync_all();
60        }
61    }
62
63    #[cfg(not(unix))]
64    {
65        let _ = dir;
66    }
67}