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;
    }
}