1use std::path::PathBuf;
4
5use crate::{
6 Application, RUSTIC_APP,
7 repository::{CliOpenRepo, get_filtered_snapshots},
8 status_err,
9};
10
11use abscissa_core::{Command, Runnable, Shutdown};
12
13use anyhow::Result;
14use chrono::{DateTime, Duration, Local};
15
16use clap::ValueHint;
17use rustic_core::{StringList, repofile::DeleteOption};
18
19#[derive(clap::Parser, Command, Debug, Default)]
21pub(crate) struct RewriteCmd {
22 #[clap(value_name = "ID")]
24 pub ids: Vec<String>,
25
26 #[clap(long, value_name = "LABEL", help_heading = "Snapshot options")]
28 pub set_label: Option<String>,
29
30 #[clap(long, help_heading = "Snapshot options")]
32 pub set_time: Option<DateTime<Local>>,
33
34 #[clap(long, value_name = "NAME", help_heading = "Snapshot options")]
36 pub set_hostname: Option<String>,
37
38 #[clap(
40 long,
41 value_name = "TAG[,TAG,..]",
42 conflicts_with = "remove_tags",
43 help_heading = "Tag options"
44 )]
45 pub add_tags: Vec<StringList>,
46
47 #[clap(
49 long,
50 value_name = "TAG[,TAG,..]",
51 conflicts_with = "remove_tags",
52 help_heading = "Tag options"
53 )]
54 pub set_tags: Vec<StringList>,
55
56 #[clap(long, value_name = "TAG[,TAG,..]", help_heading = "Tag options")]
58 pub remove_tags: Vec<StringList>,
59
60 #[clap(long, value_name = "DESCRIPTION", help_heading = "Description options")]
62 pub set_description: Option<String>,
63
64 #[clap(long, value_name = "FILE", conflicts_with = "set_description", value_hint = ValueHint::FilePath, help_heading = "Description options")]
66 pub set_description_from: Option<PathBuf>,
67
68 #[clap(
70 long,
71 conflicts_with_all = &["set_description", "set_description_from"],
72 help_heading = "Description options"
73 )]
74 pub remove_description: bool,
75
76 #[clap(
78 long,
79 conflicts_with = "set_delete_after",
80 help_heading = "Delete mark options"
81 )]
82 pub set_delete_never: bool,
83
84 #[clap(long, value_name = "DURATION", help_heading = "Delete mark options")]
86 pub set_delete_after: Option<humantime::Duration>,
87
88 #[clap(
90 long,
91 conflicts_with_all = &["set_delete_never", "set_delete_after"],
92 help_heading = "Delete mark options"
93 )]
94 pub remove_delete: bool,
95}
96
97impl Runnable for RewriteCmd {
98 fn run(&self) {
99 if let Err(err) = RUSTIC_APP
100 .config()
101 .repository
102 .run_open(|repo| self.inner_run(repo))
103 {
104 status_err!("{}", err);
105 RUSTIC_APP.shutdown(Shutdown::Crash);
106 };
107 }
108}
109
110impl RewriteCmd {
111 fn inner_run(&self, repo: CliOpenRepo) -> Result<()> {
112 let config = RUSTIC_APP.config();
113
114 let snapshots = if self.ids.is_empty() {
115 get_filtered_snapshots(&repo)?
116 } else {
117 repo.get_snapshots(&self.ids)?
118 };
119
120 let delete = match (
121 self.remove_delete,
122 self.set_delete_never,
123 self.set_delete_after,
124 ) {
125 (true, _, _) => Some(DeleteOption::NotSet),
126 (_, true, _) => Some(DeleteOption::Never),
127 (_, _, Some(d)) => Some(DeleteOption::After(Local::now() + Duration::from_std(*d)?)),
128 (false, false, None) => None,
129 };
130
131 let description = match (self.remove_description, &self.set_description_from) {
132 (true, _) => Some(None),
133 (false, Some(path)) => Some(Some(std::fs::read_to_string(path)?)),
134 (false, None) => self
135 .set_description
136 .as_ref()
137 .map(|description| Some(description.clone())),
138 };
139
140 let snapshots: Vec<_> = snapshots
141 .into_iter()
142 .filter_map(|mut sn| {
143 let mut changed = sn
144 .modify_sn(
145 self.set_tags.clone(),
146 self.add_tags.clone(),
147 &self.remove_tags,
148 &None, )
150 .is_some();
151 changed |= set_check(&mut sn.delete, &delete);
152 changed |= set_check(&mut sn.label, &self.set_label);
153 changed |= set_check(&mut sn.description, &description);
154 changed |= set_check(&mut sn.time, &self.set_time);
155 changed |= set_check(&mut sn.hostname, &self.set_hostname);
156 changed.then_some(sn)
157 })
158 .collect();
159 let old_snap_ids: Vec<_> = snapshots.iter().map(|sn| sn.id).collect();
160
161 match (old_snap_ids.is_empty(), config.global.dry_run) {
162 (true, _) => println!("no snapshot changed."),
163 (false, true) => {
164 println!("would have modified the following snapshots:\n {old_snap_ids:?}");
165 }
166 (false, false) => {
167 repo.save_snapshots(snapshots)?;
168 repo.delete_snapshots(&old_snap_ids)?;
169 }
170 }
171
172 Ok(())
173 }
174}
175
176fn set_check<T: PartialEq + Clone>(a: &mut T, b: &Option<T>) -> bool {
177 if let Some(b) = b {
178 if *a != *b {
179 *a = b.clone();
180 return true;
181 }
182 }
183 false
184}