Ajout de la capacité à épingler des items pour les protéger de l'éviction LRU et à définir un TTL pour l'expiration automatique des items temporaires. Cette implémentation inclut : - Ajout de colonnes `pinned` et `ttl_expires_at` dans la base de données - Nouvelles méthodes dans DB et Cache pour gérer le pinning et le TTL - Modification de la politique d'éviction pour exclure les items épinglés - Implémentation d'une règle métier interdisant le pinning et le TTL simultanément - API REST complète avec endpoints GET/POST/DELETE pour gérer le pinning et le TTL - Documentation OpenAPI automatique - Tests complets couvrant tous les cas d'usage Les items épinglés ne comptent pas dans la limite du cache et ne peuvent jamais être supprimés automatiquement, tandis que les items avec TTL sont supprimés automatiquement à l'expiration.
599 lines
20 KiB
Rust
599 lines
20 KiB
Rust
//! Extension pmoserver pour servir les fichiers du cache via HTTP
|
|
//!
|
|
//! Ce module fournit des handlers génériques pour servir les fichiers
|
|
//! d'un cache via des routes HTTP structurées, avec support du streaming progressif.
|
|
//!
|
|
//! ## Routes générées
|
|
//!
|
|
//! Format: `/{cache_name}/{cache_type}/{pk}[/{param}]`
|
|
//!
|
|
//! Exemples:
|
|
//! - `/covers/images/abc123` - Image avec param par défaut (orig)
|
|
//! - `/covers/images/abc123/256` - Image redimensionnée 256x256
|
|
//! - `/audio/tracks/def456` - Piste audio par défaut
|
|
//! - `/audio/tracks/def456/stream` - Piste audio streamable
|
|
//!
|
|
//! ## Streaming progressif
|
|
//!
|
|
//! Les fichiers en cours de téléchargement sont automatiquement streamés
|
|
//! au fur et à mesure de leur disponibilité.
|
|
//!
|
|
//! ## Utilisation
|
|
//!
|
|
//! ```rust,no_run
|
|
//! use pmocache::pmoserver_ext;
|
|
//! use axum::Router;
|
|
//!
|
|
//! # async fn example(cache: std::sync::Arc<pmocache::Cache<CoversConfig>>) {
|
|
//! // Créer un router pour servir les fichiers
|
|
//! let router = pmoserver_ext::create_file_router(
|
|
//! cache.clone(),
|
|
//! "image/webp" // Content-Type
|
|
//! );
|
|
//!
|
|
//! // Le router sera monté à la racine avec les routes complètes
|
|
//! // Exemple: GET /covers/images/{pk}
|
|
//! // GET /covers/images/{pk}/{param}
|
|
//! # }
|
|
//! ```
|
|
|
|
#[cfg(feature = "pmoserver")]
|
|
use crate::{Cache, CacheConfig};
|
|
#[cfg(feature = "pmoserver")]
|
|
use axum::{
|
|
body::Body,
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
routing::{get, post},
|
|
Router,
|
|
};
|
|
#[cfg(feature = "pmoserver")]
|
|
use std::future::Future;
|
|
#[cfg(feature = "pmoserver")]
|
|
use std::pin::Pin;
|
|
#[cfg(feature = "pmoserver")]
|
|
use std::sync::Arc;
|
|
#[cfg(feature = "pmoserver")]
|
|
use tokio_util::io::ReaderStream;
|
|
#[cfg(feature = "pmoserver")]
|
|
use tracing::warn;
|
|
|
|
/// Type pour le callback de génération de param
|
|
///
|
|
/// Appelé quand un fichier avec param n'existe pas.
|
|
/// Permet de générer à la volée (ex: redimensionnement d'images).
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// - `cache`: le cache
|
|
/// - `pk`: clé primaire
|
|
/// - `param`: paramètre demandé (ex: "256" pour une taille)
|
|
///
|
|
/// # Retourne
|
|
///
|
|
/// Les données générées ou None si le param n'est pas supporté
|
|
#[cfg(feature = "pmoserver")]
|
|
pub type ParamGenerator<C> = Arc<
|
|
dyn Fn(Arc<Cache<C>>, String, String) -> Pin<Box<dyn Future<Output = Option<Vec<u8>>> + Send>>
|
|
+ Send
|
|
+ Sync,
|
|
>;
|
|
|
|
/// Handler générique pour GET /{cache_name}/{cache_type}/{pk}
|
|
/// Sert un fichier avec le param par défaut
|
|
#[cfg(feature = "pmoserver")]
|
|
async fn get_file<C: CacheConfig + 'static>(
|
|
State((cache, content_type, param_generator)): State<(
|
|
Arc<Cache<C>>,
|
|
&'static str,
|
|
Option<ParamGenerator<C>>,
|
|
)>,
|
|
Path(pk): Path<String>,
|
|
request: axum::http::Request<Body>,
|
|
) -> Response {
|
|
// Utiliser le param par défaut
|
|
let param = C::default_param();
|
|
serve_file_with_streaming(&cache, &pk, param, content_type, param_generator, request).await
|
|
}
|
|
|
|
/// Handler générique pour GET /{cache_name}/{cache_type}/{pk}/{param}
|
|
/// Sert un fichier avec un param spécifique
|
|
#[cfg(feature = "pmoserver")]
|
|
async fn get_file_with_param<C: CacheConfig + 'static>(
|
|
State((cache, content_type, param_generator)): State<(
|
|
Arc<Cache<C>>,
|
|
&'static str,
|
|
Option<ParamGenerator<C>>,
|
|
)>,
|
|
Path((pk, param)): Path<(String, String)>,
|
|
request: axum::http::Request<Body>,
|
|
) -> Response {
|
|
serve_file_with_streaming(&cache, &pk, ¶m, content_type, param_generator, request).await
|
|
}
|
|
|
|
#[cfg(feature = "pmoserver")]
|
|
async fn serve_finalized_pk<C: CacheConfig + 'static>(
|
|
cache: &Arc<Cache<C>>,
|
|
pk: &str,
|
|
param: &str,
|
|
content_type: &'static str,
|
|
request: axum::http::Request<Body>,
|
|
) -> Response {
|
|
let qualifier = param.to_string();
|
|
let file_path = cache.get_file_path_with_qualifier(pk, param);
|
|
|
|
if let Err(e) = cache.db.update_hit(pk) {
|
|
warn!("Error updating hit count for {}: {}", pk, e);
|
|
}
|
|
|
|
// ROUTE 1 : Fichier en cours de téléchargement
|
|
// Utilise le streaming progressif avec Content-Length pour permettre
|
|
// la lecture pendant le téléchargement tout en préservant la durée/position
|
|
if let Some(download) = cache.get_download(pk).await {
|
|
if !download.finished().await {
|
|
let response =
|
|
stream_file_progressive(file_path, download, content_type, request).await;
|
|
|
|
if response.status().is_success() {
|
|
cache.notify_broadcast(pk, &qualifier).await;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
}
|
|
|
|
// ROUTE 2 : Fichier complètement téléchargé
|
|
// Utilise ServeFile avec support des Range requests pour le seek
|
|
let response = serve_complete_file(file_path, content_type, request).await;
|
|
|
|
if response.status().is_success() {
|
|
cache.notify_broadcast(pk, &qualifier).await;
|
|
}
|
|
|
|
response
|
|
}
|
|
|
|
/// Handler spécifique pour les lazy PK
|
|
///
|
|
/// Gère le téléchargement on-demand des fichiers lazy :
|
|
/// 1. Fast path : vérifie si déjà téléchargé
|
|
/// 2. Résout l'URL via la DB ou un provider
|
|
/// 3. Lance le téléchargement et calcule le real pk
|
|
/// 4. Met à jour la DB (lazy → downloaded)
|
|
/// 5. Broadcast l'event pour PK switching
|
|
/// 6. Sert directement le fichier téléchargé
|
|
#[cfg(feature = "pmoserver")]
|
|
async fn serve_lazy_audio_file<C: CacheConfig + 'static>(
|
|
cache: &Arc<Cache<C>>,
|
|
lazy_pk: &str,
|
|
param: &str,
|
|
content_type: &'static str,
|
|
request: axum::http::Request<Body>,
|
|
) -> Response {
|
|
tracing::info!("Lazy download triggered for pk: {}", lazy_pk);
|
|
|
|
// 1. Vérifier si déjà téléchargé (fast path)
|
|
if let Ok(Some(real_pk)) = cache.db.get_pk_by_lazy_pk(lazy_pk) {
|
|
tracing::debug!(
|
|
"Lazy PK {} already downloaded as {}, serving immediately",
|
|
lazy_pk,
|
|
real_pk
|
|
);
|
|
return serve_finalized_pk(cache, &real_pk, param, content_type, request).await;
|
|
}
|
|
|
|
// 2. Télécharger en résolvant l'URL via la DB ou un provider
|
|
let real_pk = match cache.download_lazy(lazy_pk, None).await {
|
|
Ok(pk) => pk,
|
|
Err(e) => {
|
|
tracing::error!("Failed to download lazy file: {}", e);
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
format!("Download failed: {}", e),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
|
|
// 4. Broadcast event pour prefetch ET commutation PK
|
|
cache.broadcast_lazy_downloaded(lazy_pk, &real_pk).await;
|
|
|
|
// 5. Servir directement le fichier téléchargé
|
|
serve_finalized_pk(cache, &real_pk, param, content_type, request).await
|
|
}
|
|
|
|
/// Fonction utilitaire pour servir un fichier avec streaming progressif
|
|
///
|
|
/// Si le fichier est en cours de téléchargement, il est streamé au fur et à mesure.
|
|
/// Sinon, le fichier complet est servi normalement.
|
|
/// Si le fichier n'existe pas et qu'un param_generator est fourni, tente de générer le param.
|
|
#[cfg(feature = "pmoserver")]
|
|
async fn serve_file_with_streaming<C: CacheConfig + 'static>(
|
|
cache: &Arc<Cache<C>>,
|
|
pk: &str,
|
|
param: &str,
|
|
content_type: &'static str,
|
|
param_generator: Option<ParamGenerator<C>>,
|
|
request: axum::http::Request<Body>,
|
|
) -> Response {
|
|
// LAZY PK SUPPORT: Détecter si c'est un lazy PK
|
|
if crate::cache::is_lazy_pk(pk) {
|
|
return serve_lazy_audio_file(cache, pk, param, content_type, request).await;
|
|
}
|
|
|
|
let file_path = cache.get_file_path_with_qualifier(pk, param);
|
|
let qualifier = param.to_string();
|
|
|
|
// Si le fichier n'existe pas et qu'on a un générateur, l'utiliser
|
|
if !file_path.exists() {
|
|
if let Some(generator) = param_generator {
|
|
if let Some(data) = generator(cache.clone(), pk.to_string(), param.to_string()).await {
|
|
// Le générateur a créé les données, les servir directement
|
|
let response =
|
|
(StatusCode::OK, [("content-type", content_type)], data).into_response();
|
|
|
|
if response.status().is_success() {
|
|
cache.notify_broadcast(pk, &qualifier).await;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mettre à jour les stats d'utilisation
|
|
if let Err(e) = cache.db.update_hit(pk) {
|
|
warn!("Error updating hit count for {}: {}", pk, e);
|
|
}
|
|
|
|
// Vérifier si le download est en cours
|
|
if let Some(download) = cache.get_download(pk).await {
|
|
// Le fichier est en cours de téléchargement
|
|
if !download.finished().await {
|
|
// Streaming progressif avec support Range
|
|
let response =
|
|
stream_file_progressive(file_path, download, content_type, request).await;
|
|
|
|
if response.status().is_success() {
|
|
cache.notify_broadcast(pk, &qualifier).await;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
}
|
|
|
|
// Fichier terminé ou pas de download en cours, servir normalement avec support Range
|
|
let response = serve_complete_file(file_path, content_type, request).await;
|
|
|
|
if response.status().is_success() {
|
|
cache.notify_broadcast(pk, &qualifier).await;
|
|
}
|
|
|
|
response
|
|
}
|
|
|
|
/// Stream un fichier en cours de téléchargement de manière progressive
|
|
///
|
|
/// Supporte les Range requests pour permettre le seek même pendant le téléchargement
|
|
#[cfg(feature = "pmoserver")]
|
|
async fn stream_file_progressive(
|
|
file_path: std::path::PathBuf,
|
|
download: Arc<crate::download::Download>,
|
|
content_type: &'static str,
|
|
request: axum::http::Request<Body>,
|
|
) -> Response {
|
|
use axum::http::header;
|
|
use tokio::io::{AsyncReadExt, AsyncSeekExt};
|
|
|
|
// Attendre qu'au moins 64 KB soient disponibles avant de commencer
|
|
const MIN_SIZE_TO_START: u64 = 64 * 1024;
|
|
|
|
if let Err(e) = download.wait_until_min_size(MIN_SIZE_TO_START).await {
|
|
warn!("Error waiting for download to start: {}", e);
|
|
if let Some(error_msg) = download.error().await {
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
format!("Download error: {}", error_msg),
|
|
)
|
|
.into_response();
|
|
}
|
|
return (StatusCode::NOT_FOUND, "File not available").into_response();
|
|
}
|
|
|
|
// Récupérer la taille attendue du fichier si disponible
|
|
let expected_size = download.expected_size().await;
|
|
|
|
// Vérifier s'il y a un header Range
|
|
let range_header = request.headers().get(header::RANGE);
|
|
|
|
// Ouvrir le fichier en lecture
|
|
let mut file = match tokio::fs::File::open(&file_path).await {
|
|
Ok(f) => f,
|
|
Err(e) => {
|
|
warn!("Error opening file {:?}: {}", file_path, e);
|
|
return (StatusCode::NOT_FOUND, "File not found").into_response();
|
|
}
|
|
};
|
|
|
|
// Parser le Range header si présent
|
|
if let Some(range_value) = range_header {
|
|
if let Ok(range_str) = range_value.to_str() {
|
|
// Format: "bytes=start-end" ou "bytes=start-"
|
|
if let Some(range_spec) = range_str.strip_prefix("bytes=") {
|
|
if let Some((start_str, _)) = range_spec.split_once('-') {
|
|
if let Ok(start) = start_str.parse::<u64>() {
|
|
// Seek vers la position demandée
|
|
if let Err(e) = file.seek(std::io::SeekFrom::Start(start)).await {
|
|
warn!("Error seeking to position {}: {}", start, e);
|
|
return (StatusCode::RANGE_NOT_SATISFIABLE, "Invalid range")
|
|
.into_response();
|
|
}
|
|
|
|
// Créer un stream à partir de la position
|
|
let stream = ReaderStream::new(file);
|
|
let body = Body::from_stream(stream);
|
|
|
|
// Calculer la range
|
|
let end = expected_size.map(|s| s - 1).unwrap_or(start + 1_000_000);
|
|
let content_length = expected_size.map(|s| s - start);
|
|
|
|
// Retourner une réponse 206 Partial Content
|
|
let mut response_builder = axum::http::Response::builder()
|
|
.status(StatusCode::PARTIAL_CONTENT)
|
|
.header(header::CONTENT_TYPE, content_type)
|
|
.header(header::ACCEPT_RANGES, "bytes");
|
|
|
|
if let Some(total_size) = expected_size {
|
|
response_builder = response_builder.header(
|
|
header::CONTENT_RANGE,
|
|
format!("bytes {}-{}/{}", start, end, total_size),
|
|
);
|
|
}
|
|
|
|
if let Some(length) = content_length {
|
|
response_builder =
|
|
response_builder.header(header::CONTENT_LENGTH, length);
|
|
}
|
|
|
|
return response_builder.body(body).unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pas de Range header : retourner le fichier complet
|
|
let stream = ReaderStream::new(file);
|
|
let body = Body::from_stream(stream);
|
|
|
|
let mut response = axum::http::Response::builder()
|
|
.status(StatusCode::OK)
|
|
.header(header::CONTENT_TYPE, content_type)
|
|
.header(header::ACCEPT_RANGES, "bytes");
|
|
|
|
if let Some(size) = expected_size {
|
|
response = response.header(header::CONTENT_LENGTH, size);
|
|
} else {
|
|
response = response.header(header::TRANSFER_ENCODING, "chunked");
|
|
}
|
|
|
|
response.body(body).unwrap()
|
|
}
|
|
|
|
/// Sert un fichier complet déjà téléchargé
|
|
///
|
|
/// Utilise `tower_http::services::ServeFile` pour bénéficier automatiquement de :
|
|
/// - Support des HTTP Range requests (seek)
|
|
/// - Header `Accept-Ranges: bytes`
|
|
/// - Réponses `206 Partial Content`
|
|
/// - Streaming efficace sans chargement complet en RAM
|
|
#[cfg(feature = "pmoserver")]
|
|
async fn serve_complete_file(
|
|
file_path: std::path::PathBuf,
|
|
content_type: &'static str,
|
|
request: axum::http::Request<Body>,
|
|
) -> Response {
|
|
use axum::http::header;
|
|
use tower::ServiceExt; // Pour oneshot()
|
|
use tower_http::services::ServeFile;
|
|
|
|
if !file_path.exists() {
|
|
warn!("File not found: {:?}", file_path);
|
|
return (StatusCode::NOT_FOUND, "File not found").into_response();
|
|
}
|
|
|
|
// Utiliser ServeFile avec support automatique des Range requests
|
|
// ServeFile traite automatiquement les headers Range: dans la requête
|
|
let serve_file = ServeFile::new(file_path);
|
|
|
|
match serve_file.oneshot(request).await {
|
|
Ok(response) => {
|
|
// Convertir la réponse ServeFile en Response<Body> avec le bon Content-Type
|
|
let (mut parts, body) = response.into_parts();
|
|
parts
|
|
.headers
|
|
.insert(header::CONTENT_TYPE, content_type.parse().unwrap());
|
|
|
|
// Convertir ServeFileSystemResponseBody en Body
|
|
axum::http::Response::from_parts(parts, Body::new(body))
|
|
}
|
|
Err(e) => {
|
|
warn!("Error serving file: {}", e);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "Error serving file").into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Crée un router pour servir les fichiers d'un cache
|
|
///
|
|
/// Crée un router avec les routes complètes incluant cache_name et cache_type.
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `cache` - Instance du cache
|
|
/// * `content_type` - Type MIME des fichiers (ex: "image/webp", "audio/flac")
|
|
///
|
|
/// # Routes créées
|
|
///
|
|
/// - `GET /{cache_name}/{cache_type}/{pk}` - Fichier avec param par défaut
|
|
/// - `GET /{cache_name}/{cache_type}/{pk}/{param}` - Fichier avec param spécifique
|
|
///
|
|
/// # Exemple
|
|
///
|
|
/// ```rust,no_run
|
|
/// use pmocache::pmoserver_ext;
|
|
/// use axum::Router;
|
|
/// use pmoserver::Server;
|
|
///
|
|
/// # async fn example(server: &mut Server, cache: std::sync::Arc<pmocache::Cache<CoversConfig>>) {
|
|
/// let router = pmoserver_ext::create_file_router(
|
|
/// cache.clone(),
|
|
/// "image/webp"
|
|
/// );
|
|
///
|
|
/// // Le router sera monté à la racine avec les routes complètes:
|
|
/// // GET /covers/images/{pk}
|
|
/// // GET /covers/images/{pk}/{param}
|
|
/// server.add_router("/", router).await;
|
|
/// # }
|
|
/// ```
|
|
#[cfg(feature = "pmoserver")]
|
|
pub fn create_file_router<C: CacheConfig + 'static>(
|
|
cache: Arc<Cache<C>>,
|
|
content_type: &'static str,
|
|
) -> Router {
|
|
create_file_router_with_generator(cache, content_type, None)
|
|
}
|
|
|
|
/// Crée un router pour servir les fichiers d'un cache avec générateur de param
|
|
///
|
|
/// Similaire à `create_file_router` mais permet de fournir un générateur
|
|
/// pour créer des variantes à la volée (ex: redimensionnement d'images).
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `cache` - Instance du cache
|
|
/// * `content_type` - Type MIME des fichiers (ex: "image/webp", "audio/flac")
|
|
/// * `param_generator` - Générateur optionnel pour créer des params à la volée
|
|
///
|
|
/// # Exemple
|
|
///
|
|
/// ```rust,no_run
|
|
/// use pmocache::pmoserver_ext::{create_file_router_with_generator, ParamGenerator};
|
|
/// use std::sync::Arc;
|
|
///
|
|
/// # async fn example(cache: std::sync::Arc<pmocache::Cache<CoversConfig>>) {
|
|
/// let generator: ParamGenerator<CoversConfig> = Arc::new(|cache, pk, param| {
|
|
/// Box::pin(async move {
|
|
/// // Générer une variante si param est numérique
|
|
/// if let Ok(size) = param.parse::<usize>() {
|
|
/// // Générer et retourner les données
|
|
/// Some(vec![])
|
|
/// } else {
|
|
/// None
|
|
/// }
|
|
/// })
|
|
/// });
|
|
///
|
|
/// let router = create_file_router_with_generator(
|
|
/// cache.clone(),
|
|
/// "image/webp",
|
|
/// Some(generator)
|
|
/// );
|
|
/// # }
|
|
/// ```
|
|
#[cfg(feature = "pmoserver")]
|
|
pub fn create_file_router_with_generator<C: CacheConfig + 'static>(
|
|
cache: Arc<Cache<C>>,
|
|
content_type: &'static str,
|
|
param_generator: Option<ParamGenerator<C>>,
|
|
) -> Router {
|
|
let cache_name = C::cache_name();
|
|
let cache_type = C::cache_type();
|
|
|
|
let path_with_param = format!("/{}/{}/{{pk}}/{{param}}", cache_name, cache_type);
|
|
let path_without_param = format!("/{}/{}/{{pk}}", cache_name, cache_type);
|
|
|
|
Router::new()
|
|
.route(&path_without_param, get(get_file::<C>))
|
|
.route(&path_with_param, get(get_file_with_param::<C>))
|
|
.with_state((cache, content_type, param_generator))
|
|
}
|
|
|
|
/// Crée un router pour l'API REST du cache
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `cache` - Instance du cache
|
|
///
|
|
/// # Routes créées
|
|
///
|
|
/// - `GET /` - Liste des items
|
|
/// - `POST /` - Ajouter un item
|
|
/// - `DELETE /` - Purger le cache
|
|
/// - `GET /{pk}` - Info d'un item
|
|
/// - `GET /{pk}/status` - Status du download
|
|
/// - `DELETE /{pk}` - Supprimer un item
|
|
/// - `POST /consolidate` - Consolider le cache
|
|
/// - `GET /{pk}/pin` - Statut de pinning
|
|
/// - `POST /{pk}/pin` - Épingler un item
|
|
/// - `DELETE /{pk}/pin` - Désépingler un item
|
|
/// - `POST /{pk}/ttl` - Définir le TTL
|
|
/// - `DELETE /{pk}/ttl` - Supprimer le TTL
|
|
#[cfg(feature = "pmoserver")]
|
|
pub fn create_api_router<C: CacheConfig + 'static>(cache: Arc<Cache<C>>) -> Router {
|
|
use crate::api;
|
|
|
|
Router::new()
|
|
.route(
|
|
"/",
|
|
get(api::list_items::<C>)
|
|
.post(api::add_item::<C>)
|
|
.delete(api::purge_cache::<C>),
|
|
)
|
|
.route(
|
|
"/{pk}",
|
|
get(api::get_item_info::<C>).delete(api::delete_item::<C>),
|
|
)
|
|
.route("/{pk}/status", get(api::get_download_status::<C>))
|
|
.route(
|
|
"/{pk}/pin",
|
|
get(api::get_pin_status::<C>)
|
|
.post(api::pin_item::<C>)
|
|
.delete(api::unpin_item::<C>),
|
|
)
|
|
.route(
|
|
"/{pk}/ttl",
|
|
post(api::set_item_ttl::<C>).delete(api::clear_item_ttl::<C>),
|
|
)
|
|
.route("/consolidate", post(api::consolidate_cache::<C>))
|
|
.with_state(cache)
|
|
}
|
|
|
|
/// Trait d'extension pour pmoserver::Server
|
|
///
|
|
/// Permet d'initialiser un cache générique avec routes HTTP complètes
|
|
#[cfg(feature = "pmoserver")]
|
|
pub trait GenericCacheExt {
|
|
/// Initialise un cache générique avec routes complètes
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `cache_dir` - Répertoire de stockage du cache
|
|
/// * `limit` - Limite de taille du cache (nombre d'éléments)
|
|
/// * `content_type` - Type MIME des fichiers (ex: "image/webp", "audio/flac")
|
|
///
|
|
/// # Routes créées
|
|
///
|
|
/// - Fichiers: `/{cache_name}/{cache_type}/{pk}[/{param}]`
|
|
/// - API: `/api/{cache_name}/*`
|
|
/// - Swagger: `/swagger-ui/{cache_name}`
|
|
async fn init_generic_cache<C: CacheConfig + 'static>(
|
|
&mut self,
|
|
cache_dir: &str,
|
|
limit: usize,
|
|
content_type: &'static str,
|
|
) -> anyhow::Result<Arc<Cache<C>>>;
|
|
}
|