Skip to main content

rustic_core/commands/
backup.rs

1//! `backup` subcommand
2use 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/// `backup` subcommand
36#[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]
44/// Options how the backup command uses a parent snapshot.
45pub struct ParentOptions {
46    /// Group snapshots by any combination of host,label,paths,tags to find a suitable parent (default: host,label,paths)
47    #[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    /// Snapshot to use as parent (can be specified multiple times)
53    #[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    /// Skip writing of snapshot if nothing changed w.r.t. the parent snapshot.
61    #[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    /// Use no parent, read all files
66    #[cfg_attr(feature = "clap", clap(long, short))]
67    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
68    pub force: bool,
69
70    /// Ignore ctime changes when checking for modified files
71    #[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    /// Ignore inode number changes when checking for modified files
76    #[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    /// Get parent snapshot.
83    ///
84    /// # Type Parameters
85    ///
86    /// * `P` - The type of the progress bars.
87    /// * `S` - The type of the indexed tree.
88    ///
89    /// # Arguments
90    ///
91    /// * `repo` - The repository to use
92    /// * `snap` - The snapshot to use
93    ///
94    /// # Returns
95    ///
96    /// The parent snapshot ids and the parent object.
97    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            // get suitable snapshot group from snapshot and opts.group_by. This is used to filter snapshots for the parent detection
107            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]
149/// Options for the `backup` command.
150pub struct BackupOptions {
151    /// Set filename to be used when backing up from stdin
152    #[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    /// Call the given command and use its output as stdin
160    #[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    /// Manually set backup path in snapshot
165    #[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    /// Don't scan the backup source for its size - this disables ETA estimation for backup.
170    #[cfg_attr(feature = "clap", clap(long))]
171    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
172    pub no_scan: bool,
173
174    /// Dry-run mode: Don't write any data or snapshot
175    #[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    /// Options how to use a parent snapshot
182    pub parent_opts: ParentOptions,
183
184    #[cfg_attr(feature = "clap", clap(flatten))]
185    #[serde(flatten)]
186    /// Options how to save entries from a local source
187    pub ignore_save_opts: LocalSourceSaveOptions,
188
189    #[cfg_attr(feature = "clap", clap(flatten))]
190    #[serde(flatten)]
191    /// excludes
192    pub excludes: Excludes,
193
194    #[cfg_attr(feature = "clap", clap(flatten))]
195    #[serde(flatten)]
196    /// Options how to filter from a local source
197    pub ignore_filter_opts: LocalSourceFilterOptions,
198}
199
200/// Backup data, create a snapshot.
201///
202/// # Type Parameters
203///
204/// * `P` - The type of the progress bars.
205/// * `S` - The type of the indexed tree.
206///
207/// # Arguments
208///
209/// * `repo` - The repository to use
210/// * `opts` - The backup options
211/// * `src` - The source to backup
212/// * `snap` - The snapshot with raw information
213///
214/// # Errors
215///
216/// * If sending the message to the raw packer fails.
217/// * If converting the data length to u64 fails
218/// * If sending the message to the raw packer fails.
219/// * If the index file could not be serialized.
220/// * If the time is not in the range of `Local::now()`
221///
222/// # Returns
223///
224/// The snapshot pointing to the backup'ed data.
225pub(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
300/// Backup data, create a snapshot.
301///
302/// # Type Parameters
303///
304/// * `S` - The type of the indexed tree.
305///
306/// # Arguments
307///
308/// * `repo` - The repository to use
309/// * `opts` - The backup options
310/// * `source` - The source to backup
311/// * `snap` - The snapshot with raw information
312///
313/// # Errors
314///
315/// * If sending the message to the raw packer fails.
316/// * If converting the data length to u64 fails
317/// * If sending the message to the raw packer fails.
318/// * If the index file could not be serialized.
319/// * If the time is not in the range of `Local::now()`
320///
321/// # Returns
322///
323/// The snapshot pointing to the backup'ed data.
324pub(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}