use anyhow::{Context, Result};
use std::collections::BTreeMap;
use std::fs;
use super::{
walk_files, CacheDescriptor, CacheListEntry, CacheListOutput, ObjectStore,
ProcessorCacheStats,
};
impl ObjectStore {
pub fn size(&self) -> (u64, usize) {
let mut total_bytes = 0u64;
let mut object_count = 0usize;
for dir in [&self.objects_dir, &self.descriptors_dir] {
if !dir.exists() {
continue;
}
for path in walk_files(dir) {
if let Ok(metadata) = fs::metadata(&path) {
total_bytes += metadata.len();
object_count += 1;
}
}
}
(total_bytes, object_count)
}
pub fn trim(&self) -> Result<(u64, usize)> {
let mut removed_bytes = 0u64;
let mut removed_count = 0usize;
if !self.objects_dir.exists() {
return Ok((0, 0));
}
let mut referenced: std::collections::HashSet<String> = std::collections::HashSet::new();
if self.descriptors_dir.exists() {
for path in walk_files(&self.descriptors_dir) {
let data = fs::read(&path)
.with_context(|| format!("Failed to read descriptor during trim: {}", path.display()))?;
let desc = serde_json::from_slice::<CacheDescriptor>(&data)
.with_context(|| format!("Failed to parse descriptor during trim: {} (remove it to proceed)", path.display()))?;
match desc {
CacheDescriptor::Marker => {}
CacheDescriptor::Blob { checksum, .. } => {
referenced.insert(checksum);
}
CacheDescriptor::Tree { entries } => {
for entry in entries {
referenced.insert(entry.checksum);
}
}
}
}
}
let mut to_remove = Vec::new();
for path in walk_files(&self.objects_dir) {
if let (Some(prefix), Some(rest)) = (
path.parent().and_then(|p| p.file_name()).and_then(|n| n.to_str()),
path.file_name().and_then(|n| n.to_str())
) {
let rest = rest.strip_suffix(".zst").unwrap_or(rest);
let checksum = format!("{prefix}{rest}");
if !referenced.contains(&checksum) {
if let Ok(metadata) = fs::metadata(&path) {
removed_bytes += metadata.len();
removed_count += 1;
}
to_remove.push(path);
}
}
}
for path in to_remove {
#[allow(clippy::permissions_set_readonly_false)]
if let Ok(mut perms) = fs::metadata(&path).map(|m| m.permissions()) {
perms.set_readonly(false);
fs::set_permissions(&path, perms)
.with_context(|| format!("Failed to make cache object writable: {}", path.display()))?;
}
fs::remove_file(&path)
.with_context(|| format!("Failed to remove cache object: {}", path.display()))?;
if let Some(parent) = path.parent() {
let _ = fs::remove_dir(parent);
}
}
Ok((removed_bytes, removed_count))
}
pub fn remove_stale(&self, valid_descriptor_keys: &std::collections::HashSet<String>) -> Result<usize> {
let mut count = 0;
if !self.descriptors_dir.exists() {
return Ok(0);
}
for path in walk_files(&self.descriptors_dir) {
if let (Some(prefix), Some(rest)) = (
path.parent().and_then(|p| p.file_name()).and_then(|n| n.to_str()),
path.file_name().and_then(|n| n.to_str())
) {
let key = format!("{prefix}{rest}");
if !valid_descriptor_keys.contains(&key) {
#[allow(clippy::permissions_set_readonly_false)]
if let Ok(mut perms) = fs::metadata(&path).map(|m| m.permissions()) {
perms.set_readonly(false);
fs::set_permissions(&path, perms)
.with_context(|| format!("Failed to make stale descriptor writable: {}", path.display()))?;
}
fs::remove_file(&path)
.with_context(|| format!("Failed to remove stale descriptor: {}", path.display()))?;
count += 1;
if let Some(parent) = path.parent() {
let _ = fs::remove_dir(parent);
}
}
}
}
Ok(count)
}
pub fn list(&self) -> Vec<CacheListEntry> {
if !self.descriptors_dir.exists() {
return Vec::new();
}
let mut entries: Vec<CacheListEntry> = walk_files(&self.descriptors_dir)
.into_iter()
.filter_map(|path| {
let data = fs::read(&path).ok()?;
let desc: CacheDescriptor = serde_json::from_slice(&data).ok()?;
let prefix = path.parent()?.file_name()?.to_str()?;
let rest = path.file_name()?.to_str()?;
let cache_key = format!("{prefix}{rest}");
let outputs = match desc {
CacheDescriptor::Marker => Vec::new(),
CacheDescriptor::Blob { ref checksum, .. } => {
vec![CacheListOutput { path: "(blob)".to_string(), exists: self.has_object(checksum) }]
}
CacheDescriptor::Tree { entries } => {
entries.iter().map(|e| CacheListOutput {
path: e.path.clone(),
exists: self.has_object(&e.checksum),
}).collect()
}
};
Some(CacheListEntry { cache_key, outputs })
})
.collect();
entries.sort_by(|a, b| a.cache_key.cmp(&b.cache_key));
entries
}
pub fn stats_by_processor(&self) -> BTreeMap<String, ProcessorCacheStats> {
let mut stats: BTreeMap<String, ProcessorCacheStats> = BTreeMap::new();
if !self.descriptors_dir.exists() {
return stats;
}
for path in walk_files(&self.descriptors_dir) {
let Ok(data) = fs::read(&path) else { continue };
let Ok(desc) = serde_json::from_slice::<CacheDescriptor>(&data) else { continue };
let processor = "all".to_string();
let proc_stats = stats.entry(processor).or_default();
proc_stats.entry_count += 1;
match desc {
CacheDescriptor::Marker => {}
CacheDescriptor::Blob { ref checksum, .. } => {
proc_stats.output_count += 1;
proc_stats.output_bytes += self.object_size(checksum);
}
CacheDescriptor::Tree { ref entries } => {
proc_stats.output_count += entries.len();
for entry in entries {
proc_stats.output_bytes += self.object_size(&entry.checksum);
}
}
}
}
stats
}
}