rustic_core/commands/
backup.rs

1//! `backup` subcommand
2use 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/// `backup` subcommand
33#[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]
41/// Options how the backup command uses a parent snapshot.
42pub struct ParentOptions {
43    /// Group snapshots by any combination of host,label,paths,tags to find a suitable parent (default: host,label,paths)
44    #[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    /// Snapshot to use as parent
50    #[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    /// Skip writing of snapshot if nothing changed w.r.t. the parent snapshot.
58    #[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    /// Use no parent, read all files
63    #[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    /// Ignore ctime changes when checking for modified files
68    #[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    /// Ignore inode number changes when checking for modified files
73    #[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    /// Get parent snapshot.
80    ///
81    /// # Type Parameters
82    ///
83    /// * `P` - The type of the progress bars.
84    /// * `S` - The type of the indexed tree.
85    ///
86    /// # Arguments
87    ///
88    /// * `repo` - The repository to use
89    /// * `snap` - The snapshot to use
90    /// * `backup_stdin` - Whether the backup is from stdin
91    ///
92    /// # Returns
93    ///
94    /// The parent snapshot id and the parent object or `None` if no parent is used.
95    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                // get suitable snapshot group from snapshot and opts.group_by. This is used to filter snapshots for the parent detection
105                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]
137/// Options for the `backup` command.
138pub struct BackupOptions {
139    /// Set filename to be used when backing up from stdin
140    #[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    /// Call the given command and use its output as stdin
148    #[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    /// Manually set backup path in snapshot
153    #[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    /// Don't scan the backup source for its size - this disables ETA estimation for backup.
158    #[cfg_attr(feature = "clap", clap(long))]
159    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
160    pub no_scan: bool,
161
162    /// Dry-run mode: Don't write any data or snapshot
163    #[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    /// Options how to use a parent snapshot
170    pub parent_opts: ParentOptions,
171
172    #[cfg_attr(feature = "clap", clap(flatten))]
173    #[serde(flatten)]
174    /// Options how to save entries from a local source
175    pub ignore_save_opts: LocalSourceSaveOptions,
176
177    #[cfg_attr(feature = "clap", clap(flatten))]
178    #[serde(flatten)]
179    /// Options how to filter from a local source
180    pub ignore_filter_opts: LocalSourceFilterOptions,
181}
182
183/// Backup data, create a snapshot.
184///
185/// # Type Parameters
186///
187/// * `P` - The type of the progress bars.
188/// * `S` - The type of the indexed tree.
189///
190/// # Arguments
191///
192/// * `repo` - The repository to use
193/// * `opts` - The backup options
194/// * `source` - The source to backup
195/// * `snap` - The snapshot to backup
196///
197/// # Errors
198///
199/// * If sending the message to the raw packer fails.
200/// * If converting the data length to u64 fails
201/// * If sending the message to the raw packer fails.
202/// * If the index file could not be serialized.
203/// * If the time is not in the range of `Local::now()`
204///
205/// # Returns
206///
207/// The snapshot pointing to the backup'ed data.
208#[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}