diff --git a/Cargo.toml b/Cargo.toml index dd05ee3..7441402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,8 @@ zip = "0.5.11" # Dependency from workspace oof = { path = "./oof" } +[target.'cfg(unix)'.dependencies] +termion = "1.5.6" [profile.release] lto = true codegen-units = 1 diff --git a/src/bytes.rs b/src/bytes.rs deleted file mode 100644 index cd7e9b3..0000000 --- a/src/bytes.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::cmp; - -const UNITS: [&str; 4] = ["B", "kB", "MB", "GB"]; - -pub struct Bytes { - bytes: f64, -} - -impl Bytes { - pub fn new(bytes: u64) -> Self { - Self { - bytes: bytes as f64, - } - } -} - -impl std::fmt::Display for Bytes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let num = self.bytes; - debug_assert!(num >= 0.0); - if num < 1_f64 { - return write!(f, "{} B", num); - } - let delimiter = 1000_f64; - let exponent = cmp::min((num.ln() / 6.90775).floor() as i32, 4); - - write!(f, "{:.2} ", num / delimiter.powi(exponent))?; - write!(f, "{}", UNITS[exponent as usize]) - } -} diff --git a/src/cli.rs b/src/cli.rs index ef54fe3..0eb4cc5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -7,8 +7,6 @@ use std::{ use oof::{arg_flag, flag}; -pub const VERSION: &str = "0.1.5"; - #[derive(PartialEq, Eq, Debug)] pub enum Command { /// Files to be compressed @@ -29,7 +27,6 @@ pub enum Command { pub struct CommandInfo { pub command: Command, pub flags: oof::Flags, - // pub config: Config, // From .TOML, maybe, in the future } /// Calls parse_args_and_flags_from using std::env::args_os ( argv ) @@ -41,7 +38,6 @@ pub fn parse_args() -> crate::Result { pub struct ParsedArgs { pub command: Command, pub flags: oof::Flags, - // pub program_called: OsString, // Useful? } fn canonicalize<'a, P>(path: P) -> crate::Result @@ -54,7 +50,7 @@ where if !path.as_ref().exists() { Err(crate::Error::FileNotFound(PathBuf::from(path.as_ref()))) } else { - eprintln!("{} {}", "[ERROR]", io_err); + eprintln!("[ERROR] {}", io_err); Err(crate::Error::IoError) } } diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..2547226 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,265 @@ +use std::{ + fs, + io::Write, + path::{Path, PathBuf}, +}; + +use colored::Colorize; + +use crate::{ + cli::Command, + compressors::{ + BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor, + ZipCompressor, + }, + decompressors::{ + BzipDecompressor, DecompressionResult, Decompressor, GzipDecompressor, LzmaDecompressor, + TarDecompressor, ZipDecompressor, + }, + dialogs::Confirmation, + extension::{CompressionFormat, Extension}, + file::File, + utils, +}; + +pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> { + match command { + Command::Compress { + files, + compressed_output_path, + } => compress_files(files, &compressed_output_path, flags)?, + Command::Decompress { + files, + output_folder, + } => { + // From Option to Option<&Path> + let output_folder = output_folder.as_ref().map(|path| Path::new(path)); + for file in files.iter() { + decompress_file(file, output_folder, flags)?; + } + } + Command::ShowHelp => crate::help_command(), + Command::ShowVersion => crate::version_command(), + } + Ok(()) +} + +type BoxedCompressor = Box; +type BoxedDecompressor = Box; + +fn get_compressor(file: &File) -> crate::Result<(Option, BoxedCompressor)> { + let extension = match &file.extension { + Some(extension) => extension.clone(), + None => { + // This is reached when the output file given does not have an extension or has an unsupported one + return Err(crate::Error::MissingExtensionError(file.path.to_path_buf())); + } + }; + + // Supported first compressors: + // .tar and .zip + let first_compressor: Option> = match extension.first_ext { + Some(ext) => match ext { + CompressionFormat::Tar => Some(Box::new(TarCompressor)), + CompressionFormat::Zip => Some(Box::new(ZipCompressor)), + CompressionFormat::Bzip => Some(Box::new(BzipCompressor)), + CompressionFormat::Gzip => Some(Box::new(GzipCompressor)), + CompressionFormat::Lzma => Some(Box::new(LzmaCompressor)), + }, + None => None, + }; + + // Supported second compressors: + // any + let second_compressor: Box = match extension.second_ext { + CompressionFormat::Tar => Box::new(TarCompressor), + CompressionFormat::Zip => Box::new(ZipCompressor), + CompressionFormat::Bzip => Box::new(BzipCompressor), + CompressionFormat::Gzip => Box::new(GzipCompressor), + CompressionFormat::Lzma => Box::new(LzmaCompressor), + }; + + Ok((first_compressor, second_compressor)) +} + +fn get_decompressor(file: &File) -> crate::Result<(Option, BoxedDecompressor)> { + let extension = match &file.extension { + Some(extension) => extension.clone(), + None => { + // This block *should* be unreachable + eprintln!( + "{} reached Evaluator::get_decompressor without known extension.", + "[internal error]".red() + ); + return Err(crate::Error::InvalidInput); + } + }; + + let second_decompressor: Box = match extension.second_ext { + CompressionFormat::Tar => Box::new(TarDecompressor), + CompressionFormat::Zip => Box::new(ZipDecompressor), + CompressionFormat::Gzip => Box::new(GzipDecompressor), + CompressionFormat::Lzma => Box::new(LzmaDecompressor), + CompressionFormat::Bzip => Box::new(BzipDecompressor), + }; + + let first_decompressor: Option> = match extension.first_ext { + Some(ext) => match ext { + CompressionFormat::Tar => Some(Box::new(TarDecompressor)), + CompressionFormat::Zip => Some(Box::new(ZipDecompressor)), + _other => None, + }, + None => None, + }; + + Ok((first_decompressor, second_decompressor)) +} + +fn decompress_file_in_memory( + bytes: Vec, + file_path: &Path, + decompressor: Option>, + output_file: Option, + extension: Option, + flags: &oof::Flags, +) -> crate::Result<()> { + let output_file_path = utils::get_destination_path(&output_file); + + let file_name = file_path + .file_stem() + .map(Path::new) + .unwrap_or(output_file_path); + + if "." == file_name.as_os_str() { + // I believe this is only possible when the supplied input has a name + // of the sort `.tar` or `.zip' and no output has been supplied. + // file_name = OsStr::new("ouch-output"); + todo!("Pending review, what is this supposed to do??"); + } + + // If there is a decompressor to use, we'll create a file in-memory and decompress it + let decompressor = match decompressor { + Some(decompressor) => decompressor, + None => { + // There is no more processing to be done on the input file (or there is but currently unsupported) + // Therefore, we'll save what we have in memory into a file. + println!("{}: saving to {:?}.", "info".yellow(), file_name); + + if file_name.exists() { + let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); + if !utils::permission_for_overwriting(&file_name, flags, &confirm)? { + return Ok(()); + } + } + + let mut f = fs::File::create(output_file_path.join(file_name))?; + f.write_all(&bytes)?; + return Ok(()); + } + }; + + let file = File { + path: file_name, + contents_in_memory: Some(bytes), + extension, + }; + + let decompression_result = decompressor.decompress(file, &output_file, flags)?; + if let DecompressionResult::FileInMemory(_) = decompression_result { + unreachable!("Shouldn't"); + } + + Ok(()) +} + +fn compress_files( + files: Vec, + output_path: &Path, + flags: &oof::Flags, +) -> crate::Result<()> { + let mut output = File::from(output_path)?; + + let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); + let (first_compressor, second_compressor) = get_compressor(&output)?; + + if output_path.exists() && !utils::permission_for_overwriting(&output_path, flags, &confirm)? { + // The user does not want to overwrite the file + return Ok(()); + } + + let bytes = match first_compressor { + Some(first_compressor) => { + let mut entry = Entry::Files(files); + let bytes = first_compressor.compress(entry)?; + + output.contents_in_memory = Some(bytes); + entry = Entry::InMemory(output); + second_compressor.compress(entry)? + } + None => { + let entry = Entry::Files(files); + second_compressor.compress(entry)? + } + }; + + println!( + "{}: writing to {:?}. ({})", + "info".yellow(), + output_path, + utils::Bytes::new(bytes.len() as u64) + ); + fs::write(output_path, bytes)?; + + Ok(()) +} + +fn decompress_file( + file_path: &Path, + output: Option<&Path>, + flags: &oof::Flags, +) -> crate::Result<()> { + // The file to be decompressed + let file = File::from(file_path)?; + // The file must have a supported decompressible format + if file.extension == None { + return Err(crate::Error::MissingExtensionError(PathBuf::from( + file_path, + ))); + } + + let output = match output { + Some(inner) => Some(File::from(inner)?), + None => None, + }; + let (first_decompressor, second_decompressor) = get_decompressor(&file)?; + + let extension = file.extension.clone(); + + let decompression_result = second_decompressor.decompress(file, &output, &flags)?; + + match decompression_result { + DecompressionResult::FileInMemory(bytes) => { + // We'll now decompress a file currently in memory. + // This will currently happen in the case of .bz, .xz and .lzma + decompress_file_in_memory( + bytes, + file_path, + first_decompressor, + output, + extension, + flags, + )?; + } + DecompressionResult::FilesUnpacked(_files) => { + // If the file's last extension was an archival method, + // such as .tar, .zip or (to-do) .rar, then we won't look for + // further processing. + // The reason for this is that cases such as "file.xz.tar" are too rare + // to worry about, at least at the moment. + + // TODO: use the `files` variable for something + } + } + + Ok(()) +} diff --git a/src/compressors/bzip.rs b/src/compressors/bzip.rs index 28afd51..a08c7c9 100644 --- a/src/compressors/bzip.rs +++ b/src/compressors/bzip.rs @@ -3,20 +3,15 @@ use std::{fs, io::Write, path::PathBuf}; use colored::Colorize; use super::{Compressor, Entry}; -use crate::{ - bytes::Bytes, - extension::CompressionFormat, - file::File, - utils::{check_for_multiple_files, ensure_exists}, -}; +use crate::{extension::CompressionFormat, file::File, utils}; -pub struct BzipCompressor {} +pub struct BzipCompressor; impl BzipCompressor { fn compress_files(files: Vec, format: CompressionFormat) -> crate::Result> { - check_for_multiple_files(&files, &format)?; + utils::check_for_multiple_files(&files, &format)?; let path = &files[0]; - ensure_exists(path)?; + utils::ensure_exists(path)?; let contents = { let bytes = fs::read(path)?; Self::compress_bytes(&*bytes)? @@ -26,7 +21,7 @@ impl BzipCompressor { "{}: compressed {:?} into memory ({})", "info".yellow(), &path, - Bytes::new(contents.len() as u64) + utils::Bytes::new(contents.len() as u64) ); Ok(contents) diff --git a/src/compressors/compressor.rs b/src/compressors/compressor.rs index 77ff2b8..7773dce 100644 --- a/src/compressors/compressor.rs +++ b/src/compressors/compressor.rs @@ -2,12 +2,6 @@ use std::path::PathBuf; use crate::file::File; -// pub enum CompressionResult { -// ZipArchive(Vec), -// TarArchive(Vec), -// FileInMemory(Vec) -// } - pub enum Entry<'a> { Files(Vec), InMemory(File<'a>), diff --git a/src/compressors/gzip.rs b/src/compressors/gzip.rs index a390971..9399469 100644 --- a/src/compressors/gzip.rs +++ b/src/compressors/gzip.rs @@ -3,24 +3,19 @@ use std::{fs, io::Write, path::PathBuf}; use colored::Colorize; use super::{Compressor, Entry}; -use crate::{ - bytes::Bytes, - extension::CompressionFormat, - file::File, - utils::{check_for_multiple_files, ensure_exists}, -}; +use crate::{extension::CompressionFormat, file::File, utils}; -pub struct GzipCompressor {} +pub struct GzipCompressor; impl GzipCompressor { pub fn compress_files( files: Vec, format: CompressionFormat, ) -> crate::Result> { - check_for_multiple_files(&files, &format)?; + utils::check_for_multiple_files(&files, &format)?; let path = &files[0]; - ensure_exists(path)?; + utils::ensure_exists(path)?; let bytes = { let bytes = fs::read(path)?; @@ -31,7 +26,7 @@ impl GzipCompressor { "{}: compressed {:?} into memory ({})", "info".yellow(), &path, - Bytes::new(bytes.len() as u64) + utils::Bytes::new(bytes.len() as u64) ); Ok(bytes) diff --git a/src/compressors/lzma.rs b/src/compressors/lzma.rs index 71d93c1..d18fc0d 100644 --- a/src/compressors/lzma.rs +++ b/src/compressors/lzma.rs @@ -3,24 +3,19 @@ use std::{fs, io::Write, path::PathBuf}; use colored::Colorize; use super::{Compressor, Entry}; -use crate::{ - bytes::Bytes, - extension::CompressionFormat, - file::File, - utils::{check_for_multiple_files, ensure_exists}, -}; +use crate::{extension::CompressionFormat, file::File, utils}; -pub struct LzmaCompressor {} +pub struct LzmaCompressor; impl LzmaCompressor { pub fn compress_files( files: Vec, format: CompressionFormat, ) -> crate::Result> { - check_for_multiple_files(&files, &format)?; + utils::check_for_multiple_files(&files, &format)?; let path = &files[0]; - ensure_exists(path)?; + utils::ensure_exists(path)?; let bytes = { let bytes = fs::read(path)?; @@ -31,7 +26,7 @@ impl LzmaCompressor { "{}: compressed {:?} into memory ({})", "info".yellow(), &path, - Bytes::new(bytes.len() as u64) + utils::Bytes::new(bytes.len() as u64) ); Ok(bytes) diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs index f4c73bb..ecfed8b 100644 --- a/src/compressors/mod.rs +++ b/src/compressors/mod.rs @@ -1,3 +1,4 @@ +//! This module contains the Compressor trait and an implementor for each format. mod bzip; mod compressor; mod gzip; diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index 5de2537..0e0c417 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -7,7 +7,7 @@ use walkdir::WalkDir; use super::compressor::Entry; use crate::{compressors::Compressor, file::File, utils}; -pub struct TarCompressor {} +pub struct TarCompressor; impl TarCompressor { // TODO: implement this diff --git a/src/compressors/zip.rs b/src/compressors/zip.rs index be9f1fe..1d814d5 100644 --- a/src/compressors/zip.rs +++ b/src/compressors/zip.rs @@ -8,7 +8,7 @@ use walkdir::WalkDir; use super::compressor::Entry; use crate::{compressors::Compressor, file::File, utils}; -pub struct ZipCompressor {} +pub struct ZipCompressor; impl ZipCompressor { // TODO: this function does not seem to be working correctly ;/ diff --git a/src/decompressors/mod.rs b/src/decompressors/mod.rs index 689109e..c996f41 100644 --- a/src/decompressors/mod.rs +++ b/src/decompressors/mod.rs @@ -1,3 +1,5 @@ +//! This module contains the Decompressor trait and an implementor for each format. + mod decompressor; mod tar; mod to_memory; @@ -5,11 +7,6 @@ mod zip; pub use decompressor::{DecompressionResult, Decompressor}; -// These decompressors only decompress to memory, -// unlike {Tar, Zip}Decompressor which are capable of -// decompressing directly to storage -pub use self::{ - tar::TarDecompressor, - to_memory::{BzipDecompressor, GzipDecompressor, LzmaDecompressor}, - zip::ZipDecompressor, -}; +pub use self::to_memory::{BzipDecompressor, GzipDecompressor, LzmaDecompressor}; +// The .tar and .zip decompressors are capable of decompressing directly to storage +pub use self::{tar::TarDecompressor, zip::ZipDecompressor}; diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 5461b08..7981750 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -8,10 +8,10 @@ use colored::Colorize; use tar::{self, Archive}; use super::decompressor::{DecompressionResult, Decompressor}; -use crate::{bytes::Bytes, dialogs::Confirmation, file::File, utils}; +use crate::{dialogs::Confirmation, file::File, utils}; #[derive(Debug)] -pub struct TarDecompressor {} +pub struct TarDecompressor; impl TarDecompressor { fn unpack_files(from: File, into: &Path, flags: &oof::Flags) -> crate::Result> { @@ -48,7 +48,7 @@ impl TarDecompressor { "{}: {:?} extracted. ({})", "info".yellow(), into.join(file.path()?), - Bytes::new(file.size()) + utils::Bytes::new(file.size()) ); let file_path = fs::canonicalize(file_path)?; diff --git a/src/decompressors/to_memory.rs b/src/decompressors/to_memory.rs index d7af622..a76e060 100644 --- a/src/decompressors/to_memory.rs +++ b/src/decompressors/to_memory.rs @@ -6,14 +6,12 @@ use std::{ use colored::Colorize; use super::decompressor::{DecompressionResult, Decompressor}; -use crate::bytes::Bytes; -use crate::utils; -use crate::{extension::CompressionFormat, file::File}; +use crate::{extension::CompressionFormat, file::File, utils}; -struct DecompressorToMemory {} -pub struct GzipDecompressor {} -pub struct LzmaDecompressor {} -pub struct BzipDecompressor {} +struct DecompressorToMemory; +pub struct GzipDecompressor; +pub struct LzmaDecompressor; +pub struct BzipDecompressor; fn get_decoder<'a>( format: CompressionFormat, @@ -40,7 +38,7 @@ impl DecompressorToMemory { "{}: {:?} extracted into memory ({}).", "info".yellow(), path, - Bytes::new(bytes_read as u64) + utils::Bytes::new(bytes_read as u64) ); Ok(buffer) diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index dcf1329..ac2120c 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -8,7 +8,7 @@ use colored::Colorize; use zip::{self, read::ZipFile, ZipArchive}; use super::decompressor::{DecompressionResult, Decompressor}; -use crate::{bytes::Bytes, dialogs::Confirmation, file::File, utils}; +use crate::{dialogs::Confirmation, file::File, utils}; #[cfg(unix)] fn __unix_set_permissions(file_path: &Path, file: &ZipFile) { @@ -19,7 +19,7 @@ fn __unix_set_permissions(file_path: &Path, file: &ZipFile) { } } -pub struct ZipDecompressor {} +pub struct ZipDecompressor; impl ZipDecompressor { fn check_for_comments(file: &ZipFile) { @@ -76,7 +76,7 @@ impl ZipDecompressor { "{}: \"{}\" extracted. ({})", "info".yellow(), file_path.display(), - Bytes::new(file.size()) + utils::Bytes::new(file.size()) ); let mut output_file = fs::File::create(&file_path)?; diff --git a/src/error.rs b/src/error.rs index 6d1c547..3b7c3e0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -66,7 +66,10 @@ impl fmt::Display for Error { } Error::MissingArgumentsForCompression => { write!(f, "{} ", "[ERROR]".red())?; - write!(f,"The compress subcommands demands at least 2 arguments, see usage: ") + let spacing = " "; + writeln!(f,"The compress subcommands demands at least 2 arguments, an input file and an output file.")?; + writeln!(f,"{}Example: `ouch compress img.jpeg img.zip", spacing)?; + write!(f,"{}For more information, run `ouch --help`", spacing) } Error::InternalError => { write!(f, "{} ", "[ERROR]".red())?; diff --git a/src/evaluator.rs b/src/evaluator.rs deleted file mode 100644 index 697c0f1..0000000 --- a/src/evaluator.rs +++ /dev/null @@ -1,298 +0,0 @@ -use std::{ - fs, - io::Write, - path::{Path, PathBuf}, -}; - -use colored::Colorize; - -use crate::{ - bytes::Bytes, - cli::{Command, VERSION}, - compressors::{ - BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor, - ZipCompressor, - }, - decompressors::{ - BzipDecompressor, DecompressionResult, Decompressor, GzipDecompressor, LzmaDecompressor, - TarDecompressor, ZipDecompressor, - }, - dialogs::Confirmation, - extension::{CompressionFormat, Extension}, - file::File, - utils, -}; - -pub struct Evaluator {} - -type BoxedCompressor = Box; -type BoxedDecompressor = Box; - -impl Evaluator { - pub fn get_compressor( - file: &File, - ) -> crate::Result<(Option, BoxedCompressor)> { - let extension = match &file.extension { - Some(extension) => extension.clone(), - None => { - // This block *should* be unreachable - eprintln!( - "{} reached Evaluator::get_decompressor without known extension.", - "[internal error]".red() - ); - return Err(crate::Error::InternalError); - } - }; - - // Supported first compressors: - // .tar and .zip - let first_compressor: Option> = match extension.first_ext { - Some(ext) => match ext { - CompressionFormat::Tar => Some(Box::new(TarCompressor {})), - CompressionFormat::Zip => Some(Box::new(ZipCompressor {})), - // _other => Some(Box::new(NifflerCompressor {})), - _other => { - todo!(); - } - }, - None => None, - }; - - // Supported second compressors: - // any - let second_compressor: Box = match extension.second_ext { - CompressionFormat::Tar => Box::new(TarCompressor {}), - CompressionFormat::Zip => Box::new(ZipCompressor {}), - CompressionFormat::Bzip => Box::new(BzipCompressor {}), - CompressionFormat::Gzip => Box::new(GzipCompressor {}), - CompressionFormat::Lzma => Box::new(LzmaCompressor {}), - }; - - Ok((first_compressor, second_compressor)) - } - - pub fn get_decompressor( - file: &File, - ) -> crate::Result<(Option, BoxedDecompressor)> { - let extension = match &file.extension { - Some(extension) => extension.clone(), - None => { - // This block *should* be unreachable - eprintln!( - "{} reached Evaluator::get_decompressor without known extension.", - "[internal error]".red() - ); - return Err(crate::Error::InvalidInput); - } - }; - - let second_decompressor: Box = match extension.second_ext { - CompressionFormat::Tar => Box::new(TarDecompressor {}), - CompressionFormat::Zip => Box::new(ZipDecompressor {}), - CompressionFormat::Gzip => Box::new(GzipDecompressor {}), - CompressionFormat::Lzma => Box::new(LzmaDecompressor {}), - CompressionFormat::Bzip => Box::new(BzipDecompressor {}), - }; - - let first_decompressor: Option> = match extension.first_ext { - Some(ext) => match ext { - CompressionFormat::Tar => Some(Box::new(TarDecompressor {})), - CompressionFormat::Zip => Some(Box::new(ZipDecompressor {})), - _other => None, - }, - None => None, - }; - - Ok((first_decompressor, second_decompressor)) - } - - fn decompress_file_in_memory( - bytes: Vec, - file_path: &Path, - decompressor: Option>, - output_file: Option, - extension: Option, - flags: &oof::Flags, - ) -> crate::Result<()> { - let output_file_path = utils::get_destination_path(&output_file); - - let file_name = file_path - .file_stem() - .map(Path::new) - .unwrap_or(output_file_path); - - if "." == file_name.as_os_str() { - // I believe this is only possible when the supplied input has a name - // of the sort `.tar` or `.zip' and no output has been supplied. - // file_name = OsStr::new("ouch-output"); - todo!("Pending review, what is this supposed to do??"); - } - - // If there is a decompressor to use, we'll create a file in-memory and decompress it - let decompressor = match decompressor { - Some(decompressor) => decompressor, - None => { - // There is no more processing to be done on the input file (or there is but currently unsupported) - // Therefore, we'll save what we have in memory into a file. - println!("{}: saving to {:?}.", "info".yellow(), file_name); - - if file_name.exists() { - let confirm = - Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); - if !utils::permission_for_overwriting(&file_name, flags, &confirm)? { - return Ok(()); - } - } - - let mut f = fs::File::create(output_file_path.join(file_name))?; - f.write_all(&bytes)?; - return Ok(()); - } - }; - - let file = File { - path: file_name, - contents_in_memory: Some(bytes), - extension, - }; - - let decompression_result = decompressor.decompress(file, &output_file, flags)?; - if let DecompressionResult::FileInMemory(_) = decompression_result { - unreachable!("Shouldn't"); - } - - Ok(()) - } - - fn compress_files( - files: Vec, - output_path: &Path, - flags: &oof::Flags, - ) -> crate::Result<()> { - let mut output = File::from(output_path)?; - - let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); - let (first_compressor, second_compressor) = Self::get_compressor(&output)?; - - if output_path.exists() - && !utils::permission_for_overwriting(&output_path, flags, &confirm)? - { - // The user does not want to overwrite the file - return Ok(()); - } - - let bytes = match first_compressor { - Some(first_compressor) => { - let mut entry = Entry::Files(files); - let bytes = first_compressor.compress(entry)?; - - output.contents_in_memory = Some(bytes); - entry = Entry::InMemory(output); - second_compressor.compress(entry)? - } - None => { - let entry = Entry::Files(files); - second_compressor.compress(entry)? - } - }; - - println!( - "{}: writing to {:?}. ({})", - "info".yellow(), - output_path, - Bytes::new(bytes.len() as u64) - ); - fs::write(output_path, bytes)?; - - Ok(()) - } - - fn decompress_file( - file_path: &Path, - output: Option<&Path>, - flags: &oof::Flags, - ) -> crate::Result<()> { - // The file to be decompressed - let file = File::from(file_path)?; - // The file must have a supported decompressible format - if file.extension == None { - return Err(crate::Error::MissingExtensionError(PathBuf::from( - file_path, - ))); - } - - let output = match output { - Some(inner) => Some(File::from(inner)?), - None => None, - }; - let (first_decompressor, second_decompressor) = Self::get_decompressor(&file)?; - - let extension = file.extension.clone(); - - let decompression_result = second_decompressor.decompress(file, &output, &flags)?; - - match decompression_result { - DecompressionResult::FileInMemory(bytes) => { - // We'll now decompress a file currently in memory. - // This will currently happen in the case of .bz, .xz and .lzma - Self::decompress_file_in_memory( - bytes, - file_path, - first_decompressor, - output, - extension, - flags, - )?; - } - DecompressionResult::FilesUnpacked(_files) => { - // If the file's last extension was an archival method, - // such as .tar, .zip or (to-do) .rar, then we won't look for - // further processing. - // The reason for this is that cases such as "file.xz.tar" are too rare - // to worry about, at least at the moment. - - // TODO: use the `files` variable for something - } - } - - Ok(()) - } - - pub fn evaluate(command: Command, flags: &oof::Flags) -> crate::Result<()> { - match command { - Command::Compress { - files, - compressed_output_path, - } => Self::compress_files(files, &compressed_output_path, flags)?, - Command::Decompress { - files, - output_folder, - } => { - // From Option to Option<&Path> - let output_folder = output_folder.as_ref().map(|path| Path::new(path)); - for file in files.iter() { - Self::decompress_file(file, output_folder, flags)?; - } - } - Command::ShowHelp => help_message(), - Command::ShowVersion => version_message(), - } - Ok(()) - } -} - -#[inline] -fn version_message() { - println!("ouch {}", VERSION); -} - -fn help_message() { - version_message(); - println!("Vinícius R. M. & João M. Bezerra"); - println!("ouch is a unified compression & decompression utility"); - println!(); - println!(" COMPRESSION USAGE:"); - println!(" ouch compress output-file"); - println!("DECOMPRESSION USAGE:"); - println!(" ouch [-o/--output output-folder]"); -} diff --git a/src/extension.rs b/src/extension.rs index 43e0e1a..06de63d 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -7,7 +7,7 @@ use std::{ use CompressionFormat::*; -use crate::utils::to_utf; +use crate::utils; /// Represents the extension of a file, but only really caring about /// compression formats (and .tar). @@ -49,7 +49,7 @@ impl Extension { _ if ext == "gz" => Ok(Gzip), _ if ext == "bz" || ext == "bz2" => Ok(Bzip), _ if ext == "xz" || ext == "lz" || ext == "lzma" => Ok(Lzma), - other => Err(crate::Error::UnknownExtensionError(to_utf(other))), + other => Err(crate::Error::UnknownExtensionError(utils::to_utf(other))), }; let (first_ext, second_ext) = match get_extension_from_filename(&file_name) { @@ -86,16 +86,11 @@ impl Extension { #[derive(Clone, PartialEq, Eq, Debug)] /// Accepted extensions for input and output pub enum CompressionFormat { - // .gz - Gzip, - // .bz - Bzip, - // .lzma - Lzma, - // .tar (technically not a compression extension, but will do for now) - Tar, - // .zip - Zip, + Gzip, // .gz + Bzip, // .bz + Lzma, // .lzma + Tar, // .tar (technically not a compression extension, but will do for now) + Zip, // .zip } fn extension_from_os_str(ext: &OsStr) -> Result { diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fa9bad7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,81 @@ +// Public modules +pub mod cli; +pub mod commands; + +// Private modules +mod compressors; +mod decompressors; +mod dialogs; +mod error; +mod extension; +mod file; +mod utils; + +pub use error::{Error, Result}; + +const VERSION: &str = "0.1.5"; + +fn help_command() { + use utils::colors::*; + /* + ouch - Obvious Unified Compressed files Helper + + USAGE: + ouch Decompresses files. + + ouch compress OUTPUT.EXT Compresses files into OUTPUT.EXT, + where EXT must be a supported format. + + FLAGS: + -h, --help Display this help information. + -y, --yes Skip overwrite questions. + -n, --no Skip overwrite questions. + --version Display version information. + + SPECIFIC FLAGS: + -o, --output FOLDER_PATH When decompressing, to decompress files to + another folder. + + Visit https://github.com/vrmiguel/ouch for more usage examples. + */ + + println!( + "\ +{cyan}ouch{reset} - Obvious Unified Compression files Helper + +{cyan}USAGE:{reset} + {green}ouch {magenta}{reset} Decompresses files. + + {green}ouch compress {magenta} OUTPUT.EXT{reset} Compresses files into {magenta}OUTPUT.EXT{reset}, + where {magenta}EXT{reset} must be a supported format. + +{cyan}FLAGS:{reset} + {yellow}-h{white}, {yellow}--help{reset} Display this help information. + {yellow}-y{white}, {yellow}--yes{reset} Skip overwrite questions. + {yellow}-n{white}, {yellow}--no{reset} Skip overwrite questions. + {yellow}--version{reset} Display version information. + +{cyan}SPECIFIC FLAGS:{reset} + {yellow}-o{reset}, {yellow}--output{reset} FOLDER_PATH When decompressing, to decompress files to + another folder. + +Visit https://github.com/vrmiguel/ouch for more usage examples.", + magenta = magenta(), + white = white(), + green = green(), + yellow = yellow(), + reset = reset(), + cyan = cyan() + ); +} + +#[inline] +fn version_command() { + use utils::colors::*; + println!( + "{green}ouch{reset} {}", + crate::VERSION, + green = green(), + reset = reset(), + ); +} diff --git a/src/main.rs b/src/main.rs index 984f191..af7fc43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,7 @@ -mod bytes; -mod cli; -mod compressors; -mod decompressors; -mod dialogs; -mod error; -mod evaluator; -mod extension; -mod file; -mod test; -mod utils; - -use error::{Error, Result}; -use evaluator::Evaluator; - -use crate::cli::ParsedArgs; +use ouch::{ + cli::{parse_args, ParsedArgs}, + commands, Result, +}; fn main() { if let Err(err) = run() { @@ -23,6 +11,6 @@ fn main() { } fn run() -> crate::Result<()> { - let ParsedArgs { command, flags } = cli::parse_args()?; - Evaluator::evaluate(command, &flags) + let ParsedArgs { command, flags } = parse_args()?; + commands::run(command, &flags) } diff --git a/src/utils.rs b/src/utils.rs index 3b6d4d1..a607180 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,5 @@ use std::{ - env, + cmp, env, ffi::OsStr, fs, path::{Path, PathBuf}, @@ -25,7 +25,7 @@ macro_rules! debug { }; } -pub(crate) fn ensure_exists<'a, P>(path: P) -> crate::Result<()> +pub fn ensure_exists<'a, P>(path: P) -> crate::Result<()> where P: AsRef + 'a, { @@ -36,19 +36,26 @@ where Ok(()) } -pub(crate) fn check_for_multiple_files( +pub fn check_for_multiple_files( files: &[PathBuf], format: &CompressionFormat, ) -> crate::Result<()> { if files.len() != 1 { - eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "[ERROR]".red(), format, format); + eprintln!( + "{}: cannot compress multiple files directly to {:#?}.\n\ + Try using an intermediate archival method such as Tar.\n\ + Example: filename.tar{}", + "[ERROR]".red(), + format, + format + ); return Err(crate::Error::InvalidInput); } Ok(()) } -pub(crate) fn create_path_if_non_existent(path: &Path) -> crate::Result<()> { +pub fn create_path_if_non_existent(path: &Path) -> crate::Result<()> { if !path.exists() { println!( "{}: attempting to create folder {:?}.", @@ -65,7 +72,7 @@ pub(crate) fn create_path_if_non_existent(path: &Path) -> crate::Result<()> { Ok(()) } -pub(crate) fn get_destination_path<'a>(dest: &'a Option) -> &'a Path { +pub fn get_destination_path<'a>(dest: &'a Option) -> &'a Path { match dest { Some(output_file) => { // Must be None according to the way command-line arg. parsing in Ouch works @@ -76,7 +83,7 @@ pub(crate) fn get_destination_path<'a>(dest: &'a Option) -> &'a Path { } } -pub(crate) fn change_dir_and_return_parent(filename: &Path) -> crate::Result { +pub fn change_dir_and_return_parent(filename: &Path) -> crate::Result { let previous_location = env::current_dir()?; let parent = if let Some(parent) = filename.parent() { @@ -114,3 +121,125 @@ pub fn to_utf(os_str: impl AsRef) -> String { let text = format!("{:?}", os_str.as_ref()); text.trim_matches('"').to_string() } + +pub struct Bytes { + bytes: f64, +} + +/// Module with a list of bright colors. +#[allow(dead_code)] +#[cfg(target_family = "unix")] +pub mod colors { + use termion::color::*; + + pub fn reset() -> &'static str { + Reset.fg_str() + } + pub fn black() -> &'static str { + LightBlack.fg_str() + } + pub fn blue() -> &'static str { + LightBlue.fg_str() + } + pub fn cyan() -> &'static str { + LightCyan.fg_str() + } + pub fn green() -> &'static str { + LightGreen.fg_str() + } + pub fn magenta() -> &'static str { + LightMagenta.fg_str() + } + pub fn red() -> &'static str { + LightRed.fg_str() + } + pub fn white() -> &'static str { + LightWhite.fg_str() + } + pub fn yellow() -> &'static str { + LightYellow.fg_str() + } +} +// Termion does not support Windows +#[allow(dead_code, non_upper_case_globals)] +#[cfg(not(target_family = "unix"))] +pub mod colors { + pub fn empty() -> &'static str { + "" + } + pub const reset: fn() -> &'static str = empty; + pub const black: fn() -> &'static str = empty; + pub const blue: fn() -> &'static str = empty; + pub const cyan: fn() -> &'static str = empty; + pub const green: fn() -> &'static str = empty; + pub const magenta: fn() -> &'static str = empty; + pub const red: fn() -> &'static str = empty; + pub const white: fn() -> &'static str = empty; + pub const yellow: fn() -> &'static str = empty; +} + +impl Bytes { + const UNIT_PREFIXES: [&'static str; 6] = ["", "k", "M", "G", "T", "P"]; + + pub fn new(bytes: u64) -> Self { + Self { + bytes: bytes as f64, + } + } +} + +impl std::fmt::Display for Bytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let num = self.bytes; + debug_assert!(num >= 0.0); + if num < 1_f64 { + return write!(f, "{} B", num); + } + let delimiter = 1000_f64; + let exponent = cmp::min((num.ln() / 6.90775).floor() as i32, 4); + + write!(f, "{:.2} ", num / delimiter.powi(exponent))?; + write!(f, "{}B", Bytes::UNIT_PREFIXES[exponent as usize]) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pretty_bytes_formatting() { + fn format_bytes(bytes: u64) -> String { + format!("{}", Bytes::new(bytes)) + } + let b = 1; + let kb = b * 1000; + let mb = kb * 1000; + let gb = mb * 1000; + + assert_eq!("0 B", format_bytes(0)); // This is weird + assert_eq!("1.00 B", format_bytes(b)); + assert_eq!("999.00 B", format_bytes(b * 999)); + assert_eq!("12.00 MB", format_bytes(mb * 12)); + assert_eq!("123.00 MB", format_bytes(mb * 123)); + assert_eq!("5.50 MB", format_bytes(mb * 5 + kb * 500)); + assert_eq!("7.54 GB", format_bytes(gb * 7 + 540 * mb)); + assert_eq!("1.20 TB", format_bytes(gb * 1200)); + + // bytes + assert_eq!("234.00 B", format_bytes(234)); + assert_eq!("999.00 B", format_bytes(999)); + // kilobytes + assert_eq!("2.23 kB", format_bytes(2234)); + assert_eq!("62.50 kB", format_bytes(62500)); + assert_eq!("329.99 kB", format_bytes(329990)); + // megabytes + assert_eq!("2.75 MB", format_bytes(2750000)); + assert_eq!("55.00 MB", format_bytes(55000000)); + assert_eq!("987.65 MB", format_bytes(987654321)); + // gigabytes + assert_eq!("5.28 GB", format_bytes(5280000000)); + assert_eq!("95.20 GB", format_bytes(95200000000)); + assert_eq!("302.00 GB", format_bytes(302000000000)); + } +}