mirror of
https://github.com/ouch-org/ouch.git
synced 2025-06-06 11:35:45 +00:00
295 lines
11 KiB
Rust
295 lines
11 KiB
Rust
//! Receive command from the cli and call the respective function for that command.
|
|
|
|
mod compress;
|
|
mod decompress;
|
|
mod list;
|
|
|
|
use std::{
|
|
ops::ControlFlow,
|
|
path::PathBuf,
|
|
sync::{Arc, Condvar, Mutex},
|
|
};
|
|
|
|
use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
|
|
use utils::colors;
|
|
|
|
use crate::{
|
|
check,
|
|
cli::Subcommand,
|
|
commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
|
|
error::{Error, FinalError},
|
|
extension::{self, parse_format},
|
|
list::ListOptions,
|
|
utils::{
|
|
self,
|
|
logger::{info_accessible, map_message, setup_channel, warning, LogReceiver},
|
|
to_utf, EscapedPathDisplay, FileVisibilityPolicy,
|
|
},
|
|
CliArgs, QuestionPolicy,
|
|
};
|
|
|
|
/// Warn the user that (de)compressing this .zip archive might freeze their system.
|
|
fn warn_user_about_loading_zip_in_memory() {
|
|
const ZIP_IN_MEMORY_LIMITATION_WARNING: &str = "\n\
|
|
\tThe format '.zip' is limited and cannot be (de)compressed using encoding streams.\n\
|
|
\tWhen using '.zip' with other formats, (de)compression must be done in-memory\n\
|
|
\tCareful, you might run out of RAM if the archive is too large!";
|
|
|
|
warning(ZIP_IN_MEMORY_LIMITATION_WARNING.to_string());
|
|
}
|
|
|
|
/// Warn the user that (de)compressing this .7z archive might freeze their system.
|
|
fn warn_user_about_loading_sevenz_in_memory() {
|
|
const SEVENZ_IN_MEMORY_LIMITATION_WARNING: &str = "\n\
|
|
\tThe format '.7z' is limited and cannot be (de)compressed using encoding streams.\n\
|
|
\tWhen using '.7z' with other formats, (de)compression must be done in-memory\n\
|
|
\tCareful, you might run out of RAM if the archive is too large!";
|
|
|
|
warning(SEVENZ_IN_MEMORY_LIMITATION_WARNING.to_string());
|
|
}
|
|
|
|
/// This function checks what command needs to be run and performs A LOT of ahead-of-time checks
|
|
/// to assume everything is OK.
|
|
///
|
|
/// There are a lot of custom errors to give enough error description and explanation.
|
|
pub fn run(
|
|
args: CliArgs,
|
|
question_policy: QuestionPolicy,
|
|
file_visibility_policy: FileVisibilityPolicy,
|
|
) -> crate::Result<()> {
|
|
let log_receiver = setup_channel();
|
|
|
|
let synchronization_pair = Arc::new((Mutex::new(false), Condvar::new()));
|
|
spawn_logger_thread(log_receiver, synchronization_pair.clone());
|
|
run_cmd(args, question_policy, file_visibility_policy)?;
|
|
|
|
// Drop our sender so when all threads are done, no clones are left.
|
|
// This is needed, otherwise the logging thread will never exit since we would be keeping a
|
|
// sender alive here.
|
|
todo!();
|
|
|
|
// Prevent the main thread from exiting until the background thread handling the
|
|
// logging has set `flushed` to true.
|
|
let (lock, cvar) = &*synchronization_pair;
|
|
let guard = lock.lock().unwrap();
|
|
let _flushed = cvar.wait(guard).unwrap();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn run_cmd(
|
|
args: CliArgs,
|
|
question_policy: QuestionPolicy,
|
|
file_visibility_policy: FileVisibilityPolicy,
|
|
) -> crate::Result<()> {
|
|
match args.cmd {
|
|
Subcommand::Compress {
|
|
files,
|
|
output: output_path,
|
|
level,
|
|
fast,
|
|
slow,
|
|
} => {
|
|
// After cleaning, if there are no input files left, exit
|
|
if files.is_empty() {
|
|
return Err(FinalError::with_title("No files to compress").into());
|
|
}
|
|
|
|
// Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
|
|
let (formats_from_flag, formats) = match args.format {
|
|
Some(formats) => {
|
|
let parsed_formats = parse_format(&formats)?;
|
|
(Some(formats), parsed_formats)
|
|
}
|
|
None => (None, extension::extensions_from_path(&output_path)),
|
|
};
|
|
|
|
check::check_invalid_compression_with_non_archive_format(
|
|
&formats,
|
|
&output_path,
|
|
&files,
|
|
formats_from_flag.as_ref(),
|
|
)?;
|
|
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 level = if fast {
|
|
Some(1) // Lowest level of compression
|
|
} else if slow {
|
|
Some(i16::MAX) // Highest level of compression
|
|
} else {
|
|
level
|
|
};
|
|
|
|
let compress_result = compress_files(
|
|
files,
|
|
formats,
|
|
output_file,
|
|
&output_path,
|
|
args.quiet,
|
|
question_policy,
|
|
file_visibility_policy,
|
|
level,
|
|
);
|
|
|
|
if let Ok(true) = compress_result {
|
|
// this is only printed once, so it doesn't result in much text. On the other hand,
|
|
// having a final status message is important especially in an accessibility context
|
|
// as screen readers may not read a commands exit code, making it hard to reason
|
|
// about whether the command succeeded without such a message
|
|
info_accessible(format!("Successfully compressed '{}'.", to_utf(&output_path)));
|
|
} else {
|
|
// If Ok(false) or Err() occurred, delete incomplete file at `output_path`
|
|
//
|
|
// if deleting fails, print an extra alert message pointing
|
|
// out that we left a possibly CORRUPTED file at `output_path`
|
|
if utils::remove_file_or_dir(&output_path).is_err() {
|
|
eprintln!("{red}FATAL ERROR:\n", red = *colors::RED);
|
|
eprintln!(
|
|
" Ouch failed to delete the file '{}'.",
|
|
EscapedPathDisplay::new(&output_path)
|
|
);
|
|
eprintln!(" Please delete it manually.");
|
|
eprintln!(" This file is corrupted if compression didn't finished.");
|
|
|
|
if compress_result.is_err() {
|
|
eprintln!(" Compression failed for reasons below.");
|
|
}
|
|
}
|
|
}
|
|
|
|
compress_result.map(|_| ())
|
|
}
|
|
Subcommand::Decompress { files, output_dir } => {
|
|
let mut output_paths = vec![];
|
|
let mut formats = vec![];
|
|
|
|
if let Some(format) = args.format {
|
|
let format = parse_format(&format)?;
|
|
for path in files.iter() {
|
|
let file_name = path.file_name().ok_or_else(|| Error::NotFound {
|
|
error_title: format!("{} does not have a file name", EscapedPathDisplay::new(path)),
|
|
})?;
|
|
output_paths.push(file_name.as_ref());
|
|
formats.push(format.clone());
|
|
}
|
|
} else {
|
|
for path in files.iter() {
|
|
let (pathbase, mut file_formats) = extension::separate_known_extensions_from_name(path);
|
|
|
|
if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
|
|
return Ok(());
|
|
}
|
|
|
|
output_paths.push(pathbase);
|
|
formats.push(file_formats);
|
|
}
|
|
}
|
|
|
|
check::check_missing_formats_when_decompressing(&files, &formats)?;
|
|
|
|
// The directory that will contain the output files
|
|
// We default to the current directory if the user didn't specify an output directory with --dir
|
|
let output_dir = if let Some(dir) = output_dir {
|
|
utils::create_dir_if_non_existent(&dir)?;
|
|
dir
|
|
} else {
|
|
PathBuf::from(".")
|
|
};
|
|
|
|
files
|
|
.par_iter()
|
|
.zip(formats)
|
|
.zip(output_paths)
|
|
.try_for_each(|((input_path, formats), file_name)| {
|
|
let output_file_path = output_dir.join(file_name); // Path used by single file format archives
|
|
decompress_file(
|
|
input_path,
|
|
formats,
|
|
&output_dir,
|
|
output_file_path,
|
|
question_policy,
|
|
args.quiet,
|
|
)
|
|
})
|
|
}
|
|
Subcommand::List { archives: files, tree } => {
|
|
let mut formats = vec![];
|
|
|
|
if let Some(format) = args.format {
|
|
let format = parse_format(&format)?;
|
|
for _ in 0..files.len() {
|
|
formats.push(format.clone());
|
|
}
|
|
} else {
|
|
for path in files.iter() {
|
|
let mut file_formats = extension::extensions_from_path(path);
|
|
|
|
if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy)? {
|
|
return Ok(());
|
|
}
|
|
|
|
formats.push(file_formats);
|
|
}
|
|
}
|
|
|
|
// Ensure we were not told to list the content of a non-archive compressed file
|
|
check::check_for_non_archive_formats(&files, &formats)?;
|
|
|
|
let list_options = ListOptions { tree };
|
|
|
|
for (i, (archive_path, formats)) in files.iter().zip(formats).enumerate() {
|
|
if i > 0 {
|
|
println!();
|
|
}
|
|
let formats = extension::flatten_compression_formats(&formats);
|
|
list_archive_contents(archive_path, formats, list_options, question_policy)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn spawn_logger_thread(log_receiver: LogReceiver, synchronization_pair: Arc<(Mutex<bool>, Condvar)>) {
|
|
rayon::spawn(move || {
|
|
const BUFFER_SIZE: usize = 10;
|
|
let mut buffer = Vec::<String>::with_capacity(BUFFER_SIZE);
|
|
|
|
loop {
|
|
let msg = log_receiver.recv();
|
|
|
|
// Senders are still active
|
|
if let Ok(msg) = msg {
|
|
// Print messages if buffer is full otherwise append to it
|
|
if buffer.len() == BUFFER_SIZE {
|
|
let mut tmp = buffer.join("\n");
|
|
|
|
if let Some(msg) = map_message(&msg) {
|
|
tmp.push_str(&msg);
|
|
}
|
|
|
|
eprintln!("{}", tmp);
|
|
buffer.clear();
|
|
} else if let Some(msg) = map_message(&msg) {
|
|
buffer.push(msg);
|
|
}
|
|
} else {
|
|
// All senders have been dropped
|
|
eprintln!("{}", buffer.join("\n"));
|
|
|
|
// Wake up the main thread
|
|
let (lock, cvar) = &*synchronization_pair;
|
|
let mut flushed = lock.lock().unwrap();
|
|
*flushed = true;
|
|
cvar.notify_one();
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|