diff --git a/src/backup/manager/meta.rs b/src/backup/manager/meta.rs index 6ba9c06..ff91310 100644 --- a/src/backup/manager/meta.rs +++ b/src/backup/manager/meta.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; /// Manages a collection of backup layers, allowing them to be utilized as a single object. pub struct MetaManager where - T: Clone + Serialize + for<'de> Deserialize<'de>, + T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug, { backup_dir: PathBuf, dirs: Vec<(PathBuf, PathBuf)>, @@ -19,7 +19,7 @@ where impl MetaManager where - T: Clone + Serialize + for<'de> Deserialize<'de>, + T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug, { pub fn new>( backup_dir: P, @@ -116,4 +116,15 @@ where None } } + + /// Restore a backup for a specific layer + pub fn restore_backup( + &self, + layer: &str, + start_time: chrono::DateTime, + ) -> Option> { + self.managers + .get(layer) + .map(|manager| manager.restore_backup(start_time)) + } } diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 32c452a..2b742f7 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -5,6 +5,8 @@ pub use config::ManagerConfig; pub use meta::MetaManager; use super::Backup; +use crate::other; +use chrono::SubsecRound; use chrono::Utc; use serde::Deserialize; use serde::Serialize; @@ -15,7 +17,7 @@ use std::path::PathBuf; /// Manages a single backup layer consisting of one or more chains of backups. pub struct Manager where - T: Clone + Serialize + for<'de> Deserialize<'de>, + T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug, { backup_dir: PathBuf, dirs: Vec<(PathBuf, PathBuf)>, @@ -28,7 +30,7 @@ where impl Manager where - T: Clone + Serialize + for<'de> Deserialize<'de>, + T: Clone + Serialize + for<'de> Deserialize<'de> + std::fmt::Debug, { const METADATA_FILE: &str = "alex.json"; @@ -156,4 +158,27 @@ where chrono::offset::Utc::now() } + + /// Restore the backup with the given start time by restoring its chain up to and including the + /// backup, in order. + pub fn restore_backup(&self, start_time: chrono::DateTime) -> 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) + { + for backup in chain.iter().take(index + 1) { + backup.restore(&self.backup_dir, &self.dirs)?; + } + + return Ok(()); + } + } + + Err(other("Unknown backup.")) + } } diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 0168e67..3bc99df 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -8,6 +8,7 @@ pub use manager::ManagerConfig; pub use manager::MetaManager; use chrono::Utc; +use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; use path::PathExt; @@ -24,7 +25,7 @@ pub enum BackupType { } /// Represents a successful backup -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct Backup { /// When the backup was started (also corresponds to the name) pub start_time: chrono::DateTime, @@ -36,6 +37,8 @@ pub struct Backup { } impl Backup<()> { + pub const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; + /// Return the path to a backup file by properly formatting the data. pub fn path>(backup_dir: P, start_time: chrono::DateTime) -> PathBuf { let backup_dir = backup_dir.as_ref(); @@ -46,8 +49,6 @@ impl Backup<()> { } impl Backup { - const FILENAME_FORMAT: &str = "%Y-%m-%d_%H-%M-%S.tar.gz"; - pub fn set_metadata(&mut self, metadata: T) { self.metadata = Some(metadata); } @@ -159,4 +160,48 @@ impl Backup { metadata: None, }) } + + pub fn restore>( + &self, + backup_dir: P, + dirs: &Vec<(PathBuf, PathBuf)>, + ) -> io::Result<()> { + let path = Backup::path(backup_dir, self.start_time); + let tar_gz = File::open(path)?; + let enc = GzDecoder::new(tar_gz); + let mut ar = tar::Archive::new(enc); + + // Unpack each file by matching it with one of the destination directories and extracting + // it to the right path + for entry in ar.entries()? { + let mut entry = entry?; + let entry_path_in_tar = entry.path()?.to_path_buf(); + + for (path_in_tar, dst_dir) in dirs { + if entry_path_in_tar.starts_with(path_in_tar) { + let dst_path = + dst_dir.join(entry_path_in_tar.strip_prefix(path_in_tar).unwrap()); + + // Ensure all parent directories are present + std::fs::create_dir_all(dst_path.parent().unwrap())?; + + entry.unpack(dst_path)?; + + break; + } + } + } + + // Remove any files + for (path_in_tar, dst_dir) in dirs { + if let Some(removed) = self.delta.removed.get(path_in_tar) { + for path in removed { + let dst_path = dst_dir.join(path); + std::fs::remove_file(dst_path)?; + } + } + } + + Ok(()) + } } diff --git a/src/cli/backup.rs b/src/cli/backup.rs index b71d992..32d45c4 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -1,11 +1,19 @@ +use crate::backup::Backup; use crate::cli::Cli; +use crate::other; +use chrono::{TimeZone, Utc}; use clap::{Args, Subcommand}; use std::io; +use std::path::PathBuf; #[derive(Subcommand)] pub enum BackupCommands { - Create(BackupCreateArgs), + /// List all tracked backups List, + /// Manually create a new backup + Create(BackupCreateArgs), + /// Restore a backup + Restore(BackupRestoreArgs), } #[derive(Args)] @@ -20,11 +28,21 @@ pub struct BackupCreateArgs { layer: String, } +#[derive(Args)] +pub struct BackupRestoreArgs { + /// Path to the backup inside the backup directory + path: PathBuf, + /// Whether to overwrite the contents of the existing directories + #[arg(short, long, default_value_t = false)] + force: bool, +} + impl BackupArgs { pub fn run(&self, cli: &Cli) -> io::Result<()> { match &self.command { BackupCommands::Create(args) => args.run(cli), BackupCommands::List => Ok(()), + BackupCommands::Restore(args) => args.run(cli), } } } @@ -40,3 +58,65 @@ impl BackupCreateArgs { } } } + +impl BackupRestoreArgs { + pub fn run(&self, cli: &Cli) -> io::Result<()> { + let backup_dir = cli.backup.canonicalize()?; + + // 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()?; + + // Clear previous contents of directories + let mut entries = cli + .config + .canonicalize()? + .read_dir()? + .chain(cli.world.canonicalize()?.read_dir()?) + .peekable(); + + if entries.peek().is_some() && !self.force { + return Err(other("Output directories are not empty. If you wish to overwrite these contents, use the force flag.")); + } + + for entry in entries { + let path = entry?.path(); + + if path.is_dir() { + std::fs::remove_dir_all(path)?; + } else { + std::fs::remove_file(path)?; + } + } + + // Restore the backup + if let Some(res) = meta.restore_backup(&layer, timestamp) { + res + } else { + Err(other("Unknown layer")) + } + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index eb4ba63..0594e8e 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -81,10 +81,10 @@ impl Cli { server_version: self.server_version.clone(), }; let dirs = vec![ - (PathBuf::from("config"), self.config.clone()), - (PathBuf::from("worlds"), self.world.clone()), + (PathBuf::from("config"), self.config.canonicalize()?), + (PathBuf::from("worlds"), self.world.canonicalize()?), ]; - let mut meta = MetaManager::new(self.backup.clone(), dirs, metadata); + let mut meta = MetaManager::new(self.backup.canonicalize()?, dirs, metadata); meta.add_all(&self.layers)?; Ok(meta) diff --git a/src/main.rs b/src/main.rs index edad9c3..f357d1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,10 @@ use crate::cli::Cli; use clap::Parser; use std::io; +pub fn other(msg: &str) -> io::Error { + io::Error::new(io::ErrorKind::Other, msg) +} + // fn commands_backup(cli: &Cli, args: &BackupArgs) -> io::Result<()> { // let metadata = server::Metadata { // server_type: cli.server, diff --git a/src/server/mod.rs b/src/server/mod.rs index 09f90a5..23a9ff5 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -8,7 +8,7 @@ use clap::ValueEnum; use serde::{Deserialize, Serialize}; use std::fmt; -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)] pub enum ServerType { Unknown, Paper, @@ -29,7 +29,7 @@ impl fmt::Display for ServerType { } } -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize, Debug)] pub struct Metadata { pub server_type: ServerType, pub server_version: String,