rustic_rs/commands/
copy.rs1use 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#[derive(clap::Parser, Command, Default, Clone, Debug, Serialize, Deserialize, Merge)]
19pub struct CopyCmd {
20 #[clap(value_name = "ID")]
22 #[serde(skip)]
23 #[merge(skip)]
24 ids: Vec<String>,
25
26 #[clap(long)]
28 #[serde(skip)]
29 #[merge(skip)]
30 init: bool,
31
32 #[clap(long = "target", value_name = "TARGET")]
34 #[merge(strategy=conflate::vec::overwrite_empty)]
35 targets: Vec<String>,
36
37 #[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 fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
49 let mut self_config = self.clone();
50 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 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 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}