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, tmp) = create_temp_file(dir, file_name)?;
{
f.write_all(bytes)?;
f.sync_all()?;
}
match fs::rename(&tmp, path) {
Ok(()) => {
sync_parent_dir(dir);
Ok(())
}
Err(e) => {
let _ = fs::remove_file(&tmp);
Err(e)
}
}
}
fn create_temp_file(dir: &Path, file_name: &str) -> std::io::Result<(File, PathBuf)> {
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, tmp)),
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"");
}
}