clean.rs
raw
use std::collections::BTreeMap;
use std::time::Duration;
use anyhow::Result;
use crate::age::MinAge;
use crate::cargo_home::{CargoHome, GlobalCache};
use crate::config::Config;
use crate::discovery::{self, Project};
use crate::policy::Policy;
use crate::progress::Reporter;
use crate::reclaim::{Category, Provider, Reclaimable, Removed, Retained};
use crate::targets::Targets;
/// Which provider a candidate came from, so a clean can hand it back.
///
/// The two are genuinely different machines — one walks the filesystem, the
/// other reads and writes cargo's database — so removal has to return to the
/// one that produced the item.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Owner {
Targets,
CargoHome,
}
impl Owner {
const ALL: [Owner; 2] = [Owner::Targets, Owner::CargoHome];
}
/// One item that may be deleted, and how long it has gone unused.
#[derive(Debug, Clone)]
pub struct Candidate {
pub item: Reclaimable,
pub age: Duration,
owner: Owner,
}
/// A count of things, and their total size when that is known at all.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Held {
pub count: usize,
/// `None` once anything in the tally went unmeasured — a clean does not
/// measure what it keeps, and a total that quietly omitted those would
/// understate itself while looking exact.
pub bytes: Option<u64>,
}
impl Default for Held {
/// An empty tally holds zero bytes, and that is a known quantity — not an
/// unknown one. Starting at `None` would mean the first thing recorded
/// found `None` already there and latched the total off for good.
fn default() -> Self {
Self {
count: 0,
bytes: Some(0),
}
}
}
impl Held {
fn record(&mut self, retained: &Retained) {
self.count += 1;
self.bytes = match (self.bytes, retained.measured) {
(Some(total), Some(measured)) => Some(total + measured.size),
// Either the total was already unknown, or this item was never
// measured. Once one is missing, the sum cannot be stated.
_ => None,
};
}
}
/// What one category adds up to, and the rule it was judged by.
#[derive(Debug, Clone, Copy)]
pub struct CategorySummary {
pub reclaimable: Removed,
pub retained: Held,
pub threshold: MinAge,
}
impl CategorySummary {
pub fn count(&self) -> usize {
self.reclaimable.count + self.retained.count
}
}
/// Everything found, judged but not yet touched.
#[derive(Debug, Default)]
pub struct Plan {
pub candidates: Vec<Candidate>,
pub retained: Vec<Retained>,
pub warnings: Vec<String>,
thresholds: BTreeMap<Category, MinAge>,
}
impl Plan {
/// Per-category totals, split by whether the age threshold lets them go.
pub fn by_category(&self) -> BTreeMap<Category, CategorySummary> {
let mut totals: BTreeMap<Category, CategorySummary> = BTreeMap::new();
let summary = |category: Category| CategorySummary {
reclaimable: Removed::default(),
retained: Held::default(),
threshold: self
.thresholds
.get(&category)
.copied()
.unwrap_or(MinAge::new(Duration::ZERO)),
};
for candidate in &self.candidates {
totals
.entry(candidate.item.category)
.or_insert_with(|| summary(candidate.item.category))
.reclaimable
.record(&candidate.item);
}
for retained in &self.retained {
totals
.entry(retained.category)
.or_insert_with(|| summary(retained.category))
.retained
.record(retained);
}
totals
}
pub fn eligible_total(&self) -> Removed {
self.candidates.iter().fold(Removed::default(), |mut t, c| {
t.record(&c.item);
t
})
}
pub fn retained_total(&self) -> Held {
self.retained.iter().fold(Held::default(), |mut t, r| {
t.record(r);
t
})
}
}
#[cfg(test)]
mod held_tests {
use super::*;
use std::path::PathBuf;
use std::time::SystemTime;
use crate::fsutil::Measurement;
fn retained(size: Option<u64>) -> Retained {
Retained {
category: Category::Target,
path: PathBuf::from("/somewhere/target"),
measured: size.map(|size| Measurement {
size,
newest: SystemTime::UNIX_EPOCH,
}),
}
}
#[test]
fn an_empty_tally_is_zero_bytes_not_unknown_bytes() {
assert_eq!(Held::default().bytes, Some(0));
}
#[test]
fn measured_items_add_up() {
let mut held = Held::default();
held.record(&retained(Some(10)));
held.record(&retained(Some(32)));
assert_eq!(held.count, 2);
assert_eq!(held.bytes, Some(42));
}
#[test]
fn one_unmeasured_item_makes_the_whole_total_unknown() {
// Reporting 10 bytes here would be worse than reporting nothing: it
// reads as exact while silently omitting whatever the other item holds.
let mut held = Held::default();
held.record(&retained(Some(10)));
held.record(&retained(None));
assert_eq!(held.count, 2);
assert_eq!(held.bytes, None);
}
#[test]
fn a_total_stays_unknown_once_it_is_unknown() {
let mut held = Held::default();
held.record(&retained(None));
held.record(&retained(Some(10)));
assert_eq!(held.count, 2);
assert_eq!(held.bytes, None);
}
}
/// What a clean actually did.
#[derive(Debug, Default)]
pub struct Report {
pub by_category: BTreeMap<Category, Removed>,
pub total: Removed,
}
/// The whole tool: the projects it manages, the cargo home, and the policy.
pub struct Grunk {
targets: Targets,
cache: GlobalCache,
cargo_home: CargoHome,
/// Warnings raised while working out what to manage.
warnings: Vec<String>,
}
impl Grunk {
pub fn new(config: &Config) -> Result<Self> {
let discovery = discovery::discover(config)?;
let cargo_home = CargoHome::open()?;
let mut warnings = discovery.warnings;
if !cargo_home.tracks_usage() {
warnings.push(format!(
"{} has no cache tracking database, so nothing in it can be aged \
or cleaned; cargo 1.76 or newer records one as it works",
cargo_home.root().display()
));
}
Ok(Self {
targets: Targets::new(discovery.projects),
cache: GlobalCache::new(cargo_home.clone()),
cargo_home,
warnings,
})
}
pub fn projects(&self) -> &[Project] {
self.targets.projects()
}
pub fn cargo_home(&self) -> &CargoHome {
&self.cargo_home
}
fn provider(&self, owner: Owner) -> &dyn Provider {
match owner {
Owner::Targets => &self.targets,
Owner::CargoHome => &self.cache,
}
}
fn provider_mut(&mut self, owner: Owner) -> &mut dyn Provider {
match owner {
Owner::Targets => &mut self.targets,
Owner::CargoHome => &mut self.cache,
}
}
/// Ask every provider what it has, under the given policy.
pub fn plan(&self, policy: &Policy, progress: &Reporter) -> Result<Plan> {
let mut plan = Plan {
warnings: self.warnings.clone(),
thresholds: policy
.categories()
.map(|category| (category, policy.min_age(category)))
.collect(),
..Plan::default()
};
for owner in Owner::ALL {
let findings = self.provider(owner).scan(policy, progress)?;
for item in findings.reclaimable {
let age = policy
.now()
.duration_since(item.last_used)
.unwrap_or(Duration::ZERO);
plan.candidates.push(Candidate { item, age, owner });
}
plan.retained.extend(findings.retained);
}
// Biggest first: the most useful thing a report can lead with is where
// the space actually went.
plan.candidates.sort_by(|a, b| {
b.item
.size
.cmp(&a.item.size)
.then(a.item.path.cmp(&b.item.path))
});
Ok(plan)
}
/// Delete everything a plan found eligible.
pub fn execute(&mut self, plan: &Plan, progress: &Reporter) -> Result<Report> {
let mut by_owner: BTreeMap<Owner, Vec<Reclaimable>> = BTreeMap::new();
for candidate in &plan.candidates {
by_owner
.entry(candidate.owner)
.or_default()
.push(candidate.item.clone());
}
// The tally comes from what each provider reports it removed, not from
// what it was asked to remove.
let mut report = Report::default();
for (owner, items) in by_owner {
for item in self.provider_mut(owner).remove(&items, progress)? {
report
.by_category
.entry(item.category)
.or_insert_with(Removed::default)
.record(&item);
report.total.record(&item);
}
}
Ok(report)
}
}