rustic_rs/commands/
forget.rs

1//! `forget` subcommand
2
3use 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/// `forget` subcommand
22#[derive(clap::Parser, Command, Debug)]
23pub(super) struct ForgetCmd {
24    /// Snapshots to forget. If none is given, use filter options to filter from all snapshots
25    #[clap(value_name = "ID")]
26    ids: Vec<String>,
27
28    /// Show infos in json format
29    #[clap(long)]
30    json: bool,
31
32    /// Don't show any output
33    #[clap(long, conflicts_with = "json")]
34    quiet: bool,
35
36    /// Forget options
37    #[clap(flatten)]
38    config: ForgetOptions,
39
40    /// Prune options (only when used with --prune)
41    #[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    // Process the given command line options, overriding settings from
50    // a configuration file using explicit flags taken from command-line
51    // arguments.
52    fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
53        let mut self_config = self.config.clone();
54        // merge "forget" section from config file, if given
55        self_config.merge(config.forget);
56        // merge "snapshot-filter" section from config file, if given
57        self_config.filter.merge(config.snapshot_filter.clone());
58        config.forget = self_config;
59        Ok(config)
60    }
61}
62
63/// Forget options
64#[serde_as]
65#[derive(Clone, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)]
66#[serde(default, rename_all = "kebab-case")]
67pub struct ForgetOptions {
68    /// Group snapshots by any combination of host,label,paths,tags (default: "host,label,paths")
69    #[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    /// Also prune the repository
75    #[clap(long)]
76    #[merge(strategy=conflate::bool::overwrite_false)]
77    prune: bool,
78
79    /// Snapshot filter options
80    #[clap(flatten, next_help_heading = "Snapshot filter options")]
81    #[serde(flatten)]
82    filter: SnapshotFilter,
83
84    /// Retention options
85    #[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    /// be careful about self vs `RUSTIC_APP.config()` usage
105    /// only the `RUSTIC_APP.config()` involves the TOML and ENV merged configurations
106    /// see <https://github.com/rustic-rs/rustic/issues/1242>
107    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
187/// Print groups to stdout
188///
189/// # Arguments
190///
191/// * `groups` - forget groups to print
192fn 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}