use std::path::PathBuf;
use crate::{
commands::{get_repository, init::init, open_repository},
helpers::bytes_size_to_string,
status_err, Application, RUSTIC_APP,
};
use abscissa_core::{Command, Runnable, Shutdown};
use anyhow::{bail, Context, Result};
use clap::ValueHint;
use log::{debug, info, warn};
use merge::Merge;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, OneOrMany};
use rustic_core::{
BackupOptions, ConfigOptions, KeyOptions, LocalSourceFilterOptions, LocalSourceSaveOptions,
ParentOptions, PathList, SnapshotOptions,
};
#[serde_as]
#[derive(Clone, Command, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[allow(clippy::struct_excessive_bools)]
pub struct BackupCmd {
#[clap(value_name = "SOURCE", value_hint = ValueHint::AnyPath)]
#[merge(skip)]
#[serde(skip)]
cli_sources: Vec<String>,
#[clap(long, value_name = "FILENAME", default_value = "stdin", value_hint = ValueHint::FilePath)]
#[merge(skip)]
stdin_filename: String,
#[clap(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
as_path: Option<PathBuf>,
#[clap(flatten)]
#[serde(flatten)]
ignore_save_opts: LocalSourceSaveOptions,
#[clap(long)]
#[merge(strategy = merge::bool::overwrite_false)]
pub no_scan: bool,
#[clap(long)]
#[merge(strategy = merge::bool::overwrite_false)]
json: bool,
#[clap(long, conflicts_with = "json")]
#[merge(strategy = merge::bool::overwrite_false)]
quiet: bool,
#[clap(long)]
#[merge(strategy = merge::bool::overwrite_false)]
init: bool,
#[clap(flatten, next_help_heading = "Options for parent processing")]
#[serde(flatten)]
parent_opts: ParentOptions,
#[clap(flatten, next_help_heading = "Exclude options")]
#[serde(flatten)]
ignore_filter_opts: LocalSourceFilterOptions,
#[clap(flatten, next_help_heading = "Snapshot options")]
#[serde(flatten)]
snap_opts: SnapshotOptions,
#[clap(flatten, next_help_heading = "Key options (when using --init)")]
#[serde(skip)]
#[merge(skip)]
key_opts: KeyOptions,
#[clap(flatten, next_help_heading = "Config options (when using --init)")]
#[serde(skip)]
#[merge(skip)]
config_opts: ConfigOptions,
#[clap(skip)]
#[merge(strategy = merge_sources)]
sources: Vec<BackupCmd>,
#[clap(skip)]
#[merge(skip)]
#[serde_as(as = "OneOrMany<_>")]
source: Vec<String>,
}
pub(crate) fn merge_sources(left: &mut Vec<BackupCmd>, mut right: Vec<BackupCmd>) {
left.append(&mut right);
left.sort_by(|opt1, opt2| opt1.source.cmp(&opt2.source));
left.dedup_by(|opt1, opt2| opt1.source == opt2.source);
}
impl Runnable for BackupCmd {
fn run(&self) {
if let Err(err) = self.inner_run() {
status_err!("{}", err);
RUSTIC_APP.shutdown(Shutdown::Crash);
};
}
}
impl BackupCmd {
fn inner_run(&self) -> Result<()> {
let config = RUSTIC_APP.config();
let repo = get_repository(&config.repository)?;
let repo = if self.init && repo.config_id()?.is_none() {
if config.global.dry_run {
bail!(
"cannot initialize repository {} in dry-run mode!",
repo.name
);
}
init(repo, &self.key_opts, &self.config_opts)?
} else {
open_repository(&config.repository)?
}
.to_indexed_ids()?;
if !config.backup.source.is_empty() {
bail!("key \"source\" is not valid in the [backup] section!");
}
let config_opts = &config.backup.sources;
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()
.map(|opt| -> Result<_> {
Ok(PathList::from_iter(&opt.source)
.sanitize()
.with_context(|| {
format!(
"error sanitizing source=\"{:?}\" in config file",
opt.source
)
})?
.merge())
})
.filter_map(|p| match p {
Ok(paths) => Some(paths),
Err(err) => {
warn!("{err}");
None
}
})
.collect();
let sources = match (self.cli_sources.is_empty(), config_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_sources.clone()
}
(true, true) => {
bail!("no backup source given.");
}
};
for source in sources {
let mut opts = self.clone();
if let Some(idx) = config_sources.iter().position(|s| s == &source) {
info!("merging source={source} section from config file");
opts.merge(config_opts[idx].clone());
}
if let Some(path) = &opts.as_path {
if source.len() > 1 {
bail!("as-path only works with a single target!");
}
if let Some(path) = path.as_os_str().to_str() {
if let Some(idx) = config_opts.iter().position(|opt| opt.source == vec![path]) {
info!("merging source=\"{path}\" section from config file");
opts.merge(config_opts[idx].clone());
}
}
}
opts.merge(config.backup.clone());
let backup_opts = BackupOptions::default()
.stdin_filename(opts.stdin_filename)
.as_path(opts.as_path)
.parent_opts(opts.parent_opts)
.ignore_save_opts(opts.ignore_save_opts)
.ignore_filter_opts(opts.ignore_filter_opts)
.no_scan(opts.no_scan)
.dry_run(config.global.dry_run);
let snap = repo.backup(&backup_opts, &source, opts.snap_opts.to_snapshot()?)?;
if opts.json {
let mut stdout = std::io::stdout();
serde_json::to_writer_pretty(&mut stdout, &snap)?;
} else if !opts.quiet {
let summary = snap.summary.unwrap();
println!(
"Files: {} new, {} changed, {} unchanged",
summary.files_new, summary.files_changed, summary.files_unmodified
);
println!(
"Dirs: {} new, {} changed, {} unchanged",
summary.dirs_new, summary.dirs_changed, summary.dirs_unmodified
);
debug!("Data Blobs: {} new", summary.data_blobs);
debug!("Tree Blobs: {} new", summary.tree_blobs);
println!(
"Added to the repo: {} (raw: {})",
bytes_size_to_string(summary.data_added_packed),
bytes_size_to_string(summary.data_added)
);
println!(
"processed {} files, {}",
summary.total_files_processed,
bytes_size_to_string(summary.total_bytes_processed)
);
println!("snapshot {} successfully saved.", snap.id);
}
info!("backup of {source} done.");
}
Ok(())
}
}