use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use serde::Serialize;
use crate::error::Result;
use crate::version::VersionControl;
#[derive(Debug, Clone, Serialize)]
pub struct GcReport {
pub referenced_blobs: usize,
pub orphaned_blobs: Vec<PathBuf>,
pub temp_files: Vec<PathBuf>,
pub reclaimable_bytes: u64,
pub deleted: bool,
}
pub fn gc(vault_path: &Path, dry_run: bool) -> Result<GcReport> {
let vc = VersionControl::new(vault_path)?;
let models = vc.list_models_owned();
let mut referenced: BTreeSet<String> = BTreeSet::new();
for model in &models {
for ver in vc.list_versions(model) {
referenced.insert(ver.file_path.clone());
}
}
let data_dir = vault_path.join("data");
let mut orphaned: Vec<PathBuf> = Vec::new();
let mut temp_files: Vec<PathBuf> = Vec::new();
let mut reclaimable: u64 = 0;
if data_dir.is_dir() {
for entry in fs::read_dir(&data_dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.to_string();
if file_name.ends_with(".tmp") || file_name.ends_with(".part") {
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
reclaimable += size;
temp_files.push(path);
continue;
}
if !referenced.contains(&file_name) {
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
reclaimable += size;
orphaned.push(path);
}
}
}
let deleted = if !dry_run {
for p in orphaned.iter().chain(temp_files.iter()) {
let _ = fs::remove_file(p);
}
true
} else {
false
};
Ok(GcReport {
referenced_blobs: referenced.len(),
orphaned_blobs: orphaned,
temp_files,
reclaimable_bytes: reclaimable,
deleted,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gc_empty_vault() {
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join("data")).unwrap();
let report = gc(dir.path(), true).unwrap();
assert_eq!(report.orphaned_blobs.len(), 0);
assert_eq!(report.temp_files.len(), 0);
assert!(!report.deleted);
}
#[test]
fn test_gc_finds_temp_files() {
let dir = tempfile::tempdir().unwrap();
let data = dir.path().join("data");
fs::create_dir_all(&data).unwrap();
fs::write(data.join("abc.tmp"), b"temp").unwrap();
fs::write(data.join("xyz.part"), b"partial").unwrap();
let report = gc(dir.path(), true).unwrap();
assert_eq!(report.temp_files.len(), 2);
assert_eq!(report.reclaimable_bytes, 11); assert!(!report.deleted);
}
#[test]
fn test_gc_deletes_when_not_dry() {
let dir = tempfile::tempdir().unwrap();
let data = dir.path().join("data");
fs::create_dir_all(&data).unwrap();
fs::write(data.join("orphan.vault"), b"stale").unwrap();
let report = gc(dir.path(), false).unwrap();
assert_eq!(report.orphaned_blobs.len(), 1);
assert!(report.deleted);
assert!(!data.join("orphan.vault").exists());
}
}