Skip to main content

rustic_rs/commands/
forget.rs

1//! `forget` subcommand
2
3use 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/// `forget` subcommand
21#[derive(clap::Parser, Command, Debug)]
22pub(super) struct ForgetCmd {
23    /// Snapshots to forget. If none is given, use filter options to filter from all snapshots
24    ///
25    /// Snapshots can be identified the following ways: "01a2b3c4" or "latest" or "latest~N" (N >= 0)
26    #[clap(value_name = "ID")]
27    ids: Vec<String>,
28
29    /// Set the date/time (e.g. "2021-01-21) to use when evaluating retention rules; can be used to test the rules (default: now)
30    #[clap(long,value_parser = RusticTime::parse_system)]
31    pub forget_time: Option<Zoned>,
32
33    /// Show infos in json format
34    #[clap(long)]
35    json: bool,
36
37    /// Forget options
38    #[clap(flatten)]
39    config: ForgetOptions,
40
41    /// Prune options (only when used with --prune)
42    #[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    // Process the given command line options, overriding settings from
51    // a configuration file using explicit flags taken from command-line
52    // arguments.
53    fn override_config(&self, mut config: RusticConfig) -> Result<RusticConfig, FrameworkError> {
54        let mut self_config = self.config.clone();
55        // merge "forget" section from config file, if given
56        self_config.merge(config.forget);
57        // merge "snapshot-filter" section from config file, if given
58        self_config.filter.merge(config.snapshot_filter.clone());
59        config.forget = self_config;
60        Ok(config)
61    }
62}
63
64/// Forget options
65#[serde_as]
66#[derive(Clone, Default, Debug, clap::Parser, Serialize, Deserialize, Merge)]
67#[serde(default, rename_all = "kebab-case")]
68pub struct ForgetOptions {
69    /// Group snapshots by any combination of host,label,paths,tags (default: "host,label,paths")
70    #[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    /// Also prune the repository
76    #[clap(long)]
77    #[merge(strategy=conflate::bool::overwrite_false)]
78    prune: bool,
79
80    /// Snapshot filter options
81    #[clap(flatten, next_help_heading = "Snapshot filter options")]
82    #[serde(flatten)]
83    filter: SnapshotFilter,
84
85    /// Retention options
86    #[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    /// be careful about self vs `RUSTIC_APP.config()` usage
106    /// only the `RUSTIC_APP.config()` involves the TOML and ENV merged configurations
107    /// see <https://github.com/rustic-rs/rustic/issues/1242>
108    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
165/// Print groups to stdout
166///
167/// # Arguments
168///
169/// * `groups` - forget groups to print
170fn 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}