Merge pull request #20 from vrmiguel/dev

Update master from dev branch
This commit is contained in:
João Marcos Bezerra 2021-04-06 23:25:11 -03:00 committed by GitHub
commit 95ab1e763b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 537 additions and 431 deletions

View File

@ -24,6 +24,8 @@ zip = "0.5.11"
# Dependency from workspace # Dependency from workspace
oof = { path = "./oof" } oof = { path = "./oof" }
[target.'cfg(unix)'.dependencies]
termion = "1.5.6"
[profile.release] [profile.release]
lto = true lto = true
codegen-units = 1 codegen-units = 1

View File

@ -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])
}
}

View File

@ -7,8 +7,6 @@ use std::{
use oof::{arg_flag, flag}; use oof::{arg_flag, flag};
pub const VERSION: &str = "0.1.5";
#[derive(PartialEq, Eq, Debug)] #[derive(PartialEq, Eq, Debug)]
pub enum Command { pub enum Command {
/// Files to be compressed /// Files to be compressed
@ -29,7 +27,6 @@ pub enum Command {
pub struct CommandInfo { pub struct CommandInfo {
pub command: Command, pub command: Command,
pub flags: oof::Flags, 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 ) /// Calls parse_args_and_flags_from using std::env::args_os ( argv )
@ -41,7 +38,6 @@ pub fn parse_args() -> crate::Result<ParsedArgs> {
pub struct ParsedArgs { pub struct ParsedArgs {
pub command: Command, pub command: Command,
pub flags: oof::Flags, pub flags: oof::Flags,
// pub program_called: OsString, // Useful?
} }
fn canonicalize<'a, P>(path: P) -> crate::Result<PathBuf> fn canonicalize<'a, P>(path: P) -> crate::Result<PathBuf>
@ -54,7 +50,7 @@ where
if !path.as_ref().exists() { if !path.as_ref().exists() {
Err(crate::Error::FileNotFound(PathBuf::from(path.as_ref()))) Err(crate::Error::FileNotFound(PathBuf::from(path.as_ref())))
} else { } else {
eprintln!("{} {}", "[ERROR]", io_err); eprintln!("[ERROR] {}", io_err);
Err(crate::Error::IoError) Err(crate::Error::IoError)
} }
} }

265
src/commands.rs Normal file
View File

@ -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<PathBuf> 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<dyn Compressor>;
type BoxedDecompressor = Box<dyn Decompressor>;
fn get_compressor(file: &File) -> crate::Result<(Option<BoxedCompressor>, 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<Box<dyn Compressor>> = 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<dyn Compressor> = 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>, 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<dyn Decompressor> = 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<Box<dyn Decompressor>> = 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<u8>,
file_path: &Path,
decompressor: Option<Box<dyn Decompressor>>,
output_file: Option<File>,
extension: Option<Extension>,
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<PathBuf>,
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(())
}

View File

@ -3,20 +3,15 @@ use std::{fs, io::Write, path::PathBuf};
use colored::Colorize; use colored::Colorize;
use super::{Compressor, Entry}; use super::{Compressor, Entry};
use crate::{ use crate::{extension::CompressionFormat, file::File, utils};
bytes::Bytes,
extension::CompressionFormat,
file::File,
utils::{check_for_multiple_files, ensure_exists},
};
pub struct BzipCompressor {} pub struct BzipCompressor;
impl BzipCompressor { impl BzipCompressor {
fn compress_files(files: Vec<PathBuf>, format: CompressionFormat) -> crate::Result<Vec<u8>> { fn compress_files(files: Vec<PathBuf>, format: CompressionFormat) -> crate::Result<Vec<u8>> {
check_for_multiple_files(&files, &format)?; utils::check_for_multiple_files(&files, &format)?;
let path = &files[0]; let path = &files[0];
ensure_exists(path)?; utils::ensure_exists(path)?;
let contents = { let contents = {
let bytes = fs::read(path)?; let bytes = fs::read(path)?;
Self::compress_bytes(&*bytes)? Self::compress_bytes(&*bytes)?
@ -26,7 +21,7 @@ impl BzipCompressor {
"{}: compressed {:?} into memory ({})", "{}: compressed {:?} into memory ({})",
"info".yellow(), "info".yellow(),
&path, &path,
Bytes::new(contents.len() as u64) utils::Bytes::new(contents.len() as u64)
); );
Ok(contents) Ok(contents)

View File

@ -2,12 +2,6 @@ use std::path::PathBuf;
use crate::file::File; use crate::file::File;
// pub enum CompressionResult {
// ZipArchive(Vec<u8>),
// TarArchive(Vec<u8>),
// FileInMemory(Vec<u8>)
// }
pub enum Entry<'a> { pub enum Entry<'a> {
Files(Vec<PathBuf>), Files(Vec<PathBuf>),
InMemory(File<'a>), InMemory(File<'a>),

View File

@ -3,24 +3,19 @@ use std::{fs, io::Write, path::PathBuf};
use colored::Colorize; use colored::Colorize;
use super::{Compressor, Entry}; use super::{Compressor, Entry};
use crate::{ use crate::{extension::CompressionFormat, file::File, utils};
bytes::Bytes,
extension::CompressionFormat,
file::File,
utils::{check_for_multiple_files, ensure_exists},
};
pub struct GzipCompressor {} pub struct GzipCompressor;
impl GzipCompressor { impl GzipCompressor {
pub fn compress_files( pub fn compress_files(
files: Vec<PathBuf>, files: Vec<PathBuf>,
format: CompressionFormat, format: CompressionFormat,
) -> crate::Result<Vec<u8>> { ) -> crate::Result<Vec<u8>> {
check_for_multiple_files(&files, &format)?; utils::check_for_multiple_files(&files, &format)?;
let path = &files[0]; let path = &files[0];
ensure_exists(path)?; utils::ensure_exists(path)?;
let bytes = { let bytes = {
let bytes = fs::read(path)?; let bytes = fs::read(path)?;
@ -31,7 +26,7 @@ impl GzipCompressor {
"{}: compressed {:?} into memory ({})", "{}: compressed {:?} into memory ({})",
"info".yellow(), "info".yellow(),
&path, &path,
Bytes::new(bytes.len() as u64) utils::Bytes::new(bytes.len() as u64)
); );
Ok(bytes) Ok(bytes)

View File

@ -3,24 +3,19 @@ use std::{fs, io::Write, path::PathBuf};
use colored::Colorize; use colored::Colorize;
use super::{Compressor, Entry}; use super::{Compressor, Entry};
use crate::{ use crate::{extension::CompressionFormat, file::File, utils};
bytes::Bytes,
extension::CompressionFormat,
file::File,
utils::{check_for_multiple_files, ensure_exists},
};
pub struct LzmaCompressor {} pub struct LzmaCompressor;
impl LzmaCompressor { impl LzmaCompressor {
pub fn compress_files( pub fn compress_files(
files: Vec<PathBuf>, files: Vec<PathBuf>,
format: CompressionFormat, format: CompressionFormat,
) -> crate::Result<Vec<u8>> { ) -> crate::Result<Vec<u8>> {
check_for_multiple_files(&files, &format)?; utils::check_for_multiple_files(&files, &format)?;
let path = &files[0]; let path = &files[0];
ensure_exists(path)?; utils::ensure_exists(path)?;
let bytes = { let bytes = {
let bytes = fs::read(path)?; let bytes = fs::read(path)?;
@ -31,7 +26,7 @@ impl LzmaCompressor {
"{}: compressed {:?} into memory ({})", "{}: compressed {:?} into memory ({})",
"info".yellow(), "info".yellow(),
&path, &path,
Bytes::new(bytes.len() as u64) utils::Bytes::new(bytes.len() as u64)
); );
Ok(bytes) Ok(bytes)

View File

@ -1,3 +1,4 @@
//! This module contains the Compressor trait and an implementor for each format.
mod bzip; mod bzip;
mod compressor; mod compressor;
mod gzip; mod gzip;

View File

@ -7,7 +7,7 @@ use walkdir::WalkDir;
use super::compressor::Entry; use super::compressor::Entry;
use crate::{compressors::Compressor, file::File, utils}; use crate::{compressors::Compressor, file::File, utils};
pub struct TarCompressor {} pub struct TarCompressor;
impl TarCompressor { impl TarCompressor {
// TODO: implement this // TODO: implement this

View File

@ -8,7 +8,7 @@ use walkdir::WalkDir;
use super::compressor::Entry; use super::compressor::Entry;
use crate::{compressors::Compressor, file::File, utils}; use crate::{compressors::Compressor, file::File, utils};
pub struct ZipCompressor {} pub struct ZipCompressor;
impl ZipCompressor { impl ZipCompressor {
// TODO: this function does not seem to be working correctly ;/ // TODO: this function does not seem to be working correctly ;/

View File

@ -1,3 +1,5 @@
//! This module contains the Decompressor trait and an implementor for each format.
mod decompressor; mod decompressor;
mod tar; mod tar;
mod to_memory; mod to_memory;
@ -5,11 +7,6 @@ mod zip;
pub use decompressor::{DecompressionResult, Decompressor}; pub use decompressor::{DecompressionResult, Decompressor};
// These decompressors only decompress to memory, pub use self::to_memory::{BzipDecompressor, GzipDecompressor, LzmaDecompressor};
// unlike {Tar, Zip}Decompressor which are capable of // The .tar and .zip decompressors are capable of decompressing directly to storage
// decompressing directly to storage pub use self::{tar::TarDecompressor, zip::ZipDecompressor};
pub use self::{
tar::TarDecompressor,
to_memory::{BzipDecompressor, GzipDecompressor, LzmaDecompressor},
zip::ZipDecompressor,
};

View File

@ -8,10 +8,10 @@ use colored::Colorize;
use tar::{self, Archive}; use tar::{self, Archive};
use super::decompressor::{DecompressionResult, Decompressor}; use super::decompressor::{DecompressionResult, Decompressor};
use crate::{bytes::Bytes, dialogs::Confirmation, file::File, utils}; use crate::{dialogs::Confirmation, file::File, utils};
#[derive(Debug)] #[derive(Debug)]
pub struct TarDecompressor {} pub struct TarDecompressor;
impl TarDecompressor { impl TarDecompressor {
fn unpack_files(from: File, into: &Path, flags: &oof::Flags) -> crate::Result<Vec<PathBuf>> { fn unpack_files(from: File, into: &Path, flags: &oof::Flags) -> crate::Result<Vec<PathBuf>> {
@ -48,7 +48,7 @@ impl TarDecompressor {
"{}: {:?} extracted. ({})", "{}: {:?} extracted. ({})",
"info".yellow(), "info".yellow(),
into.join(file.path()?), into.join(file.path()?),
Bytes::new(file.size()) utils::Bytes::new(file.size())
); );
let file_path = fs::canonicalize(file_path)?; let file_path = fs::canonicalize(file_path)?;

View File

@ -6,14 +6,12 @@ use std::{
use colored::Colorize; use colored::Colorize;
use super::decompressor::{DecompressionResult, Decompressor}; use super::decompressor::{DecompressionResult, Decompressor};
use crate::bytes::Bytes; use crate::{extension::CompressionFormat, file::File, utils};
use crate::utils;
use crate::{extension::CompressionFormat, file::File};
struct DecompressorToMemory {} struct DecompressorToMemory;
pub struct GzipDecompressor {} pub struct GzipDecompressor;
pub struct LzmaDecompressor {} pub struct LzmaDecompressor;
pub struct BzipDecompressor {} pub struct BzipDecompressor;
fn get_decoder<'a>( fn get_decoder<'a>(
format: CompressionFormat, format: CompressionFormat,
@ -40,7 +38,7 @@ impl DecompressorToMemory {
"{}: {:?} extracted into memory ({}).", "{}: {:?} extracted into memory ({}).",
"info".yellow(), "info".yellow(),
path, path,
Bytes::new(bytes_read as u64) utils::Bytes::new(bytes_read as u64)
); );
Ok(buffer) Ok(buffer)

View File

@ -8,7 +8,7 @@ use colored::Colorize;
use zip::{self, read::ZipFile, ZipArchive}; use zip::{self, read::ZipFile, ZipArchive};
use super::decompressor::{DecompressionResult, Decompressor}; use super::decompressor::{DecompressionResult, Decompressor};
use crate::{bytes::Bytes, dialogs::Confirmation, file::File, utils}; use crate::{dialogs::Confirmation, file::File, utils};
#[cfg(unix)] #[cfg(unix)]
fn __unix_set_permissions(file_path: &Path, file: &ZipFile) { 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 { impl ZipDecompressor {
fn check_for_comments(file: &ZipFile) { fn check_for_comments(file: &ZipFile) {
@ -76,7 +76,7 @@ impl ZipDecompressor {
"{}: \"{}\" extracted. ({})", "{}: \"{}\" extracted. ({})",
"info".yellow(), "info".yellow(),
file_path.display(), file_path.display(),
Bytes::new(file.size()) utils::Bytes::new(file.size())
); );
let mut output_file = fs::File::create(&file_path)?; let mut output_file = fs::File::create(&file_path)?;

View File

@ -66,7 +66,10 @@ impl fmt::Display for Error {
} }
Error::MissingArgumentsForCompression => { Error::MissingArgumentsForCompression => {
write!(f, "{} ", "[ERROR]".red())?; write!(f, "{} ", "[ERROR]".red())?;
write!(f,"The compress subcommands demands at least 2 arguments, see usage: <TODO-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 => { Error::InternalError => {
write!(f, "{} ", "[ERROR]".red())?; write!(f, "{} ", "[ERROR]".red())?;

View File

@ -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<dyn Compressor>;
type BoxedDecompressor = Box<dyn Decompressor>;
impl Evaluator {
pub fn get_compressor(
file: &File,
) -> crate::Result<(Option<BoxedCompressor>, 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<Box<dyn Compressor>> = 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<dyn Compressor> = 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>, 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<dyn Decompressor> = 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<Box<dyn Decompressor>> = 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<u8>,
file_path: &Path,
decompressor: Option<Box<dyn Decompressor>>,
output_file: Option<File>,
extension: Option<Extension>,
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<PathBuf>,
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<PathBuf> 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 <input...> output-file");
println!("DECOMPRESSION USAGE:");
println!(" ouch <input> [-o/--output output-folder]");
}

View File

@ -7,7 +7,7 @@ use std::{
use CompressionFormat::*; use CompressionFormat::*;
use crate::utils::to_utf; use crate::utils;
/// Represents the extension of a file, but only really caring about /// Represents the extension of a file, but only really caring about
/// compression formats (and .tar). /// compression formats (and .tar).
@ -49,7 +49,7 @@ impl Extension {
_ if ext == "gz" => Ok(Gzip), _ if ext == "gz" => Ok(Gzip),
_ if ext == "bz" || ext == "bz2" => Ok(Bzip), _ if ext == "bz" || ext == "bz2" => Ok(Bzip),
_ if ext == "xz" || ext == "lz" || ext == "lzma" => Ok(Lzma), _ 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) { let (first_ext, second_ext) = match get_extension_from_filename(&file_name) {
@ -86,16 +86,11 @@ impl Extension {
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
/// Accepted extensions for input and output /// Accepted extensions for input and output
pub enum CompressionFormat { pub enum CompressionFormat {
// .gz Gzip, // .gz
Gzip, Bzip, // .bz
// .bz Lzma, // .lzma
Bzip, Tar, // .tar (technically not a compression extension, but will do for now)
// .lzma Zip, // .zip
Lzma,
// .tar (technically not a compression extension, but will do for now)
Tar,
// .zip
Zip,
} }
fn extension_from_os_str(ext: &OsStr) -> Result<CompressionFormat, crate::Error> { fn extension_from_os_str(ext: &OsStr) -> Result<CompressionFormat, crate::Error> {

81
src/lib.rs Normal file
View File

@ -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 <files...> Decompresses files.
ouch compress <files...> 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}<files...>{reset} Decompresses files.
{green}ouch compress {magenta}<files...> 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(),
);
}

View File

@ -1,19 +1,7 @@
mod bytes; use ouch::{
mod cli; cli::{parse_args, ParsedArgs},
mod compressors; commands, Result,
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;
fn main() { fn main() {
if let Err(err) = run() { if let Err(err) = run() {
@ -23,6 +11,6 @@ fn main() {
} }
fn run() -> crate::Result<()> { fn run() -> crate::Result<()> {
let ParsedArgs { command, flags } = cli::parse_args()?; let ParsedArgs { command, flags } = parse_args()?;
Evaluator::evaluate(command, &flags) commands::run(command, &flags)
} }

View File

@ -1,5 +1,5 @@
use std::{ use std::{
env, cmp, env,
ffi::OsStr, ffi::OsStr,
fs, fs,
path::{Path, PathBuf}, 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 where
P: AsRef<Path> + 'a, P: AsRef<Path> + 'a,
{ {
@ -36,19 +36,26 @@ where
Ok(()) Ok(())
} }
pub(crate) fn check_for_multiple_files( pub fn check_for_multiple_files(
files: &[PathBuf], files: &[PathBuf],
format: &CompressionFormat, format: &CompressionFormat,
) -> crate::Result<()> { ) -> crate::Result<()> {
if files.len() != 1 { 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); return Err(crate::Error::InvalidInput);
} }
Ok(()) 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() { if !path.exists() {
println!( println!(
"{}: attempting to create folder {:?}.", "{}: attempting to create folder {:?}.",
@ -65,7 +72,7 @@ pub(crate) fn create_path_if_non_existent(path: &Path) -> crate::Result<()> {
Ok(()) Ok(())
} }
pub(crate) fn get_destination_path<'a>(dest: &'a Option<File>) -> &'a Path { pub fn get_destination_path<'a>(dest: &'a Option<File>) -> &'a Path {
match dest { match dest {
Some(output_file) => { Some(output_file) => {
// Must be None according to the way command-line arg. parsing in Ouch works // 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<File>) -> &'a Path {
} }
} }
pub(crate) fn change_dir_and_return_parent(filename: &Path) -> crate::Result<PathBuf> { pub fn change_dir_and_return_parent(filename: &Path) -> crate::Result<PathBuf> {
let previous_location = env::current_dir()?; let previous_location = env::current_dir()?;
let parent = if let Some(parent) = filename.parent() { let parent = if let Some(parent) = filename.parent() {
@ -114,3 +121,125 @@ pub fn to_utf(os_str: impl AsRef<OsStr>) -> String {
let text = format!("{:?}", os_str.as_ref()); let text = format!("{:?}", os_str.as_ref());
text.trim_matches('"').to_string() 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));
}
}