use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
static TEMP_SEQ: AtomicU64 = AtomicU64::new(0);
pub fn atomic_write(path: &Path, bytes: &[u8]) -> io::Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let seq = TEMP_SEQ.fetch_add(1, Ordering::Relaxed);
let tmp = parent.join(format!(
".tmp-{}-{}-{}",
path.file_name().and_then(|n| n.to_str()).unwrap_or("file"),
std::process::id(),
seq,
));
{
let mut f = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&tmp)?;
f.write_all(bytes)?;
f.sync_all()?;
}
if let Ok(dir) = File::open(parent) {
let _ = dir.sync_all();
}
fs::rename(&tmp, path)?;
Ok(())
}
pub async fn atomic_write_async(path: &Path, bytes: Vec<u8>) -> io::Result<()> {
let owned_path = path.to_path_buf();
tokio::task::spawn_blocking(move || atomic_write(&owned_path, &bytes))
.await
.map_err(|e| io::Error::other(format!("atomic_write_async join: {e}")))?
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_atomic_write_creates_file() {
let dir = TempDir::new().unwrap();
let p = dir.path().join("hello.txt");
atomic_write(&p, b"hello, world").unwrap();
let contents = std::fs::read(&p).unwrap();
assert_eq!(contents, b"hello, world");
}
#[test]
fn test_atomic_write_overwrites() {
let dir = TempDir::new().unwrap();
let p = dir.path().join("hello.txt");
atomic_write(&p, b"v1").unwrap();
assert_eq!(std::fs::read(&p).unwrap(), b"v1");
atomic_write(&p, b"v2 with more bytes").unwrap();
assert_eq!(std::fs::read(&p).unwrap(), b"v2 with more bytes");
}
#[test]
fn test_atomic_write_cleans_tmp_on_success() {
let dir = TempDir::new().unwrap();
let p = dir.path().join("hello.txt");
atomic_write(&p, b"content").unwrap();
for entry in std::fs::read_dir(dir.path()).unwrap() {
let entry = entry.unwrap();
let name = entry.file_name();
let s = name.to_str().unwrap();
assert!(!s.starts_with(".tmp-"), "found leftover temp file: {s}");
}
}
#[test]
fn test_atomic_write_ignores_stale_tmp_orphan() {
let dir = TempDir::new().unwrap();
let p = dir.path().join("target.txt");
atomic_write(&p, b"v1").unwrap();
assert_eq!(std::fs::read(&p).unwrap(), b"v1");
let stale = dir.path().join(format!(
".tmp-target.txt-{}-{}",
std::process::id() + 1, 999_999, ));
std::fs::write(&stale, b"half-written garbage from a crashed process").unwrap();
atomic_write(&p, b"v2").unwrap();
assert_eq!(std::fs::read(&p).unwrap(), b"v2", "target must not be torn");
assert!(
stale.exists(),
"stale orphan is ignored (not reaped) by atomic_write"
);
assert_eq!(
std::fs::read(&stale).unwrap(),
b"half-written garbage from a crashed process",
"stale orphan content must be untouched"
);
}
#[test]
fn test_atomic_write_empty_bytes() {
let dir = TempDir::new().unwrap();
let p = dir.path().join("empty.txt");
atomic_write(&p, b"").unwrap();
assert_eq!(std::fs::metadata(&p).unwrap().len(), 0);
}
#[test]
fn test_atomic_write_nested_path() {
let dir = TempDir::new().unwrap();
let p = dir.path().join("a").join("b").join("c.txt");
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
atomic_write(&p, b"nested").unwrap();
assert_eq!(std::fs::read(&p).unwrap(), b"nested");
}
#[test]
fn test_atomic_write_concurrent_writes_serialise_via_pid() {
let dir = TempDir::new().unwrap();
let p = dir.path().join("race.txt");
let dir_path = dir.path().to_path_buf();
let h1 = std::thread::spawn(move || {
atomic_write(&dir_path.join("race.txt"), b"AAAA").unwrap();
});
let h2 = std::thread::spawn(move || {
atomic_write(&p, b"BBBB").unwrap();
});
h1.join().unwrap();
h2.join().unwrap();
let contents = std::fs::read(dir.path().join("race.txt")).unwrap();
assert!(
contents == b"AAAA" || contents == b"BBBB",
"torn write: {:?}",
contents
);
}
#[tokio::test]
async fn test_atomic_write_async_roundtrip() {
let dir = TempDir::new().unwrap();
let p = dir.path().join("async.txt");
atomic_write_async(&p, b"async bytes".to_vec())
.await
.unwrap();
assert_eq!(std::fs::read(&p).unwrap(), b"async bytes");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_atomic_write_async_concurrent() {
let dir = TempDir::new().unwrap();
let mut handles = Vec::new();
for i in 0..4u8 {
let d = dir.path().to_path_buf();
handles.push(tokio::task::spawn_blocking(move || {
let p = d.join(format!("file_{i}.txt"));
atomic_write(&p, format!("payload-{i}").as_bytes()).unwrap();
}));
}
for h in handles {
h.await.unwrap();
}
for i in 0..4u8 {
let p = dir.path().join(format!("file_{i}.txt"));
assert_eq!(
std::fs::read(&p).unwrap(),
format!("payload-{i}").as_bytes()
);
}
}
}