use std::fs::{self, File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
pub fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let dir = path.parent().unwrap_or_else(|| Path::new("."));
fs::create_dir_all(dir)?;
let file_name = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("dbmd-tmp");
let (mut f, mut guard) = create_temp_file(dir, file_name)?;
{
f.write_all(bytes)?;
f.sync_all()?;
}
fs::rename(&guard.path, path)?;
guard.disarm();
sync_parent_dir(dir);
Ok(())
}
pub fn write_atomic_new(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let dir = path.parent().unwrap_or_else(|| Path::new("."));
fs::create_dir_all(dir)?;
let file_name = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("dbmd-tmp");
let (mut f, mut guard) = create_temp_file(dir, file_name)?;
{
f.write_all(bytes)?;
f.sync_all()?;
}
drop(f);
fs::hard_link(&guard.path, path)?;
if fs::remove_file(&guard.path).is_ok() {
guard.disarm();
}
sync_parent_dir(dir);
Ok(())
}
struct TempGuard {
path: PathBuf,
armed: bool,
}
impl TempGuard {
fn disarm(&mut self) {
self.armed = false;
}
}
impl Drop for TempGuard {
fn drop(&mut self) {
if self.armed {
let _ = fs::remove_file(&self.path);
}
}
}
fn create_temp_file(dir: &Path, file_name: &str) -> std::io::Result<(File, TempGuard)> {
static TMP_SEQ: AtomicU64 = AtomicU64::new(0);
let pid = std::process::id();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
for _ in 0..128 {
let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);
let tmp = dir.join(format!(".{file_name}.tmp.{pid}.{nanos}.{seq}"));
match OpenOptions::new().write(true).create_new(true).open(&tmp) {
Ok(file) => {
return Ok((
file,
TempGuard {
path: tmp,
armed: true,
},
))
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(e) => return Err(e),
}
}
Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"could not allocate a unique dbmd temp file",
))
}
fn sync_parent_dir(dir: &Path) {
if let Ok(d) = File::open(dir) {
let _ = d.sync_all();
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn write_atomic_creates_then_replaces_durably() {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("sub").join("file.txt");
write_atomic(&target, b"first").unwrap();
assert_eq!(std::fs::read(&target).unwrap(), b"first");
write_atomic(&target, b"second").unwrap();
assert_eq!(std::fs::read(&target).unwrap(), b"second");
let leftovers: Vec<_> = std::fs::read_dir(target.parent().unwrap())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
.collect();
assert!(leftovers.is_empty(), "no temp files may be left behind");
}
#[test]
fn write_atomic_is_byte_exact_including_empty() {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("empty.txt");
write_atomic(&target, b"").unwrap();
assert_eq!(std::fs::read(&target).unwrap(), b"");
}
#[test]
fn write_atomic_new_creates_but_refuses_existing() {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("sub").join("file.txt");
write_atomic_new(&target, b"first").unwrap();
assert_eq!(std::fs::read(&target).unwrap(), b"first");
let err = write_atomic_new(&target, b"second").unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
assert_eq!(
std::fs::read(&target).unwrap(),
b"first",
"create-new failure must leave the existing destination untouched"
);
assert_no_temp_files(target.parent().unwrap());
}
#[test]
fn write_atomic_new_allows_only_one_concurrent_creator() {
use std::sync::{Arc, Barrier};
for round in 0..40 {
let tmp = TempDir::new().unwrap();
let target = tmp.path().join("file.txt");
let barrier = Arc::new(Barrier::new(8));
let handles: Vec<_> = (0..8)
.map(|i| {
let target = target.clone();
let barrier = Arc::clone(&barrier);
std::thread::spawn(move || {
let payload = format!("payload-{i}");
barrier.wait();
let result = write_atomic_new(&target, payload.as_bytes())
.map(|_| ())
.map_err(|e| e.kind());
(payload, result)
})
})
.collect();
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
let winners: Vec<_> = results
.iter()
.filter_map(|(payload, result)| result.is_ok().then_some(payload))
.collect();
let already_exists = results
.iter()
.filter(|(_, result)| {
matches!(result, Err(kind) if *kind == std::io::ErrorKind::AlreadyExists)
})
.count();
assert_eq!(
winners.len(),
1,
"round {round}: exactly one creator may win, got {results:?}"
);
assert_eq!(
already_exists, 7,
"round {round}: every losing creator must get AlreadyExists, got {results:?}"
);
let written = std::fs::read_to_string(&target).unwrap();
assert_eq!(
written, *winners[0],
"round {round}: destination must contain the winner's payload"
);
assert_no_temp_files(tmp.path());
}
}
#[test]
fn regression_armed_guard_removes_temp_on_early_drop() {
let dir = TempDir::new().unwrap();
let (file, guard) = create_temp_file(dir.path(), "file.txt").unwrap();
let tmp_path = guard.path.clone();
assert!(
tmp_path.exists(),
"temp file should exist after create_temp_file"
);
drop(file);
drop(guard);
assert!(
!tmp_path.exists(),
"armed guard must remove the orphaned temp file on early drop"
);
let leftovers: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
.collect();
assert!(leftovers.is_empty(), "no temp files may be left behind");
}
#[test]
fn regression_disarmed_guard_leaves_file_intact() {
let dir = TempDir::new().unwrap();
let (file, mut guard) = create_temp_file(dir.path(), "kept.txt").unwrap();
drop(file);
let kept = guard.path.clone();
guard.disarm();
drop(guard);
assert!(
kept.exists(),
"disarmed guard must leave the renamed destination untouched"
);
}
fn assert_no_temp_files(dir: &Path) {
let leftovers: Vec<_> = std::fs::read_dir(dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
.collect();
assert!(leftovers.is_empty(), "no temp files may be left behind");
}
}