Sort le serveur dans une crate pmoserver
This commit is contained in:
133
Cargo.lock
generated
133
Cargo.lock
generated
@@ -8,6 +8,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"pmoconfig",
|
||||
"pmoserver",
|
||||
"pmoupnp",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -467,41 +468,6 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -1129,12 +1095,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
@@ -1511,41 +1471,50 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pmoupnp"
|
||||
name = "pmoserver"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
"axum",
|
||||
"axum-embed",
|
||||
"axum-server",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"pmoconfig",
|
||||
"pmoupnp",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pmoupnp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"base64",
|
||||
"bevy_reflect",
|
||||
"bevy_reflect_derive",
|
||||
"chrono",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"get_if_addrs",
|
||||
"hex",
|
||||
"log",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"pmoconfig",
|
||||
"pmodidl",
|
||||
"reqwest",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"validator",
|
||||
"xmltree",
|
||||
]
|
||||
|
||||
@@ -1580,28 +1549,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.101"
|
||||
@@ -2050,12 +1997,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -2492,36 +2433,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa"
|
||||
dependencies = [
|
||||
"idna",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"url",
|
||||
"validator_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "validator_derive"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"once_cell",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["PMOMusic", "pmoupnp","pmoconfig", "pmoutils", "pmodidl"]
|
||||
members = ["PMOMusic", "pmoupnp","pmoconfig", "pmoutils", "pmodidl", "pmoserver"]
|
||||
|
||||
@@ -6,6 +6,7 @@ edition = "2024"
|
||||
[dependencies]
|
||||
pmoconfig = { path = "../pmoconfig" }
|
||||
pmoupnp = { path = "../pmoupnp"}
|
||||
pmoserver = { path = "../pmoserver" }
|
||||
|
||||
|
||||
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time","signal"] }
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use pmoupnp::{mediarenderer::avtransport::{actions::SETAVTRANSPORTURI, AVTTRANSPORT}, server::{
|
||||
logs::{log_dump, log_sse, LogState, SseLayer}, ServerBuilder, Webapp
|
||||
}, UpnpObject}; // ton module pmoupnp::server
|
||||
use pmoupnp::{mediarenderer::avtransport::AVTTRANSPORT, UpnpObject};
|
||||
use pmoserver::{
|
||||
logs::{log_dump, log_sse, LogState, SseLayer},
|
||||
ServerBuilder, Webapp
|
||||
};
|
||||
use tracing_subscriber::Registry;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing::info;
|
||||
|
||||
27
pmoserver/Cargo.toml
Normal file
27
pmoserver/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "pmoserver"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
pmoconfig = { path = "../pmoconfig" }
|
||||
|
||||
axum = "0.8.4"
|
||||
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time", "signal"] }
|
||||
tokio-stream = "0.1"
|
||||
futures-util = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
||||
futures = "0.3"
|
||||
async-stream = "0.3.6"
|
||||
axum-server = "0.7.2"
|
||||
axum-embed = "0.1.0"
|
||||
rust-embed = "8.7.2"
|
||||
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||
|
||||
[dependencies.pmoupnp]
|
||||
path = "../pmoupnp"
|
||||
default-features = false
|
||||
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};
|
||||
@@ -13,8 +13,6 @@
|
||||
//! - 📚 **Documentation API** : OpenAPI/Swagger automatique avec `add_openapi()`
|
||||
//! - ⚡ **Gestion gracieuse** : Arrêt propre sur Ctrl+C
|
||||
|
||||
pub mod logs;
|
||||
|
||||
use axum::handler::Handler;
|
||||
use axum::response::Redirect;
|
||||
use axum::routing::get;
|
||||
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;
|
||||
})
|
||||
}
|
||||
}
|
||||
0
pmoserver/webapp/dist/.gitkeep
vendored
Normal file
0
pmoserver/webapp/dist/.gitkeep
vendored
Normal file
81
pmoserver/webapp/dist/assets/index-B3fd9aU-.js
vendored
Normal file
81
pmoserver/webapp/dist/assets/index-B3fd9aU-.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
pmoserver/webapp/dist/assets/index-Qw8Jzbm-.css
vendored
Normal file
1
pmoserver/webapp/dist/assets/index-Qw8Jzbm-.css
vendored
Normal file
File diff suppressed because one or more lines are too long
14
pmoserver/webapp/dist/index.html
vendored
Normal file
14
pmoserver/webapp/dist/index.html
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/app/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>webapp</title>
|
||||
<script type="module" crossorigin src="/app/assets/index-B3fd9aU-.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/app/assets/index-Qw8Jzbm-.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
1
pmoserver/webapp/dist/vite.svg
vendored
Normal file
1
pmoserver/webapp/dist/vite.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -13,28 +13,14 @@ hex = "0.4.3"
|
||||
base64 = "0.22.1"
|
||||
thiserror = "2.0.16"
|
||||
xmltree = "0.11.0"
|
||||
get_if_addrs = "0.5.3"
|
||||
axum = "0.8.4"
|
||||
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time", "signal"] }
|
||||
tokio-stream = "0.1"
|
||||
futures-util = "0.3"
|
||||
tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
log = "0.4.28"
|
||||
once_cell = "1.20"
|
||||
parking_lot = "0.12"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
||||
futures = "0.3"
|
||||
async-stream = "0.3.6"
|
||||
axum-server = "0.7.2"
|
||||
axum-embed = "0.1.0"
|
||||
rust-embed = "8.7.2"
|
||||
anyhow = "1.0"
|
||||
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
|
||||
validator = { version = "0.20.0", features = ["derive"] }
|
||||
bevy_reflect = "0.17.1"
|
||||
bevy_reflect_derive = "0.17.1"
|
||||
reqwest = "0.12.23"
|
||||
|
||||
@@ -245,7 +245,7 @@ impl DeviceInstance {
|
||||
}
|
||||
|
||||
/// Enregistre toutes les URLs du device et de ses services dans le serveur.
|
||||
pub fn register_urls<'a>(&'a self, server: &'a mut crate::server::Server) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), DeviceError>> + 'a>> {
|
||||
pub fn register_urls<'a, S: crate::UpnpServer + ?Sized>(&'a self, server: &'a mut S) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), DeviceError>> + 'a>> {
|
||||
Box::pin(async move {
|
||||
info!(
|
||||
"✅ Device description for {} available at: {}{}",
|
||||
|
||||
@@ -10,6 +10,9 @@ pub mod state_variables;
|
||||
pub mod value_ranges;
|
||||
pub mod variable_types;
|
||||
|
||||
// Re-exports
|
||||
pub use server::UpnpServer;
|
||||
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
|
||||
262
pmoupnp/src/server.rs
Normal file
262
pmoupnp/src/server.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
//! Trait pour les serveurs UPnP
|
||||
//!
|
||||
//! Ce module définit le trait [`UpnpServer`] qui permet de connecter
|
||||
//! des devices UPnP à n'importe quelle implémentation de serveur web.
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! Le trait `UpnpServer` définit une interface minimale permettant aux devices
|
||||
//! et services UPnP d'enregistrer leurs endpoints HTTP sans dépendre d'une
|
||||
//! implémentation de serveur spécifique.
|
||||
//!
|
||||
//! ## Séparation des responsabilités
|
||||
//!
|
||||
//! - **pmoupnp** : Définit le trait `UpnpServer` et l'utilise via des contraintes génériques
|
||||
//! - **pmoserver** : Fournit une implémentation concrète basée sur Axum
|
||||
//! - **Autres crates** : Peuvent fournir leurs propres implémentations (actix-web, warp, etc.)
|
||||
//!
|
||||
//! ## Exemple d'utilisation
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmoupnp::{UpnpServer, devices::{Device, DeviceInstance}};
|
||||
//! use std::sync::Arc;
|
||||
//!
|
||||
//! # async fn example<S: UpnpServer>(mut server: S) {
|
||||
//! // Créer un device
|
||||
//! let device = Device::new(
|
||||
//! "MyDevice".to_string(),
|
||||
//! "MyDeviceType".to_string(),
|
||||
//! "Friendly Name".to_string(),
|
||||
//! );
|
||||
//! let device_instance = Arc::new(DeviceInstance::new(&device));
|
||||
//!
|
||||
//! // Le device enregistre automatiquement ses routes UPnP
|
||||
//! device_instance.register_urls(&mut server).await;
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Implémentation
|
||||
//!
|
||||
//! Pour implémenter ce trait, votre serveur doit fournir trois méthodes
|
||||
//! pour enregistrer des handlers HTTP asynchrones :
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmoupnp::UpnpServer;
|
||||
//! use std::future::Future;
|
||||
//! use std::pin::Pin;
|
||||
//!
|
||||
//! struct MyServer {
|
||||
//! // votre implémentation
|
||||
//! }
|
||||
//!
|
||||
//! impl UpnpServer for MyServer {
|
||||
//! 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 = pmoupnp::server::Response> + Send + 'static,
|
||||
//! {
|
||||
//! // Enregistrer le handler pour GET requests
|
||||
//! # todo!()
|
||||
//! }
|
||||
//!
|
||||
//! fn add_post_handler_with_state<S>(
|
||||
//! &mut self,
|
||||
//! path: &str,
|
||||
//! handler: fn(axum::extract::State<S>, String)
|
||||
//! -> Pin<Box<dyn Future<Output = pmoupnp::server::Response> + Send>>,
|
||||
//! state: S,
|
||||
//! ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
|
||||
//! where
|
||||
//! S: Clone + Send + Sync + 'static,
|
||||
//! {
|
||||
//! // Enregistrer le handler pour POST avec body
|
||||
//! # todo!()
|
||||
//! }
|
||||
//!
|
||||
//! fn add_handler_with_state<S>(
|
||||
//! &mut self,
|
||||
//! path: &str,
|
||||
//! handler: fn(axum::extract::State<S>,
|
||||
//! pmoupnp::server::HeaderMap,
|
||||
//! pmoupnp::server::Request)
|
||||
//! -> Pin<Box<dyn Future<Output = pmoupnp::server::Response> + Send>>,
|
||||
//! state: S,
|
||||
//! ) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
|
||||
//! where
|
||||
//! S: Clone + Send + Sync + 'static,
|
||||
//! {
|
||||
//! // Enregistrer le handler avec accès complet à la requête
|
||||
//! # todo!()
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// Type alias pour la réponse HTTP (basé sur Axum).
|
||||
///
|
||||
/// Utilisé pour éviter une dépendance directe sur axum dans les signatures de trait,
|
||||
/// tout en restant compatible avec les types Axum.
|
||||
pub type Response = axum::response::Response;
|
||||
|
||||
/// Type alias pour les en-têtes HTTP (basé sur Axum).
|
||||
pub type HeaderMap = axum::http::HeaderMap;
|
||||
|
||||
/// Type alias pour la requête HTTP (basé sur Axum).
|
||||
pub type Request = axum::extract::Request<axum::body::Body>;
|
||||
|
||||
/// Trait pour les serveurs compatibles UPnP.
|
||||
///
|
||||
/// Ce trait définit l'interface minimale qu'un serveur web doit implémenter
|
||||
/// pour supporter l'enregistrement automatique des endpoints UPnP par les
|
||||
/// [`DeviceInstance`](crate::devices::DeviceInstance) et
|
||||
/// [`ServiceInstance`](crate::services::ServiceInstance).
|
||||
///
|
||||
/// ## Contraintes
|
||||
///
|
||||
/// - `Send + Sync` : Le serveur doit être partageable entre threads
|
||||
///
|
||||
/// ## Méthodes
|
||||
///
|
||||
/// Les trois méthodes permettent d'enregistrer différents types de handlers :
|
||||
///
|
||||
/// 1. **`add_handler`** : Handler GET simple sans état
|
||||
/// 2. **`add_post_handler_with_state`** : Handler POST avec état et body texte (pour SOAP)
|
||||
/// 3. **`add_handler_with_state`** : Handler générique avec accès complet (pour SUBSCRIBE/UNSUBSCRIBE)
|
||||
///
|
||||
/// ## Implémentations
|
||||
///
|
||||
/// - **pmoserver::Server** : Implémentation basée sur Axum (fournie par la crate `pmoserver`)
|
||||
pub trait UpnpServer: Send + Sync {
|
||||
/// Ajoute un handler GET pour un chemin donné.
|
||||
///
|
||||
/// Utilisé principalement pour servir les descripteurs XML des devices et services.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Le chemin HTTP (ex: `/device/MediaRenderer/description.xml`)
|
||||
/// * `handler` - Une closure asynchrone qui génère la réponse
|
||||
///
|
||||
/// # Retour
|
||||
///
|
||||
/// Une future qui se résout quand le handler est enregistré.
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pmoupnp::UpnpServer;
|
||||
/// use axum::response::IntoResponse;
|
||||
///
|
||||
/// # async fn example<S: UpnpServer>(mut server: S) {
|
||||
/// server.add_handler("/description.xml", || async {
|
||||
/// "<?xml version=\"1.0\"?><root></root>".into_response()
|
||||
/// }).await;
|
||||
/// # }
|
||||
/// ```
|
||||
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;
|
||||
|
||||
/// Ajoute un handler POST avec état pour un chemin donné.
|
||||
///
|
||||
/// Utilisé pour les endpoints de contrôle SOAP des services UPnP.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Le chemin HTTP (ex: `/service/AVTransport/control`)
|
||||
/// * `handler` - Un pointeur de fonction qui traite la requête SOAP
|
||||
/// * `state` - L'état partagé (typiquement une `ServiceInstance`)
|
||||
///
|
||||
/// # Retour
|
||||
///
|
||||
/// Une future qui se résout quand le handler est enregistré.
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pmoupnp::{UpnpServer, server::Response};
|
||||
/// use axum::extract::State;
|
||||
/// use std::pin::Pin;
|
||||
/// use std::future::Future;
|
||||
///
|
||||
/// fn soap_handler(
|
||||
/// State(service): State<String>,
|
||||
/// body: String,
|
||||
/// ) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||
/// Box::pin(async move {
|
||||
/// // Traiter la requête SOAP
|
||||
/// axum::response::Response::default()
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// # async fn example<S: UpnpServer>(mut server: S) {
|
||||
/// server.add_post_handler_with_state(
|
||||
/// "/control",
|
||||
/// soap_handler,
|
||||
/// "ServiceName".to_string(),
|
||||
/// ).await;
|
||||
/// # }
|
||||
/// ```
|
||||
fn add_post_handler_with_state<S>(
|
||||
&mut self,
|
||||
path: &str,
|
||||
handler: fn(axum::extract::State<S>, String) -> Pin<Box<dyn Future<Output = Response> + Send>>,
|
||||
state: S,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>
|
||||
where
|
||||
S: Clone + Send + Sync + 'static;
|
||||
|
||||
/// Ajoute un handler avec état et accès complet à la requête.
|
||||
///
|
||||
/// Utilisé pour les endpoints d'événements (SUBSCRIBE/UNSUBSCRIBE) qui nécessitent
|
||||
/// un accès aux en-têtes HTTP et à la méthode HTTP.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Le chemin HTTP (ex: `/service/AVTransport/event`)
|
||||
/// * `handler` - Un pointeur de fonction avec accès complet à la requête
|
||||
/// * `state` - L'état partagé (typiquement une `ServiceInstance`)
|
||||
///
|
||||
/// # Retour
|
||||
///
|
||||
/// Une future qui se résout quand le handler est enregistré.
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pmoupnp::{UpnpServer, server::{Response, HeaderMap, Request}};
|
||||
/// use axum::extract::State;
|
||||
/// use std::pin::Pin;
|
||||
/// use std::future::Future;
|
||||
///
|
||||
/// fn event_handler(
|
||||
/// State(service): State<String>,
|
||||
/// headers: HeaderMap,
|
||||
/// req: Request,
|
||||
/// ) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||
/// Box::pin(async move {
|
||||
/// // Traiter SUBSCRIBE/UNSUBSCRIBE
|
||||
/// axum::response::Response::default()
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// # async fn example<S: UpnpServer>(mut server: S) {
|
||||
/// server.add_handler_with_state(
|
||||
/// "/event",
|
||||
/// event_handler,
|
||||
/// "ServiceName".to_string(),
|
||||
/// ).await;
|
||||
/// # }
|
||||
/// ```
|
||||
fn add_handler_with_state<S>(
|
||||
&mut self,
|
||||
path: &str,
|
||||
handler: fn(axum::extract::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;
|
||||
}
|
||||
@@ -4,6 +4,8 @@ use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
time::Duration,
|
||||
pin::Pin,
|
||||
future::Future,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
@@ -297,12 +299,12 @@ impl ServiceInstance {
|
||||
&self.actions
|
||||
}
|
||||
|
||||
/// Enregistre les routes UPnP dans le serveur Axum.
|
||||
/// Enregistre les routes UPnP dans le serveur.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Retourne une erreur si l'enregistrement des routes échoue.
|
||||
pub async fn register_urls(&self, server: &mut crate::server::Server) -> Result<(), ServiceError> {
|
||||
pub async fn register_urls<S: crate::UpnpServer + ?Sized>(&self, server: &mut S) -> Result<(), ServiceError> {
|
||||
let device = self.device.read().unwrap();
|
||||
let device_name = device.as_ref().map(|d| d.get_name().clone()).unwrap_or_else(|| "unknown".to_string());
|
||||
let server_url = device.as_ref().map(|d| d.base_url().to_string()).unwrap_or_default();
|
||||
@@ -566,11 +568,12 @@ impl ServiceInstance {
|
||||
}
|
||||
|
||||
/// Handler Axum pour les événements (SUBSCRIBE/UNSUBSCRIBE).
|
||||
async fn event_sub_handler(
|
||||
fn event_sub_handler(
|
||||
State(instance): State<ServiceInstance>,
|
||||
headers: HeaderMap,
|
||||
req: Request<Body>,
|
||||
) -> Response {
|
||||
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||
Box::pin(async move {
|
||||
info!("📡 Event Subscription request for {}", instance.get_name());
|
||||
|
||||
let method = req.method().as_str();
|
||||
@@ -633,13 +636,15 @@ async fn event_sub_handler(
|
||||
StatusCode::METHOD_NOT_ALLOWED.into_response()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Handler Axum pour le contrôle SOAP.
|
||||
async fn control_handler(
|
||||
fn control_handler(
|
||||
State(instance): State<ServiceInstance>,
|
||||
body: String,
|
||||
) -> Response {
|
||||
_body: String,
|
||||
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||
Box::pin(async move {
|
||||
info!("📡 Control request for {}", instance.get_name());
|
||||
|
||||
// TODO: Parser le SOAP et appeler l'action correspondante
|
||||
@@ -661,6 +666,7 @@ async fn control_handler(
|
||||
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")],
|
||||
response_xml,
|
||||
).into_response()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user