Files
ImageApi/src/bin/filename_audit.rs
T
Cameron CordesandClaude Opus 5.5 8a5a7c19d7 test(filename_audit): seed library 2 before its image_exif row (#151)
run_audit_skips_disabled_and_offline_libraries inserted an image_exif
row for library 2, which migrations never create, tripping the
image_exif.library_id foreign key. Insert the libraries row first so
the suite is green on master again.

Also fix the cmp_owned clippy warning in the filename_audit bin.

Closes #151

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:31:53 -04:00

77 lines
2.7 KiB
Rust

//! 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<String> = 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(())
}