1use std::path::PathBuf;
4
5use crate::{
6 commands::{init::init, snapshots::fill_table},
7 config::hooks::Hooks,
8 helpers::{bold_cell, bytes_size_to_string, table},
9 repository::CliRepo,
10 status_err, Application, RUSTIC_APP,
11};
12
13use abscissa_core::{Command, Runnable, Shutdown};
14use anyhow::{anyhow, bail, Context, Result};
15use clap::ValueHint;
16use comfy_table::Cell;
17use conflate::Merge;
18use log::{debug, error, info, warn};
19use serde::{Deserialize, Serialize};
20use serde_with::serde_as;
21
22use rustic_core::{
23 BackupOptions, CommandInput, ConfigOptions, IndexedIds, KeyOptions, LocalSourceFilterOptions,
24 LocalSourceSaveOptions, ParentOptions, PathList, ProgressBars, Repository, SnapshotOptions,
25};
26
27#[serde_as]
29#[derive(Clone, Command, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)]
30#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
31#[allow(clippy::struct_excessive_bools)]
37pub struct BackupCmd {
38 #[clap(value_name = "SOURCE", value_hint = ValueHint::AnyPath)]
41 #[merge(skip)]
42 #[serde(skip)]
43 cli_sources: Vec<String>,
44
45 #[clap(long, value_name = "FILENAME", default_value = "stdin", value_hint = ValueHint::FilePath)]
47 #[merge(skip)]
48 stdin_filename: String,
49
50 #[clap(long, value_name = "COMMAND")]
52 #[merge(strategy=conflate::option::overwrite_none)]
53 stdin_command: Option<CommandInput>,
54
55 #[clap(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
57 #[merge(strategy=conflate::option::overwrite_none)]
58 as_path: Option<PathBuf>,
59
60 #[clap(flatten)]
62 #[serde(flatten)]
63 ignore_save_opts: LocalSourceSaveOptions,
64
65 #[clap(long)]
67 #[merge(strategy=conflate::bool::overwrite_false)]
68 pub no_scan: bool,
69
70 #[clap(long)]
72 #[merge(strategy=conflate::bool::overwrite_false)]
73 json: bool,
74
75 #[clap(long, conflicts_with = "json")]
77 #[merge(strategy=conflate::bool::overwrite_false)]
78 long: bool,
79
80 #[clap(long, conflicts_with_all = ["json", "long"])]
82 #[merge(strategy=conflate::bool::overwrite_false)]
83 quiet: bool,
84
85 #[clap(long)]
87 #[merge(strategy=conflate::bool::overwrite_false)]
88 init: bool,
89
90 #[clap(flatten, next_help_heading = "Options for parent processing")]
92 #[serde(flatten)]
93 parent_opts: ParentOptions,
94
95 #[clap(flatten, next_help_heading = "Exclude options")]
97 #[serde(flatten)]
98 ignore_filter_opts: LocalSourceFilterOptions,
99
100 #[clap(flatten, next_help_heading = "Snapshot options")]
102 #[serde(flatten)]
103 snap_opts: SnapshotOptions,
104
105 #[clap(flatten, next_help_heading = "Key options (when using --init)")]
107 #[serde(skip)]
108 #[merge(skip)]
109 key_opts: KeyOptions,
110
111 #[clap(flatten, next_help_heading = "Config options (when using --init)")]
113 #[serde(skip)]
114 #[merge(skip)]
115 config_opts: ConfigOptions,
116
117 #[clap(skip)]
119 hooks: Hooks,
120
121 #[clap(skip)]
123 #[merge(strategy = merge_snapshots)]
124 snapshots: Vec<BackupCmd>,
125
126 #[clap(skip)]
128 #[merge(skip)]
129 sources: Vec<String>,
130}
131
132pub(crate) fn merge_snapshots(left: &mut Vec<BackupCmd>, mut right: Vec<BackupCmd>) {
140 left.append(&mut right);
141 left.sort_by(|opt1, opt2| opt1.sources.cmp(&opt2.sources));
142 left.dedup_by(|opt1, opt2| opt1.sources == opt2.sources);
143}
144
145impl Runnable for BackupCmd {
146 fn run(&self) {
147 let config = RUSTIC_APP.config();
148
149 if !config.backup.sources.is_empty() {
151 status_err!("key \"sources\" is not valid in the [backup] section!");
152 RUSTIC_APP.shutdown(Shutdown::Crash);
153 }
154
155 let snapshot_opts = &config.backup.snapshots;
156 if snapshot_opts.iter().any(|opt| !opt.snapshots.is_empty()) {
158 status_err!("key \"snapshots\" is not valid in a [[backup.snapshots]] section!");
159 RUSTIC_APP.shutdown(Shutdown::Crash);
160 }
161 if let Err(err) = config.repository.run(|repo| self.inner_run(repo)) {
162 status_err!("{}", err);
163 RUSTIC_APP.shutdown(Shutdown::Crash);
164 };
165 }
166}
167
168impl BackupCmd {
169 fn inner_run(&self, repo: CliRepo) -> Result<()> {
170 let config = RUSTIC_APP.config();
171 let snapshot_opts = &config.backup.snapshots;
172
173 let repo = if self.init && repo.config_id()?.is_none() {
175 if config.global.dry_run {
176 bail!(
177 "cannot initialize repository {} in dry-run mode!",
178 repo.name
179 );
180 }
181 init(repo.0, &self.key_opts, &self.config_opts)?
182 } else {
183 repo.open()?
184 }
185 .to_indexed_ids()?;
186
187 let hooks = config.backup.hooks.with_context("backup");
188 hooks.use_with(|| -> Result<_> {
189 let config_snapshot_sources: Vec<_> = snapshot_opts
190 .iter()
191 .map(|opt| -> Result<_> {
192 Ok(PathList::from_iter(&opt.sources)
193 .sanitize()
194 .with_context(|| {
195 format!(
196 "error sanitizing sources=\"{:?}\" in config file",
197 opt.sources
198 )
199 })?
200 .merge())
201 })
202 .filter_map(|p| match p {
203 Ok(paths) => Some(paths),
204 Err(err) => {
205 warn!("{err}");
206 None
207 }
208 })
209 .collect();
210
211 let snapshot_sources = match (self.cli_sources.is_empty(), snapshot_opts.is_empty()) {
212 (false, _) => {
213 let item = PathList::from_iter(&self.cli_sources).sanitize()?;
214 vec![item]
215 }
216 (true, false) => {
217 info!("using all backup sources from config file.");
218 config_snapshot_sources.clone()
219 }
220 (true, true) => {
221 bail!("no backup source given.");
222 }
223 };
224 if snapshot_sources.is_empty() {
225 return Ok(());
226 }
227
228 let mut is_err = false;
229 for sources in snapshot_sources {
230 let mut opts = self.clone();
231
232 if let Some(idx) = config_snapshot_sources.iter().position(|s| s == &sources) {
234 info!("merging sources={sources} section from config file");
235 opts.merge(snapshot_opts[idx].clone());
236 }
237 if let Err(err) = opts.backup_snapshot(sources.clone(), &repo) {
238 error!("error backing up {sources}: {err}");
239 is_err = true;
240 }
241 }
242 if is_err {
243 Err(anyhow!("Not all snapshots were generated successfully!"))
244 } else {
245 Ok(())
246 }
247 })
248 }
249
250 fn backup_snapshot<P: ProgressBars, S: IndexedIds>(
251 mut self,
252 source: PathList,
253 repo: &Repository<P, S>,
254 ) -> Result<()> {
255 let config = RUSTIC_APP.config();
256 let snapshot_opts = &config.backup.snapshots;
257 if let Some(path) = &self.as_path {
258 if source.len() > 1 {
260 bail!("as-path only works with a single source!");
261 }
262 if let Some(path) = path.as_os_str().to_str() {
264 if let Some(idx) = snapshot_opts
265 .iter()
266 .position(|opt| opt.sources == vec![path])
267 {
268 info!("merging snapshot=\"{path}\" section from config file");
269 self.merge(snapshot_opts[idx].clone());
270 }
271 }
272 }
273
274 let hooks = self.hooks.with_context(&format!("backup {source}"));
276
277 self.merge(config.backup.clone());
279
280 let backup_opts = BackupOptions::default()
281 .stdin_filename(self.stdin_filename)
282 .stdin_command(self.stdin_command)
283 .as_path(self.as_path)
284 .parent_opts(self.parent_opts)
285 .ignore_save_opts(self.ignore_save_opts)
286 .ignore_filter_opts(self.ignore_filter_opts)
287 .no_scan(self.no_scan)
288 .dry_run(config.global.dry_run);
289
290 let snap = hooks.use_with(|| -> Result<_> {
291 Ok(repo.backup(&backup_opts, &source, self.snap_opts.to_snapshot()?)?)
292 })?;
293
294 if self.json {
295 let mut stdout = std::io::stdout();
296 serde_json::to_writer_pretty(&mut stdout, &snap)?;
297 } else if self.long {
298 let mut table = table();
299
300 let add_entry = |title: &str, value: String| {
301 _ = table.add_row([bold_cell(title), Cell::new(value)]);
302 };
303 fill_table(&snap, add_entry);
304
305 println!("{table}");
306 } else if !self.quiet {
307 let summary = snap.summary.unwrap();
308 println!(
309 "Files: {} new, {} changed, {} unchanged",
310 summary.files_new, summary.files_changed, summary.files_unmodified
311 );
312 println!(
313 "Dirs: {} new, {} changed, {} unchanged",
314 summary.dirs_new, summary.dirs_changed, summary.dirs_unmodified
315 );
316 debug!("Data Blobs: {} new", summary.data_blobs);
317 debug!("Tree Blobs: {} new", summary.tree_blobs);
318 println!(
319 "Added to the repo: {} (raw: {})",
320 bytes_size_to_string(summary.data_added_packed),
321 bytes_size_to_string(summary.data_added)
322 );
323
324 println!(
325 "processed {} files, {}",
326 summary.total_files_processed,
327 bytes_size_to_string(summary.total_bytes_processed)
328 );
329 println!("snapshot {} successfully saved.", snap.id);
330 }
331
332 info!("backup of {source} done.");
333 Ok(())
334 }
335}