use crate::types::{FileInfo, HashAlgorithm};
use crate::utils::{compute_file_hash, delete_duplicate_file, merge_duplicate_file};
use colored::Colorize;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
pub fn find_duplicates(
dir: &Path,
color: bool,
content_dups: bool,
hash_algorithm: HashAlgorithm,
) -> Vec<DuplicateGroup> {
let mut size_map: HashMap<u64, Vec<String>> = HashMap::new();
let mut duplicates: Vec<DuplicateGroup> = Vec::new();
fn scan_for_duplicates(path: &Path, size_map: &mut HashMap<u64, Vec<String>>) {
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
let entry_path = entry.path();
let file_name = entry_path.file_name().unwrap_or_default().to_string_lossy();
if file_name == ".kilo" && entry_path.is_dir() {
continue;
}
if entry_path.is_file() {
if let Ok(metadata) = entry.metadata() {
let size = metadata.len();
size_map
.entry(size)
.or_insert_with(Vec::new)
.push(entry_path.to_string_lossy().to_string());
}
} else if entry_path.is_dir() {
scan_for_duplicates(&entry_path, size_map);
}
}
}
}
scan_for_duplicates(dir, &mut size_map);
for (size, paths) in size_map.iter() {
if paths.len() > 1 {
if content_dups {
let mut hash_map: HashMap<String, Vec<String>> = HashMap::new();
for path_str in paths {
if let Some(hash) = compute_file_hash(Path::new(path_str), hash_algorithm) {
hash_map
.entry(hash)
.or_insert_with(Vec::new)
.push(path_str.clone());
}
}
for (hash, dup_paths) in hash_map.iter() {
if dup_paths.len() > 1 {
duplicates.push(DuplicateGroup {
size: *size,
hash: Some(hash.clone()),
paths: dup_paths.clone(),
});
}
}
} else {
duplicates.push(DuplicateGroup {
size: *size,
hash: None,
paths: paths.clone(),
});
}
}
}
print_duplicate_groups(&duplicates, color, hash_algorithm);
duplicates
}
fn print_duplicate_groups(
groups: &[DuplicateGroup],
color: bool,
hash_algorithm: HashAlgorithm,
) {
if groups.is_empty() {
println!("No duplicate files found.");
} else {
println!("Duplicate files found:");
println!("{}", "─".repeat(50));
for group in groups {
if color {
let size_str = crate::types::SizeUnit::auto_format_size(group.size).cyan();
if let Some(hash) = &group.hash {
let hash_display = hash.chars().take(16).collect::<String>();
println!(
"Size: {} | {}: {}... ({} files)",
size_str,
hash_algorithm.display_name().yellow(),
hash_display,
group.paths.len().to_string().yellow()
);
} else {
println!(
"Size: {} ({} files)",
size_str,
group.paths.len().to_string().yellow()
);
}
} else {
let size_str = crate::types::SizeUnit::auto_format_size(group.size);
if let Some(hash) = &group.hash {
let hash_display = hash.chars().take(16).collect::<String>();
println!(
"Size: {} | {}: {}... ({} files)",
size_str,
hash_algorithm.display_name(),
hash_display,
group.paths.len()
);
} else {
println!("Size: {} ({} files)", size_str, group.paths.len());
}
}
for path in &group.paths {
println!(" {}", path);
}
println!();
}
}
}
pub fn apply_duplicate_action(
groups: &[DuplicateGroup],
action: crate::types::DuplicateAction,
force: bool,
) {
if groups.is_empty() {
return;
}
match action {
crate::types::DuplicateAction::Delete => {
for group in groups {
if group.paths.len() <= 1 {
continue;
}
for path in &group.paths[1..] {
let p = Path::new(path);
if p.exists() {
if delete_duplicate_file(p, force) {
if force {
println!("Deleted: {}", path);
}
} else if !force {
println!("Skipped: {}", path);
}
}
}
}
}
crate::types::DuplicateAction::Merge => {
for group in groups {
if group.paths.len() <= 1 {
continue;
}
let target = Path::new(&group.paths[0]);
for path in &group.paths[1..] {
let p = Path::new(path);
if p.exists() {
if merge_duplicate_file(p, target) {
println!("Merged: {} -> {}", path, group.paths[0]);
} else {
eprintln!("Failed to merge: {}", path);
}
}
}
}
}
crate::types::DuplicateAction::None => {}
}
}
pub fn show_detailed_analysis(files: &[FileInfo], color: bool) {
let total_files = files.len();
let total_dirs = files.iter().filter(|f| f.is_directory).count();
let total_regular_files = total_files - total_dirs;
let _total_size: u64 = files.iter().map(|f| f.size).sum();
println!("");
println!("Detailed Analysis:");
println!("{}", "-".repeat(50));
if color {
println!(
"Total Items: {} ({})",
total_files.to_string().cyan(),
format!("{} files, {} dirs", total_regular_files, total_dirs).yellow()
);
} else {
println!(
"Total Items: {} ({} files, {} dirs)",
total_files, total_regular_files, total_dirs
);
}
let size_ranges = [
("Empty (0 B)", 0..1),
("Tiny (< 1 KB)", 1..1024),
("Small (1 KB - 1 MB)", 1024..1024 * 1024),
("Medium (1 MB - 100 MB)", 1024 * 1024..100 * 1024 * 1024),
("Large (100 MB - 1 GB)", 100 * 1024 * 1024..1024 * 1024 * 1024),
("Huge (> 1 GB)", 1024 * 1024 * 1024..u64::MAX),
];
println!("\nSize Distribution:");
for (label, range) in &size_ranges {
let count = files.iter().filter(|f| range.contains(&f.size)).count();
if count > 0 {
let percentage = count as f64 / total_files as f64 * 100.0;
if color {
println!(
" {}: {} files ({:.1}%)",
label.magenta(),
count.to_string().cyan(),
percentage
);
} else {
println!(" {}: {} files ({:.1}%)", label, count, percentage);
}
}
}
let now = std::time::SystemTime::now();
let age_ranges = [
("Today", 0..86400),
("This Week", 86400..604800),
("This Month", 604800..2592000),
("This Year", 2592000..31536000),
("Older", 31536000..u64::MAX),
];
println!("\nFile Age Distribution:");
for (label, range) in &age_ranges {
let count = files
.iter()
.filter(|f| {
if let Some(modified_str) = &f.modified {
if let Ok(modified_time) =
chrono::DateTime::parse_from_rfc3339(&format!("{}Z", modified_str.replace(" UTC", "")))
{
let duration = now
.duration_since(modified_time.with_timezone(&chrono::Utc).into())
.unwrap_or_default();
range.contains(&duration.as_secs())
} else {
false
}
} else {
false
}
})
.count();
if count > 0 {
let percentage = count as f64 / total_files as f64 * 100.0;
if color {
println!(
" {}: {} files ({:.1}%)",
label.magenta(),
count.to_string().cyan(),
percentage
);
} else {
println!(" {}: {} files ({:.1}%)", label, count, percentage);
}
}
}
if let Some(largest) = files.iter().filter(|f| !f.is_directory).max_by_key(|f| f.size) {
if color {
println!(
"\nLargest File: {} ({})",
largest.name.cyan(),
largest.size_human.green()
);
} else {
println!("\nLargest File: {} ({})", largest.name, largest.size_human);
}
}
if let Some(smallest) = files.iter().filter(|f| !f.is_directory && f.size > 0).min_by_key(|f| f.size) {
if color {
println!(
"Smallest File: {} ({})",
smallest.name.cyan(),
smallest.size_human.green()
);
} else {
println!("Smallest File: {} ({})", smallest.name, smallest.size_human);
}
}
let readable = files.iter().filter(|f| f.permissions.contains('r')).count();
let writable = files.iter().filter(|f| f.permissions.contains('w')).count();
let readable_only = files.iter().filter(|f| f.permissions == "r").count();
let writable_only = files.iter().filter(|f| f.permissions == "rw").count();
println!("\nPermissions Summary:");
if color {
println!(
" Readable: {} files ({:.1}%)",
readable.to_string().cyan(),
readable as f64 / total_files as f64 * 100.0
);
println!(
" Writable: {} files ({:.1}%)",
writable.to_string().cyan(),
writable as f64 / total_files as f64 * 100.0
);
println!(
" Read-only: {} files ({:.1}%)",
readable_only.to_string().cyan(),
readable_only as f64 / total_files as f64 * 100.0
);
println!(
" Read-write: {} files ({:.1}%)",
writable_only.to_string().cyan(),
writable_only as f64 / total_files as f64 * 100.0
);
} else {
println!(
" Readable: {} files ({:.1}%)",
readable,
readable as f64 / total_files as f64 * 100.0
);
println!(
" Writable: {} files ({:.1}%)",
writable,
writable as f64 / total_files as f64 * 100.0
);
println!(
" Read-only: {} files ({:.1}%)",
readable_only,
readable_only as f64 / total_files as f64 * 100.0
);
println!(
" Read-write: {} files ({:.1}%)",
writable_only,
writable_only as f64 / total_files as f64 * 100.0
);
}
}
pub(crate) struct DuplicateGroup {
size: u64,
hash: Option<String>,
paths: Vec<String>,
}