1use derive_setters::Setters;
3use itertools::Itertools;
4use log::info;
5
6use std::path::PathBuf;
7
8use path_dedot::ParseDot;
9use serde_derive::{Deserialize, Serialize};
10use serde_with::{DisplayFromStr, serde_as};
11
12use crate::{
13 CommandInput, Excludes, ReadSource,
14 archiver::{Archiver, parent::Parent},
15 backend::{
16 childstdout::ChildStdoutSource,
17 dry_run::DryRunBackend,
18 ignore::{LocalSource, LocalSourceFilterOptions, LocalSourceSaveOptions},
19 stdin::StdinSource,
20 },
21 error::{ErrorKind, RusticError, RusticResult},
22 repofile::{
23 PathList, SnapshotFile,
24 snapshotfile::{
25 SnapshotId,
26 grouping::{SnapshotGroup, SnapshotGroupCriterion},
27 },
28 },
29 repository::{IndexedIds, IndexedTree, Repository},
30};
31
32#[cfg(feature = "clap")]
33use clap::ValueHint;
34
35#[serde_as]
37#[cfg_attr(feature = "clap", derive(clap::Parser))]
38#[cfg_attr(feature = "merge", derive(conflate::Merge))]
39#[derive(Clone, Default, Debug, Deserialize, Serialize, Setters)]
40#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
41#[setters(into)]
42#[allow(clippy::struct_excessive_bools)]
43#[non_exhaustive]
44pub struct ParentOptions {
46 #[cfg_attr(feature = "clap", clap(long, short = 'g', value_name = "CRITERION",))]
48 #[serde_as(as = "Option<DisplayFromStr>")]
49 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
50 pub group_by: Option<SnapshotGroupCriterion>,
51
52 #[cfg_attr(
54 feature = "clap",
55 clap(long = "parent", value_name = "SNAPSHOT", conflicts_with = "force")
56 )]
57 #[cfg_attr(feature = "merge", merge(strategy = conflate::vec::append))]
58 pub parents: Vec<String>,
59
60 #[cfg_attr(feature = "clap", clap(long))]
62 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
63 pub skip_if_unchanged: bool,
64
65 #[cfg_attr(feature = "clap", clap(long, short))]
67 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
68 pub force: bool,
69
70 #[cfg_attr(feature = "clap", clap(long, conflicts_with = "force"))]
72 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
73 pub ignore_ctime: bool,
74
75 #[cfg_attr(feature = "clap", clap(long, conflicts_with = "force"))]
77 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
78 pub ignore_inode: bool,
79}
80
81impl ParentOptions {
82 pub(crate) fn get_parent<S: IndexedTree>(
98 &self,
99 repo: &Repository<S>,
100 snap: &SnapshotFile,
101 ) -> (Vec<SnapshotId>, Parent) {
102 let group = SnapshotGroup::from_snapshot(snap, self.group_by.unwrap_or_default());
103 let parent = if self.force {
104 Vec::new()
105 } else if self.parents.is_empty() {
106 SnapshotFile::latest(
108 repo.dbe(),
109 |snap| group.matches(snap),
110 &repo.progress_counter(""),
111 )
112 .ok()
113 .into_iter()
114 .collect()
115 } else {
116 SnapshotFile::from_strs(
117 repo.dbe(),
118 &self.parents,
119 |snap| group.matches(snap),
120 &repo.progress_counter(""),
121 )
122 .unwrap_or_default()
123 };
124
125 let (parent_trees, parent_ids): (Vec<_>, _) = parent
126 .into_iter()
127 .map(|parent| (parent.tree, parent.id))
128 .unzip();
129
130 (
131 parent_ids,
132 Parent::new(
133 repo.dbe(),
134 repo.index(),
135 parent_trees,
136 self.ignore_ctime,
137 self.ignore_inode,
138 ),
139 )
140 }
141}
142
143#[cfg_attr(feature = "clap", derive(clap::Parser))]
144#[cfg_attr(feature = "merge", derive(conflate::Merge))]
145#[derive(Clone, Default, Debug, Deserialize, Serialize, Setters)]
146#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
147#[setters(into)]
148#[non_exhaustive]
149pub struct BackupOptions {
151 #[cfg_attr(
153 feature = "clap",
154 clap(long, value_name = "FILENAME", default_value = "stdin", value_hint = ValueHint::FilePath)
155 )]
156 #[cfg_attr(feature = "merge", merge(skip))]
157 pub stdin_filename: String,
158
159 #[cfg_attr(feature = "clap", clap(long, value_name = "COMMAND"))]
161 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
162 pub stdin_command: Option<CommandInput>,
163
164 #[cfg_attr(feature = "clap", clap(long, value_name = "PATH", value_hint = ValueHint::DirPath))]
166 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
167 pub as_path: Option<PathBuf>,
168
169 #[cfg_attr(feature = "clap", clap(long))]
171 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
172 pub no_scan: bool,
173
174 #[cfg_attr(feature = "clap", clap(long))]
176 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
177 pub dry_run: bool,
178
179 #[cfg_attr(feature = "clap", clap(flatten))]
180 #[serde(flatten)]
181 pub parent_opts: ParentOptions,
183
184 #[cfg_attr(feature = "clap", clap(flatten))]
185 #[serde(flatten)]
186 pub ignore_save_opts: LocalSourceSaveOptions,
188
189 #[cfg_attr(feature = "clap", clap(flatten))]
190 #[serde(flatten)]
191 pub excludes: Excludes,
193
194 #[cfg_attr(feature = "clap", clap(flatten))]
195 #[serde(flatten)]
196 pub ignore_filter_opts: LocalSourceFilterOptions,
198}
199
200pub(crate) fn archive<R, S>(
226 repo: &Repository<S>,
227 opts: &BackupOptions,
228 src: &R,
229 mut snap: SnapshotFile,
230 backup_paths: &[PathBuf],
231) -> RusticResult<SnapshotFile>
232where
233 S: IndexedIds,
234 R: ReadSource + 'static,
235 <R as ReadSource>::Open: Send,
236 <R as ReadSource>::Iter: Send,
237{
238 let index = repo.index();
239
240 let as_path = opts
241 .as_path
242 .as_ref()
243 .map(|p| -> RusticResult<_> {
244 Ok(p.parse_dot()
245 .map_err(|err| {
246 RusticError::with_source(
247 ErrorKind::InvalidInput,
248 "Failed to parse dotted path `{path}`",
249 err,
250 )
251 .attach_context("path", p.display().to_string())
252 })?
253 .to_path_buf())
254 })
255 .transpose()?;
256
257 let paths = as_path
258 .as_ref()
259 .map_or(backup_paths, |p| std::slice::from_ref(p));
260
261 snap.paths.set_paths(paths).map_err(|err| {
262 RusticError::with_source(
263 ErrorKind::Internal,
264 "Failed to set paths `{paths}` in snapshot.",
265 err,
266 )
267 .attach_context(
268 "paths",
269 backup_paths
270 .iter()
271 .map(|p| p.display().to_string())
272 .join(","),
273 )
274 })?;
275
276 let (parent_ids, parent) = opts.parent_opts.get_parent(repo, &snap);
277 if parent_ids.is_empty() {
278 info!("using no parent");
279 } else {
280 info!("using parents {}", parent_ids.iter().join(", "));
281 snap.parent = Some(parent_ids[0]);
282 snap.parents = parent_ids;
283 }
284
285 let be = DryRunBackend::new(repo.dbe().clone(), opts.dry_run);
286 info!("starting to backup {backup_paths:?} ...");
287 let archiver = Archiver::new(be, index, repo.config(), parent, snap)?;
288 let p = repo.progress_bytes("backing up...");
289
290 archiver.archive(
291 src,
292 &backup_paths[0],
293 as_path.as_ref(),
294 opts.parent_opts.skip_if_unchanged,
295 opts.no_scan,
296 &p,
297 )
298}
299
300pub(crate) fn backup<S: IndexedIds>(
325 repo: &Repository<S>,
326 opts: &BackupOptions,
327 source: &PathList,
328 snap: SnapshotFile,
329) -> RusticResult<SnapshotFile> {
330 let backup_stdin = PathList::from_string("-")?;
331
332 let snap = if *source == backup_stdin {
333 let path = PathBuf::from(&opts.stdin_filename);
334 let backup_paths = vec![path.clone()];
335 if let Some(command) = &opts.stdin_command {
336 let src = ChildStdoutSource::new(command, path)?;
337 let res = archive(repo, opts, &src, snap, &backup_paths)?;
338 src.finish()?;
339 res
340 } else {
341 let src = StdinSource::new(path);
342 archive(repo, opts, &src, snap, &backup_paths)?
343 }
344 } else {
345 let backup_path = source.paths();
346 let src = LocalSource::new(
347 opts.ignore_save_opts,
348 &opts.excludes,
349 &opts.ignore_filter_opts,
350 &backup_path,
351 )?;
352 archive(repo, opts, &src, snap, &backup_path)?
353 };
354
355 Ok(snap)
356}