diff --git a/src/backup/delta.rs b/src/backup/delta.rs index f32f263..1bc5477 100644 --- a/src/backup/delta.rs +++ b/src/backup/delta.rs @@ -1,5 +1,6 @@ use super::State; use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; use std::fmt; /// Represents the changes relative to the previous backup @@ -26,7 +27,6 @@ impl 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. - #[allow(dead_code)] pub fn union(&self, delta: &Self) -> Self { let mut out = self.clone(); @@ -110,20 +110,35 @@ impl Delta { out } - pub fn contributions(deltas: &[Self]) -> Vec { - let mut contributions: Vec = Vec::with_capacity(deltas.len()); - // contributions[deltas.len() - 1] = deltas[deltas.len() - 1].added.clone(); + /// 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(); - // for (i, delta) in deltas.iter().enumerate() { - // // The base case for the contributions is every added file - // let mut contribution = delta.clone(); + let mut deltas = deltas.into_iter(); - // for other_delta in &deltas[i + 1..] { - // contribution = contribution.difference(other_delta); - // } + 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(); - // contributions.push(contribution); - // } + 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 2e0d703..4831f3f 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::PathBuf; +use std::path::{Path, PathBuf}; /// Manages a collection of backup layers, allowing them to be utilized as a single object. pub struct MetaManager @@ -129,6 +129,17 @@ 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 0c25aad..6573376 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -4,15 +4,17 @@ mod meta; pub use config::ManagerConfig; pub use meta::MetaManager; -use super::{Backup, State}; +use super::{Backup, Delta, State}; 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; +use std::fs::{File, OpenOptions}; use std::io; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// Manages a single backup layer consisting of one or more chains of backups. pub struct Manager @@ -187,6 +189,47 @@ where Err(other("Unknown backup.")) } + pub fn export_backup>( + &self, + start_time: chrono::DateTime, + output_path: P, + ) -> io::Result<()> { + // 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) + { + 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()?; + enc.finish()?; + + return Ok(()); + } + } + + Err(other("Unknown backup.")) + } + /// Get a reference to the underlying chains pub fn chains(&self) -> &Vec>> { &self.chains diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 0b5e322..f456c0b 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -244,6 +244,31 @@ impl Backup { 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)?; + // println!("{:?}", &state); + + 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) { + println!("{:?}", &entry_path_in_tar); + let header = entry.header().clone(); + ar.append(&header, entry)?; + } + } + + Ok(()) + } } impl fmt::Display for Backup { diff --git a/src/backup/state.rs b/src/backup/state.rs index dcd9c41..4b09acf 100644 --- a/src/backup/state.rs +++ b/src/backup/state.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::collections::{HashMap, HashSet}; use std::ops::{Deref, DerefMut}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// Struct that represents a current state for a backup. This struct acts as a smart pointer around /// a HashMap. @@ -32,6 +32,15 @@ impl State { } } } + + /// 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 diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 225fb42..dccf8d9 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -14,6 +14,8 @@ pub enum BackupCommands { Create(BackupCreateArgs), /// Restore a backup Restore(BackupRestoreArgs), + /// Export a backup into a full archive + Export(BackupExportArgs), } #[derive(Args)] @@ -36,13 +38,13 @@ pub struct BackupListArgs { #[derive(Args)] pub struct BackupRestoreArgs { - /// Path to the backup inside the backup directory + /// Path to the backup inside the backup directory to restore 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 existing directories + /// Whether to overwrite the contents of the output directories #[arg(short, long, default_value_t = false)] force: bool, /// Create output directories if they don't exist @@ -50,12 +52,24 @@ 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), } } } @@ -171,3 +185,48 @@ impl BackupListArgs { Ok(()) } } + +impl BackupExportArgs { + pub fn run(&self, cli: &Cli) -> io::Result<()> { + let backup_dir = cli.backup.canonicalize()?; + + if self.make { + if let Some(parent) = &self.output.parent() { + std::fs::create_dir_all(parent)?; + } + } + + // Parse input 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()?; + + if let Some(res) = meta.export_backup(&layer, timestamp, &self.output) { + res + } else { + Err(other("Unknown layer")) + } + } +}