//! Taking paths out of the snapshots of one tree. //! //! Where a prune removes whole snapshots, this removes files and //! directories from every snapshot of a given root, rewriting each one's //! manifest without them and sweeping whatever chunks that leaves //! unreferenced. It is how a repository gives back the space taken by //! something that should never have been backed up — and, being //! irreversible, it is a separate command from prune on purpose. //! //! What makes it safe to sweep afterwards is the rule the sweep has //! always relied on, held to more strictly: every chunk that lives //! without a snapshot naming it belongs to a run that has not published //! its work, and this refuses to run against a working state in that //! condition (see [`Refusal`]). What is left is a suspended run whose //! every recorded byte is named by the partial snapshot it published, //! which this rewrites along with the rest — and whose queues it filters //! to match, so that resuming does not put back what has just been taken //! away. use std::path::{Path, PathBuf}; use globset::GlobSet; use repository::{ChunkId, Coverage, Entry, EntryKind, Repository, Snapshot, SnapshotId}; use crate::{ patterns::matches_with_ancestors, pipeline::backup::{SuspendedRun, Unsettled, WorkingState}, sweep::{refuse_if_locked, sweep}, }; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("{}", .0.explain())] Refused(Refusal), #[error(transparent)] Sweep(#[from] crate::sweep::Error), #[error(transparent)] State(#[from] crate::pipeline::Error), #[error(transparent)] Repository(#[from] repository::Error), } /// Why an excision would not go ahead. /// /// Every one of these is a state in which sweeping could take away /// something a backup still needs, or in which the snapshots do not add /// up to what a working state believes. None is worth guessing past: an /// excision cannot be undone, and each of these has a plain way out. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Refusal { /// A backup of this tree was killed rather than suspended, so it /// still holds work that no snapshot names. RunWasKilled { state_dir: PathBuf }, /// A backup of this tree finished but has not cleared its working /// state away. RunNotTidiedUp { state_dir: PathBuf }, /// The named state directory holds some other operation's state. ForeignState { state_dir: PathBuf }, /// A partial snapshot of this tree belongs to a backup whose working /// state is not here to be corrected along with it. StraySnapshot { snapshot: SnapshotId }, } impl Refusal { pub fn explain(&self) -> String { match self { Refusal::RunWasKilled { state_dir } => format!( "a backup of this tree was interrupted without standing down \ ({state_dir:?}), so it is holding content that no snapshot \ names and that this would sweep away. Run the backup again \ and let it finish or suspend, or remove that directory to \ abandon the run." ), Refusal::RunNotTidiedUp { state_dir } => format!( "a finished backup of this tree has not cleared its working \ state away ({state_dir:?}). Run the backup command once \ more, which is all it needs to tidy up." ), Refusal::ForeignState { state_dir } => { format!("{state_dir:?} holds the state of some other operation") } Refusal::StraySnapshot { snapshot } => format!( "{snapshot} is a partial snapshot, so an unfinished backup of \ this tree is relying on it, and its working state is not \ here to be corrected along with it. Point --state-dir at \ that backup's working state, finish the backup, or prune \ the snapshot to abandon it." ), } } } /// What an excision did, or would do. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Excised { /// The snapshots that held something the patterns selected. pub rewritten: Vec, /// Snapshots of the tree that held nothing selected, and so were /// left exactly as they were. pub untouched: usize, /// Queued work dropped from a suspended backup of the tree. pub unqueued: u64, pub swept: usize, pub retained: usize, } /// One snapshot's share of an excision. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Rewritten { pub from: SnapshotId, /// What replaced it; `None` for a dry run, which writes nothing. pub to: Option, /// Entries taken out of it, and the content they held. pub entries: u64, pub content_bytes: u64, } /// Removes every path the patterns select from every snapshot of /// `root`, and sweeps what that leaves behind. /// /// Patterns are matched against paths relative to the snapshot root, /// under the rules of [`crate::patterns`]: selecting a directory selects /// everything beneath it. `state_dir` is where a suspended backup of /// this tree keeps its working state, which is edited to match. /// /// With `dry_run`, nothing is written and the counts describe what would /// have happened — except the sweep, which cannot be known in advance: /// what a rewritten manifest occupies is not decided until it is stored. pub fn excise( repository: &Repository, root: &Path, select: &GlobSet, state_dir: &Path, dry_run: bool, break_lock: bool, mut progress: impl FnMut(&str), ) -> Result { refuse_if_locked(repository, break_lock)?; // The working state comes first, before even looking at what there // is to rewrite: opening it is what proves no run is alive in it, // and a tree whose backup was killed before it published anything // deserves that answer rather than "no snapshots of that tree". let mut suspended = match SuspendedRun::open(state_dir, root)? { WorkingState::Suspended(run) => Some(run), WorkingState::Missing => None, WorkingState::Unsettled(reason) => { let state_dir = state_dir.to_owned(); return Err(Error::Refused(match reason { Unsettled::Killed | Unsettled::Unpublished => Refusal::RunWasKilled { state_dir }, Unsettled::Finished => Refusal::RunNotTidiedUp { state_dir }, Unsettled::Foreign => Refusal::ForeignState { state_dir }, })); } }; let (targets, mut keeping) = snapshots_of(repository, root)?; let published: Vec = suspended .as_ref() .map(|run| run.published().to_vec()) .unwrap_or_default(); for snapshot in &targets { // A partial snapshot is an unfinished backup's own record of its // work. Rewriting one whose run cannot be told about it leaves // that run unable to resume, quietly, and that is not something // to do without being asked. if snapshot.coverage == Coverage::Partial && !published.contains(&snapshot.id) { return Err(Error::Refused(Refusal::StraySnapshot { snapshot: snapshot.id, })); } } let mut rewritten = Vec::new(); let mut untouched = 0; let mut replaced = Vec::new(); for snapshot in &targets { progress(&format!("reading the manifest of {}", snapshot.id)); let kept = rewrite(repository, snapshot, select, dry_run)?; if kept.removed == 0 { untouched += 1; keeping.push(snapshot.id); continue; } let to = match dry_run { true => None, false => Some(publish(repository, snapshot, &kept, suspended.as_mut())?), }; if let Some(id) = to { keeping.push(id); replaced.push(snapshot.id); } rewritten.push(Rewritten { from: snapshot.id, to, entries: kept.removed, content_bytes: kept.removed_bytes, }); } let unqueued = match (&suspended, dry_run) { (Some(run), true) => run.count_paths(select)?, (Some(run), false) => run.drop_paths(select)?, (None, _) => 0, }; if dry_run || replaced.is_empty() { return Ok(Excised { rewritten, untouched, unqueued, swept: 0, retained: 0, }); } let counts = sweep(repository, &keeping, replaced, &mut progress)?; // Now that the sweep has spared everything the run still needs, its // record of what the repository had lost can be brought up to date. // Without this it would put back every file it had recorded, having // no way to know that what changed underneath it was its own doing. if let Some(run) = &mut suspended { run.observed(repository.epoch()?)?; } Ok(Excised { rewritten, untouched, unqueued, swept: counts.swept, retained: counts.retained, }) } /// The snapshots of `root`, oldest first, and the ids of all the others /// — which survive untouched, and whose chunks are no less live for it. fn snapshots_of( repository: &Repository, root: &Path, ) -> Result<(Vec, Vec), Error> { let mut targets = Vec::new(); let mut others = Vec::new(); let mut roots: Vec = Vec::new(); for id in repository.snapshots()? { let snapshot = repository.load_snapshot(id)?; if snapshot.root == root { targets.push(snapshot); continue; } if !roots.contains(&snapshot.root) { roots.push(snapshot.root.clone()); } others.push(id); } if targets.is_empty() { roots.sort(); return Err(crate::sweep::Error::UnknownRoot { requested: root.to_owned(), known: roots, } .into()); } targets.sort_by_key(|snapshot| snapshot.created); Ok((targets, others)) } /// What one snapshot's manifest looks like with the selected paths gone. struct Kept { chunks: Vec, entries: u64, content_bytes: u64, removed: u64, removed_bytes: u64, } /// Reads a manifest, storing a new one without the selected paths. /// /// The entries stream straight from the old manifest into the new, so a /// manifest of any size costs its own read and no more memory than one /// entry holds. A dry run reads the same stream and stores nothing. fn rewrite( repository: &Repository, snapshot: &Snapshot, select: &GlobSet, dry_run: bool, ) -> Result { let mut kept = Kept { chunks: Vec::new(), entries: 0, content_bytes: 0, removed: 0, removed_bytes: 0, }; let mut failure = None; { let entries = repository .manifest_entries(snapshot.manifest.clone()) .map_while(|entry| match entry { Ok(entry) => Some(entry), Err(err) => { failure = Some(err); None } }) .filter(|entry| { let bytes = content_bytes(entry); if matches_with_ancestors(select, &entry.path) { kept.removed += 1; kept.removed_bytes += bytes; return false; } kept.entries += 1; kept.content_bytes += bytes; true }); match dry_run { true => entries.for_each(drop), false => kept.chunks = repository.store_manifest(entries)?, } } // A manifest that could not be read through is no basis for // replacing anything, and the chunks stored above are simply // unreferenced — the next sweep collects them. if let Some(err) = failure { return Err(err.into()); } Ok(kept) } /// Stores a snapshot's replacement and takes the original away. /// /// In that order, always: the two are separate objects, so there is no /// moment when this backup has no snapshot at all. A crash between them /// leaves the original in place with its replacement beside it, which /// the next excision resolves — it derives the same replacement again, /// finds it already stored, and goes on to the deletion. fn publish( repository: &Repository, snapshot: &Snapshot, kept: &Kept, suspended: Option<&mut SuspendedRun>, ) -> Result { let id = repository.rewritten_snapshot_id(snapshot, &kept.chunks); repository.store_snapshot(&snapshot.rewritten( id, kept.chunks.clone(), kept.entries, kept.content_bytes, ))?; // A suspended run's own record of what it published goes next, so // that the deletion below never takes away a snapshot it is still // pointing at if let Some(run) = suspended && run.published().contains(&snapshot.id) { run.republished(id, kept.chunks.clone(), kept.entries, kept.content_bytes)?; } repository.delete_snapshot(snapshot.id)?; Ok(id) } fn content_bytes(entry: &Entry) -> u64 { match entry.kind { EntryKind::File { len, .. } => len, EntryKind::Directory | EntryKind::Symlink { .. } => 0, } }