1use crate::repository::{CliOpenRepo, get_grouped_snapshots};
4use crate::{Application, RUSTIC_APP, RusticConfig, helpers::table_with_titles, status_err};
5
6use abscissa_core::{Command, FrameworkError, Runnable};
7use abscissa_core::{Shutdown, config::Override};
8use anyhow::Result;
9use chrono::Local;
10use conflate::Merge;
11use log::info;
12use serde::{Deserialize, Serialize};
13use serde_with::{DisplayFromStr, serde_as};
14
15use crate::{commands::prune::PruneCmd, filtering::SnapshotFilter};
16
17use rustic_core::{
18 ForgetGroup, ForgetGroups, ForgetSnapshot, KeepOptions, SnapshotGroup, SnapshotGroupCriterion,
19};
20
21#[derive(clap::Parser, Command, Debug)]
23pub(super) struct ForgetCmd {
24 #[clap(value_name = "ID")]
26 ids: Vec<String>,
27
28 #[clap(long)]
30 json: bool,
31
32 #[clap(long, conflicts_with = "json")]
34 quiet: bool,
35
36 #[clap(flatten)]
38 config: ForgetOptions,
39
40 #[clap(
42 flatten,
43 next_help_heading = "PRUNE OPTIONS (only when used with --prune)"
44 )]
45 prune_opts: PruneCmd,
46}
47
48impl Override<RusticConfig> for ForgetCmd {
49 fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
53 let mut self_config = self.config.clone();
54 self_config.merge(config.forget);
56 self_config.filter.merge(config.snapshot_filter.clone());
58 config.forget = self_config;
59 Ok(config)
60 }
61}
62
63#[serde_as]
65#[derive(Clone, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)]
66#[serde(default, rename_all = "kebab-case")]
67pub struct ForgetOptions {
68 #[clap(long, short = 'g', value_name = "CRITERION")]
70 #[serde_as(as = "Option<DisplayFromStr>")]
71 #[merge(strategy=conflate::option::overwrite_none)]
72 group_by: Option<SnapshotGroupCriterion>,
73
74 #[clap(long)]
76 #[merge(strategy=conflate::bool::overwrite_false)]
77 prune: bool,
78
79 #[clap(flatten, next_help_heading = "Snapshot filter options")]
81 #[serde(flatten)]
82 filter: SnapshotFilter,
83
84 #[clap(flatten, next_help_heading = "Retention options")]
86 #[serde(flatten)]
87 keep: KeepOptions,
88}
89
90impl Runnable for ForgetCmd {
91 fn run(&self) {
92 if let Err(err) = RUSTIC_APP
93 .config()
94 .repository
95 .run_open(|repo| self.inner_run(repo))
96 {
97 status_err!("{}", err);
98 RUSTIC_APP.shutdown(Shutdown::Crash);
99 };
100 }
101}
102
103impl ForgetCmd {
104 fn inner_run(&self, repo: CliOpenRepo) -> Result<()> {
108 let config = RUSTIC_APP.config();
109
110 let group_by = config
111 .forget
112 .group_by
113 .or(config.global.group_by)
114 .unwrap_or_default();
115
116 let now = Local::now();
117
118 let groups = if self.ids.is_empty() {
119 ForgetGroups(
120 get_grouped_snapshots(&repo, group_by, &[])?
121 .into_iter()
122 .map(|(group, snapshots)| -> Result<_> {
123 Ok(ForgetGroup {
124 group,
125 snapshots: config.forget.keep.apply(snapshots, now)?,
126 })
127 })
128 .collect::<Result<_>>()?,
129 )
130 } else {
131 let now = Local::now();
132 let item = ForgetGroup {
133 group: SnapshotGroup::default(),
134 snapshots: repo
135 .get_snapshots(&self.ids)?
136 .into_iter()
137 .map(|sn| {
138 if sn.must_keep(now) {
139 ForgetSnapshot {
140 snapshot: sn,
141 keep: true,
142 reasons: vec!["snapshot".to_string()],
143 }
144 } else {
145 ForgetSnapshot {
146 snapshot: sn,
147 keep: false,
148 reasons: vec!["id argument".to_string()],
149 }
150 }
151 })
152 .collect(),
153 };
154 ForgetGroups(vec![item])
155 };
156
157 if self.json {
158 let mut stdout = std::io::stdout();
159 serde_json::to_writer_pretty(&mut stdout, &groups)?;
160 } else if !self.quiet {
161 print_groups(&groups);
162 }
163
164 let forget_snaps = groups.into_forget_ids();
165
166 match (forget_snaps.is_empty(), config.global.dry_run, self.json) {
167 (true, _, false) => info!("nothing to remove"),
168 (false, true, false) => {
169 info!("would have removed the following snapshots:\n {forget_snaps:?}");
170 }
171 (false, false, _) => {
172 repo.delete_snapshots(&forget_snaps)?;
173 }
174 (_, _, true) => {}
175 }
176
177 if config.forget.prune {
178 let mut prune_opts = self.prune_opts.clone();
179 prune_opts.opts.ignore_snaps = forget_snaps;
180 prune_opts.run();
181 }
182
183 Ok(())
184 }
185}
186
187fn print_groups(groups: &ForgetGroups) {
193 for ForgetGroup { group, snapshots } in &groups.0 {
194 let mut table = table_with_titles([
195 "ID", "Time", "Host", "Label", "Tags", "Paths", "Action", "Reason",
196 ]);
197
198 for ForgetSnapshot {
199 snapshot: sn,
200 keep,
201 reasons,
202 } in snapshots
203 {
204 let time = sn.time.format("%Y-%m-%d %H:%M:%S").to_string();
205 let tags = sn.tags.formatln();
206 let paths = sn.paths.formatln();
207 let action = if *keep { "keep" } else { "remove" };
208 let reason = reasons.join("\n");
209 _ = table.add_row([
210 &sn.id.to_string(),
211 &time,
212 &sn.hostname,
213 &sn.label,
214 &tags,
215 &paths,
216 action,
217 &reason,
218 ]);
219 }
220
221 if !group.is_empty() {
222 info!("snapshots for {group}:\n{table}");
223 } else {
224 info!("snapshots:\n{table}");
225 }
226 }
227}