d0b7bfa56b
- Add GET /api/v1/file/:file_uuid/pose endpoint - Add GET /api/v1/file/:file_uuid/appearance endpoint - Keypoint deduplication (highest confidence) - Add face-groups endpoint for Studio integration - Add profile routes (trace-profile, file-profile) - Update API documentation
170 lines
5.6 KiB
Rust
170 lines
5.6 KiB
Rust
use std::time::Duration;
|
|
|
|
use axum::{Json, Router};
|
|
use serde_json::json;
|
|
use tokio::time::timeout;
|
|
use tower_http::cors::{Any, CorsLayer};
|
|
|
|
use crate::core::cache::{MongoCache, RedisCache};
|
|
use crate::core::db::{Database, PostgresDb, QdrantDb};
|
|
use crate::Embedder;
|
|
|
|
use super::agent_api;
|
|
use super::agent_search;
|
|
use super::auth;
|
|
use super::checkin_api;
|
|
use super::docs;
|
|
use super::files;
|
|
use super::health;
|
|
use super::health::{health, health_consistency, health_detailed};
|
|
use super::identities;
|
|
use super::identity_agent_api;
|
|
use super::identity_api;
|
|
use super::identity_binding;
|
|
use super::llm_search;
|
|
use super::media_api;
|
|
use super::middleware::unified_auth;
|
|
use super::pipeline;
|
|
use super::processing;
|
|
use super::profile;
|
|
use super::scan;
|
|
use super::search::search_routes;
|
|
use super::tmdb_api;
|
|
use super::trace_agent_api;
|
|
use super::types::AppState;
|
|
use super::universal_search::universal_search_routes;
|
|
|
|
pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> {
|
|
health::init_server_state(host, port);
|
|
|
|
let embedder = std::sync::Arc::new(Embedder::new("embeddinggemma-300m".to_string()));
|
|
|
|
// ── ⚠️ WARNING: DO NOT move MongoCache::init() back to critical path ──
|
|
//
|
|
// MongoDB is ONLY a cache layer — if unavailable, the server MUST still
|
|
// start with Redis cache alone. This keeps 3002 bootable on machines
|
|
// without MongoDB installed. If you add a new dependency here, ask:
|
|
// "Can this service degrade gracefully if the dependant is missing?"
|
|
//
|
|
// See also: MongoCache::disabled() in mongo_cache.rs
|
|
let mongo_cache = match timeout(Duration::from_secs(5), MongoCache::init()).await {
|
|
Ok(Ok(cache)) => cache,
|
|
Ok(Err(e)) => {
|
|
tracing::warn!("MongoDB cache unavailable (continuing without): {e}");
|
|
MongoCache::disabled().await
|
|
}
|
|
Err(_) => {
|
|
tracing::warn!("MongoDB init timed out (continuing without cache)");
|
|
MongoCache::disabled().await
|
|
}
|
|
};
|
|
let redis_cache = RedisCache::new()?;
|
|
let db = PostgresDb::init().await?;
|
|
|
|
// Run migrations (create identity_history table if not exists)
|
|
PostgresDb::run_migrations(db.pool()).await?;
|
|
|
|
let schema_health = health::check_schema_migrations(db.pool()).await;
|
|
if schema_health.ok {
|
|
tracing::info!(
|
|
"[SCHEMA] All {}/{} required migrations applied ✓",
|
|
schema_health.required.len(),
|
|
schema_health.required.len()
|
|
);
|
|
} else if !schema_health.table_exists {
|
|
tracing::warn!("[SCHEMA] schema_migrations table not found!");
|
|
} else {
|
|
let missing: Vec<&str> = schema_health
|
|
.required
|
|
.iter()
|
|
.filter(|req| {
|
|
!schema_health
|
|
.applied
|
|
.iter()
|
|
.any(|app| app.filename == req.filename && app.checksum == req.checksum)
|
|
})
|
|
.map(|m| m.filename.as_str())
|
|
.collect();
|
|
tracing::warn!(
|
|
"[SCHEMA] {}/{} migrations match. Missing: {}",
|
|
schema_health.applied.len(),
|
|
schema_health.required.len(),
|
|
missing.join(", ")
|
|
);
|
|
}
|
|
|
|
let db = std::sync::Arc::new(db);
|
|
let api_state = super::middleware::ApiState { db: db.clone() };
|
|
|
|
let qdrant = std::sync::Arc::new(QdrantDb::new());
|
|
let state = AppState {
|
|
db,
|
|
qdrant,
|
|
embedder,
|
|
embedder_model: "nomic-embed-text-v2-moe:latest".to_string(),
|
|
mongo_cache,
|
|
redis_cache,
|
|
api_state,
|
|
};
|
|
|
|
let protected_routes = Router::new()
|
|
.merge(files::file_routes())
|
|
.merge(scan::scan_routes())
|
|
.merge(identity_binding::identity_binding_routes())
|
|
.merge(identities::identity_routes())
|
|
.merge(tmdb_api::tmdb_routes())
|
|
.merge(identity_api::identity_routes())
|
|
.merge(agent_api::agent_routes())
|
|
.merge(agent_search::agent_search_routes())
|
|
.merge(processing::processing_routes())
|
|
.merge(identity_agent_api::identity_agent_routes())
|
|
.merge(media_api::bbox_routes())
|
|
.merge(media_api::media_proxy_routes())
|
|
.merge(media_api::pose_appearance_routes())
|
|
.merge(trace_agent_api::trace_agent_routes())
|
|
.merge(search_routes())
|
|
.merge(llm_search::llm_smart_routes())
|
|
.merge(universal_search_routes())
|
|
.merge(pipeline::pipeline_routes())
|
|
.merge(checkin_api::checkin_routes())
|
|
.merge(profile::profile_routes())
|
|
.layer(axum::middleware::from_fn_with_state(
|
|
state.api_state.clone(),
|
|
unified_auth,
|
|
))
|
|
.with_state(state.clone());
|
|
|
|
let cors = CorsLayer::new()
|
|
.allow_origin(Any)
|
|
.allow_methods(Any)
|
|
.allow_headers(Any);
|
|
|
|
let public_health_routes = Router::new()
|
|
.route("/api/v1/health", axum::routing::get(health))
|
|
.route(
|
|
"/api/v1/health/detailed",
|
|
axum::routing::get(health_detailed),
|
|
)
|
|
.route(
|
|
"/api/v1/health/consistency",
|
|
axum::routing::get(health_consistency),
|
|
);
|
|
|
|
let app = Router::new()
|
|
.merge(auth::auth_routes())
|
|
.merge(public_health_routes)
|
|
.merge(health::health_routes())
|
|
.merge(docs::doc_routes())
|
|
.merge(protected_routes)
|
|
.layer(cors)
|
|
.with_state(state);
|
|
|
|
let addr: std::net::SocketAddr = format!("{}:{}", host, port).parse().unwrap();
|
|
tracing::info!("Starting API server at http://{}", addr);
|
|
|
|
let listener = tokio::net::TcpListener::bind(addr).await?;
|
|
axum::serve(listener, app).await?;
|
|
|
|
Ok(())
|
|
}
|