Merge branch 'main' into feature/add-concurrent-working-threads-option

This commit is contained in:
João Marcos 2024-12-14 19:23:14 -03:00 committed by GitHub
commit 86b44a2605
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 115 additions and 53 deletions

View File

@ -24,6 +24,7 @@ Categories Used:
- Add multithreading support for `zstd` compression [\#689](https://github.com/ouch-org/ouch/pull/689) ([nalabrie](https://github.com/nalabrie)) - Add multithreading support for `zstd` compression [\#689](https://github.com/ouch-org/ouch/pull/689) ([nalabrie](https://github.com/nalabrie))
- Add `bzip3` support [\#522](https://github.com/ouch-org/ouch/pull/522) ([freijon](https://github.com/freijon)) - Add `bzip3` support [\#522](https://github.com/ouch-org/ouch/pull/522) ([freijon](https://github.com/freijon))
- Add `--remove` flag for decompression subcommand to remove files after successful decompression [\#757](https://github.com/ouch-org/ouch/pull/757) ([ttys3](https://github.com/ttys3))
### Bug Fixes ### Bug Fixes

View File

@ -92,6 +92,10 @@ pub enum Subcommand {
/// Place results in a directory other than the current one /// Place results in a directory other than the current one
#[arg(short = 'd', long = "dir", value_hint = ValueHint::FilePath)] #[arg(short = 'd', long = "dir", value_hint = ValueHint::FilePath)]
output_dir: Option<PathBuf>, output_dir: Option<PathBuf>,
/// Remove the source file after successful decompression
#[arg(short = 'r', long)]
remove: bool,
}, },
/// List contents of an archive /// List contents of an archive
#[command(visible_aliases = ["l", "ls"])] #[command(visible_aliases = ["l", "ls"])]
@ -147,6 +151,7 @@ mod tests {
// Put a crazy value here so no test can assert it unintentionally // Put a crazy value here so no test can assert it unintentionally
files: vec!["\x00\x11\x22".into()], files: vec!["\x00\x11\x22".into()],
output_dir: None, output_dir: None,
remove: false,
}, },
} }
} }
@ -159,6 +164,7 @@ mod tests {
cmd: Subcommand::Decompress { cmd: Subcommand::Decompress {
files: to_paths(["file.tar.gz"]), files: to_paths(["file.tar.gz"]),
output_dir: None, output_dir: None,
remove: false,
}, },
..mock_cli_args() ..mock_cli_args()
} }
@ -169,6 +175,7 @@ mod tests {
cmd: Subcommand::Decompress { cmd: Subcommand::Decompress {
files: to_paths(["file.tar.gz"]), files: to_paths(["file.tar.gz"]),
output_dir: None, output_dir: None,
remove: false,
}, },
..mock_cli_args() ..mock_cli_args()
} }
@ -179,6 +186,7 @@ mod tests {
cmd: Subcommand::Decompress { cmd: Subcommand::Decompress {
files: to_paths(["a", "b", "c"]), files: to_paths(["a", "b", "c"]),
output_dir: None, output_dir: None,
remove: false,
}, },
..mock_cli_args() ..mock_cli_args()
} }

View File

