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(())
}
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 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"
);
}
}