use std::collections::HashMap; use std::fs; use std::fs::DirEntry; use std::io::Write; use std::path::{Path, PathBuf}; use std::sync::atomic::Ordering; use crossbeam_channel::{Receiver, Sender}; use fun_time::fun_time; use log::debug; use rayon::prelude::*; use crate::common::{check_if_stop_received, prepare_thread_handler_common, send_info_and_wait_for_ending_all_threads}; use crate::common_dir_traversal::{common_get_entry_data, common_get_metadata_dir, common_read_dir, get_modified_time, CheckingMethod, ProgressData, ToolType}; use crate::common_directory::Directories; use crate::common_items::ExcludedItems; use crate::common_tool::{CommonData, CommonToolData, DeleteMethod}; use crate::common_traits::{DebugPrint, PrintResults}; #[derive(Clone, Debug)] pub struct FolderEntry { pub path: PathBuf, pub(crate) parent_path: Option, // Usable only when finding pub(crate) is_empty: FolderEmptiness, pub modified_date: u64, } impl FolderEntry { pub fn get_modified_date(&self) -> u64 { self.modified_date } } pub struct EmptyFolder { common_data: CommonToolData, information: Info, empty_folder_list: HashMap, // Path, FolderEntry } /// Enum with values which show if folder is empty. /// In function "`optimize_folders`" automatically "Maybe" is changed to "Yes", so it is not necessary to put it here #[derive(Eq, PartialEq, Copy, Clone, Debug)] pub(crate) enum FolderEmptiness { No, Maybe, } #[derive(Default)] pub struct Info { pub number_of_empty_folders: usize, } impl EmptyFolder { pub fn new() -> Self { Self { common_data: CommonToolData::new(ToolType::EmptyFolders), information: Default::default(), empty_folder_list: Default::default(), } } pub const fn get_empty_folder_list(&self) -> &HashMap { &self.empty_folder_list } pub const fn get_information(&self) -> &Info { &self.information } #[fun_time(message = "find_empty_folders", level = "info")] pub fn find_empty_folders(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender>) { self.prepare_items(); if !self.check_for_empty_folders(stop_receiver, progress_sender) { self.common_data.stopped_search = true; return; } self.optimize_folders(); self.delete_files(); self.debug_print(); } fn optimize_folders(&mut self) { let mut new_directory_folders: HashMap = Default::default(); for (name, folder_entry) in &self.empty_folder_list { match &folder_entry.parent_path { Some(t) => { if !self.empty_folder_list.contains_key(t) { new_directory_folders.insert(name.clone(), folder_entry.clone()); } } None => { new_directory_folders.insert(name.clone(), folder_entry.clone()); } } } self.empty_folder_list = new_directory_folders; self.information.number_of_empty_folders = self.empty_folder_list.len(); } #[fun_time(message = "check_for_empty_folders", level = "debug")] fn check_for_empty_folders(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&Sender>) -> bool { let mut folders_to_check: Vec = self.common_data.directories.included_directories.clone(); let (progress_thread_handle, progress_thread_run, atomic_counter, _check_was_stopped) = prepare_thread_handler_common(progress_sender, 0, 0, 0, CheckingMethod::None, self.common_data.tool_type); let excluded_items = self.common_data.excluded_items.clone(); let directories = self.common_data.directories.clone(); let mut non_empty_folders: Vec = vec![]; let mut start_folder_entries = Vec::with_capacity(folders_to_check.len()); let mut new_folder_entries_list = Vec::new(); for dir in &folders_to_check { start_folder_entries.push(FolderEntry { path: dir.clone(), parent_path: None, is_empty: FolderEmptiness::Maybe, modified_date: 0, }); } while !folders_to_check.is_empty() { if check_if_stop_received(stop_receiver) { send_info_and_wait_for_ending_all_threads(&progress_thread_run, progress_thread_handle); return false; } let segments: Vec<_> = folders_to_check .into_par_iter() .map(|current_folder| { let mut dir_result = vec![]; let mut warnings = vec![]; let mut non_empty_folder = None; let mut folder_entries_list = vec![]; let current_folder_as_string = current_folder.to_string_lossy().to_string(); let Some(read_dir) = common_read_dir(¤t_folder, &mut warnings) else { return (dir_result, warnings, Some(current_folder_as_string), folder_entries_list); }; let mut counter = 0; // Check every sub folder/file/link etc. for entry in read_dir { let Some(entry_data) = common_get_entry_data(&entry, &mut warnings, ¤t_folder) else { continue; }; let Ok(file_type) = entry_data.file_type() else { continue }; if file_type.is_dir() { counter += 1; Self::process_dir_in_dir_mode( ¤t_folder, ¤t_folder_as_string, entry_data, &directories, &mut dir_result, &mut warnings, &excluded_items, &mut non_empty_folder, &mut folder_entries_list, ); } else { if non_empty_folder.is_none() { non_empty_folder = Some(current_folder_as_string.clone()); } } } if counter > 0 { // Increase counter in batch, because usually it may be slow to add multiple times atomic value atomic_counter.fetch_add(counter, Ordering::Relaxed); } (dir_result, warnings, non_empty_folder, folder_entries_list) }) .collect(); let required_size = segments.iter().map(|(segment, _, _, _)| segment.len()).sum::(); folders_to_check = Vec::with_capacity(required_size); // Process collected data for (segment, warnings, non_empty_folder, fe_list) in segments { folders_to_check.extend(segment); if !warnings.is_empty() { self.common_data.text_messages.warnings.extend(warnings); } if let Some(non_empty_folder) = non_empty_folder { non_empty_folders.push(non_empty_folder); } new_folder_entries_list.push(fe_list); } } let mut folder_entries: HashMap = HashMap::with_capacity(start_folder_entries.len() + new_folder_entries_list.iter().map(Vec::len).sum::()); for fe in start_folder_entries { folder_entries.insert(fe.path.to_string_lossy().to_string(), fe); } for fe_list in new_folder_entries_list { for fe in fe_list { folder_entries.insert(fe.path.to_string_lossy().to_string(), fe); } } // Start to for current_folder in non_empty_folders.into_iter().rev() { Self::set_as_not_empty_folder(&mut folder_entries, ¤t_folder); } for (name, folder_entry) in folder_entries { if folder_entry.is_empty != FolderEmptiness::No { self.empty_folder_list.insert(name, folder_entry); } } debug!("Found {} empty folders.", self.empty_folder_list.len()); send_info_and_wait_for_ending_all_threads(&progress_thread_run, progress_thread_handle); true } pub(crate) fn set_as_not_empty_folder(folder_entries: &mut HashMap, current_folder: &str) { let mut d = folder_entries.get_mut(current_folder).unwrap(); if d.is_empty == FolderEmptiness::No { return; // Already set as non empty by one of his child } // Loop to recursively set as non empty this and all his parent folders loop { d.is_empty = FolderEmptiness::No; if d.parent_path.is_some() { let cf = d.parent_path.clone().unwrap(); d = folder_entries.get_mut(&cf).unwrap(); if d.is_empty == FolderEmptiness::No { break; // Already set as non empty, so one of child already set it to non empty } } else { break; } } } fn process_dir_in_dir_mode( current_folder: &Path, current_folder_as_str: &str, entry_data: &DirEntry, directories: &Directories, dir_result: &mut Vec, warnings: &mut Vec, excluded_items: &ExcludedItems, non_empty_folder: &mut Option, folder_entries_list: &mut Vec, ) { let next_folder = entry_data.path(); if excluded_items.is_excluded(&next_folder) || directories.is_excluded(&next_folder) { if non_empty_folder.is_none() { *non_empty_folder = Some(current_folder_as_str.to_string()); } return; } #[cfg(target_family = "unix")] if directories.exclude_other_filesystems() { match directories.is_on_other_filesystems(&next_folder) { Ok(true) => return, Err(e) => warnings.push(e), _ => (), } } let Some(metadata) = common_get_metadata_dir(entry_data, warnings, &next_folder) else { if non_empty_folder.is_none() { *non_empty_folder = Some(current_folder_as_str.to_string()); } return; }; dir_result.push(next_folder.clone()); folder_entries_list.push(FolderEntry { path: next_folder, parent_path: Some(current_folder_as_str.to_string()), is_empty: FolderEmptiness::Maybe, modified_date: get_modified_time(&metadata, warnings, current_folder, true), }); } #[fun_time(message = "delete_files", level = "debug")] fn delete_files(&mut self) { if self.get_delete_method() == DeleteMethod::None { return; } let folders_to_remove = self.empty_folder_list.keys().collect::>(); let errors: Vec<_> = folders_to_remove .into_par_iter() .filter_map(|name| { if let Err(e) = fs::remove_dir_all(name) { Some(format!("Failed to remove folder {name:?}, reason {e}")) } else { None } }) .collect(); self.get_text_messages_mut().errors.extend(errors); } } impl Default for EmptyFolder { fn default() -> Self { Self::new() } } impl DebugPrint for EmptyFolder { fn debug_print(&self) { if !cfg!(debug_assertions) { return; } println!("---------------DEBUG PRINT---------------"); println!("Number of empty folders - {}", self.information.number_of_empty_folders); self.debug_print_common(); println!("-----------------------------------------"); } } impl PrintResults for EmptyFolder { fn write_results(&self, writer: &mut T) -> std::io::Result<()> { if !self.empty_folder_list.is_empty() { writeln!(writer, "--------------------------Empty folder list--------------------------")?; writeln!(writer, "Found {} empty folders", self.information.number_of_empty_folders)?; let mut empty_folder_list = self.empty_folder_list.keys().collect::>(); empty_folder_list.par_sort_unstable(); for name in empty_folder_list { writeln!(writer, "{name}")?; } } else { write!(writer, "Not found any empty folders.")?; } Ok(()) } fn save_results_to_file_as_json(&self, file_name: &str, pretty_print: bool) -> std::io::Result<()> { self.save_results_to_file_as_json_internal(file_name, &self.empty_folder_list.keys().collect::>(), pretty_print) } } impl CommonData for EmptyFolder { fn get_cd(&self) -> &CommonToolData { &self.common_data } fn get_cd_mut(&mut self) -> &mut CommonToolData { &mut self.common_data } }