diff --git a/Cargo.toml b/Cargo.toml index 282bcaa..2510af1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "totebag" -version = "0.2.1" +version = "0.3.0" description = "A tool for archiving files and directories and extracting several archive formats." repository = "https://github.com/tamada/totebag" readme = "README.md" @@ -17,12 +17,15 @@ edition = "2021" bzip2 = "0.4.4" clap = { version = "4.5.4", features = ["derive"] } flate2 = "1.0.29" +sevenz-rust = "0.6.0" tar = "0.4.40" time = "0.3.36" unrar = "0.5.3" +xz2 = "0.1.7" zip = "1.1.1" [build-dependencies] clap = { version = "4.5.4", features = ["derive"] } clap_complete = "4.5.2" +toml = "0.8.12" diff --git a/README.md b/README.md index 754a8d9..6363916 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # totebag -[![Version](https://shields.io/badge/Version-0.2.1-blue)](https://github.com/tamada/totebag/releases/tag/v0.2.1) +[![Version](https://shields.io/badge/Version-0.3.0-blue)](https://github.com/tamada/totebag/releases/tag/v0.3.0) [![MIT License](https://shields.io/badge/License-MIT-blue)](https://github.com/tamada/totebag/blob/main/LICENSE) [![build](https://github.com/tamada/totebag/actions/workflows/build.yaml/badge.svg)](https://github.com/tamada/totebag/actions/workflows/build.yaml) @@ -18,25 +18,34 @@ The tool can extract archive files and archive files and directories. ## Usage ```sh -totebag [OPTIONS] -OPTIONS - -m, --mode Mode of operation. available: extract, archive, and auto. - Default is auto. - -d, --dest Destination of the extraction results. - Default is the current directory. - -o, --output Output file for the archive. - Default is the totebag.zip. - The archive formats are guessed form extension of the file name. - --overwrite Overwrite the output file if it exists. - -v, --verbose Display verbose output. - -h, --help Display this help message. -ARGUMENTS - extract mode: archive files to be extracted. - archive mode: files to be archived. - auto mode: if the arguments have archive files, it will extract them. - Otherwise, it will archive the files. +A tool for archiving files and directories and extracting several archive formats. + +Usage: totebag [OPTIONS] [ARGUMENTS]... + +Arguments: + [ARGUMENTS]... List of files or directories to be processed. + +Options: + -m, --mode Mode of operation. [default: auto] [possible values: auto, archive, extract, list] + -o, --output Output file in archive mode, or output directory in extraction mode + --to-archive-name-dir extract files to DEST/ARCHIVE_NAME directory (extract mode). + -n, --no-recursive No recursive directory (archive mode). + -v, --verbose Display verbose output. + --overwrite Overwrite existing files. + -h, --help Print help + -V, --version Print version ``` +Supported archive formats: + +- Tar +- Tar+Gzip +- Tar+Bzip2 +- Tar+Xz +- Zip +- 7z +- Rar (extraction only) + ## Install ```sh diff --git a/build.rs b/build.rs index 54a899f..5dcdace 100644 --- a/build.rs +++ b/build.rs @@ -5,24 +5,36 @@ use std::path::Path; include!("src/cli.rs"); -fn generate(s: Shell, app: &mut Command, outdir: &Path, file: &str) { +fn generate(s: Shell, app: &mut Command, appname: &str, outdir: &Path, file: String) { let destfile = outdir.join(file); - println!("dest: {}", destfile.display()); std::fs::create_dir_all(destfile.parent().unwrap()).unwrap(); let mut dest = File::create(destfile).unwrap(); + + clap_complete::generate(s, app, appname, &mut dest); +} + +fn parse_cargo_toml() -> toml::Value { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"); + let file = match std::fs::read_to_string(path) { + Ok(f) => f, + Err(e) => panic!("{}", e), + }; - clap_complete::generate(s, app, "totebag", &mut dest); + file.parse().unwrap() } fn main() { + let table = parse_cargo_toml(); + let appname = table["package"]["name"].as_str().unwrap(); + let mut app = CliOpts::command(); - app.set_bin_name("totebag"); + app.set_bin_name(appname); let outdir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("target/completions/"); - generate(Shell::Bash, &mut app, &outdir, "bash/totebag"); - generate(Shell::Elvish, &mut app, &outdir, "elvish/totebag"); - generate(Shell::Fish, &mut app, &outdir, "fish/totebag"); - generate(Shell::PowerShell, &mut app, &outdir, "powershell/totebag"); - generate(Shell::Zsh, &mut app, &outdir, "zsh/_totebag"); -} \ No newline at end of file + generate(Shell::Bash, &mut app, appname, &outdir, format!("bash/{}", appname)); + generate(Shell::Elvish, &mut app, appname, &outdir, format!("elvish/{}", appname)); + generate(Shell::Fish, &mut app, appname, &outdir, format!("fish/{}", appname)); + generate(Shell::PowerShell, &mut app, appname, &outdir, format!("powershell/{}", appname)); + generate(Shell::Zsh, &mut app, appname, &outdir, format!("zsh/_{}", appname)); +} diff --git a/src/archiver.rs b/src/archiver.rs index a3a364d..944cfd5 100644 --- a/src/archiver.rs +++ b/src/archiver.rs @@ -1,21 +1,23 @@ use std::fs::{create_dir_all, File}; use std::path::PathBuf; -use crate::cli::{ToatError, Result}; -use crate::format::{find_format, Format}; -use crate::archiver::zip::ZipArchiver; use crate::archiver::rar::RarArchiver; -use crate::archiver::tar::{TarArchiver, TarGzArchiver, TarBz2Archiver}; +use crate::archiver::sevenz::SevenZArchiver; +use crate::archiver::tar::{TarArchiver, TarBz2Archiver, TarGzArchiver, TarXzArchiver}; +use crate::archiver::zip::ZipArchiver; +use crate::cli::{Result, ToteError}; +use crate::format::{find_format, Format}; use crate::verboser::{create_verboser, Verboser}; use crate::CliOpts; mod os; -mod zip; mod rar; +mod sevenz; mod tar; +mod zip; pub trait Archiver { - fn perform(&self, inout: ArchiverOpts) -> Result<()>; + fn perform(&self, inout: &ArchiverOpts) -> Result<()>; fn format(&self) -> Format; } @@ -24,12 +26,14 @@ pub fn create_archiver(dest: &PathBuf) -> Result> { match format { Ok(format) => { return match format { - Format::Zip => Ok(Box::new(ZipArchiver{})), - Format::Tar => Ok(Box::new(TarArchiver{})), - Format::TarGz => Ok(Box::new(TarGzArchiver{})), - Format::TarBz2 => Ok(Box::new(TarBz2Archiver{})), - Format::Rar => Ok(Box::new(RarArchiver{})), - _ => Err(ToatError::UnsupportedFormat("unsupported format".to_string())), + Format::Zip => Ok(Box::new(ZipArchiver {})), + Format::Tar => Ok(Box::new(TarArchiver {})), + Format::TarGz => Ok(Box::new(TarGzArchiver {})), + Format::TarBz2 => Ok(Box::new(TarBz2Archiver {})), + Format::TarXz => Ok(Box::new(TarXzArchiver {})), + Format::Rar => Ok(Box::new(RarArchiver {})), + Format::SevenZ => Ok(Box::new(SevenZArchiver {})), + _ => Err(ToteError::UnknownFormat(format.to_string())), } } Err(msg) => Err(msg), @@ -40,10 +44,12 @@ pub fn archiver_info(archiver: &Box, opts: &ArchiverOpts) -> Strin format!( "Format: {:?}\nDestination: {:?}\nTargets: {:?}", archiver.format(), - opts.destination(), - opts.targets().iter() + opts.dest_path(), + opts.targets() + .iter() .map(|item| item.to_str().unwrap()) - .collect::>().join(", ") + .collect::>() + .join(", ") ) } @@ -58,9 +64,7 @@ pub struct ArchiverOpts { impl ArchiverOpts { pub fn new(opts: &CliOpts) -> Self { let args = opts.args.clone(); - let dest = opts.output.clone().unwrap_or_else(|| { - PathBuf::from(".") - }); + let dest = opts.output.clone().unwrap_or_else(|| PathBuf::from(".")); ArchiverOpts { dest: dest, targets: args, @@ -71,26 +75,50 @@ impl ArchiverOpts { } #[cfg(test)] - pub fn create(dest: PathBuf, targets: Vec, overwrite: bool, recursive: bool, verbose: bool) -> Self { - ArchiverOpts { dest, targets, overwrite, recursive, v: create_verboser(verbose) } + pub fn create( + dest: PathBuf, + targets: Vec, + overwrite: bool, + recursive: bool, + verbose: bool, + ) -> Self { + ArchiverOpts { + dest, + targets, + overwrite, + recursive, + v: create_verboser(verbose), + } } pub fn targets(&self) -> Vec { self.targets.clone() } + + /// Simply return the path for destination. + pub fn dest_path(&self) -> PathBuf { + self.dest.clone() + } + + /// Returns the destination file for the archive with opening it and create the parent directories. + /// If the path for destination is a directory or exists and overwrite is false, + /// this function returns an error. pub fn destination(&self) -> Result { let p = self.dest.as_path(); + print!("{:?}: {}\n", p, p.exists()); if p.is_file() && p.exists() && !self.overwrite { - return Err(ToatError::FileExists(self.dest.clone())) + return Err(ToteError::FileExists(self.dest.clone())); } if let Some(parent) = p.parent() { if !parent.exists() { - let _ = create_dir_all(parent); + if let Err(e) = create_dir_all(parent) { + return Err(ToteError::IOError(e)); + } } } match File::create(self.dest.as_path()) { - Err(e) => Err(ToatError::IOError(e)), Ok(f) => Ok(f), + Err(e) => Err(ToteError::IOError(e)), } } } @@ -121,4 +149,4 @@ mod tests { assert!(a5.is_ok()); assert_eq!(a5.unwrap().format(), Format::Rar); } -} \ No newline at end of file +} diff --git a/src/archiver/rar.rs b/src/archiver/rar.rs index c461356..e1a77e7 100644 --- a/src/archiver/rar.rs +++ b/src/archiver/rar.rs @@ -1,12 +1,12 @@ use crate::archiver::{Archiver, Format, ArchiverOpts}; -use crate::cli::{ToatError, Result}; +use crate::cli::{ToteError, Result}; pub(super) struct RarArchiver { } impl Archiver for RarArchiver { - fn perform(&self, _: ArchiverOpts) -> Result<()> { - Err(ToatError::UnsupportedFormat("only extraction support for rar".to_string())) + fn perform(&self, _: &ArchiverOpts) -> Result<()> { + Err(ToteError::UnsupportedFormat("only extraction support for rar".to_string())) } fn format(&self) -> Format { Format::Rar @@ -36,7 +36,7 @@ mod tests { recursive: false, v: create_verboser(false), }; - let r = archiver.perform(opts); + let r = archiver.perform(&opts); assert!(r.is_err()); } } diff --git a/src/archiver/sevenz.rs b/src/archiver/sevenz.rs new file mode 100644 index 0000000..8213372 --- /dev/null +++ b/src/archiver/sevenz.rs @@ -0,0 +1,121 @@ +use std::fs::File; +use std::path::PathBuf; + +use sevenz_rust::{SevenZArchiveEntry, SevenZWriter}; + +use crate::archiver::{Archiver, ArchiverOpts}; +use crate::cli::{Result, ToteError}; +use crate::format::Format; + +pub(super) struct SevenZArchiver {} + +impl Archiver for SevenZArchiver { + fn perform(&self, opts: &ArchiverOpts) -> Result<()> { + match opts.destination() { + Err(e) => Err(e), + Ok(file) => write_sevenz(file, opts.targets(), opts.recursive), + } + } + + fn format(&self) -> Format { + Format::SevenZ + } +} + +fn process_file(szw: &mut SevenZWriter, target: PathBuf) -> Result<()> { + let name = target.to_str().unwrap(); + if let Err(e) = szw.push_archive_entry( + SevenZArchiveEntry::from_path(&target, name.to_string()), + Some(File::open(target).unwrap()), + ) { + return Err(ToteError::ArchiverError(e.to_string())); + } + Ok(()) +} + +fn process_dir(szw: &mut SevenZWriter, target: PathBuf) -> Result<()> { + for entry in target.read_dir().unwrap() { + if let Ok(e) = entry { + let p = e.path(); + if p.is_dir() { + process_dir(szw, e.path())? + } else if p.is_file() { + process_file(szw, e.path())? + } + } + } + Ok(()) +} + +fn write_sevenz_impl( + mut szw: SevenZWriter, + targets: Vec, + recursive: bool, +) -> Result<()> { + for target in targets { + let path = target.as_path(); + if path.is_dir() && recursive { + process_dir(&mut szw, path.to_path_buf())? + } else { + process_file(&mut szw, path.to_path_buf())? + } + } + if let Err(e) = szw.finish() { + return Err(ToteError::ArchiverError(e.to_string())); + } + Ok(()) +} + +fn write_sevenz(dest: File, targets: Vec, recursive: bool) -> Result<()> { + match SevenZWriter::new(dest) { + Ok(write) => write_sevenz_impl(write, targets, recursive), + Err(e) => Err(ToteError::ArchiverError(e.to_string())), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::path::PathBuf; + + #[test] + fn test_format() { + let archiver = SevenZArchiver {}; + assert_eq!(archiver.format(), Format::SevenZ); + } + + fn run_test(f: F) + where + F: FnOnce(), + { + // setup(); // 予めやりたい処理 + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); + teardown(); // 後片付け処理 + + if let Err(err) = result { + std::panic::resume_unwind(err); + } + } + + #[test] + fn test_zip() { + run_test(|| { + let archiver = SevenZArchiver {}; + let inout = ArchiverOpts::create( + PathBuf::from("results/test.7z"), + vec![PathBuf::from("src"), PathBuf::from("Cargo.toml")], + true, + true, + false, + ); + let result = archiver.perform(&inout); + assert!(result.is_ok()); + assert_eq!(archiver.format(), Format::SevenZ); + }); + } + + fn teardown() { + let _ = std::fs::remove_file("results/test.7z"); + } +} diff --git a/src/archiver/tar.rs b/src/archiver/tar.rs index 5e6c42d..20ef253 100644 --- a/src/archiver/tar.rs +++ b/src/archiver/tar.rs @@ -1,11 +1,13 @@ +use std::fs::File; use std::io::Write; use std::path::PathBuf; use flate2::write::GzEncoder; use bzip2::write::BzEncoder; use tar::Builder; +use xz2::write::XzEncoder; use crate::archiver::{Archiver, Format, ArchiverOpts}; -use crate::cli::{ToatError, Result}; +use crate::cli::{ToteError, Result}; pub(super) struct TarArchiver { } @@ -14,51 +16,74 @@ pub(super) struct TarGzArchiver { pub(super) struct TarBz2Archiver { } +pub(super) struct TarXzArchiver { +} + impl Archiver for TarArchiver { - fn perform(&self, inout: ArchiverOpts) -> Result<()> { - match inout.destination() { - Err(e) => Err(e), - Ok(file) => { - write_to_tar(file, inout.targets(), inout.recursive) - } - } + fn perform(&self, inout: &ArchiverOpts) -> Result<()> { + write_tar(inout, |file| file) } fn format(&self) -> Format { Format::Tar } } + impl Archiver for TarGzArchiver{ - fn perform(&self, inout: ArchiverOpts) -> Result<()> { - match inout.destination() { - Err(e) => Err(e), - Ok(file) => { - let enc = GzEncoder::new(file, flate2::Compression::default()); - write_to_tar(enc, inout.targets(), inout.recursive) - } - } + fn perform(&self, inout: &ArchiverOpts) -> Result<()> { + write_tar(inout, |file| GzEncoder::new(file, flate2::Compression::default())) } fn format(&self) -> Format { Format::TarGz } } impl Archiver for TarBz2Archiver { - fn perform(&self, inout: ArchiverOpts) -> Result<()> { - match inout.destination() { - Err(e) => Err(e), - Ok(file) => { - let enc = BzEncoder::new(file, bzip2::Compression::best()); - write_to_tar(enc, inout.targets(), inout.recursive) - } - } + fn perform(&self, opts: &ArchiverOpts) -> Result<()> { + write_tar(opts, |file| BzEncoder::new(file, bzip2::Compression::best())) + } + fn format(&self) -> Format { + Format::TarBz2 + } +} + +impl Archiver for TarXzArchiver { + fn perform(&self, inout: &ArchiverOpts) -> Result<()> { + write_tar(inout, |file| XzEncoder::new(file, 9)) } fn format(&self) -> Format { Format::TarBz2 } } +fn write_tar(opts: &ArchiverOpts, f: F) -> Result<()> + where F: FnOnce(File) -> W { + match opts.destination() { + Err(e) => Err(e), + Ok(file) => { + let enc = f(file); + write_tar_impl(enc, opts.targets(), opts.recursive) + } + } +} + +fn write_tar_impl(file: W, targets: Vec, recursive: bool) -> Result<()> { + let mut builder = tar::Builder::new(file); + for target in targets { + let path = target.as_path(); + if path.is_dir() && recursive { + process_dir(&mut builder, path.to_path_buf(), recursive)? + } else { + process_file(&mut builder, path.to_path_buf())? + } + } + if let Err(e) = builder.finish() { + return Err(ToteError::ArchiverError(e.to_string())) + } + Ok(()) +} + fn process_dir(builder: &mut Builder, target: PathBuf, recursive: bool) -> Result<()> { if let Err(e) = builder.append_dir(&target, &target) { - return Err(ToatError::ArchiverError(e.to_string())) + return Err(ToteError::ArchiverError(e.to_string())) } for entry in target.read_dir().unwrap() { if let Ok(e) = entry { @@ -75,28 +100,12 @@ fn process_dir(builder: &mut Builder, target: PathBuf, recursive: b fn process_file(builder: &mut Builder, target: PathBuf) -> Result<()> { if let Err(e) = builder.append_path(target) { - Err(ToatError::ArchiverError(e.to_string())) + Err(ToteError::ArchiverError(e.to_string())) } else { Ok(()) } } -fn write_to_tar(file: W, targets: Vec, recursive: bool) -> Result<()> { - let mut builder = tar::Builder::new(file); - for target in targets { - let path = target.as_path(); - if path.is_dir() && recursive { - process_dir(&mut builder, path.to_path_buf(), recursive)? - } else { - process_file(&mut builder, path.to_path_buf())? - } - } - if let Err(e) = builder.finish() { - return Err(ToatError::ArchiverError(e.to_string())) - } - Ok(()) -} - #[cfg(test)] mod tests { use std::path::PathBuf; @@ -123,7 +132,7 @@ mod tests { run_test(|| { let archiver = TarArchiver{}; let inout = ArchiverOpts::create(PathBuf::from("results/test.tar"), vec![PathBuf::from("src"), PathBuf::from("Cargo.toml")], true, true, false); - let result = archiver.perform(inout); + let result = archiver.perform(&inout); let path = PathBuf::from("results/test.tar"); assert!(result.is_ok()); assert!(path.exists()); @@ -138,7 +147,7 @@ mod tests { run_test(|| { let archiver = TarGzArchiver{}; let inout = ArchiverOpts::create(PathBuf::from("results/test.tar.gz"), vec![PathBuf::from("src"), PathBuf::from("Cargo.toml")], true, true, false); - let result = archiver.perform(inout); + let result = archiver.perform(&inout); let path = PathBuf::from("results/test.tar.gz"); assert!(result.is_ok()); assert!(path.exists()); @@ -152,7 +161,7 @@ mod tests { run_test(|| { let archiver = TarBz2Archiver{}; let inout = ArchiverOpts::create(PathBuf::from("results/test.tar.bz2"), vec![PathBuf::from("src"), PathBuf::from("Cargo.toml")], true, true, false); - let result = archiver.perform(inout); + let result = archiver.perform(&inout); let path = PathBuf::from("results/test.tar.bz2"); assert!(result.is_ok()); assert!(path.exists()); diff --git a/src/archiver/zip.rs b/src/archiver/zip.rs index c73543f..3dcea3c 100644 --- a/src/archiver/zip.rs +++ b/src/archiver/zip.rs @@ -11,13 +11,13 @@ use zip::ZipWriter; use crate::archiver::{Archiver, Format, ArchiverOpts}; use crate::archiver::os; -use crate::cli::{ToatError, Result}; +use crate::cli::{ToteError, Result}; pub(super) struct ZipArchiver { } impl Archiver for ZipArchiver { - fn perform(&self, inout: ArchiverOpts) -> Result<()> { + fn perform(&self, inout: &ArchiverOpts) -> Result<()> { match inout.destination() { Err(e) => Err(e), Ok(file) => { @@ -49,11 +49,11 @@ fn process_file (zw: &mut ZipWriter, target: PathBuf) -> Result let name = target.to_str().unwrap(); let opts = create(&target); if let Err(e) = zw.start_file(name, opts) { - return Err(ToatError::ArchiverError(e.to_string())); + return Err(ToteError::ArchiverError(e.to_string())); } let mut file = BufReader::new(File::open(target).unwrap()); if let Err(e) = std::io::copy(&mut file, zw) { - return Err(ToatError::IOError(e)) + return Err(ToteError::IOError(e)) } Ok(()) } @@ -69,7 +69,7 @@ fn write_to_zip(dest: File, targets: Vec, recursive: bool) -> Result<() } } if let Err(e) = zw.finish() { - return Err(ToatError::ArchiverError(e.to_string())); + return Err(ToteError::ArchiverError(e.to_string())); } Ok(()) } @@ -96,7 +96,7 @@ mod tests { run_test(|| { let archiver = ZipArchiver{}; let inout = ArchiverOpts::create(PathBuf::from("results/test.zip"), vec![PathBuf::from("src"), PathBuf::from("Cargo.toml")], true, true, false); - let result = archiver.perform(inout); + let result = archiver.perform(&inout); assert!(result.is_ok()); assert_eq!(archiver.format(), Format::Zip); }); diff --git a/src/cli.rs b/src/cli.rs index d3a7a67..8ddb219 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use clap::{Parser, ValueEnum}; -pub type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Parser, Debug)] #[clap( @@ -28,7 +28,7 @@ pub struct CliOpts { impl CliOpts { pub fn run_mode(&mut self) -> Result { if self.args.len() == 0 { - return Err(ToatError::NoArgumentsGiven) + return Err(ToteError::NoArgumentsGiven) } if self.mode == RunMode::Auto { if is_all_args_archives(&self.args) { @@ -47,7 +47,7 @@ impl CliOpts { fn is_all_args_archives(args: &[PathBuf]) -> bool { args.iter().all(|arg| { let name = arg.to_str().unwrap().to_lowercase(); - let exts = vec![".zip", ".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".rar", ".jar", ".war", ".ear", ]; + let exts = vec![".zip", ".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tbz2", ".rar", ".jar", ".war", ".ear", "7z", ]; for ext in exts.iter() { if name.ends_with(ext) { return true @@ -66,14 +66,16 @@ pub enum RunMode { } #[derive(Debug)] -pub enum ToatError { +pub enum ToteError { NoArgumentsGiven, FileNotFound(PathBuf), FileExists(PathBuf), IOError(std::io::Error), ArchiverError(String), UnsupportedFormat(String), + UnknownFormat(String), UnknownError(String), + SomeError(Box) } #[cfg(test)] diff --git a/src/extractor.rs b/src/extractor.rs index 8f2078e..5ab7d81 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -1,13 +1,14 @@ use std::path::PathBuf; use crate::format::{find_format, Format}; -use crate::cli::{Result, ToatError}; +use crate::cli::{Result, ToteError}; use crate::CliOpts; use crate::verboser::{create_verboser, Verboser}; mod zip; mod rar; mod tar; +mod sevenz; pub struct ExtractorOpts { pub dest: PathBuf, @@ -17,6 +18,20 @@ pub struct ExtractorOpts { } impl ExtractorOpts { + pub fn new(opts: &CliOpts) -> ExtractorOpts { + let d = opts.output.clone(); + ExtractorOpts { + dest: d.unwrap_or_else(|| { + PathBuf::from(".") + }), + use_archive_name_dir: opts.to_archive_name_dir, + overwrite: opts.overwrite, + v: create_verboser(opts.verbose), + } + } + + /// Returns the base of the destination directory for the archive file. + /// The target is the archive file name of source. pub fn destination(&self, target: &PathBuf) -> PathBuf { if self.use_archive_name_dir { let file_name = target.file_name().unwrap().to_str().unwrap(); @@ -36,18 +51,6 @@ pub trait Extractor { fn format(&self) -> Format; } -pub fn create_extract_opts(opts: &CliOpts) -> ExtractorOpts { - let d = opts.output.clone(); - ExtractorOpts { - dest: d.unwrap_or_else(|| { - PathBuf::from(".") - }), - use_archive_name_dir: opts.to_archive_name_dir, - overwrite: opts.overwrite, - v: create_verboser(opts.verbose), - } -} - pub fn create_extractor(file: &PathBuf) -> Result> { let format = find_format(file.file_name()); match format { @@ -58,7 +61,9 @@ pub fn create_extractor(file: &PathBuf) -> Result> { Format::Tar => Ok(Box::new(tar::TarExtractor{})), Format::TarGz => Ok(Box::new(tar::TarGzExtractor{})), Format::TarBz2 => Ok(Box::new(tar::TarBz2Extractor{})), - _ => Err(ToatError::UnsupportedFormat("unsupported format".to_string())), + Format::TarXz => Ok(Box::new(tar::TarXzExtractor{})), + Format::SevenZ => Ok(Box::new(sevenz::SevenZExtractor{})), + Format::Unknown(s) => Err(ToteError::UnknownFormat(format!("{}: unsupported format", s))), } } Err(msg) => Err(msg), diff --git a/src/extractor/sevenz.rs b/src/extractor/sevenz.rs new file mode 100644 index 0000000..9605f48 --- /dev/null +++ b/src/extractor/sevenz.rs @@ -0,0 +1,85 @@ +use std::fs::File; +use std::path::PathBuf; + +use sevenz_rust::{Archive, BlockDecoder, Password}; + +use crate::extractor::Extractor; +use crate::format::Format; +use crate::cli::{Result, ToteError}; + +use super::ExtractorOpts; + +pub(super) struct SevenZExtractor { +} + +impl Extractor for SevenZExtractor { + fn list_archives(&self, archive_file: PathBuf) -> Result> { + let mut reader = File::open(archive_file).unwrap(); + let len = reader.metadata().unwrap().len(); + match Archive::read(&mut reader,len, Password::empty().as_ref()) { + Ok(archive) => { + let mut r = Vec::::new(); + for entry in &archive.files { + r.push(entry.name.clone()) + } + Ok(r) + }, + Err(e) => Err(ToteError::SomeError(Box::new(e))), + } + } + fn perform(&self, archive_file: PathBuf, opts: &ExtractorOpts) -> Result<()> { + let mut file = match File::open(&archive_file) { + Ok(file) => { + file + }, + Err(e) => return Err(ToteError::IOError(e)), + }; + extract(&mut file, archive_file, opts) + } + fn format(&self) -> Format { + Format::SevenZ + } +} + +fn extract(mut file: &File, path: PathBuf, opts: &ExtractorOpts) -> Result<()> { + let len = file.metadata().unwrap().len(); + let password = Password::empty(); + let archive = match Archive::read(&mut file, len, password.as_ref()) { + Ok(reader) => { + reader + }, + Err(e) => return Err(ToteError::SomeError(Box::new(e))), + }; + let folder_count = archive.folders.len(); + for findex in 0..folder_count { + let folder_decoder = BlockDecoder::new(findex, &archive, password.as_slice(), &mut file); + if let Err(e) = folder_decoder.for_each_entries(&mut |entry, reader| { + let dest = opts.destination(&path).join(entry.name.clone()); + sevenz_rust::default_entry_extract_fn(entry, reader, &dest) + }) { + return Err(ToteError::SomeError(Box::new(e))) + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_list() { + let extractor = SevenZExtractor{}; + let file = PathBuf::from("testdata/test.7z"); + match extractor.list_archives(file) { + Ok(r) => { + assert_eq!(r.len(), 21); + assert_eq!(r.get(0), Some("Cargo.toml".to_string()).as_ref()); + assert_eq!(r.get(1), Some("build.rs".to_string()).as_ref()); + assert_eq!(r.get(2), Some("LICENSE".to_string()).as_ref()); + assert_eq!(r.get(3), Some("README.md".to_string()).as_ref()); + }, + Err(_) => assert!(false), + } + } +} \ No newline at end of file diff --git a/src/extractor/tar.rs b/src/extractor/tar.rs index 2cb29be..f262745 100644 --- a/src/extractor/tar.rs +++ b/src/extractor/tar.rs @@ -2,7 +2,10 @@ use std::fs::create_dir_all; use std::io::Read; use std::{fs::File, path::PathBuf}; -use crate::cli::Result; +use tar::Archive; +use xz2::read::XzDecoder; + +use crate::cli::{ToteError, Result}; use crate::extractor::{Extractor, ExtractorOpts}; use crate::format::Format; @@ -12,17 +15,21 @@ pub(super) struct TarGzExtractor { } pub(super) struct TarBz2Extractor { } +pub(super) struct TarXzExtractor { +} impl Extractor for TarExtractor { fn list_archives(&self, archive_file: PathBuf) -> Result> { - let file = File::open(archive_file).unwrap(); - let mut archive = tar::Archive::new(file); - list_tar(&mut archive) + match open_tar_file(&archive_file, |f| f) { + Ok(archive) => list_tar(archive), + Err(e) => Err(e), + } } fn perform(&self, archive_file: PathBuf, opts: &ExtractorOpts) -> Result<()> { - let file = File::open(&archive_file).unwrap(); - let mut archive = tar::Archive::new(file); - extract_tar(&mut archive, archive_file, opts) + match open_tar_file(&archive_file, |f| f) { + Err(e) => Err(e), + Ok(archive) => extract_tar(archive, archive_file, opts), + } } fn format(&self) -> Format { Format::Tar @@ -31,16 +38,16 @@ impl Extractor for TarExtractor { impl Extractor for TarGzExtractor { fn list_archives(&self, archive_file: PathBuf) -> Result> { - let file = File::open(archive_file).unwrap(); - let targz = flate2::read::GzDecoder::new(file); - let mut archive = tar::Archive::new(targz); - list_tar(&mut archive) + match open_tar_file(&archive_file, |f| flate2::read::GzDecoder::new(f)) { + Ok(archive) => list_tar(archive), + Err(e) => Err(e), + } } fn perform(&self, archive_file: PathBuf, opts: &ExtractorOpts) -> Result<()> { - let file = File::open(&archive_file).unwrap(); - let targz = flate2::read::GzDecoder::new(file); - let mut archive = tar::Archive::new(targz); - extract_tar(&mut archive, archive_file, opts) + match open_tar_file(&archive_file, |f| flate2::read::GzDecoder::new(f)) { + Ok(archive) => extract_tar(archive, archive_file, opts), + Err(e) => Err(e), + } } fn format(&self) -> Format { Format::TarGz @@ -49,23 +56,51 @@ impl Extractor for TarGzExtractor { impl Extractor for TarBz2Extractor { fn list_archives(&self, archive_file: PathBuf) -> Result> { - let file = File::open(archive_file).unwrap(); - let tarbz2 = bzip2::read::BzDecoder::new(file); - let mut archive = tar::Archive::new(tarbz2); - list_tar(&mut archive) + match open_tar_file(&archive_file, |f| bzip2::read::BzDecoder::new(f)) { + Ok(archive) => list_tar(archive), + Err(e) => Err(e), + } } fn perform(&self, archive_file: PathBuf, opts: &ExtractorOpts) -> Result<()> { - let file = File::open(&archive_file).unwrap(); - let tarbz2 = bzip2::read::BzDecoder::new(file); - let mut archive = tar::Archive::new(tarbz2); - extract_tar(&mut archive, archive_file, opts) + match open_tar_file(&archive_file, |f| bzip2::read::BzDecoder::new(f)) { + Err(e) => Err(e), + Ok(archive) => extract_tar(archive, archive_file, opts), + } } fn format(&self) -> Format { Format::TarBz2 } } -fn extract_tar(archive: &mut tar::Archive, original: PathBuf, opts: &ExtractorOpts) -> Result<()> { +impl Extractor for TarXzExtractor { + fn list_archives(&self, archive_file: PathBuf) -> Result> { + match open_tar_file(&archive_file, |f| XzDecoder::new(f)) { + Err(e) => Err(e), + Ok(archive) => list_tar(archive), + } + } + fn perform(&self, archive_file: PathBuf, opts: &ExtractorOpts) -> Result<()> { + match open_tar_file(&archive_file, |f| XzDecoder::new(f)) { + Err(e) => Err(e), + Ok(archive) => extract_tar(archive, archive_file, opts), + } + } + fn format(&self) -> Format { + Format::TarXz + } +} + +fn open_tar_file(file: &PathBuf, opener: F) -> Result> + where F: FnOnce(File) -> R { + let file = match File::open(file) { + Ok(f) => f, + Err(e) => return Err(ToteError::IOError(e)), + }; + let writer = opener(file); + Ok(Archive::new(writer)) +} + +fn extract_tar(mut archive: tar::Archive, original: PathBuf, opts: &ExtractorOpts) -> Result<()> { for entry in archive.entries().unwrap() { let mut entry = entry.unwrap(); let path = entry.header().path().unwrap(); @@ -90,7 +125,7 @@ fn is_filename_mac_finder_file(path: PathBuf) -> bool { filename == ".DS_Store" || filename.starts_with("._") } -fn list_tar(archive: &mut tar::Archive) -> Result> { +fn list_tar(mut archive: tar::Archive) -> Result> { let mut result = Vec::::new(); for entry in archive.entries().unwrap() { let entry = entry.unwrap(); diff --git a/src/extractor/zip.rs b/src/extractor/zip.rs index ed65ab0..73cc624 100644 --- a/src/extractor/zip.rs +++ b/src/extractor/zip.rs @@ -26,11 +26,12 @@ impl Extractor for ZipExtractor { fn perform(&self, archive_file: PathBuf, opts: &ExtractorOpts) -> Result<()> { let zip_file = File::open(&archive_file).unwrap(); let mut zip = zip::ZipArchive::new(zip_file).unwrap(); + let dest_base = opts.destination(&archive_file); for i in 0..zip.len() { let mut file = zip.by_index(i).unwrap(); if file.is_file() { opts.v.verbose(format!("extracting {} ({} bytes)", file.name(), file.size())); - let dest = opts.destination(&archive_file).join(PathBuf::from(file.name().to_string())); + let dest = dest_base.join(PathBuf::from(file.name().to_string())); create_dir_all(dest.parent().unwrap()).unwrap(); let mut out = File::create(dest).unwrap(); copy(&mut file, &mut out).unwrap(); diff --git a/src/format.rs b/src/format.rs index d9a00f7..d0dfe80 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,5 +1,6 @@ use std::ffi::OsStr; -use crate::cli::{ToatError, Result}; +use std::fmt::Display; +use crate::cli::{ToteError, Result}; pub fn find_format(file_name: Option<&OsStr>) -> Result { match file_name { @@ -9,6 +10,10 @@ pub fn find_format(file_name: Option<&OsStr>) -> Result { return Ok(Format::TarGz); } else if name.ends_with(".tar.bz2") || name.ends_with(".tbz2") { return Ok(Format::TarBz2); + } else if name.ends_with(".tar.xz") || name.ends_with(".txz") { + return Ok(Format::TarXz); + } else if name.ends_with(".7z") { + return Ok(Format::SevenZ); } else if name.ends_with(".tar") { return Ok(Format::Tar); } else if name.ends_with(".rar") { @@ -16,10 +21,10 @@ pub fn find_format(file_name: Option<&OsStr>) -> Result { } else if name.ends_with(".zip") || name.ends_with(".jar") || name.ends_with(".war") || name.ends_with(".ear") { return Ok(Format::Zip); } else { - return Ok(Format::Unknown); + return Ok(Format::Unknown(file_name.to_str().unwrap().to_string())); } } - None => Err(ToatError::UnsupportedFormat("no file name provided".to_string())), + None => Err(ToteError::NoArgumentsGiven), } } @@ -30,8 +35,25 @@ pub enum Format { Tar, TarGz, TarBz2, + TarXz, + SevenZ, Rar, - Unknown, + Unknown(String), +} + +impl Display for Format { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Format::Zip => write!(f, "Zip"), + Format::Tar => write!(f, "Tar"), + Format::TarGz => write!(f, "TarGz"), + Format::TarBz2 => write!(f, "TarBz2"), + Format::TarXz => write!(f, "TarXz"), + Format::SevenZ => write!(f, "SevenZ"), + Format::Rar => write!(f, "Rar"), + Format::Unknown(s) => write!(f, "{}: unknown format", s), + } + } } #[cfg(test)] @@ -45,7 +67,7 @@ mod tests { assert_eq!(f, Format::Zip); } if let Ok(f) = find_format(Some(OsStr::new("hoge.unknown"))) { - assert_eq!(f, Format::Unknown); + assert_eq!(f.to_string(), "hoge.unknown: unknown format".to_string()); } if let Ok(f) = find_format(Some(OsStr::new("hoge.tar"))) { assert_eq!(f, Format::Tar); @@ -59,5 +81,11 @@ mod tests { if let Ok(f) = find_format(Some(OsStr::new("hoge.tar.bz2"))) { assert_eq!(f, Format::TarBz2); } + if let Ok(f) = find_format(Some(OsStr::new("hoge.tar.xz"))) { + assert_eq!(f, Format::TarXz); + } + if let Ok(f) = find_format(Some(OsStr::new("hoge.7z"))) { + assert_eq!(f, Format::SevenZ); + } } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 62cf0b9..f796193 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,24 @@ +use archiver::{archiver_info, ArchiverOpts}; use clap::Parser; use cli::*; -use cli::{ToatError, RunMode}; -use archiver::{archiver_info, ArchiverOpts}; -use extractor::{create_extract_opts, extractor_info}; +use cli::{RunMode, ToteError}; +use extractor::{extractor_info, ExtractorOpts}; -mod cli; -mod format; mod archiver; +mod cli; mod extractor; +mod format; mod verboser; fn perform(mut opts: CliOpts) -> Result<()> { match opts.run_mode() { - Ok(RunMode::Archive) => { - return perform_archive(opts) - } - Ok(RunMode::Extract) => { - return perform_extract(opts) - } - Ok(RunMode::List) => { - return perform_list(opts) - } + Ok(RunMode::Archive) => return perform_archive(opts), + Ok(RunMode::Extract) => return perform_extract(opts), + Ok(RunMode::List) => return perform_list(opts), Ok(RunMode::Auto) => { - return Err(ToatError::UnknownError("cannot distinguish archiving and extracting".to_string())) + return Err(ToteError::UnknownError( + "cannot distinguish archiving and extracting".to_string(), + )) } Err(e) => { return Err(e); @@ -32,13 +28,15 @@ fn perform(mut opts: CliOpts) -> Result<()> { fn perform_extract(opts: CliOpts) -> Result<()> { let args = opts.args.clone(); - let extract_opts = create_extract_opts(&opts); + let extract_opts = ExtractorOpts::new(&opts); for arg in args.iter() { let extractor = extractor::create_extractor(arg).unwrap(); let target = arg.to_path_buf(); - extract_opts.v.verbose(extractor_info(&extractor, &target, &extract_opts)); + extract_opts + .v + .verbose(extractor_info(&extractor, &target, &extract_opts)); extractor.perform(target, &extract_opts)?; - }; + } Ok(()) } @@ -46,7 +44,7 @@ fn perform_list(opts: CliOpts) -> Result<()> { let args = opts.args.clone(); for arg in args.iter() { if !arg.exists() { - return Err(ToatError::FileNotFound(arg.to_path_buf())) + return Err(ToteError::FileNotFound(arg.to_path_buf())); } let extractor = extractor::create_extractor(&arg).unwrap(); if args.len() > 1 { @@ -58,14 +56,17 @@ fn perform_list(opts: CliOpts) -> Result<()> { } } Ok(()) - } fn perform_archive(opts: CliOpts) -> Result<()> { let inout = ArchiverOpts::new(&opts); - let archiver = archiver::create_archiver(&opts.output.unwrap()).unwrap(); - inout.v.verbose(archiver_info(&archiver, &inout)); - archiver.perform(inout) + match archiver::create_archiver(&opts.output.unwrap()) { + Ok(archiver) => { + inout.v.verbose(archiver_info(&archiver, &inout)); + archiver.perform(&inout) + } + Err(e) => Err(e), + } } fn main() -> Result<()> { @@ -73,13 +74,19 @@ fn main() -> Result<()> { Ok(_) => Ok(()), Err(e) => { match e { - ToatError::NoArgumentsGiven => println!("No arguments given. Use --help for usage."), - ToatError::FileNotFound(p) => println!("{}: file not found", p.to_str().unwrap()), - ToatError::FileExists(p) => println!("{}: file already exists", p.to_str().unwrap()), - ToatError::IOError(e) => println!("IO error: {}", e), - ToatError::ArchiverError(s) => println!("Archive error: {}", s), - ToatError::UnsupportedFormat(f) => println!("{}: unsupported format", f), - ToatError::UnknownError(s) => println!("Unknown error: {}", s), + ToteError::NoArgumentsGiven => { + println!("No arguments given. Use --help for usage.") + } + ToteError::FileNotFound(p) => println!("{}: file not found", p.to_str().unwrap()), + ToteError::FileExists(p) => { + println!("{}: file already exists", p.to_str().unwrap()) + } + ToteError::IOError(e) => println!("IO error: {}", e), + ToteError::ArchiverError(s) => println!("Archive error: {}", s), + ToteError::UnknownFormat(f) => println!("{}: unknown format", f), + ToteError::UnsupportedFormat(f) => println!("{}: unsupported format", f), + ToteError::SomeError(e) => println!("Error: {}", e), + ToteError::UnknownError(s) => println!("Unknown error: {}", s), } std::process::exit(1); } @@ -88,16 +95,32 @@ fn main() -> Result<()> { #[cfg(test)] mod tests { - use std::path::PathBuf; - use cli::RunMode; use super::*; + use cli::RunMode; + use std::path::PathBuf; #[test] fn test_run() { - let opts = CliOpts::parse_from(&["totebag_test", "-o", "test.zip", "src", "LICENSE", "README.md", "Cargo.toml"]); + let opts = CliOpts::parse_from(&[ + "totebag_test", + "-o", + "test.zip", + "src", + "LICENSE", + "README.md", + "Cargo.toml", + ]); assert_eq!(opts.mode, RunMode::Auto); assert_eq!(opts.output, Some(PathBuf::from("test.zip"))); assert_eq!(opts.args.len(), 4); - assert_eq!(opts.args, vec![PathBuf::from("src"), PathBuf::from("LICENSE"), PathBuf::from("README.md"), PathBuf::from("Cargo.toml")]); + assert_eq!( + opts.args, + vec![ + PathBuf::from("src"), + PathBuf::from("LICENSE"), + PathBuf::from("README.md"), + PathBuf::from("Cargo.toml") + ] + ); } -} \ No newline at end of file +} diff --git a/templates/README.md b/templates/README.md index 96337ab..762ca6e 100644 --- a/templates/README.md +++ b/templates/README.md @@ -18,25 +18,34 @@ The tool can extract archive files and archive files and directories. ## Usage ```sh -totebag [OPTIONS] -OPTIONS - -m, --mode Mode of operation. available: extract, archive, and auto. - Default is auto. - -d, --dest Destination of the extraction results. - Default is the current directory. - -o, --output Output file for the archive. - Default is the totebag.zip. - The archive formats are guessed form extension of the file name. - --overwrite Overwrite the output file if it exists. - -v, --verbose Display verbose output. - -h, --help Display this help message. -ARGUMENTS - extract mode: archive files to be extracted. - archive mode: files to be archived. - auto mode: if the arguments have archive files, it will extract them. - Otherwise, it will archive the files. +A tool for archiving files and directories and extracting several archive formats. + +Usage: totebag [OPTIONS] [ARGUMENTS]... + +Arguments: + [ARGUMENTS]... List of files or directories to be processed. + +Options: + -m, --mode Mode of operation. [default: auto] [possible values: auto, archive, extract, list] + -o, --output Output file in archive mode, or output directory in extraction mode + --to-archive-name-dir extract files to DEST/ARCHIVE_NAME directory (extract mode). + -n, --no-recursive No recursive directory (archive mode). + -v, --verbose Display verbose output. + --overwrite Overwrite existing files. + -h, --help Print help + -V, --version Print version ``` +Supported archive formats: + +- Tar +- Tar+Gzip +- Tar+Bzip2 +- Tar+Xz +- Zip +- 7z +- Rar (extraction only) + ## Install ```sh diff --git a/testdata/test.7z b/testdata/test.7z new file mode 100644 index 0000000..65102d6 Binary files /dev/null and b/testdata/test.7z differ