use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static TMP_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn atomic_write(path: &Path, bytes: &[u8]) -> io::Result<()> {
let target = resolve_target(path);
let tmp = tmp_path(&target);
fs::write(&tmp, bytes)?;
match fs::rename(&tmp, &target) {
Ok(()) => Ok(()),
Err(e) => {
let _ = fs::remove_file(&tmp);
Err(e)
}
}
}
#[cfg(unix)]
fn resolve_target(path: &Path) -> PathBuf {
match fs::symlink_metadata(path) {
Ok(meta) if meta.file_type().is_symlink() => match fs::canonicalize(path) {
Ok(resolved) => resolved,
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"atomic_write: failed to canonicalize symlink; replacing link with regular file"
);
path.to_path_buf()
}
},
_ => path.to_path_buf(),
}
}
#[cfg(windows)]
fn resolve_target(path: &Path) -> PathBuf {
if let Ok(meta) = fs::symlink_metadata(path) {
if meta.file_type().is_symlink() {
tracing::warn!(
path = %path.display(),
"atomic_write on Windows replaces symlink with a regular file; pointee untouched"
);
}
}
path.to_path_buf()
}
fn tmp_path(path: &Path) -> PathBuf {
let pid = std::process::id();
let nanos = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0);
let ctr = TMP_COUNTER.fetch_add(1, Ordering::Relaxed);
let mut s = path.as_os_str().to_owned();
s.push(format!(".tmp.{pid}.{nanos:x}.{ctr:x}"));
PathBuf::from(s)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn write_succeeds() {
let dir = tempdir().unwrap();
let p = dir.path().join("a.txt");
atomic_write(&p, b"hello").unwrap();
assert_eq!(fs::read(&p).unwrap(), b"hello");
}
#[test]
fn existing_file_overwritten() {
let dir = tempdir().unwrap();
let p = dir.path().join("a.txt");
fs::write(&p, b"old").unwrap();
atomic_write(&p, b"new").unwrap();
assert_eq!(fs::read(&p).unwrap(), b"new");
}
#[test]
fn temp_file_cleaned_on_success() {
let dir = tempdir().unwrap();
let p = dir.path().join("a.txt");
atomic_write(&p, b"x").unwrap();
let entries: Vec<_> = fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name())
.collect();
assert_eq!(entries, vec![std::ffi::OsString::from("a.txt")]);
}
#[test]
fn stale_temp_from_prior_crash_does_not_block_write() {
let dir = tempdir().unwrap();
let p = dir.path().join("a.txt");
let mut stale = p.as_os_str().to_owned();
stale.push(".tmp.99999.deadbeef.0");
let stale = PathBuf::from(stale);
fs::write(&stale, b"garbage").unwrap();
atomic_write(&p, b"fresh").unwrap();
assert_eq!(fs::read(&p).unwrap(), b"fresh");
assert!(stale.exists(), "stale temp from a foreign writer is left untouched");
}
#[test]
fn tmp_paths_are_unique_per_call() {
let dir = tempdir().unwrap();
let p = dir.path().join("a.txt");
let t1 = tmp_path(&p);
let t2 = tmp_path(&p);
assert_ne!(t1, t2, "consecutive tmp_path calls must differ");
}
}