use std::fs;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_path_for(path: &Path) -> PathBuf {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let file_name = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "agentool".to_string());
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
parent.join(format!(
".{file_name}.tmp.{}.{}",
std::process::id(),
unique
))
}
pub(crate) fn write_atomic(path: &Path, content: &[u8]) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let tmp = temp_path_for(path);
let write_result = (|| {
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp)?;
file.write_all(content)?;
file.sync_all()?;
drop(file);
fs::rename(&tmp, path)
})();
if write_result.is_err() {
let _ = fs::remove_file(&tmp);
}
write_result
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use super::write_atomic;
fn tmp_dir() -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"agentool_atomic_test_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
fs::create_dir_all(&dir).expect("create tmp");
dir
}
#[test]
fn write_atomic_creates_parent_directories_and_replaces_content() {
let dir = tmp_dir();
let path = dir.join("nested").join("data.txt");
write_atomic(&path, b"first").expect("initial write");
write_atomic(&path, b"second").expect("replacement write");
assert_eq!(fs::read_to_string(&path).unwrap(), "second");
let leftovers: Vec<_> = fs::read_dir(path.parent().unwrap())
.unwrap()
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(leftovers, vec!["data.txt"]);
let _ = fs::remove_dir_all(&dir);
}
}