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(&current_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(&current_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