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, } /// 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, /// Too new to touch. pub retained: Vec, } 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; /// 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>; } /// 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; } }