mirror of
https://github.com/ouch-org/ouch.git
synced 2025-06-03 10:00:19 +00:00
Merge folders in decompression (#798)
Signed-off-by: tommady <tommady@users.noreply.github.com>
This commit is contained in:
parent
c97bb6a2d6
commit
1ff1932e3d
@ -21,7 +21,10 @@ Categories Used:
|
||||
## [Unreleased](https://github.com/ouch-org/ouch/compare/0.6.1...HEAD)
|
||||
|
||||
### New Features
|
||||
|
||||
- Merge folders in decompression [\#798](https://github.com/ouch-org/ouch/pull/798) ([tommady](https://github.com/tommady))
|
||||
- Add `--no-smart-unpack` flag to decompression command to disable smart unpack [\#809](https://github.com/ouch-org/ouch/pull/809) ([talis-fb](https://github.com/talis-fb))
|
||||
|
||||
### Improvements
|
||||
### Bug Fixes
|
||||
### Tweaks
|
||||
|
@ -18,8 +18,6 @@ pub fn unpack_archive(
|
||||
password: Option<&[u8]>,
|
||||
quiet: bool,
|
||||
) -> crate::Result<usize> {
|
||||
assert!(output_folder.read_dir().expect("dir exists").next().is_none());
|
||||
|
||||
let archive = match password {
|
||||
Some(password) => Archive::with_password(archive_path, password),
|
||||
None => Archive::new(archive_path),
|
||||
|
@ -24,7 +24,6 @@ use crate::{
|
||||
/// Unpacks the archive given by `archive` into the folder given by `into`.
|
||||
/// Assumes that output_folder is empty
|
||||
pub fn unpack_archive(reader: Box<dyn Read>, output_folder: &Path, quiet: bool) -> crate::Result<usize> {
|
||||
assert!(output_folder.read_dir().expect("dir exists").next().is_none());
|
||||
let mut archive = tar::Archive::new(reader);
|
||||
|
||||
let mut files_unpacked = 0;
|
||||
|
@ -37,8 +37,6 @@ pub fn unpack_archive<R>(
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
assert!(output_folder.read_dir().expect("dir exists").next().is_none());
|
||||
|
||||
let mut unpacked_files = 0;
|
||||
|
||||
for idx in 0..archive.len() {
|
||||
|
@ -139,7 +139,11 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
|
||||
Gzip | Bzip | Bzip3 | Lz4 | Lzma | Snappy | Zstd | Brotli => {
|
||||
reader = chain_reader_decoder(&first_extension, reader)?;
|
||||
|
||||
let mut writer = match utils::ask_to_create_file(&options.output_file_path, options.question_policy)? {
|
||||
let mut writer = match utils::ask_to_create_file(
|
||||
&options.output_file_path,
|
||||
options.question_policy,
|
||||
QuestionAction::Decompression,
|
||||
)? {
|
||||
Some(file) => file,
|
||||
None => return Ok(()),
|
||||
};
|
||||
@ -331,7 +335,7 @@ fn unpack(
|
||||
let output_dir_cleaned = if is_valid_output_dir {
|
||||
output_dir.to_owned()
|
||||
} else {
|
||||
match utils::resolve_path_conflict(output_dir, question_policy)? {
|
||||
match utils::resolve_path_conflict(output_dir, question_policy, QuestionAction::Decompression)? {
|
||||
Some(path) => path,
|
||||
None => return Ok(ControlFlow::Break(())),
|
||||
}
|
||||
@ -387,7 +391,7 @@ fn smart_unpack(
|
||||
|
||||
// Before moving, need to check if a file with the same name already exists
|
||||
// If it does, need to ask the user what to do
|
||||
new_path = match utils::resolve_path_conflict(&new_path, question_policy)? {
|
||||
new_path = match utils::resolve_path_conflict(&new_path, question_policy, QuestionAction::Decompression)? {
|
||||
Some(path) => path,
|
||||
None => return Ok(ControlFlow::Break(())),
|
||||
};
|
||||
|
@ -20,6 +20,7 @@ use crate::{
|
||||
list::ListOptions,
|
||||
utils::{
|
||||
self, colors::*, is_path_stdin, logger::info_accessible, path_to_str, EscapedPathDisplay, FileVisibilityPolicy,
|
||||
QuestionAction,
|
||||
},
|
||||
CliArgs, QuestionPolicy,
|
||||
};
|
||||
@ -91,10 +92,11 @@ pub fn run(
|
||||
)?;
|
||||
check::check_archive_formats_position(&formats, &output_path)?;
|
||||
|
||||
let output_file = match utils::ask_to_create_file(&output_path, question_policy)? {
|
||||
Some(writer) => writer,
|
||||
None => return Ok(()),
|
||||
};
|
||||
let output_file =
|
||||
match utils::ask_to_create_file(&output_path, question_policy, QuestionAction::Compression)? {
|
||||
Some(writer) => writer,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let level = if fast {
|
||||
Some(1) // Lowest level of compression
|
||||
|
@ -11,7 +11,7 @@ use fs_err as fs;
|
||||
use super::{question::FileConflitOperation, user_wants_to_overwrite};
|
||||
use crate::{
|
||||
extension::Extension,
|
||||
utils::{logger::info_accessible, EscapedPathDisplay},
|
||||
utils::{logger::info_accessible, EscapedPathDisplay, QuestionAction},
|
||||
QuestionPolicy,
|
||||
};
|
||||
|
||||
@ -26,9 +26,13 @@ pub fn is_path_stdin(path: &Path) -> bool {
|
||||
/// * `Ok(None)` means the user wants to cancel the operation
|
||||
/// * `Ok(Some(path))` returns a valid PathBuf without any another file or directory with the same name
|
||||
/// * `Err(_)` is an error
|
||||
pub fn resolve_path_conflict(path: &Path, question_policy: QuestionPolicy) -> crate::Result<Option<PathBuf>> {
|
||||
pub fn resolve_path_conflict(
|
||||
path: &Path,
|
||||
question_policy: QuestionPolicy,
|
||||
question_action: QuestionAction,
|
||||
) -> crate::Result<Option<PathBuf>> {
|
||||
if path.exists() {
|
||||
match user_wants_to_overwrite(path, question_policy)? {
|
||||
match user_wants_to_overwrite(path, question_policy, question_action)? {
|
||||
FileConflitOperation::Cancel => Ok(None),
|
||||
FileConflitOperation::Overwrite => {
|
||||
remove_file_or_dir(path)?;
|
||||
@ -38,6 +42,7 @@ pub fn resolve_path_conflict(path: &Path, question_policy: QuestionPolicy) -> cr
|
||||
let renamed_path = rename_for_available_filename(path);
|
||||
Ok(Some(renamed_path))
|
||||
}
|
||||
FileConflitOperation::Merge => Ok(Some(path.to_path_buf())),
|
||||
}
|
||||
} else {
|
||||
Ok(Some(path.to_path_buf()))
|
||||
|
@ -48,49 +48,71 @@ pub enum FileConflitOperation {
|
||||
/// Rename the file
|
||||
/// It'll be put "_1" at the end of the filename or "_2","_3","_4".. if already exists
|
||||
Rename,
|
||||
/// Merge conflicting folders
|
||||
Merge,
|
||||
}
|
||||
|
||||
/// Check if QuestionPolicy flags were set, otherwise, ask user if they want to overwrite.
|
||||
pub fn user_wants_to_overwrite(path: &Path, question_policy: QuestionPolicy) -> crate::Result<FileConflitOperation> {
|
||||
pub fn user_wants_to_overwrite(
|
||||
path: &Path,
|
||||
question_policy: QuestionPolicy,
|
||||
question_action: QuestionAction,
|
||||
) -> crate::Result<FileConflitOperation> {
|
||||
use FileConflitOperation as Op;
|
||||
|
||||
match question_policy {
|
||||
QuestionPolicy::AlwaysYes => Ok(Op::Overwrite),
|
||||
QuestionPolicy::AlwaysNo => Ok(Op::Cancel),
|
||||
QuestionPolicy::Ask => ask_file_conflict_operation(path),
|
||||
QuestionPolicy::Ask => ask_file_conflict_operation(path, question_action),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask the user if they want to overwrite or rename the &Path
|
||||
pub fn ask_file_conflict_operation(path: &Path) -> Result<FileConflitOperation> {
|
||||
pub fn ask_file_conflict_operation(path: &Path, question_action: QuestionAction) -> Result<FileConflitOperation> {
|
||||
use FileConflitOperation as Op;
|
||||
|
||||
let path = path_to_str(strip_cur_dir(path));
|
||||
|
||||
ChoicePrompt::new(
|
||||
format!("Do you want to overwrite {path}?"),
|
||||
[
|
||||
("yes", Op::Overwrite, *colors::GREEN),
|
||||
("no", Op::Cancel, *colors::RED),
|
||||
("rename", Op::Rename, *colors::BLUE),
|
||||
],
|
||||
)
|
||||
.ask()
|
||||
match question_action {
|
||||
QuestionAction::Compression => ChoicePrompt::new(
|
||||
format!("Do you want to overwrite {path}?"),
|
||||
[
|
||||
("yes", Op::Overwrite, *colors::GREEN),
|
||||
("no", Op::Cancel, *colors::RED),
|
||||
("rename", Op::Rename, *colors::BLUE),
|
||||
],
|
||||
)
|
||||
.ask(),
|
||||
QuestionAction::Decompression => ChoicePrompt::new(
|
||||
format!("Do you want to overwrite {path}?"),
|
||||
[
|
||||
("yes", Op::Overwrite, *colors::GREEN),
|
||||
("no", Op::Cancel, *colors::RED),
|
||||
("rename", Op::Rename, *colors::BLUE),
|
||||
("merge", Op::Merge, *colors::ORANGE),
|
||||
],
|
||||
)
|
||||
.ask(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the file if it doesn't exist and if it does then ask to overwrite it.
|
||||
/// If the user doesn't want to overwrite then we return [`Ok(None)`]
|
||||
pub fn ask_to_create_file(path: &Path, question_policy: QuestionPolicy) -> Result<Option<fs::File>> {
|
||||
pub fn ask_to_create_file(
|
||||
path: &Path,
|
||||
question_policy: QuestionPolicy,
|
||||
question_action: QuestionAction,
|
||||
) -> Result<Option<fs::File>> {
|
||||
match fs::OpenOptions::new().write(true).create_new(true).open(path) {
|
||||
Ok(w) => Ok(Some(w)),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
let action = match question_policy {
|
||||
QuestionPolicy::AlwaysYes => FileConflitOperation::Overwrite,
|
||||
QuestionPolicy::AlwaysNo => FileConflitOperation::Cancel,
|
||||
QuestionPolicy::Ask => ask_file_conflict_operation(path)?,
|
||||
QuestionPolicy::Ask => ask_file_conflict_operation(path, question_action)?,
|
||||
};
|
||||
|
||||
match action {
|
||||
FileConflitOperation::Merge => Ok(Some(fs::File::create(path)?)),
|
||||
FileConflitOperation::Overwrite => {
|
||||
utils::remove_file_or_dir(path)?;
|
||||
Ok(Some(fs::File::create(path)?))
|
||||
|
@ -59,7 +59,7 @@ enum Extension {
|
||||
}
|
||||
|
||||
/// Converts a list of extension structs to string
|
||||
fn merge_extensions(ext: impl ToString, exts: Vec<FileExtension>) -> String {
|
||||
fn merge_extensions(ext: impl ToString, exts: &Vec<FileExtension>) -> String {
|
||||
once(ext.to_string())
|
||||
.chain(exts.into_iter().map(|x| x.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
@ -114,7 +114,7 @@ fn single_empty_file(ext: Extension, #[any(size_range(0..8).lift())] exts: Vec<F
|
||||
let before = &dir.join("before");
|
||||
fs::create_dir(before).unwrap();
|
||||
let before_file = &before.join("file");
|
||||
let archive = &dir.join(format!("file.{}", merge_extensions(ext, exts)));
|
||||
let archive = &dir.join(format!("file.{}", merge_extensions(ext, &exts)));
|
||||
let after = &dir.join("after");
|
||||
fs::write(before_file, []).unwrap();
|
||||
ouch!("-A", "c", before_file, archive);
|
||||
@ -137,7 +137,7 @@ fn single_file(
|
||||
let before = &dir.join("before");
|
||||
fs::create_dir(before).unwrap();
|
||||
let before_file = &before.join("file");
|
||||
let archive = &dir.join(format!("file.{}", merge_extensions(ext, exts)));
|
||||
let archive = &dir.join(format!("file.{}", merge_extensions(ext, &exts)));
|
||||
let after = &dir.join("after");
|
||||
write_random_content(
|
||||
&mut fs::File::create(before_file).unwrap(),
|
||||
@ -167,7 +167,7 @@ fn single_file_stdin(
|
||||
let before = &dir.join("before");
|
||||
fs::create_dir(before).unwrap();
|
||||
let before_file = &before.join("file");
|
||||
let format = merge_extensions(&ext, exts);
|
||||
let format = merge_extensions(&ext, &exts);
|
||||
let archive = &dir.join(format!("file.{}", format));
|
||||
let after = &dir.join("after");
|
||||
write_random_content(
|
||||
@ -208,7 +208,7 @@ fn multiple_files(
|
||||
let before = &dir.join("before");
|
||||
let before_dir = &before.join("dir");
|
||||
fs::create_dir_all(before_dir).unwrap();
|
||||
let archive = &dir.join(format!("archive.{}", merge_extensions(&ext, extra_extensions)));
|
||||
let archive = &dir.join(format!("archive.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
let after = &dir.join("after");
|
||||
create_random_files(before_dir, depth, &mut SmallRng::from_entropy());
|
||||
ouch!("-A", "c", before_dir, archive);
|
||||
@ -235,7 +235,7 @@ fn multiple_files_with_conflict_and_choice_to_overwrite(
|
||||
fs::create_dir_all(after_dir).unwrap();
|
||||
create_random_files(after_dir, depth, &mut SmallRng::from_entropy());
|
||||
|
||||
let archive = &dir.join(format!("archive.{}", merge_extensions(&ext, extra_extensions)));
|
||||
let archive = &dir.join(format!("archive.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
ouch!("-A", "c", before_dir, archive);
|
||||
|
||||
crate::utils::cargo_bin()
|
||||
@ -276,7 +276,7 @@ fn multiple_files_with_conflict_and_choice_to_not_overwrite(
|
||||
fs::write(after_dir.join("something.txt"), "Some content").unwrap();
|
||||
fs::copy(after_dir.join("something.txt"), after_backup_dir.join("something.txt")).unwrap();
|
||||
|
||||
let archive = &dir.join(format!("archive.{}", merge_extensions(&ext, extra_extensions)));
|
||||
let archive = &dir.join(format!("archive.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
ouch!("-A", "c", before_dir, archive);
|
||||
|
||||
crate::utils::cargo_bin()
|
||||
@ -309,7 +309,7 @@ fn multiple_files_with_conflict_and_choice_to_rename(
|
||||
fs::create_dir_all(&dest_files_path).unwrap();
|
||||
create_n_random_files(5, &dest_files_path, &mut SmallRng::from_entropy());
|
||||
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions)));
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
ouch!("-A", "c", &src_files_path, archive);
|
||||
|
||||
let dest_files_path_renamed = &root_path.join("dest_files_1");
|
||||
@ -349,7 +349,7 @@ fn multiple_files_with_conflict_and_choice_to_rename_with_already_a_renamed(
|
||||
fs::create_dir_all(&dest_files_path_1).unwrap();
|
||||
create_n_random_files(5, &dest_files_path_1, &mut SmallRng::from_entropy());
|
||||
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions)));
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
ouch!("-A", "c", &src_files_path, archive);
|
||||
|
||||
let dest_files_path_renamed = &root_path.join("dest_files_2");
|
||||
@ -387,7 +387,7 @@ fn smart_unpack_with_single_file(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions)));
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
|
||||
crate::utils::cargo_bin()
|
||||
.arg("compress")
|
||||
@ -438,7 +438,7 @@ fn smart_unpack_with_multiple_files(
|
||||
.map(|entry| entry.unwrap().path())
|
||||
.collect::<Vec<PathBuf>>();
|
||||
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions)));
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
|
||||
let output_path = root_path.join("archive");
|
||||
assert!(!output_path.exists());
|
||||
@ -487,7 +487,7 @@ fn no_smart_unpack_with_single_file(
|
||||
.map(|entry| entry.unwrap().path())
|
||||
.collect::<Vec<PathBuf>>();
|
||||
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions)));
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
|
||||
let output_path = root_path.join("archive");
|
||||
assert!(!output_path.exists());
|
||||
@ -537,7 +537,7 @@ fn no_smart_unpack_with_multiple_files(
|
||||
.map(|entry| entry.unwrap().path())
|
||||
.collect::<Vec<PathBuf>>();
|
||||
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions)));
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
|
||||
let output_path = root_path.join("archive");
|
||||
assert!(!output_path.exists());
|
||||
@ -585,7 +585,7 @@ fn multiple_files_with_disabled_smart_unpack_by_dir(
|
||||
let dest_files_path = root_path.join("dest_files");
|
||||
fs::create_dir_all(&dest_files_path).unwrap();
|
||||
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions)));
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
|
||||
crate::utils::cargo_bin()
|
||||
.arg("compress")
|
||||
@ -702,7 +702,7 @@ fn symlink_pack_and_unpack(
|
||||
|
||||
files_path.push(symlink_path);
|
||||
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, extra_extensions)));
|
||||
let archive = &root_path.join(format!("archive.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
|
||||
crate::utils::cargo_bin()
|
||||
.arg("compress")
|
||||
@ -798,3 +798,67 @@ fn no_git_folder_after_decompression_with_gitignore_flag_active() {
|
||||
".git folder should not exist after decompression"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "allow_piped_choice")]
|
||||
#[proptest(cases = 25)]
|
||||
fn unpack_multiple_sources_into_the_same_destination_with_merge(
|
||||
ext: DirectoryExtension,
|
||||
#[any(size_range(0..1).lift())] extra_extensions: Vec<FileExtension>,
|
||||
) {
|
||||
let temp_dir = tempdir()?;
|
||||
let root_path = temp_dir.path();
|
||||
let source_path = root_path
|
||||
.join(format!("example_{}", merge_extensions(&ext, &extra_extensions)))
|
||||
.join("sub_a")
|
||||
.join("sub_b")
|
||||
.join("sub_c");
|
||||
|
||||
fs::create_dir_all(&source_path)?;
|
||||
let archive = root_path.join(format!("archive.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
crate::utils::cargo_bin()
|
||||
.arg("compress")
|
||||
.args([
|
||||
fs::File::create(source_path.join("file1.txt"))?.path(),
|
||||
fs::File::create(source_path.join("file2.txt"))?.path(),
|
||||
fs::File::create(source_path.join("file3.txt"))?.path(),
|
||||
])
|
||||
.arg(&archive)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
fs::remove_dir_all(&source_path)?;
|
||||
fs::create_dir_all(&source_path)?;
|
||||
let archive1 = root_path.join(format!("archive1.{}", merge_extensions(&ext, &extra_extensions)));
|
||||
crate::utils::cargo_bin()
|
||||
.arg("compress")
|
||||
.args([
|
||||
fs::File::create(source_path.join("file3.txt"))?.path(),
|
||||
fs::File::create(source_path.join("file4.txt"))?.path(),
|
||||
fs::File::create(source_path.join("file5.txt"))?.path(),
|
||||
])
|
||||
.arg(&archive1)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let out_path = root_path.join(format!("out_{}", merge_extensions(&ext, &extra_extensions)));
|
||||
fs::create_dir_all(&out_path)?;
|
||||
|
||||
crate::utils::cargo_bin()
|
||||
.arg("decompress")
|
||||
.arg(archive)
|
||||
.arg("-d")
|
||||
.arg(&out_path)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
crate::utils::cargo_bin()
|
||||
.arg("decompress")
|
||||
.arg(archive1)
|
||||
.arg("-d")
|
||||
.arg(&out_path)
|
||||
.write_stdin("m")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert_eq!(5, out_path.as_path().read_dir()?.count());
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user