rustic_core 0.12.0

rustic_core - library for fast, encrypted, deduplicated backups that powers rustic-rs
Documentation
//! `backup` subcommand
use derive_setters::Setters;
use itertools::Itertools;
use log::info;

use std::path::PathBuf;

use path_dedot::ParseDot;
use serde_derive::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as};

use crate::{
    CommandInput, Excludes, ReadSource,
    archiver::{Archiver, parent::Parent},
    backend::{
        childstdout::ChildStdoutSource,
        dry_run::DryRunBackend,
        ignore::{LocalSource, LocalSourceFilterOptions, LocalSourceSaveOptions},
        stdin::StdinSource,
    },
    error::{ErrorKind, RusticError, RusticResult},
    repofile::{
        PathList, SnapshotFile,
        snapshotfile::{
            SnapshotId,
            grouping::{SnapshotGroup, SnapshotGroupCriterion},
        },
    },
    repository::{IndexedIds, IndexedTree, Repository},
};

#[cfg(feature = "clap")]
use clap::ValueHint;

/// `backup` subcommand
#[serde_as]
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "merge", derive(conflate::Merge))]
#[derive(Clone, Default, Debug, Deserialize, Serialize, Setters)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[setters(into)]
#[allow(clippy::struct_excessive_bools)]
#[non_exhaustive]
/// Options how the backup command uses a parent snapshot.
pub struct ParentOptions {
    /// Group snapshots by any combination of host,label,paths,tags to find a suitable parent (default: host,label,paths)
    #[cfg_attr(feature = "clap", clap(long, short = 'g', value_name = "CRITERION",))]
    #[serde_as(as = "Option<DisplayFromStr>")]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
    pub group_by: Option<SnapshotGroupCriterion>,

    /// Snapshot to use as parent (can be specified multiple times)
    #[cfg_attr(
        feature = "clap",
        clap(long = "parent", value_name = "SNAPSHOT", conflicts_with = "force")
    )]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::vec::append))]
    pub parents: Vec<String>,

    /// Skip writing of snapshot if nothing changed w.r.t. the parent snapshot.
    #[cfg_attr(feature = "clap", clap(long))]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
    pub skip_if_unchanged: bool,

    /// Use no parent, read all files
    #[cfg_attr(feature = "clap", clap(long, short))]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
    pub force: bool,

    /// Ignore ctime changes when checking for modified files
    #[cfg_attr(feature = "clap", clap(long, conflicts_with = "force"))]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
    pub ignore_ctime: bool,

    /// Ignore inode number changes when checking for modified files
    #[cfg_attr(feature = "clap", clap(long, conflicts_with = "force"))]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
    pub ignore_inode: bool,
}

impl ParentOptions {
    /// Get parent snapshot.
    ///
    /// # Type Parameters
    ///
    /// * `P` - The type of the progress bars.
    /// * `S` - The type of the indexed tree.
    ///
    /// # Arguments
    ///
    /// * `repo` - The repository to use
    /// * `snap` - The snapshot to use
    ///
    /// # Returns
    ///
    /// The parent snapshot ids and the parent object.
    pub(crate) fn get_parent<S: IndexedTree>(
        &self,
        repo: &Repository<S>,
        snap: &SnapshotFile,
    ) -> (Vec<SnapshotId>, Parent) {
        let group = SnapshotGroup::from_snapshot(snap, self.group_by.unwrap_or_default());
        let parent = if self.force {
            Vec::new()
        } else if self.parents.is_empty() {
            // get suitable snapshot group from snapshot and opts.group_by. This is used to filter snapshots for the parent detection
            SnapshotFile::latest(
                repo.dbe(),
                |snap| group.matches(snap),
                &repo.progress_counter(""),
            )
            .ok()
            .into_iter()
            .collect()
        } else {
            SnapshotFile::from_strs(
                repo.dbe(),
                &self.parents,
                |snap| group.matches(snap),
                &repo.progress_counter(""),
            )
            .unwrap_or_default()
        };

        let (parent_trees, parent_ids): (Vec<_>, _) = parent
            .into_iter()
            .map(|parent| (parent.tree, parent.id))
            .unzip();

        (
            parent_ids,
            Parent::new(
                repo.dbe(),
                repo.index(),
                parent_trees,
                self.ignore_ctime,
                self.ignore_inode,
            ),
        )
    }
}

#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "merge", derive(conflate::Merge))]
#[derive(Clone, Default, Debug, Deserialize, Serialize, Setters)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[setters(into)]
#[non_exhaustive]
/// Options for the `backup` command.
pub struct BackupOptions {
    /// Set filename to be used when backing up from stdin
    #[cfg_attr(
        feature = "clap",
        clap(long, value_name = "FILENAME", default_value = "stdin", value_hint = ValueHint::FilePath)
    )]
    #[cfg_attr(feature = "merge", merge(skip))]
    pub stdin_filename: String,

    /// Call the given command and use its output as stdin
    #[cfg_attr(feature = "clap", clap(long, value_name = "COMMAND"))]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
    pub stdin_command: Option<CommandInput>,

    /// Manually set backup path in snapshot
    #[cfg_attr(feature = "clap", clap(long, value_name = "PATH", value_hint = ValueHint::DirPath))]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
    pub as_path: Option<PathBuf>,

    /// Don't scan the backup source for its size - this disables ETA estimation for backup.
    #[cfg_attr(feature = "clap", clap(long))]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
    pub no_scan: bool,

    /// Dry-run mode: Don't write any data or snapshot
    #[cfg_attr(feature = "clap", clap(long))]
    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
    pub dry_run: bool,

    #[cfg_attr(feature = "clap", clap(flatten))]
    #[serde(flatten)]
    /// Options how to use a parent snapshot
    pub parent_opts: ParentOptions,

    #[cfg_attr(feature = "clap", clap(flatten))]
    #[serde(flatten)]
    /// Options how to save entries from a local source
    pub ignore_save_opts: LocalSourceSaveOptions,

    #[cfg_attr(feature = "clap", clap(flatten))]
    #[serde(flatten)]
    /// excludes
    pub excludes: Excludes,

    #[cfg_attr(feature = "clap", clap(flatten))]
    #[serde(flatten)]
    /// Options how to filter from a local source
    pub ignore_filter_opts: LocalSourceFilterOptions,
}

