use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use onelf_format::cache_layout as layout;
fn cache_root() -> io::Result<PathBuf> {
layout::resolve_root(rustix::process::getuid().as_raw(), None).ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"no safe cache directory (set HOME or XDG_CACHE_HOME to an absolute path)",
)
})
}
fn try_remove_locked(root: &Path, package_id: &str) -> bool {
let lock_path = layout::lock_path(root, package_id);
let held = match fs::File::open(&lock_path) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => {
remove_package(root, package_id);
return true;
}
Err(_) => return false,
};
if rustix::fs::flock(&held, rustix::fs::FlockOperation::NonBlockingLockExclusive).is_err() {
return false; }
remove_package(root, package_id);
true
}
fn remove_package(root: &Path, package_id: &str) {
let _ = fs::remove_file(layout::ready_marker(root, package_id));
let _ = fs::remove_dir_all(layout::pkg_dir(root, package_id));
let _ = fs::remove_file(layout::meta_path(root, package_id));
let _ = fs::remove_file(layout::extract_lock_path(root, package_id));
}
fn collect_cas(root: &Path) -> u64 {
use std::os::unix::fs::MetadataExt;
let Ok(lock) = fs::File::options()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(layout::cas_lock_path(root))
else {
return 0;
};
if rustix::fs::flock(&lock, rustix::fs::FlockOperation::NonBlockingLockExclusive).is_err() {
return 0;
}
let Ok(shards) = fs::read_dir(root.join("cas")) else {
return 0;
};
let mut reclaimed = 0u64;
for shard in shards.flatten() {
let Ok(blobs) = fs::read_dir(shard.path()) else {
continue;
};
for blob in blobs.flatten() {
let Ok(md) = blob.metadata() else { continue };
if !md.is_file() || md.nlink() > 1 {
continue;
}
let len = md.len();
if fs::remove_file(blob.path()).is_ok() {
reclaimed += len;
}
}
}
reclaimed
}
fn cas_size(root: &Path) -> u64 {
let Ok(shards) = fs::read_dir(root.join("cas")) else {
return 0;
};
shards
.flatten()
.filter_map(|shard| fs::read_dir(shard.path()).ok())
.flat_map(|blobs| blobs.flatten())
.filter_map(|b| b.metadata().ok())
.map(|m| m.len())
.sum()
}
fn mib(bytes: u64) -> f64 {
bytes as f64 / 1_048_576.0
}
pub fn cache_list() -> io::Result<()> {
let root = cache_root()?;
let pkg_dir = root.join("pkg");
if !pkg_dir.exists() {
println!("No cached packages.");
return Ok(());
}
let now = SystemTime::now();
let mut count = 0u64;
for entry in fs::read_dir(&pkg_dir)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let name = entry.file_name();
let id = name.to_string_lossy();
let last_used = fs::metadata(layout::meta_path(&root, &id))
.and_then(|m| m.modified())
.ok()
.and_then(|t| now.duration_since(t).ok())
.map(|age| format!("{}s ago", age.as_secs()))
.unwrap_or_else(|| "unknown".into());
println!(" {id} (last used: {last_used})");
count += 1;
}
println!();
println!(
"{count} packages, CAS total: {:.1} MB (run `onelf cache gc` to reclaim)",
mib(cas_size(&root))
);
Ok(())
}
pub fn cache_clear() -> io::Result<()> {
let root = cache_root()?;
if root.exists() {
fs::remove_dir_all(&root)?;
println!("Cache cleared.");
} else {
println!("No cache to clear.");
}
Ok(())
}
pub fn cache_gc(max_age_days: u64) -> io::Result<()> {
let root = cache_root()?;
let meta_dir = root.join("meta");
if !meta_dir.exists() {
println!("No cached packages.");
return Ok(());
}
let now = SystemTime::now();
let max_age = std::time::Duration::from_secs(max_age_days * 86400);
let mut removed = 0u64;
let mut skipped = 0u64;
for entry in fs::read_dir(&meta_dir)? {
let entry = entry?;
let Ok(modified) = entry.metadata().and_then(|m| m.modified()) else {
continue;
};
let Ok(age) = now.duration_since(modified) else {
continue; };
if age <= max_age {
continue;
}
let name = entry.file_name();
let id = name.to_string_lossy();
if try_remove_locked(&root, &id) {
removed += 1;
} else {
skipped += 1;
}
}
let reclaimed = if removed > 0 { collect_cas(&root) } else { 0 };
println!(
"Removed {removed} stale packages (older than {max_age_days} days), \
reclaimed {:.1} MB.",
mib(reclaimed)
);
if skipped > 0 {
println!("Skipped {skipped} package(s) still in use by a running instance.");
}
Ok(())
}