use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
pub(crate) fn write_atomic(path: &Path, contents: &[u8]) -> anyhow::Result<()> {
let parent = path.parent().filter(|p| !p.as_os_str().is_empty());
if let Some(parent) = parent {
fs::create_dir_all(parent)?;
}
let dir = parent.unwrap_or_else(|| Path::new("."));
let file_name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
let tmp = dir.join(format!(".{file_name}.rag-rat.{}.tmp", std::process::id()));
let write_result = (|| -> anyhow::Result<()> {
let mut file = File::create(&tmp)?;
file.write_all(contents)?;
file.sync_all()?;
Ok(())
})();
if let Err(e) = write_result {
let _ = fs::remove_file(&tmp);
return Err(e);
}
if let Err(e) = fs::rename(&tmp, path) {
let _ = fs::remove_file(&tmp);
return Err(e.into());
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicU64, Ordering};
use super::*;
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn temp_dir() -> std::path::PathBuf {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!("rag-rat-atomic-{}-{n}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn writes_new_file_with_exact_contents() {
let dir = temp_dir();
let path = dir.join("settings.json");
write_atomic(&path, b"hello\n").unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), "hello\n");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn overwrites_existing_file() {
let dir = temp_dir();
let path = dir.join("settings.json");
fs::write(&path, b"old contents").unwrap();
write_atomic(&path, b"new contents").unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), "new contents");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn leaves_no_temp_residue_behind() {
let dir = temp_dir();
let path = dir.join("hook");
write_atomic(&path, b"#!/bin/sh\n").unwrap();
let leftovers: Vec<_> = fs::read_dir(&dir)
.unwrap()
.filter_map(Result::ok)
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|name| name != "hook")
.collect();
assert!(leftovers.is_empty(), "unexpected temp files left behind: {leftovers:?}");
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn creates_missing_parent_directories() {
let dir = temp_dir();
let path = dir.join("nested").join(".claude").join("settings.json");
write_atomic(&path, b"{}").unwrap();
assert_eq!(fs::read_to_string(&path).unwrap(), "{}");
let _ = fs::remove_dir_all(&dir);
}
}