use std::{
collections::HashSet,
fs, io,
path::{Path, PathBuf},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use super::sidecar::sidecar_path_for_backup;
use crate::fs::atomic::fsync_parent_dir;
pub fn prune_backups(
target: &crate::types::safepath::SafePath,
tag: &str,
count_limit: Option<usize>,
age_limit: Option<Duration>,
) -> io::Result<crate::types::PruneResult> {
let tpath = target.as_path();
let name = tpath
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("target");
let parent = tpath.parent().unwrap_or_else(|| Path::new("."));
let prefix = format!(".{name}.{tag}.");
let mut seen = HashSet::<u128>::new();
let mut stamps: Vec<(u128, PathBuf)> = Vec::new();
let rd = fs::read_dir(parent)?;
for entry_res in rd {
let Ok(entry) = entry_res else { continue };
let fname = entry.file_name(); let Some(s) = fname.to_str() else { continue };
let Some(rest) = s.strip_prefix(&prefix) else {
continue;
};
let core_opt = rest
.strip_suffix(".bak")
.or_else(|| rest.strip_suffix(".bak.meta.json"));
let Some(core) = core_opt else { continue };
let ts_s = core.rsplit('.').next().unwrap_or("");
let Ok(ts) = ts_s.parse::<u128>() else {
continue;
};
if !seen.insert(ts) {
continue;
}
let base = parent.join(format!("{prefix}{ts}.bak"));
stamps.push((ts, base));
}
if stamps.is_empty() {
return Ok(crate::types::PruneResult {
pruned_count: 0,
retained_count: 0,
});
}
stamps.sort_unstable_by_key(|(ts, _)| std::cmp::Reverse(*ts));
let now_ms: u128 = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let age_cutoff_ms: Option<u128> = age_limit.map(|d| d.as_millis());
let desired_keep_by_count = count_limit.map_or(usize::MAX, |n| n.max(1));
let mut to_delete: Vec<PathBuf> = Vec::new();
let mut retained: usize = 0;
for (idx, (ts, base)) in stamps.iter().enumerate() {
if idx == 0 {
retained += 1;
continue;
}
let count_violation = idx >= desired_keep_by_count;
let age_violation = age_cutoff_ms.is_some_and(|cut| now_ms.saturating_sub(*ts) > cut);
if count_violation || age_violation {
to_delete.push(base.clone());
} else {
retained += 1;
}
}
let mut pruned = 0usize;
for base in to_delete {
let _ = fs::remove_file(&base);
let sc = sidecar_path_for_backup(&base);
let _ = fs::remove_file(&sc);
pruned += 1;
}
let _ = fsync_parent_dir(&tpath);
Ok(crate::types::PruneResult {
pruned_count: pruned,
retained_count: retained,
})
}