rustic_rs/commands/
rewrite.rs

1//! `rewrite` subcommand
2
3use 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/// `rewrite` subcommand
20#[derive(clap::Parser, Command, Debug, Default)]
21pub(crate) struct RewriteCmd {
22    /// Snapshots to rewrite. If none is given, use filter to filter from all snapshots.
23    #[clap(value_name = "ID")]
24    pub ids: Vec<String>,
25
26    /// Set label
27    #[clap(long, value_name = "LABEL", help_heading = "Snapshot options")]
28    pub set_label: Option<String>,
29
30    /// Set the backup time (e.g. "2021-01-21 14:15:23+0000")
31    #[clap(long, help_heading = "Snapshot options")]
32    pub set_time: Option<DateTime<Local>>,
33
34    /// Set the host name
35    #[clap(long, value_name = "NAME", help_heading = "Snapshot options")]
36    pub set_hostname: Option<String>,
37
38    /// Tags to add (can be specified multiple times)
39    #[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    /// Tag list to set (can be specified multiple times)
48    #[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    /// Tags to remove (can be specified multiple times)
57    #[clap(long, value_name = "TAG[,TAG,..]", help_heading = "Tag options")]
58    pub remove_tags: Vec<StringList>,
59
60    /// Set description
61    #[clap(long, value_name = "DESCRIPTION", help_heading = "Description options")]
62    pub set_description: Option<String>,
63
64    /// Read description to set from the given file
65    #[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    /// Remove description
69    #[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    /// Mark snapshot as uneraseable
77    #[clap(
78        long,
79        conflicts_with = "set_delete_after",
80        help_heading = "Delete mark options"
81    )]
82    pub set_delete_never: bool,
83
84    /// Mark snapshot to be deleted after given duration (e.g. 10d)
85    #[clap(long, value_name = "DURATION", help_heading = "Delete mark options")]
86    pub set_delete_after: Option<humantime::Duration>,
87
88    /// Remove any delete mark
89    #[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, // TODO: Remove after modify_sn is refactored to modify_tags
149                    )
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}