163 lines
4.2 KiB
Rust
163 lines
4.2 KiB
Rust
use std::{
|
|
borrow::Borrow,
|
|
collections::{HashMap, HashSet},
|
|
ops::{Deref, DerefMut},
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::Delta;
|
|
|
|
/// Struct that represents a current state for a backup. This struct acts as a smart pointer around
|
|
/// a HashMap.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct State(HashMap<PathBuf, HashSet<PathBuf>>);
|
|
|
|
impl State {
|
|
/// Apply the delta to the current state.
|
|
pub fn apply(&mut self, delta: &Delta) {
|
|
// First we add new files, then we remove the old ones
|
|
for (dir, added) in delta.added.iter() {
|
|
if let Some(current) = self.0.get_mut(dir) {
|
|
current.extend(added.iter().cloned());
|
|
} else {
|
|
self.0.insert(dir.clone(), added.clone());
|
|
}
|
|
}
|
|
|
|
for (dir, removed) in delta.removed.iter() {
|
|
if let Some(current) = self.0.get_mut(dir) {
|
|
current.retain(|k| !removed.contains(k));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns whether the provided relative path is part of the given state.
|
|
pub fn contains<P: AsRef<Path>>(&self, path: P) -> bool {
|
|
let path = path.as_ref();
|
|
|
|
self.0.iter().any(|(dir, files)| {
|
|
path.starts_with(dir) && files.contains(path.strip_prefix(dir).unwrap())
|
|
})
|
|
}
|
|
|
|
/// Returns whether the state is empty.
|
|
///
|
|
/// Note that this does not necessarily mean that the state does not contain any sets, but
|
|
/// rather that any sets that it does contain are also empty.
|
|
pub fn is_empty(&self) -> bool {
|
|
self.0.values().all(|s| s.is_empty())
|
|
}
|
|
|
|
pub fn append_dir<I>(&mut self, dir: impl Into<PathBuf>, files: I)
|
|
where
|
|
I: IntoIterator,
|
|
I::Item: Into<PathBuf>,
|
|
{
|
|
let dir = dir.into();
|
|
let files = files.into_iter().map(Into::into);
|
|
|
|
if let Some(dir_files) = self.0.get_mut(&dir) {
|
|
dir_files.extend(files);
|
|
} else {
|
|
self.0.insert(dir, files.collect());
|
|
}
|
|
}
|
|
|
|
pub fn with_dir<I>(mut self, dir: impl Into<PathBuf>, files: I) -> Self
|
|
where
|
|
I: IntoIterator,
|
|
I::Item: Into<PathBuf>,
|
|
{
|
|
self.append_dir(dir, files);
|
|
self
|
|
}
|
|
}
|
|
|
|
impl PartialEq for State {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
let self_non_empty = self.0.values().filter(|files| !files.is_empty()).count();
|
|
let other_non_empty = other.0.values().filter(|files| !files.is_empty()).count();
|
|
|
|
if self_non_empty != other_non_empty {
|
|
return false;
|
|
}
|
|
|
|
// If both states have the same number of non-empty directories, then comparing each
|
|
// directory of one with the other will only be true if their list of non-empty directories
|
|
// is identical.
|
|
self.0
|
|
.iter()
|
|
.all(|(dir, files)| files.is_empty() || other.0.get(dir).map_or(false, |v| v == files))
|
|
}
|
|
}
|
|
|
|
impl Eq for State {}
|
|
|
|
impl<T> From<T> for State
|
|
where
|
|
T: IntoIterator,
|
|
T::Item: Borrow<Delta>,
|
|
{
|
|
fn from(deltas: T) -> Self {
|
|
let mut state = State::default();
|
|
|
|
for delta in deltas {
|
|
state.apply(delta.borrow());
|
|
}
|
|
|
|
state
|
|
}
|
|
}
|
|
|
|
impl AsRef<HashMap<PathBuf, HashSet<PathBuf>>> for State {
|
|
fn as_ref(&self) -> &HashMap<PathBuf, HashSet<PathBuf>> {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl Deref for State {
|
|
type Target = HashMap<PathBuf, HashSet<PathBuf>>;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
impl DerefMut for State {
|
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
&mut self.0
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_eq() {
|
|
let a = State::default().with_dir("dir1", ["file1", "file2"]);
|
|
let b = State::default().with_dir("dir1", ["file1", "file2"]);
|
|
|
|
assert_eq!(a, b);
|
|
|
|
let b = b.with_dir("dir2", ["file3"]);
|
|
|
|
assert_ne!(a, b);
|
|
}
|
|
|
|
#[test]
|
|
fn test_eq_empty_dirs() {
|
|
let a = State::default().with_dir("dir1", ["file1", "file2"]);
|
|
let b = State::default()
|
|
.with_dir("dir1", ["file1", "file2"])
|
|
.with_dir("dir2", Vec::<PathBuf>::new());
|
|
|
|
assert_eq!(a, b);
|
|
|
|
let b = b.with_dir("dir2", ["file3"]);
|
|
assert_ne!(a, b);
|
|
}
|
|
}
|