Refactoring manuel
This commit is contained in:
44
Cargo.lock
generated
44
Cargo.lock
generated
@@ -38,18 +38,6 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -352,7 +340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "239c3f54e9e25a01e4d0f54a3a066e1d8d6d5509b071a78c729c1e0143db7f4b"
|
||||
dependencies = [
|
||||
"critical-section",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
"futures-channel",
|
||||
"hashbrown 0.16.0",
|
||||
"js-sys",
|
||||
@@ -385,7 +373,7 @@ dependencies = [
|
||||
"disqualified",
|
||||
"downcast-rs",
|
||||
"erased-serde",
|
||||
"foldhash",
|
||||
"foldhash 0.2.0",
|
||||
"glam",
|
||||
"inventory",
|
||||
"serde",
|
||||
@@ -1005,6 +993,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
@@ -1257,11 +1251,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1276,11 +1270,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1739,9 +1733,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.30.1"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||
checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
@@ -2283,6 +2277,7 @@ name = "pmocache"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"chrono",
|
||||
"hex",
|
||||
"reqwest",
|
||||
@@ -2290,6 +2285,7 @@ dependencies = [
|
||||
"serde",
|
||||
"sha1",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
@@ -2920,9 +2916,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.32.1"
|
||||
version = "0.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||
checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f"
|
||||
dependencies = [
|
||||
"bitflags 2.9.4",
|
||||
"fallible-iterator",
|
||||
|
||||
@@ -11,7 +11,7 @@ pmocache = { path = "../pmocache" }
|
||||
pmodidl = { path = "../pmodidl" }
|
||||
|
||||
# Base de données
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
rusqlite = { version = "0.37", features = ["bundled"] }
|
||||
chrono = "0.4"
|
||||
|
||||
# Métadonnées audio
|
||||
|
||||
@@ -234,7 +234,9 @@
|
||||
//! - [`pmocovers`] : Cache d'images
|
||||
//! - [`pmoserver`] : Serveur HTTP
|
||||
|
||||
pub mod cache;
|
||||
mod pmoserver_ext;
|
||||
mod cache;
|
||||
|
||||
pub mod metadata;
|
||||
pub mod flac;
|
||||
pub mod db;
|
||||
@@ -243,29 +245,9 @@ pub use cache::AudioCache;
|
||||
pub use metadata::AudioMetadata;
|
||||
pub use db::{AudioDB, AudioCacheEntry};
|
||||
|
||||
/// Trait pour étendre un serveur HTTP avec des fonctionnalités de cache audio.
|
||||
///
|
||||
/// Ce trait permet à `pmoaudiocache` d'ajouter des méthodes d'extension sur des types
|
||||
/// de serveurs externes (comme `pmoserver::Server`) sans que ces crates dépendent de `pmoaudiocache`.
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub trait AudioCacheExt {
|
||||
/// Initialise le cache audio et enregistre les routes HTTP.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cache_dir` - Répertoire de stockage du cache
|
||||
/// * `limit` - Limite de taille du cache (en nombre de pistes)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Arc<AudioCache>` - Instance partagée du cache
|
||||
async fn init_audio_cache(&mut self, cache_dir: &str, limit: usize) -> anyhow::Result<std::sync::Arc<AudioCache>>;
|
||||
pub use pmoserver_ext::AudioCacheExt;
|
||||
|
||||
|
||||
/// Initialise le cache audio avec la configuration par défaut.
|
||||
///
|
||||
/// Utilise automatiquement les paramètres de `pmoconfig::Config`.
|
||||
async fn init_audio_cache_configured(&mut self) -> anyhow::Result<std::sync::Arc<AudioCache>>;
|
||||
}
|
||||
|
||||
// Implémentation du trait pour pmoserver::Server (feature-gated)
|
||||
#[cfg(feature = "pmoserver")]
|
||||
|
||||
26
pmoaudiocache/src/pmoserver_ext.rs
Normal file
26
pmoaudiocache/src/pmoserver_ext.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use crate::AudioCache;
|
||||
|
||||
/// Trait pour étendre un serveur HTTP avec des fonctionnalités de cache audio.
|
||||
///
|
||||
/// Ce trait permet à `pmoaudiocache` d'ajouter des méthodes d'extension sur des types
|
||||
/// de serveurs externes (comme `pmoserver::Server`) sans que ces crates dépendent de `pmoaudiocache`.
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub trait AudioCacheExt {
|
||||
/// Initialise le cache audio et enregistre les routes HTTP.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cache_dir` - Répertoire de stockage du cache
|
||||
/// * `limit` - Limite de taille du cache (en nombre de pistes)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Arc<AudioCache>` - Instance partagée du cache
|
||||
async fn init_audio_cache(&mut self, cache_dir: &str, limit: usize) -> anyhow::Result<std::sync::Arc<AudioCache>>;
|
||||
|
||||
/// Initialise le cache audio avec la configuration par défaut.
|
||||
///
|
||||
/// Utilise automatiquement les paramètres de `pmoconfig::Config`.
|
||||
async fn init_audio_cache_configured(&mut self) -> anyhow::Result<std::sync::Arc<AudioCache>>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Implémentation du trait AudioCacheExt pour le serveur pmoserver
|
||||
|
||||
use crate::{api, AudioCache, AudioCacheExt};
|
||||
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
@@ -142,6 +142,7 @@ async fn list_collections(State(cache): State<Arc<AudioCache>>) -> Response {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
impl AudioCacheExt for Server {
|
||||
async fn init_audio_cache(
|
||||
&mut self,
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
# Base de données
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
rusqlite = { version = "0.37.0", features = ["bundled"] }
|
||||
|
||||
# HTTP client
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
@@ -25,6 +25,11 @@ tokio = { version = "1.0", features = ["full"] }
|
||||
# Feature pour OpenAPI
|
||||
utoipa = { version = "5.3", optional = true }
|
||||
|
||||
# Feature pour pmoserver (extension HTTP)
|
||||
axum = { version = "0.8", optional = true }
|
||||
tracing = { version = "0.1", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
openapi = ["dep:utoipa"]
|
||||
pmoserver = ["dep:axum", "dep:tracing"]
|
||||
|
||||
@@ -3,35 +3,32 @@
|
||||
//! 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 std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use anyhow::{anyhow, Result};
|
||||
use sha1::{Sha1, Digest};
|
||||
use tokio::sync::Mutex;
|
||||
use crate::cache_trait::FileCache;
|
||||
use crate::db::DB;
|
||||
use anyhow::{anyhow, Result};
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Configuration du cache
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CacheConfig {
|
||||
/// Répertoire de stockage
|
||||
pub dir: PathBuf,
|
||||
/// Limite de taille du cache (nombre d'éléments)
|
||||
pub limit: usize,
|
||||
/// Nom de la table dans la base de données
|
||||
pub table_name: String,
|
||||
/// Extension des fichiers dans le cache
|
||||
pub file_extension: String,
|
||||
}
|
||||
|
||||
impl CacheConfig {
|
||||
/// Crée une nouvelle configuration de cache
|
||||
pub fn new(dir: &str, limit: usize, table_name: &str, file_extension: &str) -> Self {
|
||||
Self {
|
||||
dir: PathBuf::from(dir),
|
||||
limit,
|
||||
table_name: table_name.to_string(),
|
||||
file_extension: file_extension.to_string(),
|
||||
}
|
||||
/// Trait pour définir les paramètres du cache
|
||||
pub trait CacheConfig: Send + Sync {
|
||||
/// Extension des fichiers (ex: "webp", "flac")
|
||||
fn file_extension() -> &'static str;
|
||||
/// Nom de la table dans la base de données (ex: "covers", "audio")
|
||||
fn table_name() -> &'static str {
|
||||
"cached_items"
|
||||
}
|
||||
/// Type de cache (ex: "audio", "image")
|
||||
fn cache_type() -> &'static str {
|
||||
"file"
|
||||
}
|
||||
/// Cache name (ex: "covers", "audio", "cache")
|
||||
fn cache_name() -> &'static str {
|
||||
"cache"
|
||||
}
|
||||
/// Default param extension ("orig")
|
||||
fn default_param() -> &'static str {
|
||||
"orig"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,42 +37,45 @@ impl CacheConfig {
|
||||
/// Gère le téléchargement, le stockage et la récupération de fichiers
|
||||
/// avec une base de données SQLite pour les métadonnées.
|
||||
///
|
||||
/// # Paramètres de type
|
||||
///
|
||||
/// * `C` - Configuration du cache (implémente `CacheConfig`)
|
||||
///
|
||||
/// Note : Ce type est conçu pour être utilisé derrière un `Arc<Cache>`.
|
||||
/// Les méthodes prennent `&self` et utilisent des `Arc` et `Mutex` internes
|
||||
/// pour la synchronisation.
|
||||
/// La synchronisation est gérée par le Mutex interne de la base de données SQLite.
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
pub(crate) config: CacheConfig,
|
||||
pub struct Cache<C: CacheConfig> {
|
||||
/// Répertoire de stockage
|
||||
dir: PathBuf,
|
||||
/// Limite de taille du cache (nombre d'éléments)
|
||||
limit: usize,
|
||||
/// URL de base pour la génération d'URLs
|
||||
base_url: String,
|
||||
/// Base de données SQLite
|
||||
pub db: Arc<DB>,
|
||||
mu: Arc<Mutex<()>>,
|
||||
/// Phantom data pour le type de configuration
|
||||
_phantom: std::marker::PhantomData<C>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
/// Crée un nouveau cache avec la configuration spécifiée
|
||||
impl<C: CacheConfig> Cache<C> {
|
||||
/// Crée un nouveau cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `config` - Configuration du cache
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pmocache::cache::{Cache, CacheConfig};
|
||||
///
|
||||
/// let config = CacheConfig::new("./cache", 1000, "my_cache", "webp");
|
||||
/// let cache = Cache::new(config).unwrap();
|
||||
/// ```
|
||||
pub fn new(config: CacheConfig) -> Result<Self> {
|
||||
std::fs::create_dir_all(&config.dir)?;
|
||||
let db = DB::init(
|
||||
&config.dir.join("cache.db"),
|
||||
&config.table_name
|
||||
)?;
|
||||
/// * `dir` - Répertoire de stockage du cache
|
||||
/// * `limit` - Limite de taille du cache (nombre d'éléments)
|
||||
/// * `base_url` - URL de base pour la génération d'URLs
|
||||
pub fn new(dir: &str, limit: usize, base_url: &str) -> Result<Self> {
|
||||
let directory = PathBuf::from(dir);
|
||||
std::fs::create_dir_all(&directory)?;
|
||||
let db = DB::init(&directory.join("cache.db"), C::table_name())?;
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
dir: directory,
|
||||
limit,
|
||||
base_url: base_url.to_string(),
|
||||
db: Arc::new(db),
|
||||
mu: Arc::new(Mutex::new(())),
|
||||
_phantom: std::marker::PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -120,30 +120,6 @@ impl Cache {
|
||||
self.add_from_url(url, collection).await
|
||||
}
|
||||
|
||||
/// Ajoute des données au cache
|
||||
///
|
||||
/// Cette méthode doit être surchargée par les implémentations spécifiques
|
||||
/// pour gérer la conversion et le stockage des données.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL source du fichier
|
||||
/// * `data` - Données brutes à stocker
|
||||
/// * `collection` - Collection optionnelle à laquelle appartient le fichier
|
||||
pub async fn add(&self, url: &str, data: &[u8], collection: Option<&str>) -> Result<String> {
|
||||
let pk = pk_from_url(url);
|
||||
let file_path = self.file_path(&pk);
|
||||
|
||||
let _lock = self.mu.lock().await;
|
||||
|
||||
if !file_path.exists() {
|
||||
// Par défaut, on stocke les données telles quelles
|
||||
tokio::fs::write(&file_path, data).await?;
|
||||
}
|
||||
|
||||
self.db.add(&pk, url, collection)?;
|
||||
Ok(pk)
|
||||
}
|
||||
|
||||
/// Récupère le chemin d'un fichier dans le cache
|
||||
///
|
||||
@@ -151,8 +127,6 @@ impl Cache {
|
||||
///
|
||||
/// * `pk` - Clé primaire du fichier
|
||||
pub async fn get(&self, pk: &str) -> Result<PathBuf> {
|
||||
let _lock = self.mu.lock().await;
|
||||
|
||||
self.db.get(pk)?;
|
||||
self.db.update_hit(pk)?;
|
||||
|
||||
@@ -170,8 +144,6 @@ impl Cache {
|
||||
///
|
||||
/// * `collection` - Identifiant de la collection
|
||||
pub async fn get_collection(&self, collection: &str) -> Result<Vec<PathBuf>> {
|
||||
let _lock = self.mu.lock().await;
|
||||
|
||||
let entries = self.db.get_by_collection(collection)?;
|
||||
let mut paths = Vec::new();
|
||||
|
||||
@@ -187,25 +159,22 @@ impl Cache {
|
||||
|
||||
/// Supprime tous les fichiers et entrées du cache
|
||||
pub async fn purge(&self) -> Result<()> {
|
||||
let _lock = self.mu.lock().await;
|
||||
|
||||
let mut entries = tokio::fs::read_dir(&self.config.dir).await?;
|
||||
let mut entries = tokio::fs::read_dir(&self.dir).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
if entry.path().is_file() && entry.path() != self.config.dir.join("cache.db") {
|
||||
if entry.path().is_file() && entry.path() != self.dir.join("cache.db") {
|
||||
tokio::fs::remove_file(entry.path()).await?;
|
||||
}
|
||||
}
|
||||
|
||||
self.db.purge().map_err(|e| anyhow!("Database error: {}", e))
|
||||
self.db
|
||||
.purge()
|
||||
.map_err(|e| anyhow!("Database error: {}", e))
|
||||
}
|
||||
|
||||
/// Consolide le cache en supprimant les orphelins et en re-téléchargeant les fichiers manquants
|
||||
pub async fn consolidate(&self) -> Result<()> {
|
||||
// Récupérer la liste des entrées à traiter
|
||||
let entries = {
|
||||
let _lock = self.mu.lock().await;
|
||||
self.db.get_all()?
|
||||
};
|
||||
let entries = self.db.get_all()?;
|
||||
|
||||
// Supprimer les entrées sans fichiers correspondants
|
||||
for entry in entries {
|
||||
@@ -214,10 +183,10 @@ impl Cache {
|
||||
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.add(&entry.source_url, &data, entry.collection.as_deref())
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
let _lock = self.mu.lock().await;
|
||||
self.db.delete(&entry.pk)?;
|
||||
}
|
||||
}
|
||||
@@ -225,15 +194,17 @@ impl Cache {
|
||||
}
|
||||
|
||||
// Supprimer les fichiers sans entrées DB correspondantes
|
||||
let _lock = self.mu.lock().await;
|
||||
let mut dir_entries = tokio::fs::read_dir(&self.config.dir).await?;
|
||||
let mut dir_entries = tokio::fs::read_dir(&self.dir).await?;
|
||||
while let Some(entry) = dir_entries.next_entry().await? {
|
||||
let path = entry.path();
|
||||
if path.is_file() && path != self.config.dir.join("cache.db") {
|
||||
if path.is_file() && path != self.dir.join("cache.db") {
|
||||
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
let pk = file_name.trim_end_matches(&format!(".{}", self.config.file_extension));
|
||||
if self.db.get(pk).is_err() {
|
||||
tokio::fs::remove_file(path).await?;
|
||||
// Format attendu: {pk}.{qualifier}.{EXT}
|
||||
// On extrait le pk (première partie avant le premier point)
|
||||
if let Some(pk) = file_name.split('.').next() {
|
||||
if self.db.get(pk).is_err() {
|
||||
tokio::fs::remove_file(path).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -243,22 +214,78 @@ impl Cache {
|
||||
}
|
||||
|
||||
/// Retourne le répertoire du cache
|
||||
pub fn cache_dir(&self) -> String {
|
||||
self.config.dir.to_string_lossy().to_string()
|
||||
pub fn cache_dir(&self) -> &Path {
|
||||
&self.dir
|
||||
}
|
||||
|
||||
/// Construit le chemin complet d'un fichier dans le cache
|
||||
fn file_path(&self, pk: &str) -> PathBuf {
|
||||
self.config.dir.join(format!("{}.{}", pk, self.config.file_extension))
|
||||
/// Retourne l'URL de base
|
||||
pub fn get_base_url(&self) -> &str {
|
||||
&self.base_url
|
||||
}
|
||||
|
||||
/// Valide les données avant de les stocker
|
||||
/// Par défaut, accepte toutes les données
|
||||
pub fn validate_data(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||
Ok(data.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
/// Génère une clé primaire à partir d'une URL
|
||||
///
|
||||
/// Utilise SHA1 pour hasher l'URL et retourne les 8 premiers octets en hexadécimal.
|
||||
pub fn pk_from_url(url: &str) -> String {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(url.as_bytes());
|
||||
let result = hasher.finalize();
|
||||
hex::encode(&result[..8])
|
||||
|
||||
/// Implémentation du trait FileCache pour Cache
|
||||
impl<C: CacheConfig> FileCache for Cache<C> {
|
||||
fn cache_type(&self) -> &str {
|
||||
C::cache_type()
|
||||
}
|
||||
|
||||
fn validate_data(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||
// Le cache générique accepte toutes les données
|
||||
Ok(data.to_vec())
|
||||
}
|
||||
|
||||
async fn add_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
|
||||
self.add_from_url(url, collection).await
|
||||
}
|
||||
|
||||
async fn ensure_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
|
||||
self.ensure_from_url(url, collection).await
|
||||
}
|
||||
|
||||
async fn add(&self, url: &str, data: &[u8], collection: Option<&str>) -> Result<String> {
|
||||
// Valider les données avant de les ajouter
|
||||
let validated_data = self.validate_data(data)?;
|
||||
|
||||
let pk = pk_from_url(url);
|
||||
let file_path = self.file_path(&pk);
|
||||
|
||||
if !file_path.exists() {
|
||||
tokio::fs::write(&file_path, &validated_data).await?;
|
||||
}
|
||||
|
||||
self.db.add(&pk, url, collection)?;
|
||||
Ok(pk)
|
||||
}
|
||||
|
||||
async fn get(&self, pk: &str) -> Result<PathBuf> {
|
||||
self.get(pk).await
|
||||
}
|
||||
|
||||
async fn get_collection(&self, collection: &str) -> Result<Vec<PathBuf>> {
|
||||
self.get_collection(collection).await
|
||||
}
|
||||
|
||||
async fn purge(&self) -> Result<()> {
|
||||
self.purge().await
|
||||
}
|
||||
|
||||
async fn consolidate(&self) -> Result<()> {
|
||||
self.consolidate().await
|
||||
}
|
||||
|
||||
fn get_cache_dir(&self) -> String {
|
||||
self.cache_dir()
|
||||
}
|
||||
|
||||
fn get_base_url(&self) -> &str {
|
||||
self.get_base_url()
|
||||
}
|
||||
}
|
||||
|
||||
126
pmocache/src/cache_trait.rs
Normal file
126
pmocache/src/cache_trait.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use anyhow::Result;
|
||||
use sha1::{Digest, Sha1};
|
||||
use std::{path::{Path, PathBuf}, sync::Arc};
|
||||
|
||||
use crate::{CacheConfig, DB};
|
||||
|
||||
/// Trait générique pour les caches de fichiers
|
||||
///
|
||||
/// Définit l'interface commune pour tous les types de caches (images, audio, etc.)
|
||||
pub trait FileCache<C: CacheConfig>: Send + Sync {
|
||||
|
||||
fn get_cache_dir(&self) -> &Path;
|
||||
fn get_database(&self) -> Arc<DB>;
|
||||
fn get_base_url(&self) -> &str;
|
||||
/// Valide les données avant de les stocker dans le cache
|
||||
///
|
||||
/// Cette méthode peut être surchargée pour vérifier le type MIME,
|
||||
/// le magic number, ou effectuer des conversions (ex: WebP, FLAC)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `data` - Données brutes à valider
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Les données validées/converties ou une erreur
|
||||
fn validate_data(&self, data: &[u8]) -> Result<Vec<u8>> {
|
||||
// Par défaut, on accepte les données telles quelles
|
||||
Ok(data.to_vec())
|
||||
}
|
||||
|
||||
/// Retourne le type de cache
|
||||
fn cache_type(&self) -> &'static str {
|
||||
C::cache_type()
|
||||
}
|
||||
|
||||
/// Retourne le type de cache
|
||||
fn cache_name(&self) -> &'static str {
|
||||
C::cache_name()
|
||||
}
|
||||
|
||||
/// Retourne le type de cache
|
||||
fn default_param(&self) -> &'static str {
|
||||
C::default_param()
|
||||
}
|
||||
|
||||
/// Retourne l'extension des fichiers
|
||||
fn file_extension(&self) -> &'static str {
|
||||
C::file_extension()
|
||||
}
|
||||
|
||||
/// Retourne le nom de la table
|
||||
fn table_name(&self) -> &'static str {
|
||||
C::table_name()
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Construit le chemin complet d'un fichier dans le cache
|
||||
///
|
||||
/// Format: `{pk}.{qualificatif}.{extension}`
|
||||
/// Pour le fichier original: `{pk}.orig.{extension}`
|
||||
fn file_path(&self, pk: &str) -> PathBuf {
|
||||
self.file_path_with_qualifier(pk, self.default_param())
|
||||
}
|
||||
|
||||
/// Construit le chemin d'un fichier avec un qualificatif
|
||||
///
|
||||
/// Format: `{pk}.{qualificatif}.{extension}`
|
||||
fn file_path_with_qualifier(&self, pk: &str, qualifier: &str) -> PathBuf {
|
||||
self.get_cache_dir().join(format!("{}.{}.{}", pk, qualifier, C::file_extension()))
|
||||
}
|
||||
|
||||
/// Télécharge un fichier depuis une URL et l'ajoute au cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL du fichier à télécharger
|
||||
/// * `collection` - Collection optionnelle à laquelle appartient le fichier
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// La clé primaire (pk) du fichier dans le cache
|
||||
async fn add_from_url(&self, url: &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
|
||||
///
|
||||
/// * `pk` - Clé primaire du fichier
|
||||
async fn get(&self, pk: &str) -> Result<PathBuf>;
|
||||
|
||||
/// Récupère tous les fichiers d'une collection
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `collection` - Identifiant de la collection
|
||||
async fn get_collection(&self, collection: &str) -> Result<Vec<PathBuf>>;
|
||||
|
||||
/// Supprime tous les fichiers et entrées du cache
|
||||
async fn purge(&self) -> Result<()>;
|
||||
|
||||
/// Consolide le cache en supprimant les orphelins et en re-téléchargeant les fichiers manquants
|
||||
async fn consolidate(&self) -> Result<()>;
|
||||
|
||||
}
|
||||
|
||||
/// Génère une clé primaire à partir d'une URL
|
||||
///
|
||||
/// Utilise SHA1 pour hasher l'URL et retourne les 8 premiers octets en hexadécimal.
|
||||
pub fn pk_from_url(url: &str) -> String {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(url.as_bytes());
|
||||
let result = hasher.finalize();
|
||||
hex::encode(&result[..8])
|
||||
}
|
||||
@@ -34,12 +34,19 @@
|
||||
//! ### Exemple basique
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmocache::cache::{Cache, CacheConfig};
|
||||
//! use pmocache::{Cache, CacheConfig};
|
||||
//!
|
||||
//! // Définir la configuration du cache
|
||||
//! struct MyConfig;
|
||||
//! impl CacheConfig for MyConfig {
|
||||
//! fn file_extension() -> &'static str { "dat" }
|
||||
//! fn table_name() -> &'static str { "my_cache" }
|
||||
//! fn cache_type() -> &'static str { "generic" }
|
||||
//! }
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> anyhow::Result<()> {
|
||||
//! let config = CacheConfig::new("./cache", 1000, "my_cache", "dat");
|
||||
//! let cache = Cache::new(config)?;
|
||||
//! let cache = Cache::<MyConfig>::new("./cache", 1000, "http://localhost:8080")?;
|
||||
//!
|
||||
//! // Ajouter un fichier depuis une URL
|
||||
//! let pk = cache.add_from_url("http://example.com/file.dat", None).await?;
|
||||
@@ -56,12 +63,18 @@
|
||||
//! ### Utilisation avec des collections
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmocache::cache::{Cache, CacheConfig};
|
||||
//! use pmocache::{Cache, CacheConfig};
|
||||
//!
|
||||
//! struct AudioConfig;
|
||||
//! impl CacheConfig for AudioConfig {
|
||||
//! fn file_extension() -> &'static str { "flac" }
|
||||
//! fn table_name() -> &'static str { "audio" }
|
||||
//! fn cache_type() -> &'static str { "audio" }
|
||||
//! }
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> anyhow::Result<()> {
|
||||
//! let config = CacheConfig::new("./cache", 1000, "audio", "flac");
|
||||
//! let cache = Cache::new(config)?;
|
||||
//! let cache = Cache::<AudioConfig>::new("./cache", 1000, "http://localhost:8080")?;
|
||||
//!
|
||||
//! // Ajouter des pistes d'un album
|
||||
//! let album_id = "album:the_wall";
|
||||
@@ -111,6 +124,11 @@
|
||||
|
||||
pub mod db;
|
||||
pub mod cache;
|
||||
pub mod cache_trait;
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub mod pmoserver_ext;
|
||||
|
||||
pub use db::{DB, CacheEntry};
|
||||
pub use cache::{Cache, CacheConfig, pk_from_url};
|
||||
pub use cache_trait::FileCache;
|
||||
|
||||
142
pmocache/src/pmoserver_ext.rs
Normal file
142
pmocache/src/pmoserver_ext.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
//! 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.
|
||||
//!
|
||||
//! ## Routes générées
|
||||
//!
|
||||
//! Format: `/{name}/{type}/{pk}[/{param}]`
|
||||
//!
|
||||
//! Exemples:
|
||||
//! - `/covers/images/abc123` - Image avec param par défaut (orig)
|
||||
//! - `/covers/images/abc123/thumb` - Image avec param spécifique
|
||||
//! - `/audio/tracks/def456/stream` - Piste audio
|
||||
//!
|
||||
//! ## 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 peut être monté sur n'importe quel chemin
|
||||
//! // Exemple: /covers/images -> 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,
|
||||
Router,
|
||||
};
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use tracing::warn;
|
||||
|
||||
/// Handler générique pour GET /{pk}
|
||||
/// Sert un fichier avec le param par défaut
|
||||
#[cfg(feature = "pmoserver")]
|
||||
async fn get_file<C: CacheConfig + 'static>(
|
||||
State((cache, content_type)): State<(Arc<Cache<C>>, &'static str)>,
|
||||
Path(pk): Path<String>,
|
||||
) -> Response {
|
||||
match cache.get(&pk).await {
|
||||
Ok(file_path) => match tokio::fs::read(&file_path).await {
|
||||
Ok(data) => (
|
||||
StatusCode::OK,
|
||||
[("content-type", content_type)],
|
||||
data,
|
||||
)
|
||||
.into_response(),
|
||||
Err(_) => (StatusCode::NOT_FOUND, "File not found").into_response(),
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("Error getting file {}: {}", pk, e);
|
||||
(StatusCode::NOT_FOUND, "Item not found").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler générique pour GET /{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)): State<(Arc<Cache<C>>, &'static str)>,
|
||||
Path((pk, param)): Path<(String, String)>,
|
||||
) -> Response {
|
||||
let file_path = cache.file_path_with_qualifier(&pk, ¶m);
|
||||
|
||||
if !file_path.exists() {
|
||||
warn!("File not found: {:?}", file_path);
|
||||
return (StatusCode::NOT_FOUND, "File not found").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);
|
||||
}
|
||||
|
||||
match tokio::fs::read(&file_path).await {
|
||||
Ok(data) => (
|
||||
StatusCode::OK,
|
||||
[("content-type", content_type)],
|
||||
data,
|
||||
)
|
||||
.into_response(),
|
||||
Err(_) => (StatusCode::NOT_FOUND, "File not found").into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un router pour servir les fichiers d'un cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `cache` - Instance du cache
|
||||
/// * `content_type` - Type MIME des fichiers (ex: "image/webp", "audio/flac")
|
||||
///
|
||||
/// # Routes créées
|
||||
///
|
||||
/// - `GET /{pk}` - Fichier avec param par défaut
|
||||
/// - `GET /{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"
|
||||
/// );
|
||||
///
|
||||
/// // Monter le router sur /covers/images
|
||||
/// server.add_router("/covers/images", router).await;
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub fn create_file_router<C: CacheConfig + 'static>(
|
||||
cache: Arc<Cache<C>>,
|
||||
content_type: &'static str,
|
||||
) -> Router {
|
||||
Router::new()
|
||||
.route("/:pk", get(get_file::<C>))
|
||||
.route("/:pk/:param", get(get_file_with_param::<C>))
|
||||
.with_state((cache, content_type))
|
||||
}
|
||||
@@ -31,4 +31,4 @@ tracing = "0.1.41"
|
||||
|
||||
[features]
|
||||
default = ["pmoserver"]
|
||||
pmoserver = ["dep:pmoserver", "dep:pmoconfig", "dep:axum", "dep:utoipa", "pmocache/openapi"]
|
||||
pmoserver = ["dep:pmoserver", "dep:pmoconfig", "dep:axum", "dep:utoipa", "pmocache/openapi", "pmocache/pmoserver"]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! - Supprimer des images
|
||||
//! - Consulter les statistiques
|
||||
|
||||
use crate::{Cache, CacheEntry};
|
||||
use crate::{Cache, CacheEntry, ImageCacheExt};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
@@ -146,7 +146,7 @@ pub async fn add_image(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
match cache.add_from_url(&req.url).await {
|
||||
match cache.add_image_from_url(&req.url).await {
|
||||
Ok(pk) => (
|
||||
StatusCode::CREATED,
|
||||
Json(AddImageResponse {
|
||||
@@ -200,7 +200,8 @@ pub async fn delete_image(
|
||||
}
|
||||
|
||||
// Supprimer les fichiers (original + variantes)
|
||||
let orig_path = cache.dir.join(format!("{}.orig.webp", pk));
|
||||
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 (
|
||||
@@ -215,7 +216,7 @@ pub async fn delete_image(
|
||||
}
|
||||
|
||||
// Supprimer toutes les variantes (*.{pk}.*.webp)
|
||||
if let Ok(mut entries) = tokio::fs::read_dir(&cache.dir).await {
|
||||
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) {
|
||||
|
||||
@@ -3,132 +3,163 @@
|
||||
//! 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.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use anyhow::{anyhow, Result};
|
||||
use pmocache::{Cache as GenericCache, CacheConfig};
|
||||
use anyhow::Result;
|
||||
use pmocache::{CacheConfig, FileCache};
|
||||
use crate::webp;
|
||||
use crate::db::DB;
|
||||
use std::path::PathBuf;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// Configuration pour le cache de couvertures
|
||||
pub struct CoversConfig;
|
||||
|
||||
impl CacheConfig for CoversConfig {
|
||||
fn file_extension() -> &'static str {
|
||||
"webp"
|
||||
}
|
||||
|
||||
fn table_name() -> &'static str {
|
||||
"covers"
|
||||
}
|
||||
|
||||
fn cache_type() -> &'static str {
|
||||
"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
|
||||
///
|
||||
/// Gère le téléchargement, la conversion en WebP, le stockage et la génération
|
||||
/// de variantes de tailles pour les images de couvertures.
|
||||
/// Format des fichiers : `{pk}.{qualificatif}.webp`
|
||||
/// Exemple : `a1b2c3d4.orig.webp`, `a1b2c3d4.thumb.webp`
|
||||
///
|
||||
/// 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 {
|
||||
cache: GenericCache,
|
||||
pub(crate) dir: PathBuf,
|
||||
pub(crate) limit: usize,
|
||||
pub db: Arc<DB>,
|
||||
}
|
||||
pub struct Cache(pmocache::Cache<CoversConfig>);
|
||||
|
||||
impl Cache {
|
||||
/// Crée un nouveau cache d'images
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `dir` - Répertoire de stockage du cache
|
||||
/// * `limit` - Limite de taille du cache (nombre d'images)
|
||||
///
|
||||
/// # Exemple
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use pmocovers::Cache;
|
||||
///
|
||||
/// let cache = Cache::new("./cache", 1000).unwrap();
|
||||
/// ```
|
||||
pub fn new(dir: &str, limit: usize) -> Result<Self> {
|
||||
let config = CacheConfig::new(dir, limit, "covers", "orig.webp");
|
||||
let cache = GenericCache::new(config)?;
|
||||
pub fn new(dir: &str, limit: usize, base_url: &str) -> Result<Self> {
|
||||
Ok(Self(pmocache::Cache::new(dir, limit, base_url)?))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
dir: PathBuf::from(dir),
|
||||
limit,
|
||||
db: Arc::clone(&cache.db),
|
||||
cache,
|
||||
})
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Télécharge une image depuis une URL et l'ajoute au cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL de l'image à télécharger
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// La clé primaire (pk) de l'image dans le cache
|
||||
pub async fn add_from_url(&self, url: &str) -> Result<String> {
|
||||
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!("Bad status: {}", response.status()));
|
||||
return Err(anyhow::anyhow!("Bad status: {}", response.status()));
|
||||
}
|
||||
|
||||
let data = response.bytes().await?;
|
||||
self.add(url, &data).await
|
||||
self.add(url, &data, collection).await
|
||||
}
|
||||
|
||||
/// S'assure qu'une image est présente dans le cache
|
||||
///
|
||||
/// Si l'image existe déjà, retourne sa clé. Sinon, la télécharge.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL de l'image
|
||||
pub async fn ensure_from_url(&self, url: &str) -> Result<String> {
|
||||
self.cache.ensure_from_url(url, None).await
|
||||
}
|
||||
|
||||
/// Ajoute une image au cache avec conversion en WebP
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `url` - URL source de l'image
|
||||
/// * `data` - Données brutes de l'image
|
||||
pub async fn add(&self, url: &str, data: &[u8]) -> Result<String> {
|
||||
async fn ensure_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
|
||||
let pk = pmocache::pk_from_url(url);
|
||||
let orig_path = self.dir.join(format!("{}.orig.webp", pk));
|
||||
|
||||
// Vérifier si le fichier existe déjà
|
||||
if !orig_path.exists() {
|
||||
// Convertir l'image en WebP
|
||||
let img = image::load_from_memory(data)?;
|
||||
let webp_data = webp::encode_webp(&img)?;
|
||||
tokio::fs::write(&orig_path, webp_data).await?;
|
||||
if self.db.get(&pk).is_ok() {
|
||||
let file_path = self.file_path(&pk);
|
||||
if file_path.exists() {
|
||||
return Ok(pk);
|
||||
}
|
||||
}
|
||||
|
||||
// Ajouter à la DB (sans collection pour les covers)
|
||||
self.db.add(&pk, url, None)?;
|
||||
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)
|
||||
}
|
||||
|
||||
/// Récupère le chemin d'une image dans le cache
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pk` - Clé primaire de l'image
|
||||
pub async fn get(&self, pk: &str) -> Result<PathBuf> {
|
||||
self.cache.get(pk).await
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime tous les fichiers et entrées du cache
|
||||
pub async fn purge(&self) -> Result<()> {
|
||||
self.cache.purge().await
|
||||
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)
|
||||
}
|
||||
|
||||
/// Consolide le cache en supprimant les orphelins et en re-téléchargeant les images manquantes
|
||||
pub async fn consolidate(&self) -> Result<()> {
|
||||
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 orig_path = self.dir.join(format!("{}.orig.webp", entry.pk));
|
||||
if !orig_path.exists() {
|
||||
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).await?;
|
||||
self.add(&entry.source_url, &data, entry.collection.as_deref())
|
||||
.await?;
|
||||
}
|
||||
_ => {
|
||||
self.db.delete(&entry.pk)?;
|
||||
@@ -138,13 +169,14 @@ impl Cache {
|
||||
}
|
||||
|
||||
// Supprimer les fichiers sans entrées DB correspondantes
|
||||
let mut dir_entries = tokio::fs::read_dir(&self.dir).await?;
|
||||
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 != self.dir.join("cache.db") {
|
||||
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()) {
|
||||
if file_name.ends_with(".orig.webp") {
|
||||
let pk = file_name.trim_end_matches(".orig.webp");
|
||||
// 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?;
|
||||
}
|
||||
@@ -156,8 +188,11 @@ impl Cache {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retourne le répertoire du cache
|
||||
pub fn cache_dir(&self) -> String {
|
||||
self.dir.to_string_lossy().to_string()
|
||||
fn get_cache_dir(&self) -> String {
|
||||
self.get_cache_dir()
|
||||
}
|
||||
|
||||
fn get_base_url(&self) -> &str {
|
||||
self.get_base_url()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,13 +100,14 @@
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use pmocovers::Cache;
|
||||
//! use pmocache::FileCache;
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> anyhow::Result<()> {
|
||||
//! let cache = Cache::new("./cache", 1000)?;
|
||||
//! let cache = Cache::new("./cache", 1000, "http://localhost:8080")?;
|
||||
//!
|
||||
//! // Ajouter une image depuis une URL
|
||||
//! let pk = cache.add_from_url("http://example.com/cover.jpg").await?;
|
||||
//! // 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
|
||||
@@ -197,7 +198,7 @@ pub mod api;
|
||||
#[cfg(feature = "pmoserver")]
|
||||
pub mod openapi;
|
||||
|
||||
pub use cache::Cache;
|
||||
pub use cache::{Cache, CoversConfig};
|
||||
pub use db::{CacheEntry, DB};
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
|
||||
@@ -29,80 +29,39 @@
|
||||
|
||||
use crate::{api, Cache, CoverCacheExt};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::State,
|
||||
http::{Request, StatusCode},
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{delete, get, post},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use pmoserver::Server;
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing::{info, warn};
|
||||
use std::sync::Arc;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
|
||||
|
||||
/// Handler pour GET /covers/images/{pk}
|
||||
async fn get_cover_image(
|
||||
State(cache): State<Arc<Cache>>,
|
||||
req: Request<Body>,
|
||||
) -> Response {
|
||||
// Extraire pk du path
|
||||
let path = req.uri().path();
|
||||
let parts: Vec<&str> = path.split('/').collect();
|
||||
|
||||
warn!("{:?}",parts);
|
||||
|
||||
if parts.len() != 2 {
|
||||
return (StatusCode::BAD_REQUEST, "Invalid path").into_response();
|
||||
}
|
||||
|
||||
let pk = parts[1];
|
||||
|
||||
match cache.get(pk).await {
|
||||
Ok(file_path) => {
|
||||
match tokio::fs::read(&file_path).await {
|
||||
Ok(data) => (
|
||||
StatusCode::OK,
|
||||
[("content-type", "image/webp")],
|
||||
data,
|
||||
)
|
||||
.into_response(),
|
||||
Err(_) => (StatusCode::NOT_FOUND, "File not found").into_response(),
|
||||
}
|
||||
}
|
||||
Err(_) => (StatusCode::NOT_FOUND, "Image not found").into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler pour GET /covers/images/{pk}/{size}
|
||||
/// Génère une variante d'image à la demande
|
||||
async fn get_cover_variant(
|
||||
State(cache): State<Arc<Cache>>,
|
||||
req: Request<Body>,
|
||||
Path((pk, size)): Path<(String, String)>,
|
||||
) -> Response {
|
||||
// Extraire pk et size du path
|
||||
let path = req.uri().path();
|
||||
let parts: Vec<&str> = path.split('/').collect();
|
||||
|
||||
if parts.len() != 3 {
|
||||
return (StatusCode::BAD_REQUEST, "Invalid path").into_response();
|
||||
}
|
||||
|
||||
let pk = parts[1];
|
||||
let size = match parts[2].parse::<usize>() {
|
||||
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 {
|
||||
match crate::webp::generate_variant(&cache, &pk, size).await {
|
||||
Ok(data) => (
|
||||
StatusCode::OK,
|
||||
[("content-type", "image/webp")],
|
||||
data,
|
||||
)
|
||||
.into_response(),
|
||||
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Cannot generate variant").into_response(),
|
||||
Err(e) => {
|
||||
warn!("Cannot generate variant for {}: {}", pk, e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, "Cannot generate variant").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,18 +75,28 @@ async fn get_cover_stats(State(cache): State<Arc<Cache>>) -> Response {
|
||||
|
||||
impl CoverCacheExt for Server {
|
||||
async fn init_cover_cache(&mut self, cache_dir: &str, limit: usize) -> anyhow::Result<Arc<Cache>> {
|
||||
let cache = Arc::new(Cache::new(cache_dir, limit)?);
|
||||
// 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)?);
|
||||
|
||||
// Enregistrer les routes HTTP classiques pour servir les images
|
||||
let image_router = Router::new()
|
||||
.route("/{pk}", get(get_cover_image))
|
||||
// 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;
|
||||
|
||||
self.add_router("/covers/images", image_router).await;
|
||||
// Route pour les stats
|
||||
self.add_handler_with_state("/covers/stats", get_cover_stats, cache.clone()).await;
|
||||
|
||||
// Router API RESTful
|
||||
// Router API RESTful qui sera nesté sous /api/covers par add_openapi
|
||||
let api_router = Router::new()
|
||||
// Liste et ajout
|
||||
|
||||
@@ -39,13 +39,14 @@ pub fn ensure_square(img: &DynamicImage, size: u32) -> DynamicImage {
|
||||
}
|
||||
|
||||
pub async fn generate_variant(cache: &super::cache::Cache, pk: &str, size: usize) -> Result<Vec<u8>> {
|
||||
let variant_path = cache.dir.join(format!("{}.{}.webp", pk, size));
|
||||
// Utiliser file_path_with_qualifier pour obtenir le chemin
|
||||
let variant_path = cache.file_path_with_qualifier(pk, &size.to_string());
|
||||
|
||||
if variant_path.exists() {
|
||||
return Ok(tokio::fs::read(variant_path).await?);
|
||||
}
|
||||
|
||||
let orig_path = cache.dir.join(format!("{}.orig.webp", pk));
|
||||
let orig_path = cache.file_path_with_qualifier(pk, "orig");
|
||||
|
||||
// Charger l'image de manière synchrone (image::open n'est pas async)
|
||||
let img = tokio::task::spawn_blocking(move || {
|
||||
|
||||
@@ -14,7 +14,7 @@ use std::time::SystemTime;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[cfg(feature = "cache")]
|
||||
use pmocovers::Cache as CoverCache;
|
||||
use pmocovers::{Cache as CoverCache, ImageCacheExt};
|
||||
#[cfg(feature = "cache")]
|
||||
use pmoaudiocache::{AudioCache, AudioMetadata};
|
||||
|
||||
@@ -226,7 +226,7 @@ impl QobuzSource {
|
||||
let cached_cover_pk = if let Some(ref cover_cache) = self.inner.cover_cache {
|
||||
if let Some(ref album) = track.album {
|
||||
if let Some(ref image_url) = album.image {
|
||||
match cover_cache.add_from_url(image_url).await {
|
||||
match cover_cache.add_image_from_url(image_url).await {
|
||||
Ok(pk) => {
|
||||
tracing::info!("Successfully cached cover for track {}: {}", track_id, pk);
|
||||
Some(pk)
|
||||
|
||||
Reference in New Issue
Block a user