use std::{ collections::HashSet, io::Read, path::PathBuf, time::{SystemTime, UNIX_EPOCH}, }; use crate::{ChunkId, Error, LockId, PruneId, Repository, SnapshotId}; /// Whether a snapshot is a backup's finished word, or only what one had /// stored by the time it was interrupted. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum Coverage { /// Everything the backup that published this meant to record, as it /// read them. The run it came from is over — whether it walked the /// whole tree or deliberately stopped short of it. #[default] Complete, /// Only the objects a run had stored when it published this, while /// it was still going. Restoring one yields a subset of the tree — /// the point of it is that a backup interrupted halfway is still /// worth something — and a finished snapshot supersedes it. Partial, } impl Coverage { /// How this claim enters a snapshot's id; see /// [`Repository::snapshot_id`]. pub(crate) fn tag(self) -> &'static [u8] { match self { Coverage::Complete => b"complete", Coverage::Partial => b"partial", } } } /// A backup: metadata plus the chunk list of its manifest. /// /// The manifest itself — the stream of [`Entry`] values describing every /// backed-up filesystem object — is stored through the chunk store like /// file content, so mostly-unchanged manifests deduplicate across /// snapshots. The snapshot record is the small, encrypted root object /// that makes the manifest findable. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Snapshot { pub id: SnapshotId, /// Seconds since the Unix epoch at which the snapshot was completed. pub created: u64, /// The root directory that was backed up. #[serde(with = "crate::os_path")] pub root: PathBuf, /// The chunks holding the manifest stream, in order. pub manifest: Vec, /// Number of entries recorded in the manifest. /// /// An upper bound on the objects the snapshot covers rather than an /// exact count: a backup interrupted mid-directory records that /// directory's children again when it resumes, and reading the /// manifest yields each path once regardless. pub entries: u64, /// Total plaintext bytes of file content covered by the snapshot. pub content_bytes: u64, /// Whether the backup that produced this snapshot ran to completion. /// /// Defaulted for the sake of records written before partial /// snapshots existed, and [`Coverage::Complete`] is the right /// reading of those: back then a snapshot was only ever stored once /// the whole tree had been. #[serde(default)] pub coverage: Coverage, } impl Snapshot { /// A snapshot record for a manifest already stored via /// [`Repository::store_manifest`], stamped with the current time. /// /// Compute `id` with [`Repository::snapshot_id`] so that redoing an /// interrupted backup converges on one snapshot object instead of /// producing duplicates. pub fn new( id: SnapshotId, root: PathBuf, manifest: Vec, entries: u64, content_bytes: u64, ) -> Snapshot { Snapshot::with_coverage( id, root, manifest, entries, content_bytes, Coverage::Complete, ) } /// A snapshot record for what a backup had stored by the time it /// was written — published as a run goes and again when one stands /// down, so there is always something to restore from. Otherwise /// exactly like [`Snapshot::new`]: the manifest is real, the chunks /// it names are in the repository, and restoring it works — it /// simply does not cover the whole tree. pub fn partial( id: SnapshotId, root: PathBuf, manifest: Vec, entries: u64, content_bytes: u64, ) -> Snapshot { Snapshot::with_coverage( id, root, manifest, entries, content_bytes, Coverage::Partial, ) } /// The same backup over a narrowed manifest, as an excision leaves /// it. /// /// Everything the record says about the run that made it is kept — /// when it ran, what it covered, how completely — because none of /// that has changed. What the manifest holds has. Derive `id` with /// [`Repository::rewritten_snapshot_id`], which keeps two snapshots /// two even when an excision leaves them the same manifest. pub fn rewritten( &self, id: SnapshotId, manifest: Vec, entries: u64, content_bytes: u64, ) -> Snapshot { Snapshot { id, created: self.created, root: self.root.clone(), manifest, entries, content_bytes, coverage: self.coverage, } } fn with_coverage( id: SnapshotId, root: PathBuf, manifest: Vec, entries: u64, content_bytes: u64, coverage: Coverage, ) -> Snapshot { Snapshot { id, created: now(), root, manifest, entries, content_bytes, coverage, } } } /// What one prune left behind: enough to audit the repository's /// history, and — far more importantly — an object whose existence tells /// every local chunk cache that something has been removed. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PruneRecord { pub id: PruneId, /// Seconds since the Unix epoch at which the prune began. pub started: u64, /// The snapshots it removed. pub removed: Vec, /// Where it ran, for the benefit of whoever reads the history. pub host: String, } impl PruneRecord { pub fn new(removed: Vec, host: impl Into) -> PruneRecord { PruneRecord { id: PruneId::generate(), started: now(), removed, host: host.into(), } } } /// A running backup's claim on the repository: it holds chunks that no /// snapshot names yet, so a prune would sweep them out from under it. /// /// Taken when a backup starts and released once everything it has /// recorded is published — which is when it finishes, and also when it /// suspends gracefully, since suspending publishes a partial snapshot. /// A run that was killed leaves its lock behind, which is exactly the /// state that should stop a prune. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Lock { pub id: LockId, /// Seconds since the Unix epoch at which the lock was taken. pub taken: u64, pub host: String, pub process: u32, /// The tree being backed up, so a stale lock can be recognised. #[serde(with = "crate::os_path")] pub root: PathBuf, } impl Lock { pub fn new(root: PathBuf, host: impl Into) -> Lock { Lock { id: LockId::generate(), taken: now(), host: host.into(), process: std::process::id(), root, } } } fn now() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|elapsed| elapsed.as_secs()) .unwrap_or(0) } /// One filesystem object recorded in a snapshot manifest. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Entry { /// Path relative to the snapshot root. #[serde(with = "crate::os_path")] pub path: PathBuf, pub kind: EntryKind, /// Unix permission bits, where the platform has them. pub mode: Option, /// Modification time in nanoseconds since the Unix epoch. pub mtime: Option, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum EntryKind { File { /// The file's content as chunk store references, in order. chunks: Vec, /// Plaintext length in bytes. len: u64, }, Directory, Symlink { #[serde(with = "crate::os_path")] target: PathBuf, }, } /// Adapts an iterator of entries into the CBOR byte stream that gets /// chunked and stored as the manifest. pub(crate) struct EntryStream { entries: I, buffer: Vec, position: usize, } impl EntryStream { pub(crate) fn new(entries: I) -> EntryStream { EntryStream { entries, buffer: Vec::new(), position: 0, } } } impl Read for EntryStream where I: Iterator, { fn read(&mut self, out: &mut [u8]) -> std::io::Result { while self.position == self.buffer.len() { let Some(entry) = self.entries.next() else { return Ok(0); }; self.buffer.clear(); self.position = 0; ciborium::into_writer(&entry, &mut self.buffer).map_err(std::io::Error::other)?; } let available = &self.buffer[self.position..]; let count = available.len().min(out.len()); out[..count].copy_from_slice(&available[..count]); self.position += count; Ok(count) } } /// Presents a manifest's chunk list as one continuous byte stream, /// fetching and decrypting each chunk as the reader reaches it. struct ChunkChain<'r> { repository: &'r Repository, ids: std::vec::IntoIter, current: Vec, position: usize, } impl ChunkChain<'_> { /// Ensures at least one unread byte is buffered, reporting whether /// the stream has ended instead. This lets the manifest reader /// distinguish a clean end-of-stream from a truncated entry. fn has_data(&mut self) -> std::io::Result { while self.position == self.current.len() { let Some(id) = self.ids.next() else { return Ok(false); }; self.current = self .repository .load_chunk(id) .map_err(std::io::Error::other)?; self.position = 0; } Ok(true) } } impl Read for ChunkChain<'_> { fn read(&mut self, out: &mut [u8]) -> std::io::Result { if !self.has_data()? { return Ok(0); } let available = &self.current[self.position..]; let count = available.len().min(out.len()); out[..count].copy_from_slice(&available[..count]); self.position += count; Ok(count) } } /// Iterator over the entries of a stored manifest, created by /// [`Repository::manifest_entries`]. pub struct ManifestEntries<'r> { chain: ChunkChain<'r>, failed: bool, seen: HashSet<[u8; 16]>, } impl<'r> ManifestEntries<'r> { pub(crate) fn new(repository: &'r Repository, manifest: Vec) -> ManifestEntries<'r> { ManifestEntries { chain: ChunkChain { repository, ids: manifest.into_iter(), current: Vec::new(), position: 0, }, failed: false, seen: HashSet::new(), } } } /// Yields each recorded object once, in the order it was recorded. /// /// A manifest is an append-only log and may name the same path more than /// once: a backup that is suspended mid-directory leaves that directory /// to be scanned again on resume, and its segment is already stored by /// then. The first occurrence is the definitive one — any of them is a /// valid observation of the object as it was read, and taking the first /// consistently is what stops a path recorded as a file and later as a /// directory from being restored as both. impl Iterator for ManifestEntries<'_> { type Item = Result; fn next(&mut self) -> Option { loop { if self.failed { return None; } match self.chain.has_data() { Ok(true) => {} Ok(false) => return None, Err(err) => { self.failed = true; return Some(Err(err.into())); } } let entry: Entry = match ciborium::from_reader(&mut self.chain) { Ok(entry) => entry, Err(err) => { self.failed = true; return Some(Err(err.into())); } }; if self.seen.insert(path_digest(&entry.path)) { return Some(Ok(entry)); } } } } /// A fixed-size digest of a path for the deduplication set: 128 bits of /// SHA-256, small enough to hold millions in memory and wide enough that /// a collision (which would silently drop an entry) will not happen. fn path_digest(path: &std::path::Path) -> [u8; 16] { use sha2::Digest as _; let digest = sha2::Sha256::digest(path.as_os_str().as_encoded_bytes()); digest[..16].try_into().expect("SHA-256 yields 32 bytes") }