From c4b8ebf15cf0182ab7a2a04154245faada5fdfa1 Mon Sep 17 00:00:00 2001 From: Alexander Weiss Date: Sun, 16 Apr 2023 09:53:19 +0200 Subject: [PATCH] Make --dry-run a global option --- changelog/new.txt | 1 + src/commands/backup.rs | 10 +++------- src/commands/copy.rs | 12 +++++------ src/commands/forget.rs | 12 ++++------- src/commands/mod.rs | 21 +++++++++++-------- src/commands/prune.rs | 17 ++++++++-------- src/commands/repair.rs | 40 ++++++++++++++++++------------------- src/commands/restore.rs | 24 ++++++++++------------ src/commands/self_update.rs | 2 +- src/commands/tag.rs | 9 +++------ 10 files changed, 70 insertions(+), 78 deletions(-) diff --git a/changelog/new.txt b/changelog/new.txt index dfbc833..4ae6404 100644 --- a/changelog/new.txt +++ b/changelog/new.txt @@ -6,4 +6,5 @@ Bugs fixed: - restore: Warm-up options given by the command line didn't work. This has been fixed. New features: +- Option --dry-run is now a global option and can also be defined in the config file or via env variable - Updated to clap v4 diff --git a/src/commands/backup.rs b/src/commands/backup.rs index 3786df3..91ad0d4 100644 --- a/src/commands/backup.rs +++ b/src/commands/backup.rs @@ -10,7 +10,7 @@ use path_dedot::ParseDot; use serde::Deserialize; use toml::Value; -use super::{bytes, progress_bytes, progress_counter, RusticConfig}; +use super::{bytes, progress_bytes, progress_counter, GlobalOpts, RusticConfig}; use crate::archiver::Archiver; use crate::backend::{DryRunBackend, LocalSource, LocalSourceOptions, StdinSource}; use crate::index::IndexBackend; @@ -29,11 +29,6 @@ pub(super) struct Opts { #[serde(skip)] cli_sources: Vec, - /// Do not upload or write any data, just show what would be done - #[clap(long, short = 'n')] - #[merge(strategy = merge::bool::overwrite_false)] - dry_run: bool, - /// Group snapshots by any combination of host,label,paths,tags to find a suitable parent (default: host,label,paths) #[clap( long, @@ -120,6 +115,7 @@ pub(super) struct Opts { pub(super) fn execute( repo: OpenRepository, + gopts: GlobalOpts, opts: Opts, config_file: RusticConfig, command: String, @@ -187,7 +183,7 @@ pub(super) fn execute( // merge "backup" section from config file, if given config_file.merge_into("backup", &mut opts)?; - let be = DryRunBackend::new(repo.dbe.clone(), opts.dry_run); + let be = DryRunBackend::new(repo.dbe.clone(), gopts.dry_run); info!("starting to backup {source}..."); let as_path = match opts.as_path { None => None, diff --git a/src/commands/copy.rs b/src/commands/copy.rs index 2850869..3ffd86e 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -5,7 +5,7 @@ use clap::Parser; use log::*; use rayon::prelude::*; -use super::{progress_counter, table_with_titles, RusticConfig}; +use super::{progress_counter, table_with_titles, GlobalOpts, RusticConfig}; use crate::backend::DecryptWriteBackend; use crate::blob::{BlobType, NodeType, Packer, TreeStreamerOnce}; use crate::index::{IndexBackend, IndexedBackend, Indexer, ReadIndex}; @@ -18,10 +18,6 @@ pub(super) struct Opts { #[clap(value_name = "ID")] ids: Vec, - /// Don't copy any snapshot, only show what would be done - #[clap(long, short = 'n')] - dry_run: bool, - #[clap( flatten, next_help_heading = "Snapshot filter options (if no snapshot is given)" @@ -31,6 +27,7 @@ pub(super) struct Opts { pub(super) fn execute( repo: OpenRepository, + gopts: GlobalOpts, mut opts: Opts, config_file: RusticConfig, ) -> Result<()> { @@ -60,7 +57,7 @@ pub(super) fn execute( if poly != repo_dest.config.poly()? { bail!("cannot copy to repository with different chunker parameter (re-chunking not implemented)!"); } - copy(&snapshots, index.clone(), repo_dest, &opts)?; + copy(&snapshots, index.clone(), repo_dest, &gopts, &opts)?; } Ok(()) } @@ -69,12 +66,13 @@ fn copy( snapshots: &[SnapshotFile], index: impl IndexedBackend, repo_dest: OpenRepository, + gopts: &GlobalOpts, opts: &Opts, ) -> Result<()> { let be_dest = &repo_dest.dbe; let snapshots = relevant_snapshots(snapshots, &repo_dest, &opts.filter)?; - match (snapshots.len(), opts.dry_run) { + match (snapshots.len(), gopts.dry_run) { (count, true) => { info!("would have copied {count} snapshots"); return Ok(()); diff --git a/src/commands/forget.rs b/src/commands/forget.rs index 6de004a..d1f7c40 100644 --- a/src/commands/forget.rs +++ b/src/commands/forget.rs @@ -8,7 +8,7 @@ use merge::Merge; use serde::Deserialize; use serde_with::{serde_as, DisplayFromStr}; -use super::{progress_counter, prune, table_with_titles, RusticConfig}; +use super::{progress_counter, prune, table_with_titles, GlobalOpts, RusticConfig}; use crate::backend::{DecryptWriteBackend, FileType}; use crate::repofile::{ SnapshotFile, SnapshotFilter, SnapshotGroup, SnapshotGroupCriterion, StringList, @@ -29,10 +29,6 @@ pub(super) struct Opts { next_help_heading = "PRUNE OPTIONS (only when used with --prune)" )] prune_opts: prune::Opts, - - /// Don't remove anything, only show what would be done - #[clap(skip)] - dry_run: bool, } #[serde_as] @@ -60,6 +56,7 @@ struct ConfigOpts { pub(super) fn execute( repo: OpenRepository, + gopts: GlobalOpts, mut opts: Opts, config_file: RusticConfig, ) -> Result<()> { @@ -69,7 +66,6 @@ pub(super) fn execute( // merge "snapshot-filter" section from config file, if given config_file.merge_into("snapshot-filter", &mut opts.config.filter)?; - opts.dry_run = opts.prune_opts.dry_run; let group_by = opts .config .group_by @@ -147,7 +143,7 @@ pub(super) fn execute( println!(); } - match (forget_snaps.is_empty(), opts.dry_run) { + match (forget_snaps.is_empty(), gopts.dry_run) { (true, _) => println!("nothing to remove"), (false, true) => println!("would have removed the following snapshots:\n {forget_snaps:?}"), (false, false) => { @@ -157,7 +153,7 @@ pub(super) fn execute( } if opts.config.prune { - prune::execute(repo, opts.prune_opts, forget_snaps)?; + prune::execute(repo, gopts, opts.prune_opts, forget_snaps)?; } Ok(()) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e19c8d6..818e0c7 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -67,6 +67,11 @@ struct Opts { #[derive(Default, Parser, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] struct GlobalOpts { + /// Only show what would be done without modifying anything. Does not affect read-only commands + #[clap(long, short = 'n', global = true, env = "RUSTIC_DRY_RUN")] + #[merge(strategy = merge::bool::overwrite_false)] + dry_run: bool, + /// Use this log level [default: info] #[clap(long, global = true, env = "RUSTIC_LOG_LEVEL")] #[serde_as(as = "Option")] @@ -172,7 +177,7 @@ pub fn execute() -> Result<()> { // start logger let level_filter = gopts.log_level.unwrap_or(LevelFilter::Info); - match gopts.log_file { + match &gopts.log_file { None => TermLogger::init( level_filter, ConfigBuilder::new() @@ -238,15 +243,15 @@ pub fn execute() -> Result<()> { #[allow(clippy::match_same_arms)] match args.command { - Command::Backup(opts) => backup::execute(repo, opts, config_file, command)?, + Command::Backup(opts) => backup::execute(repo, gopts, opts, config_file, command)?, Command::Config(opts) => config::execute(repo, opts)?, Command::Cat(opts) => cat::execute(repo, opts, config_file)?, Command::Check(opts) => check::execute(repo, opts)?, Command::Completions(_) => {} // already handled above - Command::Copy(opts) => copy::execute(repo, opts, config_file)?, + Command::Copy(opts) => copy::execute(repo, gopts, opts, config_file)?, Command::Diff(opts) => diff::execute(repo, opts, config_file)?, Command::Dump(opts) => dump::execute(repo, opts, config_file)?, - Command::Forget(opts) => forget::execute(repo, opts, config_file)?, + Command::Forget(opts) => forget::execute(repo, gopts, opts, config_file)?, Command::Init(_) => {} // already handled above Command::Key(opts) => key::execute(repo, opts)?, Command::List(opts) => list::execute(repo, opts)?, @@ -254,11 +259,11 @@ pub fn execute() -> Result<()> { Command::Merge(opts) => merge_cmd::execute(repo, opts, config_file, command)?, Command::SelfUpdate(_) => {} // already handled above Command::Snapshots(opts) => snapshots::execute(repo, opts, config_file)?, - Command::Prune(opts) => prune::execute(repo, opts, vec![])?, - Command::Restore(opts) => restore::execute(repo, opts, config_file)?, - Command::Repair(opts) => repair::execute(repo, opts, config_file)?, + Command::Prune(opts) => prune::execute(repo, gopts, opts, vec![])?, + Command::Restore(opts) => restore::execute(repo, gopts, opts, config_file)?, + Command::Repair(opts) => repair::execute(repo, gopts, opts, config_file)?, Command::Repoinfo(opts) => repoinfo::execute(repo, opts)?, - Command::Tag(opts) => tag::execute(repo, opts, config_file)?, + Command::Tag(opts) => tag::execute(repo, gopts, opts, config_file)?, }; Ok(()) diff --git a/src/commands/prune.rs b/src/commands/prune.rs index e79fa79..1d107c4 100644 --- a/src/commands/prune.rs +++ b/src/commands/prune.rs @@ -12,7 +12,7 @@ use itertools::Itertools; use log::*; use rayon::prelude::*; -use super::{bytes, no_progress, progress_bytes, progress_counter, warm_up_wait}; +use super::{bytes, no_progress, progress_bytes, progress_counter, warm_up_wait, GlobalOpts}; use crate::backend::{DecryptReadBackend, DecryptWriteBackend, FileType, ReadBackend}; use crate::blob::{ BlobType, BlobTypeMap, Initialize, NodeType, PackSizer, Repacker, Sum, TreeStreamerOnce, @@ -26,10 +26,6 @@ use crate::repository::OpenRepository; #[derive(Parser)] #[group(id = "prune_opts")] pub(super) struct Opts { - /// Don't remove anything, only show what would be done - #[clap(long, short = 'n')] - pub(crate) dry_run: bool, - /// Define maximum data to repack in % of reposize or as size (e.g. '5b', '2 kB', '3M', '4TiB') or 'unlimited' #[clap(long, value_name = "LIMIT", default_value = "unlimited")] max_repack: LimitOption, @@ -71,7 +67,12 @@ pub(super) struct Opts { no_resize: bool, } -pub(super) fn execute(repo: OpenRepository, opts: Opts, ignore_snaps: Vec) -> Result<()> { +pub(super) fn execute( + repo: OpenRepository, + gopts: GlobalOpts, + opts: Opts, + ignore_snaps: Vec, +) -> Result<()> { let be = &repo.dbe; if repo.config.version < 2 && opts.repack_uncompressed { bail!("--repack-uncompressed makes no sense for v1 repo!"); @@ -131,9 +132,9 @@ pub(super) fn execute(repo: OpenRepository, opts: Opts, ignore_snaps: Vec) - pruner.filter_index_files(opts.instant_delete); pruner.print_stats(); - warm_up_wait(&repo, pruner.repack_packs().into_iter(), !opts.dry_run)?; + warm_up_wait(&repo, pruner.repack_packs().into_iter(), !gopts.dry_run)?; - if !opts.dry_run { + if !gopts.dry_run { pruner.do_prune(repo, opts)?; } Ok(()) diff --git a/src/commands/repair.rs b/src/commands/repair.rs index 54dfefe..832e852 100644 --- a/src/commands/repair.rs +++ b/src/commands/repair.rs @@ -18,7 +18,7 @@ use crate::repofile::{ use crate::repository::OpenRepository; use super::rustic_config::RusticConfig; -use super::{progress_counter, progress_spinner, warm_up_wait}; +use super::{progress_counter, progress_spinner, warm_up_wait, GlobalOpts}; #[derive(Parser)] pub(super) struct Opts { @@ -36,10 +36,6 @@ enum Command { #[derive(Default, Parser)] struct IndexOpts { - // Only show what would be repaired - #[clap(long, short = 'n')] - dry_run: bool, - // Read all data packs, i.e. completely re-create the index #[clap(long)] read_all: bool, @@ -50,10 +46,6 @@ struct SnapOpts { #[clap(flatten, next_help_heading = "Snapshot filter options")] filter: SnapshotFilter, - /// Only show what would be repaired - #[clap(long, short = 'n')] - dry_run: bool, - /// Also remove defect snapshots - WARNING: This can result in data loss! #[clap(long)] delete: bool, @@ -71,14 +63,19 @@ struct SnapOpts { ids: Vec, } -pub(super) fn execute(repo: OpenRepository, opts: Opts, config_file: RusticConfig) -> Result<()> { +pub(super) fn execute( + repo: OpenRepository, + gopts: GlobalOpts, + opts: Opts, + config_file: RusticConfig, +) -> Result<()> { match opts.command { - Command::Index(opt) => repair_index(&repo, opt), - Command::Snapshots(opt) => repair_snaps(&repo.dbe, opt, config_file, &repo.config), + Command::Index(opt) => repair_index(&repo, gopts, opt), + Command::Snapshots(opt) => repair_snaps(&repo.dbe, gopts, opt, config_file, &repo.config), } } -fn repair_index(repo: &OpenRepository, opts: IndexOpts) -> Result<()> { +fn repair_index(repo: &OpenRepository, gopts: GlobalOpts, opts: IndexOpts) -> Result<()> { let be = &repo.dbe; let p = progress_spinner("listing packs..."); let mut packs: HashMap<_, _> = be.list_with_size(FileType::Pack)?.into_iter().collect(); @@ -137,7 +134,7 @@ fn repair_index(repo: &OpenRepository, opts: IndexOpts) -> Result<()> { for p in index.packs_to_delete { process_pack(p, true, &mut new_index, &mut changed); } - match (changed, opts.dry_run) { + match (changed, gopts.dry_run) { (true, true) => info!("would have modified index file {index_id}"), (true, false) => { if !new_index.packs.is_empty() || !new_index.packs_to_delete.is_empty() { @@ -172,7 +169,7 @@ fn repair_index(repo: &OpenRepository, opts: IndexOpts) -> Result<()> { pack.blobs = header.into_blobs(); } } - if !opts.dry_run { + if !gopts.dry_run { indexer.write().unwrap().add_with(pack, to_delete)?; } p.inc(1); @@ -185,6 +182,7 @@ fn repair_index(repo: &OpenRepository, opts: IndexOpts) -> Result<()> { fn repair_snaps( be: &impl DecryptFullBackend, + gopts: GlobalOpts, mut opts: SnapOpts, config_file: RusticConfig, config: &ConfigFile, @@ -219,6 +217,7 @@ fn repair_snaps( Some(snap.tree), &mut replaced, &mut seen, + &gopts, &opts, )? { (Changed::None, _) => { @@ -235,7 +234,7 @@ fn repair_snaps( } snap.set_tags(opts.tag.clone()); snap.tree = id; - if opts.dry_run { + if gopts.dry_run { info!("would have modified snapshot {snap_id}."); } else { let new_id = be.save_file(&snap)?; @@ -246,13 +245,13 @@ fn repair_snaps( } } - if !opts.dry_run { + if !gopts.dry_run { packer.finalize()?; indexer.write().unwrap().finalize()?; } if opts.delete { - if opts.dry_run { + if gopts.dry_run { info!("would have removed {} snapshots.", delete.len()); } else { be.delete_list( @@ -280,6 +279,7 @@ fn repair_tree( id: Option, replaced: &mut HashMap, seen: &mut HashSet, + gopts: &GlobalOpts, opts: &SnapOpts, ) -> Result<(Changed, Id)> { let (tree, changed) = match id { @@ -331,7 +331,7 @@ fn repair_tree( } NodeType::Dir {} => { let (c, tree_id) = - repair_tree(be, packer, node.subtree, replaced, seen, opts)?; + repair_tree(be, packer, node.subtree, replaced, seen, gopts, opts)?; match c { Changed::None => {} Changed::This => { @@ -363,7 +363,7 @@ fn repair_tree( (_, c) => { // the tree has been changed => save it let (chunk, new_id) = tree.serialize()?; - if !be.has_tree(&new_id) && !opts.dry_run { + if !be.has_tree(&new_id) && !gopts.dry_run { packer.add(&chunk, &new_id)?; } if let Some(id) = id { diff --git a/src/commands/restore.rs b/src/commands/restore.rs index 46757d6..ba6b8d9 100644 --- a/src/commands/restore.rs +++ b/src/commands/restore.rs @@ -13,7 +13,7 @@ use log::*; use rayon::ThreadPoolBuilder; use super::rustic_config::RusticConfig; -use super::{bytes, progress_bytes, progress_counter, warm_up_wait}; +use super::{bytes, progress_bytes, progress_counter, warm_up_wait, GlobalOpts}; use crate::backend::{DecryptReadBackend, FileType, LocalDestination}; use crate::blob::{Node, NodeStreamer, NodeType, Tree, TreeStreamerOptions}; use crate::commands::helpers::progress_spinner; @@ -33,10 +33,6 @@ pub(super) struct Opts { #[clap(value_name = "DESTINATION")] dest: String, - /// Dry-run: don't restore, only show what would be done - #[clap(long, short = 'n')] - dry_run: bool, - /// Remove all files/dirs in destination which are not contained in snapshot. /// WARNING: Use with care, maybe first try this with --dry-run? #[clap(long)] @@ -66,6 +62,7 @@ pub(super) struct Opts { pub(super) fn execute( repo: OpenRepository, + gopts: GlobalOpts, mut opts: Opts, config_file: RusticConfig, ) -> Result<()> { @@ -81,7 +78,7 @@ pub(super) fn execute( let dest = LocalDestination::new(&opts.dest, true, !node.is_dir())?; let p = progress_spinner("collecting file information..."); - let (file_infos, stats) = allocate_and_collect(&dest, index.clone(), &node, &opts)?; + let (file_infos, stats) = allocate_and_collect(&dest, index.clone(), &node, &gopts, &opts)?; p.finish(); let fs = stats.file; @@ -106,13 +103,13 @@ pub(super) fn execute( if file_infos.restore_size == 0 { info!("all file contents are fine."); } else { - warm_up_wait(&repo, file_infos.to_packs().into_iter(), !opts.dry_run)?; - if !opts.dry_run { + warm_up_wait(&repo, file_infos.to_packs().into_iter(), !gopts.dry_run)?; + if !gopts.dry_run { restore_contents(be, &dest, file_infos)?; } } - if !opts.dry_run { + if !gopts.dry_run { let p = progress_spinner("setting metadata..."); restore_metadata(&dest, index, &node, &opts)?; p.finish(); @@ -142,6 +139,7 @@ fn allocate_and_collect( dest: &LocalDestination, index: impl IndexedBackend + Unpin, node: &Node, + gopts: &GlobalOpts, opts: &Opts, ) -> Result<(FileInfos, RestoreStats)> { let dest_path = Path::new(&opts.dest); @@ -165,7 +163,7 @@ fn allocate_and_collect( } match ( opts.delete, - opts.dry_run, + gopts.dry_run, entry.file_type().unwrap().is_dir(), ) { (true, true, true) => { @@ -210,7 +208,7 @@ fn allocate_and_collect( } else { stats.dir.restore += 1; debug!("to restore: {path:?}"); - if !opts.dry_run { + if !gopts.dry_run { dest.create_dir(path) .with_context(|| format!("error creating {path:?}"))?; } @@ -239,7 +237,7 @@ fn allocate_and_collect( (true, AddFileResult::New(size) | AddFileResult::Modify(size)) => { stats.file.modify += 1; debug!("to modify: {path:?}"); - if !opts.dry_run { + if !gopts.dry_run { // set the right file size dest.set_length(path, size) .with_context(|| format!("error setting length for {path:?}"))?; @@ -248,7 +246,7 @@ fn allocate_and_collect( (false, AddFileResult::New(size) | AddFileResult::Modify(size)) => { stats.file.restore += 1; debug!("to restore: {path:?}"); - if !opts.dry_run { + if !gopts.dry_run { // create the file as it doesn't exist dest.set_length(path, size) .with_context(|| format!("error creating {path:?}"))?; diff --git a/src/commands/self_update.rs b/src/commands/self_update.rs index 0697309..002592e 100644 --- a/src/commands/self_update.rs +++ b/src/commands/self_update.rs @@ -5,7 +5,7 @@ use self_update::cargo_crate_version; #[derive(Parser)] pub(super) struct Opts { /// Do not ask before processing the self-update - #[clap(long)] + #[clap(long, conflicts_with = "dry_run")] force: bool, } diff --git a/src/commands/tag.rs b/src/commands/tag.rs index 3822d81..ee4b538 100644 --- a/src/commands/tag.rs +++ b/src/commands/tag.rs @@ -2,7 +2,7 @@ use anyhow::Result; use chrono::{Duration, Local}; use clap::Parser; -use super::{progress_counter, RusticConfig}; +use super::{progress_counter, GlobalOpts, RusticConfig}; use crate::backend::{DecryptWriteBackend, FileType}; use crate::id::Id; use crate::repofile::{DeleteOption, SnapshotFile, SnapshotFilter, StringList}; @@ -15,10 +15,6 @@ pub(super) struct Opts { #[clap(value_name = "ID")] ids: Vec, - /// Don't change any snapshot, only show which would be modified - #[clap(long, short = 'n')] - dry_run: bool, - #[clap( flatten, next_help_heading = "Snapshot filter options (if no snapshot is given)" @@ -70,6 +66,7 @@ pub(super) struct Opts { pub(super) fn execute( repo: OpenRepository, + gopts: GlobalOpts, mut opts: Opts, config_file: RusticConfig, ) -> Result<()> { @@ -102,7 +99,7 @@ pub(super) fn execute( snap.id = Id::default(); } - match (old_snap_ids.is_empty(), opts.dry_run) { + match (old_snap_ids.is_empty(), gopts.dry_run) { (true, _) => println!("no snapshot changed."), (false, true) => { println!("would have modified the following snapshots:\n {old_snap_ids:?}");