prune.rs raw

//! Removing snapshots, and with them the chunks nothing references any
//! more.
//!
//! The care this takes not to sweep away something still wanted lives in
//! [`crate::sweep`], which it shares with [`crate::excise`]. What is
//! particular to a prune is the decision: the snapshots named on the
//! command line go, and everything else stays.

use repository::{Coverage, Repository, Snapshot, SnapshotId};

pub use crate::sweep::Error;

use crate::sweep::{mark, refuse_if_locked, sweep};

/// Says so before a snapshot that a suspended backup may be relying on
/// is taken away from it.
///
/// Only a prune has this to say. An excision replaces such a snapshot
/// rather than removing it, and tells the run where its replacement is;
/// where it cannot, it refuses outright instead of going ahead under a
/// warning.
fn warn_if_partial(snapshot: &Snapshot, progress: &mut impl FnMut(&str)) {
    if snapshot.coverage != Coverage::Partial {
        return;
    }

    progress(&format!(
        "warning: {} is a partial snapshot, left by an interrupted backup of \
         {}. If that backup is still suspended, removing this leaves it \
         unable to finish, and it will have to start again.",
        snapshot.id,
        snapshot.root.display()
    ));
}

/// What a prune did, or would do.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pruned {
    pub removed: Vec<SnapshotId>,
    pub kept: usize,
    pub swept: usize,
    pub retained: usize,
}

/// Removes `remove` from the repository and sweeps whatever chunks are
/// then unreferenced.
///
/// With `dry_run`, nothing is deleted or recorded and the counts
/// describe what would have happened.
pub fn prune(
    repository: &Repository,
    remove: &[SnapshotId],
    dry_run: bool,
    break_lock: bool,
    mut progress: impl FnMut(&str),
) -> Result<Pruned, Error> {
    refuse_if_locked(repository, break_lock)?;

    let all = repository.snapshots()?;

    for id in remove {
        if !all.contains(id) {
            return Err(Error::UnknownSnapshot(*id));
        }
    }

    for id in remove {
        warn_if_partial(&repository.load_snapshot(*id)?, &mut progress);
    }

    let keeping: Vec<SnapshotId> = all
        .iter()
        .copied()
        .filter(|id| !remove.contains(id))
        .collect();

    let counts = if dry_run {
        mark(repository, &keeping, &mut progress)?.counts()
    } else {
        // The snapshot records go before anything is swept, so that an
        // interruption can only ever leave chunks nothing references —
        // never a snapshot whose chunks have gone
        for id in remove {
            repository.delete_snapshot(*id)?;
        }

        sweep(repository, &keeping, remove.to_vec(), &mut progress)?
    };

    Ok(Pruned {
        removed: remove.to_vec(),
        kept: keeping.len(),
        swept: counts.swept,
        retained: counts.retained,
    })
}