From 2cf5dcd513b3a3913f7b7fe4f3a4371141f4f22a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Mikrut?= Date: Sat, 26 Sep 2020 21:50:16 +0200 Subject: [PATCH] Added support for empty file finder --- czkawka_cli/src/main.rs | 55 ++++- czkawka_core/src/big_file.rs | 2 +- czkawka_core/src/duplicate.rs | 12 +- czkawka_core/src/empty_files.rs | 379 +++++++++++++++++++++++++++++++ czkawka_core/src/empty_folder.rs | 6 +- czkawka_core/src/lib.rs | 1 + 6 files changed, 444 insertions(+), 11 deletions(-) create mode 100644 czkawka_core/src/empty_files.rs diff --git a/czkawka_cli/src/main.rs b/czkawka_cli/src/main.rs index 109f03f..650218c 100644 --- a/czkawka_cli/src/main.rs +++ b/czkawka_cli/src/main.rs @@ -198,8 +198,8 @@ fn main() { bf.set_excluded_directory(ArgumentsPair::get_argument(&arguments, "-e", false)); } - if ArgumentsPair::has_command(&arguments, "-s") { - let number_of_files = match ArgumentsPair::get_argument(&arguments, "-s", false).parse::() { + if ArgumentsPair::has_command(&arguments, "-l") { + let number_of_files = match ArgumentsPair::get_argument(&arguments, "-l", false).parse::() { Ok(t) => { if t == 0 { println!("ERROR: Minimum one biggest file must be showed.."); @@ -242,6 +242,45 @@ fn main() { bf.get_text_messages().print_messages(); } + "--y" => { + let mut yf = empty_files::EmptyFiles::new(); + + if ArgumentsPair::has_command(&arguments, "-i") { + yf.set_included_directory(ArgumentsPair::get_argument(&arguments, "-i", false)); + } else { + println!("FATAL ERROR: Parameter -i with set of included files is required."); + process::exit(1); + } + if ArgumentsPair::has_command(&arguments, "-e") { + yf.set_excluded_directory(ArgumentsPair::get_argument(&arguments, "-e", false)); + } + if ArgumentsPair::has_command(&arguments, "-k") { + yf.set_excluded_items(ArgumentsPair::get_argument(&arguments, "-k", false)); + } + + if ArgumentsPair::has_command(&arguments, "-o") { + yf.set_recursive_search(false); + } + + if ArgumentsPair::has_command(&arguments, "-delete") { + yf.set_delete_method(empty_files::DeleteMethod::Delete); + } + + yf.find_empty_files(); + + #[allow(clippy::collapsible_if)] + if ArgumentsPair::has_command(&arguments, "-f") { + if !yf.save_results_to_file(&ArgumentsPair::get_argument(&arguments, "-f", false)) { + yf.get_text_messages().print_messages(); + process::exit(1); + } + } + + #[cfg(not(debug_assertions))] // This will show too much probably unnecessary data to debug, comment line only if needed + yf.print_results(); + + yf.get_text_messages().print_messages(); + } "--version" | "v" => { println!("Czkawka CLI {}", CZKAWKA_VERSION); process::exit(0); @@ -293,15 +332,23 @@ Usage of Czkawka: Usage example: czkawka --e -i "/home/rafal/rr, /home/gateway" -e "/home/rafal/rr/2" -delete - --b <-i directory_to_search> [-e exclude_directories = ""] [-k excluded_items = ""] [-s number_of_files = 50] [-x allowed_extension = ""] [-o] [-f file_to_save = "results.txt"] + --b <-i directory_to_search> [-e exclude_directories = ""] [-k excluded_items = ""] [-l number_of_files = 50] [-x allowed_extension = ""] [-o] [-f file_to_save = "results.txt"] -i directory_to_search - list of directories which should will be searched like /home/rafal -e exclude_directories - list of directories which will be excluded from search. -k excluded_items - list of excluded items which contains * wildcard(may be slow) -o - this options prevents from recursive check of folders -f file_to_save - saves results to file - -s number_of_files - number of showed the biggest files. + -l number_of_files - number of showed the biggest files. -x allowed_extension - list of checked extension, e.g. "jpg,mp4" will allow to check "book.jpg" and "car.mp4" but not roman.png. There are also helpful macros which allow to easy use a typcal extension like IMAGE("jpg,kra,gif,png,bmp,tiff,webp,hdr,svg") or TEXT("txt,doc,docx,odt,rtf") + --y <-i directory_to_search> [-e exclude_directories = ""] [-k excluded_items = ""] [-o] [-f file_to_save = "results.txt"] [-delete] - search for duplicates files + -i directory_to_search - list of directories which should will be searched like /home/rafal + -e exclude_directories - list of directories which will be excluded from search. + -k excluded_items - list of excluded items which contains * wildcard(may be slow) + -o - this options prevents from recursive check of folders + -f file_to_save - saves results to file + -delete - delete found files + --version / --v - prints program name and version "### diff --git a/czkawka_core/src/big_file.rs b/czkawka_core/src/big_file.rs index 23f3c29..2d40957 100644 --- a/czkawka_core/src/big_file.rs +++ b/czkawka_core/src/big_file.rs @@ -376,6 +376,6 @@ impl PrintResults for BigFile { println!("{} ({} bytes) - {}", size.file_size(options::BINARY).unwrap(), size, entry.path); } } - Common::print_time(start_time, SystemTime::now(), "print_duplicated_entries".to_string()); + Common::print_time(start_time, SystemTime::now(), "print_entries".to_string()); } } diff --git a/czkawka_core/src/duplicate.rs b/czkawka_core/src/duplicate.rs index 96dea7d..efc74de 100644 --- a/czkawka_core/src/duplicate.rs +++ b/czkawka_core/src/duplicate.rs @@ -516,7 +516,13 @@ impl SaveResults for DuplicateFinder { } }; - match file.write_all(format!("Results of searching in {:?}\n", self.directories.included_directories).as_bytes()) { + match file.write_all( + format!( + "Results of searching {:?} with excluded directories {:?} and excluded items {:?}\n", + self.directories.included_directories, self.directories.excluded_directories, self.excluded_items.items + ) + .as_bytes(), + ) { Ok(_) => (), Err(_) => { self.text_messages.errors.push(format!("Failed to save results to file {}", file_name)); @@ -565,7 +571,7 @@ impl SaveResults for DuplicateFinder { } } } else { - file.write_all(b"Not found any empty folders.").unwrap(); + file.write_all(b"Not found any duplicates.").unwrap(); } Common::print_time(start_time, SystemTime::now(), "save_results_to_file".to_string()); true @@ -627,7 +633,7 @@ impl PrintResults for DuplicateFinder { panic!("Checking Method shouldn't be ever set to None"); } } - Common::print_time(start_time, SystemTime::now(), "print_duplicated_entries".to_string()); + Common::print_time(start_time, SystemTime::now(), "print_entries".to_string()); } } diff --git a/czkawka_core/src/empty_files.rs b/czkawka_core/src/empty_files.rs new file mode 100644 index 0000000..3fa45ae --- /dev/null +++ b/czkawka_core/src/empty_files.rs @@ -0,0 +1,379 @@ +use std::fs; +use std::fs::{File, Metadata}; +use std::io::prelude::*; +use std::time::SystemTime; + +use crate::common::Common; +use crate::common_directory::Directories; +use crate::common_extensions::Extensions; +use crate::common_items::ExcludedItems; +use crate::common_messages::Messages; +use crate::common_traits::*; + +#[derive(Eq, PartialEq, Clone, Debug)] +pub enum DeleteMethod { + None, + Delete, +} + +#[derive(Clone)] +pub struct FileEntry { + pub path: String, + pub created_date: SystemTime, + pub modified_date: SystemTime, +} + +/// Info struck with helpful information's about results +pub struct Info { + pub number_of_checked_files: usize, + pub number_of_checked_folders: usize, + pub number_of_ignored_files: usize, + pub number_of_ignored_things: usize, + pub number_of_empty_files: usize, + pub number_of_removed_files: usize, + pub number_of_failed_to_remove_files: usize, +} +impl Info { + pub fn new() -> Info { + Info { + number_of_checked_files: 0, + number_of_ignored_files: 0, + number_of_checked_folders: 0, + number_of_ignored_things: 0, + number_of_empty_files: 0, + number_of_removed_files: 0, + number_of_failed_to_remove_files: 0, + } + } +} + +impl Default for Info { + fn default() -> Self { + Self::new() + } +} + +/// Struct with required information's to work +pub struct EmptyFiles { + text_messages: Messages, + information: Info, + empty_files: Vec, + directories: Directories, + allowed_extensions: Extensions, + excluded_items: ExcludedItems, + recursive_search: bool, + delete_method: DeleteMethod, +} + +impl EmptyFiles { + pub fn new() -> EmptyFiles { + EmptyFiles { + text_messages: Messages::new(), + information: Info::new(), + recursive_search: true, + allowed_extensions: Extensions::new(), + directories: Directories::new(), + excluded_items: ExcludedItems::new(), + empty_files: vec![], + delete_method: DeleteMethod::None, + } + } + + /// Finding empty files, save results to internal struct variables + pub fn find_empty_files(&mut self) { + self.directories.optimize_directories(self.recursive_search, &mut self.text_messages); + self.check_files(); + self.delete_files(); + self.debug_print(); + } + + pub fn get_text_messages(&self) -> &Messages { + &self.text_messages + } + + pub fn get_information(&self) -> &Info { + &self.information + } + + pub fn set_delete_method(&mut self, delete_method: DeleteMethod) { + self.delete_method = delete_method; + } + + pub fn set_recursive_search(&mut self, recursive_search: bool) { + self.recursive_search = recursive_search; + } + + pub fn set_included_directory(&mut self, included_directory: String) -> bool { + self.directories.set_included_directory(included_directory, &mut self.text_messages) + } + + pub fn set_excluded_directory(&mut self, excluded_directory: String) { + self.directories.set_excluded_directory(excluded_directory, &mut self.text_messages); + } + pub fn set_allowed_extensions(&mut self, allowed_extensions: String) { + self.allowed_extensions.set_allowed_extensions(allowed_extensions, &mut self.text_messages); + } + + pub fn set_excluded_items(&mut self, excluded_items: String) { + self.excluded_items.set_excluded_items(excluded_items, &mut self.text_messages); + } + + /// Check files for any with size == 0 + fn check_files(&mut self) { + let start_time: SystemTime = SystemTime::now(); + let mut folders_to_check: Vec = Vec::with_capacity(1024 * 2); // This should be small enough too not see to big difference and big enough to store most of paths without needing to resize vector + + // Add root folders for finding + for id in &self.directories.included_directories { + folders_to_check.push(id.to_string()); + } + self.information.number_of_checked_folders += folders_to_check.len(); + + let mut current_folder: String; + let mut next_folder: String; + while !folders_to_check.is_empty() { + current_folder = folders_to_check.pop().unwrap(); + + // Read current dir, if permission are denied just go to next + let read_dir = match fs::read_dir(¤t_folder) { + Ok(t) => t, + Err(_) => { + self.text_messages.warnings.push("Cannot open dir ".to_string() + current_folder.as_str()); + continue; + } // Permissions denied + }; + + // Check every sub folder/file/link etc. + for entry in read_dir { + let entry_data = match entry { + Ok(t) => t, + Err(_) => { + self.text_messages.warnings.push("Cannot read entry in dir ".to_string() + current_folder.as_str()); + continue; + } //Permissions denied + }; + let metadata: Metadata = match entry_data.metadata() { + Ok(t) => t, + Err(_) => { + self.text_messages.warnings.push("Cannot read metadata in dir ".to_string() + current_folder.as_str()); + continue; + } //Permissions denied + }; + if metadata.is_dir() { + self.information.number_of_checked_folders += 1; + // if entry_data.file_name().into_string().is_err() { // Probably this can be removed, if crash still will be happens, then uncomment this line + // self.text_messages.warnings.push("Cannot read folder name in dir ".to_string() + current_folder.as_str()); + // continue; // Permissions denied + // } + + if !self.recursive_search { + continue; + } + + let mut is_excluded_dir = false; + next_folder = "".to_owned() + ¤t_folder + &entry_data.file_name().into_string().unwrap() + "/"; + + for ed in &self.directories.excluded_directories { + if next_folder == *ed { + is_excluded_dir = true; + break; + } + } + if !is_excluded_dir { + let mut found_expression: bool = false; + for expression in &self.excluded_items.items { + if Common::regex_check(expression, &next_folder) { + found_expression = true; + break; + } + } + if found_expression { + break; + } + folders_to_check.push(next_folder); + } + } else if metadata.is_file() { + let mut have_valid_extension: bool; + let file_name_lowercase: String = entry_data.file_name().into_string().unwrap().to_lowercase(); + + // Checking allowed extensions + if !self.allowed_extensions.file_extensions.is_empty() { + have_valid_extension = false; + for extension in &self.allowed_extensions.file_extensions { + if file_name_lowercase.ends_with((".".to_string() + extension.to_lowercase().as_str()).as_str()) { + have_valid_extension = true; + break; + } + } + } else { + have_valid_extension = true; + } + + // Checking files + if metadata.len() == 0 && have_valid_extension { + let current_file_name = "".to_owned() + ¤t_folder + &entry_data.file_name().into_string().unwrap(); + + // Checking expressions + let mut found_expression: bool = false; + for expression in &self.excluded_items.items { + if Common::regex_check(expression, ¤t_file_name) { + found_expression = true; + break; + } + } + if found_expression { + break; + } + + // Creating new file entry + let fe: FileEntry = FileEntry { + path: current_file_name.clone(), + created_date: match metadata.created() { + Ok(t) => t, + Err(_) => { + self.text_messages.warnings.push("Unable to get creation date from file ".to_string() + current_file_name.as_str()); + continue; + } // Permissions Denied + }, + modified_date: match metadata.modified() { + Ok(t) => t, + Err(_) => { + self.text_messages.warnings.push("Unable to get modification date from file ".to_string() + current_file_name.as_str()); + continue; + } // Permissions Denied + }, + }; + + // Adding files to Vector + self.empty_files.push(fe); + + self.information.number_of_checked_files += 1; + } else { + self.information.number_of_ignored_files += 1; + } + } else { + // Probably this is symbolic links so we are free to ignore this + self.information.number_of_ignored_things += 1; + } + } + } + self.information.number_of_empty_files = self.empty_files.len(); + + Common::print_time(start_time, SystemTime::now(), "check_files_size".to_string()); + } + + /// Function to delete files, from filed Vector + fn delete_files(&mut self) { + let start_time: SystemTime = SystemTime::now(); + + match self.delete_method { + DeleteMethod::Delete => { + for file_entry in &self.empty_files { + if fs::remove_file(file_entry.path.clone()).is_err() { + self.text_messages.warnings.push(file_entry.path.clone()); + } + } + } + DeleteMethod::None => { + //Just do nothing + } + } + + Common::print_time(start_time, SystemTime::now(), "delete_files".to_string()); + } +} +impl Default for EmptyFiles { + fn default() -> Self { + Self::new() + } +} + +impl DebugPrint for EmptyFiles { + #[allow(dead_code)] + #[allow(unreachable_code)] + /// Debugging printing - only available on debug build + fn debug_print(&self) { + #[cfg(not(debug_assertions))] + { + return; + } + println!("---------------DEBUG PRINT---------------"); + println!("### Information's"); + + println!("Errors size - {}", self.text_messages.errors.len()); + println!("Warnings size - {}", self.text_messages.warnings.len()); + println!("Messages size - {}", self.text_messages.messages.len()); + println!("Number of checked files - {}", self.information.number_of_checked_files); + println!("Number of checked folders - {}", self.information.number_of_checked_folders); + println!("Number of ignored files - {}", self.information.number_of_ignored_files); + println!("Number of ignored things(like symbolic links) - {}", self.information.number_of_ignored_things); + println!("Number of removed files - {}", self.information.number_of_removed_files); + println!("Number of failed to remove files - {}", self.information.number_of_failed_to_remove_files); + + println!("### Other"); + + println!("Empty list size - {}", self.empty_files.len()); + println!("Allowed extensions - {:?}", self.allowed_extensions.file_extensions); + println!("Excluded items - {:?}", self.excluded_items.items); + println!("Included directories - {:?}", self.directories.included_directories); + println!("Excluded directories - {:?}", self.directories.excluded_directories); + println!("Recursive search - {}", self.recursive_search.to_string()); + println!("Delete Method - {:?}", self.delete_method); + println!("-----------------------------------------"); + } +} +impl SaveResults for EmptyFiles { + fn save_results_to_file(&mut self, file_name: &str) -> bool { + let start_time: SystemTime = SystemTime::now(); + let file_name: String = match file_name { + "" => "results.txt".to_string(), + k => k.to_string(), + }; + + let mut file = match File::create(&file_name) { + Ok(t) => t, + Err(_) => { + self.text_messages.errors.push(format!("Failed to create file {}", file_name)); + return false; + } + }; + + match file.write_all( + format!( + "Results of searching {:?} with excluded directories {:?} and excluded items {:?}\n", + self.directories.included_directories, self.directories.excluded_directories, self.excluded_items.items + ) + .as_bytes(), + ) { + Ok(_) => (), + Err(_) => { + self.text_messages.errors.push(format!("Failed to save results to file {}", file_name)); + return false; + } + } + + if !self.empty_files.is_empty() { + file.write_all(format!("Found {} empty files.\n", self.information.number_of_empty_files).as_bytes()).unwrap(); + for file_entry in self.empty_files.iter() { + file.write_all(format!("{} \n", file_entry.path).as_bytes()).unwrap(); + } + } else { + file.write_all(b"Not found any empty files.").unwrap(); + } + Common::print_time(start_time, SystemTime::now(), "save_results_to_file".to_string()); + true + } +} +impl PrintResults for EmptyFiles { + /// Print information's about duplicated entries + /// Only needed for CLI + fn print_results(&self) { + let start_time: SystemTime = SystemTime::now(); + println!("Found {} empty files.\n", self.information.number_of_empty_files); + for file_entry in self.empty_files.iter() { + println!("{}", file_entry.path); + } + + Common::print_time(start_time, SystemTime::now(), "print_entries".to_string()); + } +} diff --git a/czkawka_core/src/empty_folder.rs b/czkawka_core/src/empty_folder.rs index 4379dc5..504826e 100644 --- a/czkawka_core/src/empty_folder.rs +++ b/czkawka_core/src/empty_folder.rs @@ -288,7 +288,7 @@ impl SaveResults for EmptyFolder { } }; - match file.write_all(format!("Results of searching in {:?}\n", self.directories.included_directories).as_bytes()) { + match file.write_all(format!("Results of searching {:?} with excluded directories {:?}\n", self.directories.included_directories, self.directories.excluded_directories).as_bytes()) { Ok(_) => (), Err(_) => { self.text_messages.errors.push("Failed to save results to file ".to_string() + file_name.as_str()); @@ -316,8 +316,8 @@ impl PrintResults for EmptyFolder { if !self.empty_folder_list.is_empty() { println!("Found {} empty folders", self.empty_folder_list.len()); } - for i in &self.empty_folder_list { - println!("{}", i.0); + for name in self.empty_folder_list.keys() { + println!("{}", name); } } } diff --git a/czkawka_core/src/lib.rs b/czkawka_core/src/lib.rs index ddf7afd..7a3dc02 100644 --- a/czkawka_core/src/lib.rs +++ b/czkawka_core/src/lib.rs @@ -1,5 +1,6 @@ pub mod big_file; pub mod duplicate; +pub mod empty_files; pub mod empty_folder; pub mod common;