use crate::cleanup::database_updater::DatabaseUpdater; use crate::cleanup::file_type_detector::{detect_file_type, should_rename}; use crate::cleanup::types::{CleanupConfig, CleanupStats}; use anyhow::Result; use log::{error, warn}; use std::fs; use std::path::{Path, PathBuf}; use walkdir::WalkDir; /// Phase 2: Validate file types and rename mismatches pub fn validate_file_types( config: &CleanupConfig, db_updater: &mut DatabaseUpdater, ) -> Result { let mut stats = CleanupStats::new(); let mut auto_fix_all = config.auto_fix; let mut skip_all = false; println!("\nPhase 2: File Type Validation"); println!("------------------------------"); // Walk the filesystem println!("Scanning filesystem..."); let files: Vec = WalkDir::new(&config.base_path) .into_iter() .filter_map(|e| e.ok()) .filter(|e| e.file_type().is_file()) .filter(|e| is_supported_media_file(e.path())) .map(|e| e.path().to_path_buf()) .collect(); println!("Files found: {}\n", files.len()); stats.files_checked = files.len(); println!("Detecting file types..."); let mut mismatches_found = 0; let mut files_renamed = 0; let mut user_skipped = 0; for file_path in files { // Get current extension let current_ext = match file_path.extension() { Some(ext) => ext.to_str().unwrap_or(""), None => continue, // Skip files without extensions }; // Detect actual file type match detect_file_type(&file_path) { Ok(Some(detected_ext)) => { // Check if we should rename if should_rename(current_ext, &detected_ext) { mismatches_found += 1; stats.issues_found += 1; // Get relative path for display and database let relative_path = match file_path.strip_prefix(&config.base_path) { Ok(rel) => rel.to_str().unwrap_or(""), Err(_) => { error!("Failed to get relative path for {:?}", file_path); continue; } }; println!("\nFile type mismatch:"); println!(" Path: {}", relative_path); println!(" Current: .{}", current_ext); println!(" Actual: .{}", detected_ext); // Calculate new path let new_file_path = file_path.with_extension(&detected_ext); let new_relative_path = match new_file_path.strip_prefix(&config.base_path) { Ok(rel) => rel.to_str().unwrap_or(""), Err(_) => { error!("Failed to get new relative path for {:?}", new_file_path); continue; } }; // Check if destination already exists if new_file_path.exists() { warn!("✗ Destination already exists: {}", new_relative_path); stats.add_error(format!( "Destination exists for {}: {}", relative_path, new_relative_path )); continue; } // Determine if we should proceed let should_proceed = if config.dry_run { println!(" (dry-run mode - would rename to {})", new_relative_path); false } else if skip_all { println!(" Skipped (skip all)"); user_skipped += 1; false } else if auto_fix_all { true } else { // Interactive prompt match prompt_for_rename(&new_relative_path) { RenameDecision::Yes => true, RenameDecision::No => { user_skipped += 1; false } RenameDecision::All => { auto_fix_all = true; true } RenameDecision::SkipAll => { skip_all = true; user_skipped += 1; false } } }; if should_proceed { // Rename the file match fs::rename(&file_path, &new_file_path) { Ok(_) => { println!("✓ Renamed file"); // Update database match db_updater.update_file_path(relative_path, new_relative_path) { Ok(_) => { files_renamed += 1; stats.issues_fixed += 1; } Err(e) => { error!( "File renamed but DB update failed for {}: {:?}", relative_path, e ); stats.add_error(format!( "DB update failed for {}: {}", relative_path, e )); } } } Err(e) => { error!("✗ Failed to rename file: {:?}", e); stats.add_error(format!( "Rename failed for {}: {}", relative_path, e )); } } } } } Ok(None) => { // Could not detect file type - skip // This is normal for some RAW formats or corrupted files } Err(e) => { warn!("Failed to detect type for {:?}: {:?}", file_path, e); } } } println!("\nResults:"); println!("- Files scanned: {}", stats.files_checked); println!("- Mismatches found: {}", mismatches_found); if config.dry_run { println!("- Would rename: {}", mismatches_found); } else { println!("- Files renamed: {}", files_renamed); if user_skipped > 0 { println!("- User skipped: {}", user_skipped); } } if !stats.errors.is_empty() { println!("- Errors: {}", stats.errors.len()); } Ok(stats) } /// Check if a file is a supported media file based on extension fn is_supported_media_file(path: &Path) -> bool { if let Some(ext) = path.extension() { if let Some(ext_str) = ext.to_str() { let ext_lower = ext_str.to_lowercase(); return matches!( ext_lower.as_str(), "jpg" | "jpeg" | "png" | "webp" | "tiff" | "tif" | "heif" | "heic" | "avif" | "nef" | "mp4" | "mov" ); } } false } #[derive(Debug)] enum RenameDecision { Yes, No, All, SkipAll, } /// Prompt the user for rename decision fn prompt_for_rename(new_path: &str) -> RenameDecision { println!("\nRename to {}?", new_path); println!(" [y] Yes"); println!(" [n] No (default)"); println!(" [a] Yes to all"); println!(" [s] Skip all remaining"); print!("Choice: "); // Force flush stdout use std::io::{self, Write}; let _ = io::stdout().flush(); let mut input = String::new(); match io::stdin().read_line(&mut input) { Ok(_) => { let choice = input.trim().to_lowercase(); match choice.as_str() { "y" | "yes" => RenameDecision::Yes, "a" | "all" => RenameDecision::All, "s" | "skip" => RenameDecision::SkipAll, _ => RenameDecision::No, } } Err(_) => RenameDecision::No, } } #[cfg(test)] mod tests { use super::*; #[test] fn test_is_supported_media_file() { assert!(is_supported_media_file(Path::new("test.jpg"))); assert!(is_supported_media_file(Path::new("test.JPG"))); assert!(is_supported_media_file(Path::new("test.png"))); assert!(is_supported_media_file(Path::new("test.webp"))); assert!(is_supported_media_file(Path::new("test.mp4"))); assert!(is_supported_media_file(Path::new("test.mov"))); assert!(!is_supported_media_file(Path::new("test.txt"))); assert!(!is_supported_media_file(Path::new("test"))); } }