//! # Module Server - API de haut niveau pour Axum //! //! Ce module fournit une abstraction simple et ergonomique pour créer des serveurs HTTP //! avec Axum, en cachant la complexité de la configuration et du routage. //! //! ## Fonctionnalités //! //! - 🚀 **Routes JSON simples** : Ajoutez des endpoints API avec `add_route()` //! - 📁 **Fichiers statiques** : Servez des assets avec `add_dir()` //! - ⚛️ **Applications SPA** : Support pour Vue.js/React avec `add_spa()` //! - 🔀 **Redirections** : Redirigez des routes avec `add_redirect()` //! - 🎯 **Handlers personnalisés** : Support SSE, WebSocket, etc. avec `add_handler_with_state()` //! - ⚡ **Gestion gracieuse** : Arrêt propre sur Ctrl+C //! //! ## Exemple d'utilisation //! //! ```rust,no_run //! use pmoupnp::server::{ServerBuilder, Server}; //! use rust_embed::RustEmbed; //! //! #[derive(RustEmbed, Clone)] //! #[folder = "static/"] //! struct Assets; //! //! #[tokio::main] //! async fn main() { //! let mut server = ServerBuilder::new("MyAPI", "http://localhost:3000", 3000) //! .build(); //! //! // Route JSON simple //! server.add_route("/api/hello", || async { //! serde_json::json!({"message": "Hello World"}) //! }).await; //! //! // Redirection //! server.add_redirect("/", "/app").await; //! //! // Application Vue.js //! server.add_spa::("/app").await; //! //! server.start().await; //! server.wait().await; //! } //! ``` pub mod logs; use axum::handler::Handler; use axum::response::Redirect; use axum::routing::get; use axum::{Json, Router}; use axum_embed::ServeEmbed; use pmoconfig::get_config; use rust_embed::RustEmbed; use serde::Serialize; use std::{net::SocketAddr, sync::Arc}; use tokio::{signal, sync::RwLock, task::JoinHandle}; use tracing::{info,warn,debug,error}; /// Info serveur sérialisable #[derive(Clone, Serialize)] pub struct ServerInfo { pub name: String, pub base_url: String, pub http_port: u16, } /// Serveur principal pub struct Server { name: String, base_url: String, http_port: u16, router: Arc>, join_handle: Option>, } #[derive(RustEmbed, Clone)] #[folder = "webapp/dist"] pub struct Webapp; impl Server { /// Crée une nouvelle instance de serveur /// /// # Arguments /// /// * `name` - Nom du serveur (pour les logs) /// * `base_url` - URL de base (ex: "http://localhost:3000") /// * `http_port` - Port HTTP à écouter /// /// # Exemple /// /// ```rust /// # use pmoupnp::server::Server; /// let server = Server::new("MyAPI", "http://localhost:3000", 3000); /// ``` pub fn new(name: impl Into, base_url: impl Into, http_port: u16) -> Self { Self { name: name.into(), base_url: base_url.into(), http_port, router: Arc::new(RwLock::new(Router::new())), join_handle: None, } } pub fn new_configured() -> Self { let config = get_config(); let url = config.get_base_url(); let port = config.get_http_port(); return Self::new("PMO-Music-Server", url, port); } /// Ajoute une route dynamique /// Ajoute une route JSON dynamique /// /// Crée un endpoint qui retourne du JSON. La closure fournie sera appelée /// à chaque requête GET sur le chemin spécifié. /// /// # Arguments /// /// * `path` - Chemin de la route (ex: "/api/hello") /// * `f` - Closure async retournant une valeur sérialisable /// /// # Exemple /// /// ```rust,no_run /// # use pmoupnp::server::Server; /// # #[tokio::main] /// # async fn main() { /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); /// server.add_route("/api/status", || async { /// serde_json::json!({ /// "status": "online", /// "version": "1.0.0" /// }) /// }).await; /// # } /// ``` pub async fn add_route(&mut self, path: &str, f: F) where F: Fn() -> Fut + Send + Sync + 'static, Fut: std::future::Future + Send + 'static, T: Serialize + Send + 'static, { let f = Arc::new(f); let handler = { let f = f.clone(); move || { let f = f.clone(); async move { Json(f().await) } } }; let route = Router::new().route("/", get(handler)); let mut r = self.router.write().await; *r = std::mem::take(&mut *r).nest(path, route); } /// Ajoute un répertoire de fichiers statiques /// /// Sert des fichiers embarqués via `RustEmbed`. Les fichiers sont compilés /// dans le binaire à la compilation. /// /// # Arguments /// /// * `path` - Chemin où monter les fichiers statiques /// /// # Type Parameter /// /// * `E` - Type RustEmbed définissant le répertoire à servir /// /// # Exemple /// /// ```rust,no_run /// # use pmoupnp::server::Server; /// # use rust_embed::RustEmbed; /// #[derive(RustEmbed, Clone)] /// #[folder = "static/"] /// struct Assets; /// /// # #[tokio::main] /// # async fn main() { /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); /// server.add_dir::("/assets").await; /// // Les fichiers de static/ sont accessibles via /assets/* /// # } /// ``` pub async fn add_dir(&mut self, path: &str) where E: RustEmbed + Clone + Send + Sync + 'static, { let serve = ServeEmbed::::new(); let mut r = self.router.write().await; if path == "/" { *r = std::mem::take(&mut *r).fallback_service(serve); } else { let route = Router::new().fallback_service(serve); *r = std::mem::take(&mut *r).nest(path, route); } } /// Ajoute une Single Page Application (SPA) /// /// Sert une application JavaScript moderne (Vue.js, React, etc.) avec support /// du routage côté client. Tous les chemins non trouvés renvoient `index.html` /// pour permettre au routeur JavaScript de gérer la navigation. /// /// # Arguments /// /// * `path` - Chemin où monter l'application (souvent "/" ou "/app") /// /// # Type Parameter /// /// * `E` - Type RustEmbed contenant les fichiers de la SPA /// /// # Exemple avec Vue.js /// /// ```rust,no_run /// # use pmoupnp::server::Server; /// # use rust_embed::RustEmbed; /// #[derive(RustEmbed, Clone)] /// #[folder = "webapp/dist"] // Build output de Vue.js /// struct WebApp; /// /// # #[tokio::main] /// # async fn main() { /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); /// server.add_spa::("/").await; /// // L'app Vue.js gère toutes les routes comme /about, /users, etc. /// # } /// ``` /// /// # Note /// /// Pour Vue.js/Vite, configure le `base` dans `vite.config.js` si tu montes /// sur un sous-chemin : /// ```javascript /// export default { /// base: '/app/' /// } /// ``` pub async fn add_spa(&mut self, path: &str) where E: RustEmbed + Clone + Send + Sync + 'static, { let serve = ServeEmbed::::with_parameters( Some("index.html".to_string()), axum_embed::FallbackBehavior::Ok, Some("index.html".to_string()), ); let mut r = self.router.write().await; if path == "/" { *r = std::mem::take(&mut *r).fallback_service(serve); } else { let route = Router::new().fallback_service(serve); *r = std::mem::take(&mut *r).nest(path, route); } } /// Ajoute un handler Axum personnalisé /// /// Pour des cas d'usage avancés nécessitant un contrôle complet sur le handler. /// /// # Arguments /// /// * `path` - Chemin de la route /// * `handler` - Handler Axum /// /// # Exemple /// /// ```rust,no_run /// # use pmoupnp::server::Server; /// # use axum::response::Html; /// # #[tokio::main] /// # async fn main() { /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); /// async fn custom_handler() -> Html<&'static str> { /// Html("

