use anyhow::Result;
use axum::{
routing::{get, post, put},
Router,
middleware,
};
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio::signal;
use tower::ServiceBuilder;
use tower_http::{
compression::CompressionLayer,
cors::CorsLayer,
services::ServeDir,
trace::TraceLayer,
};
use tracing::{info, warn};
use crate::{
config::Config,
handlers,
middleware as app_middleware,
services::{FileService, SearchService},
ai::AiService,
};
pub struct Server {
config: Arc<Config>,
file_service: Arc<FileService>,
search_service: Arc<SearchService>,
ai_service: Option<Arc<AiService>>,
}
impl Server {
pub async fn new(config: Arc<Config>) -> Result<Self> {
info!("Initializing server components");
let file_service = Arc::new(FileService::new(config.clone()).await?);
let search_service = Arc::new(SearchService::new(config.clone()).await?);
let ai_service = if config.ai.enabled {
info!("Initializing AI service");
Some(Arc::new(AiService::new(Arc::new(config.ai.clone())).await?))
} else {
info!("AI features disabled");
None
};
Ok(Self {
config,
file_service,
search_service,
ai_service,
})
}
fn build_router(&self) -> Router {
let mut router = Router::new()
.route("/api/files/*path", get(handlers::files::list).post(handlers::files::upload))
.route("/api/files/*path", put(handlers::files::update).delete(handlers::files::delete))
.route("/api/files/download/*path", get(handlers::files::download))
.route("/api/files/metadata/*path", get(handlers::files::metadata))
.route("/api/search", get(handlers::search::search))
.route("/api/search/semantic", post(handlers::search::semantic_search))
.route("/api/search/files", get(handlers::search::file_search))
.route("/dav/*path", axum::routing::any(handlers::webdav::handle_webdav))
.nest_service("/", ServeDir::new(&self.config.server.root_dir)
.precompressed_br()
.precompressed_gzip())
.route("/health", get(handlers::health::health_check))
.route("/metrics", get(handlers::metrics::metrics));
if self.ai_service.is_some() {
router = router
.route("/api/ai/analyze", post(handlers::ai::analyze_content))
.route("/api/ai/summarize", post(handlers::ai::summarize))
.route("/api/ai/organize", post(handlers::ai::suggest_organization))
.route("/api/ai/chat", post(handlers::ai::chat));
}
router
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new())
.layer(CorsLayer::permissive())
.layer(middleware::from_fn_with_state(
self.build_app_state(),
app_middleware::auth::auth_middleware
))
.layer(middleware::from_fn(app_middleware::logging::logging_middleware))
)
.with_state(self.build_app_state())
}
fn build_app_state(&self) -> AppState {
AppState {
config: self.config.clone(),
file_service: self.file_service.clone(),
search_service: self.search_service.clone(),
ai_service: self.ai_service.clone(),
}
}
pub async fn run(&self) -> Result<()> {
let app = self.build_router();
let addr = format!("{}:{}", self.config.server.host, self.config.server.port);
info!("Starting server on {}", addr);
let listener = TcpListener::bind(&addr).await?;
axum::serve(listener, app.into_make_service())
.with_graceful_shutdown(shutdown_signal())
.await?;
info!("Server stopped");
Ok(())
}
}
#[derive(Clone)]
pub struct AppState {
pub config: Arc<Config>,
pub file_service: Arc<FileService>,
pub search_service: Arc<SearchService>,
pub ai_service: Option<Arc<AiService>>,
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
warn!("Shutdown signal received, starting graceful shutdown");
}