//! 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>, req: Request, ) -> 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>, req: Request, ) -> 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::() { 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>) -> 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> { 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> { 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 } }