use super::meta::read_meta;
use std::collections::BTreeSet;
use std::path::Path;
pub struct GcConfig {
pub keep_generations: usize,
pub older_than_days: Option<u64>,
}
impl Default for GcConfig {
fn default() -> Self {
Self {
keep_generations: 5,
older_than_days: None,
}
}
}
#[derive(Debug, Clone)]
pub struct GcReport {
pub live: BTreeSet<String>,
pub dead: BTreeSet<String>,
pub total: usize,
}
pub fn collect_roots(
profile_hashes: &[String],
lockfile_hashes: &[String],
gc_roots_dir: Option<&Path>,
) -> BTreeSet<String> {
let mut roots: BTreeSet<String> = profile_hashes.iter().cloned().collect();
roots.extend(lockfile_hashes.iter().cloned());
if let Some(dir) = gc_roots_dir {
roots.extend(scan_gc_roots_dir(dir));
}
roots
}
pub fn mark_and_sweep(roots: &BTreeSet<String>, store_dir: &Path) -> Result<GcReport, String> {
let all_entries = list_store_entries(store_dir)?;
let live = mark_live(roots, store_dir);
let dead: BTreeSet<String> = all_entries.difference(&live).cloned().collect();
let total = all_entries.len();
Ok(GcReport { live, dead, total })
}
fn mark_live(roots: &BTreeSet<String>, store_dir: &Path) -> BTreeSet<String> {
let mut live = BTreeSet::new();
let mut queue: Vec<String> = roots.iter().cloned().collect();
while let Some(hash) = queue.pop() {
if !live.insert(hash.clone()) {
continue;
}
let entry_dir = store_dir.join(hash.strip_prefix("blake3:").unwrap_or(&hash));
if let Ok(meta) = read_meta(&entry_dir) {
for r in &meta.references {
if !live.contains(r) {
queue.push(r.clone());
}
}
}
}
live
}
fn scan_gc_roots_dir(dir: &Path) -> Vec<String> {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
entries
.flatten()
.filter(|e| e.path().is_symlink())
.filter_map(|e| std::fs::read_link(e.path()).ok())
.filter_map(|target| extract_store_hash(&target))
.collect()
}
fn list_store_entries(store_dir: &Path) -> Result<BTreeSet<String>, String> {
let read_dir = std::fs::read_dir(store_dir)
.map_err(|e| format!("read store dir {}: {e}", store_dir.display()))?;
let entries = read_dir
.flatten()
.filter(|e| e.path().is_dir())
.filter_map(|e| e.file_name().to_str().map(|s| s.to_string()))
.filter(|name| name != ".gc-roots")
.map(|name| format!("blake3:{name}"))
.collect();
Ok(entries)
}
fn extract_store_hash(target: &Path) -> Option<String> {
target
.to_str()?
.split('/')
.find(|c| c.len() == 64 && c.chars().all(|ch| ch.is_ascii_hexdigit()))
.map(|c| format!("blake3:{c}"))
}