use crate::bin_progress; 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 indicatif::ProgressBar; 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(); let mut mismatches_found = 0; let mut files_renamed = 0; let mut user_skipped = 0; let pb = bin_progress::determinate(files.len() as u64, "detecting"); for file_path in files { // Get current extension let current_ext = match file_path.extension() { Some(ext) => ext.to_str().unwrap_or(""), None => { pb.inc(1); continue; } }; // 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); pb.inc(1); continue; } }; pb.println(format!( "mismatch: {} .{} → .{}", relative_path, current_ext, 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); pb.inc(1); 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 )); pb.inc(1); continue; } // Determine if we should proceed let should_proceed = if config.dry_run { pb.println(format!( " (dry-run — would rename to {})", new_relative_path )); false } else if skip_all { user_skipped += 1; false } else if auto_fix_all { true } else { // Interactive prompt — suspend the bar so the prompt is visible. let decision = pb.suspend(|| prompt_for_rename(new_relative_path, &pb)); match decision { 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(_) => { // 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); } } pb.set_message(format!( "mismatches={} renamed={} skipped={}", mismatches_found, files_renamed, user_skipped )); pb.inc(1); } pb.finish_and_clear(); 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 { use crate::file_types::is_media_file; is_media_file(path) } #[derive(Debug)] enum RenameDecision { Yes, No, All, SkipAll, } /// Prompt the user for rename decision. Caller must `pb.suspend` so the /// progress bar isn't redrawing over the prompt. fn prompt_for_rename(new_path: &str, _pb: &ProgressBar) -> 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"))); } }