Custom Response

") /// } /// /// server.add_handler("/custom", custom_handler).await; /// # } /// ``` pub async fn add_handler(&mut self, path: &str, handler: H) where H: Handler, T: 'static, { let route = Router::new().route("/", get(handler)); let mut r = self.router.write().await; *r = std::mem::take(&mut *r).nest(path, route); } /// Ajoute un handler avec state (pour SSE, extracteurs, etc.) /// /// Permet d'utiliser des extracteurs Axum comme `State`, `Query`, etc. /// Idéal pour Server-Sent Events (SSE), WebSockets ou tout handler nécessitant un état partagé. /// /// # Arguments /// /// * `path` - Chemin de la route /// * `handler` - Handler Axum avec extracteurs /// * `state` - État partagé (doit être Clone + Send + Sync) /// /// # Exemple avec SSE /// /// ```rust,no_run /// # use pmoupnp::server::Server; /// # use axum::extract::State; /// # use axum::response::sse::{Event, Sse, KeepAlive}; /// # use tokio::sync::broadcast; /// # #[derive(Clone)] /// # struct LogState { tx: broadcast::Sender } /// # impl LogState { fn subscribe(&self) -> broadcast::Receiver { self.tx.subscribe() } } /// # #[tokio::main] /// # async fn main() { /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); /// async fn log_sse(State(state): State) -> Sse>> { /// let mut rx = state.subscribe(); /// let stream = async_stream::stream! { /// while let Ok(msg) = rx.recv().await { /// yield Ok(Event::default().data(msg)); /// } /// }; /// Sse::new(stream).keep_alive(KeepAlive::default()) /// } /// /// let log_state = LogState { tx: broadcast::channel(100).0 }; /// server.add_handler_with_state("/logs", log_sse, log_state).await; /// # } /// ``` pub async fn add_handler_with_state(&mut self, path: &str, handler: H, state: S) where H: Handler, T: 'static, S: Clone + Send + Sync + 'static, { let route = Router::new() .route("/", get(handler)) .with_state(state); let mut r = self.router.write().await; *r = std::mem::take(&mut *r).nest(path, route); } /// Ajoute un handler POST avec state /// /// Similaire à `add_handler_with_state` mais pour les requêtes POST. /// /// # Arguments /// /// * `path` - Chemin de la route /// * `handler` - Handler Axum pour POST /// * `state` - État partagé pub async fn add_post_handler_with_state(&mut self, path: &str, handler: H, state: S) where H: Handler, T: 'static, S: Clone + Send + Sync + 'static, { let route = Router::new() .route("/", axum::routing::post(handler)) .with_state(state); let mut r = self.router.write().await; *r = std::mem::take(&mut *r).nest(path, route); } /// Ajoute une redirection HTTP /// /// Redirige automatiquement les requêtes d'un chemin vers un autre avec un code 308 (permanent). /// /// # Arguments /// /// * `from` - Chemin source (peut être "/" pour la racine) /// * `to` - Chemin de destination /// /// # Exemple /// /// ```rust,no_run /// # use pmoupnp::server::Server; /// # #[tokio::main] /// # async fn main() { /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); /// // Rediriger la racine vers /app /// server.add_redirect("/", "/app").await; /// # } /// ``` pub async fn add_redirect(&mut self, from: &str, to: &str) { let to = to.to_string(); let handler = move || { let to = to.clone(); async move { Redirect::permanent(&to) } }; let mut r = self.router.write().await; if from == "/" { // Pour la racine, utiliser merge au lieu de nest let route = Router::new().route("/", get(handler)); *r = std::mem::take(&mut *r).merge(route); } else { let route = Router::new().route("/", get(handler)); *r = std::mem::take(&mut *r).nest(from, route); } } /// Démarre le serveur HTTP /// /// Lance le serveur sur le port configuré et met en place la gestion /// de Ctrl+C pour un arrêt gracieux. /// /// # Exemple /// /// ```rust,no_run /// # use pmoupnp::server::Server; /// # #[tokio::main] /// # async fn main() { /// # let mut server = Server::new("Test", "http://localhost:3000", 3000); /// server.start().await; /// server.wait().await; // Attend Ctrl+C /// # } /// ``` pub async fn start(&mut self) { let addr = SocketAddr::from(([0, 0, 0, 0], self.http_port)); info!("Server {} running at [http://{}:{}](http://{}:{})", self.name, self.base_url, self.http_port, self.base_url, self.http_port); let router = self.router.clone(); let server_task = tokio::spawn(async move { let r = router.read().await.clone(); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, r.into_make_service()).await.unwrap(); }); let shutdown_task = tokio::spawn(async move { signal::ctrl_c().await.expect("failed to listen for ctrl_c"); info!("Ctrl+C reçu, arrêt gracieux"); }); self.join_handle = Some(tokio::spawn(async move { tokio::select! { _ = server_task => {}, _ = shutdown_task => {}, } })); } /// Attend la fin du serveur pub async fn wait(&mut self) { if let Some(h) = self.join_handle.take() { let _ = h.await; } } /// Récupère les infos du serveur pub fn info(&self) -> ServerInfo { ServerInfo { name: self.name.clone(), base_url: self.base_url.clone(), http_port: self.http_port, } } } /// Builder pattern pub struct ServerBuilder { name: String, base_url: String, http_port: u16, } impl ServerBuilder { /// Crée un nouveau builder /// /// # Arguments /// /// * `name` - Nom du serveur /// * `base_url` - URL de base (ex: "http://localhost:3000") /// * `http_port` - Port HTTP pub fn new(name: impl Into, base_url: impl Into, http_port: u16) -> Self { Self { name: name.into(), base_url: base_url.into(), http_port, } } pub fn new_configured() -> Self { let config = get_config(); Self { name: "PMO-Music-Server".to_string(), base_url: config.get_base_url(), http_port: config.get_http_port() } } /// Construit le serveur /// /// Consomme le builder et retourne une instance de `Server` prête à l'emploi. /// /// # Exemple /// /// ```rust /// # use pmoupnp::server::ServerBuilder; /// let mut server = ServerBuilder::new("MyAPI", "http://localhost:3000", 3000) /// .build(); /// ``` pub fn build(self) -> Server { Server::new(self.name, self.base_url, self.http_port) } }