adaptation de la crate pmocovers
This commit is contained in:
@@ -1,312 +0,0 @@
|
||||
//! API REST pour la gestion du cache de couvertures
|
||||
//!
|
||||
//! Ce module expose une API REST documentée avec OpenAPI/Swagger pour :
|
||||
//! - Lister les images en cache
|
||||
//! - Ajouter des images depuis une URL
|
||||
//! - Supprimer des images
|
||||
//! - Consulter les statistiques
|
||||
|
||||
use crate::{Cache, CacheEntry, ImageCacheExt};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// Requête pour ajouter une image au cache
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AddImageRequest {
|
||||
/// URL de l'image source
|
||||
#[schema(example = "https://example.com/cover.jpg")]
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
/// Réponse après ajout d'une image
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AddImageResponse {
|
||||
/// Clé primaire (pk) de l'image ajoutée
|
||||
#[schema(example = "1a2b3c4d5e6f7a8b")]
|
||||
pub pk: String,
|
||||
/// URL source de l'image
|
||||
#[schema(example = "https://example.com/cover.jpg")]
|
||||
pub url: String,
|
||||
/// Message de succès
|
||||
#[schema(example = "Image added successfully")]
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Réponse de suppression d'une image
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct DeleteImageResponse {
|
||||
/// Message de succès
|
||||
#[schema(example = "Image deleted successfully")]
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Réponse d'erreur générique
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ErrorResponse {
|
||||
/// Code d'erreur
|
||||
#[schema(example = "NOT_FOUND")]
|
||||
pub error: String,
|
||||
/// Message descriptif
|
||||
#[schema(example = "Image not found in cache")]
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Liste toutes les images en cache avec leurs statistiques
|
||||
///
|
||||
/// Retourne la liste complète des entrées du cache triées par nombre d'accès décroissant.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/covers",
|
||||
responses(
|
||||
(status = 200, description = "Liste des images en cache", body = Vec<CacheEntry>),
|
||||
(status = 500, description = "Erreur serveur", body = ErrorResponse)
|
||||
),
|
||||
tag = "covers"
|
||||
)]
|
||||
pub async fn list_images(State(cache): State<Arc<Cache>>) -> impl IntoResponse {
|
||||
match cache.db.get_all() {
|
||||
Ok(entries) => (StatusCode::OK, Json(entries)).into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: "DATABASE_ERROR".to_string(),
|
||||
message: format!("Cannot retrieve cache entries: {}", e),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère les informations d'une image spécifique
|
||||
///
|
||||
/// Retourne les métadonnées d'une image identifiée par sa clé (pk).
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/covers/{pk}",
|
||||
params(
|
||||
("pk" = String, Path, description = "Clé primaire de l'image", example = "1a2b3c4d5e6f7a8b")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Informations de l'image", body = CacheEntry),
|
||||
(status = 404, description = "Image non trouvée", body = ErrorResponse)
|
||||
),
|
||||
tag = "covers"
|
||||
)]
|
||||
pub async fn get_image_info(
|
||||
State(cache): State<Arc<Cache>>,
|
||||
Path(pk): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match cache.db.get(&pk) {
|
||||
Ok(entry) => (StatusCode::OK, Json(entry)).into_response(),
|
||||
Err(_) => (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(ErrorResponse {
|
||||
error: "NOT_FOUND".to_string(),
|
||||
message: format!("Image with pk '{}' not found in cache", pk),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ajoute une image au cache depuis une URL
|
||||
///
|
||||
/// Télécharge l'image depuis l'URL fournie, la convertit en WebP et l'ajoute au cache.
|
||||
/// Si l'image existe déjà, elle est mise à jour.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/covers",
|
||||
request_body = AddImageRequest,
|
||||
responses(
|
||||
(status = 201, description = "Image ajoutée avec succès", body = AddImageResponse),
|
||||
(status = 400, description = "Requête invalide", body = ErrorResponse),
|
||||
(status = 500, description = "Erreur lors du téléchargement ou de la conversion", body = ErrorResponse)
|
||||
),
|
||||
tag = "covers"
|
||||
)]
|
||||
pub async fn add_image(
|
||||
State(cache): State<Arc<Cache>>,
|
||||
Json(req): Json<AddImageRequest>,
|
||||
) -> impl IntoResponse {
|
||||
if req.url.is_empty() {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorResponse {
|
||||
error: "INVALID_REQUEST".to_string(),
|
||||
message: "URL cannot be empty".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
match cache.add_image_from_url(&req.url).await {
|
||||
Ok(pk) => (
|
||||
StatusCode::CREATED,
|
||||
Json(AddImageResponse {
|
||||
pk,
|
||||
url: req.url,
|
||||
message: "Image added successfully".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: "PROCESSING_ERROR".to_string(),
|
||||
message: format!("Cannot add image: {}", e),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime une image du cache
|
||||
///
|
||||
/// Supprime l'image et toutes ses variantes du disque et de la base de données.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/covers/{pk}",
|
||||
params(
|
||||
("pk" = String, Path, description = "Clé primaire de l'image à supprimer", example = "1a2b3c4d5e6f7a8b")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Image supprimée avec succès", body = DeleteImageResponse),
|
||||
(status = 404, description = "Image non trouvée", body = ErrorResponse),
|
||||
(status = 500, description = "Erreur lors de la suppression", body = ErrorResponse)
|
||||
),
|
||||
tag = "covers"
|
||||
)]
|
||||
pub async fn delete_image(
|
||||
State(cache): State<Arc<Cache>>,
|
||||
Path(pk): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
// Vérifier que l'image existe
|
||||
if cache.db.get(&pk).is_err() {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(ErrorResponse {
|
||||
error: "NOT_FOUND".to_string(),
|
||||
message: format!("Image with pk '{}' not found in cache", pk),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// Supprimer les fichiers (original + variantes)
|
||||
let cache_dir = std::path::PathBuf::from(cache.cache_dir());
|
||||
let orig_path = cache_dir.join(format!("{}.orig.webp", pk));
|
||||
if orig_path.exists() {
|
||||
if let Err(e) = tokio::fs::remove_file(&orig_path).await {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: "FILE_DELETE_ERROR".to_string(),
|
||||
message: format!("Cannot delete original file: {}", e),
|
||||
}),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer toutes les variantes (*.{pk}.*.webp)
|
||||
if let Ok(mut entries) = tokio::fs::read_dir(&cache_dir).await {
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
if let Some(filename) = entry.file_name().to_str() {
|
||||
if filename.starts_with(&pk) && filename.ends_with(".webp") && filename != format!("{}.orig.webp", pk) {
|
||||
let _ = tokio::fs::remove_file(entry.path()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer de la base de données
|
||||
match cache.db.delete(&pk) {
|
||||
Ok(_) => (
|
||||
StatusCode::OK,
|
||||
Json(DeleteImageResponse {
|
||||
message: format!("Image '{}' deleted successfully", pk),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: "DATABASE_ERROR".to_string(),
|
||||
message: format!("Cannot delete from database: {}", e),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Purge complètement le cache
|
||||
///
|
||||
/// Supprime toutes les images et vide la base de données. Opération irréversible.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/covers",
|
||||
responses(
|
||||
(status = 200, description = "Cache purgé avec succès", body = DeleteImageResponse),
|
||||
(status = 500, description = "Erreur lors de la purge", body = ErrorResponse)
|
||||
),
|
||||
tag = "covers"
|
||||
)]
|
||||
pub async fn purge_cache(State(cache): State<Arc<Cache>>) -> impl IntoResponse {
|
||||
match cache.purge().await {
|
||||
Ok(_) => (
|
||||
StatusCode::OK,
|
||||
Json(DeleteImageResponse {
|
||||
message: "Cache purged successfully".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: "PURGE_ERROR".to_string(),
|
||||
message: format!("Cannot purge cache: {}", e),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Consolide le cache
|
||||
///
|
||||
/// Re-télécharge les images manquantes et supprime les fichiers orphelins.
|
||||
/// Utile pour réparer un cache corrompu.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/covers/consolidate",
|
||||
responses(
|
||||
(status = 200, description = "Cache consolidé avec succès", body = DeleteImageResponse),
|
||||
(status = 500, description = "Erreur lors de la consolidation", body = ErrorResponse)
|
||||
),
|
||||
tag = "covers"
|
||||
)]
|
||||
pub async fn consolidate_cache(State(cache): State<Arc<Cache>>) -> impl IntoResponse {
|
||||
match cache.consolidate().await {
|
||||
Ok(_) => (
|
||||
StatusCode::OK,
|
||||
Json(DeleteImageResponse {
|
||||
message: "Cache consolidated successfully".to_string(),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ErrorResponse {
|
||||
error: "CONSOLIDATE_ERROR".to_string(),
|
||||
message: format!("Cannot consolidate cache: {}", e),
|
||||
}),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
//! Module de gestion du cache d'images avec conversion WebP
|
||||
//!
|
||||
//! Ce module étend le cache générique de `pmocache` avec des fonctionnalités
|
||||
//! spécifiques aux images : conversion WebP et génération de variantes.
|
||||
//! spécifiques aux images : conversion WebP automatique lors du téléchargement.
|
||||
|
||||
use anyhow::Result;
|
||||
use pmocache::{CacheConfig, FileCache};
|
||||
use crate::webp;
|
||||
use std::path::PathBuf;
|
||||
use std::ops::Deref;
|
||||
use pmocache::{CacheConfig, StreamTransformer};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Configuration pour le cache de couvertures
|
||||
pub struct CoversConfig;
|
||||
@@ -25,174 +23,60 @@ impl CacheConfig for CoversConfig {
|
||||
"image"
|
||||
}
|
||||
|
||||
/// Cache name (ex: "covers", "audio", "cache")
|
||||
fn cache_name() -> &'static str {
|
||||
"covers"
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache d'images avec conversion WebP et génération de variantes
|
||||
/// Type alias pour le cache de couvertures avec conversion WebP
|
||||
pub type Cache = pmocache::Cache<CoversConfig>;
|
||||
|
||||
/// Créateur de transformer WebP
|
||||
///
|
||||
/// Format des fichiers : `{pk}.{qualificatif}.webp`
|
||||
/// Exemple : `a1b2c3d4.orig.webp`, `a1b2c3d4.thumb.webp`
|
||||
/// Convertit automatiquement toute image téléchargée en format WebP
|
||||
fn create_webp_transformer() -> StreamTransformer {
|
||||
Box::new(|response, mut file, progress| {
|
||||
Box::pin(async move {
|
||||
// Télécharger tout en mémoire
|
||||
let bytes = response.bytes().await.map_err(|e| e.to_string())?;
|
||||
|
||||
// Convertir en WebP
|
||||
let img = image::load_from_memory(&bytes)
|
||||
.map_err(|e| format!("Image decode error: {}", e))?;
|
||||
let webp_data = crate::webp::encode_webp(&img)
|
||||
.map_err(|e| format!("WebP encode error: {}", e))?;
|
||||
|
||||
// Écrire et mettre à jour la progression
|
||||
use tokio::io::AsyncWriteExt;
|
||||
file.write_all(&webp_data).await.map_err(|e| e.to_string())?;
|
||||
file.flush().await.map_err(|e| e.to_string())?;
|
||||
progress(webp_data.len() as u64);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Crée un cache de couvertures avec conversion WebP automatique
|
||||
///
|
||||
/// Ce type est un wrapper autour de `pmocache::Cache<CoversConfig>` qui permet
|
||||
/// d'implémenter le trait `FileCache` avec conversion WebP automatique.
|
||||
#[derive(Debug)]
|
||||
pub struct Cache(pmocache::Cache<CoversConfig>);
|
||||
|
||||
impl Cache {
|
||||
/// Crée un nouveau cache d'images
|
||||
pub fn new(dir: &str, limit: usize, base_url: &str) -> Result<Self> {
|
||||
Ok(Self(pmocache::Cache::new(dir, limit, base_url)?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Permet d'accéder aux méthodes publiques de `pmocache::Cache` directement
|
||||
impl Deref for Cache {
|
||||
type Target = pmocache::Cache<CoversConfig>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Implémentation de FileCache pour Cache avec conversion WebP automatique
|
||||
impl FileCache for Cache {
|
||||
fn cache_type(&self) -> &str {
|
||||
CoversConfig::cache_type()
|
||||
}
|
||||
|
||||
fn validate_data(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||
// Convertir l'image en WebP
|
||||
let img = image::load_from_memory(data)?;
|
||||
webp::encode_webp(&img)
|
||||
}
|
||||
|
||||
async fn add_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
|
||||
let response = reqwest::get(url).await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow::anyhow!("Bad status: {}", response.status()));
|
||||
}
|
||||
|
||||
let data = response.bytes().await?;
|
||||
self.add(url, &data, collection).await
|
||||
}
|
||||
|
||||
async fn ensure_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
|
||||
let pk = pmocache::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
|
||||
}
|
||||
|
||||
async fn add(&self, url: &str, data: &[u8], collection: Option<&str>) -> Result<String> {
|
||||
// Valider et convertir les données en WebP
|
||||
let webp_data = self.validate_data(data)?;
|
||||
|
||||
let pk = pmocache::pk_from_url(url);
|
||||
let file_path = self.file_path(&pk);
|
||||
|
||||
if !file_path.exists() {
|
||||
tokio::fs::write(&file_path, &webp_data).await?;
|
||||
}
|
||||
|
||||
self.db.add(&pk, url, collection)?;
|
||||
Ok(pk)
|
||||
}
|
||||
|
||||
async fn get(&self, pk: &str) -> Result<PathBuf> {
|
||||
self.db.get(pk)?;
|
||||
self.db.update_hit(pk)?;
|
||||
|
||||
let file_path = self.file_path(pk);
|
||||
if file_path.exists() {
|
||||
Ok(file_path)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("File not found"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_collection(&self, collection: &str) -> Result<Vec<PathBuf>> {
|
||||
let entries = self.db.get_by_collection(collection)?;
|
||||
let mut paths = Vec::new();
|
||||
|
||||
for entry in entries {
|
||||
let path = self.file_path(&entry.pk);
|
||||
if path.exists() {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
async fn purge(&self) -> Result<()> {
|
||||
let cache_dir = PathBuf::from(self.get_cache_dir());
|
||||
let mut entries = tokio::fs::read_dir(&cache_dir).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
if entry.path().is_file() && entry.path() != cache_dir.join("cache.db") {
|
||||
tokio::fs::remove_file(entry.path()).await?;
|
||||
}
|
||||
}
|
||||
|
||||
self.db
|
||||
.purge()
|
||||
.map_err(|e| anyhow::anyhow!("Database error: {}", e))
|
||||
}
|
||||
|
||||
async fn consolidate(&self) -> Result<()> {
|
||||
// Récupérer la liste des entrées à traiter
|
||||
let entries = self.db.get_all()?;
|
||||
|
||||
// Supprimer les entrées sans fichiers correspondants
|
||||
for entry in entries {
|
||||
let file_path = self.file_path(&entry.pk);
|
||||
if !file_path.exists() {
|
||||
match reqwest::get(&entry.source_url).await {
|
||||
Ok(response) if response.status().is_success() => {
|
||||
let data = response.bytes().await?;
|
||||
self.add(&entry.source_url, &data, entry.collection.as_deref())
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
self.db.delete(&entry.pk)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer les fichiers sans entrées DB correspondantes
|
||||
let cache_dir_path = PathBuf::from(self.get_cache_dir());
|
||||
let mut dir_entries = tokio::fs::read_dir(&cache_dir_path).await?;
|
||||
while let Some(entry) = dir_entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if path.is_file() && path != cache_dir_path.join("cache.db") {
|
||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
// Format attendu: {pk}.{qualifier}.{EXT}
|
||||
if let Some(pk) = file_name.split('.').next() {
|
||||
if self.db.get(pk).is_err() {
|
||||
tokio::fs::remove_file(path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cache_dir(&self) -> String {
|
||||
self.get_cache_dir()
|
||||
}
|
||||
|
||||
fn get_base_url(&self) -> &str {
|
||||
self.get_base_url()
|
||||
}
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `dir` - Répertoire de stockage du cache
|
||||
/// * `limit` - Limite de taille du cache (nombre d'images)
|
||||
/// * `base_url` - URL de base pour la génération d'URLs
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Instance du cache configurée pour la conversion WebP automatique
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pmocovers::cache;
|
||||
///
|
||||
/// let cache = cache::new_cache("./cache", 1000, "http://localhost:8080").unwrap();
|
||||
/// ```
|
||||
pub fn new_cache(dir: &str, limit: usize, base_url: &str) -> Result<Cache> {
|
||||
let transformer_factory = Arc::new(|| create_webp_transformer());
|
||||
Cache::with_transformer(dir, limit, base_url, Some(transformer_factory))
|
||||
}
|
||||
|
||||
@@ -3,62 +3,24 @@
|
||||
//! Cette crate fournit un système de cache d'images optimisé pour les couvertures d'albums,
|
||||
//! avec conversion automatique en WebP et génération de variantes de tailles.
|
||||
//!
|
||||
//! ## Vue d'ensemble
|
||||
//! ## Fonctionnalités
|
||||
//!
|
||||
//! `pmocovers` gère le téléchargement, la conversion, le stockage et la distribution
|
||||
//! d'images de couvertures d'albums, avec :
|
||||
//! - Conversion automatique en WebP pour réduire la taille
|
||||
//! - Génération de variantes de tailles à la demande
|
||||
//! - Cache persistant avec base de données SQLite
|
||||
//! - API HTTP pour récupérer les images
|
||||
//!
|
||||
//! ## Fonctionnalités
|
||||
//!
|
||||
//! ### 📦 Gestion du cache
|
||||
//! - Téléchargement automatique depuis des URLs
|
||||
//! - Conversion des images en WebP (format optimisé)
|
||||
//! - Stockage persistant sur disque
|
||||
//! - Base de données SQLite pour le tracking
|
||||
//!
|
||||
//! ### 🎨 Génération de variantes
|
||||
//! - Redimensionnement automatique à la demande
|
||||
//! - Création d'images carrées avec centrage
|
||||
//! - Cache des variantes générées
|
||||
//! - Support de multiples tailles
|
||||
//!
|
||||
//! ### 📊 Statistiques d'utilisation
|
||||
//! - Comptage des accès (hits)
|
||||
//! - Suivi de la dernière utilisation
|
||||
//! - API de statistiques complètes
|
||||
//! - API HTTP complète (fournie par `pmocache`)
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! `pmocovers` suit le pattern d'extension des autres crates PMO :
|
||||
//! `pmocovers` est une spécialisation minimale de `pmocache` qui ajoute :
|
||||
//! 1. La conversion WebP automatique lors du téléchargement (via transformer)
|
||||
//! 2. La génération de variantes redimensionnées à la demande (via param generator)
|
||||
//!
|
||||
//! - `pmoserver` définit un serveur HTTP générique
|
||||
//! - `pmocovers` étend ce serveur avec des méthodes de cache via un trait
|
||||
//! - Le serveur n'a pas besoin de connaître `pmocovers`
|
||||
//!
|
||||
//! ## Structure des fichiers
|
||||
//!
|
||||
//! ```text
|
||||
//! pmocovers/
|
||||
//! ├── Cargo.toml
|
||||
//! ├── src/
|
||||
//! │ ├── lib.rs # Module principal (ce fichier)
|
||||
//! │ ├── cache.rs # Gestion du cache
|
||||
//! │ ├── db.rs # Base de données SQLite
|
||||
//! │ ├── webp.rs # Conversion et redimensionnement WebP
|
||||
//! │ └── pmoserver_impl.rs # Extension de pmoserver::Server
|
||||
//! └── cache/ # Répertoire de cache (généré)
|
||||
//! ├── cache.db # Base SQLite
|
||||
//! ├── *.orig.webp # Images originales
|
||||
//! └── *.{size}.webp # Variantes de tailles
|
||||
//! ```
|
||||
//! Tout le reste (API REST, serveur de fichiers, DB) est fourni par `pmocache`.
|
||||
//!
|
||||
//! ## Utilisation
|
||||
//!
|
||||
//! ### Exemple basique avec configuration automatique
|
||||
//! ### Exemple avec configuration automatique
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmocovers::CoverCacheExt;
|
||||
@@ -67,161 +29,56 @@
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> anyhow::Result<()> {
|
||||
//! let mut server = ServerBuilder::new_configured().build();
|
||||
//!
|
||||
//! // Utilise automatiquement la config (pmoconfig)
|
||||
//! server.init_cover_cache_configured().await?;
|
||||
//!
|
||||
//! server.start().await;
|
||||
//! server.wait().await;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Exemple avec paramètres personnalisés
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmocovers::CoverCacheExt;
|
||||
//! use pmoserver::ServerBuilder;
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> anyhow::Result<()> {
|
||||
//! let mut server = ServerBuilder::new("MyApp", "http://localhost:3000", 3000).build();
|
||||
//!
|
||||
//! // Paramètres personnalisés
|
||||
//! server.init_cover_cache("./cache", 1000).await?;
|
||||
//!
|
||||
//! server.start().await;
|
||||
//! server.wait().await;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Utilisation du cache directement
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmocovers::Cache;
|
||||
//! use pmocache::FileCache;
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> anyhow::Result<()> {
|
||||
//! let cache = Cache::new("./cache", 1000, "http://localhost:8080")?;
|
||||
//!
|
||||
//! // Ajouter une image depuis une URL (avec conversion WebP automatique)
|
||||
//! let pk = cache.add_from_url("http://example.com/cover.jpg", None).await?;
|
||||
//! println!("Image ajoutée avec clé: {}", pk);
|
||||
//!
|
||||
//! // Récupérer l'image originale
|
||||
//! let path = cache.get(&pk).await?;
|
||||
//! println!("Image stockée à: {:?}", path);
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## API HTTP
|
||||
//!
|
||||
//! Une fois enregistré sur un serveur via `CoverCacheExt`, les endpoints suivants sont disponibles :
|
||||
//!
|
||||
//! ### GET /covers/images/{pk}
|
||||
//! Récupère l'image originale en WebP
|
||||
//!
|
||||
//! ### GET /covers/images/{pk}/{size}
|
||||
//! Récupère une variante de taille spécifique (ex: `/covers/images/abc123/256`)
|
||||
//!
|
||||
//! ### GET /covers/stats
|
||||
//! Récupère les statistiques du cache (JSON)
|
||||
//!
|
||||
//! ## Format des clés (pk)
|
||||
//!
|
||||
//! Les images sont identifiées par une clé (pk) dérivée de l'URL source :
|
||||
//! - Hash SHA1 de l'URL
|
||||
//! - Encodé en hexadécimal (8 premiers octets)
|
||||
//! - Exemple: `"1a2b3c4d5e6f7a8b"`
|
||||
//!
|
||||
//! ## Stockage
|
||||
//!
|
||||
//! Les fichiers sont organisés comme suit :
|
||||
//!
|
||||
//! ```text
|
||||
//! cache/
|
||||
//! ├── cache.db # Base SQLite
|
||||
//! ├── 1a2b3c4d.orig.webp # Image originale
|
||||
//! ├── 1a2b3c4d.256.webp # Variante 256x256
|
||||
//! └── 1a2b3c4d.512.webp # Variante 512x512
|
||||
//! ```
|
||||
//!
|
||||
//! ## Opérations de maintenance
|
||||
//!
|
||||
//! ### Purge du cache
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use pmocovers::Cache;
|
||||
//! # async fn example(cache: &Cache) -> anyhow::Result<()> {
|
||||
//! // Supprimer tous les fichiers et entrées DB
|
||||
//! cache.purge().await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ### Consolidation du cache
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use pmocovers::Cache;
|
||||
//! # async fn example(cache: &Cache) -> anyhow::Result<()> {
|
||||
//! // Re-télécharger les images manquantes et supprimer les orphelins
|
||||
//! cache.consolidate().await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Dépendances principales
|
||||
//!
|
||||
//! - `image` : Chargement et manipulation d'images
|
||||
//! - `webp` : Encodage WebP
|
||||
//! - `rusqlite` : Base de données SQLite
|
||||
//! - `reqwest` : Téléchargement HTTP
|
||||
//! - `sha1` : Génération de clés
|
||||
//!
|
||||
//! ## Voir aussi
|
||||
//!
|
||||
//! - [`pmoserver`] : Serveur HTTP Axum
|
||||
//! - [`pmoapp`] : Application web frontend
|
||||
//! - [`pmoupnp`] : Bibliothèque UPnP MediaRenderer
|
||||
|
||||
pub mod cache;
|
||||
pub mod db;
|
||||
pub mod webp;
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub mod api;
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub mod openapi;
|
||||
|
||||
pub use cache::{Cache, CoversConfig};
|
||||
pub use db::{CacheEntry, DB};
|
||||
pub use cache::{Cache, CoversConfig, new_cache};
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub use openapi::ApiDoc;
|
||||
|
||||
use anyhow::Result;
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use utoipa::OpenApi;
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Trait pour étendre un serveur HTTP avec des fonctionnalités de cache d'images.
|
||||
/// Générateur de variantes d'images
|
||||
///
|
||||
/// Ce trait permet à `pmocovers` d'ajouter des méthodes d'extension sur des types
|
||||
/// de serveurs externes (comme `pmoserver::Server`) sans que ces crates dépendent de `pmocovers`.
|
||||
///
|
||||
/// # Architecture
|
||||
///
|
||||
/// Similaire au pattern utilisé par `pmoapp` pour `WebAppExt`, ce trait permet
|
||||
/// une extension propre et découplée :
|
||||
///
|
||||
/// - `pmoserver` définit un serveur HTTP générique
|
||||
/// - `pmocovers` étend ce serveur avec des méthodes de cache via ce trait
|
||||
/// - Le serveur n'a pas besoin de connaître `pmocovers`
|
||||
/// Si param est numérique, génère une variante redimensionnée
|
||||
#[cfg(feature = "pmoserver")]
|
||||
fn create_variant_generator() -> pmocache::pmoserver_ext::ParamGenerator<CoversConfig> {
|
||||
Arc::new(|cache, pk, param| {
|
||||
Box::pin(async move {
|
||||
// Si le param est numérique, c'est une taille de variante
|
||||
if let Ok(size) = param.parse::<usize>() {
|
||||
match webp::generate_variant(&cache, &pk, size).await {
|
||||
Ok(data) => return Some(data),
|
||||
Err(e) => {
|
||||
tracing::warn!("Cannot generate variant {}x{} for {}: {}", size, size, pk, e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Param non reconnu
|
||||
None
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Trait d'extension pour ajouter le cache de couvertures à pmoserver
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub trait CoverCacheExt {
|
||||
/// Initialise le cache d'images et enregistre les routes HTTP.
|
||||
/// Initialise le cache d'images et enregistre les routes HTTP
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -230,49 +87,59 @@ pub trait CoverCacheExt {
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Arc<Cache>` - Instance partagée du cache
|
||||
/// Instance partagée du cache
|
||||
///
|
||||
/// # Routes enregistrées
|
||||
///
|
||||
/// - `GET /covers/images/{pk}` - Image originale
|
||||
/// - `GET /covers/images/{pk}/{size}` - Variante de taille
|
||||
/// - `GET /covers/stats` - Statistiques
|
||||
/// - `GET /covers/image/{pk}` - Image originale
|
||||
/// - `GET /covers/image/{pk}/{size}` - Variante de taille (ex: 256, 512)
|
||||
/// - `GET /api/covers` - Liste des images (API REST)
|
||||
/// - `POST /api/covers` - Ajouter une image (API REST)
|
||||
/// - `DELETE /api/covers/{pk}` - Supprimer une image (API REST)
|
||||
/// - `GET /swagger-ui` - Documentation interactive
|
||||
async fn init_cover_cache(&mut self, cache_dir: &str, limit: usize) -> Result<Arc<Cache>>;
|
||||
/// - `GET /api/covers/{pk}/status` - Statut du téléchargement
|
||||
/// - `GET /swagger-ui/covers` - Documentation interactive
|
||||
async fn init_cover_cache(&mut self, cache_dir: &str, limit: usize)
|
||||
-> anyhow::Result<Arc<Cache>>;
|
||||
|
||||
/// Initialise le cache d'images avec la configuration par défaut.
|
||||
/// Initialise le cache d'images avec la configuration par défaut
|
||||
///
|
||||
/// Utilise automatiquement les paramètres de `pmoconfig::Config` :
|
||||
/// - `host.cover_cache.directory` pour le répertoire
|
||||
/// - `host.cover_cache.size` pour la limite de taille
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Arc<Cache>` - Instance partagée du cache
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pmocovers::CoverCacheExt;
|
||||
/// use pmoserver::ServerBuilder;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> anyhow::Result<()> {
|
||||
/// let mut server = ServerBuilder::new_configured().build();
|
||||
///
|
||||
/// // Utilise automatiquement la config
|
||||
/// server.init_cover_cache_configured().await?;
|
||||
///
|
||||
/// server.start().await;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
async fn init_cover_cache_configured(&mut self) -> Result<Arc<Cache>>;
|
||||
/// Utilise automatiquement les paramètres de `pmoconfig::Config`
|
||||
async fn init_cover_cache_configured(&mut self)
|
||||
-> anyhow::Result<Arc<Cache>>;
|
||||
}
|
||||
|
||||
// Implémentation du trait pour pmoserver::Server (feature-gated)
|
||||
#[cfg(feature = "pmoserver")]
|
||||
mod pmoserver_impl;
|
||||
impl CoverCacheExt for pmoserver::Server {
|
||||
async fn init_cover_cache(&mut self, cache_dir: &str, limit: usize)
|
||||
-> anyhow::Result<Arc<Cache>> {
|
||||
use pmocache::pmoserver_ext::{create_file_router_with_generator, create_api_router};
|
||||
|
||||
let base_url = self.info().base_url;
|
||||
let cache = Arc::new(cache::new_cache(cache_dir, limit, &base_url)?);
|
||||
|
||||
// Router de fichiers avec génération de variantes
|
||||
// Routes: GET /covers/image/{pk} et GET /covers/image/{pk}/{size}
|
||||
let file_router = create_file_router_with_generator(
|
||||
cache.clone(),
|
||||
"image/webp",
|
||||
Some(create_variant_generator())
|
||||
);
|
||||
self.add_router("/", file_router).await;
|
||||
|
||||
// API REST générique (pmocache)
|
||||
// Routes: GET/POST/DELETE /api/covers, etc.
|
||||
let api_router = create_api_router(cache.clone());
|
||||
let openapi = crate::ApiDoc::openapi();
|
||||
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()?;
|
||||
self.init_cover_cache(&cache_dir, limit).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
//! Documentation OpenAPI pour l'API REST du cache de couvertures
|
||||
//!
|
||||
//! Ce module fournit une documentation OpenAPI simple pour l'API REST
|
||||
//! fournie par pmocache, spécialisée pour les images de couvertures.
|
||||
|
||||
use utoipa::OpenApi;
|
||||
|
||||
/// Documentation OpenAPI pour l'API PMOCovers
|
||||
///
|
||||
/// L'API réutilise les handlers génériques de pmocache.
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
crate::api::list_images,
|
||||
crate::api::get_image_info,
|
||||
crate::api::add_image,
|
||||
crate::api::delete_image,
|
||||
crate::api::purge_cache,
|
||||
crate::api::consolidate_cache,
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
crate::db::CacheEntry,
|
||||
crate::api::AddImageRequest,
|
||||
crate::api::AddImageResponse,
|
||||
crate::api::DeleteImageResponse,
|
||||
crate::api::ErrorResponse,
|
||||
pmocache::CacheEntry,
|
||||
pmocache::api::AddItemRequest,
|
||||
pmocache::api::AddItemResponse,
|
||||
pmocache::api::DeleteItemResponse,
|
||||
pmocache::api::ErrorResponse,
|
||||
pmocache::api::DownloadStatus,
|
||||
)
|
||||
),
|
||||
tags(
|
||||
@@ -38,6 +37,38 @@ Cette API permet de gérer un cache d'images optimisé pour les couvertures d'al
|
||||
- **Consultation** : Liste des images avec statistiques d'utilisation
|
||||
- **Suppression** : Suppression individuelle ou purge complète
|
||||
- **Maintenance** : Consolidation du cache pour réparer les incohérences
|
||||
- **Statut** : Suivi des téléchargements en cours
|
||||
|
||||
## Endpoints principaux
|
||||
|
||||
### GET /api/covers
|
||||
Liste toutes les images en cache avec leurs statistiques
|
||||
|
||||
### POST /api/covers
|
||||
Ajoute une image depuis une URL (conversion WebP automatique)
|
||||
|
||||
### GET /api/covers/{pk}
|
||||
Récupère les informations d'une image
|
||||
|
||||
### DELETE /api/covers/{pk}
|
||||
Supprime une image et ses variantes
|
||||
|
||||
### GET /api/covers/{pk}/status
|
||||
Récupère le statut du téléchargement
|
||||
|
||||
### DELETE /api/covers
|
||||
Purge complètement le cache
|
||||
|
||||
### POST /api/covers/consolidate
|
||||
Consolide le cache (répare les incohérences)
|
||||
|
||||
## Servir les fichiers
|
||||
|
||||
### GET /covers/image/{pk}
|
||||
Récupère l'image originale en WebP
|
||||
|
||||
### GET /covers/image/{pk}/{size}
|
||||
Récupère une variante redimensionnée (ex: /covers/image/abc123/256)
|
||||
|
||||
## Format des images
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
//! 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::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use pmoserver::Server;
|
||||
use tracing::{info, warn};
|
||||
use std::sync::Arc;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
/// Handler pour GET /covers/images/{pk}/{size}
|
||||
/// Génère une variante d'image à la demande
|
||||
async fn get_cover_variant(
|
||||
State(cache): State<Arc<Cache>>,
|
||||
Path((pk, size)): Path<(String, String)>,
|
||||
) -> Response {
|
||||
let size = match size.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(e) => {
|
||||
warn!("Cannot generate variant for {}: {}", pk, e);
|
||||
(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>> {
|
||||
// Utiliser l'URL du serveur comme base_url
|
||||
let base_url = self.info().base_url;
|
||||
let cache = Arc::new(Cache::new(cache_dir, limit, &base_url)?);
|
||||
|
||||
// Utiliser le router générique de pmocache pour servir les fichiers
|
||||
// Routes: GET /covers/images/{pk} et GET /covers/images/{pk}/{param}
|
||||
let file_router = pmocache::pmoserver_ext::create_file_router(
|
||||
cache.clone(),
|
||||
"image/webp"
|
||||
);
|
||||
self.add_router("/covers/images", file_router).await;
|
||||
|
||||
// Route pour générer les variantes à la demande (redimensionnement)
|
||||
// Note: Cette route est spécifique à pmocovers car elle nécessite generate_variant
|
||||
let variant_router = Router::new()
|
||||
.route("/{pk}/{size}", get(get_cover_variant))
|
||||
.with_state(cache.clone());
|
||||
self.add_router("/covers/variants", variant_router).await;
|
||||
|
||||
// Route pour les stats
|
||||
self.add_handler_with_state("/covers/stats", get_cover_stats, cache.clone()).await;
|
||||
|
||||
// Router API RESTful qui sera nesté sous /api/covers par add_openapi
|
||||
let api_router = Router::new()
|
||||
// Liste et ajout
|
||||
.route(
|
||||
"/",
|
||||
get(api::list_images) // GET /api/covers
|
||||
.post(api::add_image) // POST /api/covers
|
||||
.delete(api::purge_cache), // DELETE /api/covers
|
||||
)
|
||||
// Ressource unique
|
||||
.route(
|
||||
"/{pk}",
|
||||
get(api::get_image_info) // GET /api/covers/{pk}
|
||||
.delete(api::delete_image), // DELETE /api/covers/{pk}
|
||||
)
|
||||
// Action spécifique
|
||||
.route(
|
||||
"/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
|
||||
// Le router sera nesté automatiquement sous /api/covers par add_openapi
|
||||
// Routes finales: /api/covers, /api/covers/{pk}, /api/covers/consolidate
|
||||
// Swagger UI sera disponible à /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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user