rustic_rs/commands/
forget.rs1use crate::repository::{OpenRepo, 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 conflate::Merge;
10use jiff::Zoned;
11use log::info;
12use rustic_core::repofile::RusticTime;
13use serde::{Deserialize, Serialize};
14use serde_with::{DisplayFromStr, serde_as};
15
16use crate::{commands::prune::PruneCmd, filtering::SnapshotFilter};
17
18use rustic_core::{ForgetGroups, ForgetSnapshot, KeepOptions, SnapshotGroupCriterion};
19
20#[derive(clap::Parser, Command, Debug)]
22pub(super) struct ForgetCmd {
23 #[clap(value_name = "ID")]
27 ids: Vec<String>,
28
29 #[clap(long,value_parser = RusticTime::parse_system)]
31 pub forget_time: Option<Zoned>,
32
33 #[clap(long)]
35 json: bool,
36
37 #[clap(flatten)]
39 config: ForgetOptions,
40
41 #[clap(
43 flatten,
44 next_help_heading = "PRUNE OPTIONS (only when used with --prune)"
45 )]
46 prune_opts: PruneCmd,
47}
48
49impl Override<RusticConfig> for ForgetCmd {
50 fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
54 let mut self_config = self.config.clone();
55 self_config.merge(config.forget);
57 self_config.filter.merge(config.snapshot_filter.clone());
59 config.forget = self_config;
60 Ok(config)
61 }
62}
63
64#[serde_as]
66#[derive(Clone, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)]
67#[serde(default, rename_all = "kebab-case")]
68pub struct ForgetOptions {
69 #[clap(long, short = 'g', value_name = "CRITERION")]
71 #[serde_as(as = "Option<DisplayFromStr>")]
72 #[merge(strategy=conflate::option::overwrite_none)]
73 group_by: Option<SnapshotGroupCriterion>,
74
75 #[clap(long)]
77 #[merge(strategy=conflate::bool::overwrite_false)]
78 prune: bool,
79
80 #[clap(flatten, next_help_heading = "Snapshot filter options")]
82 #[serde(flatten)]
83 filter: SnapshotFilter,
84
85 #[clap(flatten, next_help_heading = "Retention options")]
87 #[serde(flatten)]
88 keep: KeepOptions,
89}
90
91impl Runnable for ForgetCmd {
92 fn run(&self) {
93 if let Err(err) = RUSTIC_APP
94 .config()
95 .repository
96 .run_open(|repo| self.inner_run(repo))
97 {
98 status_err!("{}", err);
99 RUSTIC_APP.shutdown(Shutdown::Crash);
100 };
101 }
102}
103
104impl ForgetCmd {
105 fn inner_run(&self, repo: OpenRepo) -> Result<()> {
109 let config = RUSTIC_APP.config();
110
111 let group_by = config
112 .forget
113 .group_by
114 .or(config.global.group_by)
115 .unwrap_or_default();
116
117 if let Some(time) = &self.forget_time {
118 info!("using time: {time}");
119 }
120 let now = self.forget_time.clone().unwrap_or_else(Zoned::now);
121
122 let groups = if self.ids.is_empty() {
123 ForgetGroups::from_grouped_snapshots_with_retention(
124 get_grouped_snapshots(&repo, group_by, &[])?,
125 &config.forget.keep,
126 &now,
127 )?
128 } else {
129 ForgetGroups::from_snapshots(
130 repo.get_snapshots_from_strs(&self.ids, |sn| config.snapshot_filter.matches(sn))?,
131 &now,
132 )
133 };
134
135 if self.json {
136 let mut stdout = std::io::stdout();
137 serde_json::to_writer_pretty(&mut stdout, &groups)?;
138 } else {
139 print_groups(&groups);
140 }
141
142 let forget_snaps = groups.into_forget_ids();
143
144 match (forget_snaps.is_empty(), config.global.dry_run, self.json) {
145 (true, _, false) => info!("nothing to remove"),
146 (false, true, false) => {
147 info!("would have removed {} snapshots.", forget_snaps.len());
148 }
149 (false, false, _) => {
150 repo.delete_snapshots(&forget_snaps)?;
151 }
152 (_, _, true) => {}
153 }
154
155 if config.forget.prune {
156 let mut prune_opts = self.prune_opts.clone();
157 prune_opts.opts.ignore_snaps = forget_snaps;
158 prune_opts.run();
159 }
160
161 Ok(())
162 }
163}
164
165fn print_groups(groups: &ForgetGroups) {
171 let config = RUSTIC_APP.config();
172 for group in &groups.0 {
173 let mut table = table_with_titles([
174 "ID", "Time", "Host", "Label", "Tags", "Paths", "Action", "Reason",
175 ]);
176
177 for ForgetSnapshot {
178 snapshot: sn,
179 keep,
180 reasons,
181 } in &group.items
182 {
183 let time = config.global.format_time(&sn.time).to_string();
184 let tags = sn.tags.formatln();
185 let paths = sn.paths.formatln();
186 let action = if *keep { "keep" } else { "remove" };
187 let reason = reasons.join("\n");
188 _ = table.add_row([
189 &sn.id.to_string(),
190 &time,
191 &sn.hostname,
192 &sn.label,
193 &tags,
194 &paths,
195 action,
196 &reason,
197 ]);
198 }
199
200 if !group.group_key.is_empty() {
201 info!("snapshots for {}:\n{table}", group.group_key);
202 } else {
203 info!("snapshots:\n{table}");
204 }
205 }
206}