|
|
|
|
@@ -16,13 +16,15 @@
|
|
|
|
|
use crate::logs::{LogState, LoggingOptions, init_logging, log_dump, log_sse};
|
|
|
|
|
use axum::handler::Handler;
|
|
|
|
|
use axum::response::Redirect;
|
|
|
|
|
use axum::routing::get;
|
|
|
|
|
use axum::routing::{get, post};
|
|
|
|
|
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 std::future::Future;
|
|
|
|
|
use std::net::SocketAddr;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use tokio::{signal, sync::RwLock, task::JoinHandle};
|
|
|
|
|
use tracing::info;
|
|
|
|
|
use utoipa_swagger_ui::SwaggerUi;
|
|
|
|
|
@@ -30,11 +32,8 @@ 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,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -80,8 +79,7 @@ impl Server {
|
|
|
|
|
let config = get_config();
|
|
|
|
|
let url = config.get_base_url();
|
|
|
|
|
let port = config.get_http_port();
|
|
|
|
|
|
|
|
|
|
return Self::new("PMO-Music-Server", url, port);
|
|
|
|
|
Self::new("PMO-Music-Server", url, port)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ajoute une route JSON dynamique
|
|
|
|
|
@@ -112,11 +110,10 @@ impl Server {
|
|
|
|
|
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,
|
|
|
|
|
Fut: Future<Output = T> + Send + 'static,
|
|
|
|
|
T: Serialize + Send + 'static,
|
|
|
|
|
{
|
|
|
|
|
let f = Arc::new(f);
|
|
|
|
|
|
|
|
|
|
let handler = {
|
|
|
|
|
let f = f.clone();
|
|
|
|
|
move || {
|
|
|
|
|
@@ -128,76 +125,81 @@ impl Server {
|
|
|
|
|
let route = Router::new().route("/", get(handler));
|
|
|
|
|
|
|
|
|
|
let mut r = self.router.write().await;
|
|
|
|
|
*r = std::mem::take(&mut *r).nest(path, route);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add a new router safely:
|
|
|
|
|
/// - If `path` starts with '/', it is merged at root level.
|
|
|
|
|
/// - Otherwise, it is nested under the given subpath.
|
|
|
|
|
pub async fn add_router(&mut self, path: &str, route: Router) {
|
|
|
|
|
let mut r = self.router.write().await;
|
|
|
|
|
|
|
|
|
|
// Take current router without losing content
|
|
|
|
|
let current = std::mem::take(&mut *r);
|
|
|
|
|
|
|
|
|
|
let combined = if path.starts_with('/') {
|
|
|
|
|
// Absolute path => merge directly at root
|
|
|
|
|
tracing::debug!("Merging router at root path: {}", path);
|
|
|
|
|
current.merge(route)
|
|
|
|
|
*r = if path == "/" {
|
|
|
|
|
std::mem::take(&mut *r).merge(route)
|
|
|
|
|
} else {
|
|
|
|
|
// Relative path => nest under the given path
|
|
|
|
|
let normalized = format!("/{}", path.trim_start_matches('/'));
|
|
|
|
|
tracing::debug!("Nesting router under: {}", normalized);
|
|
|
|
|
current.nest(&normalized, route)
|
|
|
|
|
std::mem::take(&mut *r).nest(path, route)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
*r = combined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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/*
|
|
|
|
|
/// # }
|
|
|
|
|
/// ```
|
|
|
|
|
|
|
|
|
|
/// Ajoute un handler Axum standard
|
|
|
|
|
pub async fn add_handler<H, T>(&mut self, path: &str, handler: H)
|
|
|
|
|
where
|
|
|
|
|
H: Handler<T, ()> + Clone + 'static,
|
|
|
|
|
T: 'static,
|
|
|
|
|
{
|
|
|
|
|
let route = Router::new().route("/", get(handler.clone()));
|
|
|
|
|
|
|
|
|
|
let mut r = self.router.write().await;
|
|
|
|
|
*r = if path == "/" {
|
|
|
|
|
std::mem::take(&mut *r).merge(route)
|
|
|
|
|
} else {
|
|
|
|
|
std::mem::take(&mut *r).nest(path, route)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ajoute un handler POST avec état
|
|
|
|
|
pub async fn add_post_handler_with_state<H, T, S>(&mut self, path: &str, handler: H, state: S)
|
|
|
|
|
where
|
|
|
|
|
H: Handler<T, S> + Clone + 'static,
|
|
|
|
|
T: 'static,
|
|
|
|
|
S: Clone + Send + Sync + 'static,
|
|
|
|
|
{
|
|
|
|
|
let route = Router::new()
|
|
|
|
|
.route("/", post(handler.clone()))
|
|
|
|
|
.with_state(state.clone());
|
|
|
|
|
|
|
|
|
|
let mut r = self.router.write().await;
|
|
|
|
|
*r = if path == "/" {
|
|
|
|
|
std::mem::take(&mut *r).merge(route)
|
|
|
|
|
} else {
|
|
|
|
|
std::mem::take(&mut *r).nest(path, route)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ajoute un handler avec état
|
|
|
|
|
pub async fn add_handler_with_state<H, T, S>(&mut self, path: &str, handler: H, state: S)
|
|
|
|
|
where
|
|
|
|
|
H: Handler<T, S> + Clone + 'static,
|
|
|
|
|
T: 'static,
|
|
|
|
|
S: Clone + Send + Sync + 'static,
|
|
|
|
|
{
|
|
|
|
|
let route = Router::new()
|
|
|
|
|
.route("/", get(handler.clone()))
|
|
|
|
|
.with_state(state.clone());
|
|
|
|
|
|
|
|
|
|
let mut r = self.router.write().await;
|
|
|
|
|
*r = if path == "/" {
|
|
|
|
|
std::mem::take(&mut *r).merge(route)
|
|
|
|
|
} else {
|
|
|
|
|
std::mem::take(&mut *r).nest(path, route)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ajoute un répertoire statique
|
|
|
|
|
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);
|
|
|
|
|
let route = Router::new().fallback_service(serve);
|
|
|
|
|
*r = if path == "/" {
|
|
|
|
|
std::mem::take(&mut *r).merge(route)
|
|
|
|
|
} else {
|
|
|
|
|
let route = Router::new().fallback_service(serve);
|
|
|
|
|
*r = std::mem::take(&mut *r).nest(path, route);
|
|
|
|
|
}
|
|
|
|
|
std::mem::take(&mut *r).nest(path, route)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ajoute une Single Page Application (SPA)
|
|
|
|
|
@@ -252,125 +254,12 @@ impl Server {
|
|
|
|
|
|
|
|
|
|
let mut r = self.router.write().await;
|
|
|
|
|
|
|
|
|
|
if path == "/" {
|
|
|
|
|
*r = std::mem::take(&mut *r).fallback_service(serve);
|
|
|
|
|
let route = Router::new().fallback_service(serve);
|
|
|
|
|
*r = if path == "/" {
|
|
|
|
|
std::mem::take(&mut *r).merge(route)
|
|
|
|
|
} 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);
|
|
|
|
|
std::mem::take(&mut *r).nest(path, route)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ajoute une redirection HTTP
|
|
|
|
|
@@ -393,23 +282,20 @@ impl Server {
|
|
|
|
|
/// 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 make_handler = || {
|
|
|
|
|
let target = to.clone();
|
|
|
|
|
get(move || async move { Redirect::permanent(&target) })
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
*r = if from == "/" {
|
|
|
|
|
std::mem::take(&mut *r).merge(Router::new().route("/", make_handler()))
|
|
|
|
|
} else {
|
|
|
|
|
let route = Router::new().route("/", get(handler));
|
|
|
|
|
*r = std::mem::take(&mut *r).nest(from, route);
|
|
|
|
|
}
|
|
|
|
|
std::mem::take(&mut *r).nest(from, Router::new().route("/", make_handler()))
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ajoute une API documentée avec OpenAPI et Swagger UI
|
|
|
|
|
@@ -481,21 +367,16 @@ impl Server {
|
|
|
|
|
/// - `/api/api1/users` et `/api/api2/products` sont accessibles via Axum.
|
|
|
|
|
/// - `/swagger-ui/api1` et `/swagger-ui/api2` affichent la documentation Swagger correspondante.
|
|
|
|
|
/// - `/api-docs/api1.json` et `/api-docs/api2.json` fournissent les spécifications OpenAPI respectives.
|
|
|
|
|
|
|
|
|
|
pub async fn add_openapi(
|
|
|
|
|
&mut self,
|
|
|
|
|
api_router: Router,
|
|
|
|
|
openapi: utoipa::openapi::OpenApi,
|
|
|
|
|
name: &str, // nom unique pour différencier Swagger et OpenAPI
|
|
|
|
|
name: &str,
|
|
|
|
|
) {
|
|
|
|
|
use axum::routing::get;
|
|
|
|
|
use utoipa_swagger_ui::SwaggerUi;
|
|
|
|
|
|
|
|
|
|
// Stocker le router API dans self.api_router si tu veux y accéder plus tard
|
|
|
|
|
let mut api_r = self.api_router.write().await;
|
|
|
|
|
*api_r = Some(api_router.clone());
|
|
|
|
|
drop(api_r);
|
|
|
|
|
|
|
|
|
|
// Générer des chemins uniques pour Swagger UI et OpenAPI JSON
|
|
|
|
|
let swagger_path = format!("/swagger-ui/{}", name);
|
|
|
|
|
let swagger_path_static: &'static str = Box::leak(swagger_path.into_boxed_str());
|
|
|
|
|
|
|
|
|
|
@@ -504,16 +385,31 @@ impl Server {
|
|
|
|
|
|
|
|
|
|
let swagger = SwaggerUi::new(swagger_path_static).url(openapi_json_path_static, openapi);
|
|
|
|
|
|
|
|
|
|
// Nester le router API sous /api/{name}
|
|
|
|
|
let base_path = format!("/api/{}", name);
|
|
|
|
|
let nested_router = Router::new().nest(&base_path, api_router);
|
|
|
|
|
|
|
|
|
|
// Fusionner avec le router principal
|
|
|
|
|
let mut r = self.router.write().await;
|
|
|
|
|
let mut combined = std::mem::take(&mut *r);
|
|
|
|
|
combined = combined.merge(nested_router).merge(swagger);
|
|
|
|
|
*r = std::mem::take(&mut *r).merge(nested_router).merge(swagger);
|
|
|
|
|
}
|
|
|
|
|
/// Ajoute un sous-router au serveur
|
|
|
|
|
///
|
|
|
|
|
/// - Si `path` est "/", merge directement au router principal
|
|
|
|
|
/// - Sinon, nest le router sous le chemin donné
|
|
|
|
|
pub async fn add_router(&mut self, path: &str, sub_router: Router) {
|
|
|
|
|
let mut r = self.router.write().await;
|
|
|
|
|
|
|
|
|
|
let combined = if path == "/" {
|
|
|
|
|
// Merge directement à la racine
|
|
|
|
|
r.clone().merge(sub_router)
|
|
|
|
|
} else {
|
|
|
|
|
// Sous-chemin => nest
|
|
|
|
|
let normalized = format!("/{}", path.trim_start_matches('/'));
|
|
|
|
|
r.clone().nest(&normalized, sub_router)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
*r = combined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Démarre le serveur HTTP
|
|
|
|
|
///
|
|
|
|
|
/// Lance le serveur sur le port configuré et met en place la gestion
|
|
|
|
|
@@ -537,16 +433,7 @@ impl Server {
|
|
|
|
|
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();
|
|
|
|
|
|