261 lines
9.3 KiB
Rust
261 lines
9.3 KiB
Rust
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<CleanupStats> {
|
|
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<PathBuf> = 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")));
|
|
}
|
|
}
|