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
    }
}