//! Pre-cutover audit utility for the filename date class (ticket #117). //! //! One-shot, operator-run: lists every file matching a filename-class //! pattern across the configured libraries — the file's current stored //! date + source and the date / source / offset-source the re-based //! resolver would assign it. Strictly read-only: it changes nothing. //! //! This is the human spot-check that gates the production backfill //! drain: review the report, fix any mis-tagged pattern or //! misclassified file, then start the drain. //! //! Usage: `cargo run --bin filename_audit` (reads DATABASE_URL and //! EXCLUDED_DIRS from the environment, like the server). use std::path::PathBuf; use diesel::Connection; use diesel::sqlite::SqliteConnection; use image_api::filename_audit::{format_report, run_audit}; use image_api::libraries::load_all; fn main() -> anyhow::Result<()> { env_logger::init(); dotenv::dotenv().ok(); let database_url = dotenv::var("DATABASE_URL") .map_err(|_| anyhow::anyhow!("DATABASE_URL is not set (check .env)"))?; let base_path = dotenv::var("BASE_PATH").ok(); let mut conn = SqliteConnection::establish(&database_url).map_err(|e| anyhow::anyhow!(e.to_string()))?; // Migrations are already run by the server; the audit only SELECTs. // A fresh database without the date-class columns would fail the // select below with a clear SQL error — that is the expected signal. let global_excludes: Vec = dotenv::var("EXCLUDED_DIRS") .unwrap_or_default() .split(',') .map(str::trim) .filter(|s| !s.is_empty()) .map(String::from) .collect(); let libraries = load_all(&mut conn); if libraries.is_empty() { anyhow::bail!( "no rows in the `libraries` table — is DATABASE_URL pointing at the live database?" ); } // BASE_PATH is informational for the operator (the libraries table // carries the authoritative roots); warn when it differs from every // known root so a stale env var doesn't mislead the review. if let Some(base) = &base_path && let base = PathBuf::from(base) && !libraries .iter() .any(|l| std::path::Path::new(&l.root_path) == base) { eprintln!( "note: BASE_PATH ({}) does not match any library root; auditing the library table", base.display() ); } println!("Filename date class audit (read-only)"); println!("======================================"); println!("Libraries: {}", libraries.len()); println!(); let (rows, skipped) = run_audit(&mut conn, libraries, &global_excludes); print!("{}", format_report(&rows, &skipped)); Ok(()) }