@ -14,8 +14,11 @@ use crate::{
Extension, Extension,
}, },
utils::{ utils::{
self, io::lock_and_flush_output_stdio, is_path_stdin, logger::info_accessible, nice_directory_display, self,
user_wants_to_continue, io::lock_and_flush_output_stdio,
is_path_stdin,
logger::{info, info_accessible},
nice_directory_display, user_wants_to_continue,
}, },
QuestionAction, QuestionPolicy, BUFFER_CAPACITY, QuestionAction, QuestionPolicy, BUFFER_CAPACITY,
}; };
@ -23,23 +26,26 @@ use crate::{
trait ReadSeek: Read + io::Seek {} trait ReadSeek: Read + io::Seek {}
impl<T: Read + io::Seek> ReadSeek for T {} impl<T: Read + io::Seek> ReadSeek for T {}
pub struct DecompressOptions<'a> {
pub input_file_path: &'a Path,
pub formats: Vec<Extension>,
pub output_dir: &'a Path,
pub output_file_path: PathBuf,
pub question_policy: QuestionPolicy,
pub quiet: bool,
pub password: Option<&'a [u8]>,
pub remove: bool,
}
/// Decompress a file /// Decompress a file
/// ///
/// File at input_file_path is opened for reading, example: "archive.tar.gz" /// File at input_file_path is opened for reading, example: "archive.tar.gz"
/// formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order) /// formats contains each format necessary for decompression, example: [Gz, Tar] (in decompression order)
/// output_dir it's where the file will be decompressed to, this function assumes that the directory exists /// output_dir it's where the file will be decompressed to, this function assumes that the directory exists
/// output_file_path is only used when extracting single file formats, not archive formats like .tar or .zip /// output_file_path is only used when extracting single file formats, not archive formats like .tar or .zip
pub fn decompress_file( pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
input_file_path: &Path, assert!(options.output_dir.exists());
formats: Vec<Extension>, let input_is_stdin = is_path_stdin(options.input_file_path);
output_dir: &Path,
output_file_path: PathBuf,
question_policy: QuestionPolicy,
quiet: bool,
password: Option<&[u8]>,
) -> crate::Result<()> {
assert!(output_dir.exists());
let input_is_stdin = is_path_stdin(input_file_path);
// Zip archives are special, because they require io::Seek, so it requires it's logic separated // Zip archives are special, because they require io::Seek, so it requires it's logic separated
// from decoder chaining. // from decoder chaining.
@ -51,7 +57,7 @@ pub fn decompress_file(
if let [Extension { if let [Extension {
compression_formats: [Zip], compression_formats: [Zip],
.. ..
}] = formats.as_slice() }] = options.formats.as_slice()
{ {
let mut vec = vec![]; let mut vec = vec![];
let reader: Box<dyn ReadSeek> = if input_is_stdin { let reader: Box<dyn ReadSeek> = if input_is_stdin {
@ -59,14 +65,14 @@ pub fn decompress_file(
io::copy(&mut io::stdin(), &mut vec)?; io::copy(&mut io::stdin(), &mut vec)?;
Box::new(io::Cursor::new(vec)) Box::new(io::Cursor::new(vec))
} else { } else {
Box::new(fs::File::open(input_file_path)?) Box::new(fs::File::open(options.input_file_path)?)
}; };
let zip_archive = zip::ZipArchive::new(reader)?; let zip_archive = zip::ZipArchive::new(reader)?;
let files_unpacked = if let ControlFlow::Continue(files) = smart_unpack( let files_unpacked = if let ControlFlow::Continue(files) = smart_unpack(
|output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, password, quiet), |output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, options.password, options.quiet),
output_dir, options.output_dir,
&output_file_path, &options.output_file_path,
question_policy, options.question_policy,
)? { )? {
files files
} else { } else {
@ -79,10 +85,18 @@ pub fn decompress_file(
// about whether the command succeeded without such a message // about whether the command succeeded without such a message
info_accessible(format!( info_accessible(format!(
"Successfully decompressed archive in {} ({} files)", "Successfully decompressed archive in {} ({} files)",
nice_directory_display(output_dir), nice_directory_display(options.output_dir),
files_unpacked files_unpacked
)); ));
if !input_is_stdin && options.remove {
fs::remove_file(options.input_file_path)?;
info(format!(
"Removed input file {}",
nice_directory_display(options.input_file_path)
));
}
return Ok(()); return Ok(());
} }
@ -90,7 +104,7 @@ pub fn decompress_file(
let reader: Box<dyn Read> = if input_is_stdin { let reader: Box<dyn Read> = if input_is_stdin {
Box::new(io::stdin()) Box::new(io::stdin())
} else { } else {
Box::new(fs::File::open(input_file_path)?) Box::new(fs::File::open(options.input_file_path)?)
}; };
let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader); let reader = BufReader::with_capacity(BUFFER_CAPACITY, reader);
let mut reader: Box<dyn Read> = Box::new(reader); let mut reader: Box<dyn Read> = Box::new(reader);
@ -110,7 +124,7 @@ pub fn decompress_file(
Ok(decoder) Ok(decoder)
}; };
let (first_extension, extensions) = split_first_compression_format(&formats); let (first_extension, extensions) = split_first_compression_format(&options.formats);
for format in extensions.iter().rev() { for format in extensions.iter().rev() {
reader = chain_reader_decoder(format, reader)?; reader = chain_reader_decoder(format, reader)?;
@ -120,7 +134,7 @@ pub fn decompress_file(
Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd => { Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd => {
reader = chain_reader_decoder(&first_extension, reader)?; reader = chain_reader_decoder(&first_extension, reader)?;
let mut writer = match utils::ask_to_create_file(&output_file_path, question_policy)? { let mut writer = match utils::ask_to_create_file(&options.output_file_path, options.question_policy)? {
Some(file) => file, Some(file) => file,
None => return Ok(()), None => return Ok(()),
}; };
@ -131,10 +145,10 @@ pub fn decompress_file(
} }
Tar => { Tar => {
if let ControlFlow::Continue(files) = smart_unpack( if let ControlFlow::Continue(files) = smart_unpack(
|output_dir| crate::archive::tar::unpack_archive(reader, output_dir, quiet), |output_dir| crate::archive::tar::unpack_archive(reader, output_dir, options.quiet),
output_dir, options.output_dir,
&output_file_path, &options.output_file_path,
question_policy, options.question_policy,
)? { )? {
files files
} else { } else {
@ -142,13 +156,17 @@ pub fn decompress_file(
} }
} }
Zip => { Zip => {
if formats.len() > 1 { if options.formats.len() > 1 {
// Locking necessary to guarantee that warning and question // Locking necessary to guarantee that warning and question
// messages stay adjacent // messages stay adjacent
let _locks = lock_and_flush_output_stdio(); let _locks = lock_and_flush_output_stdio();
warn_user_about_loading_zip_in_memory(); warn_user_about_loading_zip_in_memory();
if !user_wants_to_continue(input_file_path, question_policy, QuestionAction::Decompression)? { if !user_wants_to_continue(
options.input_file_path,
options.question_policy,
QuestionAction::Decompression,
)? {
return Ok(()); return Ok(());
} }
} }
@ -158,10 +176,12 @@ pub fn decompress_file(
let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?; let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
if let ControlFlow::Continue(files) = smart_unpack( if let ControlFlow::Continue(files) = smart_unpack(
|output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, password, quiet), |output_dir| {
output_dir, crate::archive::zip::unpack_archive(zip_archive, output_dir, options.password, options.quiet)
&output_file_path, },
question_policy, options.output_dir,
&options.output_file_path,
options.question_policy,
)? { )? {
files files
} else { } else {
@ -171,19 +191,29 @@ pub fn decompress_file(
#[cfg(feature = "unrar")] #[cfg(feature = "unrar")]
Rar => { Rar => {
type UnpackResult = crate::Result<usize>; type UnpackResult = crate::Result<usize>;
let unpack_fn: Box<dyn FnOnce(&Path) -> UnpackResult> = if formats.len() > 1 || input_is_stdin { let unpack_fn: Box<dyn FnOnce(&Path) -> UnpackResult> = if options.formats.len() > 1 || input_is_stdin {
let mut temp_file = tempfile::NamedTempFile::new()?; let mut temp_file = tempfile::NamedTempFile::new()?;
io::copy(&mut reader, &mut temp_file)?; io::copy(&mut reader, &mut temp_file)?;
Box::new(move |output_dir| { Box::new(move |output_dir| {
crate::archive::rar::unpack_archive(temp_file.path(), output_dir, password, quiet) crate::archive::rar::unpack_archive(temp_file.path(), output_dir, options.password, options.quiet)
}) })
} else { } else {
Box::new(|output_dir| crate::archive::rar::unpack_archive(input_file_path, output_dir, password, quiet)) Box::new(|output_dir| {
crate::archive::rar::unpack_archive(
options.input_file_path,
output_dir,
options.password,
options.quiet,
)
})
}; };
if let ControlFlow::Continue(files) = if let ControlFlow::Continue(files) = smart_unpack(
smart_unpack(unpack_fn, output_dir, &output_file_path, question_policy)? unpack_fn,
{ options.output_dir,
&options.output_file_path,
options.question_policy,
)? {
files files
} else { } else {
return Ok(()); return Ok(());
@ -194,13 +224,17 @@ pub fn decompress_file(
return Err(crate::archive::rar_stub::no_support()); return Err(crate::archive::rar_stub::no_support());
} }
SevenZip => { SevenZip => {
if formats.len() > 1 { if options.formats.len() > 1 {
// Locking necessary to guarantee that warning and question // Locking necessary to guarantee that warning and question
// messages stay adjacent // messages stay adjacent
let _locks = lock_and_flush_output_stdio(); let _locks = lock_and_flush_output_stdio();
warn_user_about_loading_sevenz_in_memory(); warn_user_about_loading_sevenz_in_memory();
if !user_wants_to_continue(input_file_path, question_policy, QuestionAction::Decompression)? { if !user_wants_to_continue(
options.input_file_path,
options.question_policy,
QuestionAction::Decompression,
)? {
return Ok(()); return Ok(());
} }
} }
@ -210,11 +244,16 @@ pub fn decompress_file(
if let ControlFlow::Continue(files) = smart_unpack( if let ControlFlow::Continue(files) = smart_unpack(
|output_dir| { |output_dir| {
crate::archive::sevenz::decompress_sevenz(io::Cursor::new(vec), output_dir, password, quiet) crate::archive::sevenz::decompress_sevenz(
}, io::Cursor::new(vec),
output_dir, output_dir,
&output_file_path, options.password,
question_policy, options.quiet,
)
},
options.output_dir,
&options.output_file_path,
options.question_policy,
)? { )? {
files files
} else { } else {
@ -229,10 +268,18 @@ pub fn decompress_file(
// about whether the command succeeded without such a message // about whether the command succeeded without such a message
info_accessible(format!( info_accessible(format!(
"Successfully decompressed archive in {}", "Successfully decompressed archive in {}",
nice_directory_display(output_dir) nice_directory_display(options.output_dir)
)); ));
info_accessible(format!("Files unpacked: {}", files_unpacked)); info_accessible(format!("Files unpacked: {}", files_unpacked));
if !input_is_stdin && options.remove {
fs::remove_file(options.input_file_path)?;
info(format!(
"Removed input file {}",
nice_directory_display(options.input_file_path)
));
}
Ok(()) Ok(())
} }

View File

@ -7,6 +7,7 @@ mod list;
use std::{ops::ControlFlow, path::PathBuf}; use std::{ops::ControlFlow, path::PathBuf};
use bstr::ByteSlice; use bstr::ByteSlice;
use decompress::DecompressOptions;
use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
use utils::colors; use utils::colors;
@ -141,7 +142,11 @@ pub fn run(
compress_result.map(|_| ()) compress_result.map(|_| ())
} }
Subcommand::Decompress { files, output_dir } => { Subcommand::Decompress {
files,
output_dir,
remove,
} => {
let mut output_paths = vec![]; let mut output_paths = vec![];
let mut formats = vec![]; let mut formats = vec![];
@ -189,17 +194,18 @@ pub fn run(
} else { } else {
output_dir.join(file_name) output_dir.join(file_name)
}; };
decompress_file( decompress_file(DecompressOptions {
input_path, input_file_path: input_path,
formats, formats,
&output_dir, output_dir: &output_dir,
output_file_path, output_file_path,
question_policy, question_policy,
args.quiet, quiet: args.quiet,
args.password.as_deref().map(|str| { password: args.password.as_deref().map(|str| {
<[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed") <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed")
}), }),
) remove,
})
}) })
} }
Subcommand::List { archives: files, tree } => { Subcommand::List { archives: files, tree } => {