1 use std::fs::{File, Metadata}; 2 use std::io::prelude::*; 3 use std::path::PathBuf; 4 use std::time::{Duration, SystemTime, UNIX_EPOCH}; 5 use std::{fs, thread}; 6 7 use crate::common::Common; 8 use crate::common_directory::Directories; 9 use crate::common_extensions::Extensions; 10 use crate::common_items::ExcludedItems; 11 use crate::common_messages::Messages; 12 use crate::common_traits::*; 13 use crossbeam_channel::Receiver; 14 use std::io::BufWriter; 15 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; 16 use std::sync::Arc; 17 use std::thread::sleep; 18 19 #[derive(Debug)] 20 pub struct ProgressData { 21 pub current_stage: u8, 22 pub max_stage: u8, 23 pub files_checked: usize, 24 } 25 26 #[derive(Eq, PartialEq, Clone, Debug)] 27 pub enum DeleteMethod { 28 None, 29 Delete, 30 } 31 32 #[derive(Clone)] 33 pub struct FileEntry { 34 pub path: PathBuf, 35 pub modified_date: u64, 36 } 37 38 /// Info struck with helpful information's about results 39 #[derive(Default)] 40 pub struct Info { 41 pub number_of_empty_files: usize, 42 pub number_of_removed_files: usize, 43 pub number_of_failed_to_remove_files: usize, 44 } 45 impl Info { new() -> Self46 pub fn new() -> Self { 47 Default::default() 48 } 49 } 50 51 /// Struct with required information's to work 52 pub struct EmptyFiles { 53 text_messages: Messages, 54 information: Info, 55 empty_files: Vec<FileEntry>, 56 directories: Directories, 57 allowed_extensions: Extensions, 58 excluded_items: ExcludedItems, 59 recursive_search: bool, 60 delete_method: DeleteMethod, 61 stopped_search: bool, 62 } 63 64 impl EmptyFiles { new() -> Self65 pub fn new() -> Self { 66 Self { 67 text_messages: Messages::new(), 68 information: Info::new(), 69 recursive_search: true, 70 allowed_extensions: Extensions::new(), 71 directories: Directories::new(), 72 excluded_items: ExcludedItems::new(), 73 empty_files: vec![], 74 delete_method: DeleteMethod::None, 75 stopped_search: false, 76 } 77 } 78 79 /// Finding empty files, save results to internal struct variables find_empty_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>)80 pub fn find_empty_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) { 81 self.directories.optimize_directories(self.recursive_search, &mut self.text_messages); 82 if !self.check_files(stop_receiver, progress_sender) { 83 self.stopped_search = true; 84 return; 85 } 86 self.delete_files(); 87 self.debug_print(); 88 } 89 get_stopped_search(&self) -> bool90 pub fn get_stopped_search(&self) -> bool { 91 self.stopped_search 92 } 93 get_empty_files(&self) -> &Vec<FileEntry>94 pub const fn get_empty_files(&self) -> &Vec<FileEntry> { 95 &self.empty_files 96 } 97 get_text_messages(&self) -> &Messages98 pub const fn get_text_messages(&self) -> &Messages { 99 &self.text_messages 100 } 101 get_information(&self) -> &Info102 pub const fn get_information(&self) -> &Info { 103 &self.information 104 } 105 set_delete_method(&mut self, delete_method: DeleteMethod)106 pub fn set_delete_method(&mut self, delete_method: DeleteMethod) { 107 self.delete_method = delete_method; 108 } 109 set_recursive_search(&mut self, recursive_search: bool)110 pub fn set_recursive_search(&mut self, recursive_search: bool) { 111 self.recursive_search = recursive_search; 112 } 113 set_included_directory(&mut self, included_directory: Vec<PathBuf>) -> bool114 pub fn set_included_directory(&mut self, included_directory: Vec<PathBuf>) -> bool { 115 self.directories.set_included_directory(included_directory, &mut self.text_messages) 116 } 117 set_excluded_directory(&mut self, excluded_directory: Vec<PathBuf>)118 pub fn set_excluded_directory(&mut self, excluded_directory: Vec<PathBuf>) { 119 self.directories.set_excluded_directory(excluded_directory, &mut self.text_messages); 120 } set_allowed_extensions(&mut self, allowed_extensions: String)121 pub fn set_allowed_extensions(&mut self, allowed_extensions: String) { 122 self.allowed_extensions.set_allowed_extensions(allowed_extensions, &mut self.text_messages); 123 } 124 set_excluded_items(&mut self, excluded_items: Vec<String>)125 pub fn set_excluded_items(&mut self, excluded_items: Vec<String>) { 126 self.excluded_items.set_excluded_items(excluded_items, &mut self.text_messages); 127 } 128 129 /// Check files for any with size == 0 check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool130 fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool { 131 let start_time: SystemTime = SystemTime::now(); 132 let mut folders_to_check: Vec<PathBuf> = 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 133 134 // Add root folders for finding 135 for id in &self.directories.included_directories { 136 folders_to_check.push(id.clone()); 137 } 138 139 //// PROGRESS THREAD START 140 const LOOP_DURATION: u32 = 200; //in ms 141 let progress_thread_run = Arc::new(AtomicBool::new(true)); 142 143 let atomic_file_counter = Arc::new(AtomicUsize::new(0)); 144 145 let progress_thread_handle; 146 if let Some(progress_sender) = progress_sender { 147 let progress_send = progress_sender.clone(); 148 let progress_thread_run = progress_thread_run.clone(); 149 let atomic_file_counter = atomic_file_counter.clone(); 150 progress_thread_handle = thread::spawn(move || loop { 151 progress_send 152 .unbounded_send(ProgressData { 153 current_stage: 0, 154 max_stage: 0, 155 files_checked: atomic_file_counter.load(Ordering::Relaxed) as usize, 156 }) 157 .unwrap(); 158 if !progress_thread_run.load(Ordering::Relaxed) { 159 break; 160 } 161 sleep(Duration::from_millis(LOOP_DURATION as u64)); 162 }); 163 } else { 164 progress_thread_handle = thread::spawn(|| {}); 165 } 166 //// PROGRESS THREAD END 167 168 while !folders_to_check.is_empty() { 169 if stop_receiver.is_some() && stop_receiver.unwrap().try_recv().is_ok() { 170 // End thread which send info to gui 171 progress_thread_run.store(false, Ordering::Relaxed); 172 progress_thread_handle.join().unwrap(); 173 return false; 174 } 175 let current_folder = folders_to_check.pop().unwrap(); 176 177 // Read current dir, if permission are denied just go to next 178 let read_dir = match fs::read_dir(¤t_folder) { 179 Ok(t) => t, 180 Err(e) => { 181 self.text_messages.warnings.push(format!("Cannot open dir {}, reason {}", current_folder.display(), e)); 182 continue; 183 } // Permissions denied 184 }; 185 186 // Check every sub folder/file/link etc. 187 'dir: for entry in read_dir { 188 let entry_data = match entry { 189 Ok(t) => t, 190 Err(e) => { 191 self.text_messages.warnings.push(format!("Cannot read entry in dir {}, reason {}", current_folder.display(), e)); 192 continue; 193 } //Permissions denied 194 }; 195 let metadata: Metadata = match entry_data.metadata() { 196 Ok(t) => t, 197 Err(e) => { 198 self.text_messages.warnings.push(format!("Cannot read metadata in dir {}, reason {}", current_folder.display(), e)); 199 continue; 200 } //Permissions denied 201 }; 202 if metadata.is_dir() { 203 if !self.recursive_search { 204 continue; 205 } 206 207 let next_folder = current_folder.join(entry_data.file_name()); 208 if self.directories.is_excluded(&next_folder) || self.excluded_items.is_excluded(&next_folder) { 209 continue 'dir; 210 } 211 212 folders_to_check.push(next_folder); 213 } else if metadata.is_file() { 214 atomic_file_counter.fetch_add(1, Ordering::Relaxed); 215 let file_name_lowercase: String = match entry_data.file_name().into_string() { 216 Ok(t) => t, 217 Err(_inspected) => { 218 println!("File {:?} has not valid UTF-8 name", entry_data); 219 continue 'dir; 220 } 221 } 222 .to_lowercase(); 223 224 // Checking allowed extensions 225 if !self.allowed_extensions.file_extensions.is_empty() { 226 let allowed = self.allowed_extensions.file_extensions.iter().any(|e| file_name_lowercase.ends_with((".".to_string() + e.to_lowercase().as_str()).as_str())); 227 if !allowed { 228 // Not an allowed extension, ignore it. 229 continue 'dir; 230 } 231 } 232 // Checking files 233 if metadata.len() == 0 { 234 let current_file_name = current_folder.join(entry_data.file_name()); 235 if self.excluded_items.is_excluded(¤t_file_name) { 236 continue 'dir; 237 } 238 239 // Creating new file entry 240 let fe: FileEntry = FileEntry { 241 path: current_file_name.clone(), 242 modified_date: match metadata.modified() { 243 Ok(t) => match t.duration_since(UNIX_EPOCH) { 244 Ok(d) => d.as_secs(), 245 Err(_inspected) => { 246 self.text_messages.warnings.push(format!("File {} seems to be modified before Unix Epoch.", current_file_name.display())); 247 0 248 } 249 }, 250 Err(e) => { 251 self.text_messages.warnings.push(format!("Unable to get modification date from file {}, reason {}", current_file_name.display(), e)); 252 0 253 } // Permissions Denied 254 }, 255 }; 256 257 // Adding files to Vector 258 self.empty_files.push(fe); 259 } 260 } 261 } 262 } 263 self.information.number_of_empty_files = self.empty_files.len(); 264 // End thread which send info to gui 265 progress_thread_run.store(false, Ordering::Relaxed); 266 progress_thread_handle.join().unwrap(); 267 268 Common::print_time(start_time, SystemTime::now(), "check_files_size".to_string()); 269 true 270 } 271 272 /// Function to delete files, from filed Vector delete_files(&mut self)273 fn delete_files(&mut self) { 274 let start_time: SystemTime = SystemTime::now(); 275 276 match self.delete_method { 277 DeleteMethod::Delete => { 278 for file_entry in &self.empty_files { 279 if fs::remove_file(file_entry.path.clone()).is_err() { 280 self.text_messages.warnings.push(file_entry.path.display().to_string()); 281 } 282 } 283 } 284 DeleteMethod::None => { 285 //Just do nothing 286 } 287 } 288 289 Common::print_time(start_time, SystemTime::now(), "delete_files".to_string()); 290 } 291 } 292 impl Default for EmptyFiles { default() -> Self293 fn default() -> Self { 294 Self::new() 295 } 296 } 297 298 impl DebugPrint for EmptyFiles { 299 #[allow(dead_code)] 300 #[allow(unreachable_code)] 301 /// Debugging printing - only available on debug build debug_print(&self)302 fn debug_print(&self) { 303 #[cfg(not(debug_assertions))] 304 { 305 return; 306 } 307 println!("---------------DEBUG PRINT---------------"); 308 println!("### Information's"); 309 310 println!("Errors size - {}", self.text_messages.errors.len()); 311 println!("Warnings size - {}", self.text_messages.warnings.len()); 312 println!("Messages size - {}", self.text_messages.messages.len()); 313 println!("Number of removed files - {}", self.information.number_of_removed_files); 314 println!("Number of failed to remove files - {}", self.information.number_of_failed_to_remove_files); 315 316 println!("### Other"); 317 318 println!("Empty list size - {}", self.empty_files.len()); 319 println!("Allowed extensions - {:?}", self.allowed_extensions.file_extensions); 320 println!("Excluded items - {:?}", self.excluded_items.items); 321 println!("Included directories - {:?}", self.directories.included_directories); 322 println!("Excluded directories - {:?}", self.directories.excluded_directories); 323 println!("Recursive search - {}", self.recursive_search.to_string()); 324 println!("Delete Method - {:?}", self.delete_method); 325 println!("-----------------------------------------"); 326 } 327 } 328 impl SaveResults for EmptyFiles { save_results_to_file(&mut self, file_name: &str) -> bool329 fn save_results_to_file(&mut self, file_name: &str) -> bool { 330 let start_time: SystemTime = SystemTime::now(); 331 let file_name: String = match file_name { 332 "" => "results.txt".to_string(), 333 k => k.to_string(), 334 }; 335 336 let file_handler = match File::create(&file_name) { 337 Ok(t) => t, 338 Err(e) => { 339 self.text_messages.errors.push(format!("Failed to create file {}, reason {}", file_name, e)); 340 return false; 341 } 342 }; 343 let mut writer = BufWriter::new(file_handler); 344 345 if let Err(e) = writeln!( 346 writer, 347 "Results of searching {:?} with excluded directories {:?} and excluded items {:?}", 348 self.directories.included_directories, self.directories.excluded_directories, self.excluded_items.items 349 ) { 350 self.text_messages.errors.push(format!("Failed to save results to file {}, reason {}", file_name, e)); 351 return false; 352 } 353 354 if !self.empty_files.is_empty() { 355 writeln!(writer, "Found {} empty files.", self.information.number_of_empty_files).unwrap(); 356 for file_entry in self.empty_files.iter() { 357 writeln!(writer, "{}", file_entry.path.display()).unwrap(); 358 } 359 } else { 360 write!(writer, "Not found any empty files.").unwrap(); 361 } 362 Common::print_time(start_time, SystemTime::now(), "save_results_to_file".to_string()); 363 true 364 } 365 } 366 impl PrintResults for EmptyFiles { 367 /// Print information's about duplicated entries 368 /// Only needed for CLI print_results(&self)369 fn print_results(&self) { 370 let start_time: SystemTime = SystemTime::now(); 371 println!("Found {} empty files.\n", self.information.number_of_empty_files); 372 for file_entry in self.empty_files.iter() { 373 println!("{}", file_entry.path.display()); 374 } 375 376 Common::print_time(start_time, SystemTime::now(), "print_entries".to_string()); 377 } 378 } 379