Skip to main content

rustic_rs/commands/
copy.rs

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