backend.rs
raw
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<String> {
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<ObjectKey> {
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<Option<Vec<u8>>, 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<bool, Error>;
/// 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<Option<u64>, 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<Option<u64>, 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
}
}