From b924a054a6643ba94222c9430e424d1745435678 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 8 Jul 2023 14:12:18 +0200 Subject: [PATCH 1/4] chore: bump version to 0.3.1 --- CHANGELOG.md | 2 ++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4de051..918b0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ 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) +## [0.3.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.3.1) + ### Added * Export command to export any backup as a new full backup diff --git a/Cargo.lock b/Cargo.lock index bbe03a6..16ec2ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "alex" -version = "0.3.0" +version = "0.3.1" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index 4e3e88b..01d0593 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alex" -version = "0.3.0" +version = "0.3.1" description = "Wrapper around Minecraft server processes, designed to complement Docker image installations." authors = ["Jef Roosens"] edition = "2021" From 6cdc18742ed841435b2b24a1b861e81bf8b967f9 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 8 Jul 2023 14:50:18 +0200 Subject: [PATCH 2/4] feat: don't read non-contributing archives for export --- CHANGELOG.md | 5 +++++ src/backup/delta.rs | 11 +++++++++-- src/backup/manager/mod.rs | 11 +++++++++-- src/backup/state.rs | 8 ++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 918b0ea..62b6692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ 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) +### Changed + +* Export command no longer reads backups that do not contribute to the final + state + ## [0.3.1](https://git.rustybever.be/Chewing_Bever/alex/src/tag/0.3.1) ### Added diff --git a/src/backup/delta.rs b/src/backup/delta.rs index 1bc5477..21f626c 100644 --- a/src/backup/delta.rs +++ b/src/backup/delta.rs @@ -18,11 +18,18 @@ pub struct Delta { impl Delta { pub fn new() -> Self { Self { - added: Default::default(), - removed: Default::default(), + added: State::new(), + removed: State::new(), } } + /// Returns whether the delta is empty by checking whether both its added and removed state + /// return true for their `is_empty`. + #[allow(dead_code)] + 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 diff --git a/src/backup/manager/mod.rs b/src/backup/manager/mod.rs index 6a43f9f..7cbd43c 100644 --- a/src/backup/manager/mod.rs +++ b/src/backup/manager/mod.rs @@ -226,9 +226,16 @@ where 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)) + // We only need to consider backups that have a non-empty contribution. + // This allows us to skip reading backups that have been completely + // overwritten by their successors anyways. + for (contribution, backup) in contributions + .iter() + .rev() + .zip(chain.iter().take(index + 1)) + .filter(|(contribution, _)| !contribution.is_empty()) { + println!("{}", &backup); backup.append(&self.backup_dir, contribution, &mut ar)?; } diff --git a/src/backup/state.rs b/src/backup/state.rs index 4b09acf..de4b2c0 100644 --- a/src/backup/state.rs +++ b/src/backup/state.rs @@ -41,6 +41,14 @@ impl State { 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()) + } } impl From for State From 241bb4d68eb9fb432965de8fa5c3996419950a50 Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 8 Jul 2023 15:31:01 +0200 Subject: [PATCH 3/4] feat: add extract command --- CHANGELOG.md | 4 +++ src/backup/mod.rs | 66 +++++++++++++++++++++++++++++------------------ src/cli/backup.rs | 63 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62b6692..bef4cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ 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 + +* Extract command for working with the output of export + ### Changed * Export command no longer reads backups that do not contribute to the final diff --git a/src/backup/mod.rs b/src/backup/mod.rs index 8eecc0f..6bd798f 100644 --- a/src/backup/mod.rs +++ b/src/backup/mod.rs @@ -55,6 +55,45 @@ impl Backup<()> { let filename = format!("{}", start_time.format(Self::FILENAME_FORMAT)); backup_dir.join(filename) } + + /// Extract an archive. + /// + /// # Arguments + /// + /// * `backup_path` - Path to the archive to extract + /// * `dirs` - list of tuples `(path_in_tar, dst_dir)` with `dst_dir` the directory on-disk + /// where the files stored under `path_in_tar` inside the tarball should be extracted to. + pub fn extract_archive>( + archive_path: P, + dirs: &Vec<(PathBuf, PathBuf)>, + ) -> io::Result<()> { + let tar_gz = File::open(archive_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; + } + } + } + + Ok(()) + } } impl Backup { @@ -199,31 +238,8 @@ impl Backup { 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; - } - } - } + let backup_path = Backup::path(backup_dir, self.start_time); + Backup::extract_archive(backup_path, dirs)?; // Remove any files for (path_in_tar, dst_dir) in dirs { diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 3a993fc..2ec1e2c 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -12,10 +12,13 @@ pub enum BackupCommands { List(BackupListArgs), /// Manually create a new backup Create(BackupCreateArgs), - /// Restore a backup + /// Restore a backup including all of its required predecessors Restore(BackupRestoreArgs), /// Export a backup into a full archive Export(BackupExportArgs), + /// Extract a single backup; meant as a convenience method for working with the output of + /// `export` + Extract(BackupExtractArgs), } #[derive(Args)] @@ -63,6 +66,22 @@ pub struct BackupExportArgs { make: bool, } +#[derive(Args)] +pub struct BackupExtractArgs { + /// Path to the backup to extract + 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 + #[arg(short, long, default_value_t = false)] + force: bool, + /// 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 { @@ -70,6 +89,7 @@ impl BackupArgs { BackupCommands::List(args) => args.run(cli), BackupCommands::Restore(args) => args.run(cli), BackupCommands::Export(args) => args.run(cli), + BackupCommands::Extract(args) => args.run(cli), } } } @@ -219,3 +239,44 @@ impl BackupExportArgs { } } } + +impl BackupExtractArgs { + pub fn run(&self, _cli: &Cli) -> io::Result<()> { + // Create directories if needed + if self.make { + std::fs::create_dir_all(&self.output_config)?; + std::fs::create_dir_all(&self.output_worlds)?; + } + + let output_config = self.output_config.canonicalize()?; + let output_worlds = self.output_worlds.canonicalize()?; + let backup_path = self.path.canonicalize()?; + + // Clear previous contents of directories + let mut entries = output_config + .read_dir()? + .chain(output_worlds.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)?; + } + } + + let dirs = vec![ + (PathBuf::from("config"), output_config), + (PathBuf::from("worlds"), output_worlds), + ]; + + Backup::extract_archive(backup_path, &dirs) + } +} From bfb264e8232c9414bc049d8f1841d049e9a09b2f Mon Sep 17 00:00:00 2001 From: Chewing_Bever Date: Sat, 8 Jul 2023 15:34:24 +0200 Subject: [PATCH 4/4] docs: add some more help strings --- src/cli/backup.rs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/cli/backup.rs b/src/cli/backup.rs index 2ec1e2c..6810e42 100644 --- a/src/cli/backup.rs +++ b/src/cli/backup.rs @@ -9,15 +9,29 @@ use std::path::{Path, PathBuf}; #[derive(Subcommand)] pub enum BackupCommands { /// List all tracked backups + /// + /// Note that this will only list backups for the layers currently configured, and will ignore + /// any other layers also present in the backup directory. List(BackupListArgs), /// Manually create a new backup + /// + /// Note that backups created using this command will count towards the length of a chain, and + /// can therefore shorten how far back in time your backups will be stored. Create(BackupCreateArgs), - /// Restore a backup including all of its required predecessors + /// Restore a backup + /// + /// This command will restore the selected backup by extracting its entire chain up to and + /// including the requested backup in-order. Restore(BackupRestoreArgs), /// Export a backup into a full archive + /// + /// Just like the restore command, this will extract each backup from the chain up to and + /// including the requested backup, but instead of writing the files to disk, they will be + /// recompressed into a new tarball, resulting in a new tarball containing a full backup. Export(BackupExportArgs), - /// Extract a single backup; meant as a convenience method for working with the output of - /// `export` + /// Extract an archive file, which is assumed to be a full backup. + /// + /// This command mostly exists as a convenience method for working with the output of `export`. Extract(BackupExtractArgs), } @@ -48,6 +62,9 @@ pub struct BackupRestoreArgs { /// Directory to store worlds in output_worlds: PathBuf, /// Whether to overwrite the contents of the output directories + /// + /// If set, the output directories will be completely cleared before trying to restore the + /// backup. #[arg(short, long, default_value_t = false)] force: bool, /// Create output directories if they don't exist @@ -75,6 +92,9 @@ pub struct BackupExtractArgs { /// Directory to store worlds in output_worlds: PathBuf, /// Whether to overwrite the contents of the output directories + /// + /// If set, the output directories will be completely cleared before trying to restore the + /// backup. #[arg(short, long, default_value_t = false)] force: bool, /// Create output directories if they don't exist