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