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 rayon::prelude::*; 15 use std::io::BufWriter; 16 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; 17 use std::sync::Arc; 18 use std::thread::sleep; 19 20 #[derive(Debug)] 21 pub struct ProgressData { 22 pub current_stage: u8, 23 pub max_stage: u8, 24 pub files_checked: usize, 25 pub files_to_check: usize, 26 } 27 28 #[derive(Eq, PartialEq, Clone, Debug)] 29 pub enum DeleteMethod { 30 None, 31 Delete, 32 } 33 34 #[derive(Clone)] 35 pub struct FileEntry { 36 pub path: PathBuf, 37 pub size: u64, 38 pub modified_date: u64, 39 } 40 41 /// Info struck with helpful information's about results 42 #[derive(Default)] 43 pub struct Info { 44 pub number_of_zeroed_files: usize, 45 pub number_of_removed_files: usize, 46 pub number_of_failed_to_remove_files: usize, 47 } 48 impl Info { new() -> Self49 pub fn new() -> Self { 50 Default::default() 51 } 52 } 53 54 /// Struct with required information's to work 55 pub struct ZeroedFiles { 56 text_messages: Messages, 57 information: Info, 58 zeroed_files: Vec<FileEntry>, 59 directories: Directories, 60 allowed_extensions: Extensions, 61 excluded_items: ExcludedItems, 62 recursive_search: bool, 63 delete_method: DeleteMethod, 64 stopped_search: bool, 65 minimal_file_size: u64, 66 maximal_file_size: u64, 67 files_to_check: Vec<FileEntry>, 68 } 69 70 impl ZeroedFiles { new() -> Self71 pub fn new() -> Self { 72 Self { 73 text_messages: Messages::new(), 74 information: Info::new(), 75 recursive_search: true, 76 allowed_extensions: Extensions::new(), 77 directories: Directories::new(), 78 excluded_items: ExcludedItems::new(), 79 zeroed_files: vec![], 80 delete_method: DeleteMethod::None, 81 stopped_search: false, 82 minimal_file_size: 8192, 83 maximal_file_size: u64::MAX, 84 files_to_check: Vec::with_capacity(1024), 85 } 86 } 87 find_zeroed_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>)88 pub fn find_zeroed_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) { 89 self.directories.optimize_directories(self.recursive_search, &mut self.text_messages); 90 if !self.check_files(stop_receiver, progress_sender) { 91 self.stopped_search = true; 92 return; 93 } 94 if !self.check_for_zeroed_files(stop_receiver, progress_sender) { 95 self.stopped_search = true; 96 return; 97 } 98 self.delete_files(); 99 self.debug_print(); 100 } 101 get_stopped_search(&self) -> bool102 pub fn get_stopped_search(&self) -> bool { 103 self.stopped_search 104 } 105 get_zeroed_files(&self) -> &Vec<FileEntry>106 pub const fn get_zeroed_files(&self) -> &Vec<FileEntry> { 107 &self.zeroed_files 108 } 109 get_text_messages(&self) -> &Messages110 pub const fn get_text_messages(&self) -> &Messages { 111 &self.text_messages 112 } 113 get_information(&self) -> &Info114 pub const fn get_information(&self) -> &Info { 115 &self.information 116 } 117 set_delete_method(&mut self, delete_method: DeleteMethod)118 pub fn set_delete_method(&mut self, delete_method: DeleteMethod) { 119 self.delete_method = delete_method; 120 } 121 set_minimal_file_size(&mut self, minimal_file_size: u64)122 pub fn set_minimal_file_size(&mut self, minimal_file_size: u64) { 123 self.minimal_file_size = match minimal_file_size { 124 0 => 1, 125 t => t, 126 }; 127 } set_maximal_file_size(&mut self, maximal_file_size: u64)128 pub fn set_maximal_file_size(&mut self, maximal_file_size: u64) { 129 self.maximal_file_size = match maximal_file_size { 130 0 => 1, 131 t => t, 132 }; 133 } 134 set_recursive_search(&mut self, recursive_search: bool)135 pub fn set_recursive_search(&mut self, recursive_search: bool) { 136 self.recursive_search = recursive_search; 137 } 138 set_included_directory(&mut self, included_directory: Vec<PathBuf>) -> bool139 pub fn set_included_directory(&mut self, included_directory: Vec<PathBuf>) -> bool { 140 self.directories.set_included_directory(included_directory, &mut self.text_messages) 141 } 142 set_excluded_directory(&mut self, excluded_directory: Vec<PathBuf>)143 pub fn set_excluded_directory(&mut self, excluded_directory: Vec<PathBuf>) { 144 self.directories.set_excluded_directory(excluded_directory, &mut self.text_messages); 145 } set_allowed_extensions(&mut self, allowed_extensions: String)146 pub fn set_allowed_extensions(&mut self, allowed_extensions: String) { 147 self.allowed_extensions.set_allowed_extensions(allowed_extensions, &mut self.text_messages); 148 } 149 set_excluded_items(&mut self, excluded_items: Vec<String>)150 pub fn set_excluded_items(&mut self, excluded_items: Vec<String>) { 151 self.excluded_items.set_excluded_items(excluded_items, &mut self.text_messages); 152 } 153 154 /// Check files for files which have 0 check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool155 fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool { 156 let start_time: SystemTime = SystemTime::now(); 157 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 158 159 // Add root folders for finding 160 for id in &self.directories.included_directories { 161 folders_to_check.push(id.clone()); 162 } 163 164 //// PROGRESS THREAD START 165 const LOOP_DURATION: u32 = 200; //in ms 166 let progress_thread_run = Arc::new(AtomicBool::new(true)); 167 168 let atomic_file_counter = Arc::new(AtomicUsize::new(0)); 169 170 let progress_thread_handle; 171 if let Some(progress_sender) = progress_sender { 172 let progress_send = progress_sender.clone(); 173 let progress_thread_run = progress_thread_run.clone(); 174 let atomic_file_counter = atomic_file_counter.clone(); 175 progress_thread_handle = thread::spawn(move || loop { 176 progress_send 177 .unbounded_send(ProgressData { 178 current_stage: 0, 179 max_stage: 1, 180 files_checked: atomic_file_counter.load(Ordering::Relaxed) as usize, 181 files_to_check: 0, 182 }) 183 .unwrap(); 184 if !progress_thread_run.load(Ordering::Relaxed) { 185 break; 186 } 187 sleep(Duration::from_millis(LOOP_DURATION as u64)); 188 }); 189 } else { 190 progress_thread_handle = thread::spawn(|| {}); 191 } 192 //// PROGRESS THREAD END 193 while !folders_to_check.is_empty() { 194 if stop_receiver.is_some() && stop_receiver.unwrap().try_recv().is_ok() { 195 // End thread which send info to gui 196 progress_thread_run.store(false, Ordering::Relaxed); 197 progress_thread_handle.join().unwrap(); 198 return false; 199 } 200 let current_folder = folders_to_check.pop().unwrap(); 201 202 // Read current dir, if permission are denied just go to next 203 let read_dir = match fs::read_dir(¤t_folder) { 204 Ok(t) => t, 205 Err(e) => { 206 self.text_messages.warnings.push(format!("Cannot open dir {}, reason {}", current_folder.display(), e)); 207 continue; 208 } // Permissions denied 209 }; 210 211 // Check every sub folder/file/link etc. 212 'dir: for entry in read_dir { 213 let entry_data = match entry { 214 Ok(t) => t, 215 Err(e) => { 216 self.text_messages.warnings.push(format!("Cannot read entry in dir {}, reason {}", current_folder.display(), e)); 217 continue; 218 } //Permissions denied 219 }; 220 let metadata: Metadata = match entry_data.metadata() { 221 Ok(t) => t, 222 Err(e) => { 223 self.text_messages.warnings.push(format!("Cannot read metadata in dir {}, reason {}", current_folder.display(), e)); 224 continue; 225 } //Permissions denied 226 }; 227 if metadata.is_dir() { 228 if !self.recursive_search { 229 continue; 230 } 231 232 let next_folder = current_folder.join(entry_data.file_name()); 233 if self.directories.is_excluded(&next_folder) || self.excluded_items.is_excluded(&next_folder) { 234 continue 'dir; 235 } 236 237 folders_to_check.push(next_folder); 238 } else if metadata.is_file() { 239 atomic_file_counter.fetch_add(1, Ordering::Relaxed); 240 if metadata.len() == 0 || !(self.minimal_file_size..=self.maximal_file_size).contains(&metadata.len()) { 241 continue 'dir; 242 } 243 244 let file_name_lowercase: String = match entry_data.file_name().into_string() { 245 Ok(t) => t, 246 Err(_inspected) => { 247 println!("File {:?} has not valid UTF-8 name", entry_data); 248 continue 'dir; 249 } 250 } 251 .to_lowercase(); 252 253 // Checking allowed extensions 254 if !self.allowed_extensions.file_extensions.is_empty() { 255 let allowed = self.allowed_extensions.file_extensions.iter().any(|e| file_name_lowercase.ends_with((".".to_string() + e.to_lowercase().as_str()).as_str())); 256 if !allowed { 257 // Not an allowed extension, ignore it. 258 continue 'dir; 259 } 260 } 261 // Checking files 262 let current_file_name = current_folder.join(entry_data.file_name()); 263 if self.excluded_items.is_excluded(¤t_file_name) { 264 continue 'dir; 265 } 266 267 // Creating new file entry 268 let fe: FileEntry = FileEntry { 269 path: current_file_name.clone(), 270 size: metadata.len(), 271 modified_date: match metadata.modified() { 272 Ok(t) => match t.duration_since(UNIX_EPOCH) { 273 Ok(d) => d.as_secs(), 274 Err(_inspected) => { 275 self.text_messages.warnings.push(format!("File {} seems to be modified before Unix Epoch.", current_file_name.display())); 276 0 277 } 278 }, 279 Err(e) => { 280 self.text_messages.warnings.push(format!("Unable to get modification date from file {}, reason {}", current_file_name.display(), e)); 281 0 282 } // Permissions Denied 283 }, 284 }; 285 286 // Adding files to Vector 287 self.files_to_check.push(fe); 288 } 289 } 290 } 291 // End thread which send info to gui 292 progress_thread_run.store(false, Ordering::Relaxed); 293 progress_thread_handle.join().unwrap(); 294 295 Common::print_time(start_time, SystemTime::now(), "check_files".to_string()); 296 true 297 } 298 299 /// Check files for files which have 0 check_for_zeroed_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool300 fn check_for_zeroed_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool { 301 let start_time: SystemTime = SystemTime::now(); 302 303 //// PROGRESS THREAD START 304 const LOOP_DURATION: u32 = 200; //in ms 305 let progress_thread_run = Arc::new(AtomicBool::new(true)); 306 307 let atomic_file_counter = Arc::new(AtomicUsize::new(0)); 308 309 let progress_thread_handle; 310 if let Some(progress_sender) = progress_sender { 311 let progress_send = progress_sender.clone(); 312 let progress_thread_run = progress_thread_run.clone(); 313 let atomic_file_counter = atomic_file_counter.clone(); 314 let files_to_check = self.files_to_check.len(); 315 progress_thread_handle = thread::spawn(move || loop { 316 progress_send 317 .unbounded_send(ProgressData { 318 current_stage: 1, 319 max_stage: 1, 320 files_checked: atomic_file_counter.load(Ordering::Relaxed) as usize, 321 files_to_check, 322 }) 323 .unwrap(); 324 if !progress_thread_run.load(Ordering::Relaxed) { 325 break; 326 } 327 sleep(Duration::from_millis(LOOP_DURATION as u64)); 328 }); 329 } else { 330 progress_thread_handle = thread::spawn(|| {}); 331 } 332 //// PROGRESS THREAD END 333 334 self.zeroed_files = self 335 .files_to_check 336 .par_iter() 337 .map(|file_entry| { 338 atomic_file_counter.fetch_add(1, Ordering::Relaxed); 339 if stop_receiver.is_some() && stop_receiver.unwrap().try_recv().is_ok() { 340 // This will not break 341 return None; 342 } 343 344 let file_entry = file_entry.clone(); 345 let mut n; 346 let mut file_handler: File = match File::open(&file_entry.path) { 347 Ok(t) => t, 348 Err(_inspected) => { 349 return Some(None); 350 } 351 }; 352 353 // First search 354 let mut buffer = [0u8; 64]; 355 n = match file_handler.read(&mut buffer) { 356 Ok(t) => t, 357 Err(_inspected) => { 358 return Some(None); 359 } 360 }; 361 for i in buffer[0..n].iter() { 362 if *i != 0 { 363 return Some(None); 364 } 365 } 366 // Second search 367 loop { 368 let mut buffer = [0u8; 1024 * 32]; 369 n = match file_handler.read(&mut buffer) { 370 Ok(t) => t, 371 Err(_inspected) => { 372 return Some(None); 373 } 374 }; 375 for i in buffer[0..n].iter() { 376 if *i != 0 { 377 return Some(None); 378 } 379 } 380 if n == 0 { 381 break; 382 } 383 } 384 385 Some(Some(file_entry)) 386 }) 387 .while_some() 388 .filter(|file_entry| file_entry.is_some()) 389 .map(|file_entry| file_entry.unwrap()) 390 .collect::<Vec<_>>(); 391 392 // End thread which send info to gui 393 progress_thread_run.store(false, Ordering::Relaxed); 394 progress_thread_handle.join().unwrap(); 395 396 self.information.number_of_zeroed_files = self.zeroed_files.len(); 397 398 Common::print_time(start_time, SystemTime::now(), "search for zeroed_files".to_string()); 399 400 //Clean unused data 401 self.files_to_check.clear(); 402 403 true 404 } 405 406 /// Function to delete files, from filed Vector delete_files(&mut self)407 fn delete_files(&mut self) { 408 let start_time: SystemTime = SystemTime::now(); 409 410 match self.delete_method { 411 DeleteMethod::Delete => { 412 for file_entry in &self.zeroed_files { 413 if fs::remove_file(file_entry.path.clone()).is_err() { 414 self.text_messages.warnings.push(file_entry.path.display().to_string()); 415 } 416 } 417 } 418 DeleteMethod::None => { 419 //Just do nothing 420 } 421 } 422 423 Common::print_time(start_time, SystemTime::now(), "delete_files".to_string()); 424 } 425 } 426 impl Default for ZeroedFiles { default() -> Self427 fn default() -> Self { 428 Self::new() 429 } 430 } 431 432 impl DebugPrint for ZeroedFiles { 433 #[allow(dead_code)] 434 #[allow(unreachable_code)] 435 /// Debugging printing - only available on debug build debug_print(&self)436 fn debug_print(&self) { 437 #[cfg(not(debug_assertions))] 438 { 439 return; 440 } 441 println!("---------------DEBUG PRINT---------------"); 442 println!("### Information's"); 443 444 println!("Errors size - {}", self.text_messages.errors.len()); 445 println!("Warnings size - {}", self.text_messages.warnings.len()); 446 println!("Messages size - {}", self.text_messages.messages.len()); 447 println!("Number of removed files - {}", self.information.number_of_removed_files); 448 println!("Number of failed to remove files - {}", self.information.number_of_failed_to_remove_files); 449 450 println!("### Other"); 451 452 println!("Zeroed list size - {}", self.zeroed_files.len()); 453 println!("Allowed extensions - {:?}", self.allowed_extensions.file_extensions); 454 println!("Excluded items - {:?}", self.excluded_items.items); 455 println!("Included directories - {:?}", self.directories.included_directories); 456 println!("Excluded directories - {:?}", self.directories.excluded_directories); 457 println!("Recursive search - {}", self.recursive_search.to_string()); 458 println!("Delete Method - {:?}", self.delete_method); 459 println!("Minimal File Size - {:?}", self.minimal_file_size); 460 println!("-----------------------------------------"); 461 } 462 } 463 impl SaveResults for ZeroedFiles { save_results_to_file(&mut self, file_name: &str) -> bool464 fn save_results_to_file(&mut self, file_name: &str) -> bool { 465 let start_time: SystemTime = SystemTime::now(); 466 let file_name: String = match file_name { 467 "" => "results.txt".to_string(), 468 k => k.to_string(), 469 }; 470 471 let file_handler = match File::create(&file_name) { 472 Ok(t) => t, 473 Err(e) => { 474 self.text_messages.errors.push(format!("Failed to create file {}, reason {}", file_name, e)); 475 return false; 476 } 477 }; 478 let mut writer = BufWriter::new(file_handler); 479 480 if let Err(e) = writeln!( 481 writer, 482 "Results of searching {:?} with excluded directories {:?} and excluded items {:?}", 483 self.directories.included_directories, self.directories.excluded_directories, self.excluded_items.items 484 ) { 485 self.text_messages.errors.push(format!("Failed to save results to file {}, reason {}", file_name, e)); 486 return false; 487 } 488 489 if !self.zeroed_files.is_empty() { 490 writeln!(writer, "Found {} zeroed files.", self.information.number_of_zeroed_files).unwrap(); 491 for file_entry in self.zeroed_files.iter() { 492 writeln!(writer, "{}", file_entry.path.display()).unwrap(); 493 } 494 } else { 495 write!(writer, "Not found any zeroed files.").unwrap(); 496 } 497 Common::print_time(start_time, SystemTime::now(), "save_results_to_file".to_string()); 498 true 499 } 500 } 501 impl PrintResults for ZeroedFiles { 502 /// Print information's about duplicated entries 503 /// Only needed for CLI print_results(&self)504 fn print_results(&self) { 505 let start_time: SystemTime = SystemTime::now(); 506 println!("Found {} zeroed files.\n", self.information.number_of_zeroed_files); 507 for file_entry in self.zeroed_files.iter() { 508 println!("{}", file_entry.path.display()); 509 } 510 511 Common::print_time(start_time, SystemTime::now(), "print_entries".to_string()); 512 } 513 } 514