1
0
Fork 0
mirror of synced 2024-04-28 09:33:30 +12:00

Add broken files support (#202)

This commit is contained in:
Rafał Mikrut 2021-01-12 20:06:12 +01:00 committed by GitHub
parent 1a011644bf
commit eeaaea20cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 804 additions and 12 deletions

View file

@ -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 | |

View file

@ -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"#;

View file

@ -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();
}
}
}

View file

@ -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<FileEntry>,
broken_files: Vec<FileEntry>,
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<ProgressData>>) {
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<FileEntry> {
&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<PathBuf>) -> bool {
self.directories.set_included_directory(included_directory, &mut self.text_messages)
}
pub fn set_excluded_directory(&mut self, excluded_directory: Vec<PathBuf>) {
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<String>) {
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<ProgressData>>) -> bool {
let start_time: SystemTime = SystemTime::now();
let mut folders_to_check: Vec<PathBuf> = Vec::with_capacity(1024 * 2); // This should be small enough too not see to big difference and big enough to store most of paths without needing to resize vector
// 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(&current_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(&current_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<ProgressData>>) -> 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::<Vec<FileEntry>>();
// 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());
}
}

View file

@ -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");

View file

@ -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;
}

View file

@ -2031,6 +2031,30 @@ Author: Rafał Mikrut
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolled_window_broken_files">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="position">9</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Broken Files</property>
</object>
<packing>
<property name="position">9</property>
<property name="tab_fill">False</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>

View file

@ -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);
}
}
});
}

View file

@ -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);
});

View file

@ -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<temporary::ProgressData>,
futures_sender_zeroed: futures::channel::mpsc::Sender<zeroed::ProgressData>,
futures_sender_invalid_symlinks: futures::channel::mpsc::Sender<invalid_symlinks::ProgressData>,
futures_sender_broken_files: futures::channel::mpsc::Sender<broken_files::ProgressData>,
) {
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

View file

@ -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"]);
}
}

View file

@ -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)

View file

@ -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();

View file

@ -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<temporary::ProgressData>,
mut futures_receiver_zeroed: futures::channel::mpsc::Receiver<zeroed::ProgressData>,
mut futures_receiver_invalid_symlinks: futures::channel::mpsc::Receiver<invalid_symlinks::ProgressData>,
mut futures_receiver_broken_files: futures::channel::mpsc::Receiver<broken_files::ProgressData>,
) {
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);
}
}

View file

@ -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);
}

View file

@ -83,6 +83,15 @@ pub fn opening_double_click_function_invalid_symlinks(tree_view: &gtk::TreeView,
gtk::Inhibit(false)
}
pub fn opening_double_click_function_broken_files(tree_view: &gtk::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,

View file

@ -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<RefCell<ZeroedFiles>>,
pub shared_same_music_state: Rc<RefCell<SameMusic>>,
pub shared_same_invalid_symlinks: Rc<RefCell<InvalidSymlinks>>,
pub shared_broken_files_state: Rc<RefCell<BrokenFiles>>,
//// Entry
pub entry_info: gtk::Entry,
@ -124,6 +126,7 @@ impl GuiData {
let shared_zeroed_files_state: Rc<RefCell<_>> = Rc::new(RefCell::new(ZeroedFiles::new()));
let shared_same_music_state: Rc<RefCell<_>> = Rc::new(RefCell::new(SameMusic::new()));
let shared_same_invalid_symlinks: Rc<RefCell<_>> = Rc::new(RefCell::new(InvalidSymlinks::new()));
let shared_broken_files_state: Rc<RefCell<_>> = 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,

View file

@ -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,

View file

@ -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";

View file

@ -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

View file

@ -83,6 +83,7 @@ fn main() {
let (futures_sender_temporary, futures_receiver_temporary): (futures::channel::mpsc::Sender<temporary::ProgressData>, futures::channel::mpsc::Receiver<temporary::ProgressData>) = futures::channel::mpsc::channel(20);
let (futures_sender_zeroed, futures_receiver_zeroed): (futures::channel::mpsc::Sender<zeroed::ProgressData>, futures::channel::mpsc::Receiver<zeroed::ProgressData>) = futures::channel::mpsc::channel(20);
let (futures_sender_invalid_symlinks, futures_receiver_invalid_symlinks): (futures::channel::mpsc::Sender<invalid_symlinks::ProgressData>, futures::channel::mpsc::Receiver<invalid_symlinks::ProgressData>) = futures::channel::mpsc::channel(20);
let (futures_sender_broken_files, futures_receiver_broken_files): (futures::channel::mpsc::Sender<broken_files::ProgressData>, futures::channel::mpsc::Receiver<broken_files::ProgressData>) = 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);

View file

@ -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),
]
}

View file

@ -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