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>
77 lines
2.7 KiB
Rust
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(())
|
|
}
|