378 lines
12 KiB
Rust
378 lines
12 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>,
|
|
) -> Response {
|
|
// Utiliser le param par défaut
|
|
let param = C::default_param();
|
|
serve_file_with_streaming(&cache, &pk, param, content_type, param_generator).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)>,
|
|
) -> Response {
|
|
serve_file_with_streaming(&cache, &pk, ¶m, content_type, param_generator).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>(
|
|
cache: &Arc<Cache<C>>,
|
|
pk: &str,
|
|
param: &str,
|
|
content_type: &'static str,
|
|
param_generator: Option<ParamGenerator<C>>,
|
|
) -> Response {
|
|
let file_path = cache.file_path_with_qualifier(pk, param);
|
|
|
|
// 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
|
|
return (StatusCode::OK, [("content-type", content_type)], data).into_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
|
|
return stream_file_progressive(file_path, download, content_type).await;
|
|
}
|
|
}
|
|
|
|
// Fichier terminé ou pas de download en cours, servir normalement
|
|
serve_complete_file(file_path, content_type).await
|
|
}
|
|
|
|
/// Stream un fichier en cours de téléchargement de manière progressive
|
|
#[cfg(feature = "pmoserver")]
|
|
async fn stream_file_progressive(
|
|
file_path: std::path::PathBuf,
|
|
download: Arc<crate::download::Download>,
|
|
content_type: &'static str,
|
|
) -> Response {
|
|
// 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();
|
|
}
|
|
|
|
// Ouvrir le fichier en lecture
|
|
let 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();
|
|
}
|
|
};
|
|
|
|
// Créer un stream à partir du fichier
|
|
let stream = ReaderStream::new(file);
|
|
let body = Body::from_stream(stream);
|
|
|
|
(
|
|
StatusCode::OK,
|
|
[
|
|
("content-type", content_type),
|
|
("transfer-encoding", "chunked"),
|
|
],
|
|
body,
|
|
)
|
|
.into_response()
|
|
}
|
|
|
|
/// Sert un fichier complet déjà téléchargé
|
|
#[cfg(feature = "pmoserver")]
|
|
async fn serve_complete_file(
|
|
file_path: std::path::PathBuf,
|
|
content_type: &'static str,
|
|
) -> Response {
|
|
if !file_path.exists() {
|
|
warn!("File not found: {:?}", file_path);
|
|
return (StatusCode::NOT_FOUND, "File not found").into_response();
|
|
}
|
|
|
|
match tokio::fs::read(&file_path).await {
|
|
Ok(data) => (StatusCode::OK, [("content-type", content_type)], data).into_response(),
|
|
Err(e) => {
|
|
warn!("Error reading file {:?}: {}", file_path, e);
|
|
(StatusCode::INTERNAL_SERVER_ERROR, "Error reading 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
|
|
#[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("/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>>>;
|
|
}
|