From 55aa05b631cd7348ed99fe71c547242f34b8a087 Mon Sep 17 00:00:00 2001 From: ttyS3 Date: Sat, 30 Nov 2024 16:00:50 +0000 Subject: [PATCH 1/6] feat(cli): add option to remove source file after decompression --- src/cli/args.rs | 8 ++++++++ src/commands/decompress.rs | 24 ++++++++++++++++++++++-- src/commands/mod.rs | 14 +++++++++++++- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index 05b522c..c07e85c 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -88,6 +88,10 @@ pub enum Subcommand { /// Place results in a directory other than the current one #[arg(short = 'd', long = "dir", value_hint = ValueHint::FilePath)] output_dir: Option, + + /// Remove the source file after successful decompression + #[arg(short = 'r', long, default_value_t = false)] + remove: bool, }, /// List contents of an archive #[command(visible_aliases = ["l", "ls"])] @@ -142,6 +146,7 @@ mod tests { // Put a crazy value here so no test can assert it unintentionally files: vec!["\x00\x11\x22".into()], output_dir: None, + remove: false, }, } } @@ -154,6 +159,7 @@ mod tests { cmd: Subcommand::Decompress { files: to_paths(["file.tar.gz"]), output_dir: None, + remove: false, }, ..mock_cli_args() } @@ -164,6 +170,7 @@ mod tests { cmd: Subcommand::Decompress { files: to_paths(["file.tar.gz"]), output_dir: None, + remove: false, }, ..mock_cli_args() } @@ -174,6 +181,7 @@ mod tests { cmd: Subcommand::Decompress { files: to_paths(["a", "b", "c"]), output_dir: None, + remove: false, }, ..mock_cli_args() } diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index 6c35c25..a1d7e84 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -14,8 +14,11 @@ use crate::{ Extension, }, utils::{ - self, io::lock_and_flush_output_stdio, is_path_stdin, logger::info_accessible, nice_directory_display, - user_wants_to_continue, + self, + io::lock_and_flush_output_stdio, + is_path_stdin, + logger::{info, info_accessible}, + nice_directory_display, user_wants_to_continue, }, QuestionAction, QuestionPolicy, BUFFER_CAPACITY, }; @@ -37,6 +40,7 @@ pub fn decompress_file( question_policy: QuestionPolicy, quiet: bool, password: Option<&[u8]>, + remove: bool, ) -> crate::Result<()> { assert!(output_dir.exists()); let input_is_stdin = is_path_stdin(input_file_path); @@ -83,6 +87,14 @@ pub fn decompress_file( files_unpacked )); + if !input_is_stdin && remove { + fs::remove_file(input_file_path)?; + info(format!( + "Removed input file {}", + nice_directory_display(input_file_path) + )); + } + return Ok(()); } @@ -233,6 +245,14 @@ pub fn decompress_file( )); info_accessible(format!("Files unpacked: {}", files_unpacked)); + if !input_is_stdin && remove { + fs::remove_file(input_file_path)?; + info(format!( + "Removed input file {}", + nice_directory_display(input_file_path) + )); + } + Ok(()) } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f296359..a4b8fca 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -134,7 +134,11 @@ pub fn run( compress_result.map(|_| ()) } - Subcommand::Decompress { files, output_dir } => { + Subcommand::Decompress { + files, + output_dir, + remove, + } => { let mut output_paths = vec![]; let mut formats = vec![]; @@ -182,6 +186,13 @@ pub fn run( } else { output_dir.join(file_name) }; + info_accessible(format!( + "begin decompress file ... input_path: {}, formats: {:?}, file_name: {}, output_file_path: {}", + path_to_str(input_path), + formats, + path_to_str(file_name), + path_to_str(&output_file_path) + )); decompress_file( input_path, formats, @@ -192,6 +203,7 @@ pub fn run( args.password.as_deref().map(|str| { <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed") }), + remove, ) }) } From 62f3d78f440d0d554def265e004ea4d8846eb124 Mon Sep 17 00:00:00 2001 From: ttyS3 Date: Sat, 30 Nov 2024 16:27:02 +0000 Subject: [PATCH 2/6] refactor(decompress): refactor function to use DecompressOptions struct to make linter happy (too_many_arguments) --- src/commands/decompress.rs | 100 +++++++++++++++++++------------------ src/commands/mod.rs | 13 ++--- 2 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index a1d7e84..4a616e2 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -26,24 +26,26 @@ use crate::{ trait ReadSeek: Read + io::Seek {} impl ReadSeek for T {} +pub struct DecompressOptions<'a> { + pub input_file_path: &'a Path, + pub formats: Vec, + 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 /// /// 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) /// 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 -pub fn decompress_file( - input_file_path: &Path, - formats: Vec, - output_dir: &Path, - output_file_path: PathBuf, - question_policy: QuestionPolicy, - quiet: bool, - password: Option<&[u8]>, - remove: bool, -) -> crate::Result<()> { - assert!(output_dir.exists()); - let input_is_stdin = is_path_stdin(input_file_path); +pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { + assert!(options.output_dir.exists()); + let input_is_stdin = is_path_stdin(options.input_file_path); // Zip archives are special, because they require io::Seek, so it requires it's logic separated // from decoder chaining. @@ -55,7 +57,7 @@ pub fn decompress_file( if let [Extension { compression_formats: [Zip], .. - }] = formats.as_slice() + }] = options.formats.as_slice() { let mut vec = vec![]; let reader: Box = if input_is_stdin { @@ -63,14 +65,14 @@ pub fn decompress_file( io::copy(&mut io::stdin(), &mut vec)?; Box::new(io::Cursor::new(vec)) } 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 files_unpacked = if let ControlFlow::Continue(files) = smart_unpack( - |output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, password, quiet), - output_dir, - &output_file_path, - question_policy, + |output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, options.password, options.quiet), + options.output_dir, + &options.output_file_path, + options.question_policy, )? { files } else { @@ -83,15 +85,15 @@ pub fn decompress_file( // about whether the command succeeded without such a message info_accessible(format!( "Successfully decompressed archive in {} ({} files)", - nice_directory_display(output_dir), + nice_directory_display(options.output_dir), files_unpacked )); - if !input_is_stdin && remove { - fs::remove_file(input_file_path)?; + if !input_is_stdin && options.remove { + fs::remove_file(options.input_file_path)?; info(format!( "Removed input file {}", - nice_directory_display(input_file_path) + nice_directory_display(options.input_file_path) )); } @@ -102,7 +104,7 @@ pub fn decompress_file( let reader: Box = if input_is_stdin { Box::new(io::stdin()) } 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 mut reader: Box = Box::new(reader); @@ -122,7 +124,7 @@ pub fn decompress_file( 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() { reader = chain_reader_decoder(format, reader)?; @@ -132,7 +134,7 @@ pub fn decompress_file( Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd => { 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, None => return Ok(()), }; @@ -143,10 +145,10 @@ pub fn decompress_file( } Tar => { if let ControlFlow::Continue(files) = smart_unpack( - |output_dir| crate::archive::tar::unpack_archive(reader, output_dir, quiet), - output_dir, - &output_file_path, - question_policy, + |output_dir| crate::archive::tar::unpack_archive(reader, output_dir, options.quiet), + options.output_dir, + &options.output_file_path, + options.question_policy, )? { files } else { @@ -154,13 +156,13 @@ pub fn decompress_file( } } Zip => { - if formats.len() > 1 { + if options.formats.len() > 1 { // Locking necessary to guarantee that warning and question // messages stay adjacent let _locks = lock_and_flush_output_stdio(); 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(()); } } @@ -170,10 +172,10 @@ pub fn decompress_file( let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?; if let ControlFlow::Continue(files) = smart_unpack( - |output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, password, quiet), - output_dir, - &output_file_path, - question_policy, + |output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, options.password, options.quiet), + options.output_dir, + &options.output_file_path, + options.question_policy, )? { files } else { @@ -183,18 +185,18 @@ pub fn decompress_file( #[cfg(feature = "unrar")] Rar => { type UnpackResult = crate::Result; - let unpack_fn: Box UnpackResult> = if formats.len() > 1 || input_is_stdin { + let unpack_fn: Box UnpackResult> = if options.formats.len() > 1 || input_is_stdin { let mut temp_file = tempfile::NamedTempFile::new()?; io::copy(&mut reader, &mut temp_file)?; 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 { - 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) = - smart_unpack(unpack_fn, output_dir, &output_file_path, question_policy)? + smart_unpack(unpack_fn, options.output_dir, &options.output_file_path, options.question_policy)? { files } else { @@ -206,13 +208,13 @@ pub fn decompress_file( return Err(crate::archive::rar_stub::no_support()); } SevenZip => { - if formats.len() > 1 { + if options.formats.len() > 1 { // Locking necessary to guarantee that warning and question // messages stay adjacent let _locks = lock_and_flush_output_stdio(); 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(()); } } @@ -222,11 +224,11 @@ pub fn decompress_file( if let ControlFlow::Continue(files) = smart_unpack( |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, options.password, options.quiet) }, - output_dir, - &output_file_path, - question_policy, + options.output_dir, + &options.output_file_path, + options.question_policy, )? { files } else { @@ -241,15 +243,15 @@ pub fn decompress_file( // about whether the command succeeded without such a message info_accessible(format!( "Successfully decompressed archive in {}", - nice_directory_display(output_dir) + nice_directory_display(options.output_dir) )); info_accessible(format!("Files unpacked: {}", files_unpacked)); - if !input_is_stdin && remove { - fs::remove_file(input_file_path)?; + if !input_is_stdin && options.remove { + fs::remove_file(options.input_file_path)?; info(format!( "Removed input file {}", - nice_directory_display(input_file_path) + nice_directory_display(options.input_file_path) )); } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a4b8fca..50f9857 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -7,6 +7,7 @@ mod list; use std::{ops::ControlFlow, path::PathBuf}; use bstr::ByteSlice; +use decompress::DecompressOptions; use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; use utils::colors; @@ -193,18 +194,18 @@ pub fn run( path_to_str(file_name), path_to_str(&output_file_path) )); - decompress_file( - input_path, + decompress_file(DecompressOptions { + input_file_path: input_path, formats, - &output_dir, + output_dir: &output_dir, output_file_path, question_policy, - args.quiet, - args.password.as_deref().map(|str| { + quiet: args.quiet, + password: args.password.as_deref().map(|str| { <[u8] as ByteSlice>::from_os_str(str).expect("convert password to bytes failed") }), remove, - ) + }) }) } Subcommand::List { archives: files, tree } => { From 353c360f6f32ed7429ea93ef4a16e3f9f345faaf Mon Sep 17 00:00:00 2001 From: ttyS3 Date: Sat, 30 Nov 2024 16:28:16 +0000 Subject: [PATCH 3/6] style: cargo fmt with nightly version --- src/commands/decompress.rs | 41 ++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index 4a616e2..1d0152a 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -162,7 +162,11 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { let _locks = lock_and_flush_output_stdio(); warn_user_about_loading_zip_in_memory(); - if !user_wants_to_continue(options.input_file_path, options.question_policy, QuestionAction::Decompression)? { + if !user_wants_to_continue( + options.input_file_path, + options.question_policy, + QuestionAction::Decompression, + )? { return Ok(()); } } @@ -172,7 +176,9 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?; if let ControlFlow::Continue(files) = smart_unpack( - |output_dir| crate::archive::zip::unpack_archive(zip_archive, output_dir, options.password, options.quiet), + |output_dir| { + crate::archive::zip::unpack_archive(zip_archive, output_dir, options.password, options.quiet) + }, options.output_dir, &options.output_file_path, options.question_policy, @@ -192,12 +198,22 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { crate::archive::rar::unpack_archive(temp_file.path(), output_dir, options.password, options.quiet) }) } else { - Box::new(|output_dir| crate::archive::rar::unpack_archive(options.input_file_path, output_dir, options.password, options.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) = - smart_unpack(unpack_fn, options.output_dir, &options.output_file_path, options.question_policy)? - { + if let ControlFlow::Continue(files) = smart_unpack( + unpack_fn, + options.output_dir, + &options.output_file_path, + options.question_policy, + )? { files } else { return Ok(()); @@ -214,7 +230,11 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { let _locks = lock_and_flush_output_stdio(); warn_user_about_loading_sevenz_in_memory(); - if !user_wants_to_continue(options.input_file_path, options.question_policy, QuestionAction::Decompression)? { + if !user_wants_to_continue( + options.input_file_path, + options.question_policy, + QuestionAction::Decompression, + )? { return Ok(()); } } @@ -224,7 +244,12 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> { if let ControlFlow::Continue(files) = smart_unpack( |output_dir| { - crate::archive::sevenz::decompress_sevenz(io::Cursor::new(vec), output_dir, options.password, options.quiet) + crate::archive::sevenz::decompress_sevenz( + io::Cursor::new(vec), + output_dir, + options.password, + options.quiet, + ) }, options.output_dir, &options.output_file_path, From 5941afe66ee26b49792284885db97d4fff211267 Mon Sep 17 00:00:00 2001 From: ttyS3 Date: Sat, 30 Nov 2024 16:34:57 +0000 Subject: [PATCH 4/6] chore: remove decompress file params info to make ui_test_ok_decompress test case pass --- src/commands/mod.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 50f9857..97410d7 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -187,13 +187,6 @@ pub fn run( } else { output_dir.join(file_name) }; - info_accessible(format!( - "begin decompress file ... input_path: {}, formats: {:?}, file_name: {}, output_file_path: {}", - path_to_str(input_path), - formats, - path_to_str(file_name), - path_to_str(&output_file_path) - )); decompress_file(DecompressOptions { input_file_path: input_path, formats, From 499ad776571bfddcc8a3b894527be51f87532dd4 Mon Sep 17 00:00:00 2001 From: ttyS3 Date: Tue, 3 Dec 2024 17:01:06 +0000 Subject: [PATCH 5/6] fix(cli): remove default value for remove flag in Subcommand --- src/cli/args.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/args.rs b/src/cli/args.rs index c07e85c..7216962 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -90,7 +90,7 @@ pub enum Subcommand { output_dir: Option, /// Remove the source file after successful decompression - #[arg(short = 'r', long, default_value_t = false)] + #[arg(short = 'r', long)] remove: bool, }, /// List contents of an archive From 195483a18238e77b05a3d9d3bbc43d12de2915f0 Mon Sep 17 00:00:00 2001 From: ttyS3 Date: Tue, 3 Dec 2024 17:03:03 +0000 Subject: [PATCH 6/6] docs: update changelog with new features --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94d03ad..fe884ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `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