rustic_rs/commands/
copy.rs

1//! `copy` subcommand
2
3use crate::{
4    Application, RUSTIC_APP, RusticConfig,
5    commands::init::init_password,
6    helpers::table_with_titles,
7    repository::{CliIndexedRepo, CliRepo, get_filtered_snapshots},
8    status_err,
9};
10use abscissa_core::{Command, FrameworkError, Runnable, Shutdown, config::Override};
11use anyhow::{Result, bail};
12use conflate::Merge;
13use log::{Level, error, info, log};
14use serde::{Deserialize, Serialize};
15
16use rustic_core::{CopySnapshot, Id, KeyOptions, repofile::SnapshotFile};
17
18/// `copy` subcommand
19#[derive(clap::Parser, Command, Default, Clone, Debug, Serialize, Deserialize, Merge)]
20pub struct CopyCmd {
21    /// Snapshots to copy. If none is given, use filter options to filter from all snapshots.
22    #[clap(value_name = "ID")]
23    #[serde(skip)]
24    #[merge(skip)]
25    ids: Vec<String>,
26
27    /// Initialize non-existing target repositories
28    #[clap(long)]
29    #[serde(skip)]
30    #[merge(skip)]
31    init: bool,
32
33    /// Target repository (can be specified multiple times)
34    #[clap(long = "target", value_name = "TARGET")]
35    #[merge(strategy=conflate::vec::overwrite_empty)]
36    targets: Vec<String>,
37
38    /// Key options (when using --init)
39    #[clap(flatten, next_help_heading = "Key options (when using --init)")]
40    #[serde(skip)]
41    #[merge(skip)]
42    key_opts: KeyOptions,
43}
44
45impl Override<RusticConfig> for CopyCmd {
46    // Process the given command line options, overriding settings from
47    // a configuration file using explicit flags taken from command-line
48    // arguments.
49    fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
50        let mut self_config = self.clone();
51        // merge "copy" section from config file, if given
52        self_config.merge(config.copy);
53        config.copy = self_config;
54        Ok(config)
55    }
56}
57
58impl Runnable for CopyCmd {
59    fn run(&self) {
60        let config = RUSTIC_APP.config();
61        if config.copy.targets.is_empty() {
62            status_err!(
63                "No target given. Please specify at least 1 target either in the profile or using --target!"
64            );
65            RUSTIC_APP.shutdown(Shutdown::Crash);
66        }
67        if let Err(err) = config.repository.run_indexed(|repo| self.inner_run(repo)) {
68            status_err!("{}", err);
69            RUSTIC_APP.shutdown(Shutdown::Crash);
70        };
71    }
72}
73
74impl CopyCmd {
75    fn inner_run(&self, repo: CliIndexedRepo) -> Result<()> {
76        let config = RUSTIC_APP.config();
77        let mut snapshots = if self.ids.is_empty() {
78            get_filtered_snapshots(&repo)?
79        } else {
80            repo.get_snapshots(&self.ids)?
81        };
82        // sort for nicer output
83        snapshots.sort_unstable();
84
85        for target in &config.copy.targets {
86            let mut merge_logs = Vec::new();
87            let mut target_config = RusticConfig::default();
88            target_config.merge_profile(target, &mut merge_logs, Level::Error)?;
89            // display logs from merging
90            for (level, merge_log) in merge_logs {
91                log!(level, "{merge_log}");
92            }
93            let target_opt = &target_config.repository;
94            if let Err(err) =
95                target_opt.run(|target_repo| self.copy(&repo, target_repo, &snapshots))
96            {
97                error!("error copying to target: {err}");
98            }
99        }
100        Ok(())
101    }
102
103    fn copy(
104        &self,
105        repo: &CliIndexedRepo,
106        target_repo: CliRepo,
107        snapshots: &[SnapshotFile],
108    ) -> Result<()> {
109        let config = RUSTIC_APP.config();
110
111        info!("copying to target {}...", target_repo.name);
112        let target_repo = if self.init && target_repo.config_id()?.is_none() {
113            let mut config_dest = repo.config().clone();
114            config_dest.id = Id::random().into();
115            let pass = init_password(&target_repo)?;
116            target_repo
117                .0
118                .init_with_config(&pass, &self.key_opts, config_dest)?
119        } else {
120            target_repo.open()?
121        };
122
123        if !repo.config().has_same_chunker(target_repo.config()) {
124            bail!(
125                "cannot copy to repository with different chunker parameter (re-chunking not implemented)!"
126            );
127        }
128
129        let snaps = target_repo.relevant_copy_snapshots(
130            |sn| !self.ids.is_empty() || config.snapshot_filter.matches(sn),
131            snapshots,
132        )?;
133
134        let mut table =
135            table_with_titles(["ID", "Time", "Host", "Label", "Tags", "Paths", "Status"]);
136        for CopySnapshot { relevant, sn } in &snaps {
137            let tags = sn.tags.formatln();
138            let paths = sn.paths.formatln();
139            let time = sn.time.format("%Y-%m-%d %H:%M:%S").to_string();
140            _ = table.add_row([
141                &sn.id.to_string(),
142                &time,
143                &sn.hostname,
144                &sn.label,
145                &tags,
146                &paths,
147                &(if *relevant { "to copy" } else { "existing" }).to_string(),
148            ]);
149        }
150        println!("{table}");
151
152        let count = snaps.iter().filter(|sn| sn.relevant).count();
153        if count > 0 {
154            if config.global.dry_run {
155                info!("would have copied {count} snapshots.");
156            } else {
157                repo.copy(
158                    &target_repo.to_indexed_ids()?,
159                    snaps
160                        .iter()
161                        .filter_map(|CopySnapshot { relevant, sn }| relevant.then_some(sn)),
162                )?;
163            }
164        } else {
165            info!("nothing to copy.");
166        }
167        Ok(())
168    }
169}