rustic_core/repofile/snapshotfile/
modification.rs1use 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#[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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#[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 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}