/// Backup data, create a snapshot.
///
/// # Type Parameters
///
/// * `P` - The type of the progress bars.
/// * `S` - The type of the indexed tree.
///
/// # Arguments
///
/// * `repo` - The repository to use
/// * `opts` - The backup options
/// * `src` - The source to backup
/// * `snap` - The snapshot with raw information
///
/// # Errors
///
/// * If sending the message to the raw packer fails.
/// * If converting the data length to u64 fails
/// * If sending the message to the raw packer fails.
/// * If the index file could not be serialized.
/// * If the time is not in the range of `Local::now()`
///
/// # Returns
///
/// The snapshot pointing to the backup'ed data.
pub(crate) fn archive<R, S>(
    repo: &Repository<S>,
    opts: &BackupOptions,
    src: &R,
    mut snap: SnapshotFile,
    backup_paths: &[PathBuf],
) -> RusticResult<SnapshotFile>
where
    S: IndexedIds,
    R: ReadSource + 'static,
    <R as ReadSource>::Open: Send,
    <R as ReadSource>::Iter: Send,
{
    let index = repo.index();

    let as_path = opts
        .as_path
        .as_ref()
        .map(|p| -> RusticResult<_> {
            Ok(p.parse_dot()
                .map_err(|err| {
                    RusticError::with_source(
                        ErrorKind::InvalidInput,
                        "Failed to parse dotted path `{path}`",
                        err,
                    )
                    .attach_context("path", p.display().to_string())
                })?
                .to_path_buf())
        })
        .transpose()?;

    let paths = as_path
        .as_ref()
        .map_or(backup_paths, |p| std::slice::from_ref(p));

    snap.paths.set_paths(paths).map_err(|err| {
        RusticError::with_source(
            ErrorKind::Internal,
            "Failed to set paths `{paths}` in snapshot.",
            err,
        )
        .attach_context(
            "paths",
            backup_paths
                .iter()
                .map(|p| p.display().to_string())
                .join(","),
        )
    })?;

    let (parent_ids, parent) = opts.parent_opts.get_parent(repo, &snap);
    if parent_ids.is_empty() {
        info!("using no parent");
    } else {
        info!("using parents {}", parent_ids.iter().join(", "));
        snap.parent = Some(parent_ids[0]);
        snap.parents = parent_ids;
    }

    let be = DryRunBackend::new(repo.dbe().clone(), opts.dry_run);
    info!("starting to backup {backup_paths:?} ...");
    let archiver = Archiver::new(be, index, repo.config(), parent, snap)?;
    let p = repo.progress_bytes("backing up...");

    archiver.archive(
        src,
        &backup_paths[0],
        as_path.as_ref(),
        opts.parent_opts.skip_if_unchanged,
        opts.no_scan,
        &p,
    )
}

/// Backup data, create a snapshot.
///
/// # Type Parameters
///
/// * `S` - The type of the indexed tree.
///
/// # Arguments
///
/// * `repo` - The repository to use
/// * `opts` - The backup options
/// * `source` - The source to backup
/// * `snap` - The snapshot with raw information
///
/// # Errors
///
/// * If sending the message to the raw packer fails.
/// * If converting the data length to u64 fails
/// * If sending the message to the raw packer fails.
/// * If the index file could not be serialized.
/// * If the time is not in the range of `Local::now()`
///
/// # Returns
///
/// The snapshot pointing to the backup'ed data.
pub(crate) fn backup<S: IndexedIds>(
    repo: &Repository<S>,
    opts: &BackupOptions,
    source: &PathList,
    snap: SnapshotFile,
) -> RusticResult<SnapshotFile> {
    let backup_stdin = PathList::from_string("-")?;

    let snap = if *source == backup_stdin {
        let path = PathBuf::from(&opts.stdin_filename);
        let backup_paths = vec![path.clone()];
        if let Some(command) = &opts.stdin_command {
            let src = ChildStdoutSource::new(command, path)?;
            let res = archive(repo, opts, &src, snap, &backup_paths)?;
            src.finish()?;
            res
        } else {
            let src = StdinSource::new(path);
            archive(repo, opts, &src, snap, &backup_paths)?
        }
    } else {
        let backup_path = source.paths();
        let src = LocalSource::new(
            opts.ignore_save_opts,
            &opts.excludes,
            &opts.ignore_filter_opts,
            &backup_path,
        )?;
        archive(repo, opts, &src, snap, &backup_path)?
    };

    Ok(snap)
}