rustic_rs/commands/
copy.rs1use 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#[derive(clap::Parser, Command, Default, Clone, Debug, Serialize, Deserialize, Merge)]
20pub struct CopyCmd {
21 #[clap(value_name = "ID")]
25 #[serde(skip)]
26 #[merge(skip)]
27 ids: Vec<String>,
28
29 #[clap(long = "target", value_name = "TARGET")]
31 #[merge(strategy=conflate::vec::overwrite_empty)]
32 targets: Vec<String>,
33
34 #[clap(long)]
36 #[serde(skip)]
37 #[merge(skip)]
38 init: bool,
39
40 #[clap(long)]
42 #[serde(skip)]
43 #[merge(skip)]
44 force: bool,
45
46 #[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 fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
58 let mut self_config = self.clone();
59 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 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 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}