use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result};
pub fn epoch_now_u64() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub fn epoch_now_i64() -> i64 {
epoch_now_u64() as i64
}
pub(crate) fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
use std::io::Write;
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let parent = path.parent().context("path has no parent")?;
std::fs::create_dir_all(parent).with_context(|| format!("create dir {}", parent.display()))?;
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let tmp = path.with_extension(format!("tmp.{}.{}", std::process::id(), seq));
let write = || -> Result<()> {
{
let mut f = std::fs::File::create(&tmp)
.with_context(|| format!("create temp {}", tmp.display()))?;
f.write_all(bytes)?;
f.sync_all()?;
}
std::fs::rename(&tmp, path)
.with_context(|| format!("rename {} -> {}", tmp.display(), path.display()))?;
Ok(())
};
let result = write();
if result.is_err() {
let _ = std::fs::remove_file(&tmp);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn atomic_write_replaces_and_leaves_no_temp() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nested").join("f.toml");
atomic_write(&path, b"one").unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"one");
atomic_write(&path, b"two").unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"two");
let leftovers: Vec<_> = std::fs::read_dir(path.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 orphans: {leftovers:?}");
}
#[test]
fn epoch_pair_agrees() {
let u = epoch_now_u64();
let i = epoch_now_i64();
assert!(
i >= u as i64 && i - u as i64 <= 1,
"same clock, same second"
);
}
}