use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result};
pub async fn blocking<T, F>(ctx: &'static str, f: F) -> Result<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static,
{
tokio::task::spawn_blocking(f).await.context(ctx)
}
pub fn unique_temp_path(dir: &Path, prefix: &str) -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
dir.join(format!("{prefix}.{}.{}", std::process::id(), seq))
}
pub struct TempPathGuard(PathBuf);
impl TempPathGuard {
pub fn new(path: PathBuf) -> Self {
Self(path)
}
pub fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempPathGuard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
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 fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
use std::io::Write;
let parent = path.parent().context("path has no parent")?;
std::fs::create_dir_all(parent).with_context(|| format!("create dir {}", parent.display()))?;
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let tmp = TempPathGuard::new(unique_temp_path(parent, &format!("{name}.tmp")));
{
let mut f = std::fs::File::create(tmp.path())
.with_context(|| format!("create temp {}", tmp.path().display()))?;
f.write_all(bytes)?;
f.sync_all()?;
}
std::fs::rename(tmp.path(), path)
.with_context(|| format!("rename {} -> {}", tmp.path().display(), path.display()))?;
Ok(())
}
#[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 unique_temp_paths_never_collide_and_the_guard_cleans_up() {
let dir = tempfile::tempdir().unwrap();
let a = unique_temp_path(dir.path(), "x.tmp");
let b = unique_temp_path(dir.path(), "x.tmp");
assert_ne!(a, b, "the process-global counter makes every call unique");
std::fs::write(&a, b"partial").unwrap();
drop(TempPathGuard::new(a.clone()));
assert!(!a.exists(), "drop removes the temp file");
drop(TempPathGuard::new(b));
}
#[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"
);
}
}