Add Cleanup binary for fixing broken DB/file relations
This commit is contained in:
261
src/cleanup/phase2.rs
Normal file
261
src/cleanup/phase2.rs
Normal file
@@ -0,0 +1,261 @@
|
||||
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::{Context, Result};
|
||||
use dialoguer::Confirm;
|
||||
use log::{error, info, 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")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user