pub mod local; use crate::{ChunkId, Error, LockId, PruneId, SnapshotId}; /// Names one object held by a [`Backend`]. /// /// Everything a repository stores — the public header, encrypted /// chunks, and snapshot records — is an immutable object with one of /// these names. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ObjectKey { /// The public repository header (salt, KDF and chunker parameters, /// passphrase verifier); exactly one per repository, write-once. Header, /// An encrypted chunk of content, named by its [`ChunkId`]. Chunk(ChunkId), /// An encrypted snapshot record. Snapshot(SnapshotId), /// The record one prune left behind. Their ids taken together say /// whether anything has been removed from the repository, which is /// what a local chunk cache checks itself against. Prune(PruneId), /// A running backup's lock: it holds references that no snapshot /// names yet, so pruning must not sweep them. Lock(LockId), } impl ObjectKey { /// The path of this object relative to the repository root, as /// segments. This layout is shared by every backend, so repositories /// are portable between them. /// /// Chunks are fanned out into 256 subdirectories by the first byte of /// their id, keeping directory sizes manageable on filesystem-backed /// storage. pub fn segments(&self) -> Vec { match self { ObjectKey::Header => vec!["header".to_string()], ObjectKey::Chunk(id) => { let hex = id.to_hex(); vec!["chunks".to_string(), hex[..2].to_string(), hex] } ObjectKey::Snapshot(id) => vec!["snapshots".to_string(), id.to_hex()], ObjectKey::Prune(id) => vec!["prunes".to_string(), id.to_hex()], ObjectKey::Lock(id) => vec!["locks".to_string(), id.to_hex()], } } } /// The kinds of object a repository stores, for use with [`Backend::list`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ObjectKind { Header, Chunk, Snapshot, Prune, Lock, } impl ObjectKind { /// The directory this kind's objects live in, which is also the /// prefix every backend lists them under. Chunks are excluded: they /// are fanned out into subdirectories and listed specially. pub fn directory(self) -> &'static str { match self { ObjectKind::Header => "header", ObjectKind::Chunk => "chunks", ObjectKind::Snapshot => "snapshots", ObjectKind::Prune => "prunes", ObjectKind::Lock => "locks", } } /// Rebuilds the key of an object of this kind from its name, or /// `None` if the name is not one — stray files are not objects. pub fn key_for(self, name: &str) -> Option { match self { ObjectKind::Header => Some(ObjectKey::Header), ObjectKind::Chunk => ChunkId::from_hex(name).ok().map(ObjectKey::Chunk), ObjectKind::Snapshot => SnapshotId::from_hex(name).ok().map(ObjectKey::Snapshot), ObjectKind::Prune => PruneId::from_hex(name).ok().map(ObjectKey::Prune), ObjectKind::Lock => LockId::from_hex(name).ok().map(ObjectKey::Lock), } } } /// Storage for a repository's objects. /// /// The interface is deliberately restricted to what remote object stores /// offer cheaply — write-once puts, gets, existence checks, listing, and /// deletes — with no random writes and no renames. A remote backend is the /// intended main use of this trait; local directory storage is just the /// simplest implementation of it. /// /// Implementations must be safe to call from multiple threads at once. pub trait Backend: Send + Sync { /// Stores an object. Objects are immutable and write-once: if the /// object already exists, the backend may skip the write entirely and /// must report success. A `put` must be atomic — a crash or a /// concurrent reader never observes a partially written object. fn put(&self, key: &ObjectKey, data: &[u8]) -> Result<(), Error>; /// Retrieves an object's content, or `None` if it does not exist. fn get(&self, key: &ObjectKey) -> Result>, Error>; /// True if the object exists. This is the deduplication hot path, so /// implementations should make it as cheap as they can. fn contains(&self, key: &ObjectKey) -> Result; /// Calls `visit` once for each stored object of the given kind, in no /// particular order. fn list( &self, kind: ObjectKind, visit: &mut dyn FnMut(ObjectKey) -> Result<(), Error>, ) -> Result<(), Error>; /// Removes an object. Removing an object that does not exist is not /// an error. fn delete(&self, key: &ObjectKey) -> Result<(), Error>; /// How much room the store has left, in bytes, where it can say. /// /// `None` from a store with no such notion — an object store is /// effectively unbounded — or no way to ask for it, as FTP has no /// standard command for free space. A backup's free-space floor /// simply does not apply to those. /// /// This is what is available to *this* user, in the sense /// `statvfs` means it: reserved blocks and quotas are already /// deducted. fn free_space(&self) -> Result, Error> { Ok(None) } /// How many bytes the store is holding, where it can say. /// /// `None` from a store with no way to ask — an FTP server that /// cannot list machine-readably, say. Everything under the /// repository counts, including anything a killed writer left /// behind, because that is what occupies the room. /// /// Expect this to cost a full listing: it is for a backup deciding /// once, as it starts, how much room it has to work with, not for /// anything on a hot path. fn used_space(&self) -> Result, Error> { Ok(None) } /// The directory this backend stores its objects under, when that /// directory is on the local filesystem. A backup pipeline uses /// this to avoid backing the repository up into itself — unique /// ciphertext never deduplicates, so that mistake compounds /// geometrically. fn local_root(&self) -> Option<&std::path::Path> { None } }