1use derive_setters::Setters;
3use log::info;
4
5use std::path::PathBuf;
6
7use path_dedot::ParseDot;
8use serde_derive::{Deserialize, Serialize};
9use serde_with::{DisplayFromStr, serde_as};
10
11use crate::{
12 CommandInput,
13 archiver::{Archiver, parent::Parent},
14 backend::{
15 childstdout::ChildStdoutSource,
16 dry_run::DryRunBackend,
17 ignore::{LocalSource, LocalSourceFilterOptions, LocalSourceSaveOptions},
18 stdin::StdinSource,
19 },
20 error::{ErrorKind, RusticError, RusticResult},
21 progress::ProgressBars,
22 repofile::{
23 PathList, SnapshotFile,
24 snapshotfile::{SnapshotGroup, SnapshotGroupCriterion, SnapshotId},
25 },
26 repository::{IndexedIds, IndexedTree, Repository},
27};
28
29#[cfg(feature = "clap")]
30use clap::ValueHint;
31
32#[serde_as]
34#[cfg_attr(feature = "clap", derive(clap::Parser))]
35#[cfg_attr(feature = "merge", derive(conflate::Merge))]
36#[derive(Clone, Default, Debug, Deserialize, Serialize, Setters)]
37#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
38#[setters(into)]
39#[allow(clippy::struct_excessive_bools)]
40#[non_exhaustive]
41pub struct ParentOptions {
43 #[cfg_attr(feature = "clap", clap(long, short = 'g', value_name = "CRITERION",))]
45 #[serde_as(as = "Option<DisplayFromStr>")]
46 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
47 pub group_by: Option<SnapshotGroupCriterion>,
48
49 #[cfg_attr(
51 feature = "clap",
52 clap(long, value_name = "SNAPSHOT", conflicts_with = "force",)
53 )]
54 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
55 pub parent: Option<String>,
56
57 #[cfg_attr(feature = "clap", clap(long))]
59 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
60 pub skip_if_unchanged: bool,
61
62 #[cfg_attr(feature = "clap", clap(long, short, conflicts_with = "parent",))]
64 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
65 pub force: bool,
66
67 #[cfg_attr(feature = "clap", clap(long, conflicts_with = "force",))]
69 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
70 pub ignore_ctime: bool,
71
72 #[cfg_attr(feature = "clap", clap(long, conflicts_with = "force",))]
74 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
75 pub ignore_inode: bool,
76}
77
78impl ParentOptions {
79 pub(crate) fn get_parent<P: ProgressBars, S: IndexedTree>(
96 &self,
97 repo: &Repository<P, S>,
98 snap: &SnapshotFile,
99 backup_stdin: bool,
100 ) -> (Option<SnapshotId>, Parent) {
101 let parent = match (backup_stdin, self.force, &self.parent) {
102 (true, _, _) | (false, true, _) => None,
103 (false, false, None) => {
104 let group = SnapshotGroup::from_snapshot(snap, self.group_by.unwrap_or_default());
106 SnapshotFile::latest(
107 repo.dbe(),
108 |snap| snap.has_group(&group),
109 &repo.pb.progress_counter(""),
110 )
111 .ok()
112 }
113 (false, false, Some(parent)) => SnapshotFile::from_id(repo.dbe(), parent).ok(),
114 };
115
116 let (parent_tree, parent_id) = parent.map(|parent| (parent.tree, parent.id)).unzip();
117
118 (
119 parent_id,
120 Parent::new(
121 repo.dbe(),
122 repo.index(),
123 parent_tree,
124 self.ignore_ctime,
125 self.ignore_inode,
126 ),
127 )
128 }
129}
130
131#[cfg_attr(feature = "clap", derive(clap::Parser))]
132#[cfg_attr(feature = "merge", derive(conflate::Merge))]
133#[derive(Clone, Default, Debug, Deserialize, Serialize, Setters)]
134#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
135#[setters(into)]
136#[non_exhaustive]
137pub struct BackupOptions {
139 #[cfg_attr(
141 feature = "clap",
142 clap(long, value_name = "FILENAME", default_value = "stdin", value_hint = ValueHint::FilePath)
143 )]
144 #[cfg_attr(feature = "merge", merge(skip))]
145 pub stdin_filename: String,
146
147 #[cfg_attr(feature = "clap", clap(long, value_name = "COMMAND"))]
149 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
150 pub stdin_command: Option<CommandInput>,
151
152 #[cfg_attr(feature = "clap", clap(long, value_name = "PATH", value_hint = ValueHint::DirPath))]
154 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
155 pub as_path: Option<PathBuf>,
156
157 #[cfg_attr(feature = "clap", clap(long))]
159 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
160 pub no_scan: bool,
161
162 #[cfg_attr(feature = "clap", clap(long))]
164 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
165 pub dry_run: bool,
166
167 #[cfg_attr(feature = "clap", clap(flatten))]
168 #[serde(flatten)]
169 pub parent_opts: ParentOptions,
171
172 #[cfg_attr(feature = "clap", clap(flatten))]
173 #[serde(flatten)]
174 pub ignore_save_opts: LocalSourceSaveOptions,
176
177 #[cfg_attr(feature = "clap", clap(flatten))]
178 #[serde(flatten)]
179 pub ignore_filter_opts: LocalSourceFilterOptions,
181}
182
183#[allow(clippy::too_many_lines)]
209pub(crate) fn backup<P: ProgressBars, S: IndexedIds>(
210 repo: &Repository<P, S>,
211 opts: &BackupOptions,
212 source: &PathList,
213 mut snap: SnapshotFile,
214) -> RusticResult<SnapshotFile> {
215 let index = repo.index();
216
217 let backup_stdin = *source == PathList::from_string("-")?;
218 let backup_path = if backup_stdin {
219 vec![PathBuf::from(&opts.stdin_filename)]
220 } else {
221 source.paths()
222 };
223
224 let as_path = opts
225 .as_path
226 .as_ref()
227 .map(|p| -> RusticResult<_> {
228 Ok(p.parse_dot()
229 .map_err(|err| {
230 RusticError::with_source(
231 ErrorKind::InvalidInput,
232 "Failed to parse dotted path `{path}`",
233 err,
234 )
235 .attach_context("path", p.display().to_string())
236 })?
237 .to_path_buf())
238 })
239 .transpose()?;
240
241 match &as_path {
242 Some(p) => snap
243 .paths
244 .set_paths(std::slice::from_ref(p))
245 .map_err(|err| {
246 RusticError::with_source(
247 ErrorKind::Internal,
248 "Failed to set paths `{paths}` in snapshot.",
249 err,
250 )
251 .attach_context("paths", p.display().to_string())
252 })?,
253 None => snap.paths.set_paths(&backup_path).map_err(|err| {
254 RusticError::with_source(
255 ErrorKind::Internal,
256 "Failed to set paths `{paths}` in snapshot.",
257 err,
258 )
259 .attach_context(
260 "paths",
261 backup_path
262 .iter()
263 .map(|p| p.display().to_string())
264 .collect::<Vec<_>>()
265 .join(","),
266 )
267 })?,
268 }
269
270 let (parent_id, parent) = opts.parent_opts.get_parent(repo, &snap, backup_stdin);
271 match parent_id {
272 Some(id) => {
273 info!("using parent {id}");
274 snap.parent = Some(id);
275 }
276 None => {
277 info!("using no parent");
278 }
279 }
280
281 let be = DryRunBackend::new(repo.dbe().clone(), opts.dry_run);
282 info!("starting to backup {source} ...");
283 let archiver = Archiver::new(be, index, repo.config(), parent, snap)?;
284 let p = repo.pb.progress_bytes("backing up...");
285
286 let snap = if backup_stdin {
287 let path = &backup_path[0];
288 if let Some(command) = &opts.stdin_command {
289 let src = ChildStdoutSource::new(command, path.clone())?;
290 let res = archiver.archive(
291 &src,
292 path,
293 as_path.as_ref(),
294 opts.parent_opts.skip_if_unchanged,
295 opts.no_scan,
296 &p,
297 )?;
298 src.finish()?;
299 res
300 } else {
301 let src = StdinSource::new(path.clone());
302 archiver.archive(
303 &src,
304 path,
305 as_path.as_ref(),
306 opts.parent_opts.skip_if_unchanged,
307 opts.no_scan,
308 &p,
309 )?
310 }
311 } else {
312 let src = LocalSource::new(
313 opts.ignore_save_opts,
314 &opts.ignore_filter_opts,
315 &backup_path,
316 )?;
317 archiver.archive(
318 &src,
319 &backup_path[0],
320 as_path.as_ref(),
321 opts.parent_opts.skip_if_unchanged,
322 opts.no_scan,
323 &p,
324 )?
325 };
326
327 Ok(snap)
328}