|
|
|
@ -11,7 +11,7 @@ use std::os::unix::fs::MetadataExt;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use std::thread::sleep;
|
|
|
|
|
use std::thread::{sleep, JoinHandle};
|
|
|
|
|
use std::time::{Duration, SystemTime};
|
|
|
|
|
use std::{fs, mem, thread};
|
|
|
|
|
|
|
|
|
@ -67,6 +67,8 @@ pub struct Info {
|
|
|
|
|
pub number_of_duplicated_files_by_hash: usize,
|
|
|
|
|
pub number_of_groups_by_name: usize,
|
|
|
|
|
pub number_of_duplicated_files_by_name: usize,
|
|
|
|
|
pub number_of_groups_by_size_name: usize,
|
|
|
|
|
pub number_of_duplicated_files_by_size_name: usize,
|
|
|
|
|
pub lost_space_by_size: u64,
|
|
|
|
|
pub lost_space_by_hash: u64,
|
|
|
|
|
}
|
|
|
|
@ -81,11 +83,13 @@ impl Info {
|
|
|
|
|
pub struct DuplicateFinder {
|
|
|
|
|
text_messages: Messages,
|
|
|
|
|
information: Info,
|
|
|
|
|
files_with_identical_names: BTreeMap<String, Vec<FileEntry>>, // File Size, File Entry
|
|
|
|
|
files_with_identical_size: BTreeMap<u64, Vec<FileEntry>>, // File Size, File Entry
|
|
|
|
|
files_with_identical_hashes: BTreeMap<u64, Vec<Vec<FileEntry>>>, // File Size, next grouped by file size, next grouped by hash
|
|
|
|
|
files_with_identical_names_referenced: BTreeMap<String, (FileEntry, Vec<FileEntry>)>, // File Size, File Entry
|
|
|
|
|
files_with_identical_size_referenced: BTreeMap<u64, (FileEntry, Vec<FileEntry>)>, // File Size, File Entry
|
|
|
|
|
files_with_identical_names: BTreeMap<String, Vec<FileEntry>>, // File Size, File Entry
|
|
|
|
|
files_with_identical_size_names: BTreeMap<(u64, String), Vec<FileEntry>>, // File (Size, Name), File Entry
|
|
|
|
|
files_with_identical_size: BTreeMap<u64, Vec<FileEntry>>, // File Size, File Entry
|
|
|
|
|
files_with_identical_hashes: BTreeMap<u64, Vec<Vec<FileEntry>>>, // File Size, next grouped by file size, next grouped by hash
|
|
|
|
|
files_with_identical_names_referenced: BTreeMap<String, (FileEntry, Vec<FileEntry>)>, // File Size, File Entry
|
|
|
|
|
files_with_identical_size_names_referenced: BTreeMap<(u64, String), (FileEntry, Vec<FileEntry>)>, // File (Size, Name), File Entry
|
|
|
|
|
files_with_identical_size_referenced: BTreeMap<u64, (FileEntry, Vec<FileEntry>)>, // File Size, File Entry
|
|
|
|
|
files_with_identical_hashes_referenced: BTreeMap<u64, Vec<(FileEntry, Vec<FileEntry>)>>, // File Size, next grouped by file size, next grouped by hash
|
|
|
|
|
directories: Directories,
|
|
|
|
|
allowed_extensions: Extensions,
|
|
|
|
@ -116,8 +120,10 @@ impl DuplicateFinder {
|
|
|
|
|
information: Info::new(),
|
|
|
|
|
files_with_identical_names: Default::default(),
|
|
|
|
|
files_with_identical_size: Default::default(),
|
|
|
|
|
files_with_identical_size_names: Default::default(),
|
|
|
|
|
files_with_identical_hashes: Default::default(),
|
|
|
|
|
files_with_identical_names_referenced: Default::default(),
|
|
|
|
|
files_with_identical_size_names_referenced: Default::default(),
|
|
|
|
|
files_with_identical_size_referenced: Default::default(),
|
|
|
|
|
files_with_identical_hashes_referenced: Default::default(),
|
|
|
|
|
recursive_search: true,
|
|
|
|
@ -148,24 +154,30 @@ impl DuplicateFinder {
|
|
|
|
|
|
|
|
|
|
match self.check_method {
|
|
|
|
|
CheckingMethod::Name => {
|
|
|
|
|
if !self.check_files_name(stop_receiver, progress_sender) {
|
|
|
|
|
self.stopped_search = true;
|
|
|
|
|
self.stopped_search = !self.check_files_name(stop_receiver, progress_sender); // TODO restore this to name
|
|
|
|
|
if self.stopped_search {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CheckingMethod::SizeName => {
|
|
|
|
|
self.stopped_search = !self.check_files_size_name(stop_receiver, progress_sender);
|
|
|
|
|
if self.stopped_search {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CheckingMethod::Size => {
|
|
|
|
|
if !self.check_files_size(stop_receiver, progress_sender) {
|
|
|
|
|
self.stopped_search = true;
|
|
|
|
|
self.stopped_search = !self.check_files_size(stop_receiver, progress_sender);
|
|
|
|
|
if self.stopped_search {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CheckingMethod::Hash => {
|
|
|
|
|
if !self.check_files_size(stop_receiver, progress_sender) {
|
|
|
|
|
self.stopped_search = true;
|
|
|
|
|
self.stopped_search = !self.check_files_size(stop_receiver, progress_sender);
|
|
|
|
|
if self.stopped_search {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if !self.check_files_hash(stop_receiver, progress_sender) {
|
|
|
|
|
self.stopped_search = true;
|
|
|
|
|
self.stopped_search = !self.check_files_hash(stop_receiver, progress_sender);
|
|
|
|
|
if self.stopped_search {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@ -221,6 +233,11 @@ impl DuplicateFinder {
|
|
|
|
|
&self.files_with_identical_size
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub const fn get_files_sorted_by_size_name(&self) -> &BTreeMap<(u64, String), Vec<FileEntry>> {
|
|
|
|
|
&self.files_with_identical_size_names
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub const fn get_files_sorted_by_hash(&self) -> &BTreeMap<u64, Vec<Vec<FileEntry>>> {
|
|
|
|
|
&self.files_with_identical_hashes
|
|
|
|
@ -319,6 +336,11 @@ impl DuplicateFinder {
|
|
|
|
|
&self.files_with_identical_size_referenced
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn get_files_with_identical_size_names_referenced(&self) -> &BTreeMap<(u64, String), (FileEntry, Vec<FileEntry>)> {
|
|
|
|
|
&self.files_with_identical_size_names_referenced
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn check_files_name(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool {
|
|
|
|
|
let group_by_func = if self.case_sensitive_name_comparison {
|
|
|
|
|
|fe: &FileEntry| fe.path.file_name().unwrap().to_string_lossy().to_string()
|
|
|
|
@ -388,18 +410,7 @@ impl DuplicateFinder {
|
|
|
|
|
self.files_with_identical_names_referenced.insert(fe.path.to_string_lossy().to_string(), (fe, vec_fe));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.use_reference_folders {
|
|
|
|
|
for (_fe, vector) in self.files_with_identical_names_referenced.values() {
|
|
|
|
|
self.information.number_of_duplicated_files_by_name += vector.len();
|
|
|
|
|
self.information.number_of_groups_by_name += 1;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for vector in self.files_with_identical_names.values() {
|
|
|
|
|
self.information.number_of_duplicated_files_by_name += vector.len() - 1;
|
|
|
|
|
self.information.number_of_groups_by_name += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.calculate_name_stats();
|
|
|
|
|
|
|
|
|
|
Common::print_time(start_time, SystemTime::now(), "check_files_name");
|
|
|
|
|
true
|
|
|
|
@ -411,21 +422,33 @@ impl DuplicateFinder {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read file length and puts it to different boxes(each for different lengths)
|
|
|
|
|
/// If in box is only 1 result, then it is removed
|
|
|
|
|
fn check_files_size(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool {
|
|
|
|
|
let max_stage = match self.check_method {
|
|
|
|
|
CheckingMethod::Size => 0,
|
|
|
|
|
CheckingMethod::Hash => 2,
|
|
|
|
|
_ => panic!(),
|
|
|
|
|
fn calculate_name_stats(&mut self) {
|
|
|
|
|
if self.use_reference_folders {
|
|
|
|
|
for (_fe, vector) in self.files_with_identical_names_referenced.values() {
|
|
|
|
|
self.information.number_of_duplicated_files_by_name += vector.len();
|
|
|
|
|
self.information.number_of_groups_by_name += 1;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for vector in self.files_with_identical_names.values() {
|
|
|
|
|
self.information.number_of_duplicated_files_by_name += vector.len() - 1;
|
|
|
|
|
self.information.number_of_groups_by_name += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn check_files_size_name(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool {
|
|
|
|
|
let group_by_func = if self.case_sensitive_name_comparison {
|
|
|
|
|
|fe: &FileEntry| (fe.size, fe.path.file_name().unwrap().to_string_lossy().to_string())
|
|
|
|
|
} else {
|
|
|
|
|
|fe: &FileEntry| (fe.size, fe.path.file_name().unwrap().to_string_lossy().to_lowercase())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let result = DirTraversalBuilder::new()
|
|
|
|
|
.root_dirs(self.directories.included_directories.clone())
|
|
|
|
|
.group_by(|fe| fe.size)
|
|
|
|
|
.group_by(group_by_func)
|
|
|
|
|
.stop_receiver(stop_receiver)
|
|
|
|
|
.progress_sender(progress_sender)
|
|
|
|
|
.checking_method(self.check_method)
|
|
|
|
|
.max_stage(max_stage)
|
|
|
|
|
.checking_method(CheckingMethod::Name)
|
|
|
|
|
.directories(self.directories.clone())
|
|
|
|
|
.allowed_extensions(self.allowed_extensions.clone())
|
|
|
|
|
.excluded_items(self.excluded_items.clone())
|
|
|
|
@ -440,29 +463,23 @@ impl DuplicateFinder {
|
|
|
|
|
grouped_file_entries,
|
|
|
|
|
warnings,
|
|
|
|
|
} => {
|
|
|
|
|
self.files_with_identical_size = grouped_file_entries;
|
|
|
|
|
self.files_with_identical_size_names = grouped_file_entries;
|
|
|
|
|
self.text_messages.warnings.extend(warnings);
|
|
|
|
|
|
|
|
|
|
// Create new BTreeMap without single size entries(files have not duplicates)
|
|
|
|
|
let mut old_map: BTreeMap<u64, Vec<FileEntry>> = Default::default();
|
|
|
|
|
mem::swap(&mut old_map, &mut self.files_with_identical_size);
|
|
|
|
|
|
|
|
|
|
for (size, vec) in old_map {
|
|
|
|
|
if vec.len() <= 1 {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let vector = if self.ignore_hard_links { filter_hard_links(&vec) } else { vec };
|
|
|
|
|
let mut new_map: BTreeMap<(u64, String), Vec<FileEntry>> = Default::default();
|
|
|
|
|
|
|
|
|
|
for (name_size, vector) in &self.files_with_identical_size_names {
|
|
|
|
|
if vector.len() > 1 {
|
|
|
|
|
self.files_with_identical_size.insert(size, vector);
|
|
|
|
|
new_map.insert(name_size.clone(), vector.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.files_with_identical_size_names = new_map;
|
|
|
|
|
|
|
|
|
|
// Reference - only use in size, because later hash will be counted differently
|
|
|
|
|
if self.use_reference_folders && self.check_method == CheckingMethod::Size {
|
|
|
|
|
if self.use_reference_folders {
|
|
|
|
|
let mut btree_map = Default::default();
|
|
|
|
|
mem::swap(&mut self.files_with_identical_size, &mut btree_map);
|
|
|
|
|
mem::swap(&mut self.files_with_identical_size_names, &mut btree_map);
|
|
|
|
|
let reference_directories = self.directories.reference_directories.clone();
|
|
|
|
|
let vec = btree_map
|
|
|
|
|
.into_iter()
|
|
|
|
@ -485,24 +502,89 @@ impl DuplicateFinder {
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<(FileEntry, Vec<FileEntry>)>>();
|
|
|
|
|
for (fe, vec_fe) in vec {
|
|
|
|
|
self.files_with_identical_size_referenced.insert(fe.size, (fe, vec_fe));
|
|
|
|
|
self.files_with_identical_size_names_referenced
|
|
|
|
|
.insert((fe.size, fe.path.to_string_lossy().to_string()), (fe, vec_fe));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.calculate_size_name_stats();
|
|
|
|
|
|
|
|
|
|
if self.use_reference_folders {
|
|
|
|
|
for (size, (_fe, vector)) in &self.files_with_identical_size_referenced {
|
|
|
|
|
self.information.number_of_duplicated_files_by_size += vector.len();
|
|
|
|
|
self.information.number_of_groups_by_size += 1;
|
|
|
|
|
self.information.lost_space_by_size += (vector.len() as u64) * size;
|
|
|
|
|
Common::print_time(start_time, SystemTime::now(), "check_files_size_name");
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
DirTraversalResult::SuccessFolders { .. } => {
|
|
|
|
|
unreachable!()
|
|
|
|
|
}
|
|
|
|
|
DirTraversalResult::Stopped => false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn calculate_size_name_stats(&mut self) {
|
|
|
|
|
if self.use_reference_folders {
|
|
|
|
|
for ((size, _name), (_fe, vector)) in &self.files_with_identical_size_names_referenced {
|
|
|
|
|
self.information.number_of_duplicated_files_by_size_name += vector.len();
|
|
|
|
|
self.information.number_of_groups_by_size_name += 1;
|
|
|
|
|
self.information.lost_space_by_size += (vector.len() as u64) * size;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for ((size, _name), vector) in &self.files_with_identical_size_names {
|
|
|
|
|
self.information.number_of_duplicated_files_by_size_name += vector.len() - 1;
|
|
|
|
|
self.information.number_of_groups_by_size_name += 1;
|
|
|
|
|
self.information.lost_space_by_size += (vector.len() as u64 - 1) * size;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read file length and puts it to different boxes(each for different lengths)
|
|
|
|
|
/// If in box is only 1 result, then it is removed
|
|
|
|
|
fn check_files_size(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool {
|
|
|
|
|
let max_stage = match self.check_method {
|
|
|
|
|
CheckingMethod::Size => 0,
|
|
|
|
|
CheckingMethod::Hash => 2,
|
|
|
|
|
_ => panic!(),
|
|
|
|
|
};
|
|
|
|
|
let result = DirTraversalBuilder::new()
|
|
|
|
|
.root_dirs(self.directories.included_directories.clone())
|
|
|
|
|
.group_by(|fe| fe.size)
|
|
|
|
|
.stop_receiver(stop_receiver)
|
|
|
|
|
.progress_sender(progress_sender)
|
|
|
|
|
.checking_method(self.check_method)
|
|
|
|
|
.max_stage(max_stage)
|
|
|
|
|
.directories(self.directories.clone())
|
|
|
|
|
.allowed_extensions(self.allowed_extensions.clone())
|
|
|
|
|
.excluded_items(self.excluded_items.clone())
|
|
|
|
|
.recursive_search(self.recursive_search)
|
|
|
|
|
.minimal_file_size(self.minimal_file_size)
|
|
|
|
|
.maximal_file_size(self.maximal_file_size)
|
|
|
|
|
.build()
|
|
|
|
|
.run();
|
|
|
|
|
match result {
|
|
|
|
|
DirTraversalResult::SuccessFiles {
|
|
|
|
|
start_time,
|
|
|
|
|
grouped_file_entries,
|
|
|
|
|
warnings,
|
|
|
|
|
} => {
|
|
|
|
|
self.files_with_identical_size = grouped_file_entries;
|
|
|
|
|
self.text_messages.warnings.extend(warnings);
|
|
|
|
|
|
|
|
|
|
// Create new BTreeMap without single size entries(files have not duplicates)
|
|
|
|
|
let mut old_map: BTreeMap<u64, Vec<FileEntry>> = Default::default();
|
|
|
|
|
mem::swap(&mut old_map, &mut self.files_with_identical_size);
|
|
|
|
|
|
|
|
|
|
for (size, vec) in old_map {
|
|
|
|
|
if vec.len() <= 1 {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for (size, vector) in &self.files_with_identical_size {
|
|
|
|
|
self.information.number_of_duplicated_files_by_size += vector.len() - 1;
|
|
|
|
|
self.information.number_of_groups_by_size += 1;
|
|
|
|
|
self.information.lost_space_by_size += (vector.len() as u64 - 1) * size;
|
|
|
|
|
|
|
|
|
|
let vector = if self.ignore_hard_links { filter_hard_links(&vec) } else { vec };
|
|
|
|
|
|
|
|
|
|
if vector.len() > 1 {
|
|
|
|
|
self.files_with_identical_size.insert(size, vector);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.filter_reference_folders_by_size();
|
|
|
|
|
self.calculate_size_stats();
|
|
|
|
|
|
|
|
|
|
Common::print_time(start_time, SystemTime::now(), "check_files_size");
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
@ -513,35 +595,78 @@ impl DuplicateFinder {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The slowest checking type, which must be applied after checking for size
|
|
|
|
|
fn check_files_hash(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool {
|
|
|
|
|
assert_eq!(self.check_method, CheckingMethod::Hash);
|
|
|
|
|
|
|
|
|
|
let check_type = Arc::new(self.hash_type);
|
|
|
|
|
|
|
|
|
|
let start_time: SystemTime = SystemTime::now();
|
|
|
|
|
let check_was_stopped = AtomicBool::new(false); // Used for breaking from GUI and ending check thread
|
|
|
|
|
let mut pre_checked_map: BTreeMap<u64, Vec<FileEntry>> = Default::default();
|
|
|
|
|
fn calculate_size_stats(&mut self) {
|
|
|
|
|
if self.use_reference_folders {
|
|
|
|
|
for (size, (_fe, vector)) in &self.files_with_identical_size_referenced {
|
|
|
|
|
self.information.number_of_duplicated_files_by_size += vector.len();
|
|
|
|
|
self.information.number_of_groups_by_size += 1;
|
|
|
|
|
self.information.lost_space_by_size += (vector.len() as u64) * size;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for (size, vector) in &self.files_with_identical_size {
|
|
|
|
|
self.information.number_of_duplicated_files_by_size += vector.len() - 1;
|
|
|
|
|
self.information.number_of_groups_by_size += 1;
|
|
|
|
|
self.information.lost_space_by_size += (vector.len() as u64 - 1) * size;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//// PROGRESS THREAD START
|
|
|
|
|
let progress_thread_run = Arc::new(AtomicBool::new(true));
|
|
|
|
|
/// This step check for references, only when checking for size.
|
|
|
|
|
/// This is needed, because later reference folders looks for hashes, not size
|
|
|
|
|
fn filter_reference_folders_by_size(&mut self) {
|
|
|
|
|
if self.use_reference_folders && self.check_method == CheckingMethod::Size {
|
|
|
|
|
let mut btree_map = Default::default();
|
|
|
|
|
mem::swap(&mut self.files_with_identical_size, &mut btree_map);
|
|
|
|
|
let reference_directories = self.directories.reference_directories.clone();
|
|
|
|
|
let vec = btree_map
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter_map(|(_size, vec_file_entry)| {
|
|
|
|
|
let mut files_from_referenced_folders = Vec::new();
|
|
|
|
|
let mut normal_files = Vec::new();
|
|
|
|
|
for file_entry in vec_file_entry {
|
|
|
|
|
if reference_directories.iter().any(|e| file_entry.path.starts_with(e)) {
|
|
|
|
|
files_from_referenced_folders.push(file_entry);
|
|
|
|
|
} else {
|
|
|
|
|
normal_files.push(file_entry);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let atomic_file_counter = Arc::new(AtomicUsize::new(0));
|
|
|
|
|
if files_from_referenced_folders.is_empty() || normal_files.is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some((files_from_referenced_folders.pop().unwrap(), normal_files))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<(FileEntry, Vec<FileEntry>)>>();
|
|
|
|
|
for (fe, vec_fe) in vec {
|
|
|
|
|
self.files_with_identical_size_referenced.insert(fe.size, (fe, vec_fe));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let progress_thread_handle = if let Some(progress_sender) = progress_sender {
|
|
|
|
|
// TODO Generalize this if possible with different tools
|
|
|
|
|
fn prepare_hash_thread_handler(
|
|
|
|
|
&self,
|
|
|
|
|
progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>,
|
|
|
|
|
progress_thread_run: Arc<AtomicBool>,
|
|
|
|
|
atomic_counter: Arc<AtomicUsize>,
|
|
|
|
|
current_stage: u8,
|
|
|
|
|
max_stage: u8,
|
|
|
|
|
max_value: usize,
|
|
|
|
|
) -> JoinHandle<()> {
|
|
|
|
|
if let Some(progress_sender) = progress_sender {
|
|
|
|
|
let 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_with_identical_size.values().map(Vec::len).sum();
|
|
|
|
|
let progress_thread_run = progress_thread_run;
|
|
|
|
|
let atomic_counter = atomic_counter;
|
|
|
|
|
let checking_method = self.check_method;
|
|
|
|
|
thread::spawn(move || loop {
|
|
|
|
|
progress_send
|
|
|
|
|
.unbounded_send(ProgressData {
|
|
|
|
|
checking_method,
|
|
|
|
|
current_stage: 1,
|
|
|
|
|
max_stage: 2,
|
|
|
|
|
entries_checked: atomic_file_counter.load(Ordering::Relaxed),
|
|
|
|
|
entries_to_check: files_to_check,
|
|
|
|
|
current_stage,
|
|
|
|
|
max_stage,
|
|
|
|
|
entries_checked: atomic_counter.load(Ordering::Relaxed),
|
|
|
|
|
entries_to_check: max_value,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
if !progress_thread_run.load(Ordering::Relaxed) {
|
|
|
|
@ -551,166 +676,174 @@ impl DuplicateFinder {
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
thread::spawn(|| {})
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//// PROGRESS THREAD END
|
|
|
|
|
fn prehashing(
|
|
|
|
|
&mut self,
|
|
|
|
|
stop_receiver: Option<&Receiver<()>>,
|
|
|
|
|
progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>,
|
|
|
|
|
pre_checked_map: &mut BTreeMap<u64, Vec<FileEntry>>,
|
|
|
|
|
) -> Option<()> {
|
|
|
|
|
let start_time: SystemTime = SystemTime::now();
|
|
|
|
|
let check_type = self.hash_type;
|
|
|
|
|
let check_was_stopped = AtomicBool::new(false); // Used for breaking from GUI and ending check thread
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////// PREHASHING START
|
|
|
|
|
{
|
|
|
|
|
let loaded_hash_map;
|
|
|
|
|
let mut records_already_cached: BTreeMap<u64, Vec<FileEntry>> = Default::default();
|
|
|
|
|
let mut non_cached_files_to_check: BTreeMap<u64, Vec<FileEntry>> = Default::default();
|
|
|
|
|
let progress_thread_run = Arc::new(AtomicBool::new(true));
|
|
|
|
|
let atomic_file_counter = Arc::new(AtomicUsize::new(0));
|
|
|
|
|
let progress_thread_handle = self.prepare_hash_thread_handler(
|
|
|
|
|
progress_sender,
|
|
|
|
|
progress_thread_run.clone(),
|
|
|
|
|
atomic_file_counter.clone(),
|
|
|
|
|
1,
|
|
|
|
|
2,
|
|
|
|
|
self.files_with_identical_size.values().map(Vec::len).sum(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Cache algorithm
|
|
|
|
|
// - Load data from cache
|
|
|
|
|
// - Convert from BT<u64,Vec<FileEntry>> to BT<String,FileEntry>
|
|
|
|
|
// - Save to proper values
|
|
|
|
|
if self.use_prehash_cache {
|
|
|
|
|
loaded_hash_map = match load_hashes_from_file(&mut self.text_messages, self.delete_outdated_cache, &self.hash_type, true) {
|
|
|
|
|
Some(t) => t,
|
|
|
|
|
None => Default::default(),
|
|
|
|
|
};
|
|
|
|
|
let loaded_hash_map;
|
|
|
|
|
let mut records_already_cached: BTreeMap<u64, Vec<FileEntry>> = Default::default();
|
|
|
|
|
let mut non_cached_files_to_check: BTreeMap<u64, Vec<FileEntry>> = Default::default();
|
|
|
|
|
|
|
|
|
|
// Cache algorithm
|
|
|
|
|
// - Load data from cache
|
|
|
|
|
// - Convert from BT<u64,Vec<FileEntry>> to BT<String,FileEntry>
|
|
|
|
|
// - Save to proper values
|
|
|
|
|
if self.use_prehash_cache {
|
|
|
|
|
loaded_hash_map = match load_hashes_from_file(&mut self.text_messages, self.delete_outdated_cache, &self.hash_type, true) {
|
|
|
|
|
Some(t) => t,
|
|
|
|
|
None => Default::default(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut loaded_hash_map2: BTreeMap<String, FileEntry> = Default::default();
|
|
|
|
|
for vec_file_entry in loaded_hash_map.values() {
|
|
|
|
|
for file_entry in vec_file_entry {
|
|
|
|
|
loaded_hash_map2.insert(file_entry.path.to_string_lossy().to_string(), file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
let mut loaded_hash_map2: BTreeMap<String, FileEntry> = Default::default();
|
|
|
|
|
for vec_file_entry in loaded_hash_map.values() {
|
|
|
|
|
for file_entry in vec_file_entry {
|
|
|
|
|
loaded_hash_map2.insert(file_entry.path.to_string_lossy().to_string(), file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::if_same_then_else)]
|
|
|
|
|
for vec_file_entry in self.files_with_identical_size.values() {
|
|
|
|
|
for file_entry in vec_file_entry {
|
|
|
|
|
let name = file_entry.path.to_string_lossy().to_string();
|
|
|
|
|
if !loaded_hash_map2.contains_key(&name) {
|
|
|
|
|
// If loaded data doesn't contains current image info
|
|
|
|
|
non_cached_files_to_check.entry(file_entry.size).or_insert_with(Vec::new).push(file_entry.clone());
|
|
|
|
|
} else if file_entry.size != loaded_hash_map2.get(&name).unwrap().size || file_entry.modified_date != loaded_hash_map2.get(&name).unwrap().modified_date {
|
|
|
|
|
// When size or modification date of image changed, then it is clear that is different image
|
|
|
|
|
non_cached_files_to_check.entry(file_entry.size).or_insert_with(Vec::new).push(file_entry.clone());
|
|
|
|
|
} else {
|
|
|
|
|
// Checking may be omitted when already there is entry with same size and modification date
|
|
|
|
|
records_already_cached.entry(file_entry.size).or_insert_with(Vec::new).push(file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
#[allow(clippy::if_same_then_else)]
|
|
|
|
|
for vec_file_entry in self.files_with_identical_size.values() {
|
|
|
|
|
for file_entry in vec_file_entry {
|
|
|
|
|
let name = file_entry.path.to_string_lossy().to_string();
|
|
|
|
|
if !loaded_hash_map2.contains_key(&name) {
|
|
|
|
|
// If loaded data doesn't contains current image info
|
|
|
|
|
non_cached_files_to_check.entry(file_entry.size).or_insert_with(Vec::new).push(file_entry.clone());
|
|
|
|
|
} else if file_entry.size != loaded_hash_map2.get(&name).unwrap().size || file_entry.modified_date != loaded_hash_map2.get(&name).unwrap().modified_date {
|
|
|
|
|
// When size or modification date of image changed, then it is clear that is different image
|
|
|
|
|
non_cached_files_to_check.entry(file_entry.size).or_insert_with(Vec::new).push(file_entry.clone());
|
|
|
|
|
} else {
|
|
|
|
|
// Checking may be omitted when already there is entry with same size and modification date
|
|
|
|
|
records_already_cached.entry(file_entry.size).or_insert_with(Vec::new).push(file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
loaded_hash_map = Default::default();
|
|
|
|
|
mem::swap(&mut self.files_with_identical_size, &mut non_cached_files_to_check);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
loaded_hash_map = Default::default();
|
|
|
|
|
mem::swap(&mut self.files_with_identical_size, &mut non_cached_files_to_check);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
|
|
|
let pre_hash_results: Vec<(u64, BTreeMap<String, Vec<FileEntry>>, Vec<String>)> = non_cached_files_to_check
|
|
|
|
|
.par_iter()
|
|
|
|
|
.map(|(size, vec_file_entry)| {
|
|
|
|
|
let mut hashmap_with_hash: BTreeMap<String, Vec<FileEntry>> = Default::default();
|
|
|
|
|
let mut errors: Vec<String> = Vec::new();
|
|
|
|
|
let mut buffer = [0u8; 1024 * 2];
|
|
|
|
|
|
|
|
|
|
atomic_file_counter.fetch_add(vec_file_entry.len(), Ordering::Relaxed);
|
|
|
|
|
for file_entry in vec_file_entry {
|
|
|
|
|
if stop_receiver.is_some() && stop_receiver.unwrap().try_recv().is_ok() {
|
|
|
|
|
check_was_stopped.store(true, Ordering::Relaxed);
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
match hash_calculation(&mut buffer, file_entry, &check_type, 0) {
|
|
|
|
|
Ok(hash_string) => {
|
|
|
|
|
hashmap_with_hash.entry(hash_string.clone()).or_insert_with(Vec::new).push(file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
Err(s) => errors.push(s),
|
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
|
|
|
let pre_hash_results: Vec<(u64, BTreeMap<String, Vec<FileEntry>>, Vec<String>)> = non_cached_files_to_check
|
|
|
|
|
.par_iter()
|
|
|
|
|
.map(|(size, vec_file_entry)| {
|
|
|
|
|
let mut hashmap_with_hash: BTreeMap<String, Vec<FileEntry>> = Default::default();
|
|
|
|
|
let mut errors: Vec<String> = Vec::new();
|
|
|
|
|
let mut buffer = [0u8; 1024 * 2];
|
|
|
|
|
|
|
|
|
|
atomic_file_counter.fetch_add(vec_file_entry.len(), Ordering::Relaxed);
|
|
|
|
|
for file_entry in vec_file_entry {
|
|
|
|
|
if stop_receiver.is_some() && stop_receiver.unwrap().try_recv().is_ok() {
|
|
|
|
|
check_was_stopped.store(true, Ordering::Relaxed);
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
match hash_calculation(&mut buffer, file_entry, &check_type, 0) {
|
|
|
|
|
Ok(hash_string) => {
|
|
|
|
|
hashmap_with_hash.entry(hash_string.clone()).or_insert_with(Vec::new).push(file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
Err(s) => errors.push(s),
|
|
|
|
|
}
|
|
|
|
|
Some((*size, hashmap_with_hash, errors))
|
|
|
|
|
})
|
|
|
|
|
.while_some()
|
|
|
|
|
.collect();
|
|
|
|
|
}
|
|
|
|
|
Some((*size, hashmap_with_hash, errors))
|
|
|
|
|
})
|
|
|
|
|
.while_some()
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
// End thread which send info to gui
|
|
|
|
|
progress_thread_run.store(false, Ordering::Relaxed);
|
|
|
|
|
progress_thread_handle.join().unwrap();
|
|
|
|
|
// End thread which send info to gui
|
|
|
|
|
progress_thread_run.store(false, Ordering::Relaxed);
|
|
|
|
|
progress_thread_handle.join().unwrap();
|
|
|
|
|
|
|
|
|
|
// Check if user aborted search(only from GUI)
|
|
|
|
|
if check_was_stopped.load(Ordering::Relaxed) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
// Check if user aborted search(only from GUI)
|
|
|
|
|
if check_was_stopped.load(Ordering::Relaxed) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add data from cache
|
|
|
|
|
for (size, vec_file_entry) in &records_already_cached {
|
|
|
|
|
pre_checked_map.entry(*size).or_insert_with(Vec::new).append(&mut vec_file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
// Add data from cache
|
|
|
|
|
for (size, vec_file_entry) in &records_already_cached {
|
|
|
|
|
pre_checked_map.entry(*size).or_insert_with(Vec::new).append(&mut vec_file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check results
|
|
|
|
|
for (size, hash_map, errors) in &pre_hash_results {
|
|
|
|
|
self.text_messages.warnings.append(&mut errors.clone());
|
|
|
|
|
for vec_file_entry in hash_map.values() {
|
|
|
|
|
if vec_file_entry.len() > 1 {
|
|
|
|
|
pre_checked_map.entry(*size).or_insert_with(Vec::new).append(&mut vec_file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
// Check results
|
|
|
|
|
for (size, hash_map, errors) in &pre_hash_results {
|
|
|
|
|
self.text_messages.warnings.append(&mut errors.clone());
|
|
|
|
|
for vec_file_entry in hash_map.values() {
|
|
|
|
|
if vec_file_entry.len() > 1 {
|
|
|
|
|
pre_checked_map.entry(*size).or_insert_with(Vec::new).append(&mut vec_file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.use_prehash_cache {
|
|
|
|
|
// All results = records already cached + computed results
|
|
|
|
|
let mut save_cache_to_hashmap: BTreeMap<String, FileEntry> = Default::default();
|
|
|
|
|
if self.use_prehash_cache {
|
|
|
|
|
// All results = records already cached + computed results
|
|
|
|
|
let mut save_cache_to_hashmap: BTreeMap<String, FileEntry> = Default::default();
|
|
|
|
|
|
|
|
|
|
for (size, vec_file_entry) in loaded_hash_map {
|
|
|
|
|
if size >= self.minimal_prehash_cache_file_size {
|
|
|
|
|
for file_entry in vec_file_entry {
|
|
|
|
|
save_cache_to_hashmap.insert(file_entry.path.to_string_lossy().to_string(), file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
for (size, vec_file_entry) in loaded_hash_map {
|
|
|
|
|
if size >= self.minimal_prehash_cache_file_size {
|
|
|
|
|
for file_entry in vec_file_entry {
|
|
|
|
|
save_cache_to_hashmap.insert(file_entry.path.to_string_lossy().to_string(), file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (size, hash_map, _errors) in &pre_hash_results {
|
|
|
|
|
if *size >= self.minimal_prehash_cache_file_size {
|
|
|
|
|
for vec_file_entry in hash_map.values() {
|
|
|
|
|
for file_entry in vec_file_entry {
|
|
|
|
|
save_cache_to_hashmap.insert(file_entry.path.to_string_lossy().to_string(), file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
for (size, hash_map, _errors) in &pre_hash_results {
|
|
|
|
|
if *size >= self.minimal_prehash_cache_file_size {
|
|
|
|
|
for vec_file_entry in hash_map.values() {
|
|
|
|
|
for file_entry in vec_file_entry {
|
|
|
|
|
save_cache_to_hashmap.insert(file_entry.path.to_string_lossy().to_string(), file_entry.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
save_hashes_to_file(&save_cache_to_hashmap, &mut self.text_messages, &self.hash_type, true, self.minimal_prehash_cache_file_size);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////// PREHASHING END
|
|
|
|
|
save_hashes_to_file(&save_cache_to_hashmap, &mut self.text_messages, &self.hash_type, true, self.minimal_prehash_cache_file_size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Common::print_time(start_time, SystemTime::now(), "check_files_hash - prehash");
|
|
|
|
|
let start_time: SystemTime = SystemTime::now();
|
|
|
|
|
|
|
|
|
|
/////////////////////////
|
|
|
|
|
Some(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn full_hashing(
|
|
|
|
|
&mut self,
|
|
|
|
|
stop_receiver: Option<&Receiver<()>>,
|
|
|
|
|
progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>,
|
|
|
|
|
mut pre_checked_map: BTreeMap<u64, Vec<FileEntry>>,
|
|
|
|
|
) -> Option<()> {
|
|
|
|
|
let check_was_stopped = AtomicBool::new(false); // Used for breaking from GUI and ending check thread
|
|
|
|
|
|
|
|
|
|
let check_type = self.hash_type;
|
|
|
|
|
let start_time: SystemTime = SystemTime::now();
|
|
|
|
|
//// PROGRESS THREAD START
|
|
|
|
|
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 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 = pre_checked_map.values().map(Vec::len).sum();
|
|
|
|
|
let checking_method = self.check_method;
|
|
|
|
|
thread::spawn(move || loop {
|
|
|
|
|
progress_send
|
|
|
|
|
.unbounded_send(ProgressData {
|
|
|
|
|
checking_method,
|
|
|
|
|
current_stage: 2,
|
|
|
|
|
max_stage: 2,
|
|
|
|
|
entries_checked: atomic_file_counter.load(Ordering::Relaxed),
|
|
|
|
|
entries_to_check: files_to_check,
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
if !progress_thread_run.load(Ordering::Relaxed) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
sleep(Duration::from_millis(LOOP_DURATION as u64));
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
thread::spawn(|| {})
|
|
|
|
|
};
|
|
|
|
|
let progress_thread_handle = self.prepare_hash_thread_handler(
|
|
|
|
|
progress_sender,
|
|
|
|
|
progress_thread_run.clone(),
|
|
|
|
|
atomic_file_counter.clone(),
|
|
|
|
|
2,
|
|
|
|
|
2,
|
|
|
|
|
pre_checked_map.values().map(Vec::len).sum(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
//// PROGRESS THREAD END
|
|
|
|
|
|
|
|
|
@ -828,7 +961,7 @@ impl DuplicateFinder {
|
|
|
|
|
|
|
|
|
|
// Break if stop was clicked after saving to cache
|
|
|
|
|
if check_was_stopped.load(Ordering::Relaxed) {
|
|
|
|
|
return false;
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (size, hash_map, mut errors) in full_hash_results {
|
|
|
|
@ -840,9 +973,11 @@ impl DuplicateFinder {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Common::print_time(start_time, SystemTime::now(), "delete_files");
|
|
|
|
|
Some(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////// HASHING END
|
|
|
|
|
|
|
|
|
|
fn hash_reference_folders(&mut self) {
|
|
|
|
|
// Reference - only use in size, because later hash will be counted differently
|
|
|
|
|
if self.use_reference_folders {
|
|
|
|
|
let mut btree_map = Default::default();
|
|
|
|
@ -897,8 +1032,24 @@ impl DuplicateFinder {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Common::print_time(start_time, SystemTime::now(), "check_files_hash - full hash");
|
|
|
|
|
/// The slowest checking type, which must be applied after checking for size
|
|
|
|
|
fn check_files_hash(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool {
|
|
|
|
|
assert_eq!(self.check_method, CheckingMethod::Hash);
|
|
|
|
|
|
|
|
|
|
let mut pre_checked_map: BTreeMap<u64, Vec<FileEntry>> = Default::default();
|
|
|
|
|
let ret = self.prehashing(stop_receiver, progress_sender, &mut pre_checked_map);
|
|
|
|
|
if ret.is_none() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ret = self.full_hashing(stop_receiver, progress_sender, pre_checked_map);
|
|
|
|
|
if ret.is_none() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.hash_reference_folders();
|
|
|
|
|
|
|
|
|
|
// Clean unused data
|
|
|
|
|
self.files_with_identical_size = Default::default();
|
|
|
|
@ -920,6 +1071,11 @@ impl DuplicateFinder {
|
|
|
|
|
let _tuple: (u64, usize, usize) = delete_files(vector, &self.delete_method, &mut self.text_messages, self.dryrun);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CheckingMethod::SizeName => {
|
|
|
|
|
for vector in self.files_with_identical_size_names.values() {
|
|
|
|
|
let _tuple: (u64, usize, usize) = delete_files(vector, &self.delete_method, &mut self.text_messages, self.dryrun);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CheckingMethod::Hash => {
|
|
|
|
|
for vector_vectors in self.files_with_identical_hashes.values() {
|
|
|
|
|
for vector in vector_vectors.iter() {
|
|
|
|
@ -1053,6 +1209,30 @@ impl SaveResults for DuplicateFinder {
|
|
|
|
|
write!(writer, "Not found any files with same names.").unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CheckingMethod::SizeName => {
|
|
|
|
|
if !self.files_with_identical_names.is_empty() {
|
|
|
|
|
writeln!(
|
|
|
|
|
writer,
|
|
|
|
|
"-------------------------------------------------Files with same size and names-------------------------------------------------"
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
writeln!(
|
|
|
|
|
writer,
|
|
|
|
|
"Found {} files in {} groups with same size and name(may have different content)",
|
|
|
|
|
self.information.number_of_duplicated_files_by_size_name, self.information.number_of_groups_by_size_name,
|
|
|
|
|
)
|
|
|
|
|
.unwrap();
|
|
|
|
|
for ((size, name), vector) in self.files_with_identical_size_names.iter().rev() {
|
|
|
|
|
writeln!(writer, "Name - {}, {} - {} files ", name, format_size(*size, BINARY), vector.len()).unwrap();
|
|
|
|
|
for j in vector {
|
|
|
|
|
writeln!(writer, "{}", j.path.display()).unwrap();
|
|
|
|
|
}
|
|
|
|
|
writeln!(writer).unwrap();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
write!(writer, "Not found any files with same size and names.").unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CheckingMethod::Size => {
|
|
|
|
|
if !self.files_with_identical_size.is_empty() {
|
|
|
|
|
writeln!(
|
|
|
|
@ -1137,6 +1317,20 @@ impl PrintResults for DuplicateFinder {
|
|
|
|
|
println!();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CheckingMethod::SizeName => {
|
|
|
|
|
for i in &self.files_with_identical_size_names {
|
|
|
|
|
number_of_files += i.1.len() as u64;
|
|
|
|
|
number_of_groups += 1;
|
|
|
|
|
}
|
|
|
|
|
println!("Found {number_of_files} files in {number_of_groups} groups with same size and name(may have different content)",);
|
|
|
|
|
for ((size, name), vector) in &self.files_with_identical_size_names {
|
|
|
|
|
println!("Name - {}, {} - {} files ", name, format_size(*size, BINARY), vector.len());
|
|
|
|
|
for j in vector {
|
|
|
|
|
println!("{}", j.path.display());
|
|
|
|
|
}
|
|
|
|
|
println!();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CheckingMethod::Hash => {
|
|
|
|
|
for vector in self.files_with_identical_hashes.values() {
|
|
|
|
|
for j in vector {
|
|
|
|
|