mod config; mod http; mod ratelimit; mod routes; mod security; mod storage; use std::net::SocketAddr; use std::sync::Arc; use std::time::{Duration, Instant}; use axum::routing::{delete, get, post}; use axum::Router; use config::Config; use ratelimit::RateLimiter; use storage::Storage; #[derive(Clone)] pub struct AppState { pub config: Config, pub storage: Arc, pub secret: String, pub limiter: Arc, pub started_at: Instant, } fn build_router(state: AppState) -> Router { Router::new() .route("/api/health", get(routes::health)) .route("/api/upload", post(routes::upload::upload)) .route("/api/auth/{key}", post(routes::auth::auth)) .route("/api/files/{id}", delete(routes::delete::delete)) .fallback(routes::download::fallback) .with_state(state) } #[tokio::main] async fn main() { dotenvy::dotenv().ok(); tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "info".into()), ) .init(); let config = match config::load() { Ok(c) => c, Err(e) => { tracing::error!("configuration error: {e}"); std::process::exit(1); } }; security::set_key_length(config.key_length); let storage = Arc::new(Storage::new(config.storage_dir.clone())); if let Err(e) = storage.init().await { tracing::error!("failed to initialize storage: {e}"); std::process::exit(1); } let secret = match storage.load_secret().await { Ok(s) => s, Err(e) => { tracing::error!("failed to load server secret: {e}"); std::process::exit(1); } }; let sweep = storage.sweep().await; if sweep.0 > 0 || sweep.1 > 0 { tracing::info!( "startup sweep removed {} orphaned file(s), {} orphaned metadata record(s)", sweep.0, sweep.1 ); } let state = AppState { limiter: Arc::new(RateLimiter::new(config.rate_limit_max, config.rate_limit_window_ms)), started_at: Instant::now(), config: config.clone(), storage, secret, }; // Hourly expiration sweep. let expiry_state = state.clone(); tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(3600)); loop { interval.tick().await; let removed = expiry_state .storage .expire_expired(chrono::Utc::now().timestamp_millis()) .await; if removed > 0 { tracing::info!("expiry sweep removed {removed} file(s)"); } } }); let app = build_router(state); let addr = format!("{}:{}", config.host, config.port); let listener = match tokio::net::TcpListener::bind(&addr).await { Ok(l) => l, Err(e) => { tracing::error!("failed to bind {addr}: {e}"); std::process::exit(1); } }; tracing::info!( "listening on http://{} storage={:?} maxUpload={}", addr, config.storage_dir, config.max_file_size ); axum::serve( listener, app.into_make_service_with_connect_info::(), ) .with_graceful_shutdown(shutdown_signal()) .await .expect("server error"); } async fn shutdown_signal() { let ctrl_c = async { let _ = tokio::signal::ctrl_c().await; }; #[cfg(unix)] let terminate = async { let _ = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) .expect("failed to install SIGTERM handler") .recv() .await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, } tracing::info!("shutdown signal received"); }