feat: add extract command

main
Jef Roosens 2023-07-08 15:31:01 +02:00
parent 6cdc18742e
commit 241bb4d68e
Signed by: Jef Roosens
GPG Key ID: B75D4F293C7052DB
3 changed files with 107 additions and 26 deletions

View File

@ -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) ## [Unreleased](https://git.rustybever.be/Chewing_Bever/alex/src/branch/dev)
### Added
* Extract command for working with the output of export
### Changed ### Changed
* Export command no longer reads backups that do not contribute to the final * Export command no longer reads backups that do not contribute to the final

View File

@ -55,6 +55,45 @@ impl Backup<()> {
let filename = format!("{}", start_time.format(Self::FILENAME_FORMAT)); let filename = format!("{}", start_time.format(Self::FILENAME_FORMAT));
backup_dir.join(filename) 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<P: AsRef<Path>>(
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<T: Clone> Backup<T> { impl<T: Clone> Backup<T> {
@ -199,31 +238,8 @@ impl<T: Clone> Backup<T> {
backup_dir: P, backup_dir: P,
dirs: &Vec<(PathBuf, PathBuf)>, dirs: &Vec<(PathBuf, PathBuf)>,
) -> io::Result<()> { ) -> io::Result<()> {
let path = Backup::path(backup_dir, self.start_time); let backup_path = Backup::path(backup_dir, self.start_time);
let tar_gz = File::open(path)?; Backup::extract_archive(backup_path, dirs)?;
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 // Remove any files
for (path_in_tar, dst_dir) in dirs { for (path_in_tar, dst_dir) in dirs {

View File

@ -12,10 +12,13 @@ pub enum BackupCommands {
List(BackupListArgs), List(BackupListArgs),
/// Manually create a new backup /// Manually create a new backup
Create(BackupCreateArgs), Create(BackupCreateArgs),
/// Restore a backup /// Restore a backup including all of its required predecessors
Restore(BackupRestoreArgs), Restore(BackupRestoreArgs),
/// Export a backup into a full archive /// Export a backup into a full archive
Export(BackupExportArgs), Export(BackupExportArgs),
/// Extract a single backup; meant as a convenience method for working with the output of
/// `export`
Extract(BackupExtractArgs),
} }
#[derive(Args)] #[derive(Args)]
@ -63,6 +66,22 @@ pub struct BackupExportArgs {
make: bool, 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 { impl BackupArgs {
pub fn run(&self, cli: &Cli) -> io::Result<()> { pub fn run(&self, cli: &Cli) -> io::Result<()> {
match &self.command { match &self.command {
@ -70,6 +89,7 @@ impl BackupArgs {
BackupCommands::List(args) => args.run(cli), BackupCommands::List(args) => args.run(cli),
BackupCommands::Restore(args) => args.run(cli), BackupCommands::Restore(args) => args.run(cli),
BackupCommands::Export(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)
}
}