feat(bins): multi-library populate_knowledge + progress UX
populate_knowledge now loads real libraries from the DB instead of fabricating a single library_id=1 row from BASE_PATH. Adds --library <id|name> to restrict the walk and validates --path against the selected library roots. The full library set is still passed to InsightGenerator so resolve_full_path can probe every root when an insight resolves to a different library than the one being walked. Adds indicatif progress bars across the long-running utility binaries via a shared src/bin_progress.rs helper (determinate bar + open-ended spinner with consistent styling). Per-batch info! noise is replaced by the bar's throughput/ETA; warnings and errors route through pb.println so they scroll above the bar instead of fighting with it. populate_knowledge spinner during scan, determinate bar over all libs backfill_hashes spinner with running hashed/missing/errors counts import_calendar determinate bar; embedding/store failures inline import_location_* determinate bar advancing by chunk size import_search_* determinate bar; pb cloned into the spawn task cleanup_files P1 determinate bar over DB paths cleanup_files P2 determinate bar; pb.suspend() around y/n/a/s prompt Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
use crate::bin_progress;
|
||||
use crate::cleanup::database_updater::DatabaseUpdater;
|
||||
use crate::cleanup::types::{CleanupConfig, CleanupStats};
|
||||
use crate::file_types::IMAGE_EXTENSIONS;
|
||||
use anyhow::Result;
|
||||
use log::{error, warn};
|
||||
use log::error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// All supported image extensions to try
|
||||
@@ -25,15 +26,17 @@ pub fn resolve_missing_files(
|
||||
|
||||
stats.files_checked = all_paths.len();
|
||||
|
||||
println!("Checking file existence...");
|
||||
let mut missing_count = 0;
|
||||
let mut resolved_count = 0;
|
||||
|
||||
let pb = bin_progress::determinate(stats.files_checked as u64, "checking");
|
||||
|
||||
for path_str in all_paths {
|
||||
let full_path = config.base_path.join(&path_str);
|
||||
|
||||
// Check if file exists
|
||||
if full_path.exists() {
|
||||
pb.inc(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -43,16 +46,16 @@ pub fn resolve_missing_files(
|
||||
// Try to find the file with different extensions
|
||||
match find_file_with_alternative_extension(&config.base_path, &path_str) {
|
||||
Some(new_path_str) => {
|
||||
println!(
|
||||
"✓ {} → found as {} {}",
|
||||
pb.println(format!(
|
||||
"✓ {} → found as {}{}",
|
||||
path_str,
|
||||
new_path_str,
|
||||
if config.dry_run {
|
||||
"(dry-run, not updated)"
|
||||
" (dry-run, not updated)"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
));
|
||||
|
||||
if !config.dry_run {
|
||||
// Update database
|
||||
@@ -71,11 +74,18 @@ pub fn resolve_missing_files(
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("✗ {} → not found with any extension", path_str);
|
||||
pb.println(format!("✗ {} — not found with any extension", path_str));
|
||||
}
|
||||
}
|
||||
pb.set_message(format!(
|
||||
"missing={} resolved={}",
|
||||
missing_count, resolved_count
|
||||
));
|
||||
pb.inc(1);
|
||||
}
|
||||
|
||||
pb.finish_and_clear();
|
||||
|
||||
println!("\nResults:");
|
||||
println!("- Files checked: {}", stats.files_checked);
|
||||
println!("- Missing files: {}", missing_count);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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};
|
||||
@@ -32,16 +34,20 @@ pub fn validate_file_types(
|
||||
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;
|
||||
|
||||
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 => continue, // Skip files without extensions
|
||||
None => {
|
||||
pb.inc(1);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Detect actual file type
|
||||
@@ -57,14 +63,15 @@ pub fn validate_file_types(
|
||||
Ok(rel) => rel.to_str().unwrap_or(""),
|
||||
Err(_) => {
|
||||
error!("Failed to get relative path for {:?}", file_path);
|
||||
pb.inc(1);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
println!("\nFile type mismatch:");
|
||||
println!(" Path: {}", relative_path);
|
||||
println!(" Current: .{}", current_ext);
|
||||
println!(" Actual: .{}", detected_ext);
|
||||
pb.println(format!(
|
||||
"mismatch: {} .{} → .{}",
|
||||
relative_path, current_ext, detected_ext
|
||||
));
|
||||
|
||||
// Calculate new path
|
||||
let new_file_path = file_path.with_extension(&detected_ext);
|
||||
@@ -72,6 +79,7 @@ pub fn validate_file_types(
|
||||
Ok(rel) => rel.to_str().unwrap_or(""),
|
||||
Err(_) => {
|
||||
error!("Failed to get new relative path for {:?}", new_file_path);
|
||||
pb.inc(1);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -83,22 +91,26 @@ pub fn validate_file_types(
|
||||
"Destination exists for {}: {}",
|
||||
relative_path, new_relative_path
|
||||
));
|
||||
pb.inc(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine if we should proceed
|
||||
let should_proceed = if config.dry_run {
|
||||
println!(" (dry-run mode - would rename to {})", new_relative_path);
|
||||
pb.println(format!(
|
||||
" (dry-run — 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) {
|
||||
// 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;
|
||||
@@ -120,8 +132,6 @@ pub fn validate_file_types(
|
||||
// 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)
|
||||
{
|
||||
@@ -160,8 +170,15 @@ pub fn validate_file_types(
|
||||
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);
|
||||
@@ -195,8 +212,9 @@ enum RenameDecision {
|
||||
SkipAll,
|
||||
}
|
||||
|
||||
/// Prompt the user for rename decision
|
||||
fn prompt_for_rename(new_path: &str) -> RenameDecision {
|
||||
/// 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)");
|
||||
|
||||
Reference in New Issue
Block a user