refactoring des caches

This commit is contained in:
2025-10-25 17:14:24 +02:00
parent 180241a315
commit ecb362ae48
21 changed files with 964 additions and 245 deletions

View File

@@ -3,7 +3,7 @@
//! Ce module fournit une interface générique pour gérer un cache de fichiers
//! avec métadonnées dans une base de données SQLite.
use crate::cache_trait::{pk_from_url, FileCache};
use crate::cache_trait::FileCache;
use crate::db::DB;
use crate::download::{
download_with_transformer, ingest_with_transformer, Download, StreamTransformer,
@@ -132,8 +132,18 @@ impl<C: CacheConfig> Cache<C> {
/// Télécharge un fichier depuis une URL et l'ajoute au cache
///
/// Utilise le module download pour gérer le téléchargement asynchrone.
/// Le download est tracké dans la map jusqu'à sa fin.
/// Cette méthode utilise un système d'identifiants basé sur le contenu plutôt que sur l'URL.
/// Elle télécharge les 512 premiers octets du fichier pour calculer un identifiant unique (pk),
/// puis vérifie si le fichier est déjà en cache. Si c'est le cas, elle met à jour le timestamp
/// et retourne rapidement. Sinon, elle lance le téléchargement complet en arrière-plan.
///
/// # Workflow
///
/// 1. Télécharge les 512 premiers octets via une requête HTTP partielle
/// 2. Calcule le pk en hashant (SHA256) ces premiers octets
/// 3. Vérifie si le fichier existe déjà dans le cache
/// 4. Si oui : update timestamp et retour rapide
/// 5. Si non : lance le téléchargement complet en background
///
/// # Arguments
///
@@ -142,21 +152,46 @@ impl<C: CacheConfig> Cache<C> {
///
/// # Returns
///
/// La clé primaire (pk) du fichier dans le cache
/// La clé primaire (pk) du fichier dans le cache, calculée à partir du contenu
///
/// # Note
///
/// Deux URLs différentes pointant vers le même contenu auront le même pk,
/// permettant une déduplication automatique.
pub async fn add_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
let pk = pk_from_url(url);
let file_path = self.file_path(&pk);
// 1. Télécharger les 512 premiers octets pour calculer le pk
let header = crate::download::peek_header(url, 512)
.await
.map_err(|e| anyhow!("Failed to peek header: {}", e))?;
// Vérifier si déjà en cours de téléchargement
{
let downloads = self.downloads.read().await;
if downloads.contains_key(&pk) {
// Download déjà en cours, retourner la clé
// 2. Calculer le pk basé sur le contenu
let pk = crate::cache_trait::pk_from_content_header(&header);
tracing::debug!("Computed pk {} for URL {}", pk, url);
// 3. Vérifier si le fichier est déjà en cache
if self.db.get(&pk).is_ok() {
let file_path = self.file_path(&pk);
if file_path.exists() {
// Déjà en cache, update timestamp et retour rapide
tracing::debug!("File with pk {} already in cache, updating timestamp", pk);
self.db.update_hit(&pk)?;
return Ok(pk);
}
}
// Lancer le téléchargement avec transformer
// 4. Vérifier si un download est déjà en cours pour ce pk
{
let downloads = self.downloads.read().await;
if downloads.contains_key(&pk) {
// Download déjà en cours pour ce contenu, retourner la clé
tracing::debug!("Download already in progress for pk {}", pk);
return Ok(pk);
}
}
// 5. Lancer le téléchargement complet avec transformer
tracing::debug!("Starting full download for pk {} from URL {}", pk, url);
let file_path = self.file_path(&pk);
let transformer = self.transformer_factory.as_ref().map(|f| f());
let download = download_with_transformer(&file_path, url, transformer);
@@ -170,7 +205,6 @@ impl<C: CacheConfig> Cache<C> {
self.db.add(&pk, url, collection)?;
// Appliquer la politique d'éviction LRU si nécessaire
// Cela garantit que le cache respecte toujours la limite configurée
if let Err(e) = self.enforce_limit().await {
tracing::warn!("Error enforcing cache limit: {}", e);
}
@@ -179,9 +213,7 @@ impl<C: CacheConfig> Cache<C> {
let downloads_clone = self.downloads.clone();
let pk_clone = pk.clone();
tokio::spawn(async move {
// Attendre la fin du téléchargement
let _ = download.wait_until_finished().await;
// Retirer de la map
downloads_clone.write().await.remove(&pk_clone);
});
@@ -190,38 +222,79 @@ impl<C: CacheConfig> Cache<C> {
/// Ajoute un fichier à partir d'un flux asynchrone.
///
/// Le flux peut provenir de n'importe quelle source (stream HTTP custom, décodeur,
/// extraction en mémoire, etc.). Les mêmes transformers que `add_from_url` sont
/// appliqués.
/// Cette méthode utilise le même système d'identifiants basé sur le contenu que `add_from_url`.
/// Elle lit les 512 premiers octets du flux pour calculer l'identifiant, puis reconstitue
/// le flux complet pour l'ingestion.
///
/// # Workflow
///
/// 1. Lit les 512 premiers octets du reader
/// 2. Calcule le pk en hashant (SHA256) ces premiers octets
/// 3. Vérifie si le fichier existe déjà dans le cache
/// 4. Si oui : update timestamp et retour rapide
/// 5. Si non : reconstitue le reader (header + reste) et lance l'ingestion
///
/// # Arguments
///
/// * `source_uri` - Identifiant logique du flux (utilisé pour générer le pk)
/// * `source_uri` - Identifiant logique du flux (pour traçabilité dans la DB)
/// * `reader` - Flux asynchrone fournissant les données
/// * `length` - Taille attendue (si connue)
/// * `collection` - Collection optionnelle à laquelle appartient l'élément
///
/// # Returns
///
/// La clé primaire (pk) du fichier dans le cache, calculée à partir du contenu
pub async fn add_from_reader<R>(
&self,
source_uri: &str,
reader: R,
mut reader: R,
length: Option<u64>,
collection: Option<&str>,
) -> Result<String>
where
R: AsyncRead + Send + Unpin + 'static,
{
let pk = pk_from_url(source_uri);
let file_path = self.file_path(&pk);
// 1. Lire les 512 premiers octets pour calculer le pk
let header = crate::download::peek_reader_header(&mut reader, 512)
.await
.map_err(|e| anyhow!("Failed to peek reader header: {}", e))?;
{
let downloads = self.downloads.read().await;
if downloads.contains_key(&pk) {
// 2. Calculer le pk basé sur le contenu
let pk = crate::cache_trait::pk_from_content_header(&header);
tracing::debug!("Computed pk {} for source_uri {}", pk, source_uri);
// 3. Vérifier si le fichier est déjà en cache
if self.db.get(&pk).is_ok() {
let file_path = self.file_path(&pk);
if file_path.exists() {
// Déjà en cache, update timestamp et retour rapide
tracing::debug!("File with pk {} already in cache, updating timestamp", pk);
self.db.update_hit(&pk)?;
return Ok(pk);
}
}
// 4. Vérifier si un download est déjà en cours pour ce pk
{
let downloads = self.downloads.read().await;
if downloads.contains_key(&pk) {
tracing::debug!("Download already in progress for pk {}", pk);
return Ok(pk);
}
}
// 5. Reconstituer le reader complet (header + reste)
// Utiliser tokio::io::chain pour créer un reader composé
use std::io::Cursor;
use tokio::io::AsyncReadExt;
let header_reader = Cursor::new(header);
let full_reader = header_reader.chain(reader);
// 6. Lancer l'ingestion avec transformer
tracing::debug!("Starting ingestion for pk {} from reader", pk);
let file_path = self.file_path(&pk);
let transformer = self.transformer_factory.as_ref().map(|factory| factory());
let download = ingest_with_transformer(&file_path, reader, length, transformer);
let download = ingest_with_transformer(&file_path, full_reader, length, transformer);
{
let mut downloads = self.downloads.write().await;
@@ -246,7 +319,9 @@ impl<C: CacheConfig> Cache<C> {
/// Ajoute un fichier local au cache
///
/// Le fichier est copié dans le cache via une URL file://
/// Cette méthode lit les 512 premiers octets du fichier local pour calculer
/// l'identifiant basé sur le contenu, puis utilise `add_from_reader()` pour
/// l'ingestion complète.
///
/// # Arguments
///
@@ -255,7 +330,21 @@ impl<C: CacheConfig> Cache<C> {
///
/// # Returns
///
/// La clé primaire (pk) du fichier dans le cache
/// La clé primaire (pk) du fichier dans le cache, calculée à partir du contenu
///
/// # Exemple
///
/// ```rust,ignore
/// use pmocache::{Cache, CacheConfig};
///
/// struct MyConfig;
/// impl CacheConfig for MyConfig {
/// fn file_extension() -> &'static str { "dat" }
/// }
///
/// let cache = Cache::<MyConfig>::new("./cache", 1000)?;
/// let pk = cache.add_from_file("/path/to/file.dat", None).await?;
/// ```
pub async fn add_from_file(&self, path: &str, collection: Option<&str>) -> Result<String> {
let canonical_path = std::fs::canonicalize(path)?;
let file_url = format!("file://{}", canonical_path.display());
@@ -264,31 +353,12 @@ impl<C: CacheConfig> Cache<C> {
.ok()
.map(|m| m.len());
let reader = tokio::fs::File::open(&canonical_path).await?;
// add_from_reader() s'occupe de lire les 512 premiers octets et de calculer le pk
self.add_from_reader(&file_url, reader, length, collection)
.await
}
/// S'assure qu'un fichier est présent dans le cache
///
/// Si le fichier existe déjà, retourne sa clé. Sinon, le télécharge.
///
/// # Arguments
///
/// * `url` - URL du fichier
/// * `collection` - Collection optionnelle à laquelle appartient le fichier
pub async fn ensure_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
let pk = pk_from_url(url);
if self.db.get(&pk).is_ok() {
let file_path = self.file_path(&pk);
if file_path.exists() {
return Ok(pk);
}
}
self.add_from_url(url, collection).await
}
/// Récupère le chemin d'un fichier dans le cache
///
/// # Arguments
@@ -620,10 +690,6 @@ impl<C: CacheConfig> FileCache<C> for Cache<C> {
self.add_from_file(path, collection).await
}
async fn ensure_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
self.ensure_from_url(url, collection).await
}
async fn get(&self, pk: &str) -> Result<PathBuf> {
self.get(pk).await
}

View File

@@ -116,16 +116,6 @@ pub trait FileCache<C: CacheConfig>: Send + Sync {
/// La clé primaire (pk) du fichier dans le cache
async fn add_from_file(&self, path: &str, collection: Option<&str>) -> Result<String>;
/// S'assure qu'un fichier est présent dans le cache
///
/// Si le fichier existe déjà, retourne sa clé. Sinon, le télécharge.
///
/// # Arguments
///
/// * `url` - URL du fichier
/// * `collection` - Collection optionnelle à laquelle appartient le fichier
async fn ensure_from_url(&self, url: &str, collection: Option<&str>) -> Result<String>;
/// Récupère le chemin d'un fichier dans le cache
///
/// # Arguments
@@ -147,9 +137,48 @@ pub trait FileCache<C: CacheConfig>: Send + Sync {
async fn consolidate(&self) -> Result<()>;
}
/// Génère une clé primaire à partir d'une URL
/// Génère une clé primaire à partir des premiers octets d'un document
///
/// Utilise SHA256 pour hasher les premiers octets du contenu et retourne les 16 premiers octets
/// en hexadécimal (32 caractères). L'utilisation de 16 octets au lieu de 8 réduit considérablement
/// les risques de collision.
///
/// # Arguments
///
/// * `header` - Les premiers octets du document (typiquement 512 octets)
///
/// # Returns
///
/// Une chaîne hexadécimale de 32 caractères servant de clé primaire unique
///
/// # Exemple
///
/// ```
/// use pmocache::pk_from_content_header;
///
/// let data = b"Some file content...";
/// let pk = pk_from_content_header(data);
/// assert_eq!(pk.len(), 32); // 16 bytes = 32 hex chars
/// ```
pub fn pk_from_content_header(header: &[u8]) -> String {
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(header);
let result = hasher.finalize();
hex::encode(&result[..16]) // 16 octets = 32 caractères hex
}
/// Génère une clé primaire à partir d'une URL (legacy)
///
/// **DEPRECATED**: Cette fonction est obsolète et ne devrait plus être utilisée.
/// Utilisez `pk_from_content_header()` à la place pour générer des identifiants
/// basés sur le contenu plutôt que sur l'URL.
///
/// Utilise SHA1 pour hasher l'URL et retourne les 8 premiers octets en hexadécimal.
#[deprecated(
since = "0.2.0",
note = "Utilisez pk_from_content_header() pour des identifiants basés sur le contenu"
)]
pub fn pk_from_url(url: &str) -> String {
let mut hasher = Sha1::new();
hasher.update(url.as_bytes());

225
pmocache/src/config_ext.rs Normal file
View File

@@ -0,0 +1,225 @@
//! Extension pour intégrer la gestion des caches dans pmoconfig
//!
//! Ce module fournit le trait `CacheConfigExt` qui permet d'ajouter facilement
//! des méthodes de gestion de cache générique à pmoconfig::Config.
//!
//! Il propose également un macro `impl_cache_config_ext!` pour simplifier
//! l'implémentation de traits d'extension spécialisés (audio, covers, etc.).
use anyhow::Result;
use pmoconfig::Config;
use serde_yaml::{Number, Value};
use std::sync::Arc;
/// Trait d'extension pour ajouter la gestion des caches à pmoconfig
///
/// Ce trait étend `pmoconfig::Config` avec des méthodes génériques pour gérer
/// n'importe quel type de cache (audio, images, etc.).
///
/// # Exemple
///
/// ```rust,ignore
/// use pmoconfig::get_config;
/// use pmocache::{CacheConfigExt, AudioConfig};
///
/// let config = get_config();
/// let cache_dir = config.get_cache_dir("audio_cache", "cache_audio")?;
/// let cache_size = config.get_cache_size("audio_cache", 500)?;
/// ```
pub trait CacheConfigExt {
/// Récupère le répertoire d'un cache
///
/// # Arguments
///
/// * `cache_type` - Type de cache (ex: "audio_cache", "cover_cache")
/// * `default` - Nom de répertoire par défaut si non configuré
///
/// # Returns
///
/// Le chemin absolu du répertoire du cache
fn get_cache_dir(&self, cache_type: &str, default: &str) -> Result<String>;
/// Définit le répertoire d'un cache
///
/// # Arguments
///
/// * `cache_type` - Type de cache (ex: "audio_cache", "cover_cache")
/// * `directory` - Chemin du répertoire (absolu ou relatif au config_dir)
fn set_cache_dir(&self, cache_type: &str, directory: String) -> Result<()>;
/// Récupère la taille maximale d'un cache
///
/// # Arguments
///
/// * `cache_type` - Type de cache (ex: "audio_cache", "cover_cache")
/// * `default` - Taille par défaut si non configurée
///
/// # Returns
///
/// Le nombre maximal d'éléments dans le cache
fn get_cache_size(&self, cache_type: &str, default: usize) -> Result<usize>;
/// Définit la taille maximale d'un cache
///
/// # Arguments
///
/// * `cache_type` - Type de cache (ex: "audio_cache", "cover_cache")
/// * `size` - Nombre maximal d'éléments
fn set_cache_size(&self, cache_type: &str, size: usize) -> Result<()>;
/// Crée une instance de cache générique configurée
///
/// Cette méthode factory crée un cache en utilisant les paramètres
/// de configuration (répertoire et taille).
///
/// # Arguments
///
/// * `cache_type` - Type de cache (ex: "audio_cache", "cover_cache")
/// * `default_dir` - Répertoire par défaut
/// * `default_size` - Taille par défaut
///
/// # Returns
///
/// Une instance Arc du cache configuré
///
/// # Exemple
///
/// ```rust,ignore
/// use pmoconfig::get_config;
/// use pmocache::{CacheConfigExt, AudioConfig};
///
/// let config = get_config();
/// let cache = config.create_cache::<AudioConfig>("audio_cache", "cache_audio", 500)?;
/// ```
fn create_cache<C: crate::CacheConfig>(
&self,
cache_type: &str,
default_dir: &str,
default_size: usize,
) -> Result<Arc<crate::Cache<C>>>;
}
impl CacheConfigExt for Config {
fn get_cache_dir(&self, cache_type: &str, default: &str) -> Result<String> {
self.get_managed_dir(&["host", cache_type, "directory"], default)
}
fn set_cache_dir(&self, cache_type: &str, directory: String) -> Result<()> {
self.set_managed_dir(&["host", cache_type, "directory"], directory)
}
fn get_cache_size(&self, cache_type: &str, default: usize) -> Result<usize> {
match self.get_value(&["host", cache_type, "size"])? {
Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap() as usize),
Value::Number(n) if n.is_u64() => Ok(n.as_u64().unwrap() as usize),
_ => Ok(default),
}
}
fn set_cache_size(&self, cache_type: &str, size: usize) -> Result<()> {
let n = Number::from(size);
self.set_value(&["host", cache_type, "size"], Value::Number(n))
}
fn create_cache<C: crate::CacheConfig>(
&self,
cache_type: &str,
default_dir: &str,
default_size: usize,
) -> Result<Arc<crate::Cache<C>>> {
let dir = self.get_cache_dir(cache_type, default_dir)?;
let size = self.get_cache_size(cache_type, default_size)?;
Ok(Arc::new(crate::Cache::<C>::new(&dir, size)?))
}
}
/// Macro pour simplifier l'implémentation de traits d'extension de cache spécialisés
///
/// Ce macro génère automatiquement un trait d'extension pour `pmoconfig::Config`
/// avec des méthodes spécifiques à un type de cache (audio, covers, etc.).
///
/// # Arguments
///
/// * `trait_name` - Nom du trait à générer (ex: `AudioCacheConfigExt`)
/// * `cache_type` - Type de cache dans la config (ex: `"audio_cache"`)
/// * `default_dir` - Répertoire par défaut (ex: `"cache_audio"`)
/// * `default_size` - Taille par défaut (ex: `500`)
/// * `cache_struct` - Type du cache (ex: `crate::Cache`)
/// * `constructor` - Expression pour construire le cache (ex: `crate::cache::new_cache(&dir, size)`)
///
/// # Exemple
///
/// ```rust,ignore
/// use pmocache::impl_cache_config_ext;
///
/// impl_cache_config_ext! {
/// AudioCacheConfigExt,
/// "audio_cache",
/// "cache_audio",
/// 500,
/// crate::Cache,
/// |dir, size| crate::cache::new_cache(dir, size)
/// }
/// ```
///
/// Cela génère un trait avec les méthodes :
/// - `get_audiocache_dir()` / `set_audiocache_dir()`
/// - `get_audiocache_size()` / `set_audiocache_size()`
/// - `create_audio_cache()`
#[macro_export]
macro_rules! impl_cache_config_ext {
(
$trait_name:ident,
$cache_type:expr,
$default_dir:expr,
$default_size:expr,
$cache_type_struct:ty,
$constructor:expr
) => {
pub trait $trait_name {
/// Récupère le répertoire du cache
fn get_cache_dir_ext(&self) -> anyhow::Result<String>;
/// Définit le répertoire du cache
fn set_cache_dir_ext(&self, directory: String) -> anyhow::Result<()>;
/// Récupère la taille maximale du cache
fn get_cache_size_ext(&self) -> anyhow::Result<usize>;
/// Définit la taille maximale du cache
fn set_cache_size_ext(&self, size: usize) -> anyhow::Result<()>;
/// Crée une instance du cache configurée
fn create_cache_ext(&self) -> anyhow::Result<std::sync::Arc<$cache_type_struct>>;
}
impl $trait_name for pmoconfig::Config {
fn get_cache_dir_ext(&self) -> anyhow::Result<String> {
use $crate::CacheConfigExt;
self.get_cache_dir($cache_type, $default_dir)
}
fn set_cache_dir_ext(&self, directory: String) -> anyhow::Result<()> {
use $crate::CacheConfigExt;
self.set_cache_dir($cache_type, directory)
}
fn get_cache_size_ext(&self) -> anyhow::Result<usize> {
use $crate::CacheConfigExt;
self.get_cache_size($cache_type, $default_size)
}
fn set_cache_size_ext(&self, size: usize) -> anyhow::Result<()> {
use $crate::CacheConfigExt;
self.set_cache_size($cache_type, size)
}
fn create_cache_ext(&self) -> anyhow::Result<std::sync::Arc<$cache_type_struct>> {
let dir = self.get_cache_dir_ext()?;
let size = self.get_cache_size_ext()?;
let constructor = $constructor;
Ok(std::sync::Arc::new(constructor(&dir, size)?))
}
}
};
}

View File

@@ -460,3 +460,97 @@ async fn default_copy(
s.finished = true;
Ok(())
}
/// Lit les premiers octets d'une URL sans télécharger le fichier complet
///
/// Cette fonction effectue une requête HTTP partielle (Range header) pour télécharger
/// uniquement les premiers octets d'un fichier. C'est utilisé pour calculer l'identifiant
/// basé sur le contenu sans avoir à télécharger tout le fichier.
///
/// # Arguments
///
/// * `url` - URL du fichier à télécharger
/// * `max_bytes` - Nombre maximum d'octets à lire (par défaut 512)
///
/// # Returns
///
/// Un `Vec<u8>` contenant les premiers octets du fichier
///
/// # Exemple
///
/// ```rust,ignore
/// use pmocache::download::peek_header;
///
/// let header = peek_header("http://example.com/file.dat", 512).await?;
/// let pk = pk_from_content_header(&header);
/// ```
pub async fn peek_header(url: &str, max_bytes: usize) -> Result<Vec<u8>, String> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| e.to_string())?;
// Essayer d'abord avec une requête Range
let range_header = format!("bytes=0-{}", max_bytes - 1);
let mut response = client
.get(url)
.header("Range", range_header)
.send()
.await
.map_err(|e| format!("Failed to fetch URL '{}': {}", url, e))?;
// Si le serveur ne supporte pas Range (status 200 au lieu de 206),
// on lit quand même mais on limite la lecture
if !response.status().is_success() && response.status() != reqwest::StatusCode::PARTIAL_CONTENT {
return Err(format!("HTTP error: {}", response.status()));
}
let mut buffer = Vec::new();
while let Some(chunk) = response.chunk().await.map_err(|e| e.to_string())? {
buffer.extend_from_slice(&chunk);
if buffer.len() >= max_bytes {
buffer.truncate(max_bytes);
break;
}
}
Ok(buffer)
}
/// Lit les premiers octets d'un reader asynchrone
///
/// Cette fonction lit jusqu'à `max_bytes` octets depuis un reader asynchrone.
/// C'est utilisé pour calculer l'identifiant basé sur le contenu des fichiers locaux
/// ou des streams.
///
/// # Arguments
///
/// * `reader` - Le reader asynchrone à lire
/// * `max_bytes` - Nombre maximum d'octets à lire (par défaut 512)
///
/// # Returns
///
/// Un `Vec<u8>` contenant les premiers octets lus
///
/// # Exemple
///
/// ```rust,ignore
/// use pmocache::download::peek_reader_header;
/// use tokio::fs::File;
///
/// let mut file = File::open("file.dat").await?;
/// let header = peek_reader_header(&mut file, 512).await?;
/// let pk = pk_from_content_header(&header);
/// ```
pub async fn peek_reader_header<R>(reader: &mut R, max_bytes: usize) -> Result<Vec<u8>, String>
where
R: AsyncRead + Unpin,
{
let mut buffer = vec![0u8; max_bytes];
let n = reader
.read(&mut buffer)
.await
.map_err(|e| format!("Failed to read from stream: {}", e))?;
buffer.truncate(n);
Ok(buffer)
}

View File

@@ -136,11 +136,15 @@ pub mod api;
#[cfg(feature = "openapi")]
pub mod openapi;
#[cfg(feature = "pmoconfig")]
pub mod config_ext;
pub use cache::{Cache, CacheConfig};
pub use cache_trait::{pk_from_url, FileCache};
pub use cache_trait::{pk_from_content_header, pk_from_url, FileCache};
pub use db::{CacheEntry, DB};
pub use download::{
download, download_with_transformer, ingest_with_transformer, Download, StreamTransformer,
download, download_with_transformer, ingest_with_transformer, peek_header,
peek_reader_header, Download, StreamTransformer,
};
#[cfg(feature = "pmoserver")]
@@ -148,3 +152,6 @@ pub use pmoserver_ext::{create_api_router, create_file_router, GenericCacheExt};
#[cfg(all(feature = "pmoserver", feature = "openapi"))]
pub use api::{AddItemRequest, AddItemResponse, DeleteItemResponse, DownloadStatus, ErrorResponse};
#[cfg(feature = "pmoconfig")]
pub use config_ext::CacheConfigExt;