diff --git a/README.md b/README.md index 1ce8508..9b4f794 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ - Written in memory safe Rust - Amazingly fast - due using more or less advanced algorithms and multithreading support - Free, Open Source without ads -- Works on Linux, Windows and macOS +- Multiplatform - works on Linux, Windows and macOS - CLI frontend, very fast to automate tasks - GUI GTK frontend - uses modern GTK 3 and looks similar to FSlint - Light/Dark theme match the appearance of the system(Linux only) @@ -23,6 +23,7 @@ - Zeroed Files - Find files which are filled with zeros(usually corrupted) - Same Music - Search for music with same artist, album etc. - Invalid Symbolic Links - Shows symbolic links which points to non-existent files/directories + - Broken Files - Finds files with invalid extension or corrupted ![Czkawka](https://user-images.githubusercontent.com/41945903/100857797-69809680-348d-11eb-8382-acdec05fd3b8.gif) @@ -219,6 +220,7 @@ So still is a big room for improvements. | Zeroed Files| X | | | | Music duplicates(tags) | X | | X | | Invalid symlinks | X | X | | +| Broken Files | X | | | | Installed packages | | X | | | Invalid names | | X | | | Names conflict | | X | | diff --git a/czkawka_cli/src/commands.rs b/czkawka_cli/src/commands.rs index ce7c00d..e83a1bf 100644 --- a/czkawka_cli/src/commands.rs +++ b/czkawka_cli/src/commands.rs @@ -164,6 +164,23 @@ pub enum Commands { #[structopt(flatten)] not_recursive: NotRecursive, }, + #[structopt(name = "broken", about = "Finds broken files", help_message = HELP_MESSAGE, after_help = "EXAMPLE:\n czkawka broken -d /home/kicikici/ /home/szczek -e /home/kicikici/jestempsem -x jpg -f results.txt")] + BrokenFiles { + #[structopt(flatten)] + directories: Directories, + #[structopt(flatten)] + excluded_directories: ExcludedDirectories, + #[structopt(flatten)] + excluded_items: ExcludedItems, + #[structopt(flatten)] + allowed_extensions: AllowedExtensions, + #[structopt(short = "D", long, help = "Delete found files")] + delete_files: bool, + #[structopt(flatten)] + file_to_save: FileToSave, + #[structopt(flatten)] + not_recursive: NotRecursive, + }, } #[derive(Debug, StructOpt)] @@ -320,4 +337,5 @@ EXAMPLES: {bin} image -d /home/rafal -e /home/rafal/Pulpit -f results.txt {bin} zeroed -d /home/rafal -e /home/krzak -f results.txt" {bin} music -d /home/rafal -e /home/rafal/Pulpit -z "artist,year, ARTISTALBUM, ALBUM___tiTlE" -f results.txt - {bin} symlinks -d /home/kicikici/ /home/szczek -e /home/kicikici/jestempsem -x jpg -f results.txt"#; + {bin} symlinks -d /home/kicikici/ /home/szczek -e /home/kicikici/jestempsem -x jpg -f results.txt + {bin} broken -d /home/mikrut/ -e /home/mikrut/trakt -f results.txt"#; diff --git a/czkawka_cli/src/main.rs b/czkawka_cli/src/main.rs index 5706826..fe9ef37 100644 --- a/czkawka_cli/src/main.rs +++ b/czkawka_cli/src/main.rs @@ -7,6 +7,7 @@ use czkawka_core::common_traits::*; use czkawka_core::{ big_file::{self, BigFile}, + broken_files::{self, BrokenFiles}, duplicate::DuplicateFinder, empty_files::{self, EmptyFiles}, empty_folder::EmptyFolder, @@ -326,5 +327,39 @@ fn main() { ifs.print_results(); ifs.get_text_messages().print_messages(); } + Commands::BrokenFiles { + directories, + excluded_directories, + excluded_items, + allowed_extensions, + delete_files, + file_to_save, + not_recursive, + } => { + let mut br = BrokenFiles::new(); + + br.set_included_directory(directories.directories); + br.set_excluded_directory(excluded_directories.excluded_directories); + br.set_excluded_items(excluded_items.excluded_items); + br.set_allowed_extensions(allowed_extensions.allowed_extensions.join(",")); + br.set_recursive_search(!not_recursive.not_recursive); + + if delete_files { + br.set_delete_method(broken_files::DeleteMethod::Delete); + } + + br.find_broken_files(None, None); + + if let Some(file_name) = file_to_save.file_name() { + if !br.save_results_to_file(file_name) { + br.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 + br.print_results(); + br.get_text_messages().print_messages(); + } } } diff --git a/czkawka_core/src/broken_files.rs b/czkawka_core/src/broken_files.rs new file mode 100644 index 0000000..ede0b38 --- /dev/null +++ b/czkawka_core/src/broken_files.rs @@ -0,0 +1,474 @@ +use std::fs::{File, Metadata}; +use std::io::prelude::*; +use std::path::PathBuf; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::{fs, thread}; + +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::*; +use crossbeam_channel::Receiver; +use rayon::prelude::*; +use std::io::BufWriter; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::Arc; +use std::thread::sleep; + +#[derive(Debug)] +pub struct ProgressData { + pub current_stage: u8, + pub max_stage: u8, + pub files_checked: usize, + pub files_to_check: usize, +} + +#[derive(Eq, PartialEq, Clone, Debug)] +pub enum DeleteMethod { + None, + Delete, +} + +#[derive(Clone)] +pub struct FileEntry { + pub path: PathBuf, + pub modified_date: u64, + pub type_of_file: TypeOfFile, + pub error_string: String, +} + +#[derive(Clone, PartialEq, Eq)] +pub enum TypeOfFile { + Image, +} + +/// Info struck with helpful information's about results +#[derive(Default)] +pub struct Info { + pub number_of_broken_files: usize, + pub number_of_removed_files: usize, + pub number_of_failed_to_remove_files: usize, +} +impl Info { + pub fn new() -> Self { + Default::default() + } +} + +/// Struct with required information's to work +pub struct BrokenFiles { + text_messages: Messages, + information: Info, + files_to_check: Vec, + broken_files: Vec, + directories: Directories, + allowed_extensions: Extensions, + excluded_items: ExcludedItems, + recursive_search: bool, + delete_method: DeleteMethod, + stopped_search: bool, +} + +impl BrokenFiles { + pub fn new() -> Self { + Self { + text_messages: Messages::new(), + information: Info::new(), + recursive_search: true, + allowed_extensions: Extensions::new(), + directories: Directories::new(), + excluded_items: ExcludedItems::new(), + files_to_check: vec![], + delete_method: DeleteMethod::None, + stopped_search: false, + broken_files: vec![], + } + } + + pub fn find_broken_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::Sender>) { + self.directories.optimize_directories(self.recursive_search, &mut self.text_messages); + if !self.check_files(stop_receiver, progress_sender) { + self.stopped_search = true; + return; + } + if !self.look_for_broken_files(stop_receiver, progress_sender) { + self.stopped_search = true; + return; + } + self.delete_files(); + self.debug_print(); + } + + pub fn get_stopped_search(&self) -> bool { + self.stopped_search + } + + pub const fn get_broken_files(&self) -> &Vec { + &self.broken_files + } + + pub const fn get_text_messages(&self) -> &Messages { + &self.text_messages + } + + pub const 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: Vec) -> bool { + self.directories.set_included_directory(included_directory, &mut self.text_messages) + } + + pub fn set_excluded_directory(&mut self, excluded_directory: Vec) { + 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: Vec) { + self.excluded_items.set_excluded_items(excluded_items, &mut self.text_messages); + } + + fn check_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::Sender>) -> bool { + 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.clone()); + } + + //// PROGRESS THREAD START + const LOOP_DURATION: u32 = 200; //in ms + let progress_thread_run = Arc::new(AtomicBool::new(true)); + + let atomic_file_counter = Arc::new(AtomicUsize::new(0)); + + let progress_thread_handle; + if let Some(progress_sender) = progress_sender { + let mut progress_send = progress_sender.clone(); + let progress_thread_run = progress_thread_run.clone(); + let atomic_file_counter = atomic_file_counter.clone(); + progress_thread_handle = thread::spawn(move || loop { + progress_send + .try_send(ProgressData { + current_stage: 0, + max_stage: 1, + files_checked: atomic_file_counter.load(Ordering::Relaxed) as usize, + files_to_check: 0, + }) + .unwrap(); + if !progress_thread_run.load(Ordering::Relaxed) { + break; + } + sleep(Duration::from_millis(LOOP_DURATION as u64)); + }); + } else { + progress_thread_handle = thread::spawn(|| {}); + } + //// PROGRESS THREAD END + + while !folders_to_check.is_empty() { + if stop_receiver.is_some() && stop_receiver.unwrap().try_recv().is_ok() { + // End thread which send info to gui + progress_thread_run.store(false, Ordering::Relaxed); + progress_thread_handle.join().unwrap(); + return false; + } + let 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(format!("Cannot open dir {}", current_folder.display())); + continue; + } // Permissions denied + }; + + // Check every sub folder/file/link etc. + 'dir: for entry in read_dir { + let entry_data = match entry { + Ok(t) => t, + Err(_) => { + self.text_messages.warnings.push(format!("Cannot read entry in dir {}", current_folder.display())); + continue; + } //Permissions denied + }; + let metadata: Metadata = match entry_data.metadata() { + Ok(t) => t, + Err(_) => { + self.text_messages.warnings.push(format!("Cannot read metadata in dir {}", current_folder.display())); + continue; + } //Permissions denied + }; + if metadata.is_dir() { + if !self.recursive_search { + continue; + } + + let next_folder = current_folder.join(entry_data.file_name()); + if self.directories.is_excluded(&next_folder) || self.excluded_items.is_excluded(&next_folder) { + continue 'dir; + } + + folders_to_check.push(next_folder); + } else if metadata.is_file() { + atomic_file_counter.fetch_add(1, Ordering::Relaxed); + let file_name_lowercase: String = match entry_data.file_name().into_string() { + Ok(t) => t, + Err(_) => continue, + } + .to_lowercase(); + + let type_of_file; + + // Checking allowed image extensions + let allowed_image_extensions = ["jpg", "jpeg", "png", "bmp", "ico", "webp", "tiff", "pnm", "tga", "ff", "gif"]; + if allowed_image_extensions.iter().any(|e| file_name_lowercase.ends_with(format!(".{}", e).as_str())) { + type_of_file = TypeOfFile::Image; + } else { + continue 'dir; + } + + // Checking allowed extensions + if !self.allowed_extensions.file_extensions.is_empty() { + let allowed = self.allowed_extensions.file_extensions.iter().any(|e| file_name_lowercase.ends_with((".".to_string() + e.to_lowercase().as_str()).as_str())); + if !allowed { + // Not an allowed extension, ignore it. + continue 'dir; + } + } + + // Checking files + let current_file_name = current_folder.join(entry_data.file_name()); + if self.excluded_items.is_excluded(¤t_file_name) { + continue 'dir; + } + + // Creating new file entry + let fe: FileEntry = FileEntry { + path: current_file_name.clone(), + modified_date: match metadata.modified() { + Ok(t) => match t.duration_since(UNIX_EPOCH) { + Ok(d) => d.as_secs(), + Err(_) => { + self.text_messages.warnings.push(format!("File {} seems to be modified before Unix Epoch.", current_file_name.display())); + 0 + } + }, + Err(_) => { + self.text_messages.warnings.push(format!("Unable to get modification date from file {}", current_file_name.display())); + continue; + } // Permissions Denied + }, + type_of_file, + error_string: "".to_string(), + }; + + // Adding files to Vector + self.files_to_check.push(fe); + } + } + } + // End thread which send info to gui + progress_thread_run.store(false, Ordering::Relaxed); + progress_thread_handle.join().unwrap(); + + Common::print_time(start_time, SystemTime::now(), "check_files".to_string()); + true + } + fn look_for_broken_files(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::Sender>) -> bool { + let system_time = SystemTime::now(); + + let check_was_breaked = AtomicBool::new(false); // Used for breaking from GUI and ending check thread + + //// PROGRESS THREAD START + const LOOP_DURATION: u32 = 200; //in ms + let progress_thread_run = Arc::new(AtomicBool::new(true)); + let atomic_file_counter = Arc::new(AtomicUsize::new(0)); + + let progress_thread_handle; + if let Some(progress_sender) = progress_sender { + let mut progress_send = progress_sender.clone(); + let progress_thread_run = progress_thread_run.clone(); + let atomic_file_counter = atomic_file_counter.clone(); + let files_to_check = self.files_to_check.len(); + progress_thread_handle = thread::spawn(move || loop { + progress_send + .try_send(ProgressData { + current_stage: 1, + max_stage: 1, + files_checked: atomic_file_counter.load(Ordering::Relaxed) as usize, + files_to_check, + }) + .unwrap(); + if !progress_thread_run.load(Ordering::Relaxed) { + break; + } + sleep(Duration::from_millis(LOOP_DURATION as u64)); + }); + } else { + progress_thread_handle = thread::spawn(|| {}); + } + //// PROGRESS THREAD END + self.broken_files = self + .files_to_check + .par_iter() + .map(|file_entry| { + atomic_file_counter.fetch_add(1, Ordering::Relaxed); + if stop_receiver.is_some() && stop_receiver.unwrap().try_recv().is_ok() { + // This will not break + return None; + } + + match image::open(&file_entry.path) { + Ok(_) => Some(None), + Err(t) => { + let mut file_entry = file_entry.clone(); + file_entry.error_string = t.to_string(); + Some(Some(file_entry)) + } // Something is wrong with image + } + }) + .while_some() + .filter(|file_entry| file_entry.is_some()) + .map(|file_entry| file_entry.unwrap()) + .collect::>(); + + // End thread which send info to gui + progress_thread_run.store(false, Ordering::Relaxed); + progress_thread_handle.join().unwrap(); + + self.information.number_of_broken_files = self.broken_files.len(); + + // Check if user aborted search(only from GUI) + if check_was_breaked.load(Ordering::Relaxed) { + return false; + } + Common::print_time(system_time, SystemTime::now(), "sort_images - reading data from files in parallel".to_string()); + + // Clean data + self.files_to_check = vec![]; + + true + } + /// 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.files_to_check { + if fs::remove_file(&file_entry.path).is_err() { + self.text_messages.warnings.push(file_entry.path.display().to_string()); + } + } + } + DeleteMethod::None => { + //Just do nothing + } + } + + Common::print_time(start_time, SystemTime::now(), "delete_files".to_string()); + } +} +impl Default for BrokenFiles { + fn default() -> Self { + Self::new() + } +} + +impl DebugPrint for BrokenFiles { + #[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 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!("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 BrokenFiles { + 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 file_handler = match File::create(&file_name) { + Ok(t) => t, + Err(_) => { + self.text_messages.errors.push(format!("Failed to create file {}", file_name)); + return false; + } + }; + let mut writer = BufWriter::new(file_handler); + + if writeln!( + writer, + "Results of searching {:?} with excluded directories {:?} and excluded items {:?}", + self.directories.included_directories, self.directories.excluded_directories, self.excluded_items.items + ) + .is_err() + { + self.text_messages.errors.push(format!("Failed to save results to file {}", file_name)); + return false; + } + + if !self.broken_files.is_empty() { + writeln!(writer, "Found {} broken files.", self.information.number_of_broken_files).unwrap(); + for file_entry in self.broken_files.iter() { + writeln!(writer, "{} - {}", file_entry.path.display(), file_entry.error_string).unwrap(); + } + } else { + write!(writer, "Not found any broken files.").unwrap(); + } + Common::print_time(start_time, SystemTime::now(), "save_results_to_file".to_string()); + true + } +} +impl PrintResults for BrokenFiles { + /// Print information's about duplicated entries + /// Only needed for CLI + fn print_results(&self) { + let start_time: SystemTime = SystemTime::now(); + println!("Found {} broken files.\n", self.information.number_of_broken_files); + for file_entry in self.broken_files.iter() { + println!("{} - {}", file_entry.path.display(), file_entry.error_string); + } + + Common::print_time(start_time, SystemTime::now(), "print_entries".to_string()); + } +} diff --git a/czkawka_core/src/lib.rs b/czkawka_core/src/lib.rs index 81e8ff6..17ae5f5 100644 --- a/czkawka_core/src/lib.rs +++ b/czkawka_core/src/lib.rs @@ -2,10 +2,15 @@ extern crate bitflags; pub mod big_file; +pub mod broken_files; pub mod duplicate; pub mod empty_files; pub mod empty_folder; +pub mod invalid_symlinks; +pub mod same_music; +pub mod similar_images; pub mod temporary; +pub mod zeroed; pub mod common; pub mod common_directory; @@ -13,9 +18,5 @@ pub mod common_extensions; pub mod common_items; pub mod common_messages; pub mod common_traits; -pub mod invalid_symlinks; -pub mod same_music; -pub mod similar_images; -pub mod zeroed; pub const CZKAWKA_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/czkawka_core/src/similar_images.rs b/czkawka_core/src/similar_images.rs index 5714406..886e101 100644 --- a/czkawka_core/src/similar_images.rs +++ b/czkawka_core/src/similar_images.rs @@ -265,8 +265,8 @@ impl SimilarImages { .to_lowercase(); // Checking allowed image extensions - let allowed_image_extensions = ["jpg", "png", "bmp", "ico", "webp", "tiff", "dds"]; - if !allowed_image_extensions.iter().any(|e| file_name_lowercase.ends_with(e)) { + let allowed_image_extensions = ["jpg", "jpeg", "png", "bmp", "ico", "webp", "tiff", "pnm", "tga", "ff", "gif"]; + if !allowed_image_extensions.iter().any(|e| file_name_lowercase.ends_with(format!(".{}", e).as_str())) { continue 'dir; } diff --git a/czkawka_gui/czkawka.glade b/czkawka_gui/czkawka.glade index 1d1409c..e417d83 100644 --- a/czkawka_gui/czkawka.glade +++ b/czkawka_gui/czkawka.glade @@ -2031,6 +2031,30 @@ Author: RafaƂ Mikrut False + + + True + True + in + + + + + + 9 + + + + + True + False + Broken Files + + + 9 + False + + True diff --git a/czkawka_gui/src/connect_button_delete.rs b/czkawka_gui/src/connect_button_delete.rs index 5957a4e..e7393f4 100644 --- a/czkawka_gui/src/connect_button_delete.rs +++ b/czkawka_gui/src/connect_button_delete.rs @@ -23,6 +23,7 @@ pub fn connect_button_delete(gui_data: &GuiData) { let tree_view_zeroed_files_finder = gui_data.main_notebook.tree_view_zeroed_files_finder.clone(); let tree_view_same_music_finder = gui_data.main_notebook.tree_view_same_music_finder.clone(); let tree_view_invalid_symlinks = gui_data.main_notebook.tree_view_invalid_symlinks.clone(); + let tree_view_broken_files = gui_data.main_notebook.tree_view_broken_files.clone(); let check_button_settings_confirm_deletion = gui_data.upper_notebook.check_button_settings_confirm_deletion.clone(); let image_preview_similar_images = gui_data.main_notebook.image_preview_similar_images.clone(); @@ -88,6 +89,9 @@ pub fn connect_button_delete(gui_data: &GuiData) { NotebookMainEnum::Symlinks => { basic_remove(&tree_view_invalid_symlinks.clone(), ColumnsInvalidSymlinks::Name as i32, ColumnsInvalidSymlinks::Path as i32, &gui_data); } + NotebookMainEnum::BrokenFiles => { + basic_remove(&tree_view_broken_files.clone(), ColumnsBrokenFiles::Name as i32, ColumnsBrokenFiles::Path as i32, &gui_data); + } } }); } diff --git a/czkawka_gui/src/connect_button_save.rs b/czkawka_gui/src/connect_button_save.rs index deaed4b..9bd3e81 100644 --- a/czkawka_gui/src/connect_button_save.rs +++ b/czkawka_gui/src/connect_button_save.rs @@ -16,6 +16,7 @@ pub fn connect_button_save(gui_data: &GuiData) { let shared_same_music_state = gui_data.shared_same_music_state.clone(); let shared_zeroed_files_state = gui_data.shared_zeroed_files_state.clone(); let shared_same_invalid_symlinks = gui_data.shared_same_invalid_symlinks.clone(); + let shared_broken_files_state = gui_data.shared_broken_files_state.clone(); let notebook_main = gui_data.main_notebook.notebook_main.clone(); buttons_save.connect_clicked(move |_| { let file_name; @@ -66,6 +67,11 @@ pub fn connect_button_save(gui_data: &GuiData) { shared_same_invalid_symlinks.borrow_mut().save_results_to_file(file_name); } + NotebookMainEnum::BrokenFiles => { + file_name = "results_broken_files.txt"; + + shared_broken_files_state.borrow_mut().save_results_to_file(file_name); + } } post_save_things(file_name, &to_notebook_main_enum(notebook_main.get_current_page().unwrap()), &gui_data); }); diff --git a/czkawka_gui/src/connect_button_search.rs b/czkawka_gui/src/connect_button_search.rs index e17606f..90ad57e 100644 --- a/czkawka_gui/src/connect_button_search.rs +++ b/czkawka_gui/src/connect_button_search.rs @@ -5,6 +5,7 @@ use crate::gui_data::GuiData; use crate::help_functions::*; use crate::notebook_enums::*; use czkawka_core::big_file::BigFile; +use czkawka_core::broken_files::BrokenFiles; use czkawka_core::duplicate::DuplicateFinder; use czkawka_core::empty_files::EmptyFiles; use czkawka_core::empty_folder::EmptyFolder; @@ -33,6 +34,7 @@ pub fn connect_button_search( futures_sender_temporary: futures::channel::mpsc::Sender, futures_sender_zeroed: futures::channel::mpsc::Sender, futures_sender_invalid_symlinks: futures::channel::mpsc::Sender, + futures_sender_broken_files: futures::channel::mpsc::Sender, ) { let stop_sender = gui_data.stop_sender.clone(); let entry_info = gui_data.entry_info.clone(); @@ -75,6 +77,7 @@ pub fn connect_button_search( let tree_view_similar_images_finder = gui_data.main_notebook.tree_view_similar_images_finder.clone(); let tree_view_zeroed_files_finder = gui_data.main_notebook.tree_view_zeroed_files_finder.clone(); let tree_view_invalid_symlinks = gui_data.main_notebook.tree_view_invalid_symlinks.clone(); + let tree_view_broken_files = gui_data.main_notebook.tree_view_broken_files.clone(); let text_view_errors = gui_data.text_view_errors.clone(); let dialog_progress = gui_data.progress_dialog.dialog_progress.clone(); let label_stage = gui_data.progress_dialog.label_stage.clone(); @@ -363,6 +366,26 @@ pub fn connect_button_search( let _ = glib_stop_sender.send(Message::InvalidSymlinks(isf)); }); } + NotebookMainEnum::BrokenFiles => { + label_stage.show(); + grid_progress_stages.show(); + dialog_progress.resize(1, 1); + + get_list_store(&tree_view_broken_files).clear(); + + let futures_sender_broken_files = futures_sender_broken_files.clone(); + + thread::spawn(move || { + let mut br = BrokenFiles::new(); + + br.set_included_directory(included_directories); + br.set_excluded_directory(excluded_directories); + br.set_recursive_search(recursive_search); + br.set_excluded_items(excluded_items); + br.find_broken_files(Some(&stop_receiver), Some(&futures_sender_broken_files)); + let _ = glib_stop_sender.send(Message::BrokenFiles(br)); + }); + } } // Show progress dialog diff --git a/czkawka_gui/src/connect_button_select.rs b/czkawka_gui/src/connect_button_select.rs index 5fa9234..5c923cb 100644 --- a/czkawka_gui/src/connect_button_select.rs +++ b/czkawka_gui/src/connect_button_select.rs @@ -28,6 +28,7 @@ pub fn connect_button_select(gui_data: &GuiData) { hashmap.insert(NotebookMainEnum::Symlinks, vec!["all", "reverse", "custom"]); hashmap.insert(NotebookMainEnum::Zeroed, vec!["all", "reverse", "custom"]); hashmap.insert(NotebookMainEnum::Temporary, vec!["all", "reverse", "custom"]); + hashmap.insert(NotebookMainEnum::BrokenFiles, vec!["all", "reverse", "custom"]); } } diff --git a/czkawka_gui/src/connect_compute_results.rs b/czkawka_gui/src/connect_compute_results.rs index 91c686a..d177507 100644 --- a/czkawka_gui/src/connect_compute_results.rs +++ b/czkawka_gui/src/connect_compute_results.rs @@ -25,7 +25,9 @@ pub fn connect_compute_results(gui_data: &GuiData, glib_stop_receiver: Receiver< let tree_view_zeroed_files_finder = gui_data.main_notebook.tree_view_zeroed_files_finder.clone(); let shared_empty_folders_state = gui_data.shared_empty_folders_state.clone(); let shared_empty_files_state = gui_data.shared_empty_files_state.clone(); + let shared_broken_files_state = gui_data.shared_broken_files_state.clone(); let tree_view_big_files_finder = gui_data.main_notebook.tree_view_big_files_finder.clone(); + let tree_view_broken_files = gui_data.main_notebook.tree_view_broken_files.clone(); let tree_view_invalid_symlinks = gui_data.main_notebook.tree_view_invalid_symlinks.clone(); let shared_big_files_state = gui_data.shared_big_files_state.clone(); let shared_same_invalid_symlinks = gui_data.shared_same_invalid_symlinks.clone(); @@ -512,7 +514,7 @@ pub fn connect_compute_results(gui_data: &GuiData, glib_stop_receiver: Receiver< } Message::SameMusic(mf) => { if mf.get_stopped_search() { - entry_info.set_text("Searching for empty files was stopped by user"); + entry_info.set_text("Searching for same music was stopped by user"); } else { let information = mf.get_information(); let text_messages = mf.get_text_messages(); @@ -663,6 +665,50 @@ pub fn connect_compute_results(gui_data: &GuiData, glib_stop_receiver: Receiver< } } } + Message::BrokenFiles(br) => { + if br.get_stopped_search() { + entry_info.set_text("Searching for broken files was stopped by user"); + } else { + let information = br.get_information(); + let text_messages = br.get_text_messages(); + + let broken_files_number: usize = information.number_of_broken_files; + + entry_info.set_text(format!("Found {} broken files.", broken_files_number).as_str()); + + // Create GUI + { + let list_store = get_list_store(&tree_view_broken_files); + + let col_indices = [0, 1, 2, 3]; + + let vector = br.get_broken_files(); + + for file_entry in vector { + let (directory, file) = split_path(&file_entry.path); + let values: [&dyn ToValue; 4] = [&file, &directory, &file_entry.error_string, &(NaiveDateTime::from_timestamp(file_entry.modified_date as i64, 0).to_string())]; + list_store.set(&list_store.append(), &col_indices, &values); + } + print_text_messages_to_text_view(text_messages, &text_view_errors); + } + + // Set state + { + *shared_broken_files_state.borrow_mut() = br; + + if broken_files_number > 0 { + *shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::BrokenFiles).unwrap().get_mut("save").unwrap() = true; + *shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::BrokenFiles).unwrap().get_mut("delete").unwrap() = true; + *shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::BrokenFiles).unwrap().get_mut("select").unwrap() = true; + } else { + *shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::BrokenFiles).unwrap().get_mut("save").unwrap() = false; + *shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::BrokenFiles).unwrap().get_mut("delete").unwrap() = false; + *shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::BrokenFiles).unwrap().get_mut("select").unwrap() = false; + } + set_buttons(&mut *shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::BrokenFiles).unwrap(), &buttons_array, &buttons_names); + } + } + } } // Returning false here would close the receiver and have senders fail glib::Continue(true) diff --git a/czkawka_gui/src/connect_popovers.rs b/czkawka_gui/src/connect_popovers.rs index 6ebfb4e..074519d 100644 --- a/czkawka_gui/src/connect_popovers.rs +++ b/czkawka_gui/src/connect_popovers.rs @@ -761,6 +761,18 @@ pub fn connect_popovers(gui_data: &GuiData) { column_size_as_bytes: Some(ColumnsZeroedFiles::SizeAsBytes as i32), column_modification_as_secs: None, }, + PopoverObject { + notebook_type: NotebookMainEnum::BrokenFiles, + available_modes: vec!["all", "reverse", "custom"].iter().map(|e| e.to_string()).collect(), + tree_view: gui_data.main_notebook.tree_view_broken_files.clone(), + column_path: Some(ColumnsBrokenFiles::Path as i32), + column_name: Some(ColumnsBrokenFiles::Name as i32), + column_color: None, + column_dimensions: None, + column_size: None, + column_size_as_bytes: None, + column_modification_as_secs: None, + }, ]; let popover_select = gui_data.popovers.popover_select.clone(); diff --git a/czkawka_gui/src/connect_progress_window.rs b/czkawka_gui/src/connect_progress_window.rs index 2bbbe7d..19fc263 100644 --- a/czkawka_gui/src/connect_progress_window.rs +++ b/czkawka_gui/src/connect_progress_window.rs @@ -1,6 +1,6 @@ use crate::gui_data::GuiData; -use czkawka_core::{big_file, duplicate, empty_files, empty_folder, invalid_symlinks, same_music, similar_images, temporary, zeroed}; +use czkawka_core::{big_file, broken_files, duplicate, empty_files, empty_folder, invalid_symlinks, same_music, similar_images, temporary, zeroed}; use futures::StreamExt; use gtk::{LabelExt, ProgressBarExt, WidgetExt}; @@ -17,6 +17,7 @@ pub fn connect_progress_window( mut futures_receiver_temporary: futures::channel::mpsc::Receiver, mut futures_receiver_zeroed: futures::channel::mpsc::Receiver, mut futures_receiver_invalid_symlinks: futures::channel::mpsc::Receiver, + mut futures_receiver_broken_files: futures::channel::mpsc::Receiver, ) { let main_context = glib::MainContext::default(); @@ -241,4 +242,35 @@ pub fn connect_progress_window( }; main_context.spawn_local(future); } + { + // Broken Files + let label_stage = gui_data.progress_dialog.label_stage.clone(); + let progress_bar_current_stage = gui_data.progress_dialog.progress_bar_current_stage.clone(); + let progress_bar_all_stages = gui_data.progress_dialog.progress_bar_all_stages.clone(); + let future = async move { + while let Some(item) = futures_receiver_broken_files.next().await { + match item.current_stage { + 0 => { + progress_bar_current_stage.hide(); + label_stage.set_text(format!("Scanned {} files", item.files_checked).as_str()); + } + 1 => { + progress_bar_current_stage.show(); + if item.files_to_check != 0 { + progress_bar_all_stages.set_fraction((1f64 + (item.files_checked) as f64 / item.files_to_check as f64) / (item.max_stage + 1) as f64); + progress_bar_current_stage.set_fraction((item.files_checked) as f64 / item.files_to_check as f64); + } else { + progress_bar_all_stages.set_fraction((1f64) / (item.max_stage + 1) as f64); + progress_bar_current_stage.set_fraction(0f64); + } + label_stage.set_text(format!("Checking {}/{} files", item.files_checked, item.files_to_check).as_str()); + } + _ => { + panic!(); + } + } + } + }; + main_context.spawn_local(future); + } } diff --git a/czkawka_gui/src/create_tree_view.rs b/czkawka_gui/src/create_tree_view.rs index 89dd64e..fbe742b 100644 --- a/czkawka_gui/src/create_tree_view.rs +++ b/czkawka_gui/src/create_tree_view.rs @@ -443,3 +443,43 @@ pub fn create_tree_view_invalid_symlinks(tree_view: &mut gtk::TreeView) { tree_view.set_vexpand(true); } + +pub fn create_tree_view_broken_files(tree_view: &mut gtk::TreeView) { + let renderer = gtk::CellRendererText::new(); + let column: gtk::TreeViewColumn = TreeViewColumn::new(); + column.pack_start(&renderer, true); + column.set_title("Name"); + column.set_resizable(true); + column.set_min_width(50); + column.add_attribute(&renderer, "text", ColumnsBrokenFiles::Name as i32); + tree_view.append_column(&column); + + let renderer = gtk::CellRendererText::new(); + let column: gtk::TreeViewColumn = TreeViewColumn::new(); + column.pack_start(&renderer, true); + column.set_title("Path"); + column.set_resizable(true); + column.set_min_width(50); + column.add_attribute(&renderer, "text", ColumnsBrokenFiles::Path as i32); + tree_view.append_column(&column); + + let renderer = gtk::CellRendererText::new(); + let column: gtk::TreeViewColumn = TreeViewColumn::new(); + column.pack_start(&renderer, true); + column.set_title("ErrorType"); + column.set_resizable(true); + column.set_min_width(50); + column.add_attribute(&renderer, "text", ColumnsBrokenFiles::ErrorType as i32); + tree_view.append_column(&column); + + let renderer = gtk::CellRendererText::new(); + let column: gtk::TreeViewColumn = TreeViewColumn::new(); + column.pack_start(&renderer, true); + column.set_title("Modification Date"); + column.set_resizable(true); + column.set_min_width(50); + column.add_attribute(&renderer, "text", ColumnsBrokenFiles::Modification as i32); + tree_view.append_column(&column); + + tree_view.set_vexpand(true); +} diff --git a/czkawka_gui/src/double_click_opening.rs b/czkawka_gui/src/double_click_opening.rs index e4076ff..fc8ef05 100644 --- a/czkawka_gui/src/double_click_opening.rs +++ b/czkawka_gui/src/double_click_opening.rs @@ -83,6 +83,15 @@ pub fn opening_double_click_function_invalid_symlinks(tree_view: >k::TreeView, gtk::Inhibit(false) } +pub fn opening_double_click_function_broken_files(tree_view: >k::TreeView, event: &gdk::EventButton) -> gtk::Inhibit { + if event.get_event_type() == gdk::EventType::DoubleButtonPress && event.get_button() == 1 { + common_open_function(tree_view, ColumnsInvalidSymlinks::Name as i32, ColumnsInvalidSymlinks::Path as i32, OpenMode::PathAndName); + } else if event.get_event_type() == gdk::EventType::DoubleButtonPress && event.get_button() == 3 { + common_open_function(tree_view, ColumnsInvalidSymlinks::Name as i32, ColumnsInvalidSymlinks::Path as i32, OpenMode::OnlyPath); + } + gtk::Inhibit(false) +} + pub enum OpenMode { OnlyPath, PathAndName, diff --git a/czkawka_gui/src/gui_data.rs b/czkawka_gui/src/gui_data.rs index e5124f5..365acce 100644 --- a/czkawka_gui/src/gui_data.rs +++ b/czkawka_gui/src/gui_data.rs @@ -7,6 +7,7 @@ use crate::gui_upper_notepad::GUIUpperNotebook; use crate::notebook_enums::*; use crossbeam_channel::unbounded; use czkawka_core::big_file::BigFile; +use czkawka_core::broken_files::BrokenFiles; use czkawka_core::duplicate::DuplicateFinder; use czkawka_core::empty_files::EmptyFiles; use czkawka_core::empty_folder::EmptyFolder; @@ -52,6 +53,7 @@ pub struct GuiData { pub shared_zeroed_files_state: Rc>, pub shared_same_music_state: Rc>, pub shared_same_invalid_symlinks: Rc>, + pub shared_broken_files_state: Rc>, //// Entry pub entry_info: gtk::Entry, @@ -124,6 +126,7 @@ impl GuiData { let shared_zeroed_files_state: Rc> = Rc::new(RefCell::new(ZeroedFiles::new())); let shared_same_music_state: Rc> = Rc::new(RefCell::new(SameMusic::new())); let shared_same_invalid_symlinks: Rc> = Rc::new(RefCell::new(InvalidSymlinks::new())); + let shared_broken_files_state: Rc> = Rc::new(RefCell::new(BrokenFiles::new())); //// Entry let entry_info: gtk::Entry = builder.get_object("entry_info").unwrap(); @@ -155,6 +158,7 @@ impl GuiData { shared_zeroed_files_state, shared_same_music_state, shared_same_invalid_symlinks, + shared_broken_files_state, entry_info, text_view_errors, scrolled_window_errors, diff --git a/czkawka_gui/src/gui_main_notebook.rs b/czkawka_gui/src/gui_main_notebook.rs index 06d28d4..64b6357 100644 --- a/czkawka_gui/src/gui_main_notebook.rs +++ b/czkawka_gui/src/gui_main_notebook.rs @@ -14,6 +14,7 @@ pub struct GUIMainNotebook { pub scrolled_window_zeroed_files_finder: gtk::ScrolledWindow, pub scrolled_window_same_music_finder: gtk::ScrolledWindow, pub scrolled_window_invalid_symlinks: gtk::ScrolledWindow, + pub scrolled_window_broken_files: gtk::ScrolledWindow, pub tree_view_duplicate_finder: gtk::TreeView, pub tree_view_empty_folder_finder: gtk::TreeView, @@ -24,6 +25,7 @@ pub struct GUIMainNotebook { pub tree_view_zeroed_files_finder: gtk::TreeView, pub tree_view_same_music_finder: gtk::TreeView, pub tree_view_invalid_symlinks: gtk::TreeView, + pub tree_view_broken_files: gtk::TreeView, pub entry_similar_images_minimal_size: gtk::Entry, pub entry_duplicate_minimal_size: gtk::Entry, @@ -67,6 +69,7 @@ impl GUIMainNotebook { let scrolled_window_zeroed_files_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_zeroed_files_finder").unwrap(); let scrolled_window_same_music_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_same_music_finder").unwrap(); let scrolled_window_invalid_symlinks: gtk::ScrolledWindow = builder.get_object("scrolled_window_invalid_symlinks").unwrap(); + let scrolled_window_broken_files: gtk::ScrolledWindow = builder.get_object("scrolled_window_broken_files").unwrap(); let tree_view_duplicate_finder: gtk::TreeView = TreeView::new(); let tree_view_empty_folder_finder: gtk::TreeView = TreeView::new(); @@ -77,6 +80,7 @@ impl GUIMainNotebook { let tree_view_zeroed_files_finder: gtk::TreeView = TreeView::new(); let tree_view_same_music_finder: gtk::TreeView = TreeView::new(); let tree_view_invalid_symlinks: gtk::TreeView = TreeView::new(); + let tree_view_broken_files: gtk::TreeView = TreeView::new(); let entry_similar_images_minimal_size: gtk::Entry = builder.get_object("entry_similar_images_minimal_size").unwrap(); let entry_duplicate_minimal_size: gtk::Entry = builder.get_object("entry_duplicate_minimal_size").unwrap(); @@ -116,6 +120,7 @@ impl GUIMainNotebook { scrolled_window_zeroed_files_finder, scrolled_window_same_music_finder, scrolled_window_invalid_symlinks, + scrolled_window_broken_files, tree_view_duplicate_finder, tree_view_empty_folder_finder, tree_view_empty_files_finder, @@ -125,6 +130,7 @@ impl GUIMainNotebook { tree_view_zeroed_files_finder, tree_view_same_music_finder, tree_view_invalid_symlinks, + tree_view_broken_files, entry_similar_images_minimal_size, entry_duplicate_minimal_size, entry_big_files_number, diff --git a/czkawka_gui/src/help_functions.rs b/czkawka_gui/src/help_functions.rs index e4d3024..94432ef 100644 --- a/czkawka_gui/src/help_functions.rs +++ b/czkawka_gui/src/help_functions.rs @@ -1,4 +1,5 @@ use czkawka_core::big_file::BigFile; +use czkawka_core::broken_files::BrokenFiles; use czkawka_core::common_messages::Messages; use czkawka_core::duplicate::DuplicateFinder; use czkawka_core::empty_files::EmptyFiles; @@ -24,6 +25,7 @@ pub enum Message { ZeroedFiles(ZeroedFiles), SameMusic(SameMusic), InvalidSymlinks(InvalidSymlinks), + BrokenFiles(BrokenFiles), } pub enum ColumnsDuplicates { @@ -104,6 +106,13 @@ pub enum ColumnsInvalidSymlinks { Modification, } +pub enum ColumnsBrokenFiles { + Name = 0, + Path, + ErrorType, + Modification, +} + pub const TEXT_COLOR: &str = "#ffffff"; pub const MAIN_ROW_COLOR: &str = "#343434"; pub const HEADER_ROW_COLOR: &str = "#272727"; diff --git a/czkawka_gui/src/initialize_gui.rs b/czkawka_gui/src/initialize_gui.rs index 5febfc7..d643a86 100644 --- a/czkawka_gui/src/initialize_gui.rs +++ b/czkawka_gui/src/initialize_gui.rs @@ -29,6 +29,7 @@ pub fn initialize_gui(gui_data: &mut GuiData) { let scrolled_window_same_music_finder = gui_data.main_notebook.scrolled_window_same_music_finder.clone(); let scrolled_window_invalid_symlinks = gui_data.main_notebook.scrolled_window_invalid_symlinks.clone(); let scrolled_window_zeroed_files_finder = gui_data.main_notebook.scrolled_window_zeroed_files_finder.clone(); + let scrolled_window_broken_files = gui_data.main_notebook.scrolled_window_broken_files.clone(); let scrolled_window_included_directories = gui_data.upper_notebook.scrolled_window_included_directories.clone(); let scrolled_window_excluded_directories = gui_data.upper_notebook.scrolled_window_excluded_directories.clone(); @@ -420,6 +421,34 @@ pub fn initialize_gui(gui_data: &mut GuiData) { gtk::Inhibit(false) }); } + // Broken Files + { + let col_types: [glib::types::Type; 4] = [glib::types::Type::String, glib::types::Type::String, glib::types::Type::String, glib::types::Type::String]; + let list_store: gtk::ListStore = gtk::ListStore::new(&col_types); + + let mut tree_view: gtk::TreeView = TreeView::with_model(&list_store); + + tree_view.get_selection().set_mode(SelectionMode::Multiple); + + create_tree_view_broken_files(&mut tree_view); + + tree_view.connect_button_press_event(opening_double_click_function_broken_files); + + gui_data.main_notebook.tree_view_broken_files = tree_view.clone(); + scrolled_window_broken_files.add(&tree_view); + scrolled_window_broken_files.show_all(); + + let gui_data = gui_data.clone(); + tree_view.connect_key_release_event(move |tree_view, e| { + if let Some(button_number) = e.get_keycode() { + // Handle delete button + if button_number == 119 { + basic_remove(&tree_view, ColumnsBrokenFiles::Name as i32, ColumnsBrokenFiles::Path as i32, &gui_data); + } + } + gtk::Inhibit(false) + }); + } } // Set Included Directory diff --git a/czkawka_gui/src/main.rs b/czkawka_gui/src/main.rs index d901fac..edff8ea 100644 --- a/czkawka_gui/src/main.rs +++ b/czkawka_gui/src/main.rs @@ -83,6 +83,7 @@ fn main() { let (futures_sender_temporary, futures_receiver_temporary): (futures::channel::mpsc::Sender, futures::channel::mpsc::Receiver) = futures::channel::mpsc::channel(20); let (futures_sender_zeroed, futures_receiver_zeroed): (futures::channel::mpsc::Sender, futures::channel::mpsc::Receiver) = futures::channel::mpsc::channel(20); let (futures_sender_invalid_symlinks, futures_receiver_invalid_symlinks): (futures::channel::mpsc::Sender, futures::channel::mpsc::Receiver) = futures::channel::mpsc::channel(20); + let (futures_sender_broken_files, futures_receiver_broken_files): (futures::channel::mpsc::Sender, futures::channel::mpsc::Receiver) = futures::channel::mpsc::channel(20); initialize_gui(&mut gui_data); reset_configuration(&gui_data, false); // Fallback for invalid loading setting project @@ -102,6 +103,7 @@ fn main() { futures_sender_temporary, futures_sender_zeroed, futures_sender_invalid_symlinks, + futures_sender_broken_files, ); connect_button_select(&gui_data); connect_button_stop(&gui_data); @@ -121,6 +123,7 @@ fn main() { futures_receiver_temporary, futures_receiver_zeroed, futures_receiver_invalid_symlinks, + futures_receiver_broken_files, ); connect_hide_text_view_errors(&gui_data); connect_settings(&gui_data); diff --git a/czkawka_gui/src/notebook_enums.rs b/czkawka_gui/src/notebook_enums.rs index c90b67e..eda7e63 100644 --- a/czkawka_gui/src/notebook_enums.rs +++ b/czkawka_gui/src/notebook_enums.rs @@ -1,4 +1,4 @@ -pub const NUMBER_OF_NOTEBOOK_MAIN_TABS: usize = 9; +pub const NUMBER_OF_NOTEBOOK_MAIN_TABS: usize = 10; pub const NUMBER_OF_NOTEBOOK_UPPER_TABS: usize = 5; // Needs to be updated when changed order of notebook tabs @@ -13,6 +13,7 @@ pub enum NotebookMainEnum { SameMusic, Zeroed, Symlinks, + BrokenFiles, } pub fn to_notebook_main_enum(notebook_number: u32) -> NotebookMainEnum { match notebook_number { @@ -25,6 +26,7 @@ pub fn to_notebook_main_enum(notebook_number: u32) -> NotebookMainEnum { 6 => NotebookMainEnum::SameMusic, 7 => NotebookMainEnum::Zeroed, 8 => NotebookMainEnum::Symlinks, + 9 => NotebookMainEnum::BrokenFiles, _ => panic!("Invalid Notebook Tab"), } } @@ -39,6 +41,7 @@ pub fn get_all_main_tabs() -> [NotebookMainEnum; NUMBER_OF_NOTEBOOK_MAIN_TABS] { to_notebook_main_enum(6), to_notebook_main_enum(7), to_notebook_main_enum(8), + to_notebook_main_enum(9), ] } diff --git a/instructions/Instruction.md b/instructions/Instruction.md index a22a046..5c78014 100644 --- a/instructions/Instruction.md +++ b/instructions/Instruction.md @@ -100,7 +100,7 @@ Then, for each selected tag by which we want to search for duplicates, we perfor ### Similar Images It is a tool for finding similar images that differ e.g. in watermark, size etc. -The tool first collects images with specific extensions that can be checked - `["jpg", "png", "bmp", "ico", "webp", "tiff", "dds"]`. +The tool first collects images with specific extensions that can be checked - `["jpg", "png", "bmp", "ico", "webp", "tiff"]`. Next cached data are loaded from file to prevent hashing twice same file. Automatically cache which points to non existing data is deleted. @@ -123,6 +123,16 @@ Computed hash data is then thrown into a special tree that allows to compare has Next this hashes are saved to file, to be able to opens images without needing to hash it more times. Finally, each hash is compared with the others and if the distance between them is less than the maximum distance specified by the user, the images are considered similar and thrown from the pool of images to be searched. +### Broken Files +This tool is created to find files which are corrupted or have invalid extension. + +Currently only checking of images is implemented. + +At first image files are collected and then this files are opened. + +If an error happens when opening this image then it means that this file is corrupted. + +Only some image extensions are supported, because I rely on image crate. Also some false positives may be shown(e.g. https://github.com/image-rs/jpeg-decoder/issues/130) ## Config/Cache files For now Czkawka store only 2 files on disk: @@ -165,6 +175,7 @@ There are several buttons which do different actions: - Stop - button in progress dialog, allows to easily stop current task. Sometimes it may take a few seconds until all atomic operations ends and GUI will be able to use again - Select - allows selecting multiple entries at once - Delete - delete entirely all selected entries +- Symlink - create symlink to selected files(first file is threaten as original and rest will become symlinks) - Save - save initial state of results - Hamburger(parallel lines) - used to show/hide bottom text panel which shows warnings/errors - Add (directories) - adds directories to include or exclude