Skip to main content

rustic_core/repofile/snapshotfile/
modification.rs

1use std::path::PathBuf;
2
3use cached::proc_macro::cached;
4use derive_setters::Setters;
5use jiff::{Span, Zoned};
6use serde::{Deserialize, Serialize};
7use serde_with::{DisplayFromStr, serde_as};
8
9use crate::{
10    ErrorKind, RusticError, RusticResult, StringList,
11    repofile::{DeleteOption, RusticTime, SnapshotFile},
12};
13
14/// Modification(s) to apply to a snapshot
15#[cfg_attr(feature = "clap", derive(clap::Parser))]
16#[cfg_attr(feature = "merge", derive(conflate::Merge))]
17#[serde_as]
18#[derive(Debug, Clone, Default, Setters, Serialize, Deserialize)]
19#[setters(into)]
20#[non_exhaustive]
21pub struct SnapshotModification {
22    /// Set label
23    #[cfg_attr(feature = "clap", clap(long, value_name = "LABEL"))]
24    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
25    pub set_label: Option<String>,
26
27    /// Set the backup time (e.g. "2021-01-21 14:15:23")
28    #[cfg_attr(feature = "clap", clap(long, value_parser = crate::repofile::RusticTime::parse_system))]
29    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
30    #[serde_as(as = "Option<RusticTime>")]
31    pub set_time: Option<Zoned>,
32
33    /// Set the host name
34    #[cfg_attr(feature = "clap", clap(long, value_name = "NAME"))]
35    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
36    pub set_hostname: Option<String>,
37
38    /// Tags to add (can be specified multiple times)
39    #[cfg_attr(feature = "clap", clap(long, value_name = "TAG[,TAG,..]"))]
40    #[cfg_attr(feature = "merge", merge(strategy = conflate::vec::overwrite_empty))]
41    pub add_tags: Vec<StringList>,
42
43    /// Tag list to set (can be specified multiple times)
44    #[cfg_attr(feature = "clap", clap(long, value_name = "TAG[,TAG,..]"))]
45    #[cfg_attr(feature = "merge", merge(strategy = conflate::vec::overwrite_empty))]
46    pub set_tags: Vec<StringList>,
47
48    /// Tags to remove (can be specified multiple times)
49    #[cfg_attr(feature = "clap", clap(long, value_name = "TAG[,TAG,..]"))]
50    #[cfg_attr(feature = "merge", merge(strategy = conflate::vec::overwrite_empty))]
51    pub remove_tags: Vec<StringList>,
52
53    /// Set description
54    #[cfg_attr(feature = "clap", clap(long, value_name = "DESCRIPTION"))]
55    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
56    pub set_description: Option<String>,
57
58    /// Read description to set from the given file
59    #[cfg_attr(
60        feature = "clap",
61        clap(long, value_name = "FILE", conflicts_with = "set_description", value_hint = clap::ValueHint::FilePath)
62     )]
63    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
64    pub set_description_from: Option<PathBuf>,
65
66    /// Remove description
67    #[cfg_attr(feature = "clap", clap(long, conflicts_with_all = &["set_description", "set_description_from"]))]
68    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
69    pub remove_description: bool,
70
71    /// Mark snapshot to be deleted after given duration (e.g. 10d)
72    #[cfg_attr(feature = "clap", clap(long, value_name = "DURATION"))]
73    #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
74    #[serde_as(as = "Option<DisplayFromStr>")]
75    pub set_delete_after: Option<Span>,
76
77    /// Mark snapshot as uneraseable
78    #[cfg_attr(feature = "clap", clap(long, conflicts_with = "set_delete_after"))]
79    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
80    pub set_delete_never: bool,
81
82    /// Remove any delete mark
83    #[cfg_attr(feature = "clap", clap(long, conflicts_with_all = &["set_delete_never", "set_delete_after"]))]
84    #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
85    pub remove_delete: bool,
86}
87
88// cache description if read from file
89#[cached(size = 1)]
90fn get_description_from_file(path: PathBuf) -> Result<String, String> {
91    std::fs::read_to_string(path).map_err(|err| format!("{err:?}"))
92}
93
94impl SnapshotModification {
95    /// Apply this modification(s) to the given snapshot
96    ///
97    /// # Returns
98    /// `true` if the snapshot was changed.
99    ///
100    /// # Errors
101    /// if reading a description from a file failed
102    pub fn apply_to(&self, sn: &mut SnapshotFile) -> RusticResult<bool> {
103        let delete = match (
104            self.remove_delete,
105            self.set_delete_never,
106            self.set_delete_after,
107        ) {
108            (true, _, _) => Some(DeleteOption::NotSet),
109            (_, true, _) => Some(DeleteOption::Never),
110            (_, _, Some(d)) => Some(DeleteOption::After(Zoned::now() + d)),
111            (false, false, None) => None,
112        };
113
114        let description = match (self.remove_description, &self.set_description_from) {
115            (true, _) => Some(None),
116            (false, Some(path)) => Some(Some(get_description_from_file(path.clone()).map_err(
117                |err| {
118                    RusticError::with_source(
119                        ErrorKind::Other,
120                        "Failed to read description from file {path}.",
121                        err,
122                    )
123                    .attach_context("path", path.to_string_lossy())
124                },
125            )?)),
126            (false, None) => self
127                .set_description
128                .as_ref()
129                .map(|description| Some(description.clone())),
130        };
131
132        let mut changed = false;
133
134        if !self.set_tags.is_empty() {
135            changed |= sn.set_tags(self.set_tags.clone());
136        }
137        changed |= sn.add_tags(self.add_tags.clone());
138        changed |= sn.remove_tags(&self.remove_tags);
139        changed |= set_check(&mut sn.delete, &delete);
140        changed |= set_check(&mut sn.label, &self.set_label);
141        changed |= set_check(&mut sn.description, &description);
142        changed |= set_check(&mut sn.time, &self.set_time);
143        changed |= set_check(&mut sn.hostname, &self.set_hostname);
144        Ok(changed)
145    }
146}
147
148#[allow(clippy::ref_option)]
149fn set_check<T: PartialEq + Clone>(a: &mut T, b: &Option<T>) -> bool {
150    if let Some(b) = b
151        && *a != *b
152    {
153        *a = b.clone();
154        return true;
155    }
156    false
157}