use derive_setters::Setters;
use itertools::Itertools;
use log::info;
use std::path::PathBuf;
use path_dedot::ParseDot;
use serde_derive::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as};
use crate::{
CommandInput, Excludes,
archiver::{Archiver, parent::Parent},
backend::{
childstdout::ChildStdoutSource,
dry_run::DryRunBackend,
ignore::{LocalSource, LocalSourceFilterOptions, LocalSourceSaveOptions},
stdin::StdinSource,
},
error::{ErrorKind, RusticError, RusticResult},
repofile::{
PathList, SnapshotFile,
snapshotfile::{
SnapshotId,
grouping::{SnapshotGroup, SnapshotGroupCriterion},
},
},
repository::{IndexedIds, IndexedTree, Repository},
};
#[cfg(feature = "clap")]
use clap::ValueHint;
#[serde_as]
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "merge", derive(conflate::Merge))]
#[derive(Clone, Default, Debug, Deserialize, Serialize, Setters)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[setters(into)]
#[allow(clippy::struct_excessive_bools)]
#[non_exhaustive]
pub struct ParentOptions {
#[cfg_attr(feature = "clap", clap(long, short = 'g', value_name = "CRITERION",))]
#[serde_as(as = "Option<DisplayFromStr>")]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub group_by: Option<SnapshotGroupCriterion>,
#[cfg_attr(
feature = "clap",
clap(long = "parent", value_name = "SNAPSHOT", conflicts_with = "force")
)]
#[cfg_attr(feature = "merge", merge(strategy = conflate::vec::append))]
pub parents: Vec<String>,
#[cfg_attr(feature = "clap", clap(long))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
pub skip_if_unchanged: bool,
#[cfg_attr(feature = "clap", clap(long, short))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
pub force: bool,
#[cfg_attr(feature = "clap", clap(long, conflicts_with = "force"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
pub ignore_ctime: bool,
#[cfg_attr(feature = "clap", clap(long, conflicts_with = "force"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
pub ignore_inode: bool,
}
impl ParentOptions {
pub(crate) fn get_parent<S: IndexedTree>(
&self,
repo: &Repository<S>,
snap: &SnapshotFile,
backup_stdin: bool,
) -> (Vec<SnapshotId>, Parent) {
let group = SnapshotGroup::from_snapshot(snap, self.group_by.unwrap_or_default());
let parent = if backup_stdin || self.force {
Vec::new()
} else if self.parents.is_empty() {
SnapshotFile::latest(
repo.dbe(),
|snap| group.matches(snap),
&repo.progress_counter(""),
)
.ok()
.into_iter()
.collect()
} else {
SnapshotFile::from_strs(
repo.dbe(),
&self.parents,
|snap| group.matches(snap),
&repo.progress_counter(""),
)
.unwrap_or_default()
};
let (parent_trees, parent_ids): (Vec<_>, _) = parent
.into_iter()
.map(|parent| (parent.tree, parent.id))
.unzip();
(
parent_ids,
Parent::new(
repo.dbe(),
repo.index(),
parent_trees,
self.ignore_ctime,
self.ignore_inode,
),
)
}
}
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "merge", derive(conflate::Merge))]
#[derive(Clone, Default, Debug, Deserialize, Serialize, Setters)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
#[setters(into)]
#[non_exhaustive]
pub struct BackupOptions {
#[cfg_attr(
feature = "clap",
clap(long, value_name = "FILENAME", default_value = "stdin", value_hint = ValueHint::FilePath)
)]
#[cfg_attr(feature = "merge", merge(skip))]
pub stdin_filename: String,
#[cfg_attr(feature = "clap", clap(long, value_name = "COMMAND"))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub stdin_command: Option<CommandInput>,
#[cfg_attr(feature = "clap", clap(long, value_name = "PATH", value_hint = ValueHint::DirPath))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
pub as_path: Option<PathBuf>,
#[cfg_attr(feature = "clap", clap(long))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
pub no_scan: bool,
#[cfg_attr(feature = "clap", clap(long))]
#[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
pub dry_run: bool,
#[cfg_attr(feature = "clap", clap(flatten))]
#[serde(flatten)]
pub parent_opts: ParentOptions,
#[cfg_attr(feature = "clap", clap(flatten))]
#[serde(flatten)]
pub ignore_save_opts: LocalSourceSaveOptions,
#[cfg_attr(feature = "clap", clap(flatten))]
#[serde(flatten)]
pub excludes: Excludes,
#[cfg_attr(feature = "clap", clap(flatten))]
#[serde(flatten)]
pub ignore_filter_opts: LocalSourceFilterOptions,
}
#[allow(clippy::too_many_lines)]
pub(crate) fn backup<S: IndexedIds>(
repo: &Repository<S>,
opts: &BackupOptions,
source: &PathList,
mut snap: SnapshotFile,
) -> RusticResult<SnapshotFile> {
let index = repo.index();
let backup_stdin = *source == PathList::from_string("-")?;
let backup_path = if backup_stdin {
vec![PathBuf::from(&opts.stdin_filename)]
} else {
source.paths()
};
let as_path = opts
.as_path
.as_ref()
.map(|p| -> RusticResult<_> {
Ok(p.parse_dot()
.map_err(|err| {
RusticError::with_source(
ErrorKind::InvalidInput,
"Failed to parse dotted path `{path}`",
err,
)
.attach_context("path", p.display().to_string())
})?
.to_path_buf())
})
.transpose()?;
let paths = match &as_path {
Some(p) => std::slice::from_ref(p),
None => &backup_path,
};
snap.paths.set_paths(paths).map_err(|err| {
RusticError::with_source(
ErrorKind::Internal,
"Failed to set paths `{paths}` in snapshot.",
err,
)
.attach_context(
"paths",
backup_path
.iter()
.map(|p| p.display().to_string())
.join(","),
)
})?;
let (parent_ids, parent) = opts.parent_opts.get_parent(repo, &snap, backup_stdin);
if parent_ids.is_empty() {
info!("using no parent");
} else {
info!("using parents {}", parent_ids.iter().join(", "));
snap.parent = Some(parent_ids[0]);
snap.parents = parent_ids;
}
let be = DryRunBackend::new(repo.dbe().clone(), opts.dry_run);
info!("starting to backup {source} ...");
let archiver = Archiver::new(be, index, repo.config(), parent, snap)?;
let p = repo.progress_bytes("backing up...");
let snap = if backup_stdin {
let path = &backup_path[0];
if let Some(command) = &opts.stdin_command {
let src = ChildStdoutSource::new(command, path.clone())?;
let res = archiver.archive(
&src,
path,
as_path.as_ref(),
opts.parent_opts.skip_if_unchanged,
opts.no_scan,
&p,
)?;
src.finish()?;
res
} else {
let src = StdinSource::new(path.clone());
archiver.archive(
&src,
path,
as_path.as_ref(),
opts.parent_opts.skip_if_unchanged,
opts.no_scan,
&p,
)?
}
} else {
let src = LocalSource::new(
opts.ignore_save_opts,
&opts.excludes,
&opts.ignore_filter_opts,
&backup_path,
)?;
archiver.archive(
&src,
&backup_path[0],
as_path.as_ref(),
opts.parent_opts.skip_if_unchanged,
opts.no_scan,
&p,
)?
};
Ok(snap)
}