diff --git a/src/archive/rar.rs b/src/archive/rar.rs index 225ec1f..7b59bba 100644 --- a/src/archive/rar.rs +++ b/src/archive/rar.rs @@ -1,14 +1,19 @@ //! Contains RAR-specific building and unpacking functions -use std::path::Path; +use std::{path::Path, sync::mpsc::Sender}; -use unrar::{self, Archive}; +use unrar::Archive; -use crate::{error::Error, info, list::FileInArchive}; +use crate::{error::Error, list::FileInArchive, utils::message::PrintMessage}; /// Unpacks the archive given by `archive_path` into the folder given by `output_folder`. /// Assumes that output_folder is empty -pub fn unpack_archive(archive_path: &Path, output_folder: &Path, quiet: bool) -> crate::Result { +pub fn unpack_archive( + archive_path: &Path, + output_folder: &Path, + quiet: bool, + log_sender: Sender, +) -> crate::Result { assert!(output_folder.read_dir().expect("dir exists").count() == 0); let mut archive = Archive::new(archive_path).open_for_processing()?; @@ -18,12 +23,12 @@ pub fn unpack_archive(archive_path: &Path, output_folder: &Path, quiet: bool) -> let entry = header.entry(); archive = if entry.is_file() { if !quiet { - info!( - inaccessible, - "{} extracted. ({})", - entry.filename.display(), - entry.unpacked_size - ); + log_sender + .send(PrintMessage { + contents: format!("{} extracted. ({})", entry.filename.display(), entry.unpacked_size), + accessible: false, + }) + .unwrap(); } unpacked += 1; header.extract_with_base(output_folder)? diff --git a/src/archive/sevenz.rs b/src/archive/sevenz.rs index 0847f4c..56a6650 100644 --- a/src/archive/sevenz.rs +++ b/src/archive/sevenz.rs @@ -1,9 +1,10 @@ //! SevenZip archive format compress function use std::{ - env, io, - io::{Read, Seek, Write}, + env, + io::{self, Read, Seek, Write}, path::{Path, PathBuf}, + sync::mpsc::Sender, }; use fs_err as fs; @@ -11,8 +12,7 @@ use same_file::Handle; use crate::{ error::FinalError, - info, - utils::{self, cd_into_same_dir_as, Bytes, EscapedPathDisplay, FileVisibilityPolicy}, + utils::{self, cd_into_same_dir_as, message::PrintMessage, Bytes, EscapedPathDisplay, FileVisibilityPolicy}, warning, }; @@ -22,6 +22,7 @@ pub fn compress_sevenz( writer: W, file_visibility_policy: FileVisibilityPolicy, quiet: bool, + log_sender: Sender, ) -> crate::Result where W: Write + Seek, @@ -56,7 +57,12 @@ where // spoken text for users using screen readers, braille displays // and so on if !quiet { - info!(inaccessible, "Compressing '{}'.", EscapedPathDisplay::new(path)); + log_sender + .send(PrintMessage { + contents: format!("Compressing '{}'.", EscapedPathDisplay::new(path)), + accessible: false, + }) + .unwrap(); } let metadata = match path.metadata() { @@ -93,7 +99,12 @@ where Ok(bytes) } -pub fn decompress_sevenz(reader: R, output_path: &Path, quiet: bool) -> crate::Result +pub fn decompress_sevenz( + reader: R, + output_path: &Path, + quiet: bool, + log_sender: Sender, +) -> crate::Result where R: Read + Seek, { @@ -109,24 +120,24 @@ where if entry.is_directory() { if !quiet { - info!( - inaccessible, - "File {} extracted to \"{}\"", - entry.name(), - file_path.display() - ); + log_sender + .send(PrintMessage { + contents: format!("File {} extracted to \"{}\"", entry.name(), file_path.display()), + accessible: false, + }) + .unwrap(); } if !path.exists() { fs::create_dir_all(path)?; } } else { if !quiet { - info!( - inaccessible, - "{:?} extracted. ({})", - file_path.display(), - Bytes::new(entry.size()), - ); + log_sender + .send(PrintMessage { + contents: format!("{:?} extracted. ({})", file_path.display(), Bytes::new(entry.size()),), + accessible: false, + }) + .unwrap(); } if let Some(parent) = path.parent() { diff --git a/src/archive/tar.rs b/src/archive/tar.rs index b4fffa5..4c41b7f 100644 --- a/src/archive/tar.rs +++ b/src/archive/tar.rs @@ -13,9 +13,8 @@ use same_file::Handle; use crate::{ error::FinalError, - info, list::FileInArchive, - utils::{self, io::PrintMessage, Bytes, EscapedPathDisplay, FileVisibilityPolicy}, + utils::{self, message::PrintMessage, Bytes, EscapedPathDisplay, FileVisibilityPolicy}, warning, }; @@ -95,6 +94,7 @@ pub fn build_archive_from_paths( writer: W, file_visibility_policy: FileVisibilityPolicy, quiet: bool, + log_sender: Sender, ) -> crate::Result where W: Write, @@ -129,7 +129,12 @@ where // spoken text for users using screen readers, braille displays // and so on if !quiet { - info!(inaccessible, "Compressing '{}'.", EscapedPathDisplay::new(path)); + log_sender + .send(PrintMessage { + contents: format!("Compressing '{}'.", EscapedPathDisplay::new(path)), + accessible: false, + }) + .unwrap(); } if path.is_dir() { diff --git a/src/archive/zip.rs b/src/archive/zip.rs index 8cb48ce..9fcd3d5 100644 --- a/src/archive/zip.rs +++ b/src/archive/zip.rs @@ -14,15 +14,13 @@ use filetime_creation::{set_file_mtime, FileTime}; use fs_err as fs; use same_file::Handle; use time::OffsetDateTime; -use zip::{self, read::ZipFile, DateTime, ZipArchive}; +use zip::{read::ZipFile, DateTime, ZipArchive}; use crate::{ error::FinalError, - info, list::FileInArchive, utils::{ - self, cd_into_same_dir_as, get_invalid_utf8_paths, io::PrintMessage, pretty_format_list_of_paths, - strip_cur_dir, Bytes, EscapedPathDisplay, FileVisibilityPolicy, + self, cd_into_same_dir_as, get_invalid_utf8_paths, message::PrintMessage, pretty_format_list_of_paths, strip_cur_dir, Bytes, EscapedPathDisplay, FileVisibilityPolicy }, warning, }; @@ -51,7 +49,7 @@ where let file_path = output_folder.join(file_path); - display_zip_comment_if_exists(&file); + display_zip_comment_if_exists(&file, log_sender.clone()); match file.name().ends_with('/') { _is_dir @ true => { @@ -147,6 +145,7 @@ pub fn build_archive_from_paths( writer: W, file_visibility_policy: FileVisibilityPolicy, quiet: bool, + log_sender: Sender ) -> crate::Result where W: Write + Seek, @@ -201,7 +200,14 @@ where // spoken text for users using screen readers, braille displays // and so on if !quiet { - info!(inaccessible, "Compressing '{}'.", EscapedPathDisplay::new(path)); + log_sender + .send(PrintMessage { + contents: format!( + "Compressing '{}'.", + EscapedPathDisplay::new(path) + ), + accessible: false, + }).unwrap(); } let metadata = match path.metadata() { @@ -251,7 +257,7 @@ where Ok(bytes) } -fn display_zip_comment_if_exists(file: &ZipFile) { +fn display_zip_comment_if_exists(file: &ZipFile, log_sender: Sender) { let comment = file.comment(); if !comment.is_empty() { // Zip file comments seem to be pretty rare, but if they are used, @@ -264,7 +270,12 @@ fn display_zip_comment_if_exists(file: &ZipFile) { // the future, maybe asking the user if he wants to display the comment // (informing him of its size) would be sensible for both normal and // accessibility mode.. - info!(accessible, "Found comment in {}: {}", file.name(), comment); + log_sender + .send(PrintMessage { + contents: format!("Found comment in {}: {}", file.name(), comment), + accessible: true, + }) + .unwrap(); } } diff --git a/src/check.rs b/src/check.rs index 9e1bdac..609bbfc 100644 --- a/src/check.rs +++ b/src/check.rs @@ -5,14 +5,13 @@ use std::{ ffi::OsString, ops::ControlFlow, - path::{Path, PathBuf}, + path::{Path, PathBuf}, sync::mpsc::Sender, }; use crate::{ error::FinalError, extension::{build_archive_file_suggestion, Extension, PRETTY_SUPPORTED_ALIASES, PRETTY_SUPPORTED_EXTENSIONS}, - info, - utils::{pretty_format_list_of_paths, try_infer_extension, user_wants_to_continue, EscapedPathDisplay}, + utils::{message::PrintMessage, pretty_format_list_of_paths, try_infer_extension, user_wants_to_continue, EscapedPathDisplay}, warning, QuestionAction, QuestionPolicy, Result, }; @@ -26,6 +25,7 @@ pub fn check_mime_type( path: &Path, formats: &mut Vec, question_policy: QuestionPolicy, + log_sender: Sender ) -> Result> { if formats.is_empty() { // File with no extension @@ -33,12 +33,15 @@ pub fn check_mime_type( if let Some(detected_format) = try_infer_extension(path) { // Inferring the file extension can have unpredicted consequences (e.g. the user just // mistyped, ...) which we should always inform the user about. - info!( - accessible, - "Detected file: `{}` extension as `{}`", - path.display(), - detected_format - ); + log_sender + .send(PrintMessage { + contents: format!( + "Detected file: `{}` extension as `{}`", + path.display(), + detected_format + ), + accessible: true, + }).unwrap(); if user_wants_to_continue(path, question_policy, QuestionAction::Decompression)? { formats.push(detected_format); } else { @@ -66,11 +69,14 @@ pub fn check_mime_type( } else { // NOTE: If this actually produces no false positives, we can upgrade it in the future // to a warning and ask the user if he wants to continue decompressing. - info!( - accessible, - "Failed to confirm the format of `{}` by sniffing the contents, file might be misnamed", - path.display() - ); + log_sender + .send(PrintMessage { + contents: format!( + "Failed to confirm the format of `{}` by sniffing the contents, file might be misnamed", + path.display() + ), + accessible: true, + }).unwrap(); } Ok(ControlFlow::Continue(())) } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index cfcd67d..69bf213 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -5,7 +5,6 @@ mod args; use std::{ io, path::{Path, PathBuf}, - vec::Vec, }; use clap::Parser; diff --git a/src/commands/compress.rs b/src/commands/compress.rs index 2f24cff..3ec03bf 100644 --- a/src/commands/compress.rs +++ b/src/commands/compress.rs @@ -1,6 +1,7 @@ use std::{ io::{self, BufWriter, Cursor, Seek, Write}, path::{Path, PathBuf}, + sync::mpsc::Sender, }; use fs_err as fs; @@ -10,7 +11,7 @@ use crate::{ archive, commands::warn_user_about_loading_zip_in_memory, extension::{split_first_compression_format, CompressionFormat::*, Extension}, - utils::{user_wants_to_continue, FileVisibilityPolicy}, + utils::{message::PrintMessage, user_wants_to_continue, FileVisibilityPolicy}, QuestionAction, QuestionPolicy, BUFFER_CAPACITY, }; @@ -34,6 +35,7 @@ pub fn compress_files( question_policy: QuestionPolicy, file_visibility_policy: FileVisibilityPolicy, level: Option, + log_sender: Sender, ) -> crate::Result { // If the input files contain a directory, then the total size will be underestimated let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file); @@ -99,7 +101,14 @@ pub fn compress_files( io::copy(&mut reader, &mut writer)?; } Tar => { - archive::tar::build_archive_from_paths(&files, output_path, &mut writer, file_visibility_policy, quiet)?; + archive::tar::build_archive_from_paths( + &files, + output_path, + &mut writer, + file_visibility_policy, + quiet, + log_sender.clone(), + )?; writer.flush()?; } Zip => { @@ -119,6 +128,7 @@ pub fn compress_files( &mut vec_buffer, file_visibility_policy, quiet, + log_sender.clone() )?; vec_buffer.rewind()?; io::copy(&mut vec_buffer, &mut writer)?; @@ -140,7 +150,14 @@ pub fn compress_files( } let mut vec_buffer = Cursor::new(vec![]); - archive::sevenz::compress_sevenz(&files, output_path, &mut vec_buffer, file_visibility_policy, quiet)?; + archive::sevenz::compress_sevenz( + &files, + output_path, + &mut vec_buffer, + file_visibility_policy, + quiet, + log_sender.clone(), + )?; vec_buffer.rewind()?; io::copy(&mut vec_buffer, &mut writer)?; } diff --git a/src/commands/decompress.rs b/src/commands/decompress.rs index 0db9eae..eb88044 100644 --- a/src/commands/decompress.rs +++ b/src/commands/decompress.rs @@ -14,8 +14,7 @@ use crate::{ CompressionFormat::{self, *}, Extension, }, - info, - utils::{self, io::PrintMessage, nice_directory_display, user_wants_to_continue}, + utils::{self, message::PrintMessage, nice_directory_display, user_wants_to_continue}, QuestionAction, QuestionPolicy, BUFFER_CAPACITY, }; @@ -161,9 +160,14 @@ pub fn decompress_file( let unpack_fn: Box UnpackResult> = if formats.len() > 1 { 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, quiet)) + let log_sender_clone = log_sender.clone(); + Box::new(move |output_dir| { + crate::archive::rar::unpack_archive(temp_file.path(), output_dir, quiet, log_sender_clone) + }) } else { - Box::new(|output_dir| crate::archive::rar::unpack_archive(input_file_path, output_dir, quiet)) + Box::new(|output_dir| { + crate::archive::rar::unpack_archive(input_file_path, output_dir, quiet, log_sender.clone()) + }) }; if let ControlFlow::Continue(files) = smart_unpack( @@ -195,7 +199,14 @@ pub fn decompress_file( io::copy(&mut reader, &mut vec)?; if let ControlFlow::Continue(files) = smart_unpack( - |output_dir| crate::archive::sevenz::decompress_sevenz(io::Cursor::new(vec), output_dir, quiet), + |output_dir| { + crate::archive::sevenz::decompress_sevenz( + io::Cursor::new(vec), + output_dir, + quiet, + log_sender.clone(), + ) + }, output_dir, &output_file_path, question_policy, diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ce35cf7..513314c 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -21,7 +21,7 @@ use crate::{ extension::{self, parse_format}, info, list::ListOptions, - utils::{self, io::PrintMessage, to_utf, EscapedPathDisplay, FileVisibilityPolicy}, + utils::{self, message::PrintMessage, to_utf, EscapedPathDisplay, FileVisibilityPolicy}, warning, CliArgs, QuestionPolicy, }; @@ -54,6 +54,45 @@ pub fn run( question_policy: QuestionPolicy, file_visibility_policy: FileVisibilityPolicy, ) -> crate::Result<()> { + let (log_sender, log_receiver) = channel::(); + + let pair = Arc::new((Mutex::new(false), Condvar::new())); + let pair2 = Arc::clone(&pair); + + // Log received messages until all senders are dropped + rayon::spawn(move || { + const BUFFER_SIZE: usize = 10; + let mut buffer = Vec::::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"); + tmp.push_str(&msg.contents); + + println!("{}", tmp); + buffer.clear(); + } else { + buffer.push(msg.contents); + } + } else { + // All senders have been dropped + println!("{}", buffer.join("\n")); + + // Wake up the main thread + let (lock, cvar) = &*pair2; + let mut flushed = lock.lock().unwrap(); + *flushed = true; + cvar.notify_one(); + break; + } + } + }); + match args.cmd { Subcommand::Compress { files, @@ -106,6 +145,7 @@ pub fn run( question_policy, file_visibility_policy, level, + log_sender.clone(), ); if let Ok(true) = compress_result { @@ -113,7 +153,12 @@ pub fn run( // 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, "Successfully compressed '{}'.", to_utf(&output_path)); + log_sender + .send(PrintMessage { + contents: format!("Successfully compressed '{}'.", to_utf(&output_path)), + accessible: true, + }) + .unwrap(); } else { // If Ok(false) or Err() occurred, delete incomplete file at `output_path` // @@ -153,7 +198,7 @@ pub fn run( 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)? { + if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy, log_sender.clone())? { return Ok(()); } @@ -167,43 +212,12 @@ pub fn run( // 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)?; + utils::create_dir_if_non_existent(&dir, log_sender.clone())?; dir } else { PathBuf::from(".") }; - let (log_sender, log_receiver) = channel::(); - - let pair = Arc::new((Mutex::new(false), Condvar::new())); - let pair2 = Arc::clone(&pair); - - // Log received messages until all senders are dropped - rayon::spawn(move || { - let mut buffer = Vec::::with_capacity(10); - - loop { - let msg = log_receiver.recv(); - if let Ok(msg) = msg { - if buffer.len() == 10 { - let mut tmp = buffer.join("\n"); - tmp.push_str(&msg.contents); - println!("{}", tmp); - buffer.clear(); - } else { - buffer.push(msg.contents); - } - } else { - println!("{}", buffer.join("\n")); - let (lock, cvar) = &*pair2; - let mut flushed = lock.lock().unwrap(); - *flushed = true; - cvar.notify_one(); - break; - } - } - }); - files .par_iter() .zip(formats) @@ -220,17 +234,6 @@ pub fn run( log_sender.clone(), ) })?; - - // Drop our sender clones so when all threads are done, no clones are left - drop(log_sender); - - // Prevent the main thread from exiting until the background thread handling the - // logging has set `flushed` to true. - let (lock, cvar) = &*pair; - let mut flushed = lock.lock().unwrap(); - while !*flushed { - flushed = cvar.wait(flushed).unwrap(); - } } Subcommand::List { archives: files, tree } => { let mut formats = vec![]; @@ -244,7 +247,7 @@ pub fn run( 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)? { + if let ControlFlow::Break(_) = check::check_mime_type(path, &mut file_formats, question_policy, log_sender.clone())? { return Ok(()); } @@ -266,5 +269,15 @@ pub fn run( } } } + + // Drop our sender so when all threads are done, no clones are left + drop(log_sender); + + // Prevent the main thread from exiting until the background thread handling the + // logging has set `flushed` to true. + let (lock, cvar) = &*pair; + let guard = lock.lock().unwrap(); + let _flushed = cvar.wait(guard).unwrap(); + Ok(()) } diff --git a/src/extension.rs b/src/extension.rs index d602ec7..d374ee1 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -248,8 +248,6 @@ pub fn build_archive_file_suggestion(path: &Path, suggested_extension: &str) -> #[cfg(test)] mod tests { - use std::path::Path; - use super::*; #[test] diff --git a/src/list.rs b/src/list.rs index 4d4a945..6b86149 100644 --- a/src/list.rs +++ b/src/list.rs @@ -78,7 +78,6 @@ mod tree { use std::{ ffi::{OsStr, OsString}, io::Write, - iter::FromIterator, path, }; diff --git a/src/utils/fs.rs b/src/utils/fs.rs index dbd422e..e0c077b 100644 --- a/src/utils/fs.rs +++ b/src/utils/fs.rs @@ -4,12 +4,13 @@ use std::{ env, io::Read, path::{Path, PathBuf}, + sync::mpsc::Sender, }; use fs_err as fs; -use super::user_wants_to_overwrite; -use crate::{extension::Extension, info, utils::EscapedPathDisplay, QuestionPolicy}; +use super::{message::PrintMessage, user_wants_to_overwrite}; +use crate::{extension::Extension, utils::EscapedPathDisplay, QuestionPolicy}; /// Remove `path` asking the user to overwrite if necessary. /// @@ -36,12 +37,17 @@ pub fn remove_file_or_dir(path: &Path) -> crate::Result<()> { } /// Creates a directory at the path, if there is nothing there. -pub fn create_dir_if_non_existent(path: &Path) -> crate::Result<()> { +pub fn create_dir_if_non_existent(path: &Path, log_sender: Sender) -> crate::Result<()> { if !path.exists() { fs::create_dir_all(path)?; // creating a directory is an important change to the file system we // should always inform the user about - info!(accessible, "directory {} created.", EscapedPathDisplay::new(path)); + log_sender + .send(PrintMessage { + contents: format!("Directory {} created.", EscapedPathDisplay::new(path)), + accessible: true, + }) + .unwrap(); } Ok(()) } diff --git a/src/utils/io.rs b/src/utils/io.rs deleted file mode 100644 index b8adc87..0000000 --- a/src/utils/io.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[derive(Debug)] -pub struct PrintMessage { - pub contents: String, - pub accessible: bool, -} diff --git a/src/utils/message.rs b/src/utils/message.rs new file mode 100644 index 0000000..582dae7 --- /dev/null +++ b/src/utils/message.rs @@ -0,0 +1,22 @@ +/// Message object used for sending logs from worker threads to a logging thread via channels. +/// See +/// +/// ## Example +/// +/// ```rs +/// // This is already done in the main thread in src/commands/mod.rs +/// // Functions that want to log anything just need to have +/// // `log_sender: Sender` as an argument. +/// let (log_sender, log_receiver) = channel::(); +/// +/// log_sender +/// .send(PrintMessage { +/// contents: "Hello, world!".to_string(), +/// accessible: true, +/// }).unwrap(); +/// ``` +#[derive(Debug)] +pub struct PrintMessage { + pub contents: String, + pub accessible: bool, +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 57b545a..a57180b 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -7,7 +7,7 @@ pub mod colors; mod file_visibility; mod formatting; mod fs; -pub mod io; +pub mod message; mod question; pub use file_visibility::FileVisibilityPolicy;