diff --git a/czkawka_cli/src/commands.rs b/czkawka_cli/src/commands.rs index 144b2f8..413c873 100644 --- a/czkawka_cli/src/commands.rs +++ b/czkawka_cli/src/commands.rs @@ -147,6 +147,23 @@ pub enum Commands { #[structopt(short, long, parse(try_from_str = parse_minimal_file_size), default_value = "1024", help = "Minimum size in bytes", long_help = "Minimum size of checked files in bytes, assigning bigger value may speed up searching")] minimal_file_size: u64, }, + #[structopt(name = "symlinks", about = "Finds invalid symlinks", help_message = HELP_MESSAGE, after_help = "EXAMPLE:\n czkawka symlinks -d /home/kicikici/ /home/szczek -e /home/kicikici/jestempsem -x jpg -f results.txt")] + InvalidSymlinks { + #[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)] @@ -301,4 +318,5 @@ EXAMPLES: {bin} temp -d /home/rafal/ -E */.git */tmp* *Pulpit -f results.txt -D {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} 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"#; diff --git a/czkawka_cli/src/main.rs b/czkawka_cli/src/main.rs index 733d6ea..d5aa16a 100644 --- a/czkawka_cli/src/main.rs +++ b/czkawka_cli/src/main.rs @@ -10,6 +10,8 @@ use czkawka_core::{ duplicate::DuplicateFinder, empty_files::{self, EmptyFiles}, empty_folder::EmptyFolder, + invalid_symlinks, + invalid_symlinks::InvalidSymlinks, same_music::SameMusic, similar_images::SimilarImages, temporary::{self, Temporary}, @@ -296,5 +298,38 @@ fn main() { mf.print_results(); mf.get_text_messages().print_messages(); } + Commands::InvalidSymlinks { + directories, + excluded_directories, + excluded_items, + allowed_extensions, + file_to_save, + not_recursive, + delete_files, + } => { + let mut ifs = InvalidSymlinks::new(); + + ifs.set_included_directory(path_list_to_str(directories.directories)); + ifs.set_excluded_directory(path_list_to_str(excluded_directories.excluded_directories)); + ifs.set_excluded_items(path_list_to_str(excluded_items.excluded_items)); + ifs.set_allowed_extensions(allowed_extensions.allowed_extensions.join(",")); + ifs.set_recursive_search(!not_recursive.not_recursive); + if delete_files { + ifs.set_delete_method(invalid_symlinks::DeleteMethod::Delete); + } + + ifs.find_invalid_links(None, None); + + if let Some(file_name) = file_to_save.file_name() { + if !ifs.save_results_to_file(file_name) { + ifs.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 + ifs.print_results(); + ifs.get_text_messages().print_messages(); + } } } diff --git a/czkawka_core/src/invalid_symlinks.rs b/czkawka_core/src/invalid_symlinks.rs new file mode 100644 index 0000000..c35de40 --- /dev/null +++ b/czkawka_core/src/invalid_symlinks.rs @@ -0,0 +1,457 @@ +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 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, +} + +#[derive(Eq, PartialEq, Clone, Debug)] +pub enum DeleteMethod { + None, + Delete, +} + +const MAX_NUMBER_OF_SYMLINK_JUMPS: i32 = 20; + +#[derive(Clone)] +pub enum ErrorType { + InfiniteRecursion, + NonExistentFile, +} + +#[derive(Clone)] +pub struct FileEntry { + pub symlink_path: PathBuf, + pub destination_path: PathBuf, + pub type_of_error: ErrorType, + pub modified_date: u64, +} + +/// Info struck with helpful information's about results +#[derive(Default)] +pub struct Info { + pub number_of_checked_files: usize, + pub number_of_checked_folders: usize, + pub number_of_ignored_files: usize, + pub number_of_ignored_things: usize, + pub number_of_invalid_symlinks: 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 InvalidSymlinks { + text_messages: Messages, + information: Info, + invalid_symlinks: Vec, + directories: Directories, + allowed_extensions: Extensions, + excluded_items: ExcludedItems, + recursive_search: bool, + delete_method: DeleteMethod, + stopped_search: bool, +} + +impl InvalidSymlinks { + 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(), + invalid_symlinks: vec![], + delete_method: DeleteMethod::None, + stopped_search: false, + } + } + + pub fn find_invalid_links(&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; + } + self.delete_files(); + self.debug_print(); + } + + pub fn get_stopped_search(&self) -> bool { + self.stopped_search + } + + pub const fn get_invalid_symlinks(&self) -> &Vec { + &self.invalid_symlinks + } + + 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: String) -> bool { + self.directories.set_included_directory(included_directory, &mut self.text_messages) + } + + pub fn set_excluded_directory(&mut self, excluded_directory: String) { + 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: String) { + self.excluded_items.set_excluded_items(excluded_items, &mut self.text_messages); + } + + /// Check files for any with size == 0 + 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()); + } + self.information.number_of_checked_folders += folders_to_check.len(); + + //// 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: 0, + files_checked: atomic_file_counter.load(Ordering::Relaxed) as usize, + }) + .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() { + self.information.number_of_checked_folders += 1; + + 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); + } else if metadata.file_type().is_symlink() { + 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(); + + // 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. + self.information.number_of_ignored_files += 1; + 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; + } + + let mut destination_path = PathBuf::new(); + let type_of_error; + + match current_file_name.read_link() { + Ok(t) => { + destination_path.push(t); + let mut number_of_loop = 0; + let mut current_path = current_file_name.clone(); + loop { + if number_of_loop == 0 && !current_path.exists() { + type_of_error = ErrorType::NonExistentFile; + break; + } + if number_of_loop == MAX_NUMBER_OF_SYMLINK_JUMPS { + type_of_error = ErrorType::InfiniteRecursion; + break; + } + + current_path = match current_path.read_link() { + Ok(t) => t, + Err(_) => { + // Looks that some next symlinks are broken, but we do nothing with it + continue 'dir; + } + }; + + number_of_loop += 1; + } + } + Err(_) => { + // Failed to load info about it + type_of_error = ErrorType::NonExistentFile; + } + } + + // Creating new file entry + let fe: FileEntry = FileEntry { + symlink_path: current_file_name.clone(), + destination_path, + type_of_error, + 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 + }, + }; + + // Adding files to Vector + self.invalid_symlinks.push(fe); + + self.information.number_of_checked_files += 1; + } else { + // Not sure what exactly this is + self.information.number_of_ignored_things += 1; + } + } + } + self.information.number_of_invalid_symlinks = self.invalid_symlinks.len(); + // 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_size".to_string()); + 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.invalid_symlinks { + if fs::remove_file(file_entry.symlink_path.clone()).is_err() { + self.text_messages.warnings.push(file_entry.symlink_path.display().to_string()); + } + } + } + DeleteMethod::None => { + //Just do nothing + } + } + + Common::print_time(start_time, SystemTime::now(), "delete_files".to_string()); + } +} +impl Default for InvalidSymlinks { + fn default() -> Self { + Self::new() + } +} + +impl DebugPrint for InvalidSymlinks { + #[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 checked files - {}", self.information.number_of_checked_files); + println!("Number of checked folders - {}", self.information.number_of_checked_folders); + println!("Number of ignored files - {}", self.information.number_of_ignored_files); + println!("Number of ignored things(like symbolic links) - {}", self.information.number_of_ignored_things); + 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!("Invalid symlinks list size - {}", self.invalid_symlinks.len()); + 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 InvalidSymlinks { + 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 mut file = match File::create(&file_name) { + Ok(t) => t, + Err(_) => { + self.text_messages.errors.push(format!("Failed to create file {}", file_name)); + return false; + } + }; + + if writeln!( + file, + "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.invalid_symlinks.is_empty() { + writeln!(file, "Found {} invalid symlinks.", self.information.number_of_invalid_symlinks).unwrap(); + for file_entry in self.invalid_symlinks.iter() { + writeln!( + file, + "{}\t\t{}\t\t{}", + file_entry.symlink_path.display(), + file_entry.destination_path.display(), + match file_entry.type_of_error { + ErrorType::InfiniteRecursion => "Infinite Recursion", + ErrorType::NonExistentFile => "Non Existent File", + } + ) + .unwrap(); + } + } else { + write!(file, "Not found any invalid symlinks.").unwrap(); + } + Common::print_time(start_time, SystemTime::now(), "save_results_to_file".to_string()); + true + } +} +impl PrintResults for InvalidSymlinks { + /// Print information's about duplicated entries + /// Only needed for CLI + fn print_results(&self) { + let start_time: SystemTime = SystemTime::now(); + println!("Found {} invalid symlinks.\n", self.information.number_of_invalid_symlinks); + for file_entry in self.invalid_symlinks.iter() { + println!( + "{}\t\t{}\t\t{}", + file_entry.symlink_path.display(), + file_entry.destination_path.display(), + match file_entry.type_of_error { + ErrorType::InfiniteRecursion => "Infinite Recursion", + ErrorType::NonExistentFile => "Non Existent File", + } + ); + } + + 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 787ab80..81e8ff6 100644 --- a/czkawka_core/src/lib.rs +++ b/czkawka_core/src/lib.rs @@ -13,6 +13,7 @@ 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; diff --git a/czkawka_gui/czkawka.glade b/czkawka_gui/czkawka.glade index 7ba8d28..ff42a00 100644 --- a/czkawka_gui/czkawka.glade +++ b/czkawka_gui/czkawka.glade @@ -438,6 +438,66 @@ Author: Rafał Mikrut + + False + + + True + False + vertical + + + Select All + True + True + True + + + False + True + 0 + + + + + Unselect All + True + True + True + + + False + True + 1 + + + + + True + False + + + False + True + 2 + + + + + Reverse Selection + True + True + True + + + False + True + 3 + + + + + False 1100 @@ -1906,6 +1966,30 @@ Author: Rafał Mikrut False + + + True + True + in + + + + + + 8 + + + + + True + False + Invalid Symlinks + + + 8 + False + + True diff --git a/czkawka_gui/src/connect_button_delete.rs b/czkawka_gui/src/connect_button_delete.rs index 0caccf1..88aaa0b 100644 --- a/czkawka_gui/src/connect_button_delete.rs +++ b/czkawka_gui/src/connect_button_delete.rs @@ -6,6 +6,8 @@ use std::collections::BTreeMap; use std::fs; use std::fs::Metadata; +// TODO add support for checking if really symlink doesn't point to correct directory/file + pub fn connect_button_delete(gui_data: &GuiData) { let gui_data = gui_data.clone(); let buttons_delete = gui_data.buttons_delete.clone(); @@ -20,6 +22,7 @@ pub fn connect_button_delete(gui_data: &GuiData) { let scrolled_window_similar_images_finder = gui_data.scrolled_window_similar_images_finder.clone(); let scrolled_window_zeroed_files_finder = gui_data.scrolled_window_zeroed_files_finder.clone(); let scrolled_window_same_music_finder = gui_data.scrolled_window_same_music_finder.clone(); + let scrolled_window_invalid_symlinks = gui_data.scrolled_window_invalid_symlinks.clone(); let check_button_settings_confirm_deletion = gui_data.check_button_settings_confirm_deletion.clone(); buttons_delete.connect_clicked(move |_| { @@ -80,6 +83,9 @@ pub fn connect_button_delete(gui_data: &GuiData) { "notebook_main_same_music_finder" => { tree_remove(scrolled_window_same_music_finder.clone(), ColumnsSameMusic::Name as i32, ColumnsSameMusic::Path as i32, ColumnsSameMusic::Color as i32, &gui_data); } + "scrolled_window_invalid_symlinks" => { + basic_remove_invalid_symlinks(scrolled_window_invalid_symlinks.clone(), ColumnsInvalidSymlinks::SymlinkPath as i32, &gui_data); + } e => panic!("Not existent {}", e), } }); @@ -171,6 +177,38 @@ fn empty_folder_remover(scrolled_window: gtk::ScrolledWindow, column_file_name: selection.unselect_all(); } +fn basic_remove_invalid_symlinks(scrolled_window: gtk::ScrolledWindow, column_symlink_path: i32, gui_data: &GuiData) { + let text_view_errors = gui_data.text_view_errors.clone(); + + let tree_view = scrolled_window.get_children().get(0).unwrap().clone().downcast::().unwrap(); + let selection = tree_view.get_selection(); + + let (selection_rows, tree_model) = selection.get_selected_rows(); + if selection_rows.is_empty() { + return; + } + let list_store = tree_model.clone().downcast::().unwrap(); + + // let new_tree_model = TreeModel::new(); // TODO - maybe create new model when inserting a new data, because this seems to be not optimal when using thousands of rows + + let mut messages: String = "".to_string(); + + // Must be deleted from end to start, because when deleting entries, TreePath(and also TreeIter) will points to invalid data + for tree_path in selection_rows.iter().rev() { + let symlink_path = tree_model.get_value(&tree_model.get_iter(tree_path).unwrap(), column_symlink_path).get::().unwrap().unwrap(); + + match fs::remove_file(&symlink_path) { + Ok(_) => { + list_store.remove(&list_store.get_iter(tree_path).unwrap()); + } + Err(_) => messages += format!("Failed to remove file {} because file doesn't exists or you don't have permissions.\n", symlink_path).as_str(), + } + } + + text_view_errors.get_buffer().unwrap().set_text(messages.as_str()); + selection.unselect_all(); +} + fn basic_remove(scrolled_window: gtk::ScrolledWindow, column_file_name: i32, column_path: i32, gui_data: &GuiData) { let text_view_errors = gui_data.text_view_errors.clone(); diff --git a/czkawka_gui/src/connect_button_save.rs b/czkawka_gui/src/connect_button_save.rs index 4640bec..6d3d0eb 100644 --- a/czkawka_gui/src/connect_button_save.rs +++ b/czkawka_gui/src/connect_button_save.rs @@ -14,6 +14,7 @@ pub fn connect_button_save(gui_data: &GuiData) { let shared_similar_images_state = gui_data.shared_similar_images_state.clone(); 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 notebook_main_children_names = gui_data.notebook_main_children_names.clone(); let notebook_main = gui_data.notebook_main.clone(); buttons_save.connect_clicked(move |_| match notebook_main_children_names.get(notebook_main.get_current_page().unwrap() as usize).unwrap().as_str() { @@ -73,6 +74,13 @@ pub fn connect_button_save(gui_data: &GuiData) { post_save_things(file_name, "same_music", &gui_data); } + "scrolled_window_invalid_symlinks" => { + let file_name = "results_invalid_symlinks.txt"; + + shared_same_invalid_symlinks.borrow_mut().save_results_to_file(file_name); + + post_save_things(file_name, "invalid_symlinks", &gui_data); + } e => panic!("Not existent {}", e), }); } diff --git a/czkawka_gui/src/connect_button_search.rs b/czkawka_gui/src/connect_button_search.rs index b719c72..d5f6d87 100644 --- a/czkawka_gui/src/connect_button_search.rs +++ b/czkawka_gui/src/connect_button_search.rs @@ -7,6 +7,7 @@ use czkawka_core::big_file::BigFile; use czkawka_core::duplicate::DuplicateFinder; use czkawka_core::empty_files::EmptyFiles; use czkawka_core::empty_folder::EmptyFolder; +use czkawka_core::invalid_symlinks::InvalidSymlinks; use czkawka_core::same_music::{MusicSimilarity, SameMusic}; use czkawka_core::similar_images::SimilarImages; use czkawka_core::temporary::Temporary; @@ -14,6 +15,8 @@ use czkawka_core::zeroed::ZeroedFiles; use glib::Sender; use gtk::prelude::*; use gtk::WindowPosition; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::thread; #[allow(clippy::too_many_arguments)] @@ -28,6 +31,7 @@ pub fn connect_button_search( futures_sender_similar_images: futures::channel::mpsc::Sender, futures_sender_temporary: futures::channel::mpsc::Sender, futures_sender_zeroed: futures::channel::mpsc::Sender, + futures_sender_invalid_symlinks: futures::channel::mpsc::Sender, ) { let entry_info = gui_data.entry_info.clone(); let notebook_main_children_names = gui_data.notebook_main_children_names.clone(); @@ -68,6 +72,7 @@ pub fn connect_button_search( let scrolled_window_same_music_finder = gui_data.scrolled_window_same_music_finder.clone(); let scrolled_window_similar_images_finder = gui_data.scrolled_window_similar_images_finder.clone(); let scrolled_window_zeroed_files_finder = gui_data.scrolled_window_zeroed_files_finder.clone(); + let scrolled_window_invalid_symlinks = gui_data.scrolled_window_invalid_symlinks.clone(); let text_view_errors = gui_data.text_view_errors.clone(); let dialog_progress = gui_data.dialog_progress.clone(); let label_stage = gui_data.label_stage.clone(); @@ -76,6 +81,8 @@ pub fn connect_button_search( let progress_bar_all_stages = gui_data.progress_bar_all_stages.clone(); let image_preview_similar_images = gui_data.image_preview_similar_images.clone(); + let show_dialog = Arc::new(AtomicBool::new(true)); + buttons_search_clone.connect_clicked(move |_| { let included_directories = get_string_from_list_store(&scrolled_window_included_directories); let excluded_directories = get_string_from_list_store(&scrolled_window_excluded_directories); @@ -91,12 +98,14 @@ pub fn connect_button_search( entry_info.set_text("Searching data, it may take a while, please wait..."); // Set dialog to center to current screen(it is impossible to center it to main window) - dialog_progress.set_position(WindowPosition::CenterOnParent); + dialog_progress.set_position(WindowPosition::CenterAlways); // Resets progress bars progress_bar_all_stages.set_fraction(0 as f64); progress_bar_current_stage.set_fraction(0 as f64); + reset_text_view(&text_view_errors); + match notebook_main_children_names.get(notebook_main.get_current_page().unwrap() as usize).unwrap().as_str() { "notebook_main_duplicate_finder_label" => { label_stage.show(); @@ -104,7 +113,6 @@ pub fn connect_button_search( dialog_progress.resize(1, 1); get_list_store(&scrolled_window_duplicate_finder).clear(); - text_view_errors.get_buffer().unwrap().set_text(""); let check_method; if radio_button_duplicates_name.get_active() { @@ -147,7 +155,6 @@ pub fn connect_button_search( dialog_progress.resize(1, 1); get_list_store(&scrolled_window_main_empty_files_finder).clear(); - text_view_errors.get_buffer().unwrap().set_text(""); let glib_stop_sender = glib_stop_sender.clone(); let stop_receiver = stop_receiver.clone(); @@ -172,7 +179,6 @@ pub fn connect_button_search( dialog_progress.resize(1, 1); get_list_store(&scrolled_window_main_empty_folder_finder).clear(); - text_view_errors.get_buffer().unwrap().set_text(""); let glib_stop_sender = glib_stop_sender.clone(); let stop_receiver = stop_receiver.clone(); @@ -194,7 +200,6 @@ pub fn connect_button_search( dialog_progress.resize(1, 1); get_list_store(&scrolled_window_big_files_finder).clear(); - text_view_errors.get_buffer().unwrap().set_text(""); let numbers_of_files_to_check = match entry_big_files_number.get_text().as_str().parse::() { Ok(t) => t, @@ -223,7 +228,6 @@ pub fn connect_button_search( dialog_progress.resize(1, 1); get_list_store(&scrolled_window_main_temporary_files_finder).clear(); - text_view_errors.get_buffer().unwrap().set_text(""); let glib_stop_sender = glib_stop_sender.clone(); let stop_receiver = stop_receiver.clone(); @@ -249,7 +253,6 @@ pub fn connect_button_search( dialog_progress.resize(1, 1); get_list_store(&scrolled_window_similar_images_finder).clear(); - text_view_errors.get_buffer().unwrap().set_text(""); let glib_stop_sender = glib_stop_sender.clone(); let stop_receiver = stop_receiver.clone(); @@ -295,7 +298,6 @@ pub fn connect_button_search( dialog_progress.resize(1, 1); get_list_store(&scrolled_window_zeroed_files_finder).clear(); - text_view_errors.get_buffer().unwrap().set_text(""); let glib_stop_sender = glib_stop_sender.clone(); let stop_receiver = stop_receiver.clone(); @@ -320,7 +322,6 @@ pub fn connect_button_search( dialog_progress.resize(1, 1); get_list_store(&scrolled_window_same_music_finder).clear(); - text_view_errors.get_buffer().unwrap().set_text(""); let minimal_file_size = match entry_same_music_minimal_size.get_text().as_str().parse::() { Ok(t) => t, @@ -366,12 +367,37 @@ pub fn connect_button_search( notebook_main.set_sensitive(true); set_buttons(&mut *shared_buttons.borrow_mut().get_mut("same_music").unwrap(), &buttons_array, &buttons_names); entry_info.set_text("ERROR: You must select at least one checkbox with music searching types."); + show_dialog.store(false, Ordering::Relaxed); } } + "scrolled_window_invalid_symlinks" => { + label_stage.show(); + grid_progress_stages.hide(); + dialog_progress.resize(1, 1); + + get_list_store(&scrolled_window_invalid_symlinks).clear(); + + let glib_stop_sender = glib_stop_sender.clone(); + let stop_receiver = stop_receiver.clone(); + let futures_sender_invalid_symlinks = futures_sender_invalid_symlinks.clone(); + + thread::spawn(move || { + let mut isf = InvalidSymlinks::new(); + + isf.set_included_directory(included_directories); + isf.set_excluded_directory(excluded_directories); + isf.set_recursive_search(recursive_search); + isf.set_excluded_items(excluded_items); + isf.find_invalid_links(Some(&stop_receiver), Some(&futures_sender_invalid_symlinks)); + let _ = glib_stop_sender.send(Message::InvalidSymlinks(isf)); + }); + } e => panic!("Not existent {}", e), } // Show progress dialog - dialog_progress.show(); + if show_dialog.load(Ordering::Relaxed) { + dialog_progress.show(); + } }); } diff --git a/czkawka_gui/src/connect_button_select.rs b/czkawka_gui/src/connect_button_select.rs index 8727e57..25a10b4 100644 --- a/czkawka_gui/src/connect_button_select.rs +++ b/czkawka_gui/src/connect_button_select.rs @@ -8,6 +8,7 @@ pub fn connect_button_select(gui_data: &GuiData) { let buttons_select_clone = gui_data.buttons_select.clone(); let popover_select_duplicate = gui_data.popover_select_duplicate.clone(); let popover_select_simple_list = gui_data.popover_select_simple_list.clone(); + let popover_select_very_simple_list = gui_data.popover_select_very_simple_list.clone(); let buttons_select = gui_data.buttons_select.clone(); buttons_select_clone.connect_clicked(move |_| match notebook_main_children_names.get(notebook_main.get_current_page().unwrap() as usize).unwrap().as_str() { "notebook_main_duplicate_finder_label" | "notebook_main_same_music_finder" | "notebook_main_similar_images_finder_label" => { @@ -18,6 +19,10 @@ pub fn connect_button_select(gui_data: &GuiData) { popover_select_simple_list.set_relative_to(Some(&buttons_select)); popover_select_simple_list.popup(); } + "scrolled_window_invalid_symlinks" => { + popover_select_very_simple_list.set_relative_to(Some(&buttons_select)); + popover_select_very_simple_list.popup(); + } e => panic!("Not existent {}", e), }); } diff --git a/czkawka_gui/src/connect_compute_results.rs b/czkawka_gui/src/connect_compute_results.rs index e5c8223..2c6b345 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 shared_empty_folders_state = gui_data.shared_empty_folders_state.clone(); let shared_empty_files_state = gui_data.shared_empty_files_state.clone(); let scrolled_window_big_files_finder = gui_data.scrolled_window_big_files_finder.clone(); + let scrolled_window_invalid_symlinks = gui_data.scrolled_window_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(); let scrolled_window_main_temporary_files_finder = gui_data.scrolled_window_main_temporary_files_finder.clone(); let shared_temporary_files_state = gui_data.shared_temporary_files_state.clone(); let shared_similar_images_state = gui_data.shared_similar_images_state.clone(); @@ -605,6 +607,54 @@ pub fn connect_compute_results(gui_data: &GuiData, glib_stop_receiver: Receiver< } } } + Message::InvalidSymlinks(ifs) => { + if ifs.get_stopped_search() { + entry_info.set_text("Searching for invalid symlink was stopped by user"); + } else { + let information = ifs.get_information(); + let text_messages = ifs.get_text_messages(); + + let invalid_symlinks: usize = information.number_of_invalid_symlinks; + + entry_info.set_text(format!("Found {} invalid symlinks.", invalid_symlinks).as_str()); + + // Create GUI + { + let list_store = get_list_store(&scrolled_window_invalid_symlinks); + + let col_indices = [0, 1, 2, 3]; + + let vector = ifs.get_invalid_symlinks(); + + for file_entry in vector { + let values: [&dyn ToValue; 4] = [ + &file_entry.symlink_path.to_string_lossy().to_string(), + &file_entry.destination_path.to_string_lossy().to_string(), + &get_text_from_invalid_symlink_cause(&file_entry.type_of_error), + &(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_same_invalid_symlinks.borrow_mut() = ifs; + + if invalid_symlinks > 0 { + *shared_buttons.borrow_mut().get_mut("invalid_symlinks").unwrap().get_mut("save").unwrap() = true; + *shared_buttons.borrow_mut().get_mut("invalid_symlinks").unwrap().get_mut("delete").unwrap() = true; + *shared_buttons.borrow_mut().get_mut("invalid_symlinks").unwrap().get_mut("select").unwrap() = true; + } else { + *shared_buttons.borrow_mut().get_mut("invalid_symlinks").unwrap().get_mut("save").unwrap() = false; + *shared_buttons.borrow_mut().get_mut("invalid_symlinks").unwrap().get_mut("delete").unwrap() = false; + *shared_buttons.borrow_mut().get_mut("invalid_symlinks").unwrap().get_mut("select").unwrap() = false; + } + set_buttons(&mut *shared_buttons.borrow_mut().get_mut("invalid_symlinks").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_notebook_tabs.rs b/czkawka_gui/src/connect_notebook_tabs.rs index f4cc1a6..12d6aa7 100644 --- a/czkawka_gui/src/connect_notebook_tabs.rs +++ b/czkawka_gui/src/connect_notebook_tabs.rs @@ -15,6 +15,16 @@ pub fn connect_notebook_tabs(gui_data: &GuiData) { let notebook_upper = gui_data.notebook_upper.clone(); let notebook_upper_children_names = gui_data.notebook_upper_children_names.clone(); + // TODO Remove this because disabling it doesn't works + // Remove on Windows last tab(Invalid symlinks) because is not available + // if cfg!(target_family = "windows") { + // for (index, widget) in notebook_main_clone.get_children().iter().enumerate() { + // if widget.get_buildable_name() == Some("scrolled_window_invalid_symlinks".to_string()) { + // notebook_main_clone.remove_page(Some(index as u32)); + // } + // } + // } + notebook_main_clone.connect_switch_page(move |_, _, number| { let page: &str; match notebook_main_children_names.get(number as usize).unwrap().as_str() { @@ -26,6 +36,7 @@ pub fn connect_notebook_tabs(gui_data: &GuiData) { "notebook_main_similar_images_finder_label" => page = "similar_images", "notebook_main_zeroed_files_finder" => page = "zeroed_files", "notebook_main_same_music_finder" => page = "same_music", + "scrolled_window_invalid_symlinks" => page = "invalid_symlinks", e => { panic!("Not existent page {}", e); } diff --git a/czkawka_gui/src/connect_popovers.rs b/czkawka_gui/src/connect_popovers.rs index c738c9a..9befcc8 100644 --- a/czkawka_gui/src/connect_popovers.rs +++ b/czkawka_gui/src/connect_popovers.rs @@ -552,9 +552,12 @@ pub fn connect_select_all(gui_data: &GuiData) { let scrolled_window_zeroed_files_finder = gui_data.scrolled_window_zeroed_files_finder.clone(); let scrolled_window_same_music_finder = gui_data.scrolled_window_same_music_finder.clone(); let scrolled_window_duplicate_finder = gui_data.scrolled_window_duplicate_finder.clone(); + let scrolled_window_invalid_symlinks = gui_data.scrolled_window_invalid_symlinks.clone(); let popover_select_duplicate = gui_data.popover_select_duplicate.clone(); let popover_select_simple_list = gui_data.popover_select_simple_list.clone(); + let popover_select_very_simple_list = gui_data.popover_select_very_simple_list.clone(); let buttons_popover_simple_list_select_all = gui_data.buttons_popover_simple_list_select_all.clone(); + let buttons_popover_very_simple_list_select_all = gui_data.buttons_popover_very_simple_list_select_all.clone(); let buttons_popover_duplicate_select_all = gui_data.buttons_popover_duplicate_select_all.clone(); buttons_popover_duplicate_select_all.connect_clicked(move |_| match notebook_main_children_names.get(notebook_main.get_current_page().unwrap() as usize).unwrap().as_str() { "notebook_main_duplicate_finder_label" => { @@ -589,6 +592,15 @@ pub fn connect_select_all(gui_data: &GuiData) { } e => panic!("Not existent {}", e), }); + + let notebook_main_children_names = gui_data.notebook_main_children_names.clone(); + let notebook_main = gui_data.notebook_main.clone(); + buttons_popover_very_simple_list_select_all.connect_clicked(move |_| match notebook_main_children_names.get(notebook_main.get_current_page().unwrap() as usize).unwrap().as_str() { + "scrolled_window_invalid_symlinks" => { + popover_select_all(&popover_select_very_simple_list, &scrolled_window_invalid_symlinks); + } + e => panic!("Not existent {}", e), + }); } pub fn connect_unselect_all(gui_data: &GuiData) { let notebook_main_children_names = gui_data.notebook_main_children_names.clone(); @@ -602,9 +614,12 @@ pub fn connect_unselect_all(gui_data: &GuiData) { let scrolled_window_zeroed_files_finder = gui_data.scrolled_window_zeroed_files_finder.clone(); let scrolled_window_same_music_finder = gui_data.scrolled_window_same_music_finder.clone(); let scrolled_window_duplicate_finder = gui_data.scrolled_window_duplicate_finder.clone(); + let scrolled_window_invalid_symlinks = gui_data.scrolled_window_invalid_symlinks.clone(); let popover_select_duplicate = gui_data.popover_select_duplicate.clone(); let popover_select_simple_list = gui_data.popover_select_simple_list.clone(); + let popover_select_very_simple_list = gui_data.popover_select_very_simple_list.clone(); let buttons_popover_simple_list_unselect_all = gui_data.buttons_popover_simple_list_unselect_all.clone(); + let buttons_popover_very_simple_list_unselect_all = gui_data.buttons_popover_very_simple_list_unselect_all.clone(); let buttons_popover_duplicate_unselect_all = gui_data.buttons_popover_duplicate_unselect_all.clone(); buttons_popover_duplicate_unselect_all.connect_clicked(move |_| match notebook_main_children_names.get(notebook_main.get_current_page().unwrap() as usize).unwrap().as_str() { "notebook_main_duplicate_finder_label" => { @@ -639,6 +654,15 @@ pub fn connect_unselect_all(gui_data: &GuiData) { } e => panic!("Not existent {}", e), }); + + let notebook_main_children_names = gui_data.notebook_main_children_names.clone(); + let notebook_main = gui_data.notebook_main.clone(); + buttons_popover_very_simple_list_unselect_all.connect_clicked(move |_| match notebook_main_children_names.get(notebook_main.get_current_page().unwrap() as usize).unwrap().as_str() { + "scrolled_window_invalid_symlinks" => { + popover_unselect_all(&popover_select_very_simple_list, &scrolled_window_invalid_symlinks); + } + e => panic!("Not existent {}", e), + }); } pub fn connect_reverse(gui_data: &GuiData) { let notebook_main_children_names = gui_data.notebook_main_children_names.clone(); @@ -652,9 +676,12 @@ pub fn connect_reverse(gui_data: &GuiData) { let scrolled_window_zeroed_files_finder = gui_data.scrolled_window_zeroed_files_finder.clone(); let scrolled_window_same_music_finder = gui_data.scrolled_window_same_music_finder.clone(); let scrolled_window_duplicate_finder = gui_data.scrolled_window_duplicate_finder.clone(); + let scrolled_window_invalid_symlinks = gui_data.scrolled_window_invalid_symlinks.clone(); let popover_select_duplicate = gui_data.popover_select_duplicate.clone(); let popover_select_simple_list = gui_data.popover_select_simple_list.clone(); + let popover_select_very_simple_list = gui_data.popover_select_very_simple_list.clone(); let buttons_popover_simple_list_reverse = gui_data.buttons_popover_simple_list_reverse.clone(); + let buttons_popover_very_simple_list_reverse = gui_data.buttons_popover_very_simple_list_reverse.clone(); let buttons_popover_duplicate_reverse = gui_data.buttons_popover_duplicate_reverse.clone(); buttons_popover_duplicate_reverse.connect_clicked(move |_| match notebook_main_children_names.get(notebook_main.get_current_page().unwrap() as usize).unwrap().as_str() { "notebook_main_duplicate_finder_label" => { @@ -689,6 +716,15 @@ pub fn connect_reverse(gui_data: &GuiData) { } e => panic!("Not existent {}", e), }); + + let notebook_main_children_names = gui_data.notebook_main_children_names.clone(); + let notebook_main = gui_data.notebook_main.clone(); + buttons_popover_very_simple_list_reverse.connect_clicked(move |_| match notebook_main_children_names.get(notebook_main.get_current_page().unwrap() as usize).unwrap().as_str() { + "scrolled_window_invalid_symlinks" => { + popover_reverse(&popover_select_very_simple_list, &scrolled_window_invalid_symlinks); + } + e => panic!("Not existent {}", e), + }); } pub fn connect_all_except_oldest(gui_data: &GuiData) { diff --git a/czkawka_gui/src/connect_progress_window.rs b/czkawka_gui/src/connect_progress_window.rs index a0f92ee..42a7a57 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, same_music, similar_images, temporary, zeroed}; +use czkawka_core::{big_file, duplicate, empty_files, empty_folder, invalid_symlinks, same_music, similar_images, temporary, zeroed}; use futures::StreamExt; use gtk::{LabelExt, ProgressBarExt, WidgetExt}; @@ -16,6 +16,7 @@ pub fn connect_progress_window( mut futures_receiver_similar_images: futures::channel::mpsc::Receiver, 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, ) { let main_context = glib::MainContext::default(); @@ -230,4 +231,14 @@ pub fn connect_progress_window( }; main_context.spawn_local(future); } + { + // Invalid Symlinks + let label_stage = gui_data.label_stage.clone(); + let future = async move { + while let Some(item) = futures_receiver_invalid_symlinks.next().await { + label_stage.set_text(format!("Scanned {} files", item.files_checked).as_str()); + } + }; + main_context.spawn_local(future); + } } diff --git a/czkawka_gui/src/create_tree_view.rs b/czkawka_gui/src/create_tree_view.rs index aa4340c..d857153 100644 --- a/czkawka_gui/src/create_tree_view.rs +++ b/czkawka_gui/src/create_tree_view.rs @@ -394,3 +394,43 @@ pub fn create_tree_view_same_music(tree_view: &mut gtk::TreeView) { tree_view.set_vexpand(true); } + +pub fn create_tree_view_invalid_symlinks(tree_view: &mut gtk::TreeView) { + let renderer = gtk::CellRendererText::new(); + let column: gtk::TreeViewColumn = TreeViewColumn::new(); + column.pack_start(&renderer, true); + column.set_title("Symlink Path"); + column.set_resizable(true); + column.set_min_width(50); + column.add_attribute(&renderer, "text", ColumnsInvalidSymlinks::SymlinkPath 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("Destination Path"); + column.set_resizable(true); + column.set_min_width(50); + column.add_attribute(&renderer, "text", ColumnsInvalidSymlinks::DestinationPath 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("Type of Error"); + column.set_resizable(true); + column.set_min_width(50); + column.add_attribute(&renderer, "text", ColumnsInvalidSymlinks::TypeOfError 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", ColumnsInvalidSymlinks::Modification as i32); + tree_view.append_column(&column); + + tree_view.set_vexpand(true); +} diff --git a/czkawka_gui/src/gui_data.rs b/czkawka_gui/src/gui_data.rs index 9a9a695..bdb05cd 100644 --- a/czkawka_gui/src/gui_data.rs +++ b/czkawka_gui/src/gui_data.rs @@ -4,6 +4,7 @@ use czkawka_core::big_file::BigFile; use czkawka_core::duplicate::DuplicateFinder; use czkawka_core::empty_files::EmptyFiles; use czkawka_core::empty_folder::EmptyFolder; +use czkawka_core::invalid_symlinks::InvalidSymlinks; use czkawka_core::same_music::SameMusic; use czkawka_core::similar_images::SimilarImages; use czkawka_core::temporary::Temporary; @@ -24,9 +25,10 @@ pub struct GuiData { pub window_main: gtk::Window, // States - pub main_notebooks_labels: [String; 8], + pub main_notebooks_labels: [String; 9], pub upper_notebooks_labels: [String; 5], pub buttons_labels: [String; 5], + // Buttons state pub shared_buttons: Rc>>>, @@ -42,6 +44,7 @@ pub struct GuiData { pub shared_similar_images_state: Rc>, pub shared_zeroed_files_state: Rc>, pub shared_same_music_state: Rc>, + pub shared_same_invalid_symlinks: Rc>, //// GUI Entry pub entry_similar_images_minimal_size: gtk::Entry, @@ -82,9 +85,14 @@ pub struct GuiData { pub buttons_popover_simple_list_select_custom: gtk::Button, pub buttons_popover_simple_list_unselect_custom: gtk::Button, + pub buttons_popover_very_simple_list_select_all: gtk::Button, + pub buttons_popover_very_simple_list_unselect_all: gtk::Button, + pub buttons_popover_very_simple_list_reverse: gtk::Button, + //// Popovers pub popover_select_duplicate: gtk::Popover, pub popover_select_simple_list: gtk::Popover, + pub popover_select_very_simple_list: gtk::Popover, //// Check Buttons pub check_button_recursive: gtk::CheckButton, @@ -132,6 +140,7 @@ pub struct GuiData { pub scrolled_window_similar_images_finder: gtk::ScrolledWindow, pub scrolled_window_zeroed_files_finder: gtk::ScrolledWindow, pub scrolled_window_same_music_finder: gtk::ScrolledWindow, + pub scrolled_window_invalid_symlinks: gtk::ScrolledWindow, // Upper notebook pub scrolled_window_included_directories: gtk::ScrolledWindow, @@ -191,6 +200,7 @@ impl GuiData { "similar_images".to_string(), "zeroed_files".to_string(), "same_music".to_string(), + "invalid_symlinks".to_string(), ]; let upper_notebooks_labels = [ "included_directories".to_string(), @@ -242,6 +252,7 @@ impl GuiData { let shared_similar_images_state: Rc> = Rc::new(RefCell::new(SimilarImages::new())); 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())); //////////////////////////////////////////////////////////////////////////////////////////////// @@ -287,9 +298,14 @@ impl GuiData { let buttons_popover_simple_list_select_custom: gtk::Button = builder.get_object("buttons_popover_simple_list_select_custom").unwrap(); let buttons_popover_simple_list_unselect_custom: gtk::Button = builder.get_object("buttons_popover_simple_list_unselect_custom").unwrap(); + let buttons_popover_very_simple_list_select_all: gtk::Button = builder.get_object("buttons_popover_very_simple_list_select_all").unwrap(); + let buttons_popover_very_simple_list_unselect_all: gtk::Button = builder.get_object("buttons_popover_very_simple_list_unselect_all").unwrap(); + let buttons_popover_very_simple_list_reverse: gtk::Button = builder.get_object("buttons_popover_very_simple_list_reverse").unwrap(); + //// Popovers let popover_select_duplicate: gtk::Popover = builder.get_object("popover_select_duplicate").unwrap(); let popover_select_simple_list: gtk::Popover = builder.get_object("popover_select_simple_list").unwrap(); + let popover_select_very_simple_list: gtk::Popover = builder.get_object("popover_select_very_simple_list").unwrap(); //// Check Buttons let check_button_recursive: gtk::CheckButton = builder.get_object("check_button_recursive").unwrap(); @@ -342,6 +358,7 @@ impl GuiData { let scrolled_window_similar_images_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_similar_images_finder").unwrap(); 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(); // Upper notebook let scrolled_window_included_directories: gtk::ScrolledWindow = builder.get_object("scrolled_window_included_directories").unwrap(); @@ -396,6 +413,7 @@ impl GuiData { shared_similar_images_state, shared_zeroed_files_state, shared_same_music_state, + shared_same_invalid_symlinks, entry_similar_images_minimal_size, entry_duplicate_minimal_size, entry_allowed_extensions, @@ -428,8 +446,12 @@ impl GuiData { buttons_popover_simple_list_reverse, buttons_popover_simple_list_select_custom, buttons_popover_simple_list_unselect_custom, + buttons_popover_very_simple_list_select_all, + buttons_popover_very_simple_list_unselect_all, + buttons_popover_very_simple_list_reverse, popover_select_duplicate, popover_select_simple_list, + popover_select_very_simple_list, check_button_recursive, check_button_music_title, check_button_music_artist, @@ -460,6 +482,7 @@ impl GuiData { scrolled_window_similar_images_finder, scrolled_window_zeroed_files_finder, scrolled_window_same_music_finder, + scrolled_window_invalid_symlinks, scrolled_window_included_directories, scrolled_window_excluded_directories, dialog_progress, diff --git a/czkawka_gui/src/help_functions.rs b/czkawka_gui/src/help_functions.rs index 3825df3..8e8d46e 100644 --- a/czkawka_gui/src/help_functions.rs +++ b/czkawka_gui/src/help_functions.rs @@ -3,6 +3,8 @@ use czkawka_core::common_messages::Messages; use czkawka_core::duplicate::DuplicateFinder; use czkawka_core::empty_files::EmptyFiles; use czkawka_core::empty_folder::EmptyFolder; +use czkawka_core::invalid_symlinks; +use czkawka_core::invalid_symlinks::InvalidSymlinks; use czkawka_core::same_music::SameMusic; use czkawka_core::similar_images::{SimilarImages, Similarity}; use czkawka_core::temporary::Temporary; @@ -21,6 +23,7 @@ pub enum Message { SimilarImages(SimilarImages), ZeroedFiles(ZeroedFiles), SameMusic(SameMusic), + InvalidSymlinks(InvalidSymlinks), } pub enum ColumnsDuplicates { @@ -90,6 +93,12 @@ pub enum ColumnsSameMusic { Color, TextColor, } +pub enum ColumnsInvalidSymlinks { + SymlinkPath = 0, + DestinationPath, + TypeOfError, + Modification, +} pub const TEXT_COLOR: &str = "#ffffff"; pub const MAIN_ROW_COLOR: &str = "#343434"; @@ -248,6 +257,13 @@ pub fn get_text_from_similarity(similarity: &Similarity) -> &str { } } +pub fn get_text_from_invalid_symlink_cause(error: &invalid_symlinks::ErrorType) -> &str { + match error { + invalid_symlinks::ErrorType::InfiniteRecursion => "Infinite recursion", + invalid_symlinks::ErrorType::NonExistentFile => "Non existent destination file", + } +} + pub fn get_list_store(scrolled_window: >k::ScrolledWindow) -> ListStore { let list_store = scrolled_window.get_children().get(0).unwrap().clone().downcast::().unwrap().get_model().unwrap().downcast::().unwrap(); diff --git a/czkawka_gui/src/initialize_gui.rs b/czkawka_gui/src/initialize_gui.rs index 5aacd1b..4921cab 100644 --- a/czkawka_gui/src/initialize_gui.rs +++ b/czkawka_gui/src/initialize_gui.rs @@ -26,6 +26,7 @@ pub fn initialize_gui(gui_data: &GuiData) { let scrolled_window_big_files_finder = gui_data.scrolled_window_big_files_finder.clone(); let scrolled_window_similar_images_finder = gui_data.scrolled_window_similar_images_finder.clone(); let scrolled_window_same_music_finder = gui_data.scrolled_window_same_music_finder.clone(); + let scrolled_window_invalid_symlinks = gui_data.scrolled_window_invalid_symlinks.clone(); let scrolled_window_zeroed_files_finder = gui_data.scrolled_window_zeroed_files_finder.clone(); let scrolled_window_included_directories = gui_data.scrolled_window_included_directories.clone(); let scrolled_window_excluded_directories = gui_data.scrolled_window_excluded_directories.clone(); @@ -277,6 +278,20 @@ pub fn initialize_gui(gui_data: &GuiData) { scrolled_window_same_music_finder.add(&tree_view); scrolled_window_same_music_finder.show_all(); } + // Invalid Symlinks + { + 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_invalid_symlinks(&mut tree_view); + + scrolled_window_invalid_symlinks.add(&tree_view); + scrolled_window_invalid_symlinks.show_all(); + } } // Set Included Directory diff --git a/czkawka_gui/src/main.rs b/czkawka_gui/src/main.rs index d08495f..1b494da 100644 --- a/czkawka_gui/src/main.rs +++ b/czkawka_gui/src/main.rs @@ -76,6 +76,7 @@ fn main() { let (futures_sender_similar_images, futures_receiver_similar_images): (futures::channel::mpsc::Sender, futures::channel::mpsc::Receiver) = futures::channel::mpsc::channel(20); 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); initialize_gui(&gui_data); reset_configuration(&gui_data, false); // Fallback for invalid loading setting project @@ -94,6 +95,7 @@ fn main() { futures_sender_similar_images, futures_sender_temporary, futures_sender_zeroed, + futures_sender_invalid_symlinks, ); connect_button_select(&gui_data); connect_button_stop(&gui_data); @@ -112,6 +114,7 @@ fn main() { futures_receiver_similar_images, futures_receiver_temporary, futures_receiver_zeroed, + futures_receiver_invalid_symlinks, ); connect_hide_text_view_errors(&gui_data); connect_settings(&gui_data); diff --git a/czkawka_gui/src/saving_loading.rs b/czkawka_gui/src/saving_loading.rs index b9534c2..24abb6a 100644 --- a/czkawka_gui/src/saving_loading.rs +++ b/czkawka_gui/src/saving_loading.rs @@ -426,7 +426,7 @@ pub fn reset_configuration(gui_data: &GuiData, manual_clearing: bool) { { let entry_excluded_items = gui_data.entry_excluded_items.clone(); if cfg!(target_family = "unix") { - entry_excluded_items.set_text("*/.git/*,*/node_modules/*,*/lost+found/*,*/Trash/*,*/.Trash-*/*"); + entry_excluded_items.set_text("*/.git/*,*/node_modules/*,*/lost+found/*,*/Trash/*,*/.Trash-*/*,*/snap/*"); } if cfg!(target_family = "windows") { entry_excluded_items.set_text("*/.git/*,*/node_modules/*,*/lost+found/*,*:/windows/*");