use std::{
fs::{self, File, OpenOptions},
io,
path::Path,
time::{Duration, SystemTime},
};
pub(super) const LEASE_FILE: &str = ".lease";
#[derive(Debug)]
pub struct ScratchLease(File);
impl ScratchLease {
pub fn acquire(path: &Path) -> io::Result<Self> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path.join(LEASE_FILE))?;
file.lock_shared()?;
Ok(Self(file))
}
}
impl Drop for ScratchLease {
fn drop(&mut self) {
let _ = self.0.unlock();
}
}
#[derive(Debug)]
pub struct ScratchDir {
_lease: ScratchLease,
directory: tempfile::TempDir,
}
impl ScratchDir {
pub fn new(root: &Path, prefix: &str) -> io::Result<Self> {
fs::create_dir_all(root)?;
let directory = tempfile::Builder::new().prefix(prefix).tempdir_in(root)?;
let lease = ScratchLease::acquire(directory.path())?;
Ok(Self {
_lease: lease,
directory,
})
}
pub fn path(&self) -> &Path {
self.directory.path()
}
}
pub fn sweep_scratch(root: &Path) {
let Ok(entries) = fs::read_dir(root) else {
return;
};
let now = SystemTime::now();
for entry in entries.flatten() {
let path = entry.path();
let Ok(metadata) = fs::symlink_metadata(&path) else {
continue;
};
if metadata.file_type().is_symlink() {
continue;
}
let expired = metadata
.modified()
.ok()
.and_then(|modified| now.duration_since(modified).ok())
.is_some_and(|age| age > Duration::from_secs(86400));
if !expired {
continue;
}
let lease_path = if metadata.is_dir() {
path.join(LEASE_FILE)
} else {
path.clone()
};
let lease = match File::open(&lease_path) {
Ok(file) => Some(file),
Err(error) if metadata.is_dir() && error.kind() == io::ErrorKind::NotFound => None,
Err(_) => continue,
};
if lease.as_ref().is_some_and(|file| file.try_lock().is_err()) {
continue;
}
if metadata.is_dir() {
let _ = fs::remove_dir_all(&path);
} else {
let _ = fs::remove_file(&path);
}
}
}