1pub mod grouping;
2mod modification;
3
4pub use modification::SnapshotModification;
5
6use std::{
7 cmp::Ordering,
8 collections::{BTreeMap, BTreeSet},
9 fmt::{self, Display},
10 path::{Path, PathBuf},
11 str::FromStr,
12};
13
14use derive_setters::Setters;
15use dunce::canonicalize;
16use gethostname::gethostname;
17use itertools::Itertools;
18use jiff::{Span, Unit, Zoned};
19use log::{info, warn};
20use path_dedot::ParseDot;
21use serde::{Deserialize, Serialize};
22use serde_with::{DisplayFromStr, serde_as, skip_serializing_none};
23
24#[cfg(feature = "clap")]
25use clap::ValueHint;
26
27use crate::{
28 Id,
29 backend::{FileType, FindInBackend, decrypt::DecryptReadBackend},
30 blob::tree::TreeId,
31 error::{ErrorKind, RusticError, RusticResult},
32 id::{FindUniqueMultiple, FindUniqueResults, constants::HEX_LEN},
33 impl_repofile,
34 progress::Progress,
35 repofile::{RepoFile, RusticTime},
36};
37
38#[derive(thiserror::Error, Debug, displaydoc::Display)]
40#[non_exhaustive]
41pub enum SnapshotFileErrorKind {
42 NonUnicodePath(PathBuf),
44 ValueNotAllowed(String),
46 RemovingDotsFromPathFailed(std::io::Error),
48 CanonicalizingPathFailed(std::io::Error),
50}
51
52pub(crate) type SnapshotFileResult<T> = Result<T, SnapshotFileErrorKind>;
53
54#[serde_as]
67#[cfg_attr(feature = "merge", derive(conflate::Merge))]
68#[cfg_attr(feature = "clap", derive(clap::Parser))]
69#[derive(Deserialize, Serialize, Clone, Default, Debug, Setters)]
70#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
71#[setters(into)]
72#[non_exhaustive]
73pub struct SnapshotOptions {
74 #[cfg_attr(feature = "clap", clap(long, value_name = "LABEL"))]
76 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
77 pub label: Option<String>,
78
79 #[serde_as(as = "Vec<DisplayFromStr>")]
81 #[cfg_attr(feature = "clap", clap(long = "tag", value_name = "TAG[,TAG,..]"))]
82 #[cfg_attr(feature = "merge", merge(strategy = conflate::vec::overwrite_empty))]
83 pub tags: Vec<StringList>,
84
85 #[cfg_attr(feature = "clap", clap(long, value_name = "DESCRIPTION"))]
87 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
88 pub description: Option<String>,
89
90 #[cfg_attr(
92 feature = "clap",
93 clap(long, value_name = "FILE", conflicts_with = "description", value_hint = ValueHint::FilePath)
94 )]
95 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
96 pub description_from: Option<PathBuf>,
97
98 #[cfg_attr(feature = "clap", clap(long,value_parser = RusticTime::parse_system))]
100 #[serde_as(as = "Option<RusticTime>")]
101 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
102 pub time: Option<Zoned>,
103
104 #[cfg_attr(feature = "clap", clap(long, conflicts_with = "delete_after"))]
106 #[cfg_attr(feature = "merge", merge(strategy = conflate::bool::overwrite_false))]
107 pub delete_never: bool,
108
109 #[cfg_attr(feature = "clap", clap(long, value_name = "DURATION"))]
111 #[serde_as(as = "Option<DisplayFromStr>")]
112 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
113 pub delete_after: Option<Span>,
114
115 #[cfg_attr(feature = "clap", clap(long, value_name = "NAME"))]
117 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
118 pub host: Option<String>,
119
120 #[cfg_attr(feature = "clap", clap(long))]
122 #[cfg_attr(feature = "merge", merge(strategy = conflate::option::overwrite_none))]
123 pub command: Option<String>,
124}
125
126impl SnapshotOptions {
127 pub fn add_tags(mut self, tag: &str) -> RusticResult<Self> {
141 self.tags.push(StringList::from_str(tag).map_err(|err| {
142 RusticError::with_source(
143 ErrorKind::InvalidInput,
144 "Failed to create string list from tag `{tag}`. The value must be a valid unicode string.",
145 err,
146 )
147 .attach_context("tag", tag)
148 })?);
149 Ok(self)
150 }
151
152 pub fn to_snapshot(&self) -> RusticResult<SnapshotFile> {
162 SnapshotFile::from_options(self)
163 }
164}
165
166#[serde_as]
171#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
172#[serde(default)]
173#[non_exhaustive]
174pub struct SnapshotSummary {
175 pub files_new: u64,
177
178 pub files_changed: u64,
180
181 pub files_unmodified: u64,
183
184 pub total_files_processed: u64,
186
187 pub total_bytes_processed: u64,
189
190 pub dirs_new: u64,
192
193 pub dirs_changed: u64,
195
196 pub dirs_unmodified: u64,
198
199 pub total_dirs_processed: u64,
201
202 pub total_dirsize_processed: u64,
204
205 pub data_blobs: u64,
207
208 pub tree_blobs: u64,
210
211 pub data_added: u64,
213
214 pub data_added_packed: u64,
216
217 pub data_added_files: u64,
219
220 pub data_added_files_packed: u64,
222
223 pub data_added_trees: u64,
225
226 pub data_added_trees_packed: u64,
228
229 pub command: String,
231
232 #[serde_as(as = "RusticTime")]
238 pub backup_start: Zoned,
239
240 #[serde_as(as = "RusticTime")]
242 pub backup_end: Zoned,
243
244 pub backup_duration: f64,
246
247 pub total_duration: f64,
249}
250
251impl Default for SnapshotSummary {
252 fn default() -> Self {
253 Self {
254 files_new: Default::default(),
255 files_changed: Default::default(),
256 files_unmodified: Default::default(),
257 total_files_processed: Default::default(),
258 total_bytes_processed: Default::default(),
259 dirs_new: Default::default(),
260 dirs_changed: Default::default(),
261 dirs_unmodified: Default::default(),
262 total_dirs_processed: Default::default(),
263 total_dirsize_processed: Default::default(),
264 data_blobs: Default::default(),
265 tree_blobs: Default::default(),
266 data_added: Default::default(),
267 data_added_packed: Default::default(),
268 data_added_files: Default::default(),
269 data_added_files_packed: Default::default(),
270 data_added_trees: Default::default(),
271 data_added_trees_packed: Default::default(),
272 command: String::default(),
273 backup_start: Zoned::now(),
274 backup_end: Zoned::now(),
275 backup_duration: Default::default(),
276 total_duration: Default::default(),
277 }
278 }
279}
280
281impl SnapshotSummary {
282 pub(crate) fn finalize(&mut self, snap_time: &Zoned) {
292 let end_time = Zoned::now();
293 self.backup_duration = end_time
294 .since(&self.backup_start)
295 .and_then(|span| span.total(Unit::Second))
296 .inspect_err(|err| warn!("ignoring Datetime error: {err}"))
297 .unwrap_or_default();
298 self.total_duration = end_time
299 .since(snap_time)
300 .and_then(|span| span.total(Unit::Second))
301 .inspect_err(|err| warn!("ignoring Datetime error: {err}"))
302 .unwrap_or_default();
303 self.backup_end = end_time;
304 }
305}
306
307#[derive(Serialize, Default, Deserialize, Debug, Clone, PartialEq, Eq)]
309pub enum DeleteOption {
310 #[default]
312 NotSet,
313 Never,
315 After(Zoned),
317}
318
319impl DeleteOption {
320 const fn is_not_set(&self) -> bool {
322 matches!(self, Self::NotSet)
323 }
324}
325
326impl_repofile!(SnapshotId, FileType::Snapshot, SnapshotFile);
327
328#[serde_as]
329#[skip_serializing_none]
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct SnapshotFile {
340 #[serde_as(as = "RusticTime")]
342 pub time: Zoned,
343
344 #[serde(default, skip_serializing_if = "String::is_empty")]
346 pub program_version: String,
347
348 pub parent: Option<SnapshotId>,
350
351 #[serde(default, skip_serializing_if = "Vec::is_empty")]
353 pub parents: Vec<SnapshotId>,
354
355 pub tree: TreeId,
357
358 #[serde(default, skip_serializing_if = "String::is_empty")]
360 pub label: String,
361
362 pub paths: StringList,
364
365 #[serde(default)]
367 pub hostname: String,
368
369 #[serde(default)]
371 pub username: String,
372
373 #[serde(default)]
375 pub uid: u32,
376
377 #[serde(default)]
379 pub gid: u32,
380
381 #[serde(default)]
383 pub tags: StringList,
384
385 pub original: Option<SnapshotId>,
387
388 #[serde(default, skip_serializing_if = "DeleteOption::is_not_set")]
390 pub delete: DeleteOption,
391
392 pub summary: Option<SnapshotSummary>,
394
395 pub description: Option<String>,
397
398 #[serde(default, skip_serializing_if = "Id::is_null")]
400 pub id: SnapshotId,
401}
402
403impl Default for SnapshotFile {
404 fn default() -> Self {
405 Self {
406 time: Zoned::now(),
407 program_version: {
408 let project_version =
409 option_env!("PROJECT_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"));
410 format!("rustic {project_version}")
411 },
412 parent: Option::default(),
413 parents: Vec::default(),
414 tree: TreeId::default(),
415 label: String::default(),
416 paths: StringList::default(),
417 hostname: String::default(),
418 username: String::default(),
419 uid: Default::default(),
420 gid: Default::default(),
421 tags: StringList::default(),
422 original: Option::default(),
423 delete: DeleteOption::default(),
424 summary: Option::default(),
425 description: Option::default(),
426 id: SnapshotId::default(),
427 }
428 }
429}
430
431enum SnapshotRequest {
432 Latest(usize),
433 StartsWith(String),
434 Id(SnapshotId),
435}
436
437impl FromStr for SnapshotRequest {
438 type Err = Box<RusticError>;
439 fn from_str(s: &str) -> Result<Self, Self::Err> {
440 let err = || {
441 RusticError::new(
442 ErrorKind::InvalidInput,
443 "Invalid snapshot identifier \"{input}\". Expected either a snapshot id: \"01a2b3c4\" or \"latest\" or \"latest~N\" (N >= 0).",
444 )
445 .attach_context("input", s)
446 };
447
448 let result = match s.strip_prefix("latest") {
449 Some(suffix) => {
450 if suffix.is_empty() {
451 Self::Latest(0)
452 } else {
453 let latest_index = suffix.strip_prefix("~").ok_or_else(err)?;
454 let n = latest_index.parse::<usize>().map_err(|_| err())?;
455 Self::Latest(n)
456 }
457 }
458 None => {
459 if s.len() < HEX_LEN {
460 Self::StartsWith(s.to_string())
461 } else {
462 Self::Id(s.parse()?)
463 }
464 }
465 };
466 Ok(result)
467 }
468}
469
470struct SnapshotRequests {
471 requests: Vec<SnapshotRequest>,
472 max_n_latest: Option<usize>,
473 starts_with: Vec<String>,
474 ids: Vec<SnapshotId>,
475}
476
477impl SnapshotRequests {
478 fn from_strs<S: AsRef<str>>(strings: &[S]) -> RusticResult<Self> {
479 let requests: Vec<SnapshotRequest> = strings
480 .iter()
481 .map(|s| s.as_ref().parse())
482 .collect::<RusticResult<_>>()?;
483
484 let mut max_n_latest: Option<usize> = None;
485 let mut starts_with = Vec::new();
486 let mut ids = Vec::new();
487 for r in &requests {
488 match r {
489 SnapshotRequest::Latest(n) => {
490 max_n_latest = Some(max_n_latest.unwrap_or_default().max(*n));
491 }
492 SnapshotRequest::StartsWith(s) => starts_with.push(s.clone()),
493 SnapshotRequest::Id(id) => ids.push(*id),
494 }
495 }
496 Ok(Self {
497 requests,
498 max_n_latest,
499 starts_with,
500 ids,
501 })
502 }
503
504 fn map_results<T: Clone>(
505 self,
506 latest: &[T],
507 vec_ids_starts_with: Vec<T>,
508 vec_ids: Vec<T>,
509 ) -> Vec<T> {
510 let mut snaps_ids = vec_ids.into_iter();
511 let mut snaps_ids_start_with = vec_ids_starts_with.into_iter();
512 self.requests
513 .into_iter()
514 .map(|r| match r {
515 SnapshotRequest::Latest(n) => latest[n].clone(),
516 SnapshotRequest::StartsWith(..) => snaps_ids_start_with.next().unwrap(),
517 SnapshotRequest::Id(..) => snaps_ids.next().unwrap(),
518 })
519 .collect()
520 }
521}
522
523impl SnapshotFile {
524 pub fn from_options(opts: &SnapshotOptions) -> RusticResult<Self> {
540 let hostname = if let Some(host) = &opts.host {
541 host.clone()
542 } else {
543 let hostname = gethostname();
544 hostname
545 .to_str()
546 .ok_or_else(|| {
547 RusticError::new(
548 ErrorKind::InvalidInput,
549 "Failed to convert hostname `{hostname}` to string. The value must be a valid unicode string.",
550 )
551 .attach_context("hostname", hostname.to_string_lossy().to_string())
552 })?
553 .to_string()
554 };
555
556 let time = opts.time.clone().unwrap_or_else(Zoned::now);
557
558 let delete = match (opts.delete_never, opts.delete_after) {
559 (true, _) => DeleteOption::Never,
560 (_, Some(duration)) => DeleteOption::After(time.saturating_add(duration)),
561 (false, None) => DeleteOption::NotSet,
562 };
563
564 let command: String = opts.command.as_ref().map_or_else(
565 || {
566 std::env::args_os()
567 .map(|s| s.to_string_lossy().to_string())
568 .collect::<Vec<_>>()
569 .join(" ")
570 },
571 Clone::clone,
572 );
573
574 let mut snap = Self {
575 time,
576 hostname,
577 label: opts.label.clone().unwrap_or_default(),
578 delete,
579 summary: Some(SnapshotSummary {
580 command,
581 ..Default::default()
582 }),
583 description: opts.description.clone(),
584 ..Default::default()
585 };
586
587 if let Some(ref path) = opts.description_from {
589 snap.description = Some(std::fs::read_to_string(path).map_err(|err| {
590 RusticError::with_source(
591 ErrorKind::InvalidInput,
592 "Failed to read description file `{path}`. Please make sure the file exists and is readable.",
593 err,
594 )
595 .attach_context("path", path.to_string_lossy().to_string())
596 })?);
597 }
598
599 _ = snap.set_tags(opts.tags.clone());
600
601 Ok(snap)
602 }
603
604 fn set_id(tuple: (SnapshotId, Self)) -> Self {
610 let (id, mut snap) = tuple;
611 snap.id = id;
612 _ = snap.original.get_or_insert(id);
613 snap
614 }
615
616 fn from_backend<B: DecryptReadBackend>(be: &B, id: &SnapshotId) -> RusticResult<Self> {
623 Ok(Self::set_id((*id, be.get_file(id)?)))
624 }
625
626 pub(crate) fn from_str<B: DecryptReadBackend>(
644 be: &B,
645 string: &str,
646 predicate: impl FnMut(&Self) -> bool + Send + Sync,
647 p: &Progress,
648 ) -> RusticResult<Self> {
649 match string.parse()? {
650 SnapshotRequest::Latest(n) => Self::latest_n(be, predicate, p, n),
651 SnapshotRequest::StartsWith(id) => Self::from_id(be, &id),
652 SnapshotRequest::Id(id) => Self::from_backend(be, &id),
653 }
654 }
655
656 pub(crate) fn from_strs<B: DecryptReadBackend, S: AsRef<str>>(
674 be: &B,
675 strings: &[S],
676 predicate: impl FnMut(&Self) -> bool + Send + Sync,
677 p: &Progress,
678 ) -> RusticResult<Vec<Self>> {
679 let requests = SnapshotRequests::from_strs(strings)?;
680
681 match requests.max_n_latest {
682 None => {
683 let ids_starts_with = if requests.starts_with.is_empty() {
685 Vec::new()
686 } else {
687 be.list(FileType::Snapshot)?
688 .into_iter()
689 .find_unique_multiple(
690 |id, v| id.to_hex().starts_with(v),
691 &requests.starts_with,
692 )
693 .assert_found(&requests.starts_with)?
694 };
695
696 let ids: Vec<Id> = requests.ids.iter().map(|sn| **sn).collect();
697 let all_ids = requests.map_results(&[], ids_starts_with, ids);
698
699 Self::fill_missing(be, Vec::new(), all_ids.as_slice(), |_| true, p)
700 }
701 Some(max_n) => {
702 let ids: BTreeMap<_, _> = requests
703 .ids
704 .iter()
705 .enumerate()
706 .map(|(num, r)| (r, num))
707 .collect();
708 let mut vec_ids = vec![Self::default(); ids.len()];
709 let mut ids_starts_with = FindUniqueResults::new(&requests.starts_with);
710
711 let iter = Self::iter_all_from_backend(be, predicate, p)?.inspect(|sn| {
713 if let Some(idx) = ids.get(&sn.id) {
714 vec_ids[*idx] = sn.clone();
715 }
716 ids_starts_with.add_item(
717 sn.clone(),
718 |sn, v| sn.id.to_hex().starts_with(v),
719 &requests.starts_with,
720 );
721 });
722 let latest = Self::latest_n_from_iter(max_n, iter)?;
723 let vec_ids_starts_with = ids_starts_with.assert_found(&requests.starts_with)?;
724 Ok(requests.map_results(&latest, vec_ids_starts_with, vec_ids))
725 }
726 }
727 }
728
729 pub(crate) fn latest<B: DecryptReadBackend>(
741 be: &B,
742 predicate: impl FnMut(&Self) -> bool + Send + Sync,
743 p: &Progress,
744 ) -> RusticResult<Self> {
745 Self::latest_n(be, predicate, p, 0)
746 }
747
748 fn latest_n_from_iter(
749 n: usize,
750 iter: impl IntoIterator<Item = Self>,
751 ) -> RusticResult<Vec<Self>> {
752 let latest: Vec<_> = iter
753 .into_iter()
754 .k_smallest_by(n + 1, |s1, s2| s2.time.cmp(&s1.time))
756 .collect();
757
758 if latest.len() > n {
759 Ok(latest)
760 } else if n == 0 {
761 Err(RusticError::new(
762 ErrorKind::Repository,
763 "No snapshots found. Please make sure there are snapshots in the repository.",
764 ))
765 } else {
766 Err(RusticError::new(
767 ErrorKind::Repository,
768 "No snapshots found for latest~{n}. Please make sure there are more than {n} snapshots in the repository.",
769 ).attach_context("n", n.to_string()))
770 }
771 }
772
773 pub(crate) fn latest_n<B: DecryptReadBackend>(
786 be: &B,
787 predicate: impl FnMut(&Self) -> bool + Send + Sync,
788 p: &Progress,
789 n: usize,
790 ) -> RusticResult<Self> {
791 if n == 0 {
792 p.set_title("getting latest snapshot...");
793 } else {
794 p.set_title("getting latest~N snapshot...");
795 }
796 let mut snapshots =
797 Self::latest_n_from_iter(n, Self::iter_all_from_backend(be, predicate, p)?)?;
798
799 p.finish();
800 Ok(snapshots.pop().unwrap()) }
802
803 pub(crate) fn from_id<B: DecryptReadBackend>(be: &B, id: &str) -> RusticResult<Self> {
816 info!("getting snapshot ...");
817 let id = be.find_id(FileType::Snapshot, id)?;
818 Self::from_backend(be, &SnapshotId::from(id))
819 }
820
821 pub(crate) fn from_ids<B: DecryptReadBackend, T: AsRef<str>>(
835 be: &B,
836 ids: &[T],
837 p: &Progress,
838 ) -> RusticResult<Vec<Self>> {
839 Self::update_from_ids(be, Vec::new(), ids, p)
840 }
841
842 pub(crate) fn update_from_ids<B: DecryptReadBackend, T: AsRef<str>>(
856 be: &B,
857 current: Vec<Self>,
858 ids: &[T],
859 p: &Progress,
860 ) -> RusticResult<Vec<Self>> {
861 let ids = be.find_ids(FileType::Snapshot, ids)?;
862 Self::fill_missing(be, current, &ids, |_| true, p)
863 }
864
865 fn fill_missing<B, F>(
867 be: &B,
868 current: Vec<Self>,
869 ids: &[Id],
870 mut filter: F,
871 p: &Progress,
872 ) -> RusticResult<Vec<Self>>
873 where
874 B: DecryptReadBackend,
875 F: FnMut(&Self) -> bool,
876 {
877 let mut snaps: BTreeMap<_, _> = current.into_iter().map(|snap| (snap.id, snap)).collect();
878 let missing_ids: Vec<_> = ids
879 .iter()
880 .map(|id| SnapshotId::from(*id))
881 .filter(|id| !snaps.contains_key(id))
882 .collect();
883 for res in be.stream_list::<Self>(&missing_ids, p)? {
884 let (id, snap) = res?;
885 if filter(&snap) {
886 let _ = snaps.insert(id, snap);
887 }
888 }
889 Ok(ids
891 .iter()
892 .filter_map(|id| {
893 let id = SnapshotId::from(*id);
894 snaps.get(&id).map(|sn| Self::set_id((id, sn.clone())))
895 })
896 .collect())
897 }
898
899 pub(crate) fn iter_all_from_backend<B, F>(
901 be: &B,
902 filter: F,
903 p: &Progress,
904 ) -> RusticResult<impl Iterator<Item = Self>>
905 where
906 B: DecryptReadBackend,
907 F: FnMut(&Self) -> bool,
908 {
909 Ok(be
910 .stream_all::<Self>(p)?
911 .into_iter()
912 .map(|item| item.inspect_err(|err| warn!("Error reading snapshot: {err}")))
913 .filter_map(Result::ok)
914 .map(Self::set_id)
915 .filter(filter))
916 }
917
918 pub(crate) fn update_from_backend<B, F>(
920 be: &B,
921 current: Vec<Self>,
922 filter: F,
923 p: &Progress,
924 ) -> RusticResult<Vec<Self>>
925 where
926 B: DecryptReadBackend,
927 F: FnMut(&Self) -> bool,
928 {
929 let ids = be.list(FileType::Snapshot)?;
930 Self::fill_missing(be, current, &ids, filter, p)
931 }
932
933 pub fn add_tags(&mut self, tag_lists: Vec<StringList>) -> bool {
943 let old_tags = self.tags.clone();
944 self.tags.add_all(tag_lists);
945
946 old_tags != self.tags
947 }
948
949 pub fn set_tags(&mut self, tag_lists: Vec<StringList>) -> bool {
959 let old_tags = std::mem::take(&mut self.tags);
960 self.tags.add_all(tag_lists);
961
962 old_tags != self.tags
963 }
964
965 pub fn remove_tags(&mut self, tag_lists: &[StringList]) -> bool {
975 let old_tags = self.tags.clone();
976 self.tags.remove_all(tag_lists);
977
978 old_tags != self.tags
979 }
980
981 #[must_use]
987 pub fn must_delete(&self, now: &Zoned) -> bool {
988 matches!(&self.delete, DeleteOption::After(time) if time < now)
989 }
990
991 #[must_use]
997 pub fn must_keep(&self, now: &Zoned) -> bool {
998 match &self.delete {
999 DeleteOption::Never => true,
1000 DeleteOption::After(time) if time >= now => true,
1001 _ => false,
1002 }
1003 }
1004
1005 pub fn modify(&mut self, modification: &SnapshotModification) -> RusticResult<bool> {
1018 modification.apply_to(self)
1019 }
1020
1021 #[must_use]
1027 pub(crate) fn clear_ids(mut sn: Self) -> Self {
1028 sn.id = SnapshotId::default();
1029 sn.parent = None;
1030 sn.parents = Vec::new();
1031 sn
1032 }
1033
1034 #[must_use]
1036 pub fn get_parents(&self) -> &[SnapshotId] {
1037 if self.parents.is_empty() {
1038 self.parent.as_slice()
1039 } else {
1040 &self.parents
1041 }
1042 }
1043}
1044
1045impl PartialEq<Self> for SnapshotFile {
1046 fn eq(&self, other: &Self) -> bool {
1047 self.time.eq(&other.time)
1048 }
1049}
1050
1051impl Eq for SnapshotFile {}
1052
1053impl PartialOrd for SnapshotFile {
1054 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1055 Some(self.cmp(other))
1056 }
1057}
1058impl Ord for SnapshotFile {
1059 fn cmp(&self, other: &Self) -> Ordering {
1060 self.time.cmp(&other.time)
1061 }
1062}
1063
1064#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
1066pub struct StringList(pub(crate) BTreeSet<String>);
1067
1068impl FromStr for StringList {
1069 type Err = SnapshotFileErrorKind;
1070 fn from_str(s: &str) -> SnapshotFileResult<Self> {
1071 Ok(Self(s.split(',').map(ToString::to_string).collect()))
1072 }
1073}
1074
1075impl Display for StringList {
1076 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1077 write!(f, "{}", self.0.iter().join(","))?;
1078 Ok(())
1079 }
1080}
1081
1082impl StringList {
1083 #[must_use]
1089 pub fn contains(&self, s: &str) -> bool {
1090 self.0.contains(s)
1091 }
1092
1093 #[must_use]
1099 pub fn contains_all(&self, sl: &Self) -> bool {
1100 sl.0.is_subset(&self.0)
1101 }
1102
1103 #[must_use]
1110 pub fn matches(&self, sls: &[Self]) -> bool {
1111 sls.is_empty() || sls.iter().any(|sl| self.contains_all(sl))
1112 }
1113
1114 pub fn add(&mut self, s: String) {
1120 _ = self.0.insert(s);
1121 }
1122
1123 pub fn add_list(&mut self, mut sl: Self) {
1129 self.0.append(&mut sl.0);
1130 }
1131
1132 pub fn add_all(&mut self, string_lists: Vec<Self>) {
1138 for sl in string_lists {
1139 self.add_list(sl);
1140 }
1141 }
1142
1143 pub(crate) fn set_paths<T: AsRef<Path>>(&mut self, paths: &[T]) -> SnapshotFileResult<()> {
1153 self.0 = paths
1154 .iter()
1155 .map(|p| {
1156 Ok(p.as_ref()
1157 .to_str()
1158 .ok_or_else(|| SnapshotFileErrorKind::NonUnicodePath(p.as_ref().to_path_buf()))?
1159 .to_string())
1160 })
1161 .collect::<SnapshotFileResult<BTreeSet<_>>>()?;
1162 Ok(())
1163 }
1164
1165 pub fn remove_all(&mut self, string_lists: &[Self]) {
1171 for sl in string_lists {
1172 self.0 = &self.0 - &sl.0;
1173 }
1174 }
1175
1176 #[allow(clippy::needless_pass_by_ref_mut)]
1177 #[deprecated(note = "StringLists are now automatically sorted")]
1178 pub fn sort(&mut self) {}
1180
1181 #[must_use]
1183 pub fn formatln(&self) -> String {
1184 self.0.iter().join("\n")
1185 }
1186
1187 pub fn iter(&self) -> impl Iterator<Item = &String> {
1189 self.0.iter()
1190 }
1191}
1192
1193impl<'str> IntoIterator for &'str StringList {
1194 type Item = &'str String;
1195 type IntoIter = std::collections::btree_set::Iter<'str, String>;
1196
1197 fn into_iter(self) -> Self::IntoIter {
1198 self.0.iter()
1199 }
1200}
1201
1202#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
1204pub struct PathList(Vec<PathBuf>);
1205
1206impl Display for PathList {
1207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1208 self.0
1209 .iter()
1210 .map(|p| p.to_string_lossy())
1211 .format(",")
1212 .fmt(f)
1213 }
1214}
1215
1216impl<T: Into<PathBuf>> FromIterator<T> for PathList {
1217 fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
1218 Self(iter.into_iter().map(T::into).collect())
1219 }
1220}
1221
1222impl PathList {
1223 pub fn from_string(source: &str) -> RusticResult<Self> {
1235 Ok(Self(vec![source.into()]))
1236 }
1237
1238 #[must_use]
1240 pub fn len(&self) -> usize {
1241 self.0.len()
1242 }
1243
1244 #[must_use]
1246 pub fn is_empty(&self) -> bool {
1247 self.0.len() == 0
1248 }
1249
1250 #[must_use]
1252 pub(crate) fn paths(&self) -> Vec<PathBuf> {
1253 self.0.clone()
1254 }
1255
1256 pub fn sanitize(mut self) -> SnapshotFileResult<Self> {
1263 for path in &mut self.0 {
1264 *path = sanitize_dot(path)?;
1265 }
1266 if self.0.iter().any(|p| p.is_absolute()) {
1267 self.0 = self
1268 .0
1269 .into_iter()
1270 .map(|p| canonicalize(p).map_err(SnapshotFileErrorKind::CanonicalizingPathFailed))
1271 .collect::<Result<_, _>>()?;
1272 }
1273 Ok(self.merge())
1274 }
1275
1276 #[must_use]
1278 pub fn merge(self) -> Self {
1279 let mut paths = self.0;
1280 paths.sort_unstable();
1282
1283 let mut root_path = None;
1284
1285 paths.retain(|path| match &root_path {
1287 Some(root_path) if path.starts_with(root_path) => false,
1288 _ => {
1289 root_path = Some(path.clone());
1290 true
1291 }
1292 });
1293
1294 Self(paths)
1295 }
1296}
1297
1298fn sanitize_dot(path: &Path) -> SnapshotFileResult<PathBuf> {
1300 if path == Path::new(".") || path == Path::new("./") {
1301 return Ok(PathBuf::from("."));
1302 }
1303
1304 let path = if path.starts_with("./") {
1305 path.strip_prefix("./").unwrap()
1306 } else {
1307 path
1308 };
1309
1310 let path = path
1311 .parse_dot()
1312 .map_err(SnapshotFileErrorKind::RemovingDotsFromPathFailed)?
1313 .to_path_buf();
1314
1315 Ok(path)
1316}
1317
1318#[cfg(test)]
1319mod tests {
1320 use std::{collections::HashMap, sync::Arc};
1321
1322 use super::*;
1323 use crate::{
1324 backend::{
1325 MockBackend,
1326 decrypt::{DecryptBackend, DecryptWriteBackend},
1327 },
1328 crypto::{CryptoKey, aespoly1305::Key},
1329 progress::NoProgress,
1330 };
1331 use anyhow::Result;
1332 use bytes::Bytes;
1333 use jiff::{Timestamp, tz::TimeZone};
1334 use rstest::rstest;
1335
1336 #[rstest]
1337 #[case(".", ".")]
1338 #[case("./", ".")]
1339 #[case("test", "test")]
1340 #[case("test/", "test")]
1341 #[case("./test", "test")]
1342 #[case("./test/", "test")]
1343 fn sanitize_dot_cases(#[case] input: &str, #[case] expected: &str) {
1344 let path = Path::new(input);
1345 let expected = PathBuf::from(expected);
1346
1347 assert_eq!(expected, sanitize_dot(path).unwrap());
1348 }
1349
1350 #[rstest]
1351 #[case("abc", vec!["abc".to_string()])]
1352 #[case("abc,def", vec!["abc".to_string(), "def".to_string()])]
1353 #[case("abc,abc", vec!["abc".to_string()])]
1354 fn test_set_tags(#[case] tag: &str, #[case] expected: Vec<String>) -> Result<()> {
1355 let mut snap = SnapshotFile::from_options(&SnapshotOptions::default())?;
1356 let tags = StringList::from_str(tag)?;
1357 let expected = StringList(expected.into_iter().collect());
1358 assert!(snap.set_tags(vec![tags]));
1359 assert_eq!(snap.tags, expected);
1360 Ok(())
1361 }
1362
1363 #[test]
1364 fn test_add_tags() -> Result<()> {
1365 let tags = vec![StringList::from_str("abc")?];
1366 let mut snap = SnapshotFile::from_options(&SnapshotOptions::default().tags(tags))?;
1367 let tags = StringList::from_str("def,abc")?;
1368 assert!(snap.add_tags(vec![tags]));
1369 let expected = StringList::from_str("abc,def")?;
1370 assert_eq!(snap.tags, expected);
1371 Ok(())
1372 }
1373
1374 #[rstest]
1375 #[case(vec![], "")]
1376 #[case(vec!["test"], "test")]
1377 #[case(vec!["test", "test", "test"], "test,test,test")]
1378 fn test_display_path_list_passes(#[case] input: Vec<&str>, #[case] expected: &str) {
1379 let path_list = PathList::from_iter(input);
1380 let result = path_list.to_string();
1381 assert_eq!(expected, &result);
1382 }
1383
1384 fn fake_snapshot_file_with_id_time(
1385 id_time_vec: Vec<(Id, Zoned)>,
1386 key: &Key,
1387 ) -> HashMap<Id, Bytes> {
1388 let mut res = HashMap::new();
1389 for (id, time) in id_time_vec {
1390 let snapshot_file = SnapshotFile {
1391 id: SnapshotId(id),
1392 time,
1393 ..Default::default()
1394 };
1395 let encrypted = Bytes::from(
1396 key.encrypt_data(serde_json::to_string(&snapshot_file).unwrap().as_bytes())
1397 .unwrap(),
1398 );
1399 let _ = res.insert(id, encrypted);
1400 }
1401 res
1402 }
1403
1404 fn setup_mock_backend() -> (DecryptBackend<Key>, [Id; 3]) {
1405 let key = Key::new();
1406
1407 let id1 = Id::from_str("0011223344556677001122334455667700112233445566770000000000000001")
1408 .unwrap();
1409 let id2 = Id::from_str("0021223344556677001122334455667700112233445566770000000000000002")
1410 .unwrap();
1411 let id3 = Id::from_str("0031223344556677001122334455667700112233445566770000000000000003")
1412 .unwrap();
1413
1414 let snapshot_files = fake_snapshot_file_with_id_time(
1415 vec![
1416 (
1417 id1,
1418 Timestamp::from_second(1_752_483_600)
1419 .unwrap()
1420 .to_zoned(TimeZone::UTC),
1421 ),
1422 (
1423 id2,
1424 Timestamp::from_second(1_752_483_700)
1425 .unwrap()
1426 .to_zoned(TimeZone::UTC),
1427 ),
1428 (
1430 id3,
1431 Timestamp::from_second(1_752_483_800)
1432 .unwrap()
1433 .to_zoned(TimeZone::UTC),
1434 ),
1435 ],
1436 &key,
1437 );
1438 let mut back = MockBackend::new();
1439 let _ = back.expect_list_with_size().returning(move |_| {
1440 Ok(vec![(id2, 0), (id3, 0), (id1, 0)])
1442 });
1443 let _ = back
1444 .expect_read_full()
1445 .returning(move |_tpe, id| Ok(snapshot_files.get(id).unwrap().clone()));
1446
1447 let mut be = DecryptBackend::new(Arc::new(back), key);
1448 be.set_zstd(None);
1449
1450 (be, [id1, id2, id3])
1451 }
1452
1453 #[rstest]
1454 fn test_snapshot_file_latest() {
1455 let p = Progress::new(NoProgress);
1456 let (be, [id1, id2, id3]) = setup_mock_backend();
1457 let latest = SnapshotFile::latest(&be, |_sn| true, &p).unwrap();
1458 assert_eq!(latest.id, SnapshotId(id3));
1459
1460 let latest_n0 = SnapshotFile::latest_n(&be, |_sn| true, &p, 0).unwrap();
1461 assert_eq!(latest_n0, latest);
1462
1463 let latest_n1 = SnapshotFile::latest_n(&be, |_sn| true, &p, 1).unwrap();
1464 assert_eq!(latest_n1.id, SnapshotId(id2));
1465
1466 let latest_n2 = SnapshotFile::latest_n(&be, |_sn| true, &p, 2).unwrap();
1467 assert_eq!(latest_n2.id, SnapshotId(id1));
1468
1469 let latest_n3 = SnapshotFile::latest_n(&be, |_sn| true, &p, 3);
1470 let latest_n3_err = latest_n3.unwrap_err().to_string();
1471 let expected = "No snapshots found for latest~3.";
1472 assert!(
1473 latest_n3_err.contains(expected),
1474 "Err is: {latest_n3_err}\n\nShould contain: {expected}",
1475 );
1476 }
1477
1478 #[rstest]
1479 fn test_snapshot_file_from_str() {
1480 let p = Progress::new(NoProgress);
1481 let (be, [id1, id2, id3]) = setup_mock_backend();
1482
1483 let latest = SnapshotFile::from_str(&be, "latest", |_sn| true, &p).unwrap();
1484 assert_eq!(latest.id, SnapshotId(id3));
1485
1486 let latest_n0 = SnapshotFile::from_str(&be, "latest~0", |_sn| true, &p).unwrap();
1487 assert_eq!(latest_n0, latest);
1488
1489 let snap_id3 = SnapshotFile::from_str(
1490 &be,
1491 "0031223344556677001122334455667700112233445566770000000000000003",
1492 |_sn| true,
1493 &p,
1494 )
1495 .unwrap();
1496 assert_eq!(latest, snap_id3);
1497
1498 let snap_id3 = SnapshotFile::from_str(&be, "003", |_sn| true, &p).unwrap();
1499 assert_eq!(latest, snap_id3);
1500
1501 let latest_n1 = SnapshotFile::from_str(&be, "latest~1", |_sn| true, &p).unwrap();
1502 assert_eq!(latest_n1.id, SnapshotId(id2));
1503
1504 let latest_n2 = SnapshotFile::from_str(&be, "latest~2", |_sn| true, &p).unwrap();
1505 assert_eq!(latest_n2.id, SnapshotId(id1));
1506
1507 let latest_n3 = SnapshotFile::from_str(&be, "latest~3", |_sn| true, &p);
1508 let latest_n3_err = latest_n3.unwrap_err().to_string();
1509 let expected = "No snapshots found for latest~3.";
1510 assert!(
1511 latest_n3_err.contains(expected),
1512 "Err is: {latest_n3_err}\n\nShould contain: {expected}",
1513 );
1514
1515 let latest_syntax_err = SnapshotFile::from_str(&be, "laztet~1", |_sn| true, &p)
1516 .unwrap_err()
1517 .to_string();
1518 let expected = "No suitable id found for `laztet~1`.";
1519 assert!(
1520 latest_syntax_err.contains(expected),
1521 "Err is: {latest_syntax_err}\n\nShould contain: {expected}",
1522 );
1523 }
1524
1525 #[rstest]
1526 fn test_snapshot_file_from_strs() {
1527 let p = Progress::new(NoProgress);
1528 let (be, [id1, id2, id3]) = setup_mock_backend();
1529
1530 let snaps = SnapshotFile::from_strs(
1532 &be,
1533 &[
1534 "latest~2",
1535 "002",
1536 "0031223344556677001122334455667700112233445566770000000000000003",
1537 ],
1538 |_sn| true,
1539 &p,
1540 )
1541 .unwrap();
1542 let ids: Vec<_> = snaps.iter().map(|sn| *sn.id).collect();
1543 assert_eq!(ids, vec![id1, id2, id3]);
1544
1545 let snaps = SnapshotFile::from_strs(
1547 &be,
1548 &[
1549 "0021223344556677001122334455667700112233445566770000000000000002",
1550 "latest~1",
1551 "001",
1552 ],
1553 |_sn| true,
1554 &p,
1555 )
1556 .unwrap();
1557 let ids: Vec<_> = snaps.iter().map(|sn| *sn.id).collect();
1558 assert_eq!(ids, vec![id2, id2, id1]);
1559
1560 let snaps = SnapshotFile::from_strs(&be, &["latest", "latest~1"], |_sn| true, &p).unwrap();
1562 let ids: Vec<_> = snaps.iter().map(|sn| *sn.id).collect();
1563 assert_eq!(ids, vec![id3, id2]);
1564
1565 let latest_n3 = SnapshotFile::from_strs(&be, &["003", "latest~3"], |_sn| true, &p);
1567 let latest_n3_err = latest_n3.unwrap_err().to_string();
1568 let expected = "No snapshots found for latest~3.";
1569 assert!(
1570 latest_n3_err.contains(expected),
1571 "Err is: {latest_n3_err}\n\nShould contain: {expected}",
1572 );
1573
1574 let snaps = SnapshotFile::from_strs(
1576 &be,
1577 &[
1578 "0031223344556677001122334455667700112233445566770000000000000003",
1579 "001",
1580 ],
1581 |_sn| true,
1582 &p,
1583 )
1584 .unwrap();
1585 let ids: Vec<_> = snaps.iter().map(|sn| *sn.id).collect();
1586 assert_eq!(ids, vec![id3, id1]);
1587
1588 let snaps = SnapshotFile::from_strs(
1590 &be,
1591 &[
1592 "0031223344556677001122334455667700112233445566770000000000000003",
1593 "0011223344556677001122334455667700112233445566770000000000000001",
1594 "0031223344556677001122334455667700112233445566770000000000000003",
1595 ],
1596 |_sn| true,
1597 &p,
1598 )
1599 .unwrap();
1600 let ids: Vec<_> = snaps.iter().map(|sn| *sn.id).collect();
1601 assert_eq!(ids, vec![id3, id1, id3]);
1602 }
1603}