Skip to content

Commit

Permalink
implement extracting and listing rar, zip, tar, tar+gz, and tar+bz2
Browse files Browse the repository at this point in the history
  • Loading branch information
tamada committed Apr 30, 2024
1 parent 5543c0c commit 6e87cb2
Show file tree
Hide file tree
Showing 18 changed files with 576 additions and 98 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ clap = { version = "4.5.4", features = ["derive"] }
flate2 = "1.0.29"
tar = "0.4.40"
time = "0.3.36"
unrar = "0.5.3"
zip = "1.1.1"

[build-dependencies]
Expand Down
107 changes: 33 additions & 74 deletions src/archiver.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
use std::{ffi::OsStr, path::PathBuf};
use std::fs::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::verboser::{create_verboser, Verboser};
use crate::CliOpts;

mod zip;
mod rar;
mod tar;

pub trait Archiver {
fn perform(&self, inout: InOut) -> Result<()>;
fn perform(&self, inout: ArchiverOpts) -> Result<()>;
fn format(&self) -> Format;
}

Expand All @@ -32,28 +35,45 @@ pub fn create_archiver(dest: PathBuf) -> Result<Box<dyn Archiver>> {
}
}

pub fn archiver_info(archiver: Box<dyn Archiver>, inout: InOut) -> String {
pub fn archiver_info(archiver: &Box<dyn Archiver>, opts: &ArchiverOpts) -> String {
format!(
"Format: {:?}\nDestination: {:?}\nTargets: {:?}",
archiver.format(),
inout.destination(),
inout.targets().iter()
opts.destination(),
opts.targets().iter()
.map(|item| item.to_str().unwrap())
.collect::<Vec<_>>().join(", ")
)
}

pub struct InOut {
dest: PathBuf,
targets: Vec<PathBuf>,
overwrite: bool,
recursive: bool,
pub struct ArchiverOpts {
pub dest: PathBuf,
pub targets: Vec<PathBuf>,
pub overwrite: bool,
pub recursive: bool,
pub v: Box<dyn Verboser>,
}

impl InOut {
pub fn new(dest: PathBuf, targets: Vec<PathBuf>, overwrite: bool, recursive: bool) -> Self {
InOut { dest, targets, overwrite, recursive }
impl ArchiverOpts {
pub fn new(opts: &CliOpts) -> Self {
let args = opts.args.clone();
let dest = opts.dest.clone().unwrap_or_else(|| {
PathBuf::from(".")
});
ArchiverOpts {
dest: dest,
targets: args,
overwrite: opts.overwrite,
recursive: !opts.no_recursive,
v: create_verboser(opts.verbose),
}
}

#[cfg(test)]
pub fn create(dest: PathBuf, targets: Vec<PathBuf>, overwrite: bool, recursive: bool, verbose: bool) -> Self {
ArchiverOpts { dest, targets, overwrite, recursive, v: create_verboser(verbose) }
}

pub fn targets(&self) -> Vec<PathBuf> {
self.targets.clone()
}
Expand All @@ -68,64 +88,3 @@ impl InOut {
}
}
}

fn find_format(file_name: Option<&OsStr>) -> Result<Format> {
match file_name {
Some(file_name) => {
let name = file_name.to_str().unwrap().to_lowercase();
if name.ends_with(".tar.gz") || name.ends_with(".tgz") {
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") {
return Ok(Format::Tar);
} else if name.ends_with(".rar") {
return Ok(Format::Rar);
} else if name.ends_with(".zip") {
return Ok(Format::Zip);
} else {
return Ok(Format::Unknown);
}
}
None => Err(ToatError::UnsupportedFormat("no file name provided".to_string())),
}
}


#[derive(Debug, PartialEq)]
pub enum Format {
Zip,
Tar,
TarGz,
TarBz2,
Rar,
Unknown,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_format() {
assert!(find_format(None).is_err());
if let Ok(f) = find_format(Some(OsStr::new("hoge.zip"))) {
assert_eq!(f, Format::Zip);
}
if let Ok(f) = find_format(Some(OsStr::new("hoge.unknown"))) {
assert_eq!(f, Format::Unknown);
}
if let Ok(f) = find_format(Some(OsStr::new("hoge.tar"))) {
assert_eq!(f, Format::Tar);
}
if let Ok(f) = find_format(Some(OsStr::new("hoge.rar"))) {
assert_eq!(f, Format::Rar);
}
if let Ok(f) = find_format(Some(OsStr::new("hoge.tar.gz"))) {
assert_eq!(f, Format::TarGz);
}
if let Ok(f) = find_format(Some(OsStr::new("hoge.tar.bz2"))) {
assert_eq!(f, Format::TarBz2);
}
}
}
6 changes: 3 additions & 3 deletions src/archiver/rar.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use crate::archiver::{Archiver, InOut, Format};
use crate::archiver::{Archiver, Format, ArchiverOpts};
use crate::cli::{ToatError, Result};

pub struct RarArchiver {
pub(super) struct RarArchiver {
}

impl Archiver for RarArchiver {
fn perform(&self, _: InOut) -> Result<()> {
fn perform(&self, _: ArchiverOpts) -> Result<()> {
Err(ToatError::UnsupportedFormat("only extraction support for rar".to_string()))
}
fn format(&self) -> Format {
Expand Down
14 changes: 7 additions & 7 deletions src/archiver/tar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ use flate2::write::GzEncoder;
use bzip2::write::BzEncoder;
use tar::Builder;

use crate::archiver::{Archiver, InOut, Format};
use crate::archiver::{Archiver, Format, ArchiverOpts};
use crate::cli::{ToatError, Result};

pub struct TarArchiver {
pub(super) struct TarArchiver {
}
pub struct TarGzArchiver {
pub(super) struct TarGzArchiver {
}
pub struct TarBz2Archiver {
pub(super) struct TarBz2Archiver {
}

impl Archiver for TarArchiver {
fn perform(&self, inout: InOut) -> Result<()> {
fn perform(&self, inout: ArchiverOpts) -> Result<()> {
match inout.destination() {
Err(e) => Err(e),
Ok(file) => {
Expand All @@ -28,7 +28,7 @@ impl Archiver for TarArchiver {
}
}
impl Archiver for TarGzArchiver{
fn perform(&self, inout: InOut) -> Result<()> {
fn perform(&self, inout: ArchiverOpts) -> Result<()> {
match inout.destination() {
Err(e) => Err(e),
Ok(file) => {
Expand All @@ -42,7 +42,7 @@ impl Archiver for TarGzArchiver{
}
}
impl Archiver for TarBz2Archiver {
fn perform(&self, inout: InOut) -> Result<()> {
fn perform(&self, inout: ArchiverOpts) -> Result<()> {
match inout.destination() {
Err(e) => Err(e),
Ok(file) => {
Expand Down
9 changes: 5 additions & 4 deletions src/archiver/zip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ use std::io::{BufReader, Write, Seek};
use time::OffsetDateTime;
use zip::write::SimpleFileOptions;
use zip::{ZipWriter, DateTime};
use crate::archiver::{Archiver, InOut, Format};

use crate::archiver::{Archiver, Format, ArchiverOpts};
use crate::cli::{ToatError, Result};

pub struct ZipArchiver {
pub(super) struct ZipArchiver {
}

impl Archiver for ZipArchiver {
fn perform(&self, inout: InOut) -> Result<()> {
fn perform(&self, inout: ArchiverOpts) -> Result<()> {
match inout.destination() {
Err(e) => Err(e),
Ok(file) => {
Expand Down Expand Up @@ -96,7 +97,7 @@ mod tests {
fn test_zip() {
run_test(|| {
let archiver = ZipArchiver{};
let inout = InOut::new(PathBuf::from("test.zip"), vec![PathBuf::from("src"), PathBuf::from("Cargo.toml")], true, true);
let inout = ArchiverOpts::create(PathBuf::from("test.zip"), 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::Zip);
Expand Down
15 changes: 11 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ pub type Result<T> = std::result::Result<T, ToatError>;
pub struct CliOpts {
#[clap(short = 'm', long = "mode", default_value_t = RunMode::Auto, value_name = "MODE", required = false, ignore_case = true, value_enum, help = "Mode of operation.")]
pub mode: RunMode,
#[clap(short = 'd', long = "dest", default_value = ".", value_name = "DEST", required = false, help = "Destination of the extraction results.")]
#[clap(short = 'd', long = "dest", default_value = ".", value_name = "DEST", required = false, help = "Destination of the extraction results (extract mode).")]
pub dest: Option<PathBuf>,
#[clap(short = 'o', long = "output", default_value = "totebag.zip", value_name = "OUTPUT", required = false, help = "Output file for the archive.")]
#[clap(short = 'o', long = "output", default_value = "totebag.zip", value_name = "OUTPUT", required = false, help = "Output file (archive mode).")]
pub output: Option<PathBuf>,
#[clap(short = 'n', long = "no-recursive", help = "No recursive mode.", default_value_t = false)]
#[clap(long = "to-archive-name-dir", help = "extract files to DEST/ARCHIVE_NAME directory (extract mode).", default_value_t = false)]
pub to_archive_name_dir: bool,
#[clap(short = 'n', long = "no-recursive", help = "No recursive directory (archive mode).", default_value_t = false)]
pub no_recursive: bool,
#[clap(short = 'v', long = "verbose", help = "Display verbose output.", default_value_t = false)]
pub verbose: bool,
Expand Down Expand Up @@ -56,12 +58,13 @@ pub enum RunMode {
Auto,
Archive,
Extract,
List,
}

#[derive(Debug)]
pub enum ToatError {
NoArgumentsGiven,
FileNotFound,
FileNotFound(PathBuf),
FileExists(PathBuf),
IOError(std::io::Error),
ArchiverError(String),
Expand Down Expand Up @@ -91,6 +94,10 @@ mod tests {
let mut cli3 = CliOpts::parse_from(&["totebag_test", "src.zip", "LICENSE.tar", "README.tar.bz2", "hoge.rar"]);
let r3 = cli3.run_mode();

Check failure on line 95 in src/cli.rs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

unused variable: `r3`
assert_eq!(cli3.run_mode().unwrap(), RunMode::Extract);

let mut cli4 = CliOpts::parse_from(&["totebag_test", "src.zip", "LICENSE.tar", "README.tar.bz2", "hoge.rar", "--mode", "list"]);
let r4 = cli3.run_mode();

Check failure on line 99 in src/cli.rs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

unused variable: `r4`
assert_eq!(cli4.run_mode().unwrap(), RunMode::List);
}

#[test]
Expand Down
100 changes: 100 additions & 0 deletions src/extractor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::path::PathBuf;

use crate::format::{find_format, Format};
use crate::cli::{Result, ToatError};
use crate::CliOpts;
use crate::verboser::{create_verboser, Verboser};

mod zip;
mod rar;
mod tar;

pub struct ExtractorOpts {
pub dest: PathBuf,
pub use_archive_name_dir: bool,
pub overwrite: bool,
pub v: Box<dyn Verboser>,
}

impl ExtractorOpts {
pub fn destination(&self, target: &PathBuf) -> PathBuf {
if self.use_archive_name_dir {
let file_name = target.file_name().unwrap().to_str().unwrap();
let ext = target.extension().unwrap().to_str().unwrap();
let dir_name = file_name.trim_end_matches(ext)
.trim_end_matches(".").to_string();
self.dest.join(dir_name)
} else {
self.dest.clone()
}
}
}

pub trait Extractor {
fn list_archives(&self, archive_file: PathBuf) -> Result<Vec<String>>;
fn perform(&self, archive_file: PathBuf, opts: &ExtractorOpts) -> Result<()>;
fn format(&self) -> Format;
}

pub fn create_extract_opts(opts: CliOpts) -> ExtractorOpts {
ExtractorOpts {
dest: opts.dest.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<Box<dyn Extractor>> {
let format = find_format(file.file_name());
match format {
Ok(format) => {
return match format {
Format::Zip => Ok(Box::new(zip::ZipExtractor{})),
Format::Rar => Ok(Box::new(rar::RarExtractor{})),
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())),
}
}
Err(msg) => Err(msg),
}
}

pub fn extractor_info(extractor: &Box<dyn Extractor>, target: &PathBuf, opts: &ExtractorOpts) -> String {
format!(
"Format: {:?}\nFile: {:?}\nDestination: {:?}",
extractor.format(),
target,
opts.dest,
)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_destination() {
let opts1 = ExtractorOpts {
dest: PathBuf::from("."),
use_archive_name_dir: true,
overwrite: false,
v: create_verboser(false),
};
let target = PathBuf::from("/tmp/archive.zip");
assert_eq!(opts1.destination(&target), PathBuf::from("./archive"));

let opts2 = ExtractorOpts {
dest: PathBuf::from("."),
use_archive_name_dir: false,
overwrite: false,
v: create_verboser(false),
};
let target = PathBuf::from("/tmp/archive.zip");
assert_eq!(opts2.destination(&target), PathBuf::from("."));
}
}
Loading

0 comments on commit 6e87cb2

Please sign in to comment.