use std::{
path::{Path, PathBuf},
time::{Duration, SystemTime},
};
use tracing::warn;
fn fname_looks_obsolete(path: &Path) -> bool {
if let Some(extension) = path.extension() {
if extension == "toml" {
return true;
}
}
if let Some(stem) = path.file_stem() {
if stem == "default_guards" {
return true;
}
}
false
}
const CUTOFF: Duration = Duration::from_secs(4 * 24 * 60 * 60);
fn very_old(entry: &std::fs::DirEntry, now: SystemTime) -> std::io::Result<bool> {
Ok(match now.duration_since(entry.metadata()?.modified()?) {
Ok(age) => age > CUTOFF,
Err(_) => {
false
}
})
}
pub(super) fn files_to_delete(statepath: &Path, now: SystemTime) -> Vec<PathBuf> {
let mut result = Vec::new();
let dir_read_failed = |err: std::io::Error| {
use std::io::ErrorKind as EK;
match err.kind() {
EK::NotFound => {}
_ => warn!(
"Failed to scan directory {} for obsolete files: {}",
statepath.display(),
err,
),
}
};
let entries = std::fs::read_dir(statepath)
.map_err(dir_read_failed) .into_iter()
.flatten()
.map_while(|result| result.map_err(dir_read_failed).ok());
for entry in entries {
let path = entry.path();
let basename = entry.file_name();
if fname_looks_obsolete(Path::new(&basename)) {
match very_old(&entry, now) {
Ok(true) => result.push(path),
Ok(false) => {
warn!(
"Found obsolete file {}; will delete it when it is older.",
entry.path().display(),
);
}
Err(err) => {
warn!(
"Found obsolete file {} but could not access its modification time: {}",
entry.path().display(),
err,
);
}
}
}
}
result
}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn fnames() {
let examples = vec![
("guards", false),
("default_guards.json", true),
("guards.toml", true),
("marzipan.toml", true),
("marzipan.json", false),
];
for (name, obsolete) in examples {
assert_eq!(fname_looks_obsolete(Path::new(name)), obsolete);
}
}
#[test]
fn age() {
let dir = tempfile::TempDir::new().unwrap();
let fname1 = dir.path().join("quokka");
let now = SystemTime::now();
std::fs::write(fname1, "hello world").unwrap();
let mut r = std::fs::read_dir(dir.path()).unwrap();
let ent = r.next().unwrap().unwrap();
assert!(!very_old(&ent, now).unwrap());
assert!(very_old(&ent, now + CUTOFF * 2).unwrap());
}
#[test]
fn list() {
let dir = tempfile::TempDir::new().unwrap();
let now = SystemTime::now();
let fname1 = dir.path().join("quokka.toml");
std::fs::write(fname1, "hello world").unwrap();
let fname2 = dir.path().join("wombat.json");
std::fs::write(fname2, "greetings").unwrap();
let removable_now = files_to_delete(dir.path(), now);
assert!(removable_now.is_empty());
let removable_later = files_to_delete(dir.path(), now + CUTOFF * 2);
assert_eq!(removable_later.len(), 1);
assert_eq!(removable_later[0].file_stem().unwrap(), "quokka");
let removable_earlier = files_to_delete(dir.path(), now - CUTOFF * 2);
assert!(removable_earlier.is_empty());
}
#[test]
fn absent() {
let dir = tempfile::TempDir::new().unwrap();
let dir2 = dir.path().join("subdir_that_doesnt_exist");
let r = files_to_delete(&dir2, SystemTime::now());
assert!(r.is_empty());
}
}