diff --git a/.gitignore b/.gitignore index d8a1c86a0..67adf7187 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,8 @@ mutants.out cargo-test* coverage/*lcov .testscompletions-* + +# Ignore generated test files +/tests/generated/*.toml +/tests/generated/*.log +/tests/generated/test-restore diff --git a/Cargo.lock b/Cargo.lock index 726e22dea..0959f5d21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,6 +427,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -481,6 +487,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -975,7 +987,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ - "bitflags", + "bitflags 2.6.0", "crossterm_winapi", "libc", "parking_lot", @@ -988,7 +1000,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags", + "bitflags 2.6.0", "crossterm_winapi", "mio", "parking_lot", @@ -2188,6 +2200,20 @@ dependencies = [ "generic-array", ] +[[package]] +name = "insta" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6593a41c7a73841868772495db7dc1e8ecab43bb5c0b6da2059246c4b506ab60" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "ron", + "serde", + "similar", +] + [[package]] name = "instability" version = "0.3.2" @@ -2395,11 +2421,17 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "libc", "redox_syscall", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -2563,7 +2595,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cfg-if 1.0.0", "cfg_aliases", "libc", @@ -2841,7 +2873,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf38532d784978966f95d241226223823f351d5bb2a4bebcf6b20b9cb1e393e0" dependencies = [ - "bitflags", + "bitflags 2.6.0", "num-derive", "num-traits", "openssh-sftp-protocol-error", @@ -3387,7 +3419,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdef7f9be5c0122f890d58bdf4d964349ba6a6161f705907526d891efabba57d" dependencies = [ - "bitflags", + "bitflags 2.6.0", "cassowary", "compact_str", "crossterm 0.28.1", @@ -3408,7 +3440,7 @@ version = "11.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -3437,7 +3469,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -3585,7 +3617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61797318be89b1a268a018a92a7657096d83f3ecb31418b9e9c16dcbb043b702" dependencies = [ "ahash", - "bitflags", + "bitflags 2.6.0", "instant", "num-traits", "once_cell", @@ -3622,6 +3654,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + [[package]] name = "rsa" version = "0.9.6" @@ -3743,6 +3786,7 @@ dependencies = [ "human-panic", "humantime", "indicatif", + "insta", "itertools", "jemallocator-global", "libc", @@ -3888,7 +3932,7 @@ version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -4067,7 +4111,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -4320,6 +4364,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "simple_asn1" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index 71bf8427b..e0765ee39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,6 +106,7 @@ toml = "0.8" abscissa_core = { version = "0.7.0", default-features = false, features = ["testing"] } assert_cmd = "2.0.16" dircmp = "0.2" +insta = { version = "1.40.0", features = ["ron"] } predicates = "3.1.2" pretty_assertions = "1.4" quickcheck = "1" diff --git a/config/README.md b/config/README.md index 0063cecf4..08920c6b8 100644 --- a/config/README.md +++ b/config/README.md @@ -14,6 +14,27 @@ This specification covers all the available sections and attributes in the variable names. Users can customize their backup behavior by modifying these attributes according to their needs. +## Table of Contents + +- [Merge Precedence](#merge-precedence) +- [Profiles](#profiles) +- [Sections and Attributes](#sections-and-attributes) + - [Global Options `[global]`](#global-options-global) + - [Global Hooks `[global.hooks]`](#global-hooks-globalhooks) + - [Global Options - env variables `[global.env]`](#global-options---env-variables-globalenv) + - [Repository Options `[repository]`](#repository-options-repository) + - [Repository Options (Additional) `[repository.options]`](#repository-options-additional-repositoryoptions) + - [Repository Options for cold repo (Additional) `[repository.options-cold]`](#repository-options-for-cold-repo-additional-repositoryoptions-cold) + - [Repository Options for hot repo (Additional) `[repository.options-hot]`](#repository-options-for-hot-repo-additional-repositoryoptions-hot) + - [Repository Hooks `[repository.hooks]`](#repository-hooks-repositoryhooks) + - [Snapshot-Filter Options `[snapshot-filter]`](#snapshot-filter-options-snapshot-filter) + - [Backup Options `[backup]`](#backup-options-backup) + - [Backup Hooks `[backup.hooks]`](#backup-hooks-backuphooks) + - [Backup Snapshots `[[backup.snapshots]]`](#backup-snapshots-backupsnapshots) + - [Forget Options `[forget]`](#forget-options-forget) + - [Copy Targets `[copy]`](#copy-targets-copy) + - [WebDAV Options `[webdav]`](#webdav-options-webdav) + ## Merge Precedence The merge precedence for values is: @@ -64,6 +85,20 @@ If you want to contribute your own configuration, please | progress-interval | The interval at which progress indicators are shown. | "100ms" | "1m" | RUSTIC_PROGRESS_INTERVAL | --progress-interval | | use-profiles | Array of profiles to use. Allows to recursively use other profiles. | Empty array | ["2nd", "3rd"] | RUSTIC_USE_PROFILE | --use-profile, -P | +### Global Hooks `[global.hooks]` + +These external commands are run before and after each commands, respectively. + +**Note**: There are also repository hooks, which should be used for commands +needed to set up the repository (like mounting the repo dir), see below. + +| Attribute | Description | Default Value | Example Value | Environment Variable | +| ----------- | ------------------------------------------------- | ------------- | ------------- | -------------------- | +| run-before | Run the given commands before execution | not set | ["echo test"] | | +| run-after | Run the given commands after successful execution | not set | ["echo test"] | | +| run-failed | Run the given commands after failed execution | not set | ["echo test"] | | +| run-finally | Run the given commands after every execution | not set | ["echo test"] | | + ### Global Options - env variables `[global.env]` All given environment variables are set before processing. This is handy to @@ -115,6 +150,13 @@ upper snake case and prefix with "RUSTIC_REPO_OPTHOT_". see Repository Options +### Repository Hooks `[repository.hooks]` + +These external commands are run before and after each repository-accessing +commands, respectively. + +See [Global Hooks](#global-hooks-globalhooks). + ### Snapshot-Filter Options `[snapshot-filter]` | Attribute | Description | Default Value | Example Value | CLI Option | @@ -173,14 +215,26 @@ can be overwritten in the source-specific configuration, see below. | time | Set the time saved in the snapshot. | current time | | --time | | with-atime | If true, includes file access time (atime) in the backup. | false | | --with-atime | +### Backup Hooks `[backup.hooks]` + +These external commands are run before and after each backup, respectively. + +**Note**: Global hooks and repository hooks are run additionaly. + +See [Global Hooks](#global-hooks-globalhooks). + ### Backup Snapshots `[[backup.snapshots]]` **Note**: All of the backup options mentioned before can also be used as -source-specific option and then only apply to this source. +snapshot-specific option and then only apply to this snapshot. + +| Attribute | Description | Default Value | Example Value | +| --------- | ------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------- | +| sources | Array of source directories or file(s) to back up. | [] | ["/dir1", "/dir2"] | +| hooks | Hooks to run before and after backing up the defined sources. | Not set | { run-before = [], run-after = [], run-failed = [], run-finally = [] } | -| Attribute | Description | Default Value | Example Value | -| --------- | -------------------------------------------------- | ------------- | ------------------ | -| sources | Array of source directories or file(s) to back up. | [] | ["/dir1", "/dir2"] | +Source-specific hooks are called additionally to global, repository and backup +hooks when backing up the defined sources into a snapshot. ### Forget Options `[forget]` diff --git a/config/full.toml b/config/full.toml index d953b689a..eac32be41 100644 --- a/config/full.toml +++ b/config/full.toml @@ -16,6 +16,16 @@ progress-interval = "100ms" dry-run = false check-index = false +# Global hooks: The given commands are called for every command +[global.hooks] +run-before = [ + # long form giving command and args explicitely and allow to specify failure behavior + { command = "echo", args = ["before"], on-failure = "warn" }, # allowed values for on-failure: "error" (default), "warn", "ignore" +] # Default: [] +run-after = ["echo after"] # Run after if successful, short version, default: [] +run-failed = ["echo failed"] # Default: [] +run-finally = ["echo finally"] # Always run after, default: [] + # Global env variables: These are set by rustic before calling a subcommand, e.g. rclone or commands # defined in the repository options. [global.env] @@ -65,6 +75,13 @@ throttle = "10kB,10MB" # limit and burst per second; only opendal backends; Defa [repository.options-cold] # see [repository.options] +# Repository hooks: The given commands are called for commands accessing the repository. +[repository.hooks] +run-before = ["echo before"] # Default: [] +run-after = ["echo after"] # Run after if successful, default: [] +run-failed = ["echo failed"] # Default: [] +run-finally = ["echo finally"] # Always run after, default: [] + # Snapshot-filter options: These options apply to all commands that use snapshot filters [snapshot-filter] filter-hosts = ["host1", "host2"] # Default: [] @@ -114,12 +131,26 @@ no-scan = false quiet = false skip-identical-parent = false +# Backup hooks: The given commands are called for the `backup` command +[backup.hooks] +run-before = ["echo before"] # Default: [] +run-after = ["echo after"] # Run after if successful, default: [] +run-failed = ["echo failed"] # Default: [] +run-finally = ["echo finally"] # Always run after, default: [] + # Backup options for specific sources - all above options are also available here and replace them for the given source [[backup.snapshots]] sources = ["/path/to/source1"] label = "label" # Default: not set # .. and so on. see [backup] +# Source-specific hooks: The given commands when backing up the defined source +[backup.snapshots.hooks] +run-before = ["echo before"] # Default: [] +run-after = ["echo after"] # Run after if successful, default: [] +run-failed = ["echo failed"] # Default: [] +run-finally = ["echo finally"] # Always run after, default: [] + [[backup.snapshots]] sources = [ "/path/to/source2", diff --git a/config/hooks.toml b/config/hooks.toml new file mode 100644 index 000000000..b1cecd943 --- /dev/null +++ b/config/hooks.toml @@ -0,0 +1,35 @@ +# Hooks configuration +# +# Hooks are commands that are run during certain events in the application lifecycle. +# They can be used to run custom scripts or commands before or after certain actions. +# The hooks are run in the order they are defined in the configuration file. +# The hooks are divided into 4 categories: global, repository, backup, +# and specific backup sources. +# +# You can also read a more detailed explanation of the hooks in the documentation: +# https://rustic.cli.rs/docs/commands/misc/hooks.html +# +# Please make sure to check the in-repository documentation for the config files +# available at: https://github.com/rustic-rs/rustic/blob/main/config/README.md +# +[global.hooks] +run-before = [] +run-after = [] +run-failed = [] +run-finally = [] + +[repository.hooks] +run-before = [] +run-after = [] +run-failed = [] +run-finally = [] + +[backup.hooks] +run-before = [] +run-after = [] +run-failed = [] +run-finally = [] + +[[backup.snapshots]] +sources = [] +hooks = { run-before = [], run-after = [], run-failed = [], run-finally = [] } diff --git a/src/application.rs b/src/application.rs index ac69ae61b..40c117dd6 100644 --- a/src/application.rs +++ b/src/application.rs @@ -5,7 +5,7 @@ use abscissa_core::{ application::{self, fatal_error, AppCell}, config::{self, CfgCell}, terminal::component::Terminal, - Application, Component, FrameworkError, Shutdown, StandardPaths, + Application, Component, FrameworkError, FrameworkErrorKind, Shutdown, StandardPaths, }; use anyhow::Result; @@ -103,8 +103,13 @@ impl Application for RusticApp { env::set_var(env, value); } + let global_hooks = config.global.hooks.clone(); self.config.set_once(config); + global_hooks.run_before().map_err(|err| -> FrameworkError { + FrameworkErrorKind::ProcessError.context(err).into() + })?; + Ok(()) } @@ -121,6 +126,12 @@ impl Application for RusticApp { impl RusticApp { /// Shut down this application gracefully, exiting with given exit code. fn shutdown_with_exitcode(&self, shutdown: Shutdown, exit_code: i32) -> ! { + let hooks = &RUSTIC_APP.config().global.hooks; + match shutdown { + Shutdown::Crash => _ = hooks.run_failed(), + _ => _ = hooks.run_after(), + }; + _ = hooks.run_finally(); let result = self.state().components().shutdown(self, shutdown); if let Err(e) = result { fatal_error(self, &e) diff --git a/src/commands.rs b/src/commands.rs index ee1ade4ef..7ef95a7f8 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -45,32 +45,26 @@ use crate::{ self_update::SelfUpdateCmd, show_config::ShowConfigCmd, snapshots::SnapshotCmd, tag::TagCmd, }, - config::{progress_options::ProgressOptions, AllRepositoryOptions, RusticConfig}, - {Application, RUSTIC_APP}, + config::RusticConfig, + Application, RUSTIC_APP, }; use abscissa_core::{ config::Override, terminal::ColorChoice, Command, Configurable, FrameworkError, FrameworkErrorKind, Runnable, Shutdown, }; -use anyhow::{anyhow, Result}; +use anyhow::Result; use clap::builder::{ styling::{AnsiColor, Effects}, Styles, }; use convert_case::{Case, Casing}; -use dialoguer::Password; use human_panic::setup_panic; -use log::{log, warn, Level}; -use rustic_core::{IndexedFull, OpenStatus, ProgressBars, Repository}; +use log::{log, Level}; use simplelog::{CombinedLogger, LevelFilter, TermLogger, TerminalMode, WriteLogger}; use self::find::FindCmd; -pub(super) mod constants { - pub(super) const MAX_PASSWORD_RETRIES: usize = 5; -} - /// Rustic Subcommands /// Subcommands need to be listed in an enum. #[derive(clap::Parser, Command, Debug, Runnable)] @@ -290,108 +284,6 @@ impl Configurable for EntryPoint { } } } -/// Get the repository with the given options -/// -/// # Arguments -/// -/// * `repo_opts` - The repository options -/// -fn get_repository_with_progress

( - repo_opts: &AllRepositoryOptions, - po: P, -) -> Result> { - let backends = repo_opts.be.to_backends()?; - let repo = Repository::new_with_progress(&repo_opts.repo, &backends, po)?; - Ok(repo) -} - -/// Get the repository with the given options -/// -/// # Arguments -/// -/// * `repo_opts` - The repository options -/// -fn get_repository(repo_opts: &AllRepositoryOptions) -> Result> { - let po = RUSTIC_APP.config().global.progress_options; - get_repository_with_progress(repo_opts, po) -} - -/// Open the repository with the given options -/// -/// # Arguments -/// -/// * `repo_opts` - The repository options -/// -/// # Errors -/// -/// * [`RepositoryErrorKind::ReadingPasswordFromReaderFailed`] - If reading the password failed -/// * [`RepositoryErrorKind::OpeningPasswordFileFailed`] - If opening the password file failed -/// * [`RepositoryErrorKind::PasswordCommandExecutionFailed`] - If executing the password command failed -/// * [`RepositoryErrorKind::ReadingPasswordFromCommandFailed`] - If reading the password from the command failed -/// * [`RepositoryErrorKind::FromSplitError`] - If splitting the password command failed -/// -/// [`RepositoryErrorKind::ReadingPasswordFromReaderFailed`]: crate::error::RepositoryErrorKind::ReadingPasswordFromReaderFailed -/// [`RepositoryErrorKind::OpeningPasswordFileFailed`]: crate::error::RepositoryErrorKind::OpeningPasswordFileFailed -/// [`RepositoryErrorKind::PasswordCommandExecutionFailed`]: crate::error::RepositoryErrorKind::PasswordCommandExecutionFailed -/// [`RepositoryErrorKind::ReadingPasswordFromCommandFailed`]: crate::error::RepositoryErrorKind::ReadingPasswordFromCommandFailed -/// [`RepositoryErrorKind::FromSplitError`]: crate::error::RepositoryErrorKind::FromSplitError -fn open_repository_with_progress( - repo_opts: &AllRepositoryOptions, - po: P, -) -> Result> { - if RUSTIC_APP.config().global.check_index { - warn!("Option check-index is not supported and will be ignored!"); - } - let repo = get_repository_with_progress(repo_opts, po)?; - match repo.password()? { - // if password is given, directly return the result of find_key_in_backend and don't retry - Some(pass) => { - return Ok(repo.open_with_password(&pass)?); - } - None => { - for _ in 0..constants::MAX_PASSWORD_RETRIES { - let pass = Password::new() - .with_prompt("enter repository password") - .allow_empty_password(true) - .interact()?; - match repo.clone().open_with_password(&pass) { - Ok(repo) => return Ok(repo), - Err(err) if err.is_incorrect_password() => continue, - Err(err) => return Err(err.into()), - } - } - } - } - Err(anyhow!("incorrect password")) -} - -fn open_repository( - repo_opts: &AllRepositoryOptions, -) -> Result> { - let po = RUSTIC_APP.config().global.progress_options; - open_repository_with_progress(repo_opts, po) -} -/// helper function to get an opened and inedexed repo -fn open_repository_indexed_with_progress( - repo_opts: &AllRepositoryOptions, - po: P, -) -> Result> { - let open = open_repository_with_progress(repo_opts, po)?; - let check_index = RUSTIC_APP.config().global.check_index; - let repo = if check_index { - open.to_indexed_checked() - } else { - open.to_indexed() - }?; - Ok(repo) -} - -fn open_repository_indexed( - repo_opts: &AllRepositoryOptions, -) -> Result> { - let po = RUSTIC_APP.config().global.progress_options; - open_repository_indexed_with_progress(repo_opts, po) -} #[cfg(test)] mod tests { diff --git a/src/commands/backup.rs b/src/commands/backup.rs index 8c3e56a95..31dca3224 100644 --- a/src/commands/backup.rs +++ b/src/commands/backup.rs @@ -3,34 +3,36 @@ use std::path::PathBuf; use crate::{ - commands::{get_repository, init::init, open_repository, snapshots::fill_table}, + commands::{init::init, snapshots::fill_table}, + config::hooks::Hooks, helpers::{bold_cell, bytes_size_to_string, table}, + repository::CliRepo, status_err, Application, RUSTIC_APP, }; use abscissa_core::{Command, Runnable, Shutdown}; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use clap::ValueHint; use comfy_table::Cell; use conflate::Merge; -use log::{debug, info, warn}; +use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use rustic_core::{ - BackupOptions, CommandInput, ConfigOptions, KeyOptions, LocalSourceFilterOptions, - LocalSourceSaveOptions, ParentOptions, PathList, SnapshotOptions, + BackupOptions, CommandInput, ConfigOptions, IndexedIds, KeyOptions, LocalSourceFilterOptions, + LocalSourceSaveOptions, ParentOptions, PathList, ProgressBars, Repository, SnapshotOptions, }; /// `backup` subcommand #[serde_as] #[derive(Clone, Command, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] -// Note: using cli_sources, sources and source within this struct is a hack to support serde(deny_unknown_fields) +// Note: using cli_sources, sources and snapshots within this struct is a hack to support serde(deny_unknown_fields) // for deserializing the backup options from TOML // Unfortunately we cannot work with nested flattened structures, see // https://github.com/serde-rs/serde/issues/1547 -// A drawback is that a wrongly set "source(s) = ..." won't get correct error handling and need to be manually checked, see below. +// A drawback is that a wrongly set "snapshots = ..." won't get correct error handling and need to be manually checked, see below. #[allow(clippy::struct_excessive_bools)] pub struct BackupCmd { /// Backup source (can be specified multiple times), use - for stdin. If no source is given, uses all @@ -112,9 +114,13 @@ pub struct BackupCmd { #[merge(skip)] config_opts: ConfigOptions, + /// Hooks to use + #[clap(skip)] + hooks: Hooks, + /// Backup snapshots to generate #[clap(skip)] - #[merge(strategy = merge_sources)] + #[merge(strategy = merge_snapshots)] snapshots: Vec, /// Backup source, used within config file @@ -123,14 +129,14 @@ pub struct BackupCmd { sources: Vec, } -/// Merge backup sources +/// Merge backup snapshots to generate /// -/// If a source is already defined on left, use that. Else add it. +/// If a snapshot is already defined on left, use that. Else add it. /// /// # Arguments /// /// * `left` - Vector of backup sources -pub(crate) fn merge_sources(left: &mut Vec, mut right: Vec) { +pub(crate) fn merge_snapshots(left: &mut Vec, mut right: Vec) { left.append(&mut right); left.sort_by(|opt1, opt2| opt1.sources.cmp(&opt2.sources)); left.dedup_by(|opt1, opt2| opt1.sources == opt2.sources); @@ -138,7 +144,21 @@ pub(crate) fn merge_sources(left: &mut Vec, mut right: Vec impl Runnable for BackupCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + let config = RUSTIC_APP.config(); + + // manually check for a "source" field, check is not done by serde, see above. + if !config.backup.sources.is_empty() { + status_err!("key \"sources\" is not valid in the [backup] section!"); + RUSTIC_APP.shutdown(Shutdown::Crash); + } + + let snapshot_opts = &config.backup.snapshots; + // manually check for a "sources" field, check is not done by serde, see above. + if snapshot_opts.iter().any(|opt| !opt.snapshots.is_empty()) { + status_err!("key \"snapshots\" is not valid in a [[backup.snapshots]] section!"); + RUSTIC_APP.shutdown(Shutdown::Crash); + } + if let Err(err) = config.repository.run(|repo| self.inner_run(repo)) { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -146,9 +166,10 @@ impl Runnable for BackupCmd { } impl BackupCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = get_repository(&config.repository)?; + let snapshot_opts = &config.backup.snapshots; + // Initialize repository if --init is set and it is not yet initialized let repo = if self.init && repo.config_id()?.is_none() { if config.global.dry_run { @@ -157,24 +178,12 @@ impl BackupCmd { repo.name ); } - init(repo, &self.key_opts, &self.config_opts)? + init(repo.0, &self.key_opts, &self.config_opts)? } else { - open_repository(&config.repository)? + repo.open()? } .to_indexed_ids()?; - // manually check for a "source" field, check is not done by serde, see above. - if !config.backup.sources.is_empty() { - bail!("key \"source\" is not valid in the [backup] section!"); - } - - let snapshot_opts = &config.backup.snapshots; - - // manually check for a "sources" field, check is not done by serde, see above. - if snapshot_opts.iter().any(|opt| !opt.snapshots.is_empty()) { - bail!("key \"sources\" is not valid in a [[backup.sources]] section!"); - } - let config_snapshot_sources: Vec<_> = snapshot_opts .iter() .map(|opt| -> Result<_> { @@ -182,7 +191,7 @@ impl BackupCmd { .sanitize() .with_context(|| { format!( - "error sanitizing source=\"{:?}\" in config file", + "error sanitizing sources=\"{:?}\" in config file", opt.sources ) })? @@ -210,87 +219,117 @@ impl BackupCmd { bail!("no backup source given."); } }; + if snapshot_sources.is_empty() { + return Ok(()); + } - for sources in snapshot_sources { - let mut opts = self.clone(); + let hooks = config.backup.hooks.with_context("backup"); + hooks.use_with(|| -> Result<_> { + let mut is_err = false; + for sources in snapshot_sources { + let mut opts = self.clone(); - // merge Options from config file, if given - if let Some(idx) = config_snapshot_sources.iter().position(|s| s == &sources) { - info!("merging source={sources} section from config file"); - opts.merge(snapshot_opts[idx].clone()); - } - if let Some(path) = &opts.as_path { - // as_path only works in combination with a single target - if sources.len() > 1 { - bail!("as-path only works with a single target!"); + // merge Options from config file, if given + if let Some(idx) = config_snapshot_sources.iter().position(|s| s == &sources) { + info!("merging sources={sources} section from config file"); + opts.merge(snapshot_opts[idx].clone()); } - // merge Options from config file using as_path, if given - if let Some(path) = path.as_os_str().to_str() { - if let Some(idx) = snapshot_opts - .iter() - .position(|opt| opt.sources == vec![path]) - { - info!("merging source=\"{path}\" section from config file"); - opts.merge(snapshot_opts[idx].clone()); - } + if let Err(err) = opts.backup_snapshot(sources.clone(), &repo) { + error!("error backing up {sources}: {err}"); + is_err = true; } } + if is_err { + Err(anyhow!("Not all snapshots were generated successfully!")) + } else { + Ok(()) + } + }) + } - // merge "backup" section from config file, if given - opts.merge(config.backup.clone()); - - let backup_opts = BackupOptions::default() - .stdin_filename(opts.stdin_filename) - .stdin_command(opts.stdin_command) - .as_path(opts.as_path) - .parent_opts(opts.parent_opts) - .ignore_save_opts(opts.ignore_save_opts) - .ignore_filter_opts(opts.ignore_filter_opts) - .no_scan(opts.no_scan) - .dry_run(config.global.dry_run); - let snap = repo.backup(&backup_opts, &sources, opts.snap_opts.to_snapshot()?)?; - - if opts.json { - let mut stdout = std::io::stdout(); - serde_json::to_writer_pretty(&mut stdout, &snap)?; - } else if opts.long { - let mut table = table(); - - let add_entry = |title: &str, value: String| { - _ = table.add_row([bold_cell(title), Cell::new(value)]); - }; - fill_table(&snap, add_entry); - - println!("{table}"); - } else if !opts.quiet { - let summary = snap.summary.unwrap(); - println!( - "Files: {} new, {} changed, {} unchanged", - summary.files_new, summary.files_changed, summary.files_unmodified - ); - println!( - "Dirs: {} new, {} changed, {} unchanged", - summary.dirs_new, summary.dirs_changed, summary.dirs_unmodified - ); - debug!("Data Blobs: {} new", summary.data_blobs); - debug!("Tree Blobs: {} new", summary.tree_blobs); - println!( - "Added to the repo: {} (raw: {})", - bytes_size_to_string(summary.data_added_packed), - bytes_size_to_string(summary.data_added) - ); - - println!( - "processed {} files, {}", - summary.total_files_processed, - bytes_size_to_string(summary.total_bytes_processed) - ); - println!("snapshot {} successfully saved.", snap.id); + fn backup_snapshot( + mut self, + source: PathList, + repo: &Repository, + ) -> Result<()> { + let config = RUSTIC_APP.config(); + let snapshot_opts = &config.backup.snapshots; + if let Some(path) = &self.as_path { + // as_path only works in combination with a single target + if source.len() > 1 { + bail!("as-path only works with a single source!"); } + // merge Options from config file using as_path, if given + if let Some(path) = path.as_os_str().to_str() { + if let Some(idx) = snapshot_opts + .iter() + .position(|opt| opt.sources == vec![path]) + { + info!("merging snapshot=\"{path}\" section from config file"); + self.merge(snapshot_opts[idx].clone()); + } + } + } - info!("backup of {sources} done."); + // use the correct source-specific hooks + let hooks = self.hooks.with_context(&format!("backup {source}")); + + // merge "backup" section from config file, if given + self.merge(config.backup.clone()); + + let backup_opts = BackupOptions::default() + .stdin_filename(self.stdin_filename) + .stdin_command(self.stdin_command) + .as_path(self.as_path) + .parent_opts(self.parent_opts) + .ignore_save_opts(self.ignore_save_opts) + .ignore_filter_opts(self.ignore_filter_opts) + .no_scan(self.no_scan) + .dry_run(config.global.dry_run); + + let snap = hooks.use_with(|| -> Result<_> { + Ok(repo.backup(&backup_opts, &source, self.snap_opts.to_snapshot()?)?) + })?; + + if self.json { + let mut stdout = std::io::stdout(); + serde_json::to_writer_pretty(&mut stdout, &snap)?; + } else if self.long { + let mut table = table(); + + let add_entry = |title: &str, value: String| { + _ = table.add_row([bold_cell(title), Cell::new(value)]); + }; + fill_table(&snap, add_entry); + + println!("{table}"); + } else if !self.quiet { + let summary = snap.summary.unwrap(); + println!( + "Files: {} new, {} changed, {} unchanged", + summary.files_new, summary.files_changed, summary.files_unmodified + ); + println!( + "Dirs: {} new, {} changed, {} unchanged", + summary.dirs_new, summary.dirs_changed, summary.dirs_unmodified + ); + debug!("Data Blobs: {} new", summary.data_blobs); + debug!("Tree Blobs: {} new", summary.tree_blobs); + println!( + "Added to the repo: {} (raw: {})", + bytes_size_to_string(summary.data_added_packed), + bytes_size_to_string(summary.data_added) + ); + + println!( + "processed {} files, {}", + summary.total_files_processed, + bytes_size_to_string(summary.total_bytes_processed) + ); + println!("snapshot {} successfully saved.", snap.id); } + info!("backup of {source} done."); Ok(()) } } diff --git a/src/commands/cat.rs b/src/commands/cat.rs index 46de46a8e..dde621118 100644 --- a/src/commands/cat.rs +++ b/src/commands/cat.rs @@ -1,9 +1,6 @@ //! `cat` subcommand -use crate::{ - commands::{open_repository, open_repository_indexed}, - status_err, Application, RUSTIC_APP, -}; +use crate::{status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; @@ -62,24 +59,26 @@ impl Runnable for CatCmd { impl CatCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let data = - match &self.cmd { - CatSubCmd::Config => { - open_repository(&config.repository)?.cat_file(FileType::Config, "")? - } - CatSubCmd::Index(opt) => { - open_repository(&config.repository)?.cat_file(FileType::Index, &opt.id)? - } - CatSubCmd::Snapshot(opt) => { - open_repository(&config.repository)?.cat_file(FileType::Snapshot, &opt.id)? - } - CatSubCmd::TreeBlob(opt) => open_repository_indexed(&config.repository)? - .cat_blob(BlobType::Tree, &opt.id)?, - CatSubCmd::DataBlob(opt) => open_repository_indexed(&config.repository)? - .cat_blob(BlobType::Data, &opt.id)?, - CatSubCmd::Tree(opt) => open_repository_indexed(&config.repository)? - .cat_tree(&opt.snap, |sn| config.snapshot_filter.matches(sn))?, - }; + let data = match &self.cmd { + CatSubCmd::Config => config + .repository + .run_open(|repo| Ok(repo.cat_file(FileType::Config, "")?))?, + CatSubCmd::Index(opt) => config + .repository + .run_open(|repo| Ok(repo.cat_file(FileType::Index, &opt.id)?))?, + CatSubCmd::Snapshot(opt) => config + .repository + .run_open(|repo| Ok(repo.cat_file(FileType::Snapshot, &opt.id)?))?, + CatSubCmd::TreeBlob(opt) => config + .repository + .run_indexed(|repo| Ok(repo.cat_blob(BlobType::Tree, &opt.id)?))?, + CatSubCmd::DataBlob(opt) => config + .repository + .run_indexed(|repo| Ok(repo.cat_blob(BlobType::Data, &opt.id)?))?, + CatSubCmd::Tree(opt) => config.repository.run_indexed(|repo| { + Ok(repo.cat_tree(&opt.snap, |sn| config.snapshot_filter.matches(sn))?) + })?, + }; println!("{}", String::from_utf8(data.to_vec())?); Ok(()) diff --git a/src/commands/check.rs b/src/commands/check.rs index 7a5e1d8bc..b25628bb0 100644 --- a/src/commands/check.rs +++ b/src/commands/check.rs @@ -1,6 +1,6 @@ //! `check` subcommand -use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; +use crate::{repository::CliOpenRepo, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; @@ -20,7 +20,11 @@ pub(crate) struct CheckCmd { impl Runnable for CheckCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_open(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -28,9 +32,8 @@ impl Runnable for CheckCmd { } impl CheckCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliOpenRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(&config.repository)?; let groups = repo.get_snapshot_group(&self.ids, SnapshotGroupCriterion::new(), |sn| { config.snapshot_filter.matches(sn) diff --git a/src/commands/config.rs b/src/commands/config.rs index a201dfd5d..5d3481885 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -1,6 +1,6 @@ //! `config` subcommand -use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; +use crate::{status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; @@ -28,9 +28,10 @@ impl Runnable for ConfigCmd { impl ConfigCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(&config.repository)?; - let changed = repo.apply_config(&self.config_opts)?; + let changed = config + .repository + .run_open(|repo| Ok(repo.apply_config(&self.config_opts)?))?; if changed { println!("saved new config"); diff --git a/src/commands/copy.rs b/src/commands/copy.rs index 7dd309cdf..8cc728ca1 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -1,8 +1,9 @@ //! `copy` subcommand use crate::{ - commands::{get_repository, init::init_password, open_repository, open_repository_indexed}, + commands::init::init_password, helpers::table_with_titles, + repository::{CliIndexedRepo, CliRepo}, status_err, Application, RusticConfig, RUSTIC_APP, }; use abscissa_core::{config::Override, Command, FrameworkError, Runnable, Shutdown}; @@ -11,7 +12,7 @@ use conflate::Merge; use log::{error, info, log, Level}; use serde::{Deserialize, Serialize}; -use rustic_core::{CopySnapshot, Id, KeyOptions}; +use rustic_core::{repofile::SnapshotFile, CopySnapshot, Id, KeyOptions}; /// `copy` subcommand #[derive(clap::Parser, Command, Default, Clone, Debug, Serialize, Deserialize, Merge)] @@ -55,7 +56,12 @@ impl Override for CopyCmd { impl Runnable for CopyCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + let config = RUSTIC_APP.config(); + if config.copy.targets.is_empty() { + status_err!("No target given. Please specify at least 1 target either in the profile or using --target!"); + RUSTIC_APP.shutdown(Shutdown::Crash); + } + if let Err(err) = config.repository.run_indexed(|repo| self.inner_run(repo)) { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -63,14 +69,8 @@ impl Runnable for CopyCmd { } impl CopyCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> { let config = RUSTIC_APP.config(); - - if config.copy.targets.is_empty() { - bail!("No target given. Please specify at least 1 target either in the profile or using --target!"); - } - - let repo = open_repository_indexed(&config.repository)?; let mut snapshots = if self.ids.is_empty() { repo.get_matching_snapshots(|sn| config.snapshot_filter.matches(sn))? } else { @@ -79,7 +79,6 @@ impl CopyCmd { // sort for nicer output snapshots.sort_unstable(); - let poly = repo.config().poly()?; for target in &config.copy.targets { let mut merge_logs = Vec::new(); let mut target_config = RusticConfig::default(); @@ -89,68 +88,76 @@ impl CopyCmd { log!(level, "{}", merge_log); } let target_opt = &target_config.repository; + if let Err(err) = + target_opt.run(|target_repo| self.copy(&repo, target_repo, &snapshots)) + { + error!("error copying to target: {err}"); + } + } + Ok(()) + } - let repo_dest = get_repository(target_opt)?; - - info!("copying to target {}...", repo_dest.name); - let repo_dest = if self.init && repo_dest.config_id()?.is_none() { - if config.global.dry_run { - error!( - "cannot initialize target {} in dry-run mode!", - repo_dest.name - ); - continue; - } - let mut config_dest = repo.config().clone(); - config_dest.id = Id::random().into(); - let pass = init_password(&repo_dest)?; - repo_dest.init_with_config(&pass, &self.key_opts, config_dest)? - } else { - open_repository(target_opt)? - }; + fn copy( + &self, + repo: &CliIndexedRepo, + target_repo: CliRepo, + snapshots: &[SnapshotFile], + ) -> Result<()> { + let config = RUSTIC_APP.config(); - if poly != repo_dest.config().poly()? { - bail!("cannot copy to repository with different chunker parameter (re-chunking not implemented)!"); - } + info!("copying to target {}...", target_repo.name); + let target_repo = if self.init && target_repo.config_id()?.is_none() { + let mut config_dest = repo.config().clone(); + config_dest.id = Id::random().into(); + let pass = init_password(&target_repo)?; + target_repo + .0 + .init_with_config(&pass, &self.key_opts, config_dest)? + } else { + target_repo.open()? + }; - let snaps = repo_dest.relevant_copy_snapshots( - |sn| !self.ids.is_empty() || config.snapshot_filter.matches(sn), - &snapshots, - )?; - - let mut table = - table_with_titles(["ID", "Time", "Host", "Label", "Tags", "Paths", "Status"]); - for CopySnapshot { relevant, sn } in snaps.iter() { - let tags = sn.tags.formatln(); - let paths = sn.paths.formatln(); - let time = sn.time.format("%Y-%m-%d %H:%M:%S").to_string(); - _ = table.add_row([ - &sn.id.to_string(), - &time, - &sn.hostname, - &sn.label, - &tags, - &paths, - &(if *relevant { "to copy" } else { "existing" }).to_string(), - ]); - } - println!("{table}"); - - let count = snaps.iter().filter(|sn| sn.relevant).count(); - if count > 0 { - if config.global.dry_run { - info!("would have copied {count} snapshots."); - } else { - repo.copy( - &repo_dest.to_indexed_ids()?, - snaps - .iter() - .filter_map(|CopySnapshot { relevant, sn }| relevant.then_some(sn)), - )?; - } + if repo.config().poly()? != target_repo.config().poly()? { + bail!("cannot copy to repository with different chunker parameter (re-chunking not implemented)!"); + } + + let snaps = target_repo.relevant_copy_snapshots( + |sn| !self.ids.is_empty() || config.snapshot_filter.matches(sn), + snapshots, + )?; + + let mut table = + table_with_titles(["ID", "Time", "Host", "Label", "Tags", "Paths", "Status"]); + for CopySnapshot { relevant, sn } in snaps.iter() { + let tags = sn.tags.formatln(); + let paths = sn.paths.formatln(); + let time = sn.time.format("%Y-%m-%d %H:%M:%S").to_string(); + _ = table.add_row([ + &sn.id.to_string(), + &time, + &sn.hostname, + &sn.label, + &tags, + &paths, + &(if *relevant { "to copy" } else { "existing" }).to_string(), + ]); + } + println!("{table}"); + + let count = snaps.iter().filter(|sn| sn.relevant).count(); + if count > 0 { + if config.global.dry_run { + info!("would have copied {count} snapshots."); } else { - info!("nothing to copy."); + repo.copy( + &target_repo.to_indexed_ids()?, + snaps + .iter() + .filter_map(|CopySnapshot { relevant, sn }| relevant.then_some(sn)), + )?; } + } else { + info!("nothing to copy."); } Ok(()) } diff --git a/src/commands/diff.rs b/src/commands/diff.rs index 8bd53804a..5333ba9c9 100644 --- a/src/commands/diff.rs +++ b/src/commands/diff.rs @@ -1,6 +1,6 @@ //! `diff` subcommand -use crate::{commands::open_repository_indexed, status_err, Application, RUSTIC_APP}; +use crate::{repository::CliIndexedRepo, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use clap::ValueHint; @@ -49,7 +49,11 @@ pub(crate) struct DiffCmd { impl Runnable for DiffCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_indexed(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -57,9 +61,8 @@ impl Runnable for DiffCmd { } impl DiffCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository_indexed(&config.repository)?; let (id1, path1) = arg_to_snap_path(&self.snap1, ""); let (id2, path2) = arg_to_snap_path(&self.snap2, path1); diff --git a/src/commands/dump.rs b/src/commands/dump.rs index af204dcb9..a7cda0ee9 100644 --- a/src/commands/dump.rs +++ b/src/commands/dump.rs @@ -1,6 +1,6 @@ //! `dump` subcommand -use crate::{commands::open_repository_indexed, status_err, Application, RUSTIC_APP}; +use crate::{repository::CliIndexedRepo, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; @@ -15,7 +15,11 @@ pub(crate) struct DumpCmd { impl Runnable for DumpCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_indexed(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -23,9 +27,8 @@ impl Runnable for DumpCmd { } impl DumpCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository_indexed(&config.repository)?; let node = repo.node_from_snapshot_path(&self.snap, |sn| config.snapshot_filter.matches(sn))?; diff --git a/src/commands/find.rs b/src/commands/find.rs index 16e90cf6c..942efdfe5 100644 --- a/src/commands/find.rs +++ b/src/commands/find.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; -use crate::{commands::open_repository_indexed, status_err, Application, RUSTIC_APP}; +use crate::{repository::CliIndexedRepo, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; @@ -60,7 +60,11 @@ pub(crate) struct FindCmd { impl Runnable for FindCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_indexed(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -68,9 +72,8 @@ impl Runnable for FindCmd { } impl FindCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository_indexed(&config.repository)?; let groups = repo.get_snapshot_group(&self.ids, self.group_by, |sn| { config.snapshot_filter.matches(sn) diff --git a/src/commands/forget.rs b/src/commands/forget.rs index 2e7d2e339..f95bdb473 100644 --- a/src/commands/forget.rs +++ b/src/commands/forget.rs @@ -1,9 +1,7 @@ //! `forget` subcommand -use crate::{ - commands::open_repository, helpers::table_with_titles, status_err, Application, RusticConfig, - RUSTIC_APP, -}; +use crate::repository::CliOpenRepo; +use crate::{helpers::table_with_titles, status_err, Application, RusticConfig, RUSTIC_APP}; use abscissa_core::{config::Override, Shutdown}; use abscissa_core::{Command, FrameworkError, Runnable}; @@ -91,7 +89,11 @@ pub struct ForgetOptions { impl Runnable for ForgetCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_open(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -102,9 +104,8 @@ impl ForgetCmd { /// be careful about self vs `RUSTIC_APP.config()` usage /// only the `RUSTIC_APP.config()` involves the TOML and ENV merged configurations /// see - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliOpenRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(&config.repository)?; let group_by = config.forget.group_by.unwrap_or_default(); diff --git a/src/commands/init.rs b/src/commands/init.rs index d3b2ab053..6bfa9bb19 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -4,7 +4,7 @@ use abscissa_core::{status_err, Command, Runnable, Shutdown}; use anyhow::{bail, Result}; use dialoguer::Password; -use crate::{commands::get_repository, Application, RUSTIC_APP}; +use crate::{repository::CliRepo, Application, RUSTIC_APP}; use rustic_core::{ConfigOptions, KeyOptions, OpenStatus, Repository}; @@ -22,7 +22,11 @@ pub(crate) struct InitCmd { impl Runnable for InitCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -30,9 +34,8 @@ impl Runnable for InitCmd { } impl InitCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = get_repository(&config.repository)?; // Note: This is again checked in repo.init_with_password(), however we want to inform // users before they are prompted to enter a password @@ -48,7 +51,7 @@ impl InitCmd { ); } - let _ = init(repo, &self.key_opts, &self.config_opts)?; + let _ = init(repo.0, &self.key_opts, &self.config_opts)?; Ok(()) } } diff --git a/src/commands/key.rs b/src/commands/key.rs index 2c1cd87c2..9670a378a 100644 --- a/src/commands/key.rs +++ b/src/commands/key.rs @@ -1,6 +1,6 @@ //! `key` subcommand -use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; +use crate::{repository::CliOpenRepo, status_err, Application, RUSTIC_APP}; use std::path::PathBuf; @@ -52,7 +52,11 @@ impl Runnable for KeyCmd { impl Runnable for AddCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_open(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -60,10 +64,7 @@ impl Runnable for AddCmd { } impl AddCmd { - fn inner_run(&self) -> Result<()> { - let config = RUSTIC_APP.config(); - let repo = open_repository(&config.repository)?; - + fn inner_run(&self, repo: CliOpenRepo) -> Result<()> { // create new Repository options which just contain password information let mut pass_opts = RepositoryOptions::default(); pass_opts.password = self.new_password.clone(); diff --git a/src/commands/list.rs b/src/commands/list.rs index 97a80ee97..79ddc728a 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -2,7 +2,7 @@ use std::num::NonZero; -use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; +use crate::{repository::CliOpenRepo, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::{bail, Result}; @@ -19,7 +19,11 @@ pub(crate) struct ListCmd { impl Runnable for ListCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_open(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -27,10 +31,7 @@ impl Runnable for ListCmd { } impl ListCmd { - fn inner_run(&self) -> Result<()> { - let config = RUSTIC_APP.config(); - let repo = open_repository(&config.repository)?; - + fn inner_run(&self, repo: CliOpenRepo) -> Result<()> { match self.tpe.as_str() { // special treatment for listing blobs: read the index and display it "blobs" | "indexpacks" | "indexcontent" => { diff --git a/src/commands/ls.rs b/src/commands/ls.rs index 34279a741..89e442576 100644 --- a/src/commands/ls.rs +++ b/src/commands/ls.rs @@ -2,7 +2,7 @@ use std::path::Path; -use crate::{commands::open_repository_indexed, status_err, Application, RUSTIC_APP}; +use crate::{repository::CliIndexedRepo, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; @@ -58,7 +58,11 @@ pub(crate) struct LsCmd { impl Runnable for LsCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_indexed(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -126,9 +130,8 @@ impl NodeLs for Node { } impl LsCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository_indexed(&config.repository)?; let node = repo.node_from_snapshot_path(&self.snap, |sn| config.snapshot_filter.matches(sn))?; diff --git a/src/commands/merge.rs b/src/commands/merge.rs index aa102f349..bb1525e03 100644 --- a/src/commands/merge.rs +++ b/src/commands/merge.rs @@ -1,6 +1,6 @@ //! `merge` subcommand -use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; +use crate::{repository::CliOpenRepo, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; use log::info; @@ -31,7 +31,11 @@ pub(super) struct MergeCmd { impl Runnable for MergeCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_open(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -39,9 +43,9 @@ impl Runnable for MergeCmd { } impl MergeCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliOpenRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(&config.repository)?.to_indexed_ids()?; + let repo = repo.to_indexed_ids()?; let snapshots = if self.ids.is_empty() { repo.get_matching_snapshots(|sn| config.snapshot_filter.matches(sn))? diff --git a/src/commands/prune.rs b/src/commands/prune.rs index e71408537..1a47a56b2 100644 --- a/src/commands/prune.rs +++ b/src/commands/prune.rs @@ -1,7 +1,7 @@ //! `prune` subcommand use crate::{ - commands::open_repository, helpers::bytes_size_to_string, status_err, Application, RUSTIC_APP, + helpers::bytes_size_to_string, repository::CliOpenRepo, status_err, Application, RUSTIC_APP, }; use abscissa_core::{Command, Runnable, Shutdown}; use log::debug; @@ -21,7 +21,11 @@ pub(crate) struct PruneCmd { impl Runnable for PruneCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_open(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -29,9 +33,8 @@ impl Runnable for PruneCmd { } impl PruneCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliOpenRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(&config.repository)?; let pruner = repo.prune_plan(&self.opts)?; diff --git a/src/commands/repair.rs b/src/commands/repair.rs index aa7b8d80d..b5242b7a1 100644 --- a/src/commands/repair.rs +++ b/src/commands/repair.rs @@ -1,6 +1,9 @@ //! `repair` subcommand -use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; +use crate::{ + repository::{CliIndexedRepo, CliOpenRepo}, + status_err, Application, RUSTIC_APP, +}; use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; @@ -50,7 +53,8 @@ impl Runnable for RepairCmd { impl Runnable for IndexSubCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + let config = RUSTIC_APP.config(); + if let Err(err) = config.repository.run_open(|repo| self.inner_run(repo)) { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -58,9 +62,8 @@ impl Runnable for IndexSubCmd { } impl IndexSubCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliOpenRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(&config.repository)?; repo.repair_index(&self.opts, config.global.dry_run)?; Ok(()) } @@ -68,7 +71,8 @@ impl IndexSubCmd { impl Runnable for SnapSubCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + let config = RUSTIC_APP.config(); + if let Err(err) = config.repository.run_indexed(|repo| self.inner_run(repo)) { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -76,9 +80,8 @@ impl Runnable for SnapSubCmd { } impl SnapSubCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(&config.repository)?.to_indexed()?; let snaps = if self.ids.is_empty() { repo.get_all_snapshots()? } else { diff --git a/src/commands/repoinfo.rs b/src/commands/repoinfo.rs index 16750f6cf..5dcf686a2 100644 --- a/src/commands/repoinfo.rs +++ b/src/commands/repoinfo.rs @@ -1,8 +1,8 @@ //! `repoinfo` subcommand use crate::{ - commands::{get_repository, open_repository}, helpers::{bytes_size_to_string, table_right_from}, + repository::CliRepo, status_err, Application, RUSTIC_APP, }; @@ -30,7 +30,11 @@ pub(crate) struct RepoInfoCmd { impl Runnable for RepoInfoCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -48,21 +52,13 @@ struct Infos { } impl RepoInfoCmd { - fn inner_run(&self) -> Result<()> { - let config = RUSTIC_APP.config(); - + fn inner_run(&self, repo: CliRepo) -> Result<()> { let infos = Infos { files: (!self.only_index) - .then(|| -> Result<_> { - let repo = get_repository(&config.repository)?; - Ok(repo.infos_files()?) - }) + .then(|| -> Result<_> { Ok(repo.infos_files()?) }) .transpose()?, index: (!self.only_files) - .then(|| -> Result<_> { - let repo = open_repository(&config.repository)?; - Ok(repo.infos_index()?) - }) + .then(|| -> Result<_> { Ok(repo.open()?.infos_index()?) }) .transpose()?, }; diff --git a/src/commands/restore.rs b/src/commands/restore.rs index 42cac3300..b651a24aa 100644 --- a/src/commands/restore.rs +++ b/src/commands/restore.rs @@ -1,8 +1,7 @@ //! `restore` subcommand use crate::{ - commands::open_repository_indexed, helpers::bytes_size_to_string, status_err, Application, - RUSTIC_APP, + helpers::bytes_size_to_string, repository::CliIndexedRepo, status_err, Application, RUSTIC_APP, }; use abscissa_core::{Command, Runnable, Shutdown}; @@ -42,7 +41,11 @@ pub(crate) struct RestoreCmd { } impl Runnable for RestoreCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_indexed(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -50,10 +53,9 @@ impl Runnable for RestoreCmd { } impl RestoreCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> { let config = RUSTIC_APP.config(); let dry_run = config.global.dry_run; - let repo = open_repository_indexed(&config.repository)?; let node = repo.node_from_snapshot_path(&self.snap, |sn| config.snapshot_filter.matches(sn))?; diff --git a/src/commands/snapshots.rs b/src/commands/snapshots.rs index 097539986..27fb8a3e7 100644 --- a/src/commands/snapshots.rs +++ b/src/commands/snapshots.rs @@ -1,8 +1,8 @@ //! `smapshot` subcommand use crate::{ - commands::open_repository, helpers::{bold_cell, bytes_size_to_string, table, table_right_from}, + repository::CliOpenRepo, status_err, Application, RUSTIC_APP, }; @@ -56,7 +56,11 @@ pub(crate) struct SnapshotCmd { impl Runnable for SnapshotCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_open(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -64,14 +68,13 @@ impl Runnable for SnapshotCmd { } impl SnapshotCmd { - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliOpenRepo) -> Result<()> { #[cfg(feature = "tui")] if self.interactive { return tui::run(self.group_by); } let config = RUSTIC_APP.config(); - let repo = open_repository(&config.repository)?; let groups = repo.get_snapshot_group(&self.ids, self.group_by, |sn| { config.snapshot_filter.matches(sn) diff --git a/src/commands/tag.rs b/src/commands/tag.rs index 850a62ede..302c58123 100644 --- a/src/commands/tag.rs +++ b/src/commands/tag.rs @@ -1,9 +1,10 @@ //! `tag` subcommand -use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; +use crate::{repository::CliOpenRepo, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; +use anyhow::Result; use chrono::{Duration, Local}; use rustic_core::{repofile::DeleteOption, StringList}; @@ -61,7 +62,11 @@ pub(crate) struct TagCmd { impl Runnable for TagCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_open(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -69,9 +74,8 @@ impl Runnable for TagCmd { } impl TagCmd { - fn inner_run(&self) -> anyhow::Result<()> { + fn inner_run(&self, repo: CliOpenRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(&config.repository)?; let snapshots = if self.ids.is_empty() { repo.get_matching_snapshots(|sn| config.snapshot_filter.matches(sn))? diff --git a/src/commands/tui.rs b/src/commands/tui.rs index 835bac70f..c0d0d6113 100644 --- a/src/commands/tui.rs +++ b/src/commands/tui.rs @@ -14,7 +14,6 @@ use snapshots::Snapshots; use std::io; use std::sync::{Arc, RwLock}; -use crate::commands::open_repository_indexed_with_progress; use crate::{Application, RUSTIC_APP}; use anyhow::Result; @@ -45,13 +44,16 @@ pub fn run(group_by: SnapshotGroupCriterion) -> Result<()> { let progress = TuiProgressBars { terminal: terminal.clone(), }; - let p = progress.progress_spinner("starting rustic in interactive mode..."); - let repo = open_repository_indexed_with_progress(&config.repository, progress)?; - p.finish(); - // create app and run it - let snapshots = Snapshots::new(&repo, config.snapshot_filter.clone(), group_by)?; - let app = App { snapshots }; - let res = run_app(terminal, app); + let res = config + .repository + .run_indexed_with_progress(progress.clone(), |repo| { + let p = progress.progress_spinner("starting rustic in interactive mode..."); + p.finish(); + // create app and run it + let snapshots = Snapshots::new(&repo, config.snapshot_filter.clone(), group_by)?; + let app = App { snapshots }; + run_app(terminal, app) + }); if let Err(err) = res { println!("{err:?}"); diff --git a/src/commands/webdav.rs b/src/commands/webdav.rs index 84a3289a7..c54c7b325 100644 --- a/src/commands/webdav.rs +++ b/src/commands/webdav.rs @@ -5,7 +5,7 @@ use std::{net::ToSocketAddrs, str::FromStr}; -use crate::{commands::open_repository_indexed, status_err, Application, RusticConfig, RUSTIC_APP}; +use crate::{repository::CliIndexedRepo, status_err, Application, RusticConfig, RUSTIC_APP}; use abscissa_core::{config::Override, Command, FrameworkError, Runnable, Shutdown}; use anyhow::{anyhow, Result}; use conflate::Merge; @@ -63,7 +63,11 @@ impl Override for WebDavCmd { impl Runnable for WebDavCmd { fn run(&self) { - if let Err(err) = self.inner_run() { + if let Err(err) = RUSTIC_APP + .config() + .repository + .run_indexed(|repo| self.inner_run(repo)) + { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; @@ -74,9 +78,8 @@ impl WebDavCmd { /// be careful about self VS RUSTIC_APP.config() usage /// only the RUSTIC_APP.config() involves the TOML and ENV merged configurations /// see https://github.com/rustic-rs/rustic/issues/1242 - fn inner_run(&self) -> Result<()> { + fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository_indexed(&config.repository)?; let path_template = config .webdav diff --git a/src/config.rs b/src/config.rs index 13a706b51..9672ea622 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,28 +4,29 @@ //! application's configuration file and/or command-line options //! for specifying it. +pub(crate) mod hooks; pub(crate) mod progress_options; +use std::fmt::Debug; use std::{collections::HashMap, path::PathBuf}; -use abscissa_core::config::Config; -use abscissa_core::path::AbsPathBuf; -use abscissa_core::FrameworkError; +use abscissa_core::{config::Config, path::AbsPathBuf, FrameworkError}; +use anyhow::Result; use clap::{Parser, ValueHint}; use conflate::Merge; use directories::ProjectDirs; use itertools::Itertools; use log::Level; -use rustic_backend::BackendOptions; -use rustic_core::RepositoryOptions; use serde::{Deserialize, Serialize}; #[cfg(feature = "webdav")] use crate::commands::webdav::WebDavCmd; + use crate::{ commands::{backup::BackupCmd, copy::CopyCmd, forget::ForgetOptions}, - config::progress_options::ProgressOptions, + config::{hooks::Hooks, progress_options::ProgressOptions}, filtering::SnapshotFilter, + repository::AllRepositoryOptions, }; /// Rustic Configuration @@ -67,20 +68,6 @@ pub struct RusticConfig { pub webdav: WebDavCmd, } -#[derive(Clone, Default, Debug, Parser, Serialize, Deserialize, Merge)] -#[serde(default, rename_all = "kebab-case")] -pub struct AllRepositoryOptions { - /// Backend options - #[clap(flatten)] - #[serde(flatten)] - pub be: BackendOptions, - - /// Repository options - #[clap(flatten)] - #[serde(flatten)] - pub repo: RepositoryOptions, -} - impl RusticConfig { /// Merge a profile into the current config by reading the corresponding config file. /// Also recursively merge all profiles given within this config file. @@ -168,6 +155,10 @@ pub struct GlobalOptions { #[serde(flatten)] pub progress_options: ProgressOptions, + /// Hooks + #[clap(skip)] + pub hooks: Hooks, + /// List of environment variables to set (only in config file) #[clap(skip)] #[merge(strategy = conflate::hashmap::ignore)] diff --git a/src/config/hooks.rs b/src/config/hooks.rs new file mode 100644 index 000000000..a3df66921 --- /dev/null +++ b/src/config/hooks.rs @@ -0,0 +1,107 @@ +//! rustic hooks configuration +//! +//! Hooks are commands that are executed before and after every rustic operation. +//! They can be used to run custom scripts or commands before and after a backup, +//! copy, forget, prune or other operation. +//! +//! Depending on the hook type, the command is being executed at a different point +//! in the lifecycle of the program. The following hooks are available: +//! +//! - global hooks +//! - repository hooks +//! - backup hooks +//! - specific source-related hooks + +use anyhow::Result; +use conflate::Merge; +use serde::{Deserialize, Serialize}; + +use rustic_core::CommandInput; + +#[derive(Debug, Default, Clone, Serialize, Deserialize, Merge)] +#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] +pub struct Hooks { + /// Call this command before every rustic operation + #[merge(strategy = conflate::vec::append)] + pub run_before: Vec, + + /// Call this command after every successful rustic operation + #[merge(strategy = conflate::vec::append)] + pub run_after: Vec, + + /// Call this command after every failed rustic operation + #[merge(strategy = conflate::vec::append)] + pub run_failed: Vec, + + /// Call this command after every rustic operation + #[merge(strategy = conflate::vec::append)] + pub run_finally: Vec, + + #[serde(skip)] + #[merge(skip)] + pub context: String, +} + +impl Hooks { + pub fn with_context(&self, context: &str) -> Self { + let mut hooks = self.clone(); + hooks.context = context.to_string(); + hooks + } + + fn run_all(cmds: &[CommandInput], context: &str, what: &str) -> Result<()> { + for cmd in cmds { + cmd.run(context, what)?; + } + + Ok(()) + } + + pub fn run_before(&self) -> Result<()> { + Self::run_all(&self.run_before, &self.context, "run-before") + } + + pub fn run_after(&self) -> Result<()> { + Self::run_all(&self.run_after, &self.context, "run-after") + } + + pub fn run_failed(&self) -> Result<()> { + Self::run_all(&self.run_failed, &self.context, "run-failed") + } + + pub fn run_finally(&self) -> Result<()> { + Self::run_all(&self.run_finally, &self.context, "run-finally") + } + + /// Run the given closure using the specified hooks. + /// + /// Note: after a failure no error handling is done for the hooks `run_failed` + /// and `run_finally` which must run after. However, they already log a warning + /// or error depending on the `on_failure` setting. + pub fn use_with(&self, f: impl FnOnce() -> Result) -> Result { + match self.run_before() { + Ok(_) => match f() { + Ok(result) => match self.run_after() { + Ok(_) => { + self.run_finally()?; + Ok(result) + } + Err(err_after) => { + _ = self.run_finally(); + Err(err_after) + } + }, + Err(err_f) => { + _ = self.run_failed(); + _ = self.run_finally(); + Err(err_f) + } + }, + Err(err_before) => { + _ = self.run_failed(); + _ = self.run_finally(); + Err(err_before) + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 6d2c5e603..399bd4c3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,6 +62,7 @@ pub(crate) mod config; pub(crate) mod error; pub(crate) mod filtering; pub(crate) mod helpers; +pub(crate) mod repository; // rustic_cli Public API diff --git a/src/repository.rs b/src/repository.rs new file mode 100644 index 000000000..7f045078e --- /dev/null +++ b/src/repository.rs @@ -0,0 +1,167 @@ +//! Rustic Config +//! +//! See instructions in `commands.rs` to specify the path to your +//! application's configuration file and/or command-line options +//! for specifying it. + +use std::fmt::Debug; +use std::ops::Deref; + +use abscissa_core::Application; +use anyhow::{anyhow, bail, Result}; +use clap::Parser; +use conflate::Merge; +use dialoguer::Password; +use rustic_backend::BackendOptions; +use rustic_core::{ + FullIndex, IndexedStatus, OpenStatus, ProgressBars, Repository, RepositoryOptions, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + config::{hooks::Hooks, progress_options::ProgressOptions}, + RUSTIC_APP, +}; + +pub(super) mod constants { + pub(super) const MAX_PASSWORD_RETRIES: usize = 5; +} + +#[derive(Clone, Default, Debug, Parser, Serialize, Deserialize, Merge)] +#[serde(default, rename_all = "kebab-case")] +pub struct AllRepositoryOptions { + /// Backend options + #[clap(flatten)] + #[serde(flatten)] + pub be: BackendOptions, + + /// Repository options + #[clap(flatten)] + #[serde(flatten)] + pub repo: RepositoryOptions, + + /// Hooks + #[clap(skip)] + pub hooks: Hooks, +} + +pub type CliRepo = RusticRepo; +pub type CliOpenRepo = Repository; +pub type RusticIndexedRepo

= Repository>; +pub type CliIndexedRepo = RusticIndexedRepo; + +impl AllRepositoryOptions { + fn repository

(&self, po: P) -> Result> { + let backends = self.be.to_backends()?; + let repo = Repository::new_with_progress(&self.repo, &backends, po)?; + Ok(RusticRepo(repo)) + } + + pub fn run(&self, f: impl FnOnce(CliRepo) -> Result) -> Result { + let hooks = self.hooks.with_context("repository"); + let po = RUSTIC_APP.config().global.progress_options; + hooks.use_with(|| f(self.repository(po)?)) + } + + pub fn run_open(&self, f: impl FnOnce(CliOpenRepo) -> Result) -> Result { + let hooks = self.hooks.with_context("repository"); + let po = RUSTIC_APP.config().global.progress_options; + hooks.use_with(|| f(self.repository(po)?.open()?)) + } + + pub fn run_open_or_init_with( + &self, + do_init: bool, + init: impl FnOnce(CliRepo) -> Result, + f: impl FnOnce(CliOpenRepo) -> Result, + ) -> Result { + let hooks = self.hooks.with_context("repository"); + let po = RUSTIC_APP.config().global.progress_options; + hooks.use_with(|| { + f(self + .repository(po)? + .open_or_init_repository_with(do_init, init)?) + }) + } + + pub fn run_indexed_with_progress( + &self, + po: P, + f: impl FnOnce(RusticIndexedRepo

) -> Result, + ) -> Result { + let hooks = self.hooks.with_context("repository"); + hooks.use_with(|| f(self.repository(po)?.indexed()?)) + } + + pub fn run_indexed(&self, f: impl FnOnce(CliIndexedRepo) -> Result) -> Result { + let po = RUSTIC_APP.config().global.progress_options; + self.run_indexed_with_progress(po, f) + } +} + +#[derive(Debug)] +pub struct RusticRepo

(pub Repository); + +impl

Deref for RusticRepo

{ + type Target = Repository; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl RusticRepo

{ + pub fn open(self) -> Result> { + match self.0.password()? { + // if password is given, directly return the result of find_key_in_backend and don't retry + Some(pass) => { + return Ok(self.0.open_with_password(&pass)?); + } + None => { + for _ in 0..constants::MAX_PASSWORD_RETRIES { + let pass = Password::new() + .with_prompt("enter repository password") + .allow_empty_password(true) + .interact()?; + match self.0.clone().open_with_password(&pass) { + Ok(repo) => return Ok(repo), + Err(err) if err.is_incorrect_password() => continue, + Err(err) => return Err(err.into()), + } + } + } + } + Err(anyhow!("incorrect password")) + } + + fn open_or_init_repository_with( + self, + do_init: bool, + init: impl FnOnce(Self) -> Result>, + ) -> Result> { + let dry_run = RUSTIC_APP.config().global.check_index; + // Initialize repository if --init is set and it is not yet initialized + let repo = if do_init && self.0.config_id()?.is_none() { + if dry_run { + bail!( + "cannot initialize repository {} in dry-run mode!", + self.0.name + ); + } + init(self)? + } else { + self.open()? + }; + Ok(repo) + } + + fn indexed(self) -> Result>> { + let open = self.open()?; + let check_index = RUSTIC_APP.config().global.check_index; + let repo = if check_index { + open.to_indexed_checked() + } else { + open.to_indexed() + }?; + Ok(repo) + } +} diff --git a/tests/generated/.gitkeep b/tests/generated/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/tests/hooks-fixtures/backup_hooks_failure.toml b/tests/hooks-fixtures/backup_hooks_failure.toml new file mode 100644 index 000000000..cd30d6ea3 --- /dev/null +++ b/tests/hooks-fixtures/backup_hooks_failure.toml @@ -0,0 +1,12 @@ +[backup.hooks] +run-before = [ + "sh -c 'echo Running backup hooks before > tests/generated/backup_hooks_failure.log'", + "false", + "sh -c 'echo MUST NOT SHOW UP >> tests/generated/backup_hooks_failure.log'", +] +run-failed = [ + "sh -c 'echo Running backup hooks failed >> tests/generated/backup_hooks_failure.log'", +] +run-finally = [ + "sh -c 'echo Running backup hooks finally >> tests/generated/backup_hooks_failure.log'", +] diff --git a/tests/hooks-fixtures/backup_hooks_success.toml b/tests/hooks-fixtures/backup_hooks_success.toml new file mode 100644 index 000000000..8cee9a4af --- /dev/null +++ b/tests/hooks-fixtures/backup_hooks_success.toml @@ -0,0 +1,13 @@ +[backup.hooks] +run-before = [ + "sh -c 'echo Running backup hooks before > tests/generated/backup_hooks_success.log'", +] +run-after = [ + "sh -c 'echo Running backup hooks after >> tests/generated/backup_hooks_success.log'", +] +run-failed = [ + "sh -c 'echo Running backup hooks failed >> tests/generated/backup_hooks_success.log'", +] +run-finally = [ + "sh -c 'echo Running backup hooks finally >> tests/generated/backup_hooks_success.log'", +] diff --git a/tests/hooks-fixtures/check_not_backup_hooks_success.toml b/tests/hooks-fixtures/check_not_backup_hooks_success.toml new file mode 100644 index 000000000..1a5cd5365 --- /dev/null +++ b/tests/hooks-fixtures/check_not_backup_hooks_success.toml @@ -0,0 +1,41 @@ +[global.hooks] +run-before = [ + "sh -c 'echo Running global hooks before > tests/generated/check_not_backup_hooks_success.log'", +] +run-after = [ + "sh -c 'echo Running global hooks after >> tests/generated/check_not_backup_hooks_success.log'", +] +run-failed = [ + "sh -c 'echo Running global hooks failed >> tests/generated/check_not_backup_hooks_success.log'", +] +run-finally = [ + "sh -c 'echo Running global hooks finally >> tests/generated/check_not_backup_hooks_success.log'", +] + +[repository.hooks] +run-before = [ + "sh -c 'echo Running repository hooks before >> tests/generated/check_not_backup_hooks_success.log'", +] +run-after = [ + "sh -c 'echo Running repository hooks after >> tests/generated/check_not_backup_hooks_success.log'", +] +run-failed = [ + "sh -c 'echo Running repository hooks failed >> tests/generated/check_not_backup_hooks_success.log'", +] +run-finally = [ + "sh -c 'echo Running repository hooks finally >> tests/generated/check_not_backup_hooks_success.log'", +] + +[backup.hooks] +run-before = [ + "sh -c 'echo MUST NOT SHOW UP >> tests/generated/check_not_backup_hooks_success.log'", +] +run-after = [ + "sh -c 'echo MUST NOT SHOW UP >> tests/generated/check_not_backup_hooks_success.log'", +] +run-failed = [ + "sh -c 'echo MUST NOT SHOW UP >> tests/generated/check_not_backup_hooks_success.log'", +] +run-finally = [ + "sh -c 'echo MUST NOT SHOW UP >> tests/generated/check_not_backup_hooks_success.log'", +] diff --git a/tests/hooks-fixtures/commands_hooks_access_success.tpl b/tests/hooks-fixtures/commands_hooks_access_success.tpl new file mode 100644 index 000000000..3afc70510 --- /dev/null +++ b/tests/hooks-fixtures/commands_hooks_access_success.tpl @@ -0,0 +1,41 @@ +[global.hooks] +run-before = [ + "sh -c 'echo Running global hooks before > tests/generated/${{filename}}.log'", +] +run-after = [ + "sh -c 'echo Running global hooks after >> tests/generated/${{filename}}.log'", +] +run-failed = [ + "sh -c 'echo Running global hooks failed >> tests/generated/${{filename}}.log'", +] +run-finally = [ + "sh -c 'echo Running global hooks finally >> tests/generated/${{filename}}.log'", +] + +[repository.hooks] +run-before = [ + "sh -c 'echo Running repository hooks before >> tests/generated/${{filename}}.log'", +] +run-after = [ + "sh -c 'echo Running repository hooks after >> tests/generated/${{filename}}.log'", +] +run-failed = [ + "sh -c 'echo Running repository hooks failed >> tests/generated/${{filename}}.log'", +] +run-finally = [ + "sh -c 'echo Running repository hooks finally >> tests/generated/${{filename}}.log'", +] + +[backup.hooks] +run-before = [ + "sh -c 'echo Running backup hooks before >> tests/generated/${{filename}}.log'", +] +run-after = [ + "sh -c 'echo Running backup hooks after >> tests/generated/${{filename}}.log'", +] +run-failed = [ + "sh -c 'echo Running backup hooks failed >> tests/generated/${{filename}}.log'", +] +run-finally = [ + "sh -c 'echo Running backup hooks finally >> tests/generated/${{filename}}.log'", +] diff --git a/tests/hooks-fixtures/empty_hooks_success.toml b/tests/hooks-fixtures/empty_hooks_success.toml new file mode 100644 index 000000000..c64dfc74b --- /dev/null +++ b/tests/hooks-fixtures/empty_hooks_success.toml @@ -0,0 +1,17 @@ +[global.hooks] +run-before = [] +run-after = [] +run-failed = [] +run-finally = [] + +[repository.hooks] +run-before = [] +run-after = [] +run-failed = [] +run-finally = [] + +[backup.hooks] +run-before = [] +run-after = [] +run-failed = [] +run-finally = [] diff --git a/tests/hooks-fixtures/full_hooks_before_backup_failure.toml b/tests/hooks-fixtures/full_hooks_before_backup_failure.toml new file mode 100644 index 000000000..bdf12c364 --- /dev/null +++ b/tests/hooks-fixtures/full_hooks_before_backup_failure.toml @@ -0,0 +1,43 @@ +[global.hooks] +run-before = [ + "sh -c 'echo Running global hooks before > tests/generated/full_hooks_before_backup_failure.log'", +] +run-after = [ + "sh -c 'echo Running global hooks after >> tests/generated/full_hooks_before_backup_failure.log'", +] +run-failed = [ + "sh -c 'echo Running global hooks failed >> tests/generated/full_hooks_before_backup_failure.log'", +] +run-finally = [ + "sh -c 'echo Running global hooks finally >> tests/generated/full_hooks_before_backup_failure.log'", +] + +[repository.hooks] +run-before = [ + "sh -c 'echo Running repository hooks before >> tests/generated/full_hooks_before_backup_failure.log'", +] +run-after = [ + "sh -c 'echo Running repository hooks after >> tests/generated/full_hooks_before_backup_failure.log'", +] +run-failed = [ + "sh -c 'echo Running repository hooks failed >> tests/generated/full_hooks_before_backup_failure.log'", +] +run-finally = [ + "sh -c 'echo Running repository hooks finally >> tests/generated/full_hooks_before_backup_failure.log'", +] + +[backup.hooks] +run-before = [ + "sh -c 'echo Running backup hooks before >> tests/generated/full_hooks_before_backup_failure.log'", + "false", + "sh -c 'echo MUST NOT SHOW UP >> tests/generated/full_hooks_before_backup_failure.log'", +] +run-after = [ + "sh -c 'echo Running backup hooks after >> tests/generated/full_hooks_before_backup_failure.log'", +] +run-failed = [ + "sh -c 'echo Running backup hooks failed >> tests/generated/full_hooks_before_backup_failure.log'", +] +run-finally = [ + "sh -c 'echo Running backup hooks finally >> tests/generated/full_hooks_before_backup_failure.log'", +] diff --git a/tests/hooks-fixtures/full_hooks_before_repo_failure.toml b/tests/hooks-fixtures/full_hooks_before_repo_failure.toml new file mode 100644 index 000000000..ba2e36a12 --- /dev/null +++ b/tests/hooks-fixtures/full_hooks_before_repo_failure.toml @@ -0,0 +1,43 @@ +[global.hooks] +run-before = [ + "sh -c 'echo Running global hooks before > tests/generated/full_hooks_before_repo_failure.log'", +] +run-after = [ + "sh -c 'echo Running global hooks after >> tests/generated/full_hooks_before_repo_failure.log'", +] +run-failed = [ + "sh -c 'echo Running global hooks failed >> tests/generated/full_hooks_before_repo_failure.log'", +] +run-finally = [ + "sh -c 'echo Running global hooks finally >> tests/generated/full_hooks_before_repo_failure.log'", +] + +[repository.hooks] +run-before = [ + "sh -c 'echo Running repository hooks before >> tests/generated/full_hooks_before_repo_failure.log'", + "false", + "sh -c 'echo MUST NOT SHOW UP >> tests/generated/full_hooks_before_repo_failure.log'", +] +run-after = [ + "sh -c 'echo Running repository hooks after >> tests/generated/full_hooks_before_repo_failure.log'", +] +run-failed = [ + "sh -c 'echo Running repository hooks failed >> tests/generated/full_hooks_before_repo_failure.log'", +] +run-finally = [ + "sh -c 'echo Running repository hooks finally >> tests/generated/full_hooks_before_repo_failure.log'", +] + +[backup.hooks] +run-before = [ + "sh -c 'echo Running backup hooks before >> tests/generated/full_hooks_before_repo_failure.log'", +] +run-after = [ + "sh -c 'echo Running backup hooks after >> tests/generated/full_hooks_before_repo_failure.log'", +] +run-failed = [ + "sh -c 'echo Running backup hooks failed >> tests/generated/full_hooks_before_repo_failure.log'", +] +run-finally = [ + "sh -c 'echo Running backup hooks finally >> tests/generated/full_hooks_before_repo_failure.log'", +] diff --git a/tests/hooks-fixtures/full_hooks_success.toml b/tests/hooks-fixtures/full_hooks_success.toml new file mode 100644 index 000000000..8e51936e7 --- /dev/null +++ b/tests/hooks-fixtures/full_hooks_success.toml @@ -0,0 +1,41 @@ +[global.hooks] +run-before = [ + "sh -c 'echo Running global hooks before > tests/generated/full_hooks_success.log'", +] +run-after = [ + "sh -c 'echo Running global hooks after >> tests/generated/full_hooks_success.log'", +] +run-failed = [ + "sh -c 'echo Running global hooks failed >> tests/generated/full_hooks_success.log'", +] +run-finally = [ + "sh -c 'echo Running global hooks finally >> tests/generated/full_hooks_success.log'", +] + +[repository.hooks] +run-before = [ + "sh -c 'echo Running repository hooks before >> tests/generated/full_hooks_success.log'", +] +run-after = [ + "sh -c 'echo Running repository hooks after >> tests/generated/full_hooks_success.log'", +] +run-failed = [ + "sh -c 'echo Running repository hooks failed >> tests/generated/full_hooks_success.log'", +] +run-finally = [ + "sh -c 'echo Running repository hooks finally >> tests/generated/full_hooks_success.log'", +] + +[backup.hooks] +run-before = [ + "sh -c 'echo Running backup hooks before >> tests/generated/full_hooks_success.log'", +] +run-after = [ + "sh -c 'echo Running backup hooks after >> tests/generated/full_hooks_success.log'", +] +run-failed = [ + "sh -c 'echo Running backup hooks failed >> tests/generated/full_hooks_success.log'", +] +run-finally = [ + "sh -c 'echo Running backup hooks finally >> tests/generated/full_hooks_success.log'", +] diff --git a/tests/hooks-fixtures/global_hooks_success.toml b/tests/hooks-fixtures/global_hooks_success.toml new file mode 100644 index 000000000..0295c319e --- /dev/null +++ b/tests/hooks-fixtures/global_hooks_success.toml @@ -0,0 +1,13 @@ +[global.hooks] +run-before = [ + "sh -c 'echo Running global hooks before > tests/generated/global_hooks_success.log'", +] +run-after = [ + "sh -c 'echo Running global hooks after >> tests/generated/global_hooks_success.log'", +] +run-failed = [ + "sh -c 'echo Running global hooks failed >> tests/generated/global_hooks_success.log'", +] +run-finally = [ + "sh -c 'echo Running global hooks finally >> tests/generated/global_hooks_success.log'", +] diff --git a/tests/hooks-fixtures/repository_hooks_success.toml b/tests/hooks-fixtures/repository_hooks_success.toml new file mode 100644 index 000000000..f5ac989cd --- /dev/null +++ b/tests/hooks-fixtures/repository_hooks_success.toml @@ -0,0 +1,13 @@ +[repository.hooks] +run-before = [ + "sh -c 'echo Running repository hooks before > tests/generated/repository_hooks_success.log'", +] +run-after = [ + "sh -c 'echo Running repository hooks after >> tests/generated/repository_hooks_success.log'", +] +run-failed = [ + "sh -c 'echo Running repository hooks failed >> tests/generated/repository_hooks_success.log'", +] +run-finally = [ + "sh -c 'echo Running repository hooks finally >> tests/generated/repository_hooks_success.log'", +] diff --git a/tests/hooks.rs b/tests/hooks.rs new file mode 100644 index 000000000..897f32c14 --- /dev/null +++ b/tests/hooks.rs @@ -0,0 +1,314 @@ +//! Hooks test: runs the application as a subprocess and asserts its +//! interaction with different files due to hooks + +// #![forbid(unsafe_code)] +// #![warn( +// missing_docs, +// rust_2018_idioms, +// trivial_casts, +// unused_lifetimes, +// unused_qualifications +// )] + +use std::path::PathBuf; + +use abscissa_core::fs::remove_file; +use assert_cmd::Command; +use predicates::prelude::predicate; +use rstest::{fixture, rstest}; +use tempfile::{tempdir, TempDir}; + +use rustic_testing::TestResult; +#[fixture] +fn hook_fixture_dir() -> PathBuf { + ["tests", "hooks-fixtures"].iter().collect() +} + +#[fixture] +fn generated_dir() -> PathBuf { + ["tests", "generated"].iter().collect() +} + +#[fixture] +fn toml_fixture_dir() -> PathBuf { + hook_fixture_dir() +} + +#[fixture] +fn log_fixture_dir() -> PathBuf { + hook_fixture_dir().join("log") +} + +pub fn rustic_runner(temp_dir: &TempDir) -> TestResult { + let password = "test"; + let repo_dir = temp_dir.path().join("repo"); + + let mut runner = Command::new(env!("CARGO_BIN_EXE_rustic")); + + runner + .arg("-r") + .arg(repo_dir) + .arg("--password") + .arg(password) + .arg("--no-progress"); + + Ok(runner) +} + +#[allow(dead_code)] +enum BackupAction { + WithBackup, + WithoutBackup, +} + +// Load a template from a file and replace a placeholder with a value +// and write the result to a new file +fn load_template_replace_and_write( + template_path: &PathBuf, + placeholder: &str, + value: &str, + output_path: &PathBuf, +) -> TestResult<()> { + let template = std::fs::read_to_string(template_path)?; + let replaced = template.replace(placeholder, value); + std::fs::write(output_path, replaced)?; + + Ok(()) +} + +fn setup(with_backup: BackupAction) -> TestResult { + let temp_dir = tempdir()?; + rustic_runner(&temp_dir)? + .args(["init"]) + .assert() + .success() + .stderr(predicate::str::contains("successfully created.")) + .stderr(predicate::str::contains("successfully added.")); + + match with_backup { + BackupAction::WithBackup => { + rustic_runner(&temp_dir)? + // We need this so output on stderr is not being taken as an error + .arg("--log-level=error") + .args(["backup", "src/"]) + .assert() + .success(); + } + BackupAction::WithoutBackup => {} + } + + Ok(temp_dir) +} + +#[derive(Debug, PartialEq, Clone, Copy)] +enum RunnerStatus { + Success, + Failure, +} + +fn run_hook_comparison( + temp_dir: TempDir, + hooks_config: PathBuf, + args: &[&str], + snapshot_name: &str, + log_live_path: PathBuf, + status: RunnerStatus, +) -> TestResult<()> { + { + let runner = rustic_runner(&temp_dir)? + // We need this so output on stderr is not being taken as an error + .arg("--log-level=error") + .args(["-P", hooks_config.to_str().unwrap()]) + .args(args) + .assert(); + + match status { + RunnerStatus::Success => runner.success(), + RunnerStatus::Failure => runner.failure(), + }; + } + + let log_live = std::fs::read_to_string(&log_live_path)?; + remove_file(log_live_path)?; + insta::assert_ron_snapshot!(snapshot_name, log_live); + + Ok(()) +} + +#[rstest] +fn test_empty_hooks_do_nothing_passes(toml_fixture_dir: PathBuf) -> TestResult<()> { + let hooks_config = toml_fixture_dir.join("empty_hooks_success"); + + let temp_dir = setup(BackupAction::WithoutBackup)?; + + { + rustic_runner(&temp_dir)? + .args(["-P", hooks_config.to_str().unwrap()]) + .arg("repoinfo") + .assert() + .success() + .stdout(predicate::str::contains("Total Size")); + } + + Ok(()) +} + +macro_rules! generate_test_hook_function { + ($name:ident, $fixture:expr, $args:expr, $status:expr) => { + #[rstest] + fn $name(toml_fixture_dir: PathBuf, generated_dir: PathBuf) -> TestResult<()> { + let hooks_config_path = toml_fixture_dir.join($fixture); + let args = $args; + + let file_name = format!("{}.log", $fixture); + let log_live_path = generated_dir.join(&file_name); + + run_hook_comparison( + setup(BackupAction::WithoutBackup)?, + hooks_config_path, + args, + $fixture, + log_live_path, + $status, + )?; + + Ok(()) + } + }; +} + +// Scenario: Global hooks pass in order +generate_test_hook_function!( + test_global_hooks_order_passes, + "global_hooks_success", + &["repoinfo"], + RunnerStatus::Success +); + +// Scenario: Repository hooks pass in order +generate_test_hook_function!( + test_repository_hooks_order_passes, + "repository_hooks_success", + &["check"], + RunnerStatus::Success +); + +// Scenario: Backup hooks pass in order +generate_test_hook_function!( + test_backup_hooks_order_passes, + "backup_hooks_success", + &["backup", "src/"], + RunnerStatus::Success +); + +// Scenario: Full hooks pass in order +generate_test_hook_function!( + test_full_hooks_order_passes, + "full_hooks_success", + &["backup", "src/"], + RunnerStatus::Success +); + +// Scenario: Check do not run backup hooks +generate_test_hook_function!( + test_check_do_not_run_backup_hooks_passes, + "check_not_backup_hooks_success", + &["check"], + RunnerStatus::Success +); + +// Scenario: Failure in before backup hook does not run backup +generate_test_hook_function!( + test_backup_hooks_with_failure_passes, + "backup_hooks_failure", + &["backup", "src/"], + RunnerStatus::Failure +); + +// Scenario: Failure in after backup hook does run repo and global +// hooks failed and finally +generate_test_hook_function!( + test_full_hooks_with_failure_before_backup_passes, + "full_hooks_before_backup_failure", + &["backup", "src/"], + RunnerStatus::Failure +); + +// Scenario: Failure in before repo hook does run repo and global +// hooks failed and finally +generate_test_hook_function!( + test_full_hooks_with_failure_before_repo_passes, + "full_hooks_before_repo_failure", + &["backup", "src/"], + RunnerStatus::Failure +); + +#[rstest] +#[case(vec!["backup", "src/"], "backup", BackupAction::WithoutBackup)] +#[case(vec!["cat", "tree", "latest"], "cat", BackupAction::WithBackup)] +#[case(vec!["config"], "config", BackupAction::WithoutBackup)] +#[case(vec!["completions", "bash"], "completions", BackupAction::WithoutBackup)] +#[case(vec!["check"], "check", BackupAction::WithBackup)] +// #[case(vec!["copy"], "copy", BackupAction::WithBackup)] +// #[case(vec!["diff"], "diff", BackupAction::WithBackup)] +// TODO: Does it work? Also: Docs command requires a TTY +// #[case(vec!["docs", "--help"], "docs", BackupAction::WithoutBackup)] +#[case(vec!["dump", "latest:src/lib.rs"], "dump", BackupAction::WithBackup)] +#[case(vec!["find"], "find", BackupAction::WithoutBackup)] +#[case(vec!["forget"], "forget", BackupAction::WithoutBackup)] +// TODO: Needs special handling +// #[case(vec!["init", "--dry-run"], "init", BackupAction::WithoutBackup)] +// TODO: Needs user input +// #[case(vec!["key", "add"], "key", BackupAction::WithoutBackup)] +#[case(vec!["list", "indexpacks"], "list", BackupAction::WithBackup)] +#[case(vec!["ls", "latest"], "ls", BackupAction::WithBackup)] +#[case(vec!["merge"], "merge", BackupAction::WithoutBackup)] +#[case(vec!["snapshots"], "snapshots", BackupAction::WithBackup)] +#[case(vec!["show-config"], "show-config", BackupAction::WithoutBackup)] +// TODO: Github API errors with `NetworkError: api request failed with status: 403` +// #[case(vec!["self-update"], "self-update", BackupAction::WithoutBackup)] +#[case(vec!["prune"], "prune", BackupAction::WithBackup)] +#[case(vec!["repoinfo"], "repoinfo", BackupAction::WithBackup)] +#[case(vec!["repair", "index"], "repair", BackupAction::WithBackup)] +#[case(vec!["restore", "latest", "tests/generated/test-restore", "--dry-run"], "restore", BackupAction::WithBackup)] +#[case(vec!["tag"], "tag", BackupAction::WithBackup)] +// TODO: Requires user input +// #[case(vec!["webdav"], "webdav", BackupAction::WithBackup)] +fn test_hooks_access_for_all_commands_passes( + #[case] command_args: Vec<&str>, + #[case] command_name: &str, + #[case] backup_action: BackupAction, + toml_fixture_dir: PathBuf, + generated_dir: PathBuf, +) -> TestResult<()> { + let file_name = format!("{command_name}_hooks_access_success"); + let mut output_config_file_name = file_name.clone(); + output_config_file_name.push_str(".toml"); + + let hooks_config_path = generated_dir.join(&file_name); + let output_config_path = generated_dir.join(output_config_file_name); + + load_template_replace_and_write( + &toml_fixture_dir.join("commands_hooks_access_success.tpl"), + "${{filename}}", + &file_name, + &output_config_path, + )?; + + let log_live_path = generated_dir.join(format!("{file_name}.log")); + + let setup = setup(backup_action)?; + + run_hook_comparison( + setup, + hooks_config_path.clone(), + command_args.as_slice(), + &file_name, + log_live_path.clone(), + RunnerStatus::Success, + )?; + + remove_file(output_config_path)?; + + Ok(()) +} diff --git a/tests/show-config-fixtures/empty.txt b/tests/show-config-fixtures/empty.txt index 7db3df12b..c7ede0bed 100644 --- a/tests/show-config-fixtures/empty.txt +++ b/tests/show-config-fixtures/empty.txt @@ -4,6 +4,12 @@ dry-run = false check-index = false no-progress = false +[global.hooks] +run-before = [] +run-after = [] +run-failed = [] +run-finally = [] + [global.env] [repository] @@ -16,6 +22,12 @@ warm-up = false [repository.options-cold] +[repository.hooks] +run-before = [] +run-after = [] +run-failed = [] +run-finally = [] + [snapshot-filter] filter-hosts = [] filter-labels = [] @@ -51,6 +63,12 @@ delete-never = false snapshots = [] sources = [] +[backup.hooks] +run-before = [] +run-after = [] +run-failed = [] +run-finally = [] + [copy] targets = [] diff --git a/tests/show-config.rs b/tests/show-config.rs index 536651322..968655000 100644 --- a/tests/show-config.rs +++ b/tests/show-config.rs @@ -22,9 +22,9 @@ use abscissa_core::testing::prelude::*; use rustic_testing::{files_differ, get_temp_file, TestResult}; // Storing this value as a [`Lazy`] static ensures that all instances of -/// the runner acquire a mutex when executing commands and inspecting -/// exit statuses, serializing what would otherwise be multithreaded -/// invocations as `cargo test` executes tests in parallel by default. +// the runner acquire a mutex when executing commands and inspecting +// exit statuses, serializing what would otherwise be multithreaded +// invocations as `cargo test` executes tests in parallel by default. pub static LAZY_RUNNER: Lazy = Lazy::new(|| { let mut runner = CmdRunner::new(env!("CARGO_BIN_EXE_rustic")); runner.exclusive().capture_stdout(); @@ -35,13 +35,15 @@ fn cmd_runner() -> CmdRunner { LAZY_RUNNER.clone() } -fn fixtures_dir() -> PathBuf { - ["tests", "show-config-fixtures"].iter().collect() +fn fixture() -> PathBuf { + ["tests", "show-config-fixtures", "empty.txt"] + .iter() + .collect() } #[test] -fn show_config_passes() -> TestResult<()> { - let fixture_path = fixtures_dir().join("empty.txt"); +fn test_show_config_passes() -> TestResult<()> { + let fixture_file = fixture(); let mut file = get_temp_file()?; { @@ -56,8 +58,8 @@ fn show_config_passes() -> TestResult<()> { cmd.wait()?.expect_success(); } - if files_differ(fixture_path, file.path())? { - panic!("generated completions for bash shell differ, breaking change!"); + if files_differ(fixture_file, file.path())? { + panic!("generated empty.txt differs, breaking change!"); } Ok(()) diff --git a/tests/snapshots/hooks__backup_hooks_access_success.snap b/tests/snapshots/hooks__backup_hooks_access_success.snap new file mode 100644 index 000000000..d27c136b2 --- /dev/null +++ b/tests/snapshots/hooks__backup_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning backup hooks before\nRunning backup hooks after\nRunning backup hooks finally\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__backup_hooks_failure.snap b/tests/snapshots/hooks__backup_hooks_failure.snap new file mode 100644 index 000000000..d07707eb0 --- /dev/null +++ b/tests/snapshots/hooks__backup_hooks_failure.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running backup hooks before\nRunning backup hooks failed\nRunning backup hooks finally\n" diff --git a/tests/snapshots/hooks__backup_hooks_success.snap b/tests/snapshots/hooks__backup_hooks_success.snap new file mode 100644 index 000000000..6241ce609 --- /dev/null +++ b/tests/snapshots/hooks__backup_hooks_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running backup hooks before\nRunning backup hooks after\nRunning backup hooks finally\n" diff --git a/tests/snapshots/hooks__cat_hooks_access_success.snap b/tests/snapshots/hooks__cat_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__cat_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__check_hooks_access_success.snap b/tests/snapshots/hooks__check_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__check_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__check_not_backup_hooks_success.snap b/tests/snapshots/hooks__check_not_backup_hooks_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__check_not_backup_hooks_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__completions_hooks_access_success.snap b/tests/snapshots/hooks__completions_hooks_access_success.snap new file mode 100644 index 000000000..98755ed10 --- /dev/null +++ b/tests/snapshots/hooks__completions_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__config_hooks_access_success.snap b/tests/snapshots/hooks__config_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__config_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__dump_hooks_access_success.snap b/tests/snapshots/hooks__dump_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__dump_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__find_hooks_access_success.snap b/tests/snapshots/hooks__find_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__find_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__forget_hooks_access_success.snap b/tests/snapshots/hooks__forget_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__forget_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__full_hooks_before_backup_failure.snap b/tests/snapshots/hooks__full_hooks_before_backup_failure.snap new file mode 100644 index 000000000..99130c8bd --- /dev/null +++ b/tests/snapshots/hooks__full_hooks_before_backup_failure.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning backup hooks before\nRunning backup hooks failed\nRunning backup hooks finally\nRunning repository hooks failed\nRunning repository hooks finally\nRunning global hooks failed\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__full_hooks_before_repo_failure.snap b/tests/snapshots/hooks__full_hooks_before_repo_failure.snap new file mode 100644 index 000000000..2a556c524 --- /dev/null +++ b/tests/snapshots/hooks__full_hooks_before_repo_failure.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks failed\nRunning repository hooks finally\nRunning global hooks failed\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__full_hooks_success.snap b/tests/snapshots/hooks__full_hooks_success.snap new file mode 100644 index 000000000..d27c136b2 --- /dev/null +++ b/tests/snapshots/hooks__full_hooks_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning backup hooks before\nRunning backup hooks after\nRunning backup hooks finally\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__global_hooks_success.snap b/tests/snapshots/hooks__global_hooks_success.snap new file mode 100644 index 000000000..98755ed10 --- /dev/null +++ b/tests/snapshots/hooks__global_hooks_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__list_hooks_access_success.snap b/tests/snapshots/hooks__list_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__list_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__ls_hooks_access_success.snap b/tests/snapshots/hooks__ls_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__ls_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__merge_hooks_access_success.snap b/tests/snapshots/hooks__merge_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__merge_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__prune_hooks_access_success.snap b/tests/snapshots/hooks__prune_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__prune_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__repair_hooks_access_success.snap b/tests/snapshots/hooks__repair_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__repair_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__repoinfo_hooks_access_success.snap b/tests/snapshots/hooks__repoinfo_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__repoinfo_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__repository_hooks_success.snap b/tests/snapshots/hooks__repository_hooks_success.snap new file mode 100644 index 000000000..3f15c4171 --- /dev/null +++ b/tests/snapshots/hooks__repository_hooks_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\n" diff --git a/tests/snapshots/hooks__restore_hooks_access_success.snap b/tests/snapshots/hooks__restore_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__restore_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__self-update_hooks_access_success.snap b/tests/snapshots/hooks__self-update_hooks_access_success.snap new file mode 100644 index 000000000..98755ed10 --- /dev/null +++ b/tests/snapshots/hooks__self-update_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__show-config_hooks_access_success.snap b/tests/snapshots/hooks__show-config_hooks_access_success.snap new file mode 100644 index 000000000..98755ed10 --- /dev/null +++ b/tests/snapshots/hooks__show-config_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__snapshots_hooks_access_success.snap b/tests/snapshots/hooks__snapshots_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__snapshots_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n" diff --git a/tests/snapshots/hooks__tag_hooks_access_success.snap b/tests/snapshots/hooks__tag_hooks_access_success.snap new file mode 100644 index 000000000..08af39767 --- /dev/null +++ b/tests/snapshots/hooks__tag_hooks_access_success.snap @@ -0,0 +1,5 @@ +--- +source: tests/hooks.rs +expression: log_live +--- +"Running global hooks before\nRunning repository hooks before\nRunning repository hooks after\nRunning repository hooks finally\nRunning global hooks after\nRunning global hooks finally\n"