use std::{borrow::Borrow, fmt, path::PathBuf}; use serde::{Deserialize, Serialize}; use super::State; /// Represents the changes relative to the previous backup #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] pub struct Delta { /// What files were added/modified in each part of the tarball. pub added: State, /// What files were removed in this backup, in comparison to the previous backup. For full /// backups, this will always be empty, as they do not consider previous backups. /// The map stores a separate list for each top-level directory, as the contents of these /// directories can come for different source directories. pub removed: State, } impl Delta { /// Returns whether the delta is empty by checking whether both its added and removed state /// return true for their `is_empty`. pub fn is_empty(&self) -> bool { self.added.is_empty() && self.removed.is_empty() } /// Calculate the union of this delta with another delta. /// /// The union of two deltas is a delta that produces the same state as if you were to apply /// both deltas in-order. Note that this operation is not commutative. pub fn union(&self, delta: &Self) -> Self { let mut out = self.clone(); for (dir, added) in delta.added.iter() { // Files that were removed in the current state, but added in the new state, are no // longer removed if let Some(orig_removed) = out.removed.get_mut(dir) { orig_removed.retain(|k| !added.contains(k)); } // Newly added files are added to the state as well if let Some(orig_added) = out.added.get_mut(dir) { orig_added.extend(added.iter().cloned()); } else { out.added.insert(dir.clone(), added.clone()); } } for (dir, removed) in delta.removed.iter() { // Files that were originally added, but now deleted are removed from the added list if let Some(orig_added) = out.added.get_mut(dir) { orig_added.retain(|k| !removed.contains(k)); } // Newly removed files are added to the state as well if let Some(orig_removed) = out.removed.get_mut(dir) { orig_removed.extend(removed.iter().cloned()); } else { out.removed.insert(dir.clone(), removed.clone()); } } out } // Calculate the difference between this delta and the other delta. // // The difference simply means removing all adds and removes that are also performed in the // other delta. pub fn difference(&self, other: &Self) -> Self { let mut out = self.clone(); for (dir, added) in out.added.iter_mut() { // If files are added in the other delta, we don't add them in this delta if let Some(other_added) = other.added.get(dir) { added.retain(|k| !other_added.contains(k)); }; } for (dir, removed) in out.removed.iter_mut() { // If files are removed in the other delta, we don't remove them in this delta either if let Some(other_removed) = other.removed.get(dir) { removed.retain(|k| !other_removed.contains(k)); } } out } // Calculate the strict difference between this delta and the other delta. // // The strict difference is a difference where all operations that would be overwritten by the // other delta are also removed (a.k.a. adding a file after removing it, or vice versa) pub fn strict_difference(&self, other: &Self) -> Self { let mut out = self.difference(other); for (dir, added) in out.added.iter_mut() { // Remove additions that are removed in the other delta if let Some(other_removed) = other.removed.get(dir) { added.retain(|k| !other_removed.contains(k)); } } for (dir, removed) in out.removed.iter_mut() { // Remove removals that are re-added in the other delta if let Some(other_added) = other.added.get(dir) { removed.retain(|k| !other_added.contains(k)); } } out } /// Given a chain of deltas, ordered from last to first, calculate the "contribution" for each /// state. /// /// The contribution of a delta in a given chain is defined as the parts of the state produced /// by this chain that are actually provided by this delta. This comes down to calculating the /// strict difference of this delta and all of its successive deltas. pub fn contributions(deltas: I) -> Vec where I: IntoIterator, I::Item: Borrow, { let mut contributions: Vec = Vec::new(); let mut deltas = deltas.into_iter(); if let Some(first_delta) = deltas.next() { // From last to first, we calculate the strict difference of the delta with the union of all its // following deltas. The list of added files of this difference is the contribution for // that delta. contributions.push(first_delta.borrow().added.clone()); let mut union_future = first_delta.borrow().clone(); for delta in deltas { contributions.push(delta.borrow().strict_difference(&union_future).added); union_future = union_future.union(delta.borrow()); } } // contributions.reverse(); contributions } /// Append the given files to the directory's list of added files pub fn append_added(&mut self, dir: impl Into, files: I) where I: IntoIterator, I::Item: Into, { self.added.append_dir(dir, files); } /// Wrapper around the `append_added` method for a builder-style construction of delta's pub fn with_added(mut self, dir: impl Into, files: I) -> Self where I: IntoIterator, I::Item: Into, { self.append_added(dir, files); self } /// Append the given files to the directory's list of removed files pub fn append_removed(&mut self, dir: impl Into, files: I) where I: IntoIterator, I::Item: Into, { self.removed.append_dir(dir, files); } /// Wrapper around the `append_removed` method for a builder-style construction of delta's pub fn with_removed(mut self, dir: impl Into, files: I) -> Self where I: IntoIterator, I::Item: Into, { self.append_removed(dir, files); self } } impl fmt::Display for Delta { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let added_count: usize = self.added.values().map(|s| s.len()).sum(); let removed_count: usize = self.removed.values().map(|s| s.len()).sum(); write!(f, "+{}-{}", added_count, removed_count) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_union_disjunct_dirs() { let a = Delta::default() .with_added("dir_added_1", ["file1", "file2"]) .with_removed("dir_removed_1", ["file1", "file2"]); let b = Delta::default() .with_added("dir_added_3", ["file1", "file2"]) .with_removed("dir_removed_3", ["file1", "file2"]); let expected = Delta::default() .with_added("dir_added_1", ["file1", "file2"]) .with_added("dir_added_3", ["file1", "file2"]) .with_removed("dir_removed_1", ["file1", "file2"]) .with_removed("dir_removed_3", ["file1", "file2"]); assert_eq!(expected, a.union(&b)); assert_eq!(expected, b.union(&a)); } #[test] fn test_union_disjunct_files() { let a = Delta::default() .with_added("dir_added_1", ["file1", "file2"]) .with_removed("dir_removed_1", ["file1", "file2"]); let b = Delta::default() .with_added("dir_added_1", ["file3", "file4"]) .with_removed("dir_removed_1", ["file3", "file4"]); let expected = Delta::default() .with_added("dir_added_1", ["file1", "file2", "file3", "file4"]) .with_removed("dir_removed_1", ["file1", "file2", "file3", "file4"]); assert_eq!(expected, a.union(&b)); assert_eq!(expected, b.union(&a)); } }