Files
pmomusic/pmocovers/src/pmoserver_impl.rs
2025-10-07 11:41:10 +02:00

167 lines
5.3 KiB
Rust

//! Implémentation du trait CoverCacheExt pour le serveur pmoserver
//!
//! Ce module enrichit `pmoserver::Server` avec les fonctionnalités de cache d'images en
//! implémentant le trait [`CoverCacheExt`](crate::CoverCacheExt). Cette implémentation
//! permet d'initialiser facilement le cache et d'enregistrer les routes HTTP.
//!
//! ## Architecture
//!
//! `pmocovers` étend `pmoserver::Server` sans que `pmoserver` connaisse `pmocovers`.
//! C'est le pattern d'extension : `pmocovers` ajoute des fonctionnalités à un type
//! externe via un trait, similaire au pattern utilisé par `pmoapp` pour `WebAppExt`.
//!
//! ## Exemple d'utilisation
//!
//! ```rust,no_run
//! use pmocovers::CoverCacheExt;
//! use pmoserver::ServerBuilder;
//!
//! # async fn example() -> anyhow::Result<()> {
//! let mut server = ServerBuilder::new("MyApp", "http://localhost:3000", 3000).build();
//!
//! // Le trait CoverCacheExt est automatiquement disponible
//! let cache = server.init_cover_cache("./cache", 1000).await?;
//!
//! server.start().await;
//! # Ok(())
//! # }
//! ```
use crate::{api, Cache, CoverCacheExt};
use axum::{
body::Body,
extract::State,
http::{Request, StatusCode},
response::{IntoResponse, Response},
routing::{delete, get, post},
Json, Router,
};
use pmoserver::Server;
use tracing::{debug, info};
use std::sync::Arc;
use utoipa::OpenApi;
/// Handler pour GET /covers/images/{pk}
async fn get_cover_image(
State(cache): State<Arc<Cache>>,
req: Request<Body>,
) -> Response {
// Extraire pk du path
let path = req.uri().path();
let parts: Vec<&str> = path.split('/').collect();
if parts.len() < 4 {
return (StatusCode::BAD_REQUEST, "Invalid path").into_response();
}
let pk = parts[3];
match cache.get(pk).await {
Ok(file_path) => {
match tokio::fs::read(&file_path).await {
Ok(data) => (
StatusCode::OK,
[("content-type", "image/webp")],
data,
)
.into_response(),
Err(_) => (StatusCode::NOT_FOUND, "File not found").into_response(),
}
}
Err(_) => (StatusCode::NOT_FOUND, "Image not found").into_response(),
}
}
/// Handler pour GET /covers/images/{pk}/{size}
async fn get_cover_variant(
State(cache): State<Arc<Cache>>,
req: Request<Body>,
) -> Response {
// Extraire pk et size du path
let path = req.uri().path();
let parts: Vec<&str> = path.split('/').collect();
if parts.len() < 5 {
return (StatusCode::BAD_REQUEST, "Invalid path").into_response();
}
let pk = parts[3];
let size = match parts[4].parse::<usize>() {
Ok(s) => s,
Err(_) => return (StatusCode::BAD_REQUEST, "Invalid size").into_response(),
};
match crate::webp::generate_variant(&cache, pk, size).await {
Ok(data) => (
StatusCode::OK,
[("content-type", "image/webp")],
data,
)
.into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Cannot generate variant").into_response(),
}
}
/// Handler pour GET /covers/stats
async fn get_cover_stats(State(cache): State<Arc<Cache>>) -> Response {
match cache.db.get_all() {
Ok(entries) => Json(entries).into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Cannot retrieve stats").into_response(),
}
}
impl CoverCacheExt for Server {
async fn init_cover_cache(&mut self, cache_dir: &str, limit: usize) -> anyhow::Result<Arc<Cache>> {
let cache = Arc::new(Cache::new(cache_dir, limit)?);
// Enregistrer les routes HTTP classiques
self.add_handler_with_state("/covers/images", get_cover_image, cache.clone()).await;
self.add_handler_with_state("/covers/stats", get_cover_stats, cache.clone()).await;
// Router API RESTful
// Router API RESTful monté sur /api/covers
let api_router = Router::new()
// Liste et ajout
.route(
"/images/",
get(api::list_images) // GET /api/covers
.post(api::add_image) // POST /api/covers
.delete(api::purge_cache), // DELETE /api/covers
)
// Ressource unique
.route(
"/images/{pk}",
get(api::get_image_info) // GET /api/covers/{pk}
.delete(api::delete_image), // DELETE /api/covers/{pk}
)
// Action spécifique
.route(
"/images/consolidate",
post(api::consolidate_cache), // POST /api/covers/consolidate
)
.with_state(cache.clone());
// Documentation OpenAPI via Utoipa
let openapi = crate::ApiDoc::openapi();
// Enregistrer l'API avec Swagger UI
// /api/covers/images... et /swagger-ui/covers
self.add_openapi(api_router, openapi, "covers").await;
Ok(cache)
}
async fn init_cover_cache_configured(&mut self) -> anyhow::Result<Arc<Cache>> {
let config = pmoconfig::get_config();
let cache_dir = config.get_cover_cache_dir()?;
let limit = config.get_cover_cache_size()?;
info!("cache directory {}, size {}",cache_dir,limit);
self.init_cover_cache(&cache_dir, limit).await
}
}