use std::path::{Path, PathBuf};
use std::time::Duration;
pub fn scratch_path(target: &Path) -> PathBuf {
let file_name = target
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "scratch".to_string());
target.with_file_name(format!("{file_name}.tmp.{}", std::process::id()))
}
pub fn sweep_stale_scratch(target: &Path, max_age: Duration) {
let Some(file_name) = target.file_name().map(|n| n.to_string_lossy().into_owned()) else {
return;
};
let prefix = format!("{file_name}.tmp.");
let Some(parent) = target.parent() else {
return;
};
let dir = if parent.as_os_str().is_empty() {
Path::new(".")
} else {
parent
};
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
if !name.to_string_lossy().starts_with(&prefix) {
continue;
}
let is_stale = entry
.metadata()
.and_then(|m| m.modified())
.ok()
.and_then(|t| t.elapsed().ok())
.is_some_and(|age| age >= max_age);
if is_stale {
let _ = std::fs::remove_file(entry.path());
}
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scratch_path_embeds_pid_and_target_name() {
let p = scratch_path(Path::new("/some/dir/context.db"));
let name = p.file_name().unwrap().to_string_lossy().to_string();
assert!(name.starts_with("context.db.tmp."));
assert!(name.ends_with(&std::process::id().to_string()));
assert_eq!(p.parent().unwrap(), Path::new("/some/dir"));
}
#[test]
fn test_sweep_removes_stale_scratch_but_not_target_or_unrelated() {
let dir = tempfile::TempDir::new().unwrap();
let target = dir.path().join("data.json");
std::fs::write(&target, "{}").unwrap();
let orphan = dir.path().join("data.json.tmp.99999");
std::fs::write(&orphan, "half-written").unwrap();
let unrelated = dir.path().join("other.json.tmp.99999");
std::fs::write(&unrelated, "x").unwrap();
sweep_stale_scratch(&target, Duration::ZERO);
assert!(!orphan.exists(), "stale scratch must be swept");
assert!(target.exists(), "target itself must survive");
assert!(unrelated.exists(), "other targets' scratch must survive");
}
#[test]
fn test_sweep_spares_fresh_scratch() {
let dir = tempfile::TempDir::new().unwrap();
let target = dir.path().join("data.json");
let fresh = dir.path().join("data.json.tmp.12345");
std::fs::write(&fresh, "live writer").unwrap();
sweep_stale_scratch(&target, Duration::from_secs(3600));
assert!(fresh.exists(), "fresh scratch (live writer) must be spared");
}
}