diff --git a/changelog/new.txt b/changelog/new.txt index ef9816a76..15ffa4be6 100644 --- a/changelog/new.txt +++ b/changelog/new.txt @@ -1,13 +1,16 @@ Changes in version x.x.x: Breaking changes: +- The option `--config-profile` was renamed into `--use-profile` Bugs fixed: - restore: Warm-up options given by the command line didn't work. This has been fixed. - backup showed 1 dir as changed when backing up without parent. This has been fixed. - diff: The options --no-atime and --ignore-devid had no effect and are now removed. +- Rustic's check of additional fields in the config file didn't work in edge cases. This has been fixed. New features: +- config file: New field use-profile allows to merge options from other config profiles - backup: Backing up (small) files is now much more parallelized. - forget: Using "-1" as value for --keep-* options will keep all snapshots of that interval - prune: Added option --repack-all diff --git a/examples/full.toml b/examples/full.toml index 61cd681b1..0b62397ec 100644 --- a/examples/full.toml +++ b/examples/full.toml @@ -7,6 +7,7 @@ # Global options: These options are used for all commands. [global] +use-profile = "" log-level = "info" # any of "off", "error", "warn", "info", "debug", "trace"; default: "info" log-file = "/path/to/rustic.log" # Default: not set no-progress = false @@ -32,7 +33,7 @@ warm-up-wait = "10min" # Default: not set [repository.options] post-create-command = "par2create -qq -n1 -r5 %file" # Only local backend; Default: not set post-delete-command = "sh -c \"rm -f %file*.par2\"" # Only local backend; Default: not set -retry = true # Only rest/rclone backend +retry = "true" # Only rest/rclone backend timeout = "2min" # Ony rest/rclone backend # Snapshot-filter options: These options apply to all commands that use snapshot filters @@ -114,7 +115,7 @@ filter-host = ["host2", "host2"] # Default: no host filter filter-label = ["label1", "label2"] # Default: no label filter filter-tags = ["tag1,tag2", "tag3"] # Default: no tags filger filter-paths = ["path1", "path2,path3"] # Default: no paths filter -filter-fn = '|sn| {sn.host == "host1" || sn.description.contains("test")}'' # Default: no filter function +filter-fn = '|sn| {sn.host == "host1" || sn.description.contains("test")}' # Default: no filter function # The retention options follow. All of these are not set by default. keep-tags = ["tag1", "tag2,tag3"] keep-ids = ["123abc", "11122233"] # Keep all snapshots whose ID starts with any of these strings diff --git a/src/commands/backup.rs b/src/commands/backup.rs index 6fb82c5c9..0ed573d41 100644 --- a/src/commands/backup.rs +++ b/src/commands/backup.rs @@ -8,9 +8,8 @@ use log::*; use merge::Merge; use path_dedot::ParseDot; use serde::Deserialize; -use toml::Value; -use super::{bytes, progress_bytes, progress_counter, GlobalOpts, RusticConfig}; +use super::{bytes, progress_bytes, progress_counter, Config}; use crate::archiver::Archiver; use crate::backend::{ DryRunBackend, LocalSource, LocalSourceFilterOptions, LocalSourceSaveOptions, StdinSource, @@ -22,8 +21,13 @@ use crate::repofile::{ use crate::repository::OpenRepository; #[derive(Clone, Default, Parser, Deserialize, Merge)] -#[serde(default, rename_all = "kebab-case")] -pub(super) struct Opts { +#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] +// Note: using cli_sources, sources and source within this strict 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. +pub struct Opts { /// Backup source (can be specified multiple times), use - for stdin. If no source is given, uses all /// sources defined in the config file #[clap(value_name = "SOURCE")] @@ -103,15 +107,9 @@ pub(super) struct Opts { #[merge(strategy = merge::bool::overwrite_false)] json: bool, - // This is a hack to support serde(deny_unknown_fields) for deserializing the backup options from TOML - // while still being able to use [[backup.sources]] in the config file. - // A drawback is that a unkowen "sources = ..." won't be bailed... - // Note that unfortunately we cannot work with nested flattened structures, see - // https://github.com/serde-rs/serde/issues/1547 #[clap(skip)] #[merge(skip)] - #[serde(rename = "sources")] - config_sources: Option, + sources: Vec, /// Backup source, used within config file #[clap(skip)] @@ -121,14 +119,24 @@ pub(super) struct Opts { pub(super) fn execute( repo: OpenRepository, - gopts: GlobalOpts, + mut config: Config, opts: Opts, - config_file: RusticConfig, command: String, ) -> Result<()> { let time = Local::now(); - let config_opts: Vec = config_file.get("backup.sources")?; + // manually check for a "source" field, check is not done by serde, see above. + if !config.backup.source.is_empty() { + bail!("key \"source\" is not valid in the [backup] section!"); + } + + let config_opts = config.backup.sources; + config.backup.sources = Vec::new(); + + // manually check for a "sources" field, check is not done by serde, see above. + if config_opts.iter().any(|opt| !opt.sources.is_empty()) { + bail!("key \"sources\" is not valid in a [[backup.sources]] section!"); + } let config_sources: Vec<_> = config_opts .iter() @@ -186,10 +194,11 @@ pub(super) fn execute( } } } + // merge "backup" section from config file, if given - config_file.merge_into("backup", &mut opts)?; + opts.merge(config.backup.clone()); - let be = DryRunBackend::new(repo.dbe.clone(), gopts.dry_run); + let be = DryRunBackend::new(repo.dbe.clone(), config.global.dry_run); info!("starting to backup {source}..."); let as_path = match opts.as_path { None => None, diff --git a/src/commands/cat.rs b/src/commands/cat.rs index 9df078f5b..611819620 100644 --- a/src/commands/cat.rs +++ b/src/commands/cat.rs @@ -4,13 +4,12 @@ use anyhow::{anyhow, Result}; use clap::{Parser, Subcommand}; use indicatif::ProgressBar; -use super::progress_counter; -use super::rustic_config::RusticConfig; +use super::{progress_counter, Config}; use crate::backend::{DecryptReadBackend, FileType}; use crate::blob::{BlobType, Tree}; use crate::id::Id; use crate::index::{IndexBackend, IndexedBackend}; -use crate::repofile::{SnapshotFile, SnapshotFilter}; +use crate::repofile::SnapshotFile; use crate::repository::OpenRepository; #[derive(Parser)] @@ -46,15 +45,9 @@ struct TreeOpts { /// Snapshot/path of the tree to display #[clap(value_name = "SNAPSHOT[:PATH]")] snap: String, - - #[clap( - flatten, - next_help_heading = "Snapshot filter options (when using latest)" - )] - filter: SnapshotFilter, } -pub(super) fn execute(repo: OpenRepository, opts: Opts, config_file: RusticConfig) -> Result<()> { +pub(super) fn execute(repo: OpenRepository, config: Config, opts: Opts) -> Result<()> { let be = &repo.dbe; match opts.command { Command::Config => cat_file(be, FileType::Config, IdOpt::default()), @@ -64,7 +57,7 @@ pub(super) fn execute(repo: OpenRepository, opts: Opts, config_file: RusticConfi Command::TreeBlob(opt) => cat_blob(be, BlobType::Tree, opt), Command::DataBlob(opt) => cat_blob(be, BlobType::Data, opt), // special treatment for cating a tree within a snapshot - Command::Tree(opts) => cat_tree(be, opts, config_file), + Command::Tree(opts) => cat_tree(be, config, opts), } } @@ -84,15 +77,14 @@ fn cat_blob(be: &impl DecryptReadBackend, tpe: BlobType, opt: IdOpt) -> Result<( Ok(()) } -fn cat_tree( - be: &impl DecryptReadBackend, - mut opts: TreeOpts, - config_file: RusticConfig, -) -> Result<()> { - config_file.merge_into("snapshot-filter", &mut opts.filter)?; - +fn cat_tree(be: &impl DecryptReadBackend, config: Config, opts: TreeOpts) -> Result<()> { let (id, path) = opts.snap.split_once(':').unwrap_or((&opts.snap, "")); - let snap = SnapshotFile::from_str(be, id, |sn| sn.matches(&opts.filter), progress_counter(""))?; + let snap = SnapshotFile::from_str( + be, + id, + |sn| sn.matches(&config.snapshot_filter), + progress_counter(""), + )?; let index = IndexBackend::new(be, progress_counter(""))?; let node = Tree::node_from_path(&index, snap.tree, Path::new(path))?; let id = node.subtree.ok_or_else(|| anyhow!("{path} is no dir"))?; diff --git a/src/commands/completions.rs b/src/commands/completions.rs index 5e4d3faf0..5296932b0 100644 --- a/src/commands/completions.rs +++ b/src/commands/completions.rs @@ -26,7 +26,7 @@ pub(super) fn execute(opts: Opts) { } fn generate_completion(shell: G, buf: &mut dyn Write) { - let mut command = super::Opts::command(); + let mut command = super::Args::command(); generate(shell, &mut command, env!("CARGO_BIN_NAME"), buf); } diff --git a/src/commands/configfile.rs b/src/commands/configfile.rs new file mode 100644 index 000000000..1bb11a88b --- /dev/null +++ b/src/commands/configfile.rs @@ -0,0 +1,64 @@ +use std::path::Path; + +use anyhow::{Context, Result}; +use clap::Parser; +use directories::ProjectDirs; +use merge::Merge; +use serde::Deserialize; + +use crate::{repofile::SnapshotFilter, repository::RepositoryOptions}; + +use super::{backup, copy, forget, GlobalOpts}; + +#[derive(Default, Parser, Deserialize, Merge)] +#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] +pub struct Config { + #[clap(flatten, next_help_heading = "Global options")] + pub global: GlobalOpts, + + #[clap(flatten, next_help_heading = "Repository options")] + pub repository: RepositoryOptions, + + #[clap(flatten, next_help_heading = "Snapshot filter options")] + pub snapshot_filter: SnapshotFilter, + + #[clap(skip)] + pub backup: backup::Opts, + + #[clap(skip)] + pub copy: copy::Targets, + + #[clap(skip)] + pub forget: forget::ConfigOpts, +} + +impl Config { + pub fn merge_profile(&mut self, profile: &str) -> Result<()> { + let mut path = match ProjectDirs::from("", "", "rustic") { + Some(path) => path.config_dir().to_path_buf(), + None => Path::new(".").to_path_buf(), + }; + if !path.exists() { + path = Path::new(".").to_path_buf(); + }; + let path = path.join(profile.to_string() + ".toml"); + + if path.exists() { + // TODO: This should be log::info! - however, the logging config + // can be stored in the config file and is needed to initialize the logger + eprintln!("using config {}", path.display()); + let data = std::fs::read_to_string(path).context("error reading config file")?; + let mut config: Self = + toml::from_str(&data).context("error reading TOML from config file")?; + // if "use_profile" is defined in config file, merge this referenced profile first + if !config.global.use_profile.is_empty() { + let profile = config.global.use_profile.clone(); + config.merge_profile(&profile)?; + } + self.merge(config); + } else { + eprintln!("using no config file ({} doesn't exist)", path.display()); + }; + Ok(()) + } +} diff --git a/src/commands/copy.rs b/src/commands/copy.rs index 0725183bc..fcd44bb5e 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -3,9 +3,11 @@ use std::collections::BTreeSet; use anyhow::{bail, Result}; use clap::Parser; use log::*; +use merge::Merge; use rayon::prelude::*; +use serde::Deserialize; -use super::{progress_counter, table_with_titles, GlobalOpts, RusticConfig}; +use super::{progress_counter, table_with_titles, Config}; use crate::backend::DecryptWriteBackend; use crate::blob::{BlobType, NodeType, Packer, TreeStreamerOnce}; use crate::index::{IndexBackend, IndexedBackend, Indexer, ReadIndex}; @@ -17,30 +19,22 @@ pub(super) struct Opts { /// Snapshots to copy. If none is given, use filter options to filter from all snapshots. #[clap(value_name = "ID")] ids: Vec, - - #[clap( - flatten, - next_help_heading = "Snapshot filter options (if no snapshot is given)" - )] - filter: SnapshotFilter, } -pub(super) fn execute( - repo: OpenRepository, - gopts: GlobalOpts, - mut opts: Opts, - config_file: RusticConfig, -) -> Result<()> { - config_file.merge_into("snapshot-filter", &mut opts.filter)?; +#[derive(Default, Deserialize, Merge)] +pub struct Targets { + #[merge(strategy = merge::vec::overwrite_empty)] + targets: Vec, +} - let target_opts: Vec = config_file.get("copy.targets")?; - if target_opts.is_empty() { +pub(super) fn execute(repo: OpenRepository, config: Config, opts: Opts) -> Result<()> { + if config.copy.targets.is_empty() { bail!("no [[copy.targets]] section in config file found!"); } let be = &repo.dbe; let mut snapshots = match opts.ids.is_empty() { - true => SnapshotFile::all_from_backend(be, &opts.filter)?, + true => SnapshotFile::all_from_backend(be, &config.snapshot_filter)?, false => SnapshotFile::from_ids(be, &opts.ids)?, }; // sort for nicer output @@ -51,13 +45,13 @@ pub(super) fn execute( let poly = repo.config.poly()?; - for target_opt in target_opts { - let repo_dest = Repository::new(target_opt)?.open()?; + for target_opt in &config.copy.targets { + let repo_dest = Repository::new(target_opt.clone())?.open()?; info!("copying to target {}...", repo_dest.name); 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, &gopts, &opts)?; + copy(&snapshots, index.clone(), repo_dest, &config)?; } Ok(()) } @@ -66,13 +60,12 @@ fn copy( snapshots: &[SnapshotFile], index: impl IndexedBackend, repo_dest: OpenRepository, - gopts: &GlobalOpts, - opts: &Opts, + config: &Config, ) -> Result<()> { let be_dest = &repo_dest.dbe; - let snapshots = relevant_snapshots(snapshots, &repo_dest, &opts.filter)?; - match (snapshots.len(), gopts.dry_run) { + let snapshots = relevant_snapshots(snapshots, &repo_dest, &config.snapshot_filter)?; + match (snapshots.len(), config.global.dry_run) { (count, true) => { info!("would have copied {count} snapshots"); return Ok(()); diff --git a/src/commands/diff.rs b/src/commands/diff.rs index e812a8bc5..26078c71f 100644 --- a/src/commands/diff.rs +++ b/src/commands/diff.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use anyhow::{anyhow, bail, Context, Result}; use clap::Parser; -use super::{progress_counter, RusticConfig}; +use super::{progress_counter, Config}; use crate::backend::{ LocalDestination, LocalSource, LocalSourceFilterOptions, LocalSourceSaveOptions, ReadSourceEntry, @@ -13,7 +13,7 @@ use crate::blob::{Node, NodeStreamer, NodeType, Tree}; use crate::commands::helpers::progress_spinner; use crate::crypto::hash; use crate::index::{IndexBackend, ReadIndex}; -use crate::repofile::{SnapshotFile, SnapshotFilter}; +use crate::repofile::SnapshotFile; use crate::repository::OpenRepository; #[derive(Parser)] @@ -36,19 +36,9 @@ pub(super) struct Opts { #[clap(flatten)] ignore_opts: LocalSourceFilterOptions, - - #[clap( - flatten, - next_help_heading = "Snapshot filter options (when using latest)" - )] - filter: SnapshotFilter, } -pub(super) fn execute( - repo: OpenRepository, - mut opts: Opts, - config_file: RusticConfig, -) -> Result<()> { +pub(super) fn execute(repo: OpenRepository, config: Config, opts: Opts) -> Result<()> { let be = &repo.dbe; let (id1, path1) = arg_to_snap_path(&opts.snap1, ""); let (id2, path2) = arg_to_snap_path(&opts.snap2, path1); @@ -77,10 +67,13 @@ pub(super) fn execute( } (Some(id1), None) => { // diff between snapshot and local path - config_file.merge_into("snapshot-filter", &mut opts.filter)?; - let p = progress_spinner("getting snapshot..."); - let snap1 = SnapshotFile::from_str(be, id1, |sn| sn.matches(&opts.filter), p.clone())?; + let snap1 = SnapshotFile::from_str( + be, + id1, + |sn| sn.matches(&config.snapshot_filter), + p.clone(), + )?; p.finish(); let index = IndexBackend::new(be, progress_counter(""))?; diff --git a/src/commands/dump.rs b/src/commands/dump.rs index 3819ec292..09e0b4d9e 100644 --- a/src/commands/dump.rs +++ b/src/commands/dump.rs @@ -5,34 +5,28 @@ use std::path::Path; use crate::blob::{BlobType, NodeType, Tree}; use crate::index::{IndexBackend, IndexedBackend}; -use crate::repofile::{SnapshotFile, SnapshotFilter}; +use crate::repofile::SnapshotFile; use crate::repository::OpenRepository; -use super::{progress_counter, RusticConfig}; +use super::{progress_counter, Config}; #[derive(Parser)] pub(super) struct Opts { /// file from snapshot to dump #[clap(value_name = "SNAPSHOT[:PATH]")] snap: String, - - #[clap( - flatten, - next_help_heading = "Snapshot filter options (when using latest)" - )] - filter: SnapshotFilter, } -pub(super) fn execute( - repo: OpenRepository, - mut opts: Opts, - config_file: RusticConfig, -) -> Result<()> { - config_file.merge_into("snapshot-filter", &mut opts.filter)?; +pub(super) fn execute(repo: OpenRepository, config: Config, opts: Opts) -> Result<()> { let be = &repo.dbe; let (id, path) = opts.snap.split_once(':').unwrap_or((&opts.snap, "")); - let snap = SnapshotFile::from_str(be, id, |sn| sn.matches(&opts.filter), progress_counter(""))?; + let snap = SnapshotFile::from_str( + be, + id, + |sn| sn.matches(&config.snapshot_filter), + progress_counter(""), + )?; let index = IndexBackend::new(be, progress_counter(""))?; let node = Tree::node_from_path(&index, snap.tree, Path::new(path))?; diff --git a/src/commands/forget.rs b/src/commands/forget.rs index 5f4fa4ac8..ddfe655b2 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, GlobalOpts, RusticConfig}; +use super::{progress_counter, prune, table_with_titles, Config}; use crate::backend::{DecryptWriteBackend, FileType}; use crate::repofile::{ SnapshotFile, SnapshotFilter, SnapshotGroup, SnapshotGroupCriterion, StringList, @@ -32,9 +32,9 @@ pub(super) struct Opts { } #[serde_as] -#[derive(Default, Parser, Deserialize, Merge)] +#[derive(Clone, Default, Parser, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case")] -struct ConfigOpts { +pub struct ConfigOpts { /// Group snapshots by any combination of host,label,paths,tags (default: "host,label,paths") #[clap(long, short = 'g', value_name = "CRITERION")] #[serde_as(as = "Option")] @@ -54,17 +54,12 @@ struct ConfigOpts { keep: KeepOptions, } -pub(super) fn execute( - repo: OpenRepository, - gopts: GlobalOpts, - mut opts: Opts, - config_file: RusticConfig, -) -> Result<()> { +pub(super) fn execute(repo: OpenRepository, config: Config, mut opts: Opts) -> Result<()> { let be = &repo.dbe; // merge "forget" section from config file, if given - config_file.merge_into("forget", &mut opts.config)?; + opts.config.merge(config.forget.clone()); // merge "snapshot-filter" section from config file, if given - config_file.merge_into("snapshot-filter", &mut opts.config.filter)?; + opts.config.filter.merge(config.snapshot_filter.clone()); let group_by = opts .config @@ -143,7 +138,7 @@ pub(super) fn execute( println!(); } - match (forget_snaps.is_empty(), gopts.dry_run) { + match (forget_snaps.is_empty(), config.global.dry_run) { (true, _) => println!("nothing to remove"), (false, true) => println!("would have removed the following snapshots:\n {forget_snaps:?}"), (false, false) => { @@ -153,7 +148,7 @@ pub(super) fn execute( } if opts.config.prune { - prune::execute(repo, gopts, opts.prune_opts, forget_snaps)?; + prune::execute(repo, config, opts.prune_opts, forget_snaps)?; } Ok(()) diff --git a/src/commands/ls.rs b/src/commands/ls.rs index 8aa963eab..1f62fd88f 100644 --- a/src/commands/ls.rs +++ b/src/commands/ls.rs @@ -3,10 +3,10 @@ use clap::Parser; use std::path::Path; use super::progress_counter; -use super::rustic_config::RusticConfig; +use super::Config; use crate::blob::{NodeStreamer, Tree, TreeStreamerOptions}; use crate::index::IndexBackend; -use crate::repofile::{SnapshotFile, SnapshotFilter}; +use crate::repofile::SnapshotFile; use crate::repository::OpenRepository; #[derive(Parser)] @@ -19,22 +19,11 @@ pub(super) struct Opts { #[clap(long)] recursive: bool, - #[clap( - flatten, - next_help_heading = "Snapshot filter options (when using latest)" - )] - filter: SnapshotFilter, - #[clap(flatten)] streamer_opts: TreeStreamerOptions, } -pub(super) fn execute( - repo: OpenRepository, - mut opts: Opts, - config_file: RusticConfig, -) -> Result<()> { - config_file.merge_into("snapshot-filter", &mut opts.filter)?; +pub(super) fn execute(repo: OpenRepository, config: Config, opts: Opts) -> Result<()> { let be = &repo.dbe; let mut recursive = opts.recursive; @@ -42,7 +31,12 @@ pub(super) fn execute( recursive = true; (&opts.snap, "") }); - let snap = SnapshotFile::from_str(be, id, |sn| sn.matches(&opts.filter), progress_counter(""))?; + let snap = SnapshotFile::from_str( + be, + id, + |sn| sn.matches(&config.snapshot_filter), + progress_counter(""), + )?; let index = IndexBackend::new(be, progress_counter(""))?; let node = Tree::node_from_path(&index, snap.tree, Path::new(path))?; diff --git a/src/commands/merge_cmd.rs b/src/commands/merge_cmd.rs index b327fbad3..b20948bbc 100644 --- a/src/commands/merge_cmd.rs +++ b/src/commands/merge_cmd.rs @@ -6,11 +6,11 @@ use log::*; use crate::backend::{DecryptWriteBackend, FileType}; use crate::blob::{merge_trees, BlobType, Node, Packer, Tree}; use crate::index::{IndexBackend, Indexer, ReadIndex}; -use crate::repofile::{PathList, SnapshotFile, SnapshotFilter, SnapshotOptions}; +use crate::repofile::{PathList, SnapshotFile, SnapshotOptions}; use crate::repository::OpenRepository; use super::helpers::{progress_counter, progress_spinner}; -use super::rustic_config::RusticConfig; +use super::Config; #[derive(Default, Parser)] pub(super) struct Opts { @@ -28,24 +28,19 @@ pub(super) struct Opts { #[clap(flatten, next_help_heading = "Snapshot options")] snap_opts: SnapshotOptions, - - #[clap(flatten, next_help_heading = "Snapshot filter options")] - filter: SnapshotFilter, } pub(super) fn execute( repo: OpenRepository, - mut opts: Opts, - config_file: RusticConfig, + config: Config, + opts: Opts, command: String, ) -> Result<()> { let now = Local::now(); - let be = &repo.dbe; - config_file.merge_into("snapshot-filter", &mut opts.filter)?; let snapshots = match opts.ids.is_empty() { - true => SnapshotFile::all_from_backend(be, &opts.filter)?, + true => SnapshotFile::all_from_backend(be, &config.snapshot_filter)?, false => SnapshotFile::from_ids(be, &opts.ids)?, }; let index = IndexBackend::only_full_trees(&be.clone(), progress_counter(""))?; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 818e0c764..2ecbce69b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,10 +6,10 @@ use clap::{Parser, Subcommand}; use merge::Merge; use serde::Deserialize; use serde_with::{serde_as, DisplayFromStr}; -use simplelog::*; +use simplelog::{ColorChoice, CombinedLogger, LevelFilter, TermLogger, TerminalMode, WriteLogger}; use crate::backend::{FileType, ReadBackend}; -use crate::repository::{Repository, RepositoryOptions}; +use crate::repository::Repository; use helpers::*; @@ -18,6 +18,7 @@ mod cat; mod check; mod completions; mod config; +mod configfile; mod copy; mod diff; mod dump; @@ -32,32 +33,17 @@ mod prune; mod repair; mod repoinfo; mod restore; -mod rustic_config; mod self_update; mod snapshots; mod tag; -use rustic_config::RusticConfig; +use configfile::Config; #[derive(Parser)] #[clap(about, version, name="rustic", version = option_env!("PROJECT_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")))] -struct Opts { - /// Config profile to use. This parses the file `.toml` in the config directory. - #[clap( - short = 'P', - long, - value_name = "PROFILE", - global = true, - default_value = "rustic", - help_heading = "Global options" - )] - config_profile: String, - - #[clap(flatten, next_help_heading = "Global options")] - global: GlobalOpts, - - #[clap(flatten, next_help_heading = "Repository options")] - repository: RepositoryOptions, +struct Args { + #[clap(flatten)] + config: Config, #[clap(subcommand)] command: Command, @@ -66,7 +52,19 @@ struct Opts { #[serde_as] #[derive(Default, Parser, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] -struct GlobalOpts { +pub struct GlobalOpts { + /// Config profile to use. This parses the file `.toml` in the config directory. + #[clap( + short = 'P', + long, + global = true, + value_name = "PROFILE", + default_value = "rustic", + env = "RUSTIC_USE_PROFILE" + )] + #[merge(skip)] + use_profile: String, + /// 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)] @@ -168,19 +166,19 @@ enum Command { pub fn execute() -> Result<()> { let command: Vec<_> = std::env::args_os().collect(); - let args = Opts::parse_from(&command); + let args = Args::parse_from(&command); + let mut config = args.config; // get global options from command line / env and config file - let config_file = RusticConfig::new(&args.config_profile)?; - let mut gopts = args.global; - config_file.merge_into("global", &mut gopts)?; + let profile = config.global.use_profile.clone(); + config.merge_profile(&profile)?; // start logger - let level_filter = gopts.log_level.unwrap_or(LevelFilter::Info); - match &gopts.log_file { + let level_filter = config.global.log_level.unwrap_or(LevelFilter::Info); + match &config.global.log_file { None => TermLogger::init( level_filter, - ConfigBuilder::new() + simplelog::ConfigBuilder::new() .set_time_level(LevelFilter::Off) .build(), TerminalMode::Stderr, @@ -190,7 +188,7 @@ pub fn execute() -> Result<()> { Some(file) => CombinedLogger::init(vec![ TermLogger::new( level_filter.max(LevelFilter::Warn), - ConfigBuilder::new() + simplelog::ConfigBuilder::new() .set_time_level(LevelFilter::Off) .build(), TerminalMode::Stderr, @@ -198,18 +196,18 @@ pub fn execute() -> Result<()> { ), WriteLogger::new( level_filter, - Config::default(), + simplelog::Config::default(), File::options().create(true).append(true).open(file)?, ), ])?, } - if gopts.no_progress { + if config.global.no_progress { let mut no_progress = NO_PROGRESS.lock().unwrap(); *no_progress = true; } - if let Some(duration) = gopts.progress_interval { + if let Some(duration) = config.global.progress_interval { let mut interval = PROGRESS_INTERVAL.lock().unwrap(); *interval = *duration; } @@ -230,9 +228,7 @@ pub fn execute() -> Result<()> { .collect::>() .join(" "); - let mut repo_opts = args.repository; - config_file.merge_into("repository", &mut repo_opts)?; - let repo = Repository::new(repo_opts)?; + let repo = Repository::new(config.repository.clone())?; if let Command::Init(opts) = args.command { let config_ids = repo.be.list(FileType::Config)?; @@ -243,27 +239,27 @@ pub fn execute() -> Result<()> { #[allow(clippy::match_same_arms)] match args.command { - Command::Backup(opts) => backup::execute(repo, gopts, opts, config_file, command)?, + Command::Backup(opts) => backup::execute(repo, config, opts, command)?, Command::Config(opts) => config::execute(repo, opts)?, - Command::Cat(opts) => cat::execute(repo, opts, config_file)?, + Command::Cat(opts) => cat::execute(repo, config, opts)?, Command::Check(opts) => check::execute(repo, opts)?, Command::Completions(_) => {} // already handled above - 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, gopts, opts, config_file)?, + Command::Copy(opts) => copy::execute(repo, config, opts)?, + Command::Diff(opts) => diff::execute(repo, config, opts)?, + Command::Dump(opts) => dump::execute(repo, config, opts)?, + Command::Forget(opts) => forget::execute(repo, config, opts)?, Command::Init(_) => {} // already handled above Command::Key(opts) => key::execute(repo, opts)?, Command::List(opts) => list::execute(repo, opts)?, - Command::Ls(opts) => ls::execute(repo, opts, config_file)?, - Command::Merge(opts) => merge_cmd::execute(repo, opts, config_file, command)?, + Command::Ls(opts) => ls::execute(repo, config, opts)?, + Command::Merge(opts) => merge_cmd::execute(repo, config, opts, command)?, Command::SelfUpdate(_) => {} // already handled above - Command::Snapshots(opts) => snapshots::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::Snapshots(opts) => snapshots::execute(repo, config, opts)?, + Command::Prune(opts) => prune::execute(repo, config, opts, vec![])?, + Command::Restore(opts) => restore::execute(repo, config, opts)?, + Command::Repair(opts) => repair::execute(repo, config, opts)?, Command::Repoinfo(opts) => repoinfo::execute(repo, opts)?, - Command::Tag(opts) => tag::execute(repo, gopts, opts, config_file)?, + Command::Tag(opts) => tag::execute(repo, config, opts)?, }; Ok(()) @@ -272,5 +268,5 @@ pub fn execute() -> Result<()> { #[test] fn verify_cli() { use clap::CommandFactory; - Opts::command().debug_assert() + Args::command().debug_assert() } diff --git a/src/commands/prune.rs b/src/commands/prune.rs index a06219456..faa13345c 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, GlobalOpts}; +use super::{bytes, no_progress, progress_bytes, progress_counter, warm_up_wait, Config}; use crate::backend::{DecryptReadBackend, DecryptWriteBackend, FileType, ReadBackend}; use crate::blob::{ BlobType, BlobTypeMap, Initialize, NodeType, PackSizer, Repacker, Sum, TreeStreamerOnce, @@ -73,7 +73,7 @@ pub(super) struct Opts { pub(super) fn execute( repo: OpenRepository, - gopts: GlobalOpts, + config: Config, opts: Opts, ignore_snaps: Vec, ) -> Result<()> { @@ -137,9 +137,10 @@ pub(super) fn execute( pruner.filter_index_files(opts.instant_delete); pruner.print_stats(); - warm_up_wait(&repo, pruner.repack_packs().into_iter(), !gopts.dry_run)?; + let dry_run = config.global.dry_run; + warm_up_wait(&repo, pruner.repack_packs().into_iter(), !dry_run)?; - if !gopts.dry_run { + if !dry_run { pruner.do_prune(repo, opts)?; } Ok(()) diff --git a/src/commands/repair.rs b/src/commands/repair.rs index 24a95346f..38d192756 100644 --- a/src/commands/repair.rs +++ b/src/commands/repair.rs @@ -12,13 +12,11 @@ use crate::blob::{BlobType, NodeType, Packer, Tree}; use crate::id::Id; use crate::index::{IndexBackend, IndexedBackend, Indexer, ReadIndex}; use crate::repofile::{ - ConfigFile, IndexFile, IndexPack, PackHeader, PackHeaderRef, SnapshotFile, SnapshotFilter, - StringList, + ConfigFile, IndexFile, IndexPack, PackHeader, PackHeaderRef, SnapshotFile, StringList, }; use crate::repository::OpenRepository; -use super::rustic_config::RusticConfig; -use super::{progress_counter, progress_spinner, warm_up_wait, GlobalOpts}; +use super::{progress_counter, progress_spinner, warm_up_wait, Config}; #[derive(Parser)] pub(super) struct Opts { @@ -43,9 +41,6 @@ struct IndexOpts { #[derive(Default, Parser)] struct SnapOpts { - #[clap(flatten, next_help_heading = "Snapshot filter options")] - filter: SnapshotFilter, - /// Also remove defect snapshots - WARNING: This can result in data loss! #[clap(long)] delete: bool, @@ -63,19 +58,14 @@ struct SnapOpts { ids: Vec, } -pub(super) fn execute( - repo: OpenRepository, - gopts: GlobalOpts, - opts: Opts, - config_file: RusticConfig, -) -> Result<()> { +pub(super) fn execute(repo: OpenRepository, config: Config, opts: Opts) -> Result<()> { match opts.command { - Command::Index(opt) => repair_index(&repo, gopts, opt), - Command::Snapshots(opt) => repair_snaps(&repo.dbe, gopts, opt, config_file, &repo.config), + Command::Index(opt) => repair_index(&repo, config, opt), + Command::Snapshots(opt) => repair_snaps(&repo.dbe, config, opt, &repo.config), } } -fn repair_index(repo: &OpenRepository, gopts: GlobalOpts, opts: IndexOpts) -> Result<()> { +fn repair_index(repo: &OpenRepository, config: Config, 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(); @@ -134,7 +124,7 @@ fn repair_index(repo: &OpenRepository, gopts: GlobalOpts, opts: IndexOpts) -> Re for p in index.packs_to_delete { process_pack(p, true, &mut new_index, &mut changed); } - match (changed, gopts.dry_run) { + match (changed, config.global.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() { @@ -169,7 +159,7 @@ fn repair_index(repo: &OpenRepository, gopts: GlobalOpts, opts: IndexOpts) -> Re ..Default::default() }; - if !gopts.dry_run { + if !config.global.dry_run { indexer.write().unwrap().add_with(pack, to_delete)?; } p.inc(1); @@ -182,15 +172,12 @@ fn repair_index(repo: &OpenRepository, gopts: GlobalOpts, opts: IndexOpts) -> Re fn repair_snaps( be: &impl DecryptFullBackend, - gopts: GlobalOpts, - mut opts: SnapOpts, - config_file: RusticConfig, - config: &ConfigFile, + config: Config, + opts: SnapOpts, + config_file: &ConfigFile, ) -> Result<()> { - config_file.merge_into("snapshot-filter", &mut opts.filter)?; - let snapshots = match opts.ids.is_empty() { - true => SnapshotFile::all_from_backend(be, &opts.filter)?, + true => SnapshotFile::all_from_backend(be, &config.snapshot_filter)?, false => SnapshotFile::from_ids(be, &opts.ids)?, }; @@ -204,7 +191,7 @@ fn repair_snaps( be.clone(), BlobType::Tree, indexer.clone(), - config, + config_file, index.total_size(BlobType::Tree), )?; @@ -217,7 +204,7 @@ fn repair_snaps( Some(snap.tree), &mut replaced, &mut seen, - &gopts, + &config, &opts, )? { (Changed::None, _) => { @@ -234,7 +221,7 @@ fn repair_snaps( } snap.set_tags(opts.tag.clone()); snap.tree = id; - if gopts.dry_run { + if config.global.dry_run { info!("would have modified snapshot {snap_id}."); } else { let new_id = be.save_file(&snap)?; @@ -245,13 +232,13 @@ fn repair_snaps( } } - if !gopts.dry_run { + if !config.global.dry_run { packer.finalize()?; indexer.write().unwrap().finalize()?; } if opts.delete { - if gopts.dry_run { + if config.global.dry_run { info!("would have removed {} snapshots.", delete.len()); } else { be.delete_list( @@ -279,7 +266,7 @@ fn repair_tree( id: Option, replaced: &mut HashMap, seen: &mut HashSet, - gopts: &GlobalOpts, + config: &Config, opts: &SnapOpts, ) -> Result<(Changed, Id)> { let (tree, changed) = match id { @@ -331,7 +318,7 @@ fn repair_tree( } NodeType::Dir {} => { let (c, tree_id) = - repair_tree(be, packer, node.subtree, replaced, seen, gopts, opts)?; + repair_tree(be, packer, node.subtree, replaced, seen, config, opts)?; match c { Changed::None => {} Changed::This => { @@ -363,7 +350,7 @@ fn repair_tree( (_, c) => { // the tree has been changed => save it let (chunk, new_id) = tree.serialize()?; - if !be.has_tree(&new_id) && !gopts.dry_run { + if !be.has_tree(&new_id) && !config.global.dry_run { packer.add(chunk.into(), new_id)?; } if let Some(id) = id { diff --git a/src/commands/restore.rs b/src/commands/restore.rs index d03247880..33afa3a4e 100644 --- a/src/commands/restore.rs +++ b/src/commands/restore.rs @@ -11,8 +11,7 @@ use ignore::{DirEntry, WalkBuilder}; use log::*; use rayon::ThreadPoolBuilder; -use super::rustic_config::RusticConfig; -use super::{bytes, progress_bytes, progress_counter, warm_up_wait, GlobalOpts}; +use super::{bytes, progress_bytes, progress_counter, warm_up_wait, Config}; use crate::backend::{DecryptReadBackend, FileType, LocalDestination}; use crate::blob::{Node, NodeStreamer, NodeType, Tree, TreeStreamerOptions}; use crate::commands::helpers::progress_spinner; @@ -59,17 +58,16 @@ pub(super) struct Opts { filter: SnapshotFilter, } -pub(super) fn execute( - repo: OpenRepository, - gopts: GlobalOpts, - mut opts: Opts, - config_file: RusticConfig, -) -> Result<()> { +pub(super) fn execute(repo: OpenRepository, config: Config, opts: Opts) -> Result<()> { let be = &repo.dbe; - config_file.merge_into("snapshot-filter", &mut opts.filter)?; let (id, path) = opts.snap.split_once(':').unwrap_or((&opts.snap, "")); - let snap = SnapshotFile::from_str(be, id, |sn| sn.matches(&opts.filter), progress_counter(""))?; + let snap = SnapshotFile::from_str( + be, + id, + |sn| sn.matches(&config.snapshot_filter), + progress_counter(""), + )?; let index = IndexBackend::new(be, progress_counter(""))?; let node = Tree::node_from_path(&index, snap.tree, Path::new(path))?; @@ -77,7 +75,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, &gopts, &opts)?; + let (file_infos, stats) = allocate_and_collect(&dest, index.clone(), &node, &config, &opts)?; p.finish(); let fs = stats.file; @@ -102,13 +100,17 @@ 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(), !gopts.dry_run)?; - if !gopts.dry_run { + warm_up_wait( + &repo, + file_infos.to_packs().into_iter(), + !config.global.dry_run, + )?; + if !config.global.dry_run { restore_contents(be, &dest, file_infos)?; } } - if !gopts.dry_run { + if !config.global.dry_run { let p = progress_spinner("setting metadata..."); restore_metadata(&dest, index, &node, &opts)?; p.finish(); @@ -138,7 +140,7 @@ fn allocate_and_collect( dest: &LocalDestination, index: impl IndexedBackend + Unpin, node: &Node, - gopts: &GlobalOpts, + config: &Config, opts: &Opts, ) -> Result<(FileInfos, RestoreStats)> { let dest_path = Path::new(&opts.dest); @@ -162,7 +164,7 @@ fn allocate_and_collect( } match ( opts.delete, - gopts.dry_run, + config.global.dry_run, entry.file_type().unwrap().is_dir(), ) { (true, true, true) => { @@ -207,7 +209,7 @@ fn allocate_and_collect( } else { stats.dir.restore += 1; debug!("to restore: {path:?}"); - if !gopts.dry_run { + if !config.global.dry_run { dest.create_dir(path) .with_context(|| format!("error creating {path:?}"))?; } @@ -236,7 +238,7 @@ fn allocate_and_collect( (true, AddFileResult::New(size) | AddFileResult::Modify(size)) => { stats.file.modify += 1; debug!("to modify: {path:?}"); - if !gopts.dry_run { + if !config.global.dry_run { // set the right file size dest.set_length(path, size) .with_context(|| format!("error setting length for {path:?}"))?; @@ -245,7 +247,7 @@ fn allocate_and_collect( (false, AddFileResult::New(size) | AddFileResult::Modify(size)) => { stats.file.restore += 1; debug!("to restore: {path:?}"); - if !gopts.dry_run { + if !config.global.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/rustic_config.rs b/src/commands/rustic_config.rs deleted file mode 100644 index 09c8a7bb7..000000000 --- a/src/commands/rustic_config.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::path::Path; - -use anyhow::{Context, Result}; -use directories::ProjectDirs; -use merge::Merge; -use serde::Deserialize; -use toml::Value; - -pub struct RusticConfig(Value); - -impl RusticConfig { - pub fn new(profile: &str) -> Result { - let mut path = match ProjectDirs::from("", "", "rustic") { - Some(path) => path.config_dir().to_path_buf(), - None => Path::new(".").to_path_buf(), - }; - if !path.exists() { - path = Path::new(".").to_path_buf(); - }; - let path = path.join(profile.to_string() + ".toml"); - - let config = if path.exists() { - // TODO: This should be log::info! - however, the logging config - // can be stored in the config file and is needed to initialize the logger - eprintln!("using config {}", path.display()); - let data = std::fs::read_to_string(path).context("error reading config file")?; - toml::from_str(&data).context("error reading TOML from config file")? - } else { - eprintln!("using no config file ({} doesn't exist)", path.display()); - Value::Array(Vec::new()) - }; - - Ok(RusticConfig( - config.try_into().context("reading config file")?, - )) - } - - fn get_value(&self, section: &str) -> Option<&Value> { - // loop over subsections separated by '.' - section - .split('.') - .fold(Some(&self.0), |acc, x| acc.and_then(|value| value.get(x))) - } - - pub fn merge_into<'de, Opts>(&self, section: &str, opts: &mut Opts) -> Result<()> - where - Opts: Merge + Deserialize<'de>, - { - if let Some(value) = self.get_value(section) { - let config: Opts = value - .clone() - .try_into() - .with_context(|| format!("reading section [{section}] in config file"))?; - opts.merge(config); - } - Ok(()) - } - - pub fn get<'de, Opts>(&self, section: &str) -> Result - where - Opts: Default + Deserialize<'de>, - { - match self.get_value(section) { - Some(value) => Ok(value.clone().try_into()?), - None => Ok(Opts::default()), - } - } -} diff --git a/src/commands/snapshots.rs b/src/commands/snapshots.rs index ae8c71feb..9789f86bf 100644 --- a/src/commands/snapshots.rs +++ b/src/commands/snapshots.rs @@ -6,10 +6,8 @@ use comfy_table::Cell; use humantime::format_duration; use itertools::Itertools; -use super::{bold_cell, bytes, table, table_right_from, RusticConfig}; -use crate::repofile::{ - DeleteOption, SnapshotFile, SnapshotFilter, SnapshotGroup, SnapshotGroupCriterion, -}; +use super::{bold_cell, bytes, table, table_right_from, Config}; +use crate::repofile::{DeleteOption, SnapshotFile, SnapshotGroup, SnapshotGroupCriterion}; use crate::repository::OpenRepository; #[derive(Parser)] @@ -38,22 +36,13 @@ pub(super) struct Opts { /// Show all snapshots instead of summarizing identical follow-up snapshots #[clap(long, conflicts_with_all = &["long", "json"])] all: bool, - - #[clap(flatten, next_help_heading = "Snapshot filter options")] - filter: SnapshotFilter, } -pub(super) fn execute( - repo: OpenRepository, - mut opts: Opts, - config_file: RusticConfig, -) -> Result<()> { - config_file.merge_into("snapshot-filter", &mut opts.filter)?; - +pub(super) fn execute(repo: OpenRepository, config: Config, opts: Opts) -> Result<()> { let groups = match &opts.ids[..] { - [] => SnapshotFile::group_from_backend(&repo.dbe, &opts.filter, &opts.group_by)?, + [] => SnapshotFile::group_from_backend(&repo.dbe, &config.snapshot_filter, &opts.group_by)?, [id] if id == "latest" => { - SnapshotFile::group_from_backend(&repo.dbe, &opts.filter, &opts.group_by)? + SnapshotFile::group_from_backend(&repo.dbe, &config.snapshot_filter, &opts.group_by)? .into_iter() .map(|(group, mut snaps)| { snaps.sort_unstable(); diff --git a/src/commands/tag.rs b/src/commands/tag.rs index ee4b53887..fed32156e 100644 --- a/src/commands/tag.rs +++ b/src/commands/tag.rs @@ -2,10 +2,10 @@ use anyhow::Result; use chrono::{Duration, Local}; use clap::Parser; -use super::{progress_counter, GlobalOpts, RusticConfig}; +use super::{progress_counter, Config}; use crate::backend::{DecryptWriteBackend, FileType}; use crate::id::Id; -use crate::repofile::{DeleteOption, SnapshotFile, SnapshotFilter, StringList}; +use crate::repofile::{DeleteOption, SnapshotFile, StringList}; use crate::repository::OpenRepository; #[derive(Parser)] @@ -15,12 +15,6 @@ pub(super) struct Opts { #[clap(value_name = "ID")] ids: Vec, - #[clap( - flatten, - next_help_heading = "Snapshot filter options (if no snapshot is given)" - )] - filter: SnapshotFilter, - /// Tags to add (can be specified multiple times) #[clap( long, @@ -64,17 +58,11 @@ pub(super) struct Opts { set_delete_after: Option, } -pub(super) fn execute( - repo: OpenRepository, - gopts: GlobalOpts, - mut opts: Opts, - config_file: RusticConfig, -) -> Result<()> { - config_file.merge_into("snapshot-filter", &mut opts.filter)?; +pub(super) fn execute(repo: OpenRepository, config: Config, opts: Opts) -> Result<()> { let be = &repo.dbe; let snapshots = match opts.ids.is_empty() { - true => SnapshotFile::all_from_backend(be, &opts.filter)?, + true => SnapshotFile::all_from_backend(be, &config.snapshot_filter)?, false => SnapshotFile::from_ids(be, &opts.ids)?, }; @@ -99,7 +87,7 @@ pub(super) fn execute( snap.id = Id::default(); } - match (old_snap_ids.is_empty(), gopts.dry_run) { + match (old_snap_ids.is_empty(), config.global.dry_run) { (true, _) => println!("no snapshot changed."), (false, true) => { println!("would have modified the following snapshots:\n {old_snap_ids:?}"); diff --git a/src/repofile/snapshotfile.rs b/src/repofile/snapshotfile.rs index fe044d59f..8e079725f 100644 --- a/src/repofile/snapshotfile.rs +++ b/src/repofile/snapshotfile.rs @@ -17,7 +17,7 @@ use path_dedot::ParseDot; use rhai::serde::to_dynamic; use rhai::{Dynamic, Engine, FnPtr, AST}; use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, DisplayFromStr}; +use serde_with::{serde_as, DeserializeFromStr, DisplayFromStr}; use super::Id; use crate::backend::{DecryptReadBackend, FileType, RepoFile}; @@ -440,7 +440,7 @@ impl SnapshotFn { } #[serde_as] -#[derive(Default, Parser, Deserialize, Merge)] +#[derive(Clone, Default, Parser, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct SnapshotFilter { /// Hostname to filter (can be specified multiple times) @@ -471,7 +471,7 @@ pub struct SnapshotFilter { filter_fn: Option, } -#[derive(Clone, Default, Deserialize)] +#[derive(Clone, Default, DeserializeFromStr)] pub struct SnapshotGroupCriterion { hostname: bool, label: bool, diff --git a/src/repository/mod.rs b/src/repository/mod.rs index e234e1ac3..77fce7b29 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -29,7 +29,7 @@ use crate::crypto::Key; use crate::repofile::{find_key_in_backend, ConfigFile}; #[serde_as] -#[derive(Default, Parser, Deserialize, Merge)] +#[derive(Clone, Default, Parser, Deserialize, Merge)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct RepositoryOptions { /// Repository to use