reclaim.rs
raw
use std::fmt;
use std::path::PathBuf;
use std::time::SystemTime;
use anyhow::Result;
use clap::ValueEnum;
use serde::Deserialize;
use crate::fsutil::Measurement;
use crate::policy::Policy;
use crate::progress::Reporter;
/// A class of data that `clean` can remove and something else can put back.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum Category {
/// A project's `target/` directory.
Target,
/// Sources unpacked from a `.crate` tarball, under `registry/src`.
RegistrySrc,
/// A downloaded `.crate` tarball, under `registry/cache`.
RegistryCrate,
/// A working tree checked out from a git dependency, under `git/checkouts`.
GitCheckout,
/// A bare clone of a git dependency, under `git/db`.
GitDb,
}
impl Category {
pub const ALL: [Category; 5] = [
Category::Target,
Category::RegistrySrc,
Category::RegistryCrate,
Category::GitCheckout,
Category::GitDb,
];
pub fn name(self) -> &'static str {
match self {
Category::Target => "target",
Category::RegistrySrc => "registry-src",
Category::RegistryCrate => "registry-crate",
Category::GitCheckout => "git-checkout",
Category::GitDb => "git-db",
}
}
/// What it takes to put the data back after deleting it.
pub fn regeneration(self) -> Regeneration {
match self {
Category::Target => Regeneration::Rebuild,
Category::RegistrySrc | Category::GitCheckout => Regeneration::Unpack,
Category::RegistryCrate | Category::GitDb => Regeneration::Download,
}
}
}
impl fmt::Display for Category {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
/// The cost of restoring a reclaimed item, in ascending order of regret.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Regeneration {
/// Recompiled from sources that are still on disk.
Rebuild,
/// Re-extracted from an archive that is still in the cargo home.
Unpack,
/// Re-downloaded from the network.
Download,
}
impl Regeneration {
pub fn describe(self) -> &'static str {
match self {
Regeneration::Rebuild => "rebuilt from local sources",
Regeneration::Unpack => "re-extracted from local archives",
Regeneration::Download => "re-downloaded from the network",
}
}
}
/// One unit of data that can be deleted and losslessly regenerated.
#[derive(Debug, Clone)]
pub struct Reclaimable {
pub category: Category,
pub path: PathBuf,
pub size: u64,
/// The most recent time cargo is known to have used this data.
pub last_used: SystemTime,
pub key: EntryKey,
}
/// How the owning [`Provider`] recognizes a [`Reclaimable`] when removing it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EntryKey {
/// The path is the whole identity, and deleting it is the whole job.
Path,
/// Tracked by a row in cargo's global cache database. The row has to go
/// with the files, or cargo keeps believing the data is still there.
CacheRow(CacheRow),
}
/// A row in `$CARGO_HOME/.global-cache` that tracks a cached artifact.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CacheRow {
RegistryCrate { registry_id: i64, name: String },
RegistrySrc { registry_id: i64, name: String },
GitCheckout { git_id: i64, name: String },
GitDb { id: i64 },
}
/// Data that is staying put because it is too new to delete.
#[derive(Debug, Clone)]
pub struct Retained {
pub category: Category,
pub path: PathBuf,
/// Its size and age — present only when the scan was asked to measure what
/// it keeps. A clean is not, so this is `None` there: nothing reads the
/// number, and finding it out is the most expensive thing a clean could do.
pub measured: Option<Measurement>,
}
/// What a provider found, already judged against the policy.
///
/// Split at the source rather than returned as one list with a verdict
/// attached, because the two halves are not equally knowable: everything
/// reclaimable is measured exactly, and what is retained may not be measured at
/// all. A single list would have to make every field optional to say that.
#[derive(Debug, Default)]
pub struct Findings {
/// Old enough to delete, and measured — the report says what went.
pub reclaimable: Vec<Reclaimable>,
/// Too new to touch.
pub retained: Vec<Retained>,
}
impl Findings {
pub fn absorb(&mut self, other: Findings) {
self.reclaimable.extend(other.reclaimable);
self.retained.extend(other.retained);
}
}
/// A source of reclaimable data that also knows how to remove it.
///
/// Removal lives here rather than in the clean engine because deleting some
/// categories means more than unlinking a path — cargo home entries also carry
/// tracking rows that have to be retired in the same breath.
pub trait Provider {
/// Everything this provider holds, sorted into what may go and what stays.
///
/// Judging happens here, rather than in the caller, because only the
/// provider knows what an age costs to find out. Reading a timestamp out of
/// cargo's database is free; establishing one for a directory tree is a
/// walk of the whole thing, and a provider handed the policy can decline to
/// finish that walk the moment the answer stops mattering.
///
/// The same reasoning is why the reporter comes in here: only the provider
/// knows which of its steps are slow enough to be worth narrating.
fn scan(&self, policy: &Policy, progress: &Reporter) -> Result<Findings>;
/// Delete the given items, which must have come from this provider's [`scan`],
/// and report back exactly which of them are now gone.
///
/// Returning the removed items rather than a count keeps the caller from
/// having to assume that asking for a deletion achieved one.
///
/// [`scan`]: Provider::scan
fn remove(&mut self, items: &[Reclaimable], progress: &Reporter) -> Result<Vec<Reclaimable>>;
}
/// The tally of what a [`Provider::remove`] call actually deleted.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Removed {
pub count: usize,
pub bytes: u64,
}
impl Removed {
pub fn record(&mut self, item: &Reclaimable) {
self.count += 1;
self.bytes += item.size;
}
}
impl std::ops::AddAssign for Removed {
fn add_assign(&mut self, rhs: Self) {
self.count += rhs.count;
self.bytes += rhs.bytes;
}
}