Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,11 @@ See [Global Metrics labels](#global-metrics-labels-globalmetrics-labels).
**Note**: All of the backup options mentioned before can also be used as
snapshot-specific option and then only apply to this snapshot.

| Attribute | Description | Default Value | Example Value |
| --------- | ------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------- |
| sources | Array of source directories or file(s) to back up. | [] | ["/dir1", "/dir2"] |
| hooks | Hooks to run before and after backing up the defined sources. | Not set | { run-before = [], run-after = [], run-failed = [], run-finally = [] } |
| Attribute | Description | Default Value | Example Value |
| --------- | ---------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------- |
| name | Name to identify this snapshot (to be used with the --name CLI option) | "" | "myid" |
| sources | Array of source directories or file(s) to back up. | [] | ["/dir1", "/dir2"] |
| hooks | Hooks to run before and after backing up the defined sources. | Not set | { run-before = [], run-after = [], run-failed = [], run-finally = [] } |

Source-specific hooks are called additionally to global, repository and backup
hooks when backing up the defined sources into a snapshot.
Expand Down
164 changes: 102 additions & 62 deletions src/commands/backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use abscissa_core::{Command, Runnable, Shutdown};
use anyhow::{Context, Result, anyhow, bail};
use clap::ValueHint;
use comfy_table::Cell;
use conflate::Merge;
use conflate::{Merge, MergeFrom};
use log::{debug, error, info, warn};
use rustic_core::StringList;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -48,6 +48,16 @@ pub struct BackupCmd {
#[serde(skip)]
cli_sources: Vec<String>,

/// Backup sources defined in the config profile by the given name (can be specified multiple times)
#[clap(long = "name", value_name = "NAME", conflicts_with = "cli_sources")]
#[merge(skip)]
#[serde(skip)]
cli_name: Vec<String>,

#[clap(skip)]
#[merge(skip)]
name: Option<String>,

/// Set filename to be used when backing up from stdin
#[clap(long, value_name = "FILENAME", default_value = "stdin", value_hint = ValueHint::FilePath)]
#[merge(skip)]
Expand Down Expand Up @@ -145,6 +155,27 @@ pub struct BackupCmd {
metrics_labels: BTreeMap<String, String>,
}

impl BackupCmd {
fn validate(&self) -> Result<(), &str> {
// manually check for a "source" field, check is not done by serde, see above.
if !self.sources.is_empty() {
return Err("key \"sources\" is not valid in the [backup] section!");
}

// manually check for a "name" field, check is not done by serde, see above.
if self.name.is_some() {
return Err("key \"name\" is not valid in the [backup] section!");
}

let snapshot_opts = &self.snapshots;
// manually check for a "sources" field, check is not done by serde, see above.
if snapshot_opts.iter().any(|opt| !opt.snapshots.is_empty()) {
return Err("key \"snapshots\" is not valid in a [[backup.snapshots]] section!");
}
Ok(())
}
}

/// Merge backup snapshots to generate
///
/// If a snapshot is already defined on left, use that. Else add it.
Expand All @@ -153,25 +184,22 @@ pub struct BackupCmd {
///
/// * `left` - Vector of backup sources
pub(crate) fn merge_snapshots(left: &mut Vec<BackupCmd>, mut right: Vec<BackupCmd>) {
let order = |opt1: &BackupCmd, opt2: &BackupCmd| {
opt1.name
.cmp(&opt2.name)
.then(opt1.sources.cmp(&opt2.sources))
};

left.append(&mut right);
left.sort_by(|opt1, opt2| opt1.sources.cmp(&opt2.sources));
left.dedup_by(|opt1, opt2| opt1.sources == opt2.sources);
left.sort_by(order);
left.dedup_by(|opt1, opt2| order(opt1, opt2).is_eq());
}

impl Runnable for BackupCmd {
fn run(&self) {
let config = RUSTIC_APP.config();

// manually check for a "source" field, check is not done by serde, see above.
if !config.backup.sources.is_empty() {
status_err!("key \"sources\" is not valid in the [backup] section!");
RUSTIC_APP.shutdown(Shutdown::Crash);
}

let snapshot_opts = &config.backup.snapshots;
// manually check for a "sources" field, check is not done by serde, see above.
if snapshot_opts.iter().any(|opt| !opt.snapshots.is_empty()) {
status_err!("key \"snapshots\" is not valid in a [[backup.snapshots]] section!");
if let Err(err) = config.backup.validate() {
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
}

Expand All @@ -185,7 +213,6 @@ impl Runnable for BackupCmd {
impl BackupCmd {
fn inner_run(&self, repo: CliRepo) -> Result<()> {
let config = RUSTIC_APP.config();
let snapshot_opts = &config.backup.snapshots;

// Initialize repository if --init is set and it is not yet initialized
let repo = if self.init && repo.config_id()?.is_none() {
Expand All @@ -208,54 +235,8 @@ impl BackupCmd {
);

hooks.use_with(|| -> Result<_> {
let config_snapshot_sources: Vec<_> = snapshot_opts
.iter()
.map(|opt| -> Result<_> {
Ok(PathList::from_iter(&opt.sources)
.sanitize()
.with_context(|| {
format!(
"error sanitizing sources=\"{:?}\" in config file",
opt.sources
)
})?
.merge())
})
.filter_map(|p| match p {
Ok(paths) => Some(paths),
Err(err) => {
warn!("{err}");
None
}
})
.collect();

let snapshot_sources = match (self.cli_sources.is_empty(), snapshot_opts.is_empty()) {
(false, _) => {
let item = PathList::from_iter(&self.cli_sources).sanitize()?;
vec![item]
}
(true, false) => {
info!("using all backup sources from config file.");
config_snapshot_sources.clone()
}
(true, true) => {
bail!("no backup source given.");
}
};
if snapshot_sources.is_empty() {
return Ok(());
}

let mut is_err = false;
for sources in snapshot_sources {
let mut opts = self.clone();

// merge Options from config file, if given
if let Some(idx) = config_snapshot_sources.iter().position(|s| s == &sources) {
info!("merging sources={sources} section from config file");
opts.merge(snapshot_opts[idx].clone());
}
for (opts, sources) in self.get_snapshots_to_backup()? {
if let Err(err) = opts.backup_snapshot(sources.clone(), &repo) {
error!("error backing up {sources}: {err}");
is_err = true;
Expand All @@ -269,6 +250,65 @@ impl BackupCmd {
})
}

fn get_snapshots_to_backup(&self) -> Result<Vec<(Self, PathList)>> {
let config = RUSTIC_APP.config();
let mut config_snapshots = config
.backup
.snapshots
.iter()
.map(|opt| -> Result<_> {
Ok((
opt.clone(),
PathList::from_iter(&opt.sources)
.sanitize()
.with_context(|| {
format!(
"error sanitizing sources=\"{:?}\" in config file",
opt.sources
)
})?
.merge(),
))
})
.filter_map(|p| match p {
Ok(paths) => Some(paths),
Err(err) => {
warn!("{err}");
None
}
});

if !self.cli_sources.is_empty() {
let sources = PathList::from_iter(&self.cli_sources).sanitize()?;
let mut opts = self.clone();
// merge Options from config file, if given
if let Some((config_opts, _)) = config_snapshots.find(|(_, s)| s == &sources) {
info!("merging sources={sources} section from config file");
opts.merge(config_opts);
}
return Ok(vec![(opts, sources)]);
}

let config_snapshots: Vec<_> = config_snapshots
// filter out using cli_name, if given
.filter(|(opt, _)| {
self.cli_name.is_empty()
|| opt
.name
.as_ref()
.is_some_and(|name| self.cli_name.contains(name))
})
.map(|(opt, sources)| (self.clone().merge_from(opt), sources))
.collect();

if config_snapshots.is_empty() {
bail!("no backup source given.");
}

info!("using backup sources from config file.");
Ok(config_snapshots)
}

fn hooks(&self, hooks: &Hooks, action: &str, source: impl Display) -> Hooks {
let mut hooks_variables =
HashMap::from([("RUSTIC_ACTION".to_string(), action.to_string())]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ RusticConfig {
},
backup: BackupCmd {
cli_sources: [],
cli_name: [],
name: None,
stdin_filename: "",
stdin_command: None,
as_path: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ RusticConfig {
},
backup: BackupCmd {
cli_sources: [],
cli_name: [],
name: None,
stdin_filename: "",
stdin_command: None,
as_path: None,
Expand Down