Debug le menu debug
This commit is contained in:
@@ -4,23 +4,18 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
# Cache générique
|
||||
pmocache = { path = "../pmocache" }
|
||||
|
||||
# Gestion d'images
|
||||
image = "0.25"
|
||||
webp = "0.3"
|
||||
|
||||
# Base de données
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
|
||||
# HTTP client
|
||||
reqwest = { version = "0.12", features = ["blocking"] }
|
||||
|
||||
# Cryptographie
|
||||
sha1 = "0.10"
|
||||
hex = "0.4"
|
||||
|
||||
# Utilitaires
|
||||
anyhow = "1.0"
|
||||
chrono = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
# Async
|
||||
@@ -36,4 +31,4 @@ tracing = "0.1.41"
|
||||
|
||||
[features]
|
||||
default = ["pmoserver"]
|
||||
pmoserver = ["dep:pmoserver", "dep:pmoconfig", "dep:axum", "dep:utoipa"]
|
||||
pmoserver = ["dep:pmoserver", "dep:pmoconfig", "dep:axum", "dep:utoipa", "pmocache/openapi"]
|
||||
|
||||
@@ -1,32 +1,63 @@
|
||||
//! 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.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use anyhow::{anyhow, Result};
|
||||
use sha1::{Sha1, Digest};
|
||||
use tokio::sync::Mutex;
|
||||
use crate::db::DB;
|
||||
use pmocache::{Cache as GenericCache, CacheConfig};
|
||||
use crate::webp;
|
||||
use crate::db::DB;
|
||||
|
||||
/// 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.
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
cache: GenericCache,
|
||||
pub(crate) dir: PathBuf,
|
||||
pub(crate) limit: usize,
|
||||
pub db: DB,
|
||||
mu: Arc<Mutex<()>>,
|
||||
pub db: Arc<DB>,
|
||||
}
|
||||
|
||||
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> {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
let db = DB::init(&PathBuf::from(dir).join("cache.db"))?;
|
||||
let config = CacheConfig::new(dir, limit, "covers", "orig.webp");
|
||||
let cache = GenericCache::new(config)?;
|
||||
|
||||
Ok(Self {
|
||||
dir: PathBuf::from(dir),
|
||||
limit,
|
||||
db,
|
||||
mu: Arc::new(Mutex::new(())),
|
||||
db: Arc::clone(&cache.db),
|
||||
cache,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
let response = reqwest::get(url).await?;
|
||||
if !response.status().is_success() {
|
||||
@@ -37,67 +68,60 @@ impl Cache {
|
||||
self.add(url, &data).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> {
|
||||
let pk = pk_from_url(url);
|
||||
|
||||
if self.db.get(&pk).is_ok() {
|
||||
let orig_path = self.dir.join(format!("{}.orig.webp", pk));
|
||||
if orig_path.exists() {
|
||||
return Ok(pk);
|
||||
}
|
||||
}
|
||||
|
||||
self.add_from_url(url).await
|
||||
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> {
|
||||
let pk = pk_from_url(url);
|
||||
let pk = pmocache::pk_from_url(url);
|
||||
let orig_path = self.dir.join(format!("{}.orig.webp", pk));
|
||||
|
||||
let _lock = self.mu.lock().await;
|
||||
|
||||
// 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?;
|
||||
}
|
||||
|
||||
self.db.add(&pk, url)?;
|
||||
// Ajouter à la DB (sans collection pour les covers)
|
||||
self.db.add(&pk, url, None)?;
|
||||
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> {
|
||||
let _lock = self.mu.lock().await;
|
||||
|
||||
self.db.get(pk)?;
|
||||
self.db.update_hit(pk)?;
|
||||
|
||||
let orig_path = self.dir.join(format!("{}.orig.webp", pk));
|
||||
if orig_path.exists() {
|
||||
Ok(orig_path)
|
||||
} else {
|
||||
Err(anyhow!("File not found"))
|
||||
}
|
||||
self.cache.get(pk).await
|
||||
}
|
||||
|
||||
/// 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.dir).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
if entry.path().is_file() {
|
||||
tokio::fs::remove_file(entry.path()).await?;
|
||||
}
|
||||
}
|
||||
|
||||
self.db.purge().map_err(|e| anyhow!("Database error: {}", e))
|
||||
self.cache.purge().await
|
||||
}
|
||||
|
||||
/// Consolide le cache en supprimant les orphelins et en re-téléchargeant les images manquantes
|
||||
pub async fn consolidate(&self) -> Result<()> {
|
||||
let _lock = self.mu.lock().await;
|
||||
|
||||
// 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() {
|
||||
@@ -113,10 +137,11 @@ impl Cache {
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer les fichiers sans entrées DB correspondantes
|
||||
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() {
|
||||
if path.is_file() && path != self.dir.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");
|
||||
@@ -131,15 +156,8 @@ impl Cache {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retourne le répertoire du cache
|
||||
pub fn cache_dir(&self) -> String {
|
||||
self.dir.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
@@ -1,118 +1,7 @@
|
||||
use rusqlite::{Connection, params};
|
||||
use serde::Serialize;
|
||||
use chrono::Utc;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
//! Module de compatibilité pour l'ancien module db
|
||||
//!
|
||||
//! Ce module réexporte les types de `pmocache::db` pour maintenir
|
||||
//! la compatibilité avec l'API existante.
|
||||
|
||||
#[cfg(feature = "pmoserver")]
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[cfg_attr(feature = "pmoserver", derive(ToSchema))]
|
||||
pub struct CacheEntry {
|
||||
/// Clé primaire unique de l'image (hash SHA1 de l'URL)
|
||||
#[cfg_attr(feature = "pmoserver", schema(example = "1a2b3c4d5e6f7a8b"))]
|
||||
pub pk: String,
|
||||
/// URL source de l'image
|
||||
#[cfg_attr(feature = "pmoserver", schema(example = "https://example.com/cover.jpg"))]
|
||||
pub source_url: String,
|
||||
/// Nombre d'accès à l'image
|
||||
#[cfg_attr(feature = "pmoserver", schema(example = 42))]
|
||||
pub hits: i32,
|
||||
/// Date/heure du dernier accès (RFC3339)
|
||||
#[cfg_attr(feature = "pmoserver", schema(example = "2025-01-15T10:30:00Z"))]
|
||||
pub last_used: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DB {
|
||||
conn: Mutex<Connection>,
|
||||
}
|
||||
|
||||
impl DB {
|
||||
pub fn init(path: &Path) -> Result<Self, rusqlite::Error> {
|
||||
let conn = Connection::open(path)?;
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS covers (
|
||||
pk TEXT PRIMARY KEY,
|
||||
source_url TEXT,
|
||||
hits INTEGER DEFAULT 0,
|
||||
last_used TEXT
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
Ok(Self { conn: Mutex::new(conn) })
|
||||
}
|
||||
|
||||
pub fn add(&self, pk: &str, url: &str) -> rusqlite::Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO covers (pk, source_url, hits, last_used)
|
||||
VALUES (?1, ?2, 0, ?3)
|
||||
ON CONFLICT(pk) DO UPDATE SET
|
||||
source_url = excluded.source_url,
|
||||
last_used = excluded.last_used",
|
||||
params![pk, url, Utc::now().to_rfc3339()],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&self, pk: &str) -> rusqlite::Result<CacheEntry> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.query_row(
|
||||
"SELECT pk, source_url, hits, last_used FROM covers WHERE pk = ?1",
|
||||
[pk],
|
||||
|row| {
|
||||
Ok(CacheEntry {
|
||||
pk: row.get(0)?,
|
||||
source_url: row.get(1)?,
|
||||
hits: row.get(2)?,
|
||||
last_used: row.get(3)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update_hit(&self, pk: &str) -> rusqlite::Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute(
|
||||
"UPDATE covers SET hits = hits + 1, last_used = ?1 WHERE pk = ?2",
|
||||
params![Utc::now().to_rfc3339(), pk],
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn purge(&self) -> rusqlite::Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM covers", [])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_all(&self) -> rusqlite::Result<Vec<CacheEntry>> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT pk, source_url, hits, last_used FROM covers ORDER BY hits DESC",
|
||||
)?;
|
||||
|
||||
let entries = stmt.query_map([], |row| {
|
||||
Ok(CacheEntry {
|
||||
pk: row.get(0)?,
|
||||
source_url: row.get(1)?,
|
||||
hits: row.get(2)?,
|
||||
last_used: row.get(3)?,
|
||||
})
|
||||
})?
|
||||
.collect::<rusqlite::Result<Vec<_>>>()?;
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn delete(&self, pk: &str) -> rusqlite::Result<()> {
|
||||
let conn = self.conn.lock().unwrap();
|
||||
conn.execute("DELETE FROM covers WHERE pk = ?1", [pk])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Réexporter les types de pmocache
|
||||
pub use pmocache::db::{CacheEntry, DB};
|
||||
|
||||
Reference in New Issue
Block a user