use std::path::Path;
use serde::Serialize;
use crate::store::{VIEWS_DIR, global_blobs_dir, read_index, wipe_blobs_in};
use crate::store_gc::{GcError, blob_stem, collect_referenced_hashes, read_dir};
pub(crate) const TELEMETRY_FILENAME: &str = "telemetry.jsonl";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheComponent {
Blobs,
Views,
Lance,
GitCache,
Telemetry,
All,
}
impl CacheComponent {
pub fn as_str(self) -> &'static str {
match self {
CacheComponent::Blobs => "blobs",
CacheComponent::Views => "views",
CacheComponent::Lance => "lance",
CacheComponent::GitCache => "git-cache",
CacheComponent::Telemetry => "telemetry",
CacheComponent::All => "all",
}
}
}
impl std::str::FromStr for CacheComponent {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"blobs" => Ok(CacheComponent::Blobs),
"views" => Ok(CacheComponent::Views),
"lance" => Ok(CacheComponent::Lance),
"git-cache" => Ok(CacheComponent::GitCache),
"telemetry" => Ok(CacheComponent::Telemetry),
"all" => Ok(CacheComponent::All),
other => Err(format!(
"unknown cache component {other:?}; expected one of \
blobs|views|lance|git-cache|telemetry|all"
)),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct CacheStats {
pub blobs_bytes: u64,
pub views_bytes: u64,
pub lance_bytes: u64,
pub git_cache_bytes: u64,
pub telemetry_bytes: u64,
pub git_history_bytes: u64,
pub total_bytes: u64,
pub other_bytes: u64,
pub blob_count: usize,
pub orphan_blob_count: usize,
pub blob_accounting_ok: bool,
pub per_view_file_count: Vec<(String, usize)>,
pub rss_bytes: Option<u64>,
pub peak_rss_bytes: Option<u64>,
}
pub fn clear_component(basemind_dir: &Path, component: CacheComponent) -> Result<(), GcError> {
clear_component_in(basemind_dir, component, &global_blobs_dir())
}
pub(crate) fn clear_component_in(
basemind_dir: &Path,
component: CacheComponent,
blobs_dir: &Path,
) -> Result<(), GcError> {
match component {
CacheComponent::Blobs => wipe_blobs_in(blobs_dir)?,
CacheComponent::Views => remove_dir_if_exists(&basemind_dir.join(VIEWS_DIR))?,
CacheComponent::Lance => clear_lance(basemind_dir)?,
CacheComponent::GitCache => remove_dir_if_exists(&basemind_dir.join(crate::git_cache::GIT_CACHE_DIR))?,
CacheComponent::Telemetry => remove_file_if_exists(&basemind_dir.join(TELEMETRY_FILENAME))?,
CacheComponent::All => remove_dir_if_exists(basemind_dir)?,
}
Ok(())
}
pub fn clear_single_view(basemind_dir: &Path, name: &str) -> Result<(), GcError> {
if name.is_empty() || name.contains('/') || name.contains('\\') || name == "." || name == ".." {
return Err(GcError::Io {
path: basemind_dir.join(VIEWS_DIR).join(name),
source: std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid view name {name:?}: must be a single path component"),
),
});
}
remove_dir_if_exists(&basemind_dir.join(VIEWS_DIR).join(name))
}
pub fn cache_stats(basemind_dir: &Path) -> Result<CacheStats, GcError> {
cache_stats_in(basemind_dir, &global_blobs_dir())
}
pub(crate) fn cache_stats_in(basemind_dir: &Path, blobs_dir: &Path) -> Result<CacheStats, GcError> {
let referenced = match collect_referenced_hashes(basemind_dir) {
Ok(set) => Some(set),
Err(e) => {
tracing::warn!(
error = %e,
"cache_stats: could not read a view index (stale schema or corrupt); \
reporting sizes only, orphan accounting skipped"
);
None
}
};
let blob_accounting_ok = referenced.is_some();
let mut blob_count = 0usize;
let mut orphan_blob_count = 0usize;
if blobs_dir.exists() {
for entry in read_dir(blobs_dir)? {
let entry = entry.map_err(|source| GcError::Io {
path: blobs_dir.to_path_buf(),
source,
})?;
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(stem) = path.file_name().and_then(|n| n.to_str()).and_then(blob_stem) else {
continue;
};
blob_count += 1;
if let Some(referenced) = &referenced
&& !referenced.contains(stem)
{
orphan_blob_count += 1;
}
}
}
let blobs_bytes = dir_size(blobs_dir)?;
let views_bytes = dir_size(&basemind_dir.join(VIEWS_DIR))?;
let lance_bytes = dir_size(&basemind_dir.join("lance"))?;
let git_cache_bytes = dir_size(&basemind_dir.join(crate::git_cache::GIT_CACHE_DIR))?;
let telemetry_bytes = file_size(&basemind_dir.join(TELEMETRY_FILENAME))?;
let git_history_bytes = dir_size(&basemind_dir.join(crate::git_history::GIT_HISTORY_DIR))?;
let total_bytes = dir_size(basemind_dir)? + blobs_bytes;
let accounted = blobs_bytes + views_bytes + lance_bytes + git_cache_bytes + telemetry_bytes + git_history_bytes;
let other_bytes = total_bytes.saturating_sub(accounted);
let rss = crate::sysres::sample();
Ok(CacheStats {
blobs_bytes,
views_bytes,
lance_bytes,
git_cache_bytes,
telemetry_bytes,
git_history_bytes,
total_bytes,
other_bytes,
blob_count,
orphan_blob_count,
blob_accounting_ok,
per_view_file_count: per_view_file_count(basemind_dir)?,
rss_bytes: rss.current_bytes,
peak_rss_bytes: rss.peak_bytes,
})
}
fn per_view_file_count(basemind_dir: &Path) -> Result<Vec<(String, usize)>, GcError> {
let mut out = Vec::new();
let views_dir = basemind_dir.join(VIEWS_DIR);
if !views_dir.exists() {
return Ok(out);
}
for entry in read_dir(&views_dir)? {
let entry = entry.map_err(|source| GcError::Io {
path: views_dir.clone(),
source,
})?;
let view_dir = entry.path();
if !view_dir.is_dir() {
continue;
}
let name = view_dir.file_name().and_then(|n| n.to_str()).unwrap_or("?").to_string();
let count = read_index(&view_dir).ok().flatten().map_or(0, |idx| idx.files.len());
out.push((name, count));
}
out.sort_by(|a, b| a.0.cmp(&b.0));
Ok(out)
}
#[cfg(feature = "intelligence")]
fn clear_lance(basemind_dir: &Path) -> Result<(), GcError> {
remove_dir_if_exists(&basemind_dir.join(crate::store::LANCE_DIR))
}
#[cfg(not(feature = "intelligence"))]
fn clear_lance(_basemind_dir: &Path) -> Result<(), GcError> {
Ok(())
}
fn remove_dir_if_exists(dir: &Path) -> Result<(), GcError> {
if dir.exists() {
std::fs::remove_dir_all(dir).map_err(|source| GcError::Io {
path: dir.to_path_buf(),
source,
})?;
}
Ok(())
}
fn remove_file_if_exists(path: &Path) -> Result<(), GcError> {
if path.exists() {
std::fs::remove_file(path).map_err(|source| GcError::Io {
path: path.to_path_buf(),
source,
})?;
}
Ok(())
}
#[cfg(unix)]
fn on_disk_size(meta: &std::fs::Metadata) -> u64 {
use std::os::unix::fs::MetadataExt;
meta.blocks().saturating_mul(512)
}
#[cfg(not(unix))]
fn on_disk_size(meta: &std::fs::Metadata) -> u64 {
meta.len()
}
fn file_size(path: &Path) -> Result<u64, GcError> {
if !path.exists() {
return Ok(0);
}
let meta = std::fs::symlink_metadata(path).map_err(|source| GcError::Io {
path: path.to_path_buf(),
source,
})?;
Ok(on_disk_size(&meta))
}
pub(crate) fn dir_size(dir: &Path) -> Result<u64, GcError> {
if !dir.exists() {
return Ok(0);
}
let mut total = std::fs::symlink_metadata(dir).map(|m| on_disk_size(&m)).unwrap_or(0);
for entry in read_dir(dir)? {
let entry = entry.map_err(|source| GcError::Io {
path: dir.to_path_buf(),
source,
})?;
let path = entry.path();
let meta = match entry.metadata() {
Ok(meta) => meta,
Err(source) if source.kind() == std::io::ErrorKind::NotFound => continue,
Err(source) => {
return Err(GcError::Io {
path: path.clone(),
source,
});
}
};
if meta.is_dir() {
total += dir_size(&path)?;
} else {
total += on_disk_size(&meta);
}
}
Ok(total)
}