use std::{ env, error::Error, net::SocketAddr, path::{Path, PathBuf}, }; use axum::{ Router, routing::{get, post}, }; use tower_http::services::{ServeDir, ServeFile}; use crate::{api, storage::Storage}; const DEFAULT_BIND_ADDR: &str = "127.0.0.1:3000"; const STATIC_DIR_ENV: &str = "UPL_STATIC_DIR"; const DATA_DIR_ENV: &str = "UPL_DATA_DIR"; const BIND_ENV: &str = "UPL_BIND"; #[derive(Clone, Debug)] pub struct AppConfig { pub bind_addr: SocketAddr, pub static_dir: PathBuf, pub data_dir: PathBuf, } #[derive(Clone, Debug)] pub struct AppState { pub storage: Storage, } impl AppConfig { /// Loads bind and static directory settings from environment variables. /// /// # Errors /// /// Returns an error when `UPL_BIND` is set but is not a valid socket address. pub fn from_env() -> Result> { let bind_addr = env::var(BIND_ENV) .unwrap_or_else(|_| DEFAULT_BIND_ADDR.to_owned()) .parse()?; let static_dir = env::var_os(STATIC_DIR_ENV).map_or_else(default_static_dir, PathBuf::from); let data_dir = env::var_os(DATA_DIR_ENV).map_or_else(default_data_dir, PathBuf::from); Ok(Self { bind_addr, static_dir, data_dir, }) } #[must_use] pub fn new( bind_addr: SocketAddr, static_dir: impl Into, data_dir: impl Into, ) -> Self { Self { bind_addr, static_dir: static_dir.into(), data_dir: data_dir.into(), } } } pub fn build_router(config: &AppConfig) -> Router { let state = AppState { storage: Storage::new(&config.data_dir), }; Router::new() .route("/healthz", get(healthz)) .route("/api/uploads", post(api::create_upload)) .fallback_service(static_service(&config.static_dir)) .with_state(state) } async fn healthz() -> &'static str { "ok" } fn static_service(static_dir: &Path) -> ServeDir { ServeDir::new(static_dir).fallback(ServeFile::new(static_dir.join("index.html"))) } fn default_static_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("static") } fn default_data_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data") }