Sort le serveur dans une crate pmoserver
This commit is contained in:
79
pmoserver/src/lib.rs
Normal file
79
pmoserver/src/lib.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
//! # pmoserver - Serveur web haut niveau basé sur Axum
|
||||
//!
|
||||
//! Cette crate fournit une abstraction simple et ergonomique pour créer des serveurs HTTP
|
||||
//! avec Axum, spécialement conçue pour les applications UPnP et les serveurs multimédia.
|
||||
//!
|
||||
//! ## Fonctionnalités
|
||||
//!
|
||||
//! - 🚀 **API de haut niveau** : Interface simple pour créer des serveurs HTTP avec Axum
|
||||
//! - 🎯 **Support UPnP** : Implémentation du trait `UpnpServer` pour connecter des devices UPnP
|
||||
//! - 📡 **Server-Sent Events (SSE)** : Support intégré pour les logs en temps réel via SSE
|
||||
//! - ⚛️ **Applications SPA** : Support pour servir des applications Single Page (Vue.js, React, etc.)
|
||||
//! - 📁 **Fichiers statiques** : Serve de fichiers statiques avec `RustEmbed`
|
||||
//! - 🔀 **Redirections** : Support pour les redirections HTTP
|
||||
//! - 📚 **Documentation OpenAPI** : Génération automatique de Swagger UI
|
||||
//! - ⚡ **Arrêt gracieux** : Gestion propre de l'arrêt sur Ctrl+C
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! La crate est organisée en plusieurs modules :
|
||||
//!
|
||||
//! - [`server`] : Implémentation du serveur principal et du builder
|
||||
//! - [`logs`] : Système de logs SSE pour monitoring en temps réel
|
||||
//! - `upnp_impl` : Implémentation du trait `pmoupnp::UpnpServer` (privé)
|
||||
//!
|
||||
//! ## Exemple d'utilisation
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmoserver::{ServerBuilder, logs::{LogState, SseLayer}};
|
||||
//! use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() {
|
||||
//! // Configuration des logs avec SSE
|
||||
//! let log_state = LogState::new();
|
||||
//! tracing_subscriber::registry()
|
||||
//! .with(SseLayer::new(log_state.clone()))
|
||||
//! .init();
|
||||
//!
|
||||
//! // Création et démarrage du serveur
|
||||
//! let mut server = ServerBuilder::new("MyServer")
|
||||
//! .http_port(8080)
|
||||
//! .build();
|
||||
//!
|
||||
//! // Ajout d'une route JSON
|
||||
//! server.add_route("/api/status", || async {
|
||||
//! serde_json::json!({"status": "ok"})
|
||||
//! }).await;
|
||||
//!
|
||||
//! // Démarrage
|
||||
//! server.start().await;
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Intégration UPnP
|
||||
//!
|
||||
//! Le serveur implémente automatiquement le trait `pmoupnp::UpnpServer`, permettant
|
||||
//! de connecter des devices UPnP :
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmoupnp::{UpnpServer, mediarenderer::device::MEDIA_RENDERER};
|
||||
//! use pmoupnp::devices::DeviceInstance;
|
||||
//! use pmoserver::ServerBuilder;
|
||||
//! use std::sync::Arc;
|
||||
//!
|
||||
//! # async fn example() {
|
||||
//! let mut server = ServerBuilder::new("MediaRenderer").build();
|
||||
//! let device = Arc::new(DeviceInstance::new(&MEDIA_RENDERER));
|
||||
//!
|
||||
//! // Le device enregistre automatiquement ses routes
|
||||
//! device.register_urls(&mut server).await;
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub mod server;
|
||||
pub mod logs;
|
||||
mod upnp_impl;
|
||||
|
||||
pub use server::{Server, ServerBuilder, ServerInfo, Webapp};
|
||||
pub use logs::{LogState, SseLayer, log_sse, log_dump};
|
||||
159
pmoserver/src/logs/mod.rs
Normal file
159
pmoserver/src/logs/mod.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
// logs.rs
|
||||
mod sselayer;
|
||||
|
||||
pub use sselayer::SseLayer;
|
||||
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, RwLock},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Query, State},
|
||||
response::{
|
||||
IntoResponse,
|
||||
sse::{Event, KeepAlive, Sse},
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
/// Représente une entrée de log
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct LogEntry {
|
||||
pub timestamp: SystemTime,
|
||||
pub level: String,
|
||||
pub target: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Buffer circulaire partagé
|
||||
#[derive(Clone)]
|
||||
pub struct LogState {
|
||||
buffer: Arc<RwLock<VecDeque<LogEntry>>>,
|
||||
tx: broadcast::Sender<LogEntry>,
|
||||
}
|
||||
|
||||
impl LogState {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
buffer: Arc::new(RwLock::new(VecDeque::with_capacity(capacity))),
|
||||
tx: broadcast::channel(1000).0,
|
||||
}
|
||||
}
|
||||
|
||||
fn push(&self, entry: LogEntry) {
|
||||
let mut buf = self.buffer.write().unwrap();
|
||||
if buf.len() == buf.capacity() {
|
||||
buf.pop_front();
|
||||
}
|
||||
buf.push_back(entry.clone());
|
||||
let _ = self.tx.send(entry);
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<LogEntry> {
|
||||
self.tx.subscribe()
|
||||
}
|
||||
|
||||
pub fn dump(&self) -> Vec<LogEntry> {
|
||||
self.buffer.read().unwrap().iter().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Query params pour /log-sse
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct LogQuery {
|
||||
#[serde(default)]
|
||||
pub error: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub warn: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub info: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub debug: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub trace: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
/// Handler SSE
|
||||
// Dans logs.rs
|
||||
pub async fn log_sse(
|
||||
State(state): State<LogState>,
|
||||
Query(params): Query<LogQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let mut rx = state.subscribe();
|
||||
|
||||
// Récupérer l'historique du buffer
|
||||
let history = state.dump();
|
||||
|
||||
let stream = async_stream::stream! {
|
||||
// 1. Envoyer d'abord tous les logs historiques
|
||||
for entry in history {
|
||||
if !filter_entry(&entry, ¶ms) {
|
||||
continue;
|
||||
}
|
||||
let json = serde_json::to_string(&entry).unwrap();
|
||||
yield Ok::<_, axum::Error>(Event::default().data(json));
|
||||
}
|
||||
|
||||
// 2. Puis streamer les nouveaux logs en temps réel
|
||||
while let Ok(entry) = rx.recv().await {
|
||||
if !filter_entry(&entry, ¶ms) {
|
||||
continue;
|
||||
}
|
||||
let json = serde_json::to_string(&entry).unwrap();
|
||||
yield Ok::<_, axum::Error>(Event::default().data(json));
|
||||
}
|
||||
};
|
||||
|
||||
Sse::new(stream).keep_alive(KeepAlive::default())
|
||||
}
|
||||
|
||||
/// Handler REST (dump JSON du buffer)
|
||||
pub async fn log_dump(State(state): State<LogState>) -> impl IntoResponse {
|
||||
Json(state.dump())
|
||||
}
|
||||
|
||||
/// Fonction de filtrage
|
||||
fn filter_entry(entry: &LogEntry, q: &LogQuery) -> bool {
|
||||
// Filtrage par niveau
|
||||
let lvl = entry.level.to_lowercase();
|
||||
let mut allowed = false;
|
||||
|
||||
if let Some(true) = q.error {
|
||||
allowed |= lvl == "error";
|
||||
}
|
||||
if let Some(true) = q.warn {
|
||||
allowed |= lvl == "warn";
|
||||
}
|
||||
if let Some(true) = q.info {
|
||||
allowed |= lvl == "info";
|
||||
}
|
||||
if let Some(true) = q.debug {
|
||||
allowed |= lvl == "debug";
|
||||
}
|
||||
if let Some(true) = q.trace {
|
||||
allowed |= lvl == "trace";
|
||||
}
|
||||
|
||||
// si aucun flag → tout est autorisé
|
||||
if !(q.error.unwrap_or(false)
|
||||
|| q.warn.unwrap_or(false)
|
||||
|| q.info.unwrap_or(false)
|
||||
|| q.debug.unwrap_or(false)
|
||||
|| q.trace.unwrap_or(false))
|
||||
{
|
||||
allowed = true;
|
||||
}
|
||||
|
||||
// Filtrage par mot-clé
|
||||
if let Some(search) = &q.search {
|
||||
allowed &= entry.message.contains(search) || entry.target.contains(search);
|
||||
}
|
||||
|
||||
allowed
|
||||
}
|
||||
63
pmoserver/src/logs/sselayer.rs
Normal file
63
pmoserver/src/logs/sselayer.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use tracing::field::{Field, Visit};
|
||||
use tracing::{Event, Subscriber};
|
||||
use tracing_subscriber::{Layer, layer::Context};
|
||||
|
||||
use super::{LogEntry, LogState};
|
||||
use std::time::SystemTime;
|
||||
|
||||
struct LogVisitor {
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl LogVisitor {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
message: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for LogVisitor {
|
||||
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
|
||||
// capture le champ "message" ou concatène les autres
|
||||
if field.name() == "message" {
|
||||
self.message = format!("{:?}", value);
|
||||
} else {
|
||||
if !self.message.is_empty() {
|
||||
self.message.push(' ');
|
||||
}
|
||||
self.message
|
||||
.push_str(&format!("{}={:?}", field.name(), value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer de tracing qui pousse les events dans le buffer
|
||||
pub struct SseLayer {
|
||||
state: LogState,
|
||||
}
|
||||
|
||||
impl SseLayer {
|
||||
pub fn new(state: LogState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for SseLayer
|
||||
where
|
||||
S: Subscriber,
|
||||
{
|
||||
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
|
||||
let mut visitor = LogVisitor::new();
|
||||
event.record(&mut visitor);
|
||||
|
||||
let entry = LogEntry {
|
||||
timestamp: SystemTime::now(),
|
||||
level: event.metadata().level().to_string(),
|
||||
target: event.metadata().target().to_string(),
|
||||
message: visitor.message,
|
||||
};
|
||||
|
||||
self.state.push(entry);
|
||||
}
|
||||
}
|
||||
564
pmoserver/src/server.rs
Normal file
564
pmoserver/src/server.rs
Normal file
@@ -0,0 +1,564 @@
|
||||
//! # 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()`
|
||||
//! - 📚 **Documentation API** : OpenAPI/Swagger automatique avec `add_openapi()`
|
||||
//! - ⚡ **Gestion gracieuse** : Arrêt propre sur Ctrl+C
|
||||
|
||||
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};
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
/// Info serveur sérialisable
|
||||
#[derive(Clone, Serialize, utoipa::ToSchema)]
|
||||
pub struct ServerInfo {
|
||||
/// Nom du serveur
|
||||
pub name: String,
|
||||
/// URL de base
|
||||
pub base_url: String,
|
||||
/// Port HTTP
|
||||
pub http_port: u16,
|
||||
}
|
||||
|
||||
/// Serveur principal
|
||||
pub struct Server {
|
||||
name: String,
|
||||
base_url: String,
|
||||
http_port: u16,
|
||||
router: Arc<RwLock<Router>>,
|
||||
api_router: Arc<RwLock<Option<Router>>>,
|
||||
join_handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[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<String>, base_url: impl Into<String>, http_port: u16) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
base_url: base_url.into(),
|
||||
http_port,
|
||||
router: Arc::new(RwLock::new(Router::new())),
|
||||
api_router: Arc::new(RwLock::new(None)),
|
||||
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 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<F, Fut, T>(&mut self, path: &str, f: F)
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static,
|
||||
Fut: std::future::Future<Output = T> + 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
|
||||
///
|
||||
/// ```ignore
|
||||
/// 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>("/assets").await;
|
||||
/// // Les fichiers de static/ sont accessibles via /assets/*
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn add_dir<E>(&mut self, path: &str)
|
||||
where
|
||||
E: RustEmbed + Clone + Send + Sync + 'static,
|
||||
{
|
||||
let serve = ServeEmbed::<E>::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::<WebApp>("/").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<E>(&mut self, path: &str)
|
||||
where
|
||||
E: RustEmbed + Clone + Send + Sync + 'static,
|
||||
{
|
||||
let serve = ServeEmbed::<E>::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("<h1>Custom Response</h1>")
|
||||
/// }
|
||||
///
|
||||
/// server.add_handler("/custom", custom_handler).await;
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn add_handler<H, T>(&mut self, path: &str, handler: H)
|
||||
where
|
||||
H: Handler<T, ()>,
|
||||
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
|
||||
///
|
||||
/// ```ignore
|
||||
/// 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<String>
|
||||
/// }
|
||||
///
|
||||
/// impl LogState {
|
||||
/// fn subscribe(&self) -> broadcast::Receiver<String> {
|
||||
/// self.tx.subscribe()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// async fn log_sse(State(state): State<LogState>) -> Sse<impl futures::Stream<Item = Result<Event, std::convert::Infallible>>> {
|
||||
/// 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<H, T, S>(&mut self, path: &str, handler: H, state: S)
|
||||
where
|
||||
H: Handler<T, S>,
|
||||
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<H, T, S>(&mut self, path: &str, handler: H, state: S)
|
||||
where
|
||||
H: Handler<T, S>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ajoute une API documentée avec OpenAPI
|
||||
///
|
||||
/// Monte un routeur d'API sous `/api` et active Swagger UI sur `/swagger-ui`
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `api_router` - Router Axum contenant les routes API
|
||||
/// * `openapi` - Spécification OpenAPI générée par utoipa
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```ignore
|
||||
/// use utoipa::OpenApi;
|
||||
/// use axum::{Router, Json, routing::get};
|
||||
/// use serde::{Serialize, Deserialize};
|
||||
///
|
||||
/// #[derive(Serialize, Deserialize, utoipa::ToSchema)]
|
||||
/// struct User {
|
||||
/// id: u64,
|
||||
/// name: String,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(utoipa::OpenApi)]
|
||||
/// #[openapi(
|
||||
/// paths(get_users),
|
||||
/// components(schemas(User))
|
||||
/// )]
|
||||
/// struct ApiDoc;
|
||||
///
|
||||
/// #[utoipa::path(
|
||||
/// get,
|
||||
/// path = "/users",
|
||||
/// responses((status = 200, description = "List users"))
|
||||
/// )]
|
||||
/// async fn get_users() -> Json<Vec<User>> {
|
||||
/// Json(vec![])
|
||||
/// }
|
||||
///
|
||||
/// let api_router = Router::new()
|
||||
/// .route("/users", get(get_users));
|
||||
///
|
||||
/// server.add_openapi(api_router, ApiDoc::openapi()).await;
|
||||
/// ```
|
||||
pub async fn add_openapi(&mut self, api_router: Router, openapi: utoipa::openapi::OpenApi) {
|
||||
// Stocker le routeur API
|
||||
let mut api_r = self.api_router.write().await;
|
||||
*api_r = Some(api_router);
|
||||
|
||||
// Ajouter Swagger UI
|
||||
let swagger = SwaggerUi::new("/swagger-ui")
|
||||
.url("/api-docs/openapi.json", openapi);
|
||||
|
||||
let mut r = self.router.write().await;
|
||||
*r = std::mem::take(&mut *r).merge(swagger);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
// Merger le routeur API si présent
|
||||
let api_router = self.api_router.read().await;
|
||||
if let Some(api_r) = api_router.as_ref() {
|
||||
let mut r = self.router.write().await;
|
||||
*r = std::mem::take(&mut *r).nest("/api", api_r.clone());
|
||||
}
|
||||
drop(api_router);
|
||||
|
||||
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<String>, base_url: impl Into<String>, 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)
|
||||
}
|
||||
}
|
||||
94
pmoserver/src/upnp_impl.rs
Normal file
94
pmoserver/src/upnp_impl.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
//! Implémentation du trait UpnpServer pour le serveur pmoserver
|
||||
//!
|
||||
//! Ce module fournit l'implémentation du trait [`pmoupnp::UpnpServer`] pour
|
||||
//! le [`Server`](crate::server::Server) de pmoserver, permettant aux devices
|
||||
//! et services UPnP d'enregistrer automatiquement leurs endpoints HTTP.
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! L'implémentation fait le pont entre :
|
||||
//! - Les pointeurs de fonction du trait `UpnpServer` (agnostiques du framework web)
|
||||
//! - Les handlers Axum (spécifiques à l'implémentation `pmoserver`)
|
||||
//!
|
||||
//! Chaque méthode du trait crée un wrapper qui :
|
||||
//! 1. Convertit les pointeurs de fonction en closures compatibles Axum
|
||||
//! 2. Délègue l'enregistrement aux méthodes internes du `Server`
|
||||
//! 3. Retourne une future qui se résout une fois le handler enregistré
|
||||
//!
|
||||
//! ## Exemple d'utilisation
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmoupnp::{UpnpServer, mediarenderer::device::MEDIA_RENDERER};
|
||||
//! use pmoupnp::devices::DeviceInstance;
|
||||
//! use pmoserver::ServerBuilder;
|
||||
//! use std::sync::Arc;
|
||||
//!
|
||||
//! # async fn example() {
|
||||
//! let mut server = ServerBuilder::new("MyRenderer").build();
|
||||
//! let device = Arc::new(DeviceInstance::new(&MEDIA_RENDERER));
|
||||
//!
|
||||
//! // Le trait UpnpServer est automatiquement disponible
|
||||
//! device.register_urls(&mut server).await;
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use crate::server::Server;
|
||||
use pmoupnp::{UpnpServer, server::{Response, HeaderMap, Request}};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use axum::extract::State;
|
||||
|
||||
impl UpnpServer for Server {
|
||||
fn add_handler<F, Fut>(&mut self, path: &str, handler: F) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
|
||||
where
|
||||
F: Fn() -> Fut + Send + Sync + 'static + Clone,
|
||||
Fut: Future<Output = Response> + Send + 'static,
|
||||
{
|
||||
let path = path.to_string();
|
||||
Box::pin(async move {
|
||||
Self::add_handler(self, &path, handler).await;
|
||||
})
|
||||
}
|
||||
|
||||
fn add_post_handler_with_state<S>(
|
||||
&mut self,
|
||||
path: &str,
|
||||
handler: fn(State<S>, String) -> Pin<Box<dyn Future<Output = Response> + Send>>,
|
||||
state: S,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
let path = path.to_string();
|
||||
|
||||
// Créer un wrapper qui convertit le fn pointer en handler Axum
|
||||
let wrapper = move |State(s): State<S>, body: String| -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||
handler(State(s), body)
|
||||
};
|
||||
|
||||
Box::pin(async move {
|
||||
Self::add_post_handler_with_state(self, &path, wrapper, state).await;
|
||||
})
|
||||
}
|
||||
|
||||
fn add_handler_with_state<S>(
|
||||
&mut self,
|
||||
path: &str,
|
||||
handler: fn(State<S>, HeaderMap, Request) -> Pin<Box<dyn Future<Output = Response> + Send>>,
|
||||
state: S,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
let path = path.to_string();
|
||||
|
||||
// Créer un wrapper qui convertit le fn pointer en handler Axum
|
||||
let wrapper = move |State(s): State<S>, headers: HeaderMap, req: Request| -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||
handler(State(s), headers, req)
|
||||
};
|
||||
|
||||
Box::pin(async move {
|
||||
Self::add_handler_with_state(self, &path, wrapper, state).await;
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user