1 Commits
Author SHA1 Message Date
Cameron CordesandClaude Opus 5.5 2e207c5fa0 refactor: compile the crate once — main.rs uses the library
main.rs re-declared nearly every module (`mod ai; mod database; …`)
that lib.rs also declared, so the ~100k-line crate compiled twice in
every build (lib for the src/bin tools, bin for the server) and the
test suite ran most tests twice. watcher.rs even reached into the lib
copy via `image_api::` from inside the bin copy.

The six bin-only modules (backfill, handlers, hls_stats, knowledge,
reels, watcher) move into lib.rs; main.rs keeps only startup wiring
over `use image_api::…`. A few items main calls become `pub`, and the
dead_code allowances that only existed because each copy saw half the
callers are gone. CLAUDE.md says not to reintroduce `mod` in main.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 01:13:15 -04:00
9 changed files with 27 additions and 82 deletions

No files matched your search

+1 -1
View File
@@ -84,7 +84,7 @@ cargo run --bin create_user -- <username> <password>
### Core Components
**Layered Architecture:**
- **Startup wiring** (`main.rs`): only ~350 lines — env load, migrations, AppState, route registration, server bind. Background jobs are kicked off here but defined elsewhere.
- **Startup wiring** (`main.rs`): only ~350 lines — env load, migrations, AppState, route registration, server bind. Background jobs are kicked off here but defined elsewhere. Every module lives in the `image_api` library (`lib.rs`); `main.rs` and the `src/bin` tools only `use image_api::…`. Never add `mod` declarations to `main.rs` — that compiles the module a second time (doubling release builds and running its tests twice). Anything `main.rs` calls must be `pub`.
- **HTTP Layer** (`handlers/{image,video,favorites}.rs`, `files.rs`, `tags.rs`, `faces.rs`, `memories.rs`, `ai/handlers.rs`): the route handlers, grouped by domain.
- **Background loops** (`watcher.rs`): the file-watcher tick (`watch_files`, `process_new_files`) and the orphaned-playlist cleanup (`cleanup_orphaned_playlists`). Per-tick drains are factored into `backfill.rs` (`backfill_unhashed_backlog`, `backfill_missing_date_taken`, `backfill_missing_content_hashes`, `process_face_backlog`, `build_face_candidates`).
- **Thumbnails** (`thumbnails.rs`): generation pipeline + the `IMAGE_GAUGE` / `VIDEO_GAUGE` Prometheus metrics.
-4
View File
@@ -4517,10 +4517,6 @@ Return ONLY the summary, nothing else."#,
///
/// Calls `execute_tool` with empty file/image context; enabled tools
/// never read those fields.
///
/// Only used by the `reels` module (compiled in `main.rs`, not `lib.rs`),
/// so the `#[allow(dead_code)]` suppresses the lib-target warning.
#[allow(dead_code)]
pub(crate) async fn run_readonly_tool_loop(
&self,
backend: &ResolvedBackend,
-3
View File
@@ -13,9 +13,6 @@
//! is request-scoped and stays in `ResolvedBackend`. This is only the
//! local stack: embeddings and offline single-shot generation.
// Constructed by binaries, not the server — dead code from main.rs's view.
#![allow(dead_code)]
use std::sync::Arc;
use anyhow::Result;
-5
View File
@@ -26,11 +26,6 @@
//! The pass operates on the database alone — no filesystem access —
//! so it doesn't need the library availability gate.
// The lib doesn't call into this module directly — the watcher (in the
// bin) does. Dead-code analysis at the lib level can't see that, so
// suppress at the module level. Tests still exercise every function.
#![allow(dead_code)]
use diesel::prelude::*;
use diesel::sql_query;
use diesel::sqlite::SqliteConnection;
+1 -2
View File
@@ -1342,8 +1342,7 @@ pub struct RealFileSystem {
}
impl RealFileSystem {
#[allow(dead_code)] // Used in main.rs binary and tests
pub(crate) fn new(base_path: String) -> RealFileSystem {
pub fn new(base_path: String) -> RealFileSystem {
RealFileSystem { base_path }
}
}
+6
View File
@@ -5,6 +5,7 @@ extern crate diesel;
pub mod ai;
pub mod auth;
pub mod backfill;
pub mod bin_progress;
pub mod camera_tz;
pub mod cleanup;
@@ -25,6 +26,9 @@ pub mod file_types;
pub mod filename_audit;
pub mod files;
pub mod geo;
pub mod handlers;
pub mod hls_stats;
pub mod knowledge;
pub mod libraries;
pub mod library_maintenance;
pub mod memories;
@@ -33,6 +37,7 @@ pub mod parsers;
pub mod perceptual_hash;
pub mod personas;
pub mod prefs;
pub mod reels;
pub mod service;
pub mod state;
pub mod tags;
@@ -42,6 +47,7 @@ pub mod thumbnails;
pub mod unified_search;
pub mod utils;
pub mod video;
pub mod watcher;
// Re-export commonly used types
pub use data::{Claims, ThumbnailRequest};
+14 -62
View File
@@ -1,9 +1,7 @@
//! The server binary. Everything it runs lives in the `image_api`
//! library so the crate compiles once, shared with the `src/bin` tools.
#![allow(clippy::too_many_arguments)]
#[macro_use]
extern crate diesel;
extern crate rayon;
use actix_web::web::Data;
use actix_web_prom::PrometheusMetricsBuilder;
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
@@ -18,62 +16,16 @@ use actix_web::{App, HttpResponse, HttpServer, middleware, web};
use diesel::sqlite::Sqlite;
use std::error::Error;
use crate::ai::InsightGenerator;
use crate::auth::login;
use crate::data::*;
use crate::database::*;
use crate::files::{RealFileSystem, move_file};
use crate::service::ServiceBuilder;
use crate::state::AppState;
use crate::tags::*;
use image_api::ai::InsightGenerator;
use image_api::auth::login;
use image_api::database::*;
use image_api::files::{RealFileSystem, move_file};
use image_api::service::ServiceBuilder;
use image_api::state::AppState;
use image_api::tags::*;
use image_api::*;
use log::{error, info};
mod ai;
mod auth;
mod backfill;
// Display-zone helpers land in the read-side cutover (#119/#120); the
// binary's private copy of this module doesn't use them yet.
#[allow(dead_code)]
mod camera_tz;
mod clip_search;
mod clip_watch;
mod content_hash;
mod data;
mod database;
mod date_resolver;
mod date_view;
mod duplicates;
mod error;
mod exif;
mod face_watch;
mod faces;
mod file_scan;
mod file_types;
mod files;
mod geo;
mod handlers;
mod hls_stats;
mod libraries;
mod library_maintenance;
mod perceptual_hash;
mod state;
mod tags;
mod thumbnails;
mod unified_search;
mod utils;
mod video;
mod watcher;
mod knowledge;
mod memories;
mod otel;
mod personas;
mod prefs;
mod reels;
mod service;
#[cfg(test)]
mod testhelpers;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
fn main() -> std::io::Result<()> {
@@ -85,7 +37,7 @@ fn main() -> std::io::Result<()> {
// Recover orphaned insight generation jobs from a previous crash.
{
use crate::database::{InsightGenerationJobDao, SqliteInsightGenerationJobDao};
use image_api::database::{InsightGenerationJobDao, SqliteInsightGenerationJobDao};
let mut dao = SqliteInsightGenerationJobDao::new();
match dao.recover_orphaned_jobs() {
Ok(n) if n > 0 => {
@@ -228,9 +180,9 @@ fn main() -> std::io::Result<()> {
// Spawn background job to generate daily conversation summaries
{
use crate::ai::generate_daily_summaries;
use crate::database::{DailySummaryDao, SqliteDailySummaryDao};
use chrono::NaiveDate;
use image_api::ai::generate_daily_summaries;
use image_api::database::{DailySummaryDao, SqliteDailySummaryDao};
// Configure date range for summary generation
// Default: August 2024 ±30 days (July 1 - September 30, 2024)
@@ -453,7 +405,7 @@ fn main() -> std::io::Result<()> {
Box::new(database::SqlitePersonaDao::new()),
)))
.app_data::<Data<Mutex<faces::SqliteFaceDao>>>(Data::new(Mutex::new(face_dao)))
.app_data::<Data<crate::ai::face_client::FaceClient>>(Data::new(
.app_data::<Data<image_api::ai::face_client::FaceClient>>(Data::new(
app_data.face_client.clone(),
))
.app_data(mp::form::MultipartFormConfig::default().total_limit(1024 * 1024 * 1024)) // 1GB upload limit
+2 -2
View File
@@ -1045,7 +1045,7 @@ fn load_pregen_params(app_state: &AppState) -> (String, Option<String>, String)
}
/// Spawn the nightly pre-generation scheduler. Runs behind `REEL_PREGEN_ENABLED`.
pub(crate) async fn spawn_pregen_scheduler(app_state: web::Data<AppState>) {
pub async fn spawn_pregen_scheduler(app_state: web::Data<AppState>) {
if std::env::var("REEL_PREGEN_ENABLED").ok() != Some("1".to_string()) {
log::info!("Reel pre-generation scheduler disabled (REEL_PREGEN_ENABLED != 1)");
return;
@@ -1270,7 +1270,7 @@ fn reel_cache_max_age_secs() -> u64 {
/// Spawn the periodic on-disk reel-cache sweeper. Runs independently of the
/// pre-gen scheduler because the on-demand cache grows whether or not pre-gen
/// is enabled. Disable with `REEL_CACHE_SWEEP_ENABLED=0`.
pub(crate) async fn spawn_reel_cache_sweeper(app_state: web::Data<AppState>) {
pub async fn spawn_reel_cache_sweeper(app_state: web::Data<AppState>) {
if std::env::var("REEL_CACHE_SWEEP_ENABLED").ok().as_deref() == Some("0") {
log::info!("Reel cache sweeper disabled (REEL_CACHE_SWEEP_ENABLED=0)");
return;
+3 -3
View File
@@ -628,11 +628,11 @@ pub fn watch_files(
// See database::reconcile and CLAUDE.md "Multi-library data
// model" for the rules.
{
let mut conn = image_api::database::connect();
let mut conn = crate::database::connect();
if is_full_scan {
library_maintenance::refresh_planner_stats(&mut conn);
}
let _ = image_api::database::reconcile::run(&mut conn);
let _ = crate::database::reconcile::run(&mut conn);
// Back-ref refresh: hash-keyed rows whose
// (library_id, rel_path) tuple no longer matches any
@@ -721,7 +721,7 @@ pub fn process_new_files(
// filters. See `file_scan` for why exclusion has to happen at WalkDir
// time (filter_entry) rather than at face-detect time.
let files: Vec<(PathBuf, String)> =
image_api::file_scan::enumerate_indexable_files(base_path, excluded_dirs, modified_since);
crate::file_scan::enumerate_indexable_files(base_path, excluded_dirs, modified_since);
if files.is_empty() {
debug!("No files to process");