From 21a0376b4ea4abc560dbba1f5e8f8563f2f2559b Mon Sep 17 00:00:00 2001 From: Alexander Weiss Date: Mon, 3 Jul 2023 10:14:20 +0200 Subject: [PATCH 1/4] refactor repository opening --- Cargo.lock | 1 - crates/rustic_core/Cargo.toml | 1 - crates/rustic_core/src/error.rs | 2 + crates/rustic_core/src/repository.rs | 68 ++++++++++++---------------- src/commands.rs | 46 +++++++++++-------- src/commands/backup.rs | 4 +- src/commands/cat.rs | 7 +-- src/commands/check.rs | 21 +++++---- src/commands/config.rs | 7 +-- src/commands/copy.rs | 4 +- src/commands/diff.rs | 7 +-- src/commands/dump.rs | 4 +- src/commands/forget.rs | 4 +- src/commands/init.rs | 16 +++---- src/commands/key.rs | 7 +-- src/commands/list.rs | 7 +-- src/commands/ls.rs | 7 +-- src/commands/merge.rs | 7 +-- src/commands/prune.rs | 6 +-- src/commands/repair.rs | 9 ++-- src/commands/repoinfo.rs | 18 +++++--- src/commands/restore.rs | 6 +-- src/commands/snapshots.rs | 4 +- src/commands/tag.rs | 7 +-- 24 files changed, 122 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dfd9e3d..4cb8a6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2402,7 +2402,6 @@ dependencies = [ "crossbeam-channel", "derivative", "derive_more", - "dialoguer", "directories", "dirs", "displaydoc", diff --git a/crates/rustic_core/Cargo.toml b/crates/rustic_core/Cargo.toml index 7305c3b..4aead8a 100644 --- a/crates/rustic_core/Cargo.toml +++ b/crates/rustic_core/Cargo.toml @@ -94,7 +94,6 @@ clap = { workspace = true, optional = true } clap_complete = { workspace = true, optional = true } merge = { workspace = true, optional = true } -dialoguer = "0.10.4" directories = { workspace = true } nom = { workspace = true } path-dedot = { workspace = true } diff --git a/crates/rustic_core/src/error.rs b/crates/rustic_core/src/error.rs index 13be3e1..6a79e1b 100644 --- a/crates/rustic_core/src/error.rs +++ b/crates/rustic_core/src/error.rs @@ -205,6 +205,8 @@ pub enum IdErrorKind { pub enum RepositoryErrorKind { /// No repository given. Please use the --repository option. NoRepositoryGiven, + /// No password given. Please use one of the --password-* options. + NoPasswordGiven, /// warm-up command must contain %id! NoIDSpecified, /// error opening password file `{0:?}` diff --git a/crates/rustic_core/src/repository.rs b/crates/rustic_core/src/repository.rs index 3e71552..5b402b6 100644 --- a/crates/rustic_core/src/repository.rs +++ b/crates/rustic_core/src/repository.rs @@ -9,7 +9,6 @@ use std::{ use bytes::Bytes; use log::{debug, error, info}; -use dialoguer::Password; use nom::{ branch::alt, bytes::complete::{is_not, tag}, @@ -35,17 +34,13 @@ use crate::{ repoinfo::{IndexInfos, RepoFileInfos}, }, crypto::aespoly1305::Key, - error::RepositoryErrorKind, + error::{KeyFileErrorKind, RepositoryErrorKind, RusticErrorKind}, repofile::{configfile::ConfigFile, keyfile::find_key_in_backend}, BlobType, DecryptFullBackend, Id, IndexBackend, IndexedBackend, NoProgressBars, Node, ProgressBars, PruneOpts, PrunePlan, RusticResult, SnapshotFile, SnapshotGroup, SnapshotGroupCriterion, Tree, }; -pub(super) mod constants { - pub(super) const MAX_PASSWORD_RETRIES: usize = 5; -} - mod warm_up; use warm_up::{warm_up, warm_up_wait}; @@ -176,7 +171,7 @@ pub fn read_password_from_reader(file: &mut impl BufRead) -> RusticResult { name: String, pub be: HotColdBackend, @@ -279,17 +274,31 @@ impl

Repository { } } - pub fn open(self) -> RusticResult> { + pub fn config_id(&self) -> RusticResult> { let config_ids = self .be .list(FileType::Config) .map_err(|_| RepositoryErrorKind::ListingRepositoryConfigFileFailed)?; match config_ids.len() { - 1 => {} // ok, continue - 0 => return Err(RepositoryErrorKind::NoRepositoryConfigFound(self.name).into()), - _ => return Err(RepositoryErrorKind::MoreThanOneRepositoryConfig(self.name).into()), + 1 => Ok(Some(config_ids[0])), + 0 => Ok(None), + _ => Err(RepositoryErrorKind::MoreThanOneRepositoryConfig(self.name.clone()).into()), } + } + pub fn open(self) -> RusticResult> { + let password = self + .password()? + .ok_or(RepositoryErrorKind::NoPasswordGiven)?; + self.open_with_password(&password) + } + + pub fn open_with_password(self, password: &str) -> RusticResult> { + let config_id = self + .config_id()? + .ok_or(RepositoryErrorKind::NoRepositoryConfigFound( + self.name.clone(), + ))?; if let Some(be_hot) = &self.be_hot { let mut keys = self.be.list_with_size(FileType::Key)?; @@ -301,11 +310,18 @@ impl

Repository { } } - let key = get_key(&self.be, self.password()?)?; + let key = find_key_in_backend(&self.be, &password, None).map_err(|err| { + match err.into_inner() { + RusticErrorKind::KeyFile(KeyFileErrorKind::NoSuitableKeyFound) => { + RepositoryErrorKind::IncorrectPassword.into() + } + err => err, + } + })?; info!("repository {}: password is correct.", self.name); let dbe = DecryptBackend::new(&self.be, key); - let config: ConfigFile = dbe.get_file(&config_ids[0])?; + let config: ConfigFile = dbe.get_file(&config_id)?; match (config.is_hot == Some(true), self.be_hot.is_some()) { (true, false) => return Err(RepositoryErrorKind::HotRepositoryFlagMissing.into()), (false, true) => return Err(RepositoryErrorKind::IsNotHotRepository.into()), @@ -354,32 +370,6 @@ impl Repository { } } -pub(crate) fn get_key(be: &impl ReadBackend, password: Option) -> RusticResult { - for _ in 0..constants::MAX_PASSWORD_RETRIES { - match password { - // if password is given, directly return the result of find_key_in_backend and don't retry - Some(pass) => { - return find_key_in_backend(be, &pass, None).map_err(std::convert::Into::into) - } - None => { - // TODO: Differentiate between wrong password and other error! - if let Ok(key) = find_key_in_backend( - be, - &Password::new() - .with_prompt("enter repository password") - .allow_empty_password(true) - .interact() - .map_err(RepositoryErrorKind::ReadingPasswordFromPromptFailed)?, - None, - ) { - return Ok(key); - } - } - } - } - Err(RepositoryErrorKind::IncorrectPassword.into()) -} - pub trait Open { type DBE: DecryptFullBackend; fn key(&self) -> &Key; diff --git a/src/commands.rs b/src/commands.rs index 26f0b04..8b75e4d 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -38,11 +38,15 @@ use crate::{ {Application, RUSTIC_APP}, }; -use abscissa_core::{ - config::Override, status_err, Command, Configurable, FrameworkError, Runnable, Shutdown, -}; +use abscissa_core::{config::Override, Command, Configurable, FrameworkError, Runnable, Shutdown}; +use anyhow::{anyhow, Result}; +use dialoguer::Password; use rustic_core::{OpenStatus, Repository}; +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)] @@ -167,25 +171,29 @@ impl Configurable for EntryPoint { } } -fn open_repository

(repo: Repository) -> Repository { - match repo.open() { - Ok(it) => it, - Err(err) => { - status_err!("{}", err); - RUSTIC_APP.shutdown(Shutdown::Crash); - } - } -} - -fn get_repository(config: &Arc) -> Repository { +fn open_repository(config: &Arc) -> Result> { let po = config.global.progress_options; - match Repository::new_with_progress(&config.repository, po) { - Ok(it) => it, - Err(err) => { - status_err!("{}", err); - RUSTIC_APP.shutdown(Shutdown::Crash); + let repo = Repository::new_with_progress(&config.repository, 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), + // TODO: fail if error != Password incorrect + Err(_) => continue, + } + } } } + Err(anyhow!("incorrect password")) } #[test] diff --git a/src/commands/backup.rs b/src/commands/backup.rs index 6a81ea0..ef11bfa 100644 --- a/src/commands/backup.rs +++ b/src/commands/backup.rs @@ -3,7 +3,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. use crate::{ - commands::{get_repository, open_repository}, + commands::open_repository, helpers::bytes_size_to_string, {status_err, Application, RUSTIC_APP}, }; @@ -153,7 +153,7 @@ impl BackupCmd { let config = RUSTIC_APP.config(); let progress_options = &config.global.progress_options; - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; // manually check for a "source" field, check is not done by serde, see above. if !config.backup.source.is_empty() { diff --git a/src/commands/cat.rs b/src/commands/cat.rs index d590df3..63c4579 100644 --- a/src/commands/cat.rs +++ b/src/commands/cat.rs @@ -2,10 +2,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. -use crate::{ - commands::{get_repository, open_repository}, - status_err, Application, RUSTIC_APP, -}; +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; @@ -61,7 +58,7 @@ impl Runnable for CatCmd { impl CatCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; let data = match &self.cmd { CatSubCmd::Config => repo.cat_file(FileType::Config, "")?, diff --git a/src/commands/check.rs b/src/commands/check.rs index 5cdd90c..3812148 100644 --- a/src/commands/check.rs +++ b/src/commands/check.rs @@ -2,13 +2,10 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. -use crate::{ - commands::{get_repository, open_repository}, - status_err, Application, RUSTIC_APP, -}; +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; - +use anyhow::Result; use rustic_core::CheckOpts; /// `check` subcommand @@ -20,12 +17,18 @@ pub(crate) struct CheckCmd { impl Runnable for CheckCmd { fn run(&self) { - let config = RUSTIC_APP.config(); - - let repo = open_repository(get_repository(&config)); - if let Err(err) = repo.check(self.opts) { + if let Err(err) = self.inner_run() { status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); }; } } + +impl CheckCmd { + fn inner_run(&self) -> Result<()> { + let config = RUSTIC_APP.config(); + let repo = open_repository(&config)?; + repo.check(self.opts)?; + Ok(()) + } +} diff --git a/src/commands/config.rs b/src/commands/config.rs index 3e13d00..ccb70b2 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -2,10 +2,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. -use crate::{ - commands::{get_repository, open_repository}, - status_err, Application, RUSTIC_APP, -}; +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; @@ -33,7 +30,7 @@ impl Runnable for ConfigCmd { impl ConfigCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; let mut new_config = repo.config().clone(); self.config_opts.apply(&mut new_config)?; diff --git a/src/commands/copy.rs b/src/commands/copy.rs index 32380ba..5a6dab9 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -3,7 +3,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. use crate::{ - commands::{get_repository, init::save_config, open_repository}, + commands::{init::save_config, open_repository}, helpers::copy, status_err, Application, RUSTIC_APP, }; @@ -54,7 +54,7 @@ impl CopyCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; if config.copy.targets.is_empty() { status_err!("no [[copy.targets]] section in config file found!"); diff --git a/src/commands/diff.rs b/src/commands/diff.rs index 0614718..b4be360 100644 --- a/src/commands/diff.rs +++ b/src/commands/diff.rs @@ -2,10 +2,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. -use crate::{ - commands::{get_repository, open_repository}, - status_err, Application, RUSTIC_APP, -}; +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; @@ -56,7 +53,7 @@ impl DiffCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; let be = repo.dbe(); let (id1, path1) = arg_to_snap_path(&self.snap1, ""); diff --git a/src/commands/dump.rs b/src/commands/dump.rs index 451dd91..f74fa5f 100644 --- a/src/commands/dump.rs +++ b/src/commands/dump.rs @@ -2,7 +2,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. -use crate::{commands::get_repository, status_err, Application, RUSTIC_APP}; +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; @@ -28,7 +28,7 @@ impl DumpCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = get_repository(&config).open()?.to_indexed()?; + let repo = open_repository(&config)?.to_indexed()?; let node = repo.node_from_snapshot_path(&self.snap, |sn| config.snapshot_filter.matches(sn))?; diff --git a/src/commands/forget.rs b/src/commands/forget.rs index 1313eef..1d99a99 100644 --- a/src/commands/forget.rs +++ b/src/commands/forget.rs @@ -3,7 +3,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. use crate::{ - commands::get_repository, helpers::table_with_titles, status_err, Application, RusticConfig, + commands::open_repository, helpers::table_with_titles, status_err, Application, RusticConfig, RUSTIC_APP, }; @@ -88,7 +88,7 @@ impl Runnable for ForgetCmd { impl ForgetCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = get_repository(&config).open()?; + let repo = open_repository(&config)?; let group_by = config.forget.group_by.unwrap_or_default(); diff --git a/src/commands/init.rs b/src/commands/init.rs index 2470034..7033c59 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -5,14 +5,14 @@ use abscissa_core::{status_err, Command, Runnable, Shutdown}; use anyhow::{bail, Result}; -use crate::{commands::get_repository, Application, RUSTIC_APP}; +use crate::{Application, RUSTIC_APP}; use bytes::Bytes; use dialoguer::Password; use rustic_core::{ hash, random_poly, ConfigFile, DecryptBackend, DecryptWriteBackend, FileType, Id, Key, KeyFile, - ReadBackend, WriteBackend, + ReadBackend, Repository, WriteBackend, }; use crate::commands::{config::ConfigOpts, key::KeyOpts}; @@ -40,7 +40,8 @@ impl InitCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = get_repository(&config); + let po = config.global.progress_options; + let repo = Repository::new_with_progress(&config.repository, po)?; let config_ids = repo.be.list(FileType::Config)?; @@ -79,8 +80,8 @@ pub(crate) fn save_config( // generate key let key = Key::new(); - let pass = password.map_or_else( - || match Password::new() + let pass = password.unwrap_or_else(|| { + match Password::new() .with_prompt("enter password for new key") .allow_empty_password(true) .with_confirmation("confirm password", "passwords do not match") @@ -91,9 +92,8 @@ pub(crate) fn save_config( status_err!("{}", err); RUSTIC_APP.shutdown(Shutdown::Crash); } - }, - |pass| pass, - ); + } + }); let keyfile = KeyFile::generate( key, diff --git a/src/commands/key.rs b/src/commands/key.rs index 132558c..d7b9a0c 100644 --- a/src/commands/key.rs +++ b/src/commands/key.rs @@ -2,10 +2,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. -use crate::{ - commands::{get_repository, open_repository}, - status_err, Application, RUSTIC_APP, -}; +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; @@ -73,7 +70,7 @@ impl AddCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; let be = repo.dbe(); let key = repo.key(); diff --git a/src/commands/list.rs b/src/commands/list.rs index eae7d51..798c96a 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -2,10 +2,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. -use crate::{ - commands::{get_repository, open_repository}, - status_err, Application, RUSTIC_APP, -}; +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; @@ -34,7 +31,7 @@ impl ListCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; let tpe = match self.tpe.as_str() { // special treatment for listing blobs: read the index and display it diff --git a/src/commands/ls.rs b/src/commands/ls.rs index d45deee..9714f1b 100644 --- a/src/commands/ls.rs +++ b/src/commands/ls.rs @@ -2,10 +2,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. -use crate::{ - commands::{get_repository, open_repository}, - status_err, Application, RUSTIC_APP, -}; +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; @@ -45,7 +42,7 @@ impl LsCmd { let config = RUSTIC_APP.config(); let progress_options = &config.global.progress_options; - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; let be = repo.dbe(); let mut recursive = self.recursive; diff --git a/src/commands/merge.rs b/src/commands/merge.rs index e2156fb..2a4c0de 100644 --- a/src/commands/merge.rs +++ b/src/commands/merge.rs @@ -2,10 +2,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. -use crate::{ - commands::{get_repository, open_repository}, - status_err, Application, RUSTIC_APP, -}; +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; use log::info; @@ -57,7 +54,7 @@ impl MergeCmd { let config = RUSTIC_APP.config(); let progress_options = &config.global.progress_options; - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; let be = repo.dbe(); diff --git a/src/commands/prune.rs b/src/commands/prune.rs index c58a659..2f3835b 100644 --- a/src/commands/prune.rs +++ b/src/commands/prune.rs @@ -3,9 +3,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. use crate::{ - commands::{get_repository, open_repository}, - helpers::bytes_size_to_string, - status_err, Application, RUSTIC_APP, + commands::open_repository, helpers::bytes_size_to_string, status_err, Application, RUSTIC_APP, }; use abscissa_core::{Command, Runnable, Shutdown}; use log::debug; @@ -35,7 +33,7 @@ impl Runnable for PruneCmd { impl PruneCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; let pruner = repo.prune_plan(&self.opts)?; diff --git a/src/commands/repair.rs b/src/commands/repair.rs index 775109d..62c8995 100644 --- a/src/commands/repair.rs +++ b/src/commands/repair.rs @@ -2,10 +2,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. -use crate::{ - commands::{get_repository, open_repository}, - status_err, Application, RUSTIC_APP, -}; +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use log::{debug, info, warn}; @@ -87,7 +84,7 @@ impl IndexSubCmd { let config = RUSTIC_APP.config(); let progress_options = &config.global.progress_options; - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; let be = repo.dbe(); let p = progress_options.progress_spinner("listing packs..."); @@ -235,7 +232,7 @@ impl SnapSubCmd { let config = RUSTIC_APP.config(); let progress_options = &config.global.progress_options; - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; let be = repo.dbe(); let config_file = repo.config(); diff --git a/src/commands/repoinfo.rs b/src/commands/repoinfo.rs index 18d715e..7d43f55 100644 --- a/src/commands/repoinfo.rs +++ b/src/commands/repoinfo.rs @@ -3,7 +3,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. use crate::{ - commands::get_repository, helpers::bytes_size_to_string, status_err, Application, RUSTIC_APP, + commands::open_repository, helpers::bytes_size_to_string, status_err, Application, RUSTIC_APP, }; use abscissa_core::{Command, Runnable, Shutdown}; @@ -11,7 +11,7 @@ use serde::Serialize; use crate::helpers::table_right_from; use anyhow::Result; -use rustic_core::{IndexInfos, RepoFileInfo, RepoFileInfos}; +use rustic_core::{IndexInfos, RepoFileInfo, RepoFileInfos, Repository}; /// `repoinfo` subcommand #[derive(clap::Parser, Command, Debug)] @@ -48,15 +48,19 @@ struct Infos { impl RepoInfoCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = get_repository(&config); let infos = Infos { - files: (!self.only_index).then(|| repo.infos_files()).transpose()?, + files: (!self.only_index) + .then(|| { + let po = config.global.progress_options; + let repo = Repository::new_with_progress(&config.repository, po)?; + repo.infos_files() + }) + .transpose()?, index: (!self.only_files) .then(|| -> Result<_> { - let repo = repo.open()?; - let info_index = repo.infos_index()?; - Ok(info_index) + let repo = open_repository(&config)?; + Ok(repo.infos_index()?) }) .transpose()?, }; diff --git a/src/commands/restore.rs b/src/commands/restore.rs index 3a9c750..5f9924b 100644 --- a/src/commands/restore.rs +++ b/src/commands/restore.rs @@ -3,9 +3,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. use crate::{ - commands::{get_repository, open_repository}, - helpers::bytes_size_to_string, - status_err, Application, RUSTIC_APP, + commands::open_repository, helpers::bytes_size_to_string, status_err, Application, RUSTIC_APP, }; use log::{debug, error, info, trace, warn}; @@ -90,7 +88,7 @@ impl RestoreCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); let progress_options = &config.global.progress_options; - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; let be = repo.dbe(); let (id, path) = self.snap.split_once(':').unwrap_or((&self.snap, "")); diff --git a/src/commands/snapshots.rs b/src/commands/snapshots.rs index f1c3aed..0971505 100644 --- a/src/commands/snapshots.rs +++ b/src/commands/snapshots.rs @@ -3,7 +3,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. use crate::{ - commands::get_repository, + commands::open_repository, helpers::{bold_cell, bytes_size_to_string, table, table_right_from}, status_err, Application, RUSTIC_APP, }; @@ -56,7 +56,7 @@ impl Runnable for SnapshotCmd { impl SnapshotCmd { fn inner_run(&self) -> Result<()> { let config = RUSTIC_APP.config(); - let repo = get_repository(&config).open()?; + let repo = open_repository(&config)?; 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 1836884..93e421b 100644 --- a/src/commands/tag.rs +++ b/src/commands/tag.rs @@ -2,10 +2,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. -use crate::{ - commands::{get_repository, open_repository}, - status_err, Application, RUSTIC_APP, -}; +use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; @@ -79,7 +76,7 @@ impl Runnable for TagCmd { impl TagCmd { fn inner_run(&self) -> anyhow::Result<()> { let config = RUSTIC_APP.config(); - let repo = open_repository(get_repository(&config)); + let repo = open_repository(&config)?; let be = repo.dbe(); From e33442384848dc70039937d933ee7bf4ff896a00 Mon Sep 17 00:00:00 2001 From: Alexander Weiss Date: Tue, 4 Jul 2023 06:14:25 +0200 Subject: [PATCH 2/4] move key command to rustic_core --- crates/rustic_core/examples/key.rs | 21 +++++++ crates/rustic_core/src/commands.rs | 1 + crates/rustic_core/src/commands/key.rs | 53 ++++++++++++++++ crates/rustic_core/src/error.rs | 2 + crates/rustic_core/src/lib.rs | 1 + crates/rustic_core/src/repository.rs | 22 +++++-- src/commands/copy.rs | 10 ++-- src/commands/init.rs | 45 ++++---------- src/commands/key.rs | 83 ++++++++------------------ 9 files changed, 136 insertions(+), 102 deletions(-) create mode 100644 crates/rustic_core/examples/key.rs create mode 100644 crates/rustic_core/src/commands/key.rs diff --git a/crates/rustic_core/examples/key.rs b/crates/rustic_core/examples/key.rs new file mode 100644 index 0000000..55c528e --- /dev/null +++ b/crates/rustic_core/examples/key.rs @@ -0,0 +1,21 @@ +//! `key` example +use rustic_core::{KeyOpts, Repository, RepositoryOptions}; +use simplelog::{Config, LevelFilter, SimpleLogger}; + +fn main() { + // Display info logs + let _ = SimpleLogger::init(LevelFilter::Info, Config::default()); + + // Open repository + let repo_opts = RepositoryOptions { + repository: Some("/tmp/repo".to_string()), + password: Some("test".to_string()), + ..Default::default() + }; + + let repo = Repository::new(&repo_opts).unwrap().open().unwrap(); + + // Add a new key with the given password + let key_opts = KeyOpts::default(); + repo.add_key("new_password", &key_opts).unwrap(); +} diff --git a/crates/rustic_core/src/commands.rs b/crates/rustic_core/src/commands.rs index f450b03..d4a7aae 100644 --- a/crates/rustic_core/src/commands.rs +++ b/crates/rustic_core/src/commands.rs @@ -2,6 +2,7 @@ pub mod cat; pub mod check; pub mod dump; pub mod forget; +pub mod key; pub mod prune; pub mod repoinfo; pub mod snapshots; diff --git a/crates/rustic_core/src/commands/key.rs b/crates/rustic_core/src/commands/key.rs new file mode 100644 index 0000000..cbc72b7 --- /dev/null +++ b/crates/rustic_core/src/commands/key.rs @@ -0,0 +1,53 @@ +//! `key` subcommand +use crate::{ + error::CommandErrorKind, hash, FileType, Id, Key, KeyFile, Open, Repository, RusticResult, + WriteBackend, +}; + +#[cfg_attr(feature = "clap", derive(clap::Parser))] +#[derive(Debug, Clone, Default)] +pub struct KeyOpts { + /// Set 'hostname' in public key information + #[cfg_attr(feature = "clap", clap(long))] + pub hostname: Option, + + /// Set 'username' in public key information + #[cfg_attr(feature = "clap", clap(long))] + pub username: Option, + + /// Add 'created' date in public key information + #[cfg_attr(feature = "clap", clap(long))] + pub with_created: bool, +} + +impl KeyOpts { + pub(crate) fn add_key( + &self, + repo: &Repository, + pass: &str, + ) -> RusticResult { + let key = repo.key(); + self.add(repo, pass, *key) + } + + pub(crate) fn init_key( + &self, + repo: &Repository, + pass: &str, + ) -> RusticResult<(Key, Id)> { + // generate key + let key = Key::new(); + Ok((key, self.add(repo, pass, key)?)) + } + + fn add(&self, repo: &Repository, pass: &str, key: Key) -> RusticResult { + let ko = self.clone(); + let keyfile = KeyFile::generate(key, &pass, ko.hostname, ko.username, ko.with_created)?; + + let data = serde_json::to_vec(&keyfile).map_err(CommandErrorKind::FromJsonError)?; + let id = hash(&data); + repo.be + .write_bytes(FileType::Key, &id, false, data.into())?; + Ok(id) + } +} diff --git a/crates/rustic_core/src/error.rs b/crates/rustic_core/src/error.rs index 6a79e1b..d75e50c 100644 --- a/crates/rustic_core/src/error.rs +++ b/crates/rustic_core/src/error.rs @@ -162,6 +162,8 @@ pub enum CommandErrorKind { FromOutOfRangeError(#[from] OutOfRangeError), /// node type {0:?} not supported by dump DumpNotSupported(NodeType), + /// {0:?} + FromJsonError(#[from] serde_json::Error), } /// [`CryptoErrorKind`] describes the errors that can happen while dealing with Cryptographic functions diff --git a/crates/rustic_core/src/lib.rs b/crates/rustic_core/src/lib.rs index a876747..b73bcf7 100644 --- a/crates/rustic_core/src/lib.rs +++ b/crates/rustic_core/src/lib.rs @@ -123,6 +123,7 @@ pub use crate::{ commands::{ check::CheckOpts, forget::{ForgetGroup, ForgetGroups, ForgetSnapshot, KeepOptions}, + key::KeyOpts, prune::{PruneOpts, PrunePlan, PruneStats}, repoinfo::{BlobInfo, IndexInfos, PackInfo, RepoFileInfo, RepoFileInfos}, }, diff --git a/crates/rustic_core/src/repository.rs b/crates/rustic_core/src/repository.rs index 5b402b6..59879c3 100644 --- a/crates/rustic_core/src/repository.rs +++ b/crates/rustic_core/src/repository.rs @@ -31,6 +31,7 @@ use crate::{ self, check::CheckOpts, forget::{ForgetGroups, KeepOptions}, + key::KeyOpts, repoinfo::{IndexInfos, RepoFileInfos}, }, crypto::aespoly1305::Key, @@ -226,7 +227,8 @@ impl

Repository { status: (), }) } - +} +impl Repository { pub fn password(&self) -> RusticResult> { match ( &self.opts.password, @@ -355,6 +357,10 @@ impl

Repository { status: open, }) } + + pub fn init_key(&self, pass: &str, opts: &KeyOpts) -> RusticResult<(Key, Id)> { + opts.init_key(self, pass) + } } impl Repository { @@ -419,6 +425,16 @@ impl Open for OpenStatus { } } +impl Repository { + pub fn cat_file(&self, tpe: FileType, id: &str) -> RusticResult { + commands::cat::cat_file(self, tpe, id) + } + + pub fn add_key(&self, pass: &str, opts: &KeyOpts) -> RusticResult { + opts.add_key(self, pass) + } +} + impl Repository { pub fn get_snapshot_group( &self, @@ -450,10 +466,6 @@ impl Repository { Ok(()) } - pub fn cat_file(&self, tpe: FileType, id: &str) -> RusticResult { - commands::cat::cat_file(self, tpe, id) - } - pub fn check(&self, opts: CheckOpts) -> RusticResult<()> { opts.run(self) } diff --git a/src/commands/copy.rs b/src/commands/copy.rs index 5a6dab9..6ffd7fc 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -14,10 +14,9 @@ use log::info; use merge::Merge; use serde::Deserialize; -use crate::commands::key::KeyOpts; use rustic_core::{ - FileType, Id, IndexBackend, Open, ProgressBars, ReadBackend, Repository, RepositoryOptions, - SnapshotFile, + FileType, Id, IndexBackend, KeyOpts, Open, ProgressBars, ReadBackend, Repository, + RepositoryOptions, SnapshotFile, }; /// `copy` subcommand @@ -83,9 +82,8 @@ impl CopyCmd { config_dest.id = Id::random(); save_config( config_dest, - &repo_dest.be, - &repo_dest.be_hot, - self.key_opts.clone(), + &repo_dest, + &self.key_opts, repo_dest.password()?, )?; } diff --git a/src/commands/init.rs b/src/commands/init.rs index 7033c59..20d6bef 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -7,15 +7,14 @@ use anyhow::{bail, Result}; use crate::{Application, RUSTIC_APP}; -use bytes::Bytes; use dialoguer::Password; use rustic_core::{ - hash, random_poly, ConfigFile, DecryptBackend, DecryptWriteBackend, FileType, Id, Key, KeyFile, + random_poly, ConfigFile, DecryptBackend, DecryptWriteBackend, FileType, Id, KeyOpts, ReadBackend, Repository, WriteBackend, }; -use crate::commands::{config::ConfigOpts, key::KeyOpts}; +use crate::commands::config::ConfigOpts; /// `init` subcommand #[derive(clap::Parser, Command, Debug)] @@ -47,9 +46,6 @@ impl InitCmd { let password = repo.password()?; - let be = &repo.be; - let hot_be = &repo.be_hot; - if !config_ids.is_empty() { bail!("Config file already exists. Aborting."); } @@ -57,29 +53,21 @@ impl InitCmd { // Create config first to allow catching errors from here without writing anything let repo_id = Id::random(); let chunker_poly = random_poly()?; - let version = match self.config_opts.set_version { - None => 2, - Some(_) => 1, // will be changed later - }; - let mut config = ConfigFile::new(version, repo_id, chunker_poly); + let mut config = ConfigFile::new(2, repo_id, chunker_poly); self.config_opts.apply(&mut config)?; - save_config(config, be, hot_be, self.key_opts.clone(), password)?; + save_config(config, &repo, &self.key_opts, password)?; Ok(()) } } -pub(crate) fn save_config( +pub(crate) fn save_config( mut config: ConfigFile, - be: &impl WriteBackend, - hot_be: &Option, - key_opts: KeyOpts, + repo: &Repository, + key_opts: &KeyOpts, password: Option, ) -> Result<()> { - // generate key - let key = Key::new(); - let pass = password.unwrap_or_else(|| { match Password::new() .with_prompt("enter password for new key") @@ -95,26 +83,17 @@ pub(crate) fn save_config( } }); - let keyfile = KeyFile::generate( - key, - &pass, - key_opts.hostname, - key_opts.username, - key_opts.with_created, - )?; - let data: Bytes = serde_json::to_vec(&keyfile)?.into(); - let id = hash(&data); - be.create()?; - be.write_bytes(FileType::Key, &id, false, data)?; + repo.be.create()?; + let (key, id) = repo.init_key(&pass, key_opts)?; println!("key {id} successfully added."); // save config - let dbe = DecryptBackend::new(be, key); + let dbe = DecryptBackend::new(&repo.be, key); config.is_hot = None; _ = dbe.save_file(&config)?; - if let Some(hot_be) = hot_be { - let dbe = DecryptBackend::new(hot_be, key); + if let Some(be_hot) = &repo.be_hot { + let dbe = DecryptBackend::new(be_hot, key); config.is_hot = Some(true); _ = dbe.save_file(&config)?; } diff --git a/src/commands/key.rs b/src/commands/key.rs index d7b9a0c..62ebc53 100644 --- a/src/commands/key.rs +++ b/src/commands/key.rs @@ -4,14 +4,14 @@ /// accessors along with logging macros. Customize as you see fit. use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; +use std::path::PathBuf; + use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::Result; - -use std::{fs::File, io::BufReader}; - use dialoguer::Password; +use log::info; -use rustic_core::{hash, read_password_from_reader, FileType, KeyFile, Open, WriteBackend}; +use rustic_core::{KeyOpts, Repository, RepositoryOptions}; /// `key` subcommand #[derive(clap::Parser, Command, Debug)] @@ -30,27 +30,12 @@ enum KeySubCmd { pub(crate) struct AddCmd { /// File from which to read the new password #[clap(long)] - pub(crate) new_password_file: Option, + pub(crate) new_password_file: Option, #[clap(flatten)] pub(crate) key_opts: KeyOpts, } -#[derive(clap::Parser, Debug, Clone)] -pub(crate) struct KeyOpts { - /// Set 'hostname' in public key information - #[clap(long)] - pub(crate) hostname: Option, - - /// Set 'username' in public key information - #[clap(long)] - pub(crate) username: Option, - - /// Add 'created' date in public key information - #[clap(long)] - pub(crate) with_created: bool, -} - impl Runnable for KeyCmd { fn run(&self) { self.cmd.run(); @@ -72,46 +57,28 @@ impl AddCmd { let repo = open_repository(&config)?; - let be = repo.dbe(); - let key = repo.key(); + // create new "artificial" repo using the given password options + let repo_opts = RepositoryOptions { + password_file: self.new_password_file.clone(), + repository: Some(String::new()), // fake repository to make Repository::new() not bail + ..Default::default() + }; + let repo_newpass = Repository::new(&repo_opts)?; - let pass = self.new_password_file.as_ref().map_or_else( - || match Password::new() - .with_prompt("enter password for new key") - .allow_empty_password(true) - .with_confirmation("confirm password", "passwords do not match") - .interact() - { - Ok(it) => it, - Err(err) => { - status_err!("{}", err); - RUSTIC_APP.shutdown(Shutdown::Crash); - } - }, - |file| { - let mut file = BufReader::new(match File::open(file) { - Ok(it) => it, - Err(err) => { - status_err!("{}", err); - RUSTIC_APP.shutdown(Shutdown::Crash); - } - }); - match read_password_from_reader(&mut file) { - Ok(it) => it, - Err(err) => { - status_err!("{}", err); - RUSTIC_APP.shutdown(Shutdown::Crash); - } - } - }, - ); - let ko = self.key_opts.clone(); - let keyfile = KeyFile::generate(*key, &pass, ko.hostname, ko.username, ko.with_created)?; - let data = serde_json::to_vec(&keyfile)?; - let id = hash(&data); - be.write_bytes(FileType::Key, &id, false, data.into())?; + let pass = repo_newpass + .password() + .map_err(|err| err.into()) + .transpose() + .unwrap_or_else(|| -> Result<_> { + Ok(Password::new() + .with_prompt("enter password for new key") + .allow_empty_password(true) + .with_confirmation("confirm password", "passwords do not match") + .interact()?) + })?; - println!("key {id} successfully added."); + let id = repo.add_key(&pass, &self.key_opts)?; + info!("key {id} successfully added."); Ok(()) } From b79254a90eda453dcd40d699f7e15a81fd7f1764 Mon Sep 17 00:00:00 2001 From: Alexander Weiss Date: Wed, 5 Jul 2023 16:12:23 +0200 Subject: [PATCH 3/4] move config command to rustic_core --- crates/rustic_core/examples/config.rs | 24 +++ crates/rustic_core/src/commands.rs | 1 + crates/rustic_core/src/commands/config.rs | 184 ++++++++++++++++++++++ crates/rustic_core/src/error.rs | 15 ++ crates/rustic_core/src/lib.rs | 1 + crates/rustic_core/src/repository.rs | 5 + src/commands/config.rs | 162 +------------------ src/commands/init.rs | 6 +- 8 files changed, 238 insertions(+), 160 deletions(-) create mode 100644 crates/rustic_core/examples/config.rs create mode 100644 crates/rustic_core/src/commands/config.rs diff --git a/crates/rustic_core/examples/config.rs b/crates/rustic_core/examples/config.rs new file mode 100644 index 0000000..84333f6 --- /dev/null +++ b/crates/rustic_core/examples/config.rs @@ -0,0 +1,24 @@ +//! `config` example +use rustic_core::{ConfigOpts, Repository, RepositoryOptions}; +use simplelog::{Config, LevelFilter, SimpleLogger}; + +fn main() { + // Display info logs + let _ = SimpleLogger::init(LevelFilter::Info, Config::default()); + + // Open repository + let repo_opts = RepositoryOptions { + repository: Some("/tmp/repo".to_string()), + password: Some("test".to_string()), + ..Default::default() + }; + + let repo = Repository::new(&repo_opts).unwrap().open().unwrap(); + + // Set Config, e.g. Compression level + let config_opts = ConfigOpts { + set_compression: Some(22), + ..Default::default() + }; + repo.apply_config(&config_opts).unwrap(); +} diff --git a/crates/rustic_core/src/commands.rs b/crates/rustic_core/src/commands.rs index d4a7aae..1a5064b 100644 --- a/crates/rustic_core/src/commands.rs +++ b/crates/rustic_core/src/commands.rs @@ -1,5 +1,6 @@ pub mod cat; pub mod check; +pub mod config; pub mod dump; pub mod forget; pub mod key; diff --git a/crates/rustic_core/src/commands/config.rs b/crates/rustic_core/src/commands/config.rs new file mode 100644 index 0000000..b4edf6e --- /dev/null +++ b/crates/rustic_core/src/commands/config.rs @@ -0,0 +1,184 @@ +//! `config` subcommand +use bytesize::ByteSize; + +use crate::{ + error::CommandErrorKind, ConfigFile, DecryptBackend, DecryptWriteBackend, Open, Repository, + RusticResult, +}; + +pub(crate) fn apply_config( + repo: &Repository, + opts: &ConfigOpts, +) -> RusticResult { + let mut new_config = repo.config().clone(); + opts.apply(&mut new_config)?; + if &new_config == repo.config() { + Ok(false) + } else { + save_config(repo, new_config)?; + Ok(true) + } +} + +fn save_config( + repo: &Repository, + mut new_config: ConfigFile, +) -> RusticResult<()> { + new_config.is_hot = None; + // don't compress the config file + let mut dbe = repo.dbe().clone(); + dbe.set_zstd(None); + // for hot/cold backend, this only saves the config to the cold repo. + _ = dbe.save_file(&new_config)?; + + if let Some(hot_be) = repo.be_hot.clone() { + // save config to hot repo + let mut dbe = DecryptBackend::new(&hot_be, *repo.key()); + // don't compress the config file + dbe.set_zstd(None); + new_config.is_hot = Some(true); + _ = dbe.save_file(&new_config)?; + } + Ok(()) +} + +#[cfg_attr(feature = "clap", derive(clap::Parser))] +#[derive(Debug, Clone, Copy, Default)] +pub struct ConfigOpts { + /// Set compression level. Allowed levels are 1 to 22 and -1 to -7, see . + /// Note that 0 equals to no compression + #[cfg_attr(feature = "clap", clap(long, value_name = "LEVEL"))] + pub set_compression: Option, + + /// Set repository version. Allowed versions: 1,2 + #[cfg_attr(feature = "clap", clap(long, value_name = "VERSION"))] + pub set_version: Option, + + /// Set default packsize for tree packs. rustic tries to always produce packs greater than this value. + /// Note that for large repos, this value is grown by the grown factor. + /// Defaults to 4 MiB if not set. + #[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))] + pub set_treepack_size: Option, + + /// Set upper limit for default packsize for tree packs. + /// Note that packs actually can get up to some MiBs larger. + /// If not set, pack sizes can grow up to approximately 4 GiB. + #[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))] + pub set_treepack_size_limit: Option, + + /// Set grow factor for tree packs. The default packsize grows by the square root of the total size of all + /// tree packs multiplied with this factor. This means 32 kiB times this factor per square root of total + /// treesize in GiB. + /// Defaults to 32 (= 1MB per square root of total treesize in GiB) if not set. + #[cfg_attr(feature = "clap", clap(long, value_name = "FACTOR"))] + pub set_treepack_growfactor: Option, + + /// Set default packsize for data packs. rustic tries to always produce packs greater than this value. + /// Note that for large repos, this value is grown by the grown factor. + /// Defaults to 32 MiB if not set. + #[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))] + pub set_datapack_size: Option, + + /// Set grow factor for data packs. The default packsize grows by the square root of the total size of all + /// data packs multiplied with this factor. This means 32 kiB times this factor per square root of total + /// datasize in GiB. + /// Defaults to 32 (= 1MB per square root of total datasize in GiB) if not set. + #[cfg_attr(feature = "clap", clap(long, value_name = "FACTOR"))] + pub set_datapack_growfactor: Option, + + /// Set upper limit for default packsize for tree packs. + /// Note that packs actually can get up to some MiBs larger. + /// If not set, pack sizes can grow up to approximately 4 GiB. + #[cfg_attr(feature = "clap", clap(long, value_name = "SIZE"))] + pub set_datapack_size_limit: Option, + + /// Set minimum tolerated packsize in percent of the targeted packsize. + /// Defaults to 30 if not set. + #[cfg_attr(feature = "clap", clap(long, value_name = "PERCENT"))] + pub set_min_packsize_tolerate_percent: Option, + + /// Set maximum tolerated packsize in percent of the targeted packsize + /// A value of 0 means packs larger than the targeted packsize are always + /// tolerated. Default if not set: larger packfiles are always tolerated. + #[cfg_attr(feature = "clap", clap(long, value_name = "PERCENT"))] + pub set_max_packsize_tolerate_percent: Option, +} + +impl ConfigOpts { + pub fn apply(&self, config: &mut ConfigFile) -> RusticResult<()> { + if let Some(version) = self.set_version { + let range = 1..=2; + if !range.contains(&version) { + return Err(CommandErrorKind::VersionNotSupported(version, range).into()); + } else if version < config.version { + return Err(CommandErrorKind::CannotDowngrade(config.version, version).into()); + } + config.version = version; + } + + if let Some(compression) = self.set_compression { + if config.version == 1 && compression != 0 { + return Err(CommandErrorKind::NoCompressionV1Repo(compression).into()); + } + let range = zstd::compression_level_range(); + if !range.contains(&compression) { + return Err( + CommandErrorKind::CompressionLevelNotSupported(compression, range).into(), + ); + } + config.compression = Some(compression); + } + + if let Some(size) = self.set_treepack_size { + config.treepack_size = Some( + size.as_u64() + .try_into() + .map_err(|_| CommandErrorKind::SizeTooLarge(size))?, + ); + } + if let Some(factor) = self.set_treepack_growfactor { + config.treepack_growfactor = Some(factor); + } + if let Some(size) = self.set_treepack_size_limit { + config.treepack_size_limit = Some( + size.as_u64() + .try_into() + .map_err(|_| CommandErrorKind::SizeTooLarge(size))?, + ); + } + + if let Some(size) = self.set_datapack_size { + config.datapack_size = Some( + size.as_u64() + .try_into() + .map_err(|_| CommandErrorKind::SizeTooLarge(size))?, + ); + } + if let Some(factor) = self.set_datapack_growfactor { + config.datapack_growfactor = Some(factor); + } + if let Some(size) = self.set_datapack_size_limit { + config.datapack_size_limit = Some( + size.as_u64() + .try_into() + .map_err(|_| CommandErrorKind::SizeTooLarge(size))?, + ); + } + + if let Some(percent) = self.set_min_packsize_tolerate_percent { + if percent > 100 { + return Err(CommandErrorKind::MinPackSizeTolerateWrong.into()); + } + config.min_packsize_tolerate_percent = Some(percent); + } + + if let Some(percent) = self.set_max_packsize_tolerate_percent { + if percent < 100 && percent > 0 { + return Err(CommandErrorKind::MaxPackSizeTolerateWrong.into()); + } + config.max_packsize_tolerate_percent = Some(percent); + } + + Ok(()) + } +} diff --git a/crates/rustic_core/src/error.rs b/crates/rustic_core/src/error.rs index d75e50c..13f4f5d 100644 --- a/crates/rustic_core/src/error.rs +++ b/crates/rustic_core/src/error.rs @@ -7,6 +7,7 @@ use std::{ error::Error, ffi::OsString, num::{ParseIntError, TryFromIntError}, + ops::RangeInclusive, path::{PathBuf, StripPrefixError}, process::ExitStatus, str::Utf8Error, @@ -164,6 +165,20 @@ pub enum CommandErrorKind { DumpNotSupported(NodeType), /// {0:?} FromJsonError(#[from] serde_json::Error), + /// version {0} is not supported. Allowed values: {1:?} + VersionNotSupported(u32, RangeInclusive), + /// cannot downgrade version from {0} to {1} + CannotDowngrade(u32, u32), + /// compression level {0} is not supported for repo v1 + NoCompressionV1Repo(i32), + /// compression level {0} is not supported. Allowed values: {1:?} + CompressionLevelNotSupported(i32, RangeInclusive), + /// Size is too large: {0} + SizeTooLarge(bytesize::ByteSize), + /// min_packsize_tolerate_percent must be <= 100 + MinPackSizeTolerateWrong, + /// max_packsize_tolerate_percent must be >= 100 or 0" + MaxPackSizeTolerateWrong, } /// [`CryptoErrorKind`] describes the errors that can happen while dealing with Cryptographic functions diff --git a/crates/rustic_core/src/lib.rs b/crates/rustic_core/src/lib.rs index b73bcf7..51b9d3a 100644 --- a/crates/rustic_core/src/lib.rs +++ b/crates/rustic_core/src/lib.rs @@ -122,6 +122,7 @@ pub use crate::{ chunker::random_poly, commands::{ check::CheckOpts, + config::ConfigOpts, forget::{ForgetGroup, ForgetGroups, ForgetSnapshot, KeepOptions}, key::KeyOpts, prune::{PruneOpts, PrunePlan, PruneStats}, diff --git a/crates/rustic_core/src/repository.rs b/crates/rustic_core/src/repository.rs index 59879c3..fbda08e 100644 --- a/crates/rustic_core/src/repository.rs +++ b/crates/rustic_core/src/repository.rs @@ -30,6 +30,7 @@ use crate::{ commands::{ self, check::CheckOpts, + config::ConfigOpts, forget::{ForgetGroups, KeepOptions}, key::KeyOpts, repoinfo::{IndexInfos, RepoFileInfos}, @@ -433,6 +434,10 @@ impl Repository { pub fn add_key(&self, pass: &str, opts: &KeyOpts) -> RusticResult { opts.add_key(self, pass) } + + pub fn apply_config(&self, opts: &ConfigOpts) -> RusticResult { + commands::config::apply_config(self, opts) + } } impl Repository { diff --git a/src/commands/config.rs b/src/commands/config.rs index ccb70b2..7d882dd 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -6,10 +6,9 @@ use crate::{commands::open_repository, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; -use anyhow::{bail, Result}; -use bytesize::ByteSize; +use anyhow::Result; -use rustic_core::{ConfigFile, DecryptBackend, DecryptWriteBackend, Open}; +use rustic_core::ConfigOpts; /// `config` subcommand #[derive(clap::Parser, Command, Debug)] @@ -32,161 +31,12 @@ impl ConfigCmd { let config = RUSTIC_APP.config(); let repo = open_repository(&config)?; - let mut new_config = repo.config().clone(); - self.config_opts.apply(&mut new_config)?; - - if &new_config == repo.config() { - println!("config is unchanged"); - } else { - new_config.is_hot = None; - // don't compress the config file - let mut dbe = repo.dbe().clone(); - dbe.set_zstd(None); - // for hot/cold backend, this only saves the config to the cold repo. - _ = dbe.save_file(&new_config)?; - - if let Some(hot_be) = repo.be_hot.clone() { - // save config to hot repo - let mut dbe = DecryptBackend::new(&hot_be, *repo.key()); - // don't compress the config file - dbe.set_zstd(None); - new_config.is_hot = Some(true); - _ = dbe.save_file(&new_config)?; - } + let changed = repo.apply_config(&self.config_opts)?; + if changed { println!("saved new config"); - } - - Ok(()) - } -} - -#[derive(clap::Parser, Debug)] -pub(crate) struct ConfigOpts { - /// Set compression level. Allowed levels are 1 to 22 and -1 to -7, see . - /// Note that 0 equals to no compression - #[clap(long, value_name = "LEVEL")] - pub(crate) set_compression: Option, - - /// Set repository version. Allowed versions: 1,2 - #[clap(long, value_name = "VERSION")] - pub(crate) set_version: Option, - - /// Set default packsize for tree packs. rustic tries to always produce packs greater than this value. - /// Note that for large repos, this value is grown by the grown factor. - /// Defaults to 4 MiB if not set. - #[clap(long, value_name = "SIZE")] - pub(crate) set_treepack_size: Option, - - /// Set upper limit for default packsize for tree packs. - /// Note that packs actually can get up to some MiBs larger. - /// If not set, pack sizes can grow up to approximately 4 GiB. - #[clap(long, value_name = "SIZE")] - pub(crate) set_treepack_size_limit: Option, - - /// Set grow factor for tree packs. The default packsize grows by the square root of the total size of all - /// tree packs multiplied with this factor. This means 32 kiB times this factor per square root of total - /// treesize in GiB. - /// Defaults to 32 (= 1MB per square root of total treesize in GiB) if not set. - #[clap(long, value_name = "FACTOR")] - pub(crate) set_treepack_growfactor: Option, - - /// Set default packsize for data packs. rustic tries to always produce packs greater than this value. - /// Note that for large repos, this value is grown by the grown factor. - /// Defaults to 32 MiB if not set. - #[clap(long, value_name = "SIZE")] - pub(crate) set_datapack_size: Option, - - /// Set grow factor for data packs. The default packsize grows by the square root of the total size of all - /// data packs multiplied with this factor. This means 32 kiB times this factor per square root of total - /// datasize in GiB. - /// Defaults to 32 (= 1MB per square root of total datasize in GiB) if not set. - #[clap(long, value_name = "FACTOR")] - pub(crate) set_datapack_growfactor: Option, - - /// Set upper limit for default packsize for tree packs. - /// Note that packs actually can get up to some MiBs larger. - /// If not set, pack sizes can grow up to approximately 4 GiB. - #[clap(long, value_name = "SIZE")] - pub(crate) set_datapack_size_limit: Option, - - /// Set minimum tolerated packsize in percent of the targeted packsize. - /// Defaults to 30 if not set. - #[clap(long, value_name = "PERCENT")] - pub(crate) set_min_packsize_tolerate_percent: Option, - - /// Set maximum tolerated packsize in percent of the targeted packsize - /// A value of 0 means packs larger than the targeted packsize are always - /// tolerated. Default if not set: larger packfiles are always tolerated. - #[clap(long, value_name = "PERCENT")] - pub(crate) set_max_packsize_tolerate_percent: Option, -} - -impl ConfigOpts { - pub(crate) fn apply(&self, config: &mut ConfigFile) -> Result<()> { - if let Some(version) = self.set_version { - let range = 1..=2; - if !range.contains(&version) { - bail!( - "version {version} is not supported. Allowed values: {}..{}", - range.start(), - range.end() - ); - } else if version < config.version { - bail!( - "cannot downgrade version from {} to {version}", - config.version - ); - } - config.version = version; - } - - if let Some(compression) = self.set_compression { - if config.version == 1 && compression != 0 { - bail!("compression level {compression} is not supported for repo v1"); - } - let range = zstd::compression_level_range(); - if !range.contains(&compression) { - bail!( - "compression level {compression} is not supported. Allowed values: 0..{}", - range.end() - ); - } - config.compression = Some(compression); - } - - if let Some(size) = self.set_treepack_size { - config.treepack_size = Some(size.as_u64().try_into()?); - } - if let Some(factor) = self.set_treepack_growfactor { - config.treepack_growfactor = Some(factor); - } - if let Some(size) = self.set_treepack_size_limit { - config.treepack_size_limit = Some(size.as_u64().try_into()?); - } - - if let Some(size) = self.set_datapack_size { - config.datapack_size = Some(size.as_u64().try_into()?); - } - if let Some(factor) = self.set_datapack_growfactor { - config.datapack_growfactor = Some(factor); - } - if let Some(size) = self.set_datapack_size_limit { - config.datapack_size_limit = Some(size.as_u64().try_into()?); - } - - if let Some(percent) = self.set_min_packsize_tolerate_percent { - if percent > 100 { - bail!("set_min_packsize_tolerate_percent must be <= 100"); - } - config.min_packsize_tolerate_percent = Some(percent); - } - - if let Some(percent) = self.set_max_packsize_tolerate_percent { - if percent < 100 && percent > 0 { - bail!("set_max_packsize_tolerate_percent must be >= 100 or 0"); - } - config.max_packsize_tolerate_percent = Some(percent); + } else { + println!("config is unchanged"); } Ok(()) diff --git a/src/commands/init.rs b/src/commands/init.rs index 20d6bef..bc391ed 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -10,12 +10,10 @@ use crate::{Application, RUSTIC_APP}; use dialoguer::Password; use rustic_core::{ - random_poly, ConfigFile, DecryptBackend, DecryptWriteBackend, FileType, Id, KeyOpts, - ReadBackend, Repository, WriteBackend, + random_poly, ConfigFile, ConfigOpts, DecryptBackend, DecryptWriteBackend, FileType, Id, + KeyOpts, ReadBackend, Repository, WriteBackend, }; -use crate::commands::config::ConfigOpts; - /// `init` subcommand #[derive(clap::Parser, Command, Debug)] pub(crate) struct InitCmd { From 61314c219ebdcced27f37fd7b091664c6b307aa4 Mon Sep 17 00:00:00 2001 From: Alexander Weiss Date: Thu, 6 Jul 2023 19:08:13 +0200 Subject: [PATCH 4/4] Move init command to rustic_core --- crates/rustic_core/examples/init.rs | 23 +++++++++++ crates/rustic_core/src/commands.rs | 1 + crates/rustic_core/src/commands/config.rs | 13 +++--- crates/rustic_core/src/commands/init.rs | 40 ++++++++++++++++++ crates/rustic_core/src/error.rs | 2 + crates/rustic_core/src/repository.rs | 45 ++++++++++++++++++--- src/commands/copy.rs | 25 ++++-------- src/commands/init.rs | 49 +++++------------------ tests/backup_restore.rs | 13 +++--- 9 files changed, 139 insertions(+), 72 deletions(-) create mode 100644 crates/rustic_core/examples/init.rs create mode 100644 crates/rustic_core/src/commands/init.rs diff --git a/crates/rustic_core/examples/init.rs b/crates/rustic_core/examples/init.rs new file mode 100644 index 0000000..152045e --- /dev/null +++ b/crates/rustic_core/examples/init.rs @@ -0,0 +1,23 @@ +//! `init` example +use rustic_core::{ConfigOpts, KeyOpts, Repository, RepositoryOptions}; +use simplelog::{Config, LevelFilter, SimpleLogger}; + +fn main() { + // Display info logs + let _ = SimpleLogger::init(LevelFilter::Info, Config::default()); + + // Init repository + let repo_opts = RepositoryOptions { + repository: Some("/tmp/repo".to_string()), + password: Some("test".to_string()), + ..Default::default() + }; + let key_opts = KeyOpts::default(); + let config_opts = ConfigOpts::default(); + let _repo = Repository::new(&repo_opts) + .unwrap() + .init(&key_opts, &config_opts) + .unwrap(); + + // -> use _repo for any operation on an open repository +} diff --git a/crates/rustic_core/src/commands.rs b/crates/rustic_core/src/commands.rs index 1a5064b..465212f 100644 --- a/crates/rustic_core/src/commands.rs +++ b/crates/rustic_core/src/commands.rs @@ -3,6 +3,7 @@ pub mod check; pub mod config; pub mod dump; pub mod forget; +pub mod init; pub mod key; pub mod prune; pub mod repoinfo; diff --git a/crates/rustic_core/src/commands/config.rs b/crates/rustic_core/src/commands/config.rs index b4edf6e..1ee0cc6 100644 --- a/crates/rustic_core/src/commands/config.rs +++ b/crates/rustic_core/src/commands/config.rs @@ -2,8 +2,8 @@ use bytesize::ByteSize; use crate::{ - error::CommandErrorKind, ConfigFile, DecryptBackend, DecryptWriteBackend, Open, Repository, - RusticResult, + error::CommandErrorKind, ConfigFile, DecryptBackend, DecryptWriteBackend, Key, Open, + Repository, RusticResult, }; pub(crate) fn apply_config( @@ -15,25 +15,26 @@ pub(crate) fn apply_config( if &new_config == repo.config() { Ok(false) } else { - save_config(repo, new_config)?; + save_config(repo, new_config, *repo.key())?; Ok(true) } } -fn save_config( +pub(crate) fn save_config( repo: &Repository, mut new_config: ConfigFile, + key: Key, ) -> RusticResult<()> { new_config.is_hot = None; // don't compress the config file - let mut dbe = repo.dbe().clone(); + let mut dbe = DecryptBackend::new(&repo.be, key); dbe.set_zstd(None); // for hot/cold backend, this only saves the config to the cold repo. _ = dbe.save_file(&new_config)?; if let Some(hot_be) = repo.be_hot.clone() { // save config to hot repo - let mut dbe = DecryptBackend::new(&hot_be, *repo.key()); + let mut dbe = DecryptBackend::new(&hot_be, key); // don't compress the config file dbe.set_zstd(None); new_config.is_hot = Some(true); diff --git a/crates/rustic_core/src/commands/init.rs b/crates/rustic_core/src/commands/init.rs new file mode 100644 index 0000000..13d7f11 --- /dev/null +++ b/crates/rustic_core/src/commands/init.rs @@ -0,0 +1,40 @@ +//! `init` subcommand + +use log::info; + +use crate::{ + commands::config::save_config, random_poly, ConfigFile, ConfigOpts, Id, Key, KeyOpts, + Repository, RusticResult, WriteBackend, +}; + +pub(crate) fn init( + repo: &Repository, + pass: &str, + key_opts: &KeyOpts, + config_opts: &ConfigOpts, +) -> RusticResult<(Key, ConfigFile)> { + // Create config first to allow catching errors from here without writing anything + let repo_id = Id::random(); + let chunker_poly = random_poly()?; + let mut config = ConfigFile::new(2, repo_id, chunker_poly); + config_opts.apply(&mut config)?; + + let key = init_with_config(repo, pass, key_opts, &config)?; + info!("repository {} successfully created.", repo_id); + + Ok((key, config)) +} + +pub(crate) fn init_with_config( + repo: &Repository, + pass: &str, + key_opts: &KeyOpts, + config: &ConfigFile, +) -> RusticResult { + repo.be.create()?; + let (key, id) = key_opts.init_key(repo, pass)?; + info!("key {id} successfully added."); + save_config(repo, config.clone(), key)?; + + Ok(key) +} diff --git a/crates/rustic_core/src/error.rs b/crates/rustic_core/src/error.rs index 13f4f5d..c8a51ba 100644 --- a/crates/rustic_core/src/error.rs +++ b/crates/rustic_core/src/error.rs @@ -260,6 +260,8 @@ pub enum RepositoryErrorKind { ReadingPasswordFromReaderFailed(std::io::Error), /// reading Password from prompt failed: `{0:?}` ReadingPasswordFromPromptFailed(std::io::Error), + /// Config file already exists. Aborting. + ConfigFileExists, } /// [`IndexErrorKind`] describes the errors that can be returned by processing Indizes diff --git a/crates/rustic_core/src/repository.rs b/crates/rustic_core/src/repository.rs index fbda08e..2fd430e 100644 --- a/crates/rustic_core/src/repository.rs +++ b/crates/rustic_core/src/repository.rs @@ -289,6 +289,7 @@ impl Repository { _ => Err(RepositoryErrorKind::MoreThanOneRepositoryConfig(self.name.clone()).into()), } } + pub fn open(self) -> RusticResult> { let password = self .password()? @@ -322,9 +323,47 @@ impl Repository { } })?; info!("repository {}: password is correct.", self.name); - let dbe = DecryptBackend::new(&self.be, key); let config: ConfigFile = dbe.get_file(&config_id)?; + self.open_raw(key, config) + } + + pub fn init( + self, + key_opts: &KeyOpts, + config_opts: &ConfigOpts, + ) -> RusticResult> { + let password = self + .password()? + .ok_or(RepositoryErrorKind::NoPasswordGiven)?; + self.init_with_password(&password, key_opts, config_opts) + } + + pub fn init_with_password( + self, + pass: &str, + key_opts: &KeyOpts, + config_opts: &ConfigOpts, + ) -> RusticResult> { + if self.config_id()?.is_some() { + return Err(RepositoryErrorKind::ConfigFileExists.into()); + } + let (key, config) = commands::init::init(&self, pass, key_opts, config_opts)?; + self.open_raw(key, config) + } + + pub fn init_with_config( + self, + pass: &str, + key_opts: &KeyOpts, + config: ConfigFile, + ) -> RusticResult> { + let key = commands::init::init_with_config(&self, pass, key_opts, &config)?; + info!("repository {} successfully created.", config.id); + self.open_raw(key, config) + } + + fn open_raw(self, key: Key, config: ConfigFile) -> RusticResult> { match (config.is_hot == Some(true), self.be_hot.is_some()) { (true, false) => return Err(RepositoryErrorKind::HotRepositoryFlagMissing.into()), (false, true) => return Err(RepositoryErrorKind::IsNotHotRepository.into()), @@ -358,10 +397,6 @@ impl Repository { status: open, }) } - - pub fn init_key(&self, pass: &str, opts: &KeyOpts) -> RusticResult<(Key, Id)> { - opts.init_key(self, pass) - } } impl Repository { diff --git a/src/commands/copy.rs b/src/commands/copy.rs index 6ffd7fc..316b906 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -2,11 +2,7 @@ /// App-local prelude includes `app_reader()`/`app_writer()`/`app_config()` /// accessors along with logging macros. Customize as you see fit. -use crate::{ - commands::{init::save_config, open_repository}, - helpers::copy, - status_err, Application, RUSTIC_APP, -}; +use crate::{commands::open_repository, helpers::copy, status_err, Application, RUSTIC_APP}; use abscissa_core::{Command, Runnable, Shutdown}; use anyhow::{bail, Result}; use log::info; @@ -15,8 +11,7 @@ use merge::Merge; use serde::Deserialize; use rustic_core::{ - FileType, Id, IndexBackend, KeyOpts, Open, ProgressBars, ReadBackend, Repository, - RepositoryOptions, SnapshotFile, + Id, IndexBackend, KeyOpts, Open, ProgressBars, Repository, RepositoryOptions, SnapshotFile, }; /// `copy` subcommand @@ -73,22 +68,18 @@ impl CopyCmd { let index = IndexBackend::new(be, &config.global.progress_options.progress_counter(""))?; let poly = repo.config().poly()?; - for target_opt in &config.copy.targets { let repo_dest = Repository::new(target_opt)?; - if self.init && repo_dest.be.list(FileType::Config)?.is_empty() { + let repo_dest = if self.init && repo_dest.config_id()?.is_none() { let mut config_dest = repo.config().clone(); config_dest.id = Id::random(); - save_config( - config_dest, - &repo_dest, - &self.key_opts, - repo_dest.password()?, - )?; - } + let pass = repo_dest.password()?.unwrap(); + repo_dest.init_with_config(&pass, &self.key_opts, config_dest)? + } else { + repo_dest.open()? + }; - let repo_dest = repo_dest.open()?; info!("copying to target {:?}...", repo_dest); // TODO: repo_dest.name if poly != repo_dest.config().poly()? { bail!("cannot copy to repository with different chunker parameter (re-chunking not implemented)!"); diff --git a/src/commands/init.rs b/src/commands/init.rs index bc391ed..b13763b 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -9,10 +9,7 @@ use crate::{Application, RUSTIC_APP}; use dialoguer::Password; -use rustic_core::{ - random_poly, ConfigFile, ConfigOpts, DecryptBackend, DecryptWriteBackend, FileType, Id, - KeyOpts, ReadBackend, Repository, WriteBackend, -}; +use rustic_core::{ConfigOpts, KeyOpts, Repository}; /// `init` subcommand #[derive(clap::Parser, Command, Debug)] @@ -40,33 +37,21 @@ impl InitCmd { let po = config.global.progress_options; let repo = Repository::new_with_progress(&config.repository, po)?; - let config_ids = repo.be.list(FileType::Config)?; - - let password = repo.password()?; - - if !config_ids.is_empty() { + // Note: This is again checked in repo.init_with_password(), however we want to inform + // users before they are prompted to enter a password + if repo.config_id()?.is_some() { bail!("Config file already exists. Aborting."); } - - // Create config first to allow catching errors from here without writing anything - let repo_id = Id::random(); - let chunker_poly = random_poly()?; - let mut config = ConfigFile::new(2, repo_id, chunker_poly); - self.config_opts.apply(&mut config)?; - - save_config(config, &repo, &self.key_opts, password)?; - - Ok(()) + init(repo, &self.key_opts, &self.config_opts) } } -pub(crate) fn save_config( - mut config: ConfigFile, - repo: &Repository, +pub(crate) fn init( + repo: Repository, key_opts: &KeyOpts, - password: Option, + config_opts: &ConfigOpts, ) -> Result<()> { - let pass = password.unwrap_or_else(|| { + let pass = repo.password()?.unwrap_or_else(|| { match Password::new() .with_prompt("enter password for new key") .allow_empty_password(true) @@ -81,21 +66,7 @@ pub(crate) fn save_config( } }); - repo.be.create()?; - let (key, id) = repo.init_key(&pass, key_opts)?; - println!("key {id} successfully added."); - - // save config - let dbe = DecryptBackend::new(&repo.be, key); - config.is_hot = None; - _ = dbe.save_file(&config)?; - - if let Some(be_hot) = &repo.be_hot { - let dbe = DecryptBackend::new(be_hot, key); - config.is_hot = Some(true); - _ = dbe.save_file(&config)?; - } - println!("repository {} successfully created.", config.id); + let _ = repo.init_with_password(&pass, key_opts, config_opts)?; Ok(()) } diff --git a/tests/backup_restore.rs b/tests/backup_restore.rs index fe07cb9..4199bea 100644 --- a/tests/backup_restore.rs +++ b/tests/backup_restore.rs @@ -26,7 +26,8 @@ pub fn rustic_runner(temp_dir: &TempDir) -> CmdRunner { .arg("--password") .arg(password) .arg("--no-progress") - .capture_stdout(); + .capture_stdout() + .capture_stderr(); runner } @@ -35,11 +36,13 @@ fn setup() -> TestResult { let mut runner = rustic_runner(&temp_dir); let mut cmd = runner.args(["init"]).run(); - let mut output = String::new(); - cmd.stdout().read_to_string(&mut output)?; + let mut stdout = String::new(); + let mut stderr = String::new(); + cmd.stdout().read_to_string(&mut stdout)?; + cmd.stderr().read_to_string(&mut stderr)?; let patterns = &["successfully added.", "successfully created."]; - let matches = get_matches(patterns, output)?; + let matches = get_matches(patterns, stderr)?; assert_eq!( matches, @@ -124,7 +127,7 @@ fn test_backup_and_check_passes() -> TestResult<()> { let mut runner = rustic_runner(&temp_dir); let mut cmd = runner.args(["check", "--read-data"]).run(); let mut output = String::new(); - cmd.stdout().read_to_string(&mut output)?; + cmd.stderr().read_to_string(&mut output)?; let patterns = &["WARN", "ERROR"]; let matches = get_matches(patterns, output)?;