rustic_rs/commands/
copy.rs

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