use std::path::{Path, PathBuf};
use super::{dir_size, parse_workbench_name, prune_claude_json, ReapStats};
pub(super) const MAX_DISK_BYTES_ENV: &str = "MOADIM_MAX_WORKBENCH_DISK_BYTES";
pub(super) fn max_disk_bytes() -> u64 {
std::env::var(MAX_DISK_BYTES_ENV)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(0)
}
pub(super) struct EvictCandidate {
pub name: String,
pub path: PathBuf,
pub size: u64,
pub finish_ts: u64,
}
pub(super) fn pick_for_eviction(
mut candidates: Vec<EvictCandidate>,
cap_bytes: u64,
total_bytes: u64,
) -> Vec<EvictCandidate> {
if cap_bytes == 0 || total_bytes <= cap_bytes {
return Vec::new();
}
candidates.sort_by_key(|candidate| candidate.finish_ts);
let mut remaining = total_bytes;
let mut chosen = Vec::new();
for candidate in candidates {
if remaining <= cap_bytes {
break;
}
remaining = remaining.saturating_sub(candidate.size);
chosen.push(candidate);
}
chosen
}
pub(super) fn enforce(
dir: &Path,
cap_bytes: u64,
is_alive: &dyn Fn(&str) -> bool,
finished_at: &dyn Fn(&Path, u64) -> u64,
) -> ReapStats {
if cap_bytes == 0 {
return ReapStats::default();
}
let Ok(entries) = std::fs::read_dir(dir) else {
return ReapStats::default();
};
let mut total_bytes = 0_u64;
let mut candidates = Vec::new();
for entry in entries.flatten() {
if !entry.file_type().is_ok_and(|ft| ft.is_dir()) {
continue;
}
let name = entry.file_name().to_string_lossy().into_owned();
let Some((_slug, ts)) = parse_workbench_name(&name) else {
continue;
};
let size = dir_size(&entry.path());
total_bytes += size;
let session = format!("moadim-{name}");
if is_alive(&session) {
continue;
}
candidates.push(EvictCandidate {
name,
path: entry.path(),
size,
finish_ts: finished_at(&entry.path(), ts),
});
}
let mut stats = ReapStats::default();
for candidate in pick_for_eviction(candidates, cap_bytes, total_bytes) {
match std::fs::remove_dir_all(&candidate.path) {
Ok(()) => {
stats.removed += 1;
stats.freed_bytes += candidate.size;
log::warn!(
"cleanup: evicted not-yet-expired workbench {:?} ({} bytes) — over the {} cap",
candidate.name,
candidate.size,
MAX_DISK_BYTES_ENV
);
prune_claude_json(&candidate.path, &candidate.name);
}
Err(err) => {
log::warn!(
"cleanup: failed to evict workbench {:?}: {err}",
candidate.name
);
}
}
}
stats
}
#[cfg(test)]
#[path = "disk_cap_tests.rs"]
mod disk_cap_tests;
#[cfg(test)]
#[path = "enforce_disk_cap_tests.rs"]
mod enforce_disk_cap_tests;