246 lines
8.8 KiB
Rust
246 lines
8.8 KiB
Rust
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<I>(deltas: I) -> Vec<State>
|
|
where
|
|
I: IntoIterator,
|
|
I::Item: Borrow<Delta>,
|
|
{
|
|
let mut contributions: Vec<State> = 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<I>(&mut self, dir: impl Into<PathBuf>, files: I)
|
|
where
|
|
I: IntoIterator,
|
|
I::Item: Into<PathBuf>,
|
|
{
|
|
self.added.append_dir(dir, files);
|
|
}
|
|
|
|
/// Wrapper around the `append_added` method for a builder-style construction of delta's
|
|
pub fn with_added<I>(mut self, dir: impl Into<PathBuf>, files: I) -> Self
|
|
where
|
|
I: IntoIterator,
|
|
I::Item: Into<PathBuf>,
|
|
{
|
|
self.append_added(dir, files);
|
|
self
|
|
}
|
|
|
|
/// Append the given files to the directory's list of removed files
|
|
pub fn append_removed<I>(&mut self, dir: impl Into<PathBuf>, files: I)
|
|
where
|
|
I: IntoIterator,
|
|
I::Item: Into<PathBuf>,
|
|
{
|
|
self.removed.append_dir(dir, files);
|
|
}
|
|
|
|
/// Wrapper around the `append_removed` method for a builder-style construction of delta's
|
|
pub fn with_removed<I>(mut self, dir: impl Into<PathBuf>, files: I) -> Self
|
|
where
|
|
I: IntoIterator,
|
|
I::Item: Into<PathBuf>,
|
|
{
|
|
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));
|
|
}
|
|
|
|
#[test]
|
|
fn test_union_full_revert() {
|
|
let a = Delta::default().with_added("dir_1", ["file1", "file2"]);
|
|
let b = Delta::default().with_removed("dir_1", ["file1", "file2"]);
|
|
|
|
let expected = Delta::default().with_removed("dir_1", ["file1", "file2"]);
|
|
assert_eq!(expected, a.union(&b));
|
|
|
|
let expected = Delta::default().with_added("dir_1", ["file1", "file2"]);
|
|
assert_eq!(expected, b.union(&a));
|
|
}
|
|
}
|