diff --git a/CHANGELOG.md b/CHANGELOG.md index d4de051..2c9711d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev) -### Added - -* Export command to export any backup as a new full backup - ## [0.3.0](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.3.0) ### Added diff --git a/src/backup/delta.rs b/src/backup/delta.rs index 1bc5477..6e37e96 100644 --- a/src/backup/delta.rs +++ b/src/backup/delta.rs @@ -1,146 +1,78 @@ -use super::State; use serde::{Deserialize, Serialize}; -use std::borrow::Borrow; +use std::collections::{HashMap, HashSet}; use std::fmt; +use std::path::PathBuf; /// Represents the changes relative to the previous backup -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize)] pub struct Delta { /// What files were added/modified in each part of the tarball. - pub added: State, + pub added: HashMap>, /// 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, + pub removed: HashMap>, } impl Delta { pub fn new() -> Self { Self { - added: Default::default(), - removed: Default::default(), + added: HashMap::new(), + removed: HashMap::new(), } } - /// 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(); - + /// Update the current state so that its result becomes the merge of itself and the other + /// state. + #[allow(dead_code)] + pub fn merge(&mut self, delta: &Self) { 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) { + if let Some(orig_removed) = self.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) { + if let Some(orig_added) = self.added.get_mut(dir) { orig_added.extend(added.iter().cloned()); } else { - out.added.insert(dir.clone(), added.clone()); + self.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) { + if let Some(orig_added) = self.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) { + if let Some(orig_removed) = self.removed.get_mut(dir) { orig_removed.extend(removed.iter().cloned()); } else { - out.removed.insert(dir.clone(), removed.clone()); + self.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)); + /// Modify the given state by applying this delta's changes to it + pub fn apply(&self, state: &mut HashMap>) { + // First we add new files, then we remove the old ones + for (dir, added) in self.added.iter() { + if let Some(current) = state.get_mut(dir) { + current.extend(added.iter().cloned()); + } else { + state.insert(dir.clone(), added.clone()); } } - 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 self.removed.iter() { + if let Some(current) = state.get_mut(dir) { + current.retain(|k| !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 } } diff --git a/src/backup/manager/meta.rs b/src/backup/manager/meta.rs index 4831f3f..2e0d703 100644 --- a/src/backup/manager/meta.rs +++ b/src/backup/manager/meta.rs @@ -4,7 +4,7 @@ use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; use std::io; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; /// Manages a collection of backup layers, allowing them to be utilized as a single object. pub struct MetaManager @@ -129,17 +129,6 @@ where .map(|manager| manager.restore_backup(start_time, dirs)) } - pub fn export_backup>( - &self, - layer: &str, - start_time: chrono::DateTime, - output_path: P, - ) -> Option> { - self.managers - .get(layer) - .map(|manager| manager.export_backup(start_time, output_path)) - } - pub fn managers(&self) -> &HashMap> { &self.managers } diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 6a43f9f..72743b7 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -4,17 +4,15 @@ mod meta; pub use config::ManagerConfig; pub use meta::MetaManager; -use super::{Backup, BackupType, Delta, State}; +use super::Backup; use crate::other; use chrono::SubsecRound; use chrono::Utc; -use flate2::write::GzEncoder; -use flate2::Compression; use serde::Deserialize; use serde::Serialize; -use std::fs::{File, OpenOptions}; +use std::fs::File; use std::io; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; /// Manages a single backup layer consisting of one or more chains of backups. pub struct Manager @@ -74,10 +72,10 @@ where let mut backup = if !current_chain.is_empty() { let previous_backup = current_chain.last().unwrap(); - let previous_state = State::from(current_chain.iter().map(|b| &b.delta)); + let state = Backup::state(current_chain); Backup::create_from( - previous_state, + state, previous_backup.start_time, &self.backup_dir, &self.dirs, @@ -153,30 +151,13 @@ where /// Calculate the next time a backup should be created. If no backup has been created yet, it /// will return now. pub fn next_scheduled_time(&self) -> chrono::DateTime { - self.chains - .last() - .and_then(|last_chain| last_chain.last()) - .map(|last_backup| last_backup.start_time + self.frequency) - .unwrap_or_else(chrono::offset::Utc::now) - } - - /// Search for a chain containing a backup with the specified start time. - /// - /// # Returns - /// - /// A tuple (chain, index) with index being the index of the found backup in the returned - /// chain. - fn find(&self, start_time: chrono::DateTime) -> Option<(&Vec>, usize)> { - for chain in &self.chains { - if let Some(index) = chain - .iter() - .position(|b| b.start_time.trunc_subsecs(0) == start_time) - { - return Some((chain, index)); + if let Some(last_chain) = self.chains.last() { + if let Some(last_backup) = last_chain.last() { + return last_backup.start_time + self.frequency; } } - None + chrono::offset::Utc::now() } /// Restore the backup with the given start time by restoring its chain up to and including the @@ -186,57 +167,24 @@ where start_time: chrono::DateTime, dirs: &Vec<(PathBuf, PathBuf)>, ) -> io::Result<()> { - self.find(start_time) - .ok_or_else(|| other("Unknown layer.")) - .and_then(|(chain, index)| { + // Iterate over each chain, skipping elements until the element with the given start time + // is possibly found. + for chain in &self.chains { + // If we find the element in the chain, restore the entire chain up to and including + // the element + if let Some(index) = chain + .iter() + .position(|b| b.start_time.trunc_subsecs(0) == start_time) + { for backup in chain.iter().take(index + 1) { backup.restore(&self.backup_dir, dirs)?; } - Ok(()) - }) - } + return Ok(()); + } + } - /// Export the backup with the given start time as a new full archive. - pub fn export_backup>( - &self, - start_time: chrono::DateTime, - output_path: P, - ) -> io::Result<()> { - self.find(start_time) - .ok_or_else(|| other("Unknown layer.")) - .and_then(|(chain, index)| { - match chain[index].type_ { - // A full backup is simply copied to the output path - BackupType::Full => std::fs::copy( - Backup::path(&self.backup_dir, chain[index].start_time), - output_path, - ) - .map(|_| ()), - // Incremental backups are exported one by one according to their contribution - BackupType::Incremental => { - let contributions = Delta::contributions( - chain.iter().take(index + 1).map(|b| &b.delta).rev(), - ); - - let tar_gz = OpenOptions::new() - .write(true) - .create(true) - .open(output_path.as_ref())?; - let enc = GzEncoder::new(tar_gz, Compression::default()); - let mut ar = tar::Builder::new(enc); - - for (contribution, backup) in - contributions.iter().rev().zip(chain.iter().take(index + 1)) - { - backup.append(&self.backup_dir, contribution, &mut ar)?; - } - - let mut enc = ar.into_inner()?; - enc.try_finish() - } - } - }) + Err(other("Unknown backup.")) } /// Get a reference to the underlying chains diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 8eecc0f..a4081f9 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -2,13 +2,11 @@ mod delta; mod io_ext; pub mod manager; mod path; -mod state; use delta::Delta; pub use manager::Manager; pub use manager::ManagerConfig; pub use manager::MetaManager; -pub use state::State; use chrono::Utc; use flate2::read::GzDecoder; @@ -16,7 +14,7 @@ use flate2::write::GzEncoder; use flate2::Compression; use path::PathExt; use serde::{Deserialize, Serialize}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fmt; use std::fs::File; use std::io; @@ -63,6 +61,18 @@ impl Backup { self.metadata = Some(metadata); } + /// Resolve the state of the list of backups by applying their deltas in-order to an initially + /// empty state. + pub fn state(backups: &Vec) -> HashMap> { + let mut state: HashMap> = HashMap::new(); + + for backup in backups { + backup.delta.apply(&mut state); + } + + state + } + /// Create a new Full backup, populated with the given directories. /// /// # Arguments @@ -131,7 +141,7 @@ impl Backup { /// /// The `Backup` instance describing this new backup. pub fn create_from>( - previous_state: State, + previous_state: HashMap>, previous_start_time: chrono::DateTime, backup_dir: P, dirs: &Vec<(PathBuf, PathBuf)>, @@ -237,36 +247,6 @@ impl Backup { Ok(()) } - - pub fn open>(&self, backup_dir: P) -> io::Result>> { - let path = Backup::path(backup_dir, self.start_time); - let tar_gz = File::open(path)?; - let enc = GzDecoder::new(tar_gz); - Ok(tar::Archive::new(enc)) - } - - /// Open this backup's archive and append all its files that are part of the provided state to - /// the archive file. - pub fn append>( - &self, - backup_dir: P, - state: &State, - ar: &mut tar::Builder>, - ) -> io::Result<()> { - let mut own_ar = self.open(backup_dir)?; - - for entry in own_ar.entries()? { - let entry = entry?; - let entry_path_in_tar = entry.path()?.to_path_buf(); - - if state.contains(&entry_path_in_tar) { - let header = entry.header().clone(); - ar.append(&header, entry)?; - } - } - - Ok(()) - } } impl fmt::Display for Backup { diff --git a/src/backup/path.rs b/src/backup/path.rs index 77c6883..b8b5ae9 100644 --- a/src/backup/path.rs +++ b/src/backup/path.rs @@ -129,15 +129,16 @@ pub trait PathExt { impl PathExt for Path { fn not_modified_since(&self, timestamp: chrono::DateTime) -> bool { - self.metadata() - .and_then(|m| m.modified()) - .map(|last_modified| { + if let Ok(metadata) = self.metadata() { + if let Ok(last_modified) = metadata.modified() { let t: chrono::DateTime = last_modified.into(); let t = t.with_timezone(&Local); - t < timestamp - }) - .unwrap_or(false) + return t < timestamp; + } + } + + false } fn read_dir_recursive(&self) -> io::Result { diff --git a/src/backup/state.rs b/src/backup/state.rs deleted file mode 100644 index 4b09acf..0000000 --- a/src/backup/state.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::backup::Delta; -use serde::{Deserialize, Serialize}; -use std::borrow::Borrow; -use std::collections::{HashMap, HashSet}; -use std::ops::{Deref, DerefMut}; -use std::path::{Path, PathBuf}; - -/// Struct that represents a current state for a backup. This struct acts as a smart pointer around -/// a HashMap. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct State(HashMap>); - -impl State { - pub fn new() -> Self { - State(HashMap::new()) - } - - /// 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>(&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()) - }) - } -} - -impl From for State -where - T: IntoIterator, - T::Item: Borrow, -{ - fn from(deltas: T) -> Self { - let mut state = State::new(); - - for delta in deltas { - state.apply(delta.borrow()); - } - - state - } -} - -impl AsRef>> for State { - fn as_ref(&self) -> &HashMap> { - &self.0 - } -} - -impl Deref for State { - type Target = HashMap>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for State { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Default for State { - fn default() -> Self { - Self::new() - } -} diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 3a993fc..225fb42 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -4,7 +4,7 @@ use crate::other; use chrono::{TimeZone, Utc}; use clap::{Args, Subcommand}; use std::io; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; #[derive(Subcommand)] pub enum BackupCommands { @@ -14,8 +14,6 @@ pub enum BackupCommands { Create(BackupCreateArgs), /// Restore a backup Restore(BackupRestoreArgs), - /// Export a backup into a full archive - Export(BackupExportArgs), } #[derive(Args)] @@ -38,13 +36,13 @@ pub struct BackupListArgs { #[derive(Args)] pub struct BackupRestoreArgs { - /// Path to the backup inside the backup directory to restore + /// Path to the backup inside the backup directory path: PathBuf, /// Directory to store config in output_config: PathBuf, /// Directory to store worlds in output_worlds: PathBuf, - /// Whether to overwrite the contents of the output directories + /// Whether to overwrite the contents of the existing directories #[arg(short, long, default_value_t = false)] force: bool, /// Create output directories if they don't exist @@ -52,24 +50,12 @@ pub struct BackupRestoreArgs { make: bool, } -#[derive(Args)] -pub struct BackupExportArgs { - /// Path to the backup inside the backup directory to export - path: PathBuf, - /// Path to store the exported archive - output: PathBuf, - /// Create output directories if they don't exist - #[arg(short, long, default_value_t = false)] - make: bool, -} - impl BackupArgs { pub fn run(&self, cli: &Cli) -> io::Result<()> { match &self.command { BackupCommands::Create(args) => args.run(cli), BackupCommands::List(args) => args.run(cli), BackupCommands::Restore(args) => args.run(cli), - BackupCommands::Export(args) => args.run(cli), } } } @@ -86,63 +72,6 @@ impl BackupCreateArgs { } } -impl BackupListArgs { - pub fn run(&self, cli: &Cli) -> io::Result<()> { - let meta = cli.meta()?; - - // A bit scuffed? Sure - for (name, manager) in meta - .managers() - .iter() - .filter(|(name, _)| self.layer.is_none() || &self.layer.as_ref().unwrap() == name) - { - println!("{}", name); - - for chain in manager.chains().iter().filter(|c| !c.is_empty()) { - let mut iter = chain.iter(); - println!(" {}", iter.next().unwrap()); - - for backup in iter { - println!(" {}", backup); - } - } - } - - Ok(()) - } -} - -/// Tries to parse the given path as the path to a backup inside the backup directory with a -/// formatted timestamp. -fn parse_backup_path( - backup_dir: &Path, - backup_path: &Path, -) -> io::Result<(String, chrono::DateTime)> { - if !backup_path.starts_with(backup_dir) { - return Err(other("Provided file is not inside the backup directory.")); - } - - let layer = if let Some(parent) = backup_path.parent() { - // Backup files should be stored nested inside a layer's folder - if parent != backup_dir { - parent.file_name().unwrap().to_string_lossy() - } else { - return Err(other("Invalid path.")); - } - } else { - return Err(other("Invalid path.")); - }; - - let timestamp = if let Some(filename) = backup_path.file_name() { - Utc.datetime_from_str(&filename.to_string_lossy(), Backup::FILENAME_FORMAT) - .map_err(|_| other("Invalid filename."))? - } else { - return Err(other("Invalid filename.")); - }; - - Ok((layer.to_string(), timestamp)) -} - impl BackupRestoreArgs { pub fn run(&self, cli: &Cli) -> io::Result<()> { let backup_dir = cli.backup.canonicalize()?; @@ -157,8 +86,29 @@ impl BackupRestoreArgs { let output_worlds = self.output_worlds.canonicalize()?; // Parse input path - let backup_path = self.path.canonicalize()?; - let (layer, timestamp) = parse_backup_path(&backup_dir, &backup_path)?; + let path = self.path.canonicalize()?; + + if !path.starts_with(&backup_dir) { + return Err(other("Provided file is not inside the backup directory.")); + } + + let layer = if let Some(parent) = path.parent() { + // Backup files should be stored nested inside a layer's folder + if parent != backup_dir { + parent.file_name().unwrap().to_string_lossy() + } else { + return Err(other("Invalid path.")); + } + } else { + return Err(other("Invalid path.")); + }; + + let timestamp = if let Some(filename) = path.file_name() { + Utc.datetime_from_str(&filename.to_string_lossy(), Backup::FILENAME_FORMAT) + .map_err(|_| other("Invalid filename."))? + } else { + return Err(other("Invalid filename.")); + }; let meta = cli.meta()?; @@ -196,26 +146,28 @@ impl BackupRestoreArgs { } } -impl BackupExportArgs { +impl BackupListArgs { pub fn run(&self, cli: &Cli) -> io::Result<()> { - let backup_dir = cli.backup.canonicalize()?; + let meta = cli.meta()?; - if self.make { - if let Some(parent) = &self.output.parent() { - std::fs::create_dir_all(parent)?; + // A bit scuffed? Sure + for (name, manager) in meta + .managers() + .iter() + .filter(|(name, _)| self.layer.is_none() || &self.layer.as_ref().unwrap() == name) + { + println!("{}", name); + + for chain in manager.chains().iter().filter(|c| !c.is_empty()) { + let mut iter = chain.iter(); + println!(" {}", iter.next().unwrap()); + + for backup in iter { + println!(" {}", backup); + } } } - // Parse input path - let backup_path = self.path.canonicalize()?; - let (layer, timestamp) = parse_backup_path(&backup_dir, &backup_path)?; - - let meta = cli.meta()?; - - if let Some(res) = meta.export_backup(&layer, timestamp, &self.output) { - res - } else { - Err(other("Unknown layer")) - } + Ok(()) } }