mirror of
https://github.com/rustic-rs/rustic.git
synced 2025-10-26 11:18:51 +00:00
Add config file support
This commit is contained in:
parent
f602535e4d
commit
aaa5b4f4d5
137
Cargo.lock
generated
137
Cargo.lock
generated
@ -238,7 +238,7 @@ dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time",
|
||||
"time 0.1.44",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
@ -374,6 +374,41 @@ dependencies = [
|
||||
"cipher 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derivative"
|
||||
version = "2.2.0"
|
||||
@ -420,6 +455,15 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "4.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
|
||||
dependencies = [
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "4.0.0"
|
||||
@ -852,6 +896,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
@ -889,6 +939,7 @@ checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1032,6 +1083,28 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "merge"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9"
|
||||
dependencies = [
|
||||
"merge_derive",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "merge_derive"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07"
|
||||
dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.16"
|
||||
@ -1102,6 +1175,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.4.0"
|
||||
@ -1498,6 +1580,7 @@ dependencies = [
|
||||
"derivative",
|
||||
"derive-getters",
|
||||
"derive_more",
|
||||
"directories",
|
||||
"dirs",
|
||||
"enum-map",
|
||||
"enum-map-derive",
|
||||
@ -1511,6 +1594,7 @@ dependencies = [
|
||||
"integer-sqrt",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"merge",
|
||||
"nix",
|
||||
"path-absolutize",
|
||||
"prettytable-rs",
|
||||
@ -1525,11 +1609,13 @@ dependencies = [
|
||||
"serde",
|
||||
"serde-aux",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml",
|
||||
"users",
|
||||
"vlog",
|
||||
"walkdir",
|
||||
@ -1695,6 +1781,34 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89df7a26519371a3cce44fbb914c2819c84d9b897890987fa3ab096491cc0ea8"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time 0.3.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de337f322382fcdfbb21a014f7c224ee041a23785651db67b9827403178f698f"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.2"
|
||||
@ -1881,6 +1995,18 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"libc",
|
||||
"num_threads",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
@ -1953,6 +2079,15 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.2"
|
||||
|
||||
@ -67,6 +67,10 @@ dirs = "4"
|
||||
cachedir = "0.3"
|
||||
# commands
|
||||
clap = { version = "3", features = ["derive", "env"] }
|
||||
directories = "4"
|
||||
toml = "0.5"
|
||||
merge = "0.1"
|
||||
serde_with = "2"
|
||||
rpassword = "7"
|
||||
prettytable-rs = {version = "0.9", default-features = false }
|
||||
bytesize = "1"
|
||||
|
||||
@ -27,6 +27,7 @@ Look at the [FAQ][3] or open an issue!
|
||||
Improvements:
|
||||
* Allows using cold storage (e.g. AWS Glacier) repos which are only read in the `restore` command + supports warm-up
|
||||
* Completely lock-free pruning; option `instant-delete` is available
|
||||
* Supports configuration in a config file ([example config files](https://github.com/rustic-rs/rustic/tree/main/examples))
|
||||
* Huge decrease in memory requirement
|
||||
* Pack size can be customized
|
||||
* Already faster than restic for most operations (but not yet fully speed optimized)
|
||||
|
||||
31
examples/local.toml
Normal file
31
examples/local.toml
Normal file
@ -0,0 +1,31 @@
|
||||
# rustic config file to backup /home, /etc and /root to a local repository
|
||||
#
|
||||
# backup usage: "rustic-rs -P local backup
|
||||
# cleanup: "rustic-rs -P local forget --prune
|
||||
#
|
||||
[global]
|
||||
repository = "/backup/rustic"
|
||||
password-file = "/root/key-rustic"
|
||||
no-cache = true # no cache needed for local repository
|
||||
|
||||
[forget]
|
||||
keep-hourly = 20
|
||||
keep-daily = 14
|
||||
keep-weekly = 8
|
||||
keep-monthly = 24
|
||||
keep-yearly = 10
|
||||
|
||||
[backup]
|
||||
exclude-if-present = [".nobackup", "CACHEDIR.TAG"]
|
||||
glob-file = ["/root/rustic-local.glob"]
|
||||
one-filesystem = true
|
||||
|
||||
[[backup.sources]]
|
||||
source = "/home"
|
||||
git-ignore = true
|
||||
|
||||
[[backup.sources]]
|
||||
source = "/etc"
|
||||
|
||||
[[backup.sources]]
|
||||
source = "/root"
|
||||
31
examples/ovh-hot-cold.toml
Normal file
31
examples/ovh-hot-cold.toml
Normal file
@ -0,0 +1,31 @@
|
||||
# rustic config file to backup /home, /etc and /root to a hot/cold repository hosted by OVH
|
||||
#
|
||||
# backup usage: "rustic-rs -P ovh-hot-cold backup
|
||||
# cleanup: "rustic-rs -P ovh-hot-cold forget --prune
|
||||
#
|
||||
[global]
|
||||
repository = "rclone:ovh:backup-home"
|
||||
repo-hot = "rclone:ovh:backup-home-hot"
|
||||
password-file = "/root/key-rustic-ovh"
|
||||
cache-dir = "/var/lib/cache/rustic" # explicitely specify cache dir for remote repository
|
||||
|
||||
[forget]
|
||||
keep-daily = 8
|
||||
keep-weekly = 5
|
||||
keep-monthly = 13
|
||||
keep-yearly = 10
|
||||
|
||||
[backup]
|
||||
exclude-if-present = [".nobackup", "CACHEDIR.TAG"]
|
||||
glob-file = ["/root/rustic-ovh.glob"]
|
||||
one-filesystem = true
|
||||
|
||||
[[backup.sources]]
|
||||
source = "/home"
|
||||
git-ignore = true
|
||||
|
||||
[[backup.sources]]
|
||||
source = "/etc"
|
||||
|
||||
[[backup.sources]]
|
||||
source = "/root"
|
||||
40
examples/rustic.toml
Normal file
40
examples/rustic.toml
Normal file
@ -0,0 +1,40 @@
|
||||
# Example rustic config file.
|
||||
#
|
||||
# This file should be placed in the user's local config dir (~/.config/rustic/)
|
||||
# If you save it under NAME.toml, use "rustic-rs -P NAME" to access this profile.
|
||||
#
|
||||
# Note that most options can be overwritten by the corresponding command line option.
|
||||
|
||||
# global options: These options are used for all commands.
|
||||
[global]
|
||||
repository = "/tmp/rustic"
|
||||
password = "mySecretPassword"
|
||||
|
||||
# snapshot-filter options: These options appy to the snapshots, tag and forget command.
|
||||
[snapshot-filter]
|
||||
filter-host = ["myhost"]
|
||||
|
||||
# backup options: These options are used for all sources when calling the backup command.
|
||||
# They can be overwritten by source-specific options (see below) or command line options.
|
||||
[backup]
|
||||
git-ignore = true
|
||||
|
||||
# backup options can be given for specific sources. These options only apply
|
||||
# when calling "rustic-rs backup SOURCE".
|
||||
#
|
||||
# Note that if you call "rustic-rs backup" without any source, all sources from this config
|
||||
# file will be processed.
|
||||
[[backup.sources]]
|
||||
source = "/data/dir"
|
||||
|
||||
[[backup.sources]]
|
||||
source = "/home"
|
||||
glob = ["!/home/*/Downloads/*"]
|
||||
|
||||
# forget options
|
||||
[forget]
|
||||
filter-host = ["forgethost"] # <- this overwrites the snapshot-filter option defined above
|
||||
keep-tags = ["mytag"]
|
||||
keep-within-daily = "7 days"
|
||||
keep-monthly = 5
|
||||
keep-yearly = 2
|
||||
@ -7,6 +7,9 @@ use bytesize::ByteSize;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use clap::Parser;
|
||||
use ignore::{overrides::OverrideBuilder, DirEntry, Walk, WalkBuilder};
|
||||
use merge::Merge;
|
||||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, DisplayFromStr};
|
||||
use users::{Groups, Users, UsersCache};
|
||||
|
||||
use super::{node::Metadata, node::NodeType, Node, ReadSource};
|
||||
@ -18,42 +21,53 @@ pub struct LocalSource {
|
||||
cache: UsersCache,
|
||||
}
|
||||
|
||||
#[derive(Clone, Parser)]
|
||||
#[serde_as]
|
||||
#[derive(Default, Clone, Parser, Deserialize, Merge)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct LocalSourceOptions {
|
||||
/// Save access time for files and directories
|
||||
#[clap(long)]
|
||||
#[merge(strategy = merge::bool::overwrite_false)]
|
||||
with_atime: bool,
|
||||
|
||||
/// Glob pattern to exclude/include (can be specified multiple times)
|
||||
#[clap(long, short = 'g', help_heading = "EXCLUDE OPTIONS")]
|
||||
#[merge(strategy = merge::vec::overwrite_empty)]
|
||||
glob: Vec<String>,
|
||||
|
||||
/// Same as --glob pattern but ignores the casing of filenames
|
||||
#[clap(long, value_name = "GLOB", help_heading = "EXCLUDE OPTIONS")]
|
||||
#[merge(strategy = merge::vec::overwrite_empty)]
|
||||
iglob: Vec<String>,
|
||||
|
||||
/// Read glob patterns to exclude/include from this file (can be specified multiple times)
|
||||
#[clap(long, value_name = "FILE", help_heading = "EXCLUDE OPTIONS")]
|
||||
#[merge(strategy = merge::vec::overwrite_empty)]
|
||||
glob_file: Vec<String>,
|
||||
|
||||
/// Same as --glob-file ignores the casing of filenames in patterns
|
||||
#[clap(long, value_name = "FILE", help_heading = "EXCLUDE OPTIONS")]
|
||||
#[merge(strategy = merge::vec::overwrite_empty)]
|
||||
iglob_file: Vec<String>,
|
||||
|
||||
/// Ignore files based on .gitignore files
|
||||
#[clap(long, help_heading = "EXCLUDE OPTIONS")]
|
||||
#[merge(strategy = merge::bool::overwrite_false)]
|
||||
git_ignore: bool,
|
||||
|
||||
/// Exclude contents of directories containing this filename (can be specified multiple times)
|
||||
#[clap(long, value_name = "FILE", help_heading = "EXCLUDE OPTIONS")]
|
||||
#[merge(strategy = merge::vec::overwrite_empty)]
|
||||
exclude_if_present: Vec<String>,
|
||||
|
||||
/// Exclude other file systems, don't cross filesystem boundaries and subvolumes
|
||||
#[clap(long, short = 'x', help_heading = "EXCLUDE OPTIONS")]
|
||||
#[merge(strategy = merge::bool::overwrite_false)]
|
||||
one_file_system: bool,
|
||||
|
||||
/// Maximum size of files to be backuped. Larger files will be excluded.
|
||||
#[clap(long, value_name = "SIZE", help_heading = "EXCLUDE OPTIONS")]
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
exclude_larger_than: Option<ByteSize>,
|
||||
}
|
||||
|
||||
|
||||
@ -4,10 +4,13 @@ use anyhow::{anyhow, Result};
|
||||
use chrono::{Duration, Local};
|
||||
use clap::{AppSettings, Parser};
|
||||
use gethostname::gethostname;
|
||||
use merge::Merge;
|
||||
use path_absolutize::*;
|
||||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, DisplayFromStr};
|
||||
use vlog::*;
|
||||
|
||||
use super::{bytes, progress_bytes, progress_counter};
|
||||
use super::{bytes, progress_bytes, progress_counter, RusticConfig};
|
||||
use crate::archiver::{Archiver, Parent};
|
||||
use crate::backend::{
|
||||
DecryptFullBackend, DecryptWriteBackend, DryRunBackend, LocalSource, LocalSourceOptions,
|
||||
@ -17,11 +20,14 @@ use crate::blob::{Metadata, Node, NodeType};
|
||||
use crate::index::IndexBackend;
|
||||
use crate::repo::{ConfigFile, DeleteOption, SnapshotFile, SnapshotSummary, StringList};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[serde_as]
|
||||
#[derive(Clone, Default, Parser, Deserialize, Merge)]
|
||||
#[clap(global_setting(AppSettings::DeriveDisplayOrder))]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub(super) struct Opts {
|
||||
/// 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,
|
||||
|
||||
/// Snapshot to use as parent
|
||||
@ -30,61 +36,97 @@ pub(super) struct Opts {
|
||||
|
||||
/// Use no parent, read all files
|
||||
#[clap(long, short, conflicts_with = "parent")]
|
||||
#[merge(strategy = merge::bool::overwrite_false)]
|
||||
force: bool,
|
||||
|
||||
/// Ignore ctime changes when checking for modified files
|
||||
#[clap(long, conflicts_with = "force")]
|
||||
#[merge(strategy = merge::bool::overwrite_false)]
|
||||
ignore_ctime: bool,
|
||||
|
||||
/// Ignore inode number changes when checking for modified files
|
||||
#[clap(long, conflicts_with = "force")]
|
||||
#[merge(strategy = merge::bool::overwrite_false)]
|
||||
ignore_inode: bool,
|
||||
|
||||
/// Tags to add to backup (can be specified multiple times)
|
||||
#[clap(long, value_name = "TAG[,TAG,..]")]
|
||||
#[serde_as(as = "Vec<DisplayFromStr>")]
|
||||
#[merge(strategy = merge::vec::overwrite_empty)]
|
||||
tag: Vec<StringList>,
|
||||
|
||||
/// Mark snapshot as uneraseable
|
||||
#[clap(long, conflicts_with = "delete-after")]
|
||||
#[merge(strategy = merge::bool::overwrite_false)]
|
||||
delete_never: bool,
|
||||
|
||||
/// Mark snapshot to be deleted after given duration (e.g. 10d)
|
||||
#[clap(long, value_name = "DURATION")]
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
delete_after: Option<humantime::Duration>,
|
||||
|
||||
/// Set filename to be used when backing up from stdin
|
||||
#[clap(long, value_name = "FILENAME", default_value = "stdin")]
|
||||
#[merge(skip)]
|
||||
stdin_filename: String,
|
||||
|
||||
#[clap(flatten)]
|
||||
#[serde(flatten)]
|
||||
ignore_opts: LocalSourceOptions,
|
||||
|
||||
/// Backup source, use - for stdin
|
||||
/// 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")]
|
||||
#[merge(skip)]
|
||||
#[serde(skip)]
|
||||
sources: Vec<String>,
|
||||
|
||||
/// Backup source, used within config file
|
||||
#[clap(skip)]
|
||||
#[merge(skip)]
|
||||
source: String,
|
||||
}
|
||||
|
||||
pub(super) async fn execute(
|
||||
be: &impl DecryptFullBackend,
|
||||
opts: Opts,
|
||||
config: ConfigFile,
|
||||
config_file: RusticConfig,
|
||||
command: String,
|
||||
) -> Result<()> {
|
||||
let time = Local::now();
|
||||
let zstd = config.zstd()?;
|
||||
let mut be = DryRunBackend::new(be.clone(), opts.dry_run);
|
||||
be.set_zstd(zstd);
|
||||
|
||||
if opts.sources.is_empty() {
|
||||
v1!("no backup source given.");
|
||||
return Ok(());
|
||||
}
|
||||
let zstd = config.zstd()?;
|
||||
|
||||
let mut config_opts: Vec<Opts> = config_file.get("backup.sources")?;
|
||||
|
||||
let sources = match (opts.sources.is_empty(), config_opts.is_empty()) {
|
||||
(false, _) => opts.sources.clone(),
|
||||
(true, false) => {
|
||||
v1!("using all backup sources from config file.");
|
||||
config_opts.iter().map(|opt| opt.source.clone()).collect()
|
||||
}
|
||||
(true, true) => {
|
||||
v1!("no backup source given.");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let index = IndexBackend::only_full_trees(&be.clone(), progress_counter()).await?;
|
||||
|
||||
for source in opts.sources {
|
||||
for source in sources {
|
||||
let mut opts = opts.clone();
|
||||
|
||||
// merge Options from config file, if given
|
||||
if let Some(idx) = config_opts.iter().position(|opt| opt.source == *source) {
|
||||
opts.merge(config_opts.remove(idx));
|
||||
}
|
||||
// merge "backup" section from config file, if given
|
||||
config_file.merge_into("backup", &mut opts)?;
|
||||
|
||||
let mut be = DryRunBackend::new(be.clone(), opts.dry_run);
|
||||
be.set_zstd(zstd);
|
||||
v1!("\nbacking up {source}...");
|
||||
let be = be.clone();
|
||||
let index = index.clone();
|
||||
let backup_stdin = source == "-";
|
||||
let backup_path = if backup_stdin {
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::{DateTime, Datelike, Duration, Local, Timelike};
|
||||
use clap::{AppSettings, Parser};
|
||||
use derivative::Derivative;
|
||||
use merge::Merge;
|
||||
use prettytable::{format, row, Table};
|
||||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, DisplayFromStr};
|
||||
|
||||
use super::{progress_counter, prune};
|
||||
use super::{progress_counter, prune, RusticConfig};
|
||||
use crate::backend::{Cache, DecryptFullBackend, FileType};
|
||||
use crate::repo::{
|
||||
ConfigFile, SnapshotFile, SnapshotFilter, SnapshotGroup, SnapshotGroupCriterion, StringList,
|
||||
@ -13,20 +18,8 @@ use crate::repo::{
|
||||
#[derive(Parser)]
|
||||
#[clap(global_setting(AppSettings::DeriveDisplayOrder))]
|
||||
pub(super) struct Opts {
|
||||
/// Group snapshots by any combination of host,paths,tags
|
||||
#[clap(
|
||||
long,
|
||||
short = 'g',
|
||||
value_name = "CRITERION",
|
||||
default_value = "host,paths"
|
||||
)]
|
||||
group_by: SnapshotGroupCriterion,
|
||||
|
||||
#[clap(flatten, help_heading = "SNAPSHOT FILTER OPTIONS")]
|
||||
filter: SnapshotFilter,
|
||||
|
||||
#[clap(flatten, help_heading = "RETENTION OPTIONS")]
|
||||
keep: KeepOptions,
|
||||
#[clap(flatten)]
|
||||
config: ConfigOpts,
|
||||
|
||||
/// Also prune the repository
|
||||
#[clap(long)]
|
||||
@ -43,15 +36,45 @@ pub(super) struct Opts {
|
||||
ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Default, Parser, Deserialize, Merge)]
|
||||
#[clap(global_setting(AppSettings::DeriveDisplayOrder))]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
struct ConfigOpts {
|
||||
/// Group snapshots by any combination of host,paths,tags (default: "host,paths")
|
||||
#[clap(long, short = 'g', value_name = "CRITERION")]
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
group_by: Option<SnapshotGroupCriterion>,
|
||||
|
||||
#[clap(flatten, help_heading = "SNAPSHOT FILTER OPTIONS")]
|
||||
#[serde(flatten)]
|
||||
filter: SnapshotFilter,
|
||||
|
||||
#[clap(flatten, help_heading = "RETENTION OPTIONS")]
|
||||
#[serde(flatten)]
|
||||
keep: KeepOptions,
|
||||
}
|
||||
|
||||
pub(super) async fn execute(
|
||||
be: &(impl DecryptFullBackend + Unpin),
|
||||
cache: Option<Cache>,
|
||||
mut opts: Opts,
|
||||
config: ConfigFile,
|
||||
config_file: RusticConfig,
|
||||
) -> Result<()> {
|
||||
// merge "forget" section from config file, if given
|
||||
config_file.merge_into("forget", &mut opts.config)?;
|
||||
// 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
|
||||
.unwrap_or_else(|| SnapshotGroupCriterion::from_str("host,paths").unwrap());
|
||||
|
||||
let groups = match opts.ids.is_empty() {
|
||||
true => SnapshotFile::group_from_backend(be, &opts.filter, &opts.group_by).await?,
|
||||
true => SnapshotFile::group_from_backend(be, &opts.config.filter, &group_by).await?,
|
||||
false => vec![(
|
||||
SnapshotGroup::default(),
|
||||
SnapshotFile::from_ids(be, &opts.ids).await?,
|
||||
@ -65,7 +88,7 @@ pub(super) async fn execute(
|
||||
}
|
||||
snapshots.sort_unstable_by(|sn1, sn2| sn1.cmp(sn2).reverse());
|
||||
let latest_time = snapshots[0].time;
|
||||
let mut group_keep = opts.keep.clone();
|
||||
let mut group_keep = opts.config.keep.clone();
|
||||
let mut table = Table::new();
|
||||
|
||||
let mut iter = snapshots.iter().peekable();
|
||||
@ -74,7 +97,7 @@ pub(super) async fn execute(
|
||||
// snapshots that have no reason to be kept are removed. The only exception
|
||||
// is if no IDs are explicitely given and no keep option is set. In this
|
||||
// case, the default is to keep the snapshots.
|
||||
let default_keep = opts.ids.is_empty() && opts.keep == KeepOptions::default();
|
||||
let default_keep = opts.ids.is_empty() && group_keep == KeepOptions::default();
|
||||
|
||||
while let Some(sn) = iter.next() {
|
||||
let (action, reason) = {
|
||||
@ -135,72 +158,101 @@ pub(super) async fn execute(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Derivative, Parser)]
|
||||
#[serde_as]
|
||||
#[derive(Clone, PartialEq, Derivative, Parser, Deserialize, Merge)]
|
||||
#[derivative(Default)]
|
||||
struct KeepOptions {
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub(super) struct KeepOptions {
|
||||
/// Keep snapshots with this taglist (can be specified multiple times)
|
||||
#[clap(long, value_name = "TAG[,TAG,..]")]
|
||||
#[serde_as(as = "Vec<DisplayFromStr>")]
|
||||
#[merge(strategy=merge::vec::overwrite_empty)]
|
||||
keep_tags: Vec<StringList>,
|
||||
|
||||
/// Keep snapshots ids that start with ID (can be specified multiple times)
|
||||
#[clap(long = "keep-id", value_name = "ID")]
|
||||
#[merge(strategy=merge::vec::overwrite_empty)]
|
||||
keep_ids: Vec<String>,
|
||||
|
||||
/// Keep the last N snapshots
|
||||
#[clap(long, short = 'l', value_name = "N", default_value = "0")]
|
||||
#[merge(strategy=merge::num::overwrite_zero)]
|
||||
keep_last: u32,
|
||||
|
||||
/// Keep the last N hourly snapshots
|
||||
#[clap(long, short = 'H', value_name = "N", default_value = "0")]
|
||||
#[merge(strategy=merge::num::overwrite_zero)]
|
||||
keep_hourly: u32,
|
||||
|
||||
/// Keep the last N daily snapshots
|
||||
#[clap(long, short = 'd', value_name = "N", default_value = "0")]
|
||||
#[merge(strategy=merge::num::overwrite_zero)]
|
||||
keep_daily: u32,
|
||||
|
||||
/// Keep the last N weekly snapshots
|
||||
#[clap(long, short = 'w', value_name = "N", default_value = "0")]
|
||||
#[merge(strategy=merge::num::overwrite_zero)]
|
||||
keep_weekly: u32,
|
||||
|
||||
/// Keep the last N monthly snapshots
|
||||
#[clap(long, short = 'm', value_name = "N", default_value = "0")]
|
||||
#[merge(strategy=merge::num::overwrite_zero)]
|
||||
keep_monthly: u32,
|
||||
|
||||
/// Keep the last N yearly snapshots
|
||||
#[clap(long, short = 'y', value_name = "N", default_value = "0")]
|
||||
#[merge(strategy=merge::num::overwrite_zero)]
|
||||
keep_yearly: u32,
|
||||
|
||||
/// Keep snapshots newer than DURATION relative to latest snapshot
|
||||
#[clap(long, value_name = "DURATION", default_value = "0h")]
|
||||
#[derivative(Default(value = "std::time::Duration::ZERO.into()"))]
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[merge(strategy=overwrite_zero_duration)]
|
||||
keep_within: humantime::Duration,
|
||||
|
||||
/// Keep hourly snapshots newer than DURATION relative to latest snapshot
|
||||
#[clap(long, value_name = "DURATION", default_value = "0h")]
|
||||
#[derivative(Default(value = "std::time::Duration::ZERO.into()"))]
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[merge(strategy=overwrite_zero_duration)]
|
||||
keep_within_hourly: humantime::Duration,
|
||||
|
||||
/// Keep daily snapshots newer than DURATION relative to latest snapshot
|
||||
#[clap(long, value_name = "DURATION", default_value = "0d")]
|
||||
#[derivative(Default(value = "std::time::Duration::ZERO.into()"))]
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[merge(strategy=overwrite_zero_duration)]
|
||||
keep_within_daily: humantime::Duration,
|
||||
|
||||
/// Keep weekly snapshots newer than DURATION relative to latest snapshot
|
||||
#[clap(long, value_name = "DURATION", default_value = "0w")]
|
||||
#[derivative(Default(value = "std::time::Duration::ZERO.into()"))]
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[merge(strategy=overwrite_zero_duration)]
|
||||
keep_within_weekly: humantime::Duration,
|
||||
|
||||
/// Keep monthly snapshots newer than DURATION relative to latest snapshot
|
||||
#[clap(long, value_name = "DURATION", default_value = "0m")]
|
||||
#[derivative(Default(value = "std::time::Duration::ZERO.into()"))]
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[merge(strategy=overwrite_zero_duration)]
|
||||
keep_within_monthly: humantime::Duration,
|
||||
|
||||
/// Keep yearly snapshots newer than DURATION relative to latest snapshot
|
||||
#[clap(long, value_name = "DURATION", default_value = "0y")]
|
||||
#[derivative(Default(value = "std::time::Duration::ZERO.into()"))]
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
#[merge(strategy=overwrite_zero_duration)]
|
||||
keep_within_yearly: humantime::Duration,
|
||||
}
|
||||
|
||||
fn overwrite_zero_duration(left: &mut humantime::Duration, right: humantime::Duration) {
|
||||
if *left == std::time::Duration::ZERO.into() {
|
||||
*left = right;
|
||||
}
|
||||
}
|
||||
|
||||
fn always_false(_sn1: &SnapshotFile, _sn2: &SnapshotFile) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use merge::Merge;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::backend::{
|
||||
Cache, CachedBackend, ChooseBackend, DecryptBackend, DecryptReadBackend, FileType,
|
||||
@ -23,42 +25,48 @@ mod ls;
|
||||
mod prune;
|
||||
mod repoinfo;
|
||||
mod restore;
|
||||
mod rustic_config;
|
||||
mod self_update;
|
||||
mod snapshots;
|
||||
mod tag;
|
||||
|
||||
use helpers::*;
|
||||
use rustic_config::RusticConfig;
|
||||
use vlog::*;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(about, version)]
|
||||
struct Opts {
|
||||
/// Repository to use
|
||||
#[clap(flatten, help_heading = "GLOBAL OPTIONS")]
|
||||
global: GlobalOpts,
|
||||
|
||||
/// Config profile to use. This parses the file <PROFILE>.toml in the config directory.
|
||||
#[clap(
|
||||
short,
|
||||
short = 'P',
|
||||
long,
|
||||
value_name = "PROFILE",
|
||||
global = true,
|
||||
env = "RUSTIC_REPOSITORY",
|
||||
help_heading = "GLOBAL OPTIONS"
|
||||
default_value = "rustic"
|
||||
)]
|
||||
config_profile: String,
|
||||
|
||||
#[clap(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Default, Parser, Deserialize, Merge)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
struct GlobalOpts {
|
||||
/// Repository to use
|
||||
#[clap(short, long, global = true, env = "RUSTIC_REPOSITORY")]
|
||||
repository: Option<String>,
|
||||
|
||||
/// Repository to use as hot storage
|
||||
#[clap(
|
||||
long,
|
||||
global = true,
|
||||
env = "RUSTIC_REPO_HOT",
|
||||
help_heading = "GLOBAL OPTIONS"
|
||||
)]
|
||||
#[clap(long, global = true, env = "RUSTIC_REPO_HOT")]
|
||||
repo_hot: Option<String>,
|
||||
|
||||
/// Password of the repository - WARNING: Using --password can reveal the password in the process list!
|
||||
#[clap(
|
||||
long,
|
||||
global = true,
|
||||
env = "RUSTIC_PASSWORD",
|
||||
help_heading = "GLOBAL OPTIONS"
|
||||
)]
|
||||
#[clap(long, global = true, env = "RUSTIC_PASSWORD")]
|
||||
password: Option<String>,
|
||||
|
||||
/// File to read the password from
|
||||
@ -68,7 +76,6 @@ struct Opts {
|
||||
global = true,
|
||||
parse(from_os_str),
|
||||
env = "RUSTIC_PASSWORD_FILE",
|
||||
help_heading = "GLOBAL OPTIONS",
|
||||
conflicts_with = "password"
|
||||
)]
|
||||
password_file: Option<PathBuf>,
|
||||
@ -78,19 +85,13 @@ struct Opts {
|
||||
long,
|
||||
global = true,
|
||||
env = "RUSTIC_PASSWORD_COMMAND",
|
||||
help_heading = "GLOBAL OPTIONS",
|
||||
conflicts_with_all = &["password", "password-file"],
|
||||
)]
|
||||
password_command: Option<String>,
|
||||
|
||||
/// Increase verbosity (can be used multiple times)
|
||||
#[clap(
|
||||
long,
|
||||
short = 'v',
|
||||
global = true,
|
||||
parse(from_occurrences),
|
||||
help_heading = "GLOBAL OPTIONS"
|
||||
)]
|
||||
#[clap(long, short = 'v', global = true, parse(from_occurrences))]
|
||||
#[merge(strategy = merge::num::overwrite_zero)]
|
||||
verbose: i8,
|
||||
|
||||
/// Don't be verbose at all
|
||||
@ -99,18 +100,14 @@ struct Opts {
|
||||
short = 'q',
|
||||
global = true,
|
||||
parse(from_occurrences),
|
||||
conflicts_with = "verbose",
|
||||
help_heading = "GLOBAL OPTIONS"
|
||||
conflicts_with = "verbose"
|
||||
)]
|
||||
#[merge(strategy = merge::num::overwrite_zero)]
|
||||
quiet: i8,
|
||||
|
||||
/// Don't use a cache.
|
||||
#[clap(
|
||||
long,
|
||||
global = true,
|
||||
env = "RUSTIC_NO_CACHE",
|
||||
help_heading = "GLOBAL OPTIONS"
|
||||
)]
|
||||
#[clap(long, global = true, env = "RUSTIC_NO_CACHE")]
|
||||
#[merge(strategy = merge::bool::overwrite_false)]
|
||||
no_cache: bool,
|
||||
|
||||
/// Use this dir as cache dir instead of the standard cache dir
|
||||
@ -119,13 +116,9 @@ struct Opts {
|
||||
global = true,
|
||||
parse(from_os_str),
|
||||
conflicts_with = "no-cache",
|
||||
env = "RUSTIC_CACHE_DIR",
|
||||
help_heading = "GLOBAL OPTIONS"
|
||||
env = "RUSTIC_CACHE_DIR"
|
||||
)]
|
||||
cache_dir: Option<PathBuf>,
|
||||
|
||||
#[clap(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@ -183,6 +176,11 @@ pub async fn execute() -> Result<()> {
|
||||
let command: Vec<_> = std::env::args_os().into_iter().collect();
|
||||
let args = Opts::parse_from(&command);
|
||||
|
||||
let config_file = RusticConfig::new(&args.config_profile)?;
|
||||
|
||||
let mut opts = args.global;
|
||||
config_file.merge_into("global", &mut opts)?;
|
||||
|
||||
if let Command::SelfUpdate(opts) = args.command {
|
||||
self_update::execute(opts).await?;
|
||||
return Ok(());
|
||||
@ -194,15 +192,15 @@ pub async fn execute() -> Result<()> {
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
let verbosity = (1 + args.verbose - args.quiet).clamp(0, 3);
|
||||
let verbosity = (1 + opts.verbose - opts.quiet).clamp(0, 3);
|
||||
set_verbosity_level(verbosity as usize);
|
||||
|
||||
let be = match &args.repository {
|
||||
let be = match &opts.repository {
|
||||
Some(repo) => ChooseBackend::from_url(repo)?,
|
||||
None => bail!("No repository given. Please use the --repository option."),
|
||||
};
|
||||
|
||||
let be_hot = args
|
||||
let be_hot = opts
|
||||
.repo_hot
|
||||
.map(|repo| ChooseBackend::from_url(&repo))
|
||||
.transpose()?;
|
||||
@ -225,9 +223,9 @@ pub async fn execute() -> Result<()> {
|
||||
|
||||
let key = get_key(
|
||||
&be,
|
||||
args.password.as_deref(),
|
||||
args.password_file.as_deref(),
|
||||
args.password_command.as_deref(),
|
||||
opts.password.as_deref(),
|
||||
opts.password_file.as_deref(),
|
||||
opts.password_command.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
ve1!("password is correct.");
|
||||
@ -239,8 +237,8 @@ pub async fn execute() -> Result<()> {
|
||||
(false, true) => bail!("repo-hot is not a hot repository! Aborting."),
|
||||
_ => {}
|
||||
}
|
||||
let cache = (!args.no_cache)
|
||||
.then(|| Cache::new(config.id, args.cache_dir).ok())
|
||||
let cache = (!opts.no_cache)
|
||||
.then(|| Cache::new(config.id, opts.cache_dir).ok())
|
||||
.flatten();
|
||||
match &cache {
|
||||
None => v1!("using no cache"),
|
||||
@ -255,22 +253,22 @@ pub async fn execute() -> Result<()> {
|
||||
};
|
||||
|
||||
match cmd {
|
||||
Command::Backup(opts) => backup::execute(&dbe, opts, config, command).await?,
|
||||
Command::Backup(opts) => backup::execute(&dbe, opts, config, config_file, command).await?,
|
||||
Command::Config(opts) => config::execute(&dbe, &be_hot, opts, config).await?,
|
||||
Command::Cat(opts) => cat::execute(&dbe, opts).await?,
|
||||
Command::Check(opts) => check::execute(&dbe, &cache, &be_hot, &be, opts).await?,
|
||||
Command::Diff(opts) => diff::execute(&dbe, opts).await?,
|
||||
Command::Forget(opts) => forget::execute(&dbe, cache, opts, config).await?,
|
||||
Command::Forget(opts) => forget::execute(&dbe, cache, opts, config, config_file).await?,
|
||||
Command::Init(_) => {} // already handled above
|
||||
Command::Key(opts) => key::execute(&dbe, key, opts).await?,
|
||||
Command::List(opts) => list::execute(&dbe, opts).await?,
|
||||
Command::Ls(opts) => ls::execute(&dbe, opts).await?,
|
||||
Command::SelfUpdate(_) => {} // already handled above
|
||||
Command::Snapshots(opts) => snapshots::execute(&dbe, opts).await?,
|
||||
Command::Snapshots(opts) => snapshots::execute(&dbe, opts, config_file).await?,
|
||||
Command::Prune(opts) => prune::execute(&dbe, cache, opts, config, vec![]).await?,
|
||||
Command::Restore(opts) => restore::execute(&dbe, opts).await?,
|
||||
Command::Repoinfo(opts) => repoinfo::execute(&dbe, &be_hot, opts).await?,
|
||||
Command::Tag(opts) => tag::execute(&dbe, opts).await?,
|
||||
Command::Tag(opts) => tag::execute(&dbe, opts, config_file).await?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
|
||||
60
src/commands/rustic_config.rs
Normal file
60
src/commands/rustic_config.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::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<Self> {
|
||||
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() {
|
||||
eprintln!("using config {}", path.display());
|
||||
let data = std::fs::read_to_string(path)?;
|
||||
toml::from_str(&data)?
|
||||
} else {
|
||||
toml::from_str("")?
|
||||
};
|
||||
|
||||
Ok(RusticConfig(config))
|
||||
}
|
||||
|
||||
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()?;
|
||||
opts.merge(config);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get<'de, Opts>(&self, section: &str) -> Result<Opts>
|
||||
where
|
||||
Opts: Default + Deserialize<'de>,
|
||||
{
|
||||
match self.get_value(section) {
|
||||
Some(value) => Ok(value.clone().try_into()?),
|
||||
None => Ok(Opts::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,7 @@ use humantime::format_duration;
|
||||
use itertools::Itertools;
|
||||
use prettytable::{format, row, Table};
|
||||
|
||||
use super::bytes;
|
||||
use super::{bytes, RusticConfig};
|
||||
use crate::backend::DecryptReadBackend;
|
||||
use crate::repo::{
|
||||
DeleteOption, SnapshotFile, SnapshotFilter, SnapshotGroup, SnapshotGroupCriterion,
|
||||
@ -39,7 +39,13 @@ pub(super) struct Opts {
|
||||
ids: Vec<String>,
|
||||
}
|
||||
|
||||
pub(super) async fn execute(be: &impl DecryptReadBackend, opts: Opts) -> Result<()> {
|
||||
pub(super) async fn execute(
|
||||
be: &impl DecryptReadBackend,
|
||||
mut opts: Opts,
|
||||
config_file: RusticConfig,
|
||||
) -> Result<()> {
|
||||
config_file.merge_into("snapshot-filter", &mut opts.filter)?;
|
||||
|
||||
let groups = match &opts.ids[..] {
|
||||
[] => SnapshotFile::group_from_backend(be, &opts.filter, &opts.group_by).await?,
|
||||
[id] if id == "latest" => {
|
||||
|
||||
@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
use chrono::{Duration, Local};
|
||||
use clap::{AppSettings, Parser};
|
||||
|
||||
use super::progress_counter;
|
||||
use super::{progress_counter, RusticConfig};
|
||||
use crate::backend::{DecryptFullBackend, FileType};
|
||||
use crate::repo::{DeleteOption, SnapshotFile, SnapshotFilter, StringList};
|
||||
|
||||
@ -67,7 +67,13 @@ pub(super) struct Opts {
|
||||
ids: Vec<String>,
|
||||
}
|
||||
|
||||
pub(super) async fn execute(be: &impl DecryptFullBackend, opts: Opts) -> Result<()> {
|
||||
pub(super) async fn execute(
|
||||
be: &impl DecryptFullBackend,
|
||||
mut opts: Opts,
|
||||
config_file: RusticConfig,
|
||||
) -> 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).await?,
|
||||
false => SnapshotFile::from_ids(be, &opts.ids).await?,
|
||||
|
||||
@ -7,7 +7,9 @@ use clap::Parser;
|
||||
use derivative::Derivative;
|
||||
use futures::{future, TryStreamExt};
|
||||
use indicatif::ProgressBar;
|
||||
use merge::Merge;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{serde_as, DisplayFromStr};
|
||||
use vlog::*;
|
||||
|
||||
use super::Id;
|
||||
@ -244,9 +246,9 @@ impl SnapshotFile {
|
||||
}
|
||||
|
||||
pub fn matches(&self, filter: &SnapshotFilter) -> bool {
|
||||
self.paths.matches(&filter.paths)
|
||||
&& self.tags.matches(&filter.tags)
|
||||
&& (filter.hostnames.is_empty() || filter.hostnames.contains(&self.hostname))
|
||||
self.paths.matches(&filter.filter_paths)
|
||||
&& self.tags.matches(&filter.filter_tags)
|
||||
&& (filter.filter_host.is_empty() || filter.filter_host.contains(&self.hostname))
|
||||
}
|
||||
|
||||
/// Add tag lists to snapshot. return wheter snapshot was changed
|
||||
@ -308,19 +310,26 @@ impl Ord for SnapshotFile {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[serde_as]
|
||||
#[derive(Default, Parser, Deserialize, Merge)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct SnapshotFilter {
|
||||
/// Path list to filter (can be specified multiple times)
|
||||
#[clap(long = "filter-paths", value_name = "PATH[,PATH,..]")]
|
||||
paths: Vec<StringList>,
|
||||
#[clap(long, value_name = "PATH[,PATH,..]")]
|
||||
#[serde_as(as = "Vec<DisplayFromStr>")]
|
||||
#[merge(strategy=merge::vec::overwrite_empty)]
|
||||
filter_paths: Vec<StringList>,
|
||||
|
||||
/// Tag list to filter (can be specified multiple times)
|
||||
#[clap(long = "filter-tags", value_name = "TAG[,TAG,..]")]
|
||||
tags: Vec<StringList>,
|
||||
#[clap(long, value_name = "TAG[,TAG,..]")]
|
||||
#[serde_as(as = "Vec<DisplayFromStr>")]
|
||||
#[merge(strategy=merge::vec::overwrite_empty)]
|
||||
filter_tags: Vec<StringList>,
|
||||
|
||||
/// Hostname to filter (can be specified multiple times)
|
||||
#[clap(long = "filter-host", value_name = "HOSTNAME")]
|
||||
hostnames: Vec<String>,
|
||||
#[clap(long, value_name = "HOSTNAME")]
|
||||
#[merge(strategy=merge::vec::overwrite_empty)]
|
||||
filter_host: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user