2025-10-12 21:39:20 +02:00
|
|
|
|
//! Module de gestion du cache générique
|
|
|
|
|
|
//!
|
|
|
|
|
|
//! 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.
|
|
|
|
|
|
|
2025-10-25 17:14:24 +02:00
|
|
|
|
use crate::cache_trait::FileCache;
|
2025-10-12 21:39:20 +02:00
|
|
|
|
use crate::db::DB;
|
2025-10-19 17:26:06 +02:00
|
|
|
|
use crate::download::{
|
|
|
|
|
|
download_with_transformer, ingest_with_transformer, Download, StreamTransformer,
|
|
|
|
|
|
};
|
2025-12-15 15:05:35 +01:00
|
|
|
|
use crate::lazy::{lazy_prefix_from_pk, LazyEntryRemoteData, LazyProvider};
|
2025-10-26 13:05:46 +01:00
|
|
|
|
use anyhow::{anyhow, bail, Result};
|
|
|
|
|
|
use serde_json::{Number, Value};
|
2025-12-12 16:36:31 +00:00
|
|
|
|
use sha2::{Digest, Sha256};
|
2025-10-17 19:48:53 +02:00
|
|
|
|
use std::collections::HashMap;
|
2025-10-17 14:36:39 +02:00
|
|
|
|
use std::path::{Path, PathBuf};
|
2025-11-29 12:38:23 +01:00
|
|
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
2025-12-15 15:05:35 +01:00
|
|
|
|
use std::sync::{Arc, RwLock as StdRwLock};
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
use tokio::io::{AsyncRead, AsyncReadExt};
|
2025-12-12 16:36:31 +00:00
|
|
|
|
use tokio::sync::{broadcast, RwLock};
|
2025-10-17 22:15:00 +02:00
|
|
|
|
use tracing;
|
2025-10-12 21:39:20 +02:00
|
|
|
|
|
2025-12-15 15:05:35 +01:00
|
|
|
|
enum FinalizeMode<'a> {
|
|
|
|
|
|
InsertNew,
|
|
|
|
|
|
ConvertLazy { lazy_pk: &'a str },
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-12 16:36:31 +00:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// LAZY PK SUPPORT
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/// Préfixe magique pour identifier les lazy PK
|
|
|
|
|
|
const LAZY_PK_PREFIX: &str = "L:";
|
|
|
|
|
|
|
|
|
|
|
|
/// Génère un lazy PK à partir d'une URL
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Le lazy PK est calculable sans télécharger le fichier, ce qui permet
|
|
|
|
|
|
/// de créer des URLs UPnP stables avant tout téléchargement.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Format: "L:" + hex(sha256(url)[..16])
|
|
|
|
|
|
pub fn generate_lazy_pk(url: &str) -> String {
|
|
|
|
|
|
let mut hasher = Sha256::new();
|
|
|
|
|
|
hasher.update(url.as_bytes());
|
|
|
|
|
|
let hash = hasher.finalize();
|
|
|
|
|
|
format!("{}{}", LAZY_PK_PREFIX, hex::encode(&hash[..16]))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Vérifie si un PK est en mode lazy
|
|
|
|
|
|
pub fn is_lazy_pk(pk: &str) -> bool {
|
2025-12-15 15:05:35 +01:00
|
|
|
|
if pk.starts_with(LAZY_PK_PREFIX) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lazy_prefix_from_pk(pk).is_some()
|
2025-12-12 16:36:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Events émis par le cache pour notifier les changements d'état
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub enum CacheEvent {
|
|
|
|
|
|
/// Un fichier a été servi via HTTP
|
2025-12-15 11:18:58 +01:00
|
|
|
|
Served { pk: String, format: String },
|
2025-12-12 16:36:31 +00:00
|
|
|
|
/// Un fichier lazy a été téléchargé et est maintenant disponible
|
2025-12-15 11:18:58 +01:00
|
|
|
|
LazyDownloaded { lazy_pk: String, real_pk: String },
|
2025-12-12 16:36:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-29 12:38:23 +01:00
|
|
|
|
/// Informations transmises lors de la diffusion d'un élément du cache via HTTP.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// - Emis uniquement quand une réponse 2xx est renvoyée par les routes HTTP générées
|
|
|
|
|
|
/// (fichier complet, stream progressif ou variante générée).
|
|
|
|
|
|
/// - Inclut le qualifier utilisé pour la requête afin de distinguer `orig`, `stream`, etc.
|
|
|
|
|
|
/// - Peut être utilisé pour synchroniser des clients (ex: WebSocket) ou tracer les hits.
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct CacheBroadcastEvent {
|
|
|
|
|
|
/// Identifiant unique du fichier servi.
|
|
|
|
|
|
pub pk: String,
|
|
|
|
|
|
/// Qualifier (paramètre de route) utilisé pour cette diffusion.
|
|
|
|
|
|
pub qualifier: String,
|
|
|
|
|
|
/// Nom logique du cache (`CacheConfig::cache_name`).
|
|
|
|
|
|
pub cache_name: &'static str,
|
|
|
|
|
|
/// Type du cache (`CacheConfig::cache_type`).
|
|
|
|
|
|
pub cache_type: &'static str,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Handle retourné lors de l'abonnement à un évènement de diffusion.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Conservez-le pour pouvoir vous désabonner explicitement via
|
|
|
|
|
|
/// [`Cache::unsubscribe_broadcast`]. Le couple `(pk, id)` identifie de manière
|
|
|
|
|
|
/// unique la callback enregistrée.
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
|
pub struct CacheSubscription {
|
|
|
|
|
|
pub pk: String,
|
|
|
|
|
|
pub id: u64,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type CacheServeCallback = Arc<dyn Fn(&CacheBroadcastEvent) -> bool + Send + Sync>;
|
|
|
|
|
|
|
2025-11-06 08:05:41 +00:00
|
|
|
|
/// Taille minimale de prébuffering par défaut (512 KB = ~5 secondes de FLAC)
|
|
|
|
|
|
pub const DEFAULT_PREBUFFER_SIZE: u64 = 512 * 1024;
|
|
|
|
|
|
|
2025-10-26 13:44:41 +01:00
|
|
|
|
/// Paramètres statiques d'un cache spécialisé.
|
2025-10-17 14:36:39 +02:00
|
|
|
|
pub trait CacheConfig: Send + Sync {
|
2025-10-26 13:44:41 +01:00
|
|
|
|
/// Extension des fichiers générés (ex: `"webp"`, `"flac"`).
|
2025-10-17 14:36:39 +02:00
|
|
|
|
fn file_extension() -> &'static str;
|
2025-10-26 13:44:41 +01:00
|
|
|
|
/// Type logique exposé (ex: `"audio"`, `"image"`). Sert notamment pour les routes HTTP.
|
2025-10-17 14:36:39 +02:00
|
|
|
|
fn cache_type() -> &'static str {
|
|
|
|
|
|
"file"
|
|
|
|
|
|
}
|
2025-10-26 13:44:41 +01:00
|
|
|
|
/// Nom du cache (ex: `"covers"`, `"audio"`). Utilisé pour composer les chemins d'accès.
|
2025-10-19 13:42:29 +02:00
|
|
|
|
fn cache_name() -> &'static str {
|
2025-10-17 14:36:39 +02:00
|
|
|
|
"cache"
|
|
|
|
|
|
}
|
2025-10-26 13:44:41 +01:00
|
|
|
|
/// Qualifier par défaut associé au fichier original (ex: `"orig"`).
|
2025-10-17 14:36:39 +02:00
|
|
|
|
fn default_param() -> &'static str {
|
|
|
|
|
|
"orig"
|
2025-10-12 21:39:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Cache générique pour stocker des fichiers avec métadonnées
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 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.
|
|
|
|
|
|
///
|
2025-10-17 14:36:39 +02:00
|
|
|
|
/// # Paramètres de type
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `C` - Configuration du cache (implémente `CacheConfig`)
|
|
|
|
|
|
///
|
2025-10-12 21:39:20 +02:00
|
|
|
|
/// Note : Ce type est conçu pour être utilisé derrière un `Arc<Cache>`.
|
2025-10-17 19:48:53 +02:00
|
|
|
|
/// La synchronisation est gérée par le Mutex interne de la base de données SQLite
|
|
|
|
|
|
/// et par le RwLock pour la map des downloads.
|
2025-10-17 14:36:39 +02:00
|
|
|
|
pub struct Cache<C: CacheConfig> {
|
|
|
|
|
|
/// Répertoire de stockage
|
|
|
|
|
|
dir: PathBuf,
|
|
|
|
|
|
/// Limite de taille du cache (nombre d'éléments)
|
|
|
|
|
|
limit: usize,
|
|
|
|
|
|
/// Base de données SQLite
|
2025-10-12 21:39:20 +02:00
|
|
|
|
pub db: Arc<DB>,
|
2025-10-17 19:48:53 +02:00
|
|
|
|
/// Map des downloads en cours (pk -> Download)
|
|
|
|
|
|
downloads: Arc<RwLock<HashMap<String, Arc<Download>>>>,
|
2025-11-29 12:38:23 +01:00
|
|
|
|
/// Callback(s) à déclencher lorsqu'un élément est servi via HTTP (pk -> callbacks)
|
|
|
|
|
|
serve_subscribers: Arc<RwLock<HashMap<String, Vec<(u64, CacheServeCallback)>>>>,
|
|
|
|
|
|
/// Générateur d'identifiants uniques pour les abonnements
|
|
|
|
|
|
subscriber_counter: AtomicU64,
|
2025-10-17 22:15:00 +02:00
|
|
|
|
/// Factory pour créer des transformers (optionnel)
|
|
|
|
|
|
transformer_factory: Option<Arc<dyn Fn() -> StreamTransformer + Send + Sync>>,
|
2025-11-06 08:05:41 +00:00
|
|
|
|
/// Taille minimale de prébuffering en octets (0 = désactivé)
|
|
|
|
|
|
min_prebuffer_size: u64,
|
2025-12-12 16:36:31 +00:00
|
|
|
|
/// LAZY PK SUPPORT: Channel pour broadcaster les events (lazy downloads, etc.)
|
|
|
|
|
|
served_tx: Option<broadcast::Sender<CacheEvent>>,
|
2025-12-15 15:05:35 +01:00
|
|
|
|
/// Providers responsables de préfixes lazy spécifiques
|
|
|
|
|
|
lazy_providers: StdRwLock<HashMap<String, Arc<dyn LazyProvider>>>,
|
2025-10-17 14:36:39 +02:00
|
|
|
|
/// Phantom data pour le type de configuration
|
|
|
|
|
|
_phantom: std::marker::PhantomData<C>,
|
2025-10-12 21:39:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-17 15:15:10 +01:00
|
|
|
|
impl<C: CacheConfig + 'static> Cache<C> {
|
2025-11-07 05:43:25 +00:00
|
|
|
|
/// Retourne le chemin du fichier marker de complétion
|
|
|
|
|
|
fn get_completion_marker_path(&self, pk: &str) -> PathBuf {
|
2025-11-14 10:43:53 +01:00
|
|
|
|
self.get_file_path(pk)
|
|
|
|
|
|
.with_extension(format!("{}.complete", C::file_extension()))
|
2025-11-07 05:43:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
/// Vérifie si un fichier est en cache et complet
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
///
|
2025-11-07 05:43:25 +00:00
|
|
|
|
/// - `Ok(true)` si le fichier est en cache et complet (fichier .complete existe)
|
2025-11-17 22:31:27 +01:00
|
|
|
|
/// - `Ok(false)` si le fichier n'est pas en cache ou incomplet (et supprime les fichiers incomplets SI aucun download en cours)
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
/// - `Err` en cas d'erreur
|
|
|
|
|
|
async fn check_cached_and_complete(&self, pk: &str) -> Result<bool> {
|
|
|
|
|
|
if self.db.get(pk, false).is_ok() {
|
|
|
|
|
|
let file_path = self.get_file_path(pk);
|
2025-11-07 05:43:25 +00:00
|
|
|
|
let completion_marker = self.get_completion_marker_path(pk);
|
|
|
|
|
|
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
if file_path.exists() {
|
2025-11-07 05:43:25 +00:00
|
|
|
|
// Vérifier si le fichier marker de complétion existe
|
|
|
|
|
|
if completion_marker.exists() {
|
|
|
|
|
|
tracing::debug!("File with pk {} is complete (marker exists)", pk);
|
|
|
|
|
|
return Ok(true);
|
|
|
|
|
|
} else {
|
2025-11-17 22:31:27 +01:00
|
|
|
|
// Vérifier si un download est en cours avant de supprimer
|
|
|
|
|
|
let is_downloading = {
|
|
|
|
|
|
let downloads = self.downloads.read().await;
|
|
|
|
|
|
downloads.contains_key(pk)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if is_downloading {
|
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
|
"File with pk {} has no completion marker but download is in progress, waiting",
|
|
|
|
|
|
pk
|
|
|
|
|
|
);
|
|
|
|
|
|
return Ok(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-07 05:43:25 +00:00
|
|
|
|
tracing::warn!(
|
2025-11-17 22:31:27 +01:00
|
|
|
|
"File with pk {} in cache has no completion marker and no download in progress, will re-download/re-ingest",
|
2025-11-07 05:43:25 +00:00
|
|
|
|
pk
|
|
|
|
|
|
);
|
2025-11-17 22:31:27 +01:00
|
|
|
|
// Supprimer le fichier incomplet ET l'entrée DB seulement si pas de download en cours
|
2025-11-07 05:43:25 +00:00
|
|
|
|
let _ = std::fs::remove_file(&file_path);
|
2025-11-17 22:31:27 +01:00
|
|
|
|
let _ = std::fs::remove_file(&completion_marker); // Nettoyer aussi le marker s'il existe
|
2025-11-16 08:34:33 +01:00
|
|
|
|
if let Err(e) = self.db.delete(pk) {
|
2025-11-17 22:31:27 +01:00
|
|
|
|
tracing::warn!(
|
|
|
|
|
|
"Failed to delete DB entry for incomplete file {}: {}",
|
|
|
|
|
|
pk,
|
|
|
|
|
|
e
|
|
|
|
|
|
);
|
2025-11-16 08:34:33 +01:00
|
|
|
|
}
|
2025-11-07 05:43:25 +00:00
|
|
|
|
return Ok(false);
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
Ok(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Vérifie si un download est en cours et attend le prébuffering si nécessaire
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
///
|
|
|
|
|
|
/// - `Ok(Some(pk))` si un download est en cours (et prébuffering terminé)
|
|
|
|
|
|
/// - `Ok(None)` si aucun download en cours
|
|
|
|
|
|
/// - `Err` en cas d'erreur de prébuffering
|
|
|
|
|
|
async fn check_ongoing_download(&self, pk: &str) -> Result<Option<String>> {
|
|
|
|
|
|
let download_handle = {
|
|
|
|
|
|
let downloads = self.downloads.read().await;
|
|
|
|
|
|
downloads.get(pk).cloned()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(download) = download_handle {
|
2025-11-14 10:43:53 +01:00
|
|
|
|
tracing::debug!(
|
|
|
|
|
|
"Download already in progress for pk {}, waiting for prebuffering",
|
|
|
|
|
|
pk
|
|
|
|
|
|
);
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
|
|
|
|
|
|
if self.min_prebuffer_size > 0 {
|
2025-11-14 10:43:53 +01:00
|
|
|
|
download
|
|
|
|
|
|
.wait_until_min_size(self.min_prebuffer_size)
|
|
|
|
|
|
.await
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
.map_err(|e| anyhow!("Prebuffering failed: {}", e))?;
|
|
|
|
|
|
tracing::debug!("Prebuffering complete for pk {}", pk);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Ok(Some(pk.to_string()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(None)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Finalise l'ajout d'un fichier au cache
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Cette fonction helper gère le prébuffering et le nettoyage en background
|
2025-11-16 08:34:33 +01:00
|
|
|
|
async fn finalize_download(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
pk: &str,
|
|
|
|
|
|
download: Arc<Download>,
|
|
|
|
|
|
collection: Option<&str>,
|
|
|
|
|
|
origin_url: Option<&str>,
|
2025-12-15 15:05:35 +01:00
|
|
|
|
mode: FinalizeMode<'_>,
|
2025-11-16 08:34:33 +01:00
|
|
|
|
) -> Result<String> {
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
// Attendre le prébuffering (pour le cache progressif)
|
|
|
|
|
|
if self.min_prebuffer_size > 0 {
|
2025-11-14 10:43:53 +01:00
|
|
|
|
download
|
|
|
|
|
|
.wait_until_min_size(self.min_prebuffer_size)
|
|
|
|
|
|
.await
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
.map_err(|e| anyhow!("Prebuffering failed: {}", e))?;
|
2025-11-14 10:43:53 +01:00
|
|
|
|
tracing::debug!(
|
|
|
|
|
|
"Prebuffering complete for pk {} ({} bytes)",
|
|
|
|
|
|
pk,
|
|
|
|
|
|
self.min_prebuffer_size
|
|
|
|
|
|
);
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 15:05:35 +01:00
|
|
|
|
// Ajouter ou commuter la DB selon le mode
|
|
|
|
|
|
match mode {
|
|
|
|
|
|
FinalizeMode::InsertNew => {
|
|
|
|
|
|
self.db.add(pk, None, collection)?;
|
|
|
|
|
|
if let Some(url) = origin_url {
|
|
|
|
|
|
self.db.set_origin_url(pk, url)?;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
FinalizeMode::ConvertLazy { lazy_pk } => {
|
|
|
|
|
|
self.db.update_lazy_to_downloaded(lazy_pk, pk)?;
|
|
|
|
|
|
}
|
2025-11-16 08:34:33 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-24 08:06:56 +01:00
|
|
|
|
// Sauvegarder les métadonnées techniques du transformer
|
|
|
|
|
|
if let Some(transform) = download.transform_metadata().await {
|
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
|
"Cache: Got transform metadata for pk {}: sr={:?}, bps={:?}, ch={:?}, ts={:?}",
|
|
|
|
|
|
pk,
|
|
|
|
|
|
transform.sample_rate,
|
|
|
|
|
|
transform.bits_per_sample,
|
|
|
|
|
|
transform.channels,
|
|
|
|
|
|
transform.total_samples
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(sr) = transform.sample_rate {
|
2025-11-29 14:19:16 +01:00
|
|
|
|
self.db
|
|
|
|
|
|
.set_a_metadata(pk, "sample_rate", serde_json::json!(sr))?;
|
2025-11-24 08:06:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
if let Some(bps) = transform.bits_per_sample {
|
2025-11-29 14:19:16 +01:00
|
|
|
|
self.db
|
|
|
|
|
|
.set_a_metadata(pk, "bits_per_sample", serde_json::json!(bps))?;
|
2025-11-24 08:06:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
if let Some(ch) = transform.channels {
|
2025-11-29 14:19:16 +01:00
|
|
|
|
self.db
|
|
|
|
|
|
.set_a_metadata(pk, "channels", serde_json::json!(ch))?;
|
2025-11-24 08:06:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
if let Some(ts) = transform.total_samples {
|
2025-11-29 14:19:16 +01:00
|
|
|
|
self.db
|
|
|
|
|
|
.set_a_metadata(pk, "total_samples", serde_json::json!(ts))?;
|
2025-11-24 08:06:56 +01:00
|
|
|
|
|
|
|
|
|
|
// Calculer la durée à partir de total_samples et sample_rate
|
|
|
|
|
|
if let Some(sr) = transform.sample_rate {
|
|
|
|
|
|
if sr > 0 {
|
|
|
|
|
|
let secs = (ts as f64 / sr as f64).round() as u64;
|
2025-11-29 14:19:16 +01:00
|
|
|
|
self.db
|
|
|
|
|
|
.set_a_metadata(pk, "duration_secs", serde_json::json!(secs))?;
|
2025-11-24 08:06:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tracing::debug!("Cache: No transform metadata available for pk {}", pk);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-16 08:34:33 +01:00
|
|
|
|
if let Err(e) = self.enforce_limit().await {
|
|
|
|
|
|
tracing::warn!("Error enforcing cache limit: {}", e);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-07 05:43:25 +00:00
|
|
|
|
// Lancer une tâche de nettoyage et marquage de complétion en background
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
let downloads_clone = self.downloads.clone();
|
|
|
|
|
|
let pk_clone = pk.to_string();
|
2025-11-07 05:43:25 +00:00
|
|
|
|
let completion_marker = self.get_completion_marker_path(pk);
|
|
|
|
|
|
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
tokio::spawn(async move {
|
2025-11-07 05:43:25 +00:00
|
|
|
|
let result = download.wait_until_finished().await;
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
downloads_clone.write().await.remove(&pk_clone);
|
2025-11-07 05:43:25 +00:00
|
|
|
|
|
|
|
|
|
|
// Créer le fichier marker de complétion si le téléchargement a réussi
|
|
|
|
|
|
if result.is_ok() {
|
|
|
|
|
|
if let Err(e) = std::fs::write(&completion_marker, "") {
|
2025-11-14 10:43:53 +01:00
|
|
|
|
tracing::warn!(
|
|
|
|
|
|
"Failed to create completion marker for pk {}: {}",
|
|
|
|
|
|
pk_clone,
|
|
|
|
|
|
e
|
|
|
|
|
|
);
|
2025-11-07 05:43:25 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
tracing::debug!("Created completion marker for pk {}", pk_clone);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Ok(pk.to_string())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 22:15:00 +02:00
|
|
|
|
/// Crée un nouveau cache sans transformer
|
2025-10-12 21:39:20 +02:00
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
2025-10-17 14:36:39 +02:00
|
|
|
|
/// * `dir` - Répertoire de stockage du cache
|
|
|
|
|
|
/// * `limit` - Limite de taille du cache (nombre d'éléments)
|
2025-10-18 09:58:39 +02:00
|
|
|
|
pub fn new(dir: &str, limit: usize) -> Result<Self> {
|
|
|
|
|
|
Self::with_transformer(dir, limit, None)
|
2025-10-17 22:15:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Crée un nouveau cache avec un transformer optionnel
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `dir` - Répertoire de stockage du cache
|
|
|
|
|
|
/// * `limit` - Limite de taille du cache (nombre d'éléments)
|
|
|
|
|
|
/// * `transformer_factory` - Factory pour créer des transformers à chaque téléchargement
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Exemple
|
|
|
|
|
|
///
|
2025-12-17 10:10:56 +01:00
|
|
|
|
/// ```rust,ignore
|
2025-10-17 22:15:00 +02:00
|
|
|
|
/// use pmocache::{Cache, CacheConfig, StreamTransformer};
|
|
|
|
|
|
/// use std::sync::Arc;
|
|
|
|
|
|
///
|
|
|
|
|
|
/// struct MyConfig;
|
|
|
|
|
|
/// impl CacheConfig for MyConfig {
|
|
|
|
|
|
/// fn file_extension() -> &'static str { "dat" }
|
|
|
|
|
|
/// }
|
|
|
|
|
|
///
|
|
|
|
|
|
/// let transformer_factory = Arc::new(|| {
|
2025-12-17 10:10:56 +01:00
|
|
|
|
/// // Créer un transformer qui effectue une opération personnalisée
|
|
|
|
|
|
/// Box::new(|_input, _file, _ctx| {
|
2025-10-17 22:15:00 +02:00
|
|
|
|
/// Box::pin(async move {
|
|
|
|
|
|
/// // Transformation personnalisée
|
|
|
|
|
|
/// Ok(())
|
|
|
|
|
|
/// })
|
|
|
|
|
|
/// }) as StreamTransformer
|
|
|
|
|
|
/// });
|
|
|
|
|
|
///
|
|
|
|
|
|
/// let cache = Cache::<MyConfig>::with_transformer(
|
|
|
|
|
|
/// "./cache",
|
|
|
|
|
|
/// 1000,
|
|
|
|
|
|
/// Some(transformer_factory)
|
|
|
|
|
|
/// ).unwrap();
|
|
|
|
|
|
/// ```
|
|
|
|
|
|
pub fn with_transformer(
|
|
|
|
|
|
dir: &str,
|
|
|
|
|
|
limit: usize,
|
|
|
|
|
|
transformer_factory: Option<Arc<dyn Fn() -> StreamTransformer + Send + Sync>>,
|
|
|
|
|
|
) -> Result<Self> {
|
2025-10-17 14:36:39 +02:00
|
|
|
|
let directory = PathBuf::from(dir);
|
|
|
|
|
|
std::fs::create_dir_all(&directory)?;
|
2025-10-26 09:18:48 +01:00
|
|
|
|
let db = DB::init(&directory.join("cache.db"))?;
|
2025-10-12 21:39:20 +02:00
|
|
|
|
|
2025-12-12 16:36:31 +00:00
|
|
|
|
// Créer un channel pour les events (capacité de 100 events en buffer)
|
|
|
|
|
|
let (served_tx, _) = broadcast::channel(100);
|
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
|
Ok(Self {
|
2025-10-17 14:36:39 +02:00
|
|
|
|
dir: directory,
|
|
|
|
|
|
limit,
|
2025-10-12 21:39:20 +02:00
|
|
|
|
db: Arc::new(db),
|
2025-10-17 19:48:53 +02:00
|
|
|
|
downloads: Arc::new(RwLock::new(HashMap::new())),
|
2025-11-29 12:38:23 +01:00
|
|
|
|
serve_subscribers: Arc::new(RwLock::new(HashMap::new())),
|
|
|
|
|
|
subscriber_counter: AtomicU64::new(1),
|
2025-10-17 22:15:00 +02:00
|
|
|
|
transformer_factory,
|
2025-11-06 08:05:41 +00:00
|
|
|
|
min_prebuffer_size: DEFAULT_PREBUFFER_SIZE,
|
2025-12-12 16:36:31 +00:00
|
|
|
|
served_tx: Some(served_tx),
|
2025-12-15 15:05:35 +01:00
|
|
|
|
lazy_providers: StdRwLock::new(HashMap::new()),
|
2025-10-17 14:36:39 +02:00
|
|
|
|
_phantom: std::marker::PhantomData,
|
2025-10-12 21:39:20 +02:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-17 15:15:10 +01:00
|
|
|
|
/// Lance une consolidation en arrière-plan pour un cache existant
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Cette fonction utilitaire lance une tâche asynchrone qui consolide le cache
|
|
|
|
|
|
/// (supprime les fichiers incomplets sans marker de complétion) et retourne
|
|
|
|
|
|
/// immédiatement le cache fourni en paramètre.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Idéal pour les crates spécialisées qui veulent offrir une fonction
|
|
|
|
|
|
/// `new_cache_with_consolidation` sans dupliquer la logique de lancement.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `cache` - Instance du cache à consolider
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Le même `Arc<Cache<C>>` fourni en paramètre
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Exemple
|
|
|
|
|
|
///
|
|
|
|
|
|
/// ```rust,ignore
|
|
|
|
|
|
/// use pmocache::{Cache, CacheConfig};
|
|
|
|
|
|
/// use std::sync::Arc;
|
|
|
|
|
|
///
|
|
|
|
|
|
/// struct MyConfig;
|
|
|
|
|
|
/// impl CacheConfig for MyConfig {
|
|
|
|
|
|
/// fn file_extension() -> &'static str { "dat" }
|
|
|
|
|
|
/// }
|
|
|
|
|
|
///
|
|
|
|
|
|
/// async fn create_cache_with_cleanup() -> anyhow::Result<Arc<Cache<MyConfig>>> {
|
|
|
|
|
|
/// let cache = Arc::new(Cache::new("./cache", 1000)?);
|
|
|
|
|
|
/// Ok(Cache::with_consolidation(cache).await)
|
|
|
|
|
|
/// }
|
|
|
|
|
|
/// ```
|
|
|
|
|
|
pub async fn with_consolidation(cache: Arc<Cache<C>>) -> Arc<Cache<C>> {
|
|
|
|
|
|
let cache_clone = cache.clone();
|
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
|
if let Err(e) = cache_clone.consolidate().await {
|
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
|
"Failed to consolidate {} cache on startup: {}",
|
|
|
|
|
|
C::cache_name(),
|
|
|
|
|
|
e
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tracing::info!(
|
|
|
|
|
|
"{} cache consolidated successfully on startup",
|
|
|
|
|
|
C::cache_name()
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
cache
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-06 08:05:41 +00:00
|
|
|
|
/// Configure la taille minimale de prébuffering
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `size` - Taille minimale en octets (0 = désactivé)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Exemple
|
|
|
|
|
|
///
|
|
|
|
|
|
/// ```rust,no_run
|
|
|
|
|
|
/// use pmocache::{Cache, CacheConfig};
|
|
|
|
|
|
///
|
|
|
|
|
|
/// struct MyConfig;
|
|
|
|
|
|
/// impl CacheConfig for MyConfig {
|
|
|
|
|
|
/// fn file_extension() -> &'static str { "dat" }
|
|
|
|
|
|
/// }
|
|
|
|
|
|
///
|
|
|
|
|
|
/// let mut cache = Cache::<MyConfig>::new("./cache", 1000).unwrap();
|
|
|
|
|
|
/// cache.set_prebuffer_size(1024 * 1024); // 1 MB de prébuffering
|
|
|
|
|
|
/// ```
|
|
|
|
|
|
pub fn set_prebuffer_size(&mut self, size: u64) {
|
|
|
|
|
|
self.min_prebuffer_size = size;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Retourne la taille minimale de prébuffering configurée
|
|
|
|
|
|
pub fn get_prebuffer_size(&self) -> u64 {
|
|
|
|
|
|
self.min_prebuffer_size
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 15:05:35 +01:00
|
|
|
|
/// Enregistre un provider responsable d'un préfixe de lazy PK.
|
|
|
|
|
|
pub fn register_lazy_provider(&self, provider: Arc<dyn LazyProvider>) {
|
|
|
|
|
|
let prefix = provider.lazy_prefix().to_string();
|
|
|
|
|
|
let mut guard = self
|
|
|
|
|
|
.lazy_providers
|
|
|
|
|
|
.write()
|
|
|
|
|
|
.expect("lazy provider registry poisoned");
|
|
|
|
|
|
guard.insert(prefix, provider);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Désenregistre un provider à partir de son préfixe.
|
|
|
|
|
|
pub fn unregister_lazy_provider(&self, prefix: &str) {
|
|
|
|
|
|
let mut guard = self
|
|
|
|
|
|
.lazy_providers
|
|
|
|
|
|
.write()
|
|
|
|
|
|
.expect("lazy provider registry poisoned");
|
|
|
|
|
|
guard.remove(prefix);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn provider_for_lazy_pk(&self, lazy_pk: &str) -> Option<Arc<dyn LazyProvider>> {
|
|
|
|
|
|
let prefix = lazy_prefix_from_pk(lazy_pk)?;
|
|
|
|
|
|
let guard = self
|
|
|
|
|
|
.lazy_providers
|
|
|
|
|
|
.read()
|
|
|
|
|
|
.expect("lazy provider registry poisoned");
|
|
|
|
|
|
guard.get(prefix).cloned()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-29 12:38:23 +01:00
|
|
|
|
/// S'abonne aux diffusions HTTP pour un `pk` donné.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// La callback est appelée à chaque fois qu'un élément est servi avec succès via les routes
|
|
|
|
|
|
/// HTTP du cache. Si la callback retourne `false`, elle est automatiquement désinscrite ;
|
|
|
|
|
|
/// retourner `true` permet de rester abonné aux diffusions suivantes.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Retourne un [`CacheSubscription`] à conserver pour se désabonner explicitement via
|
|
|
|
|
|
/// [`Cache::unsubscribe_broadcast`].
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Exemple
|
|
|
|
|
|
///
|
|
|
|
|
|
/// ```rust,no_run
|
|
|
|
|
|
/// use pmocache::{Cache, CacheConfig, CacheSubscription};
|
|
|
|
|
|
/// use std::sync::Arc;
|
|
|
|
|
|
///
|
|
|
|
|
|
/// struct MyConfig;
|
|
|
|
|
|
/// impl CacheConfig for MyConfig {
|
|
|
|
|
|
/// fn file_extension() -> &'static str { "dat" }
|
|
|
|
|
|
/// }
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # async fn demo() -> anyhow::Result<()> {
|
|
|
|
|
|
/// let cache = Arc::new(Cache::<MyConfig>::new("/tmp/cache", 100)?);
|
|
|
|
|
|
/// let token: CacheSubscription = cache
|
|
|
|
|
|
/// .subscribe_broadcast("abc123", |event| {
|
|
|
|
|
|
/// println!("{} served with param {}", event.pk, event.qualifier);
|
|
|
|
|
|
/// // Retourner true pour rester abonné
|
|
|
|
|
|
/// true
|
|
|
|
|
|
/// })
|
|
|
|
|
|
/// .await;
|
|
|
|
|
|
///
|
|
|
|
|
|
/// // ... plus tard, pour se désabonner explicitement :
|
|
|
|
|
|
/// cache.unsubscribe_broadcast(&token).await;
|
|
|
|
|
|
/// # Ok(())
|
|
|
|
|
|
/// # }
|
|
|
|
|
|
/// ```
|
|
|
|
|
|
pub async fn subscribe_broadcast<F>(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
pk: impl Into<String>,
|
|
|
|
|
|
callback: F,
|
|
|
|
|
|
) -> CacheSubscription
|
|
|
|
|
|
where
|
|
|
|
|
|
F: Fn(&CacheBroadcastEvent) -> bool + Send + Sync + 'static,
|
|
|
|
|
|
{
|
|
|
|
|
|
let pk = pk.into();
|
|
|
|
|
|
let id = self.subscriber_counter.fetch_add(1, Ordering::Relaxed);
|
|
|
|
|
|
let mut subscribers = self.serve_subscribers.write().await;
|
|
|
|
|
|
subscribers
|
|
|
|
|
|
.entry(pk.clone())
|
|
|
|
|
|
.or_default()
|
|
|
|
|
|
.push((id, Arc::new(callback)));
|
|
|
|
|
|
|
|
|
|
|
|
CacheSubscription { pk, id }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Désabonne une callback précédemment enregistrée via [`Cache::subscribe_broadcast`].
|
|
|
|
|
|
///
|
|
|
|
|
|
/// N'a aucun effet si le token est inconnu ou déjà désinscrit.
|
|
|
|
|
|
pub async fn unsubscribe_broadcast(&self, token: &CacheSubscription) {
|
|
|
|
|
|
let mut subscribers = self.serve_subscribers.write().await;
|
|
|
|
|
|
if let Some(list) = subscribers.get_mut(&token.pk) {
|
|
|
|
|
|
list.retain(|(id, _)| *id != token.id);
|
|
|
|
|
|
if list.is_empty() {
|
|
|
|
|
|
subscribers.remove(&token.pk);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Notifie les abonnés qu'un élément du cache a été diffusé via HTTP.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Interne au crate : les routes Axum appellent cette méthode lorsqu'une réponse 2xx est
|
|
|
|
|
|
/// renvoyée. Les callbacks qui retournent `false` sont retirées.
|
|
|
|
|
|
pub(crate) async fn notify_broadcast(&self, pk: &str, qualifier: &str) {
|
|
|
|
|
|
let mut subscribers = self.serve_subscribers.write().await;
|
|
|
|
|
|
if let Some(callbacks) = subscribers.get_mut(pk) {
|
|
|
|
|
|
let event = CacheBroadcastEvent {
|
|
|
|
|
|
pk: pk.to_string(),
|
|
|
|
|
|
qualifier: qualifier.to_string(),
|
|
|
|
|
|
cache_name: C::cache_name(),
|
|
|
|
|
|
cache_type: C::cache_type(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let mut to_keep = Vec::new();
|
|
|
|
|
|
for (id, callback) in callbacks.drain(..) {
|
|
|
|
|
|
if callback(&event) {
|
|
|
|
|
|
to_keep.push((id, callback));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if to_keep.is_empty() {
|
|
|
|
|
|
subscribers.remove(pk);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
callbacks.extend(to_keep);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
|
/// Télécharge un fichier depuis une URL et l'ajoute au cache
|
|
|
|
|
|
///
|
2025-10-25 17:14:24 +02:00
|
|
|
|
/// Cette méthode utilise un système d'identifiants basé sur le contenu plutôt que sur l'URL.
|
|
|
|
|
|
/// Elle télécharge les 512 premiers octets du fichier pour calculer un identifiant unique (pk),
|
|
|
|
|
|
/// puis vérifie si le fichier est déjà en cache. Si c'est le cas, elle met à jour le timestamp
|
|
|
|
|
|
/// et retourne rapidement. Sinon, elle lance le téléchargement complet en arrière-plan.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Workflow
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 1. Télécharge les 512 premiers octets via une requête HTTP partielle
|
|
|
|
|
|
/// 2. Calcule le pk en hashant (SHA256) ces premiers octets
|
|
|
|
|
|
/// 3. Vérifie si le fichier existe déjà dans le cache
|
|
|
|
|
|
/// 4. Si oui : update timestamp et retour rapide
|
|
|
|
|
|
/// 5. Si non : lance le téléchargement complet en background
|
2025-10-17 19:48:53 +02:00
|
|
|
|
///
|
2025-10-12 21:39:20 +02:00
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `url` - URL du fichier à télécharger
|
|
|
|
|
|
/// * `collection` - Collection optionnelle à laquelle appartient le fichier
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
///
|
2025-10-25 17:14:24 +02:00
|
|
|
|
/// La clé primaire (pk) du fichier dans le cache, calculée à partir du contenu
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Note
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Deux URLs différentes pointant vers le même contenu auront le même pk,
|
|
|
|
|
|
/// permettant une déduplication automatique.
|
2025-10-12 21:39:20 +02:00
|
|
|
|
pub async fn add_from_url(&self, url: &str, collection: Option<&str>) -> Result<String> {
|
2025-11-26 10:35:45 +01:00
|
|
|
|
// 0. Vérifier d'abord si cette URL est déjà en cache (optimisation réseau)
|
|
|
|
|
|
if let Ok(Some(existing_pk)) = self.db.get_pk_by_origin_url(url) {
|
|
|
|
|
|
// Vérifier que le fichier est toujours complet et valide
|
|
|
|
|
|
if self.check_cached_and_complete(&existing_pk).await? {
|
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
|
"URL {} already in cache with pk {}, skipping download",
|
|
|
|
|
|
url,
|
|
|
|
|
|
existing_pk
|
|
|
|
|
|
);
|
|
|
|
|
|
self.db.update_hit(&existing_pk)?;
|
|
|
|
|
|
return Ok(existing_pk);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
|
"URL {} found in DB with pk {} but file is incomplete, re-downloading",
|
|
|
|
|
|
url,
|
|
|
|
|
|
existing_pk
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Télécharger les 2048 premiers octets pour calculer le pk
|
|
|
|
|
|
let header = crate::download::peek_header(url, 2048)
|
2025-10-25 17:14:24 +02:00
|
|
|
|
.await
|
|
|
|
|
|
.map_err(|e| anyhow!("Failed to peek header: {}", e))?;
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Calculer le pk basé sur le contenu
|
|
|
|
|
|
let pk = crate::cache_trait::pk_from_content_header(&header);
|
|
|
|
|
|
tracing::debug!("Computed pk {} for URL {}", pk, url);
|
|
|
|
|
|
|
2025-11-06 08:09:33 +00:00
|
|
|
|
// 3. Vérifier si le fichier est déjà en cache ET complet
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
if self.check_cached_and_complete(&pk).await? {
|
|
|
|
|
|
tracing::debug!("File with pk {} already in cache, updating timestamp", pk);
|
|
|
|
|
|
self.db.update_hit(&pk)?;
|
|
|
|
|
|
return Ok(pk);
|
2025-10-25 17:14:24 +02:00
|
|
|
|
}
|
2025-10-17 19:48:53 +02:00
|
|
|
|
|
2025-10-25 17:14:24 +02:00
|
|
|
|
// 4. Vérifier si un download est déjà en cours pour ce pk
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
if let Some(pk) = self.check_ongoing_download(&pk).await? {
|
2025-11-06 08:05:41 +00:00
|
|
|
|
return Ok(pk);
|
2025-10-17 19:48:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-25 17:14:24 +02:00
|
|
|
|
// 5. Lancer le téléchargement complet avec transformer
|
|
|
|
|
|
tracing::debug!("Starting full download for pk {} from URL {}", pk, url);
|
2025-10-26 13:05:46 +01:00
|
|
|
|
let file_path = self.get_file_path(&pk);
|
2025-10-17 22:15:00 +02:00
|
|
|
|
let transformer = self.transformer_factory.as_ref().map(|f| f());
|
2025-10-19 13:42:29 +02:00
|
|
|
|
let download = download_with_transformer(&file_path, url, transformer);
|
2025-10-17 19:48:53 +02:00
|
|
|
|
|
|
|
|
|
|
// Stocker dans la map des downloads en cours
|
|
|
|
|
|
{
|
|
|
|
|
|
let mut downloads = self.downloads.write().await;
|
|
|
|
|
|
downloads.insert(pk.clone(), download.clone());
|
2025-10-12 21:39:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
// Finaliser avec prébuffering et nettoyage
|
2025-12-17 10:10:56 +01:00
|
|
|
|
self.finalize_download(
|
|
|
|
|
|
&pk,
|
|
|
|
|
|
download,
|
|
|
|
|
|
collection,
|
|
|
|
|
|
Some(url),
|
|
|
|
|
|
FinalizeMode::InsertNew,
|
|
|
|
|
|
)
|
|
|
|
|
|
.await
|
2025-12-15 15:05:35 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Télécharge un fichier lazy et commute l'entrée existante
|
|
|
|
|
|
pub async fn download_lazy_from_url(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
lazy_pk: &str,
|
|
|
|
|
|
url: &str,
|
|
|
|
|
|
collection: Option<&str>,
|
|
|
|
|
|
) -> Result<String> {
|
|
|
|
|
|
// Si déjà converti, retourner directement
|
|
|
|
|
|
if let Ok(Some(real_pk)) = self.db.get_pk_by_lazy_pk(lazy_pk) {
|
|
|
|
|
|
return Ok(real_pk);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Si l'URL pointe déjà vers un fichier complet, commuter sans re-télécharger
|
|
|
|
|
|
if let Ok(Some(existing_pk)) = self.db.get_pk_by_origin_url(url) {
|
|
|
|
|
|
if existing_pk != lazy_pk && self.check_cached_and_complete(&existing_pk).await? {
|
|
|
|
|
|
self.db.update_lazy_to_downloaded(lazy_pk, &existing_pk)?;
|
|
|
|
|
|
return Ok(existing_pk);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Télécharger les 2048 premiers octets pour calculer le pk
|
|
|
|
|
|
let header = crate::download::peek_header(url, 2048)
|
2025-11-16 08:34:33 +01:00
|
|
|
|
.await
|
2025-12-15 15:05:35 +01:00
|
|
|
|
.map_err(|e| anyhow!("Failed to peek header: {}", e))?;
|
|
|
|
|
|
let pk = crate::cache_trait::pk_from_content_header(&header);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Si déjà en cache (complet), commuter directement
|
|
|
|
|
|
if self.check_cached_and_complete(&pk).await? {
|
|
|
|
|
|
self.db.update_lazy_to_downloaded(lazy_pk, &pk)?;
|
|
|
|
|
|
return Ok(pk);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. Lancer le téléchargement complet
|
|
|
|
|
|
tracing::debug!("Starting lazy download for pk {} (lazy {})", pk, lazy_pk);
|
|
|
|
|
|
let file_path = self.get_file_path(&pk);
|
|
|
|
|
|
let transformer = self.transformer_factory.as_ref().map(|f| f());
|
|
|
|
|
|
let download = download_with_transformer(&file_path, url, transformer);
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
let mut downloads = self.downloads.write().await;
|
|
|
|
|
|
downloads.insert(pk.clone(), download.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.finalize_download(
|
|
|
|
|
|
&pk,
|
|
|
|
|
|
download,
|
|
|
|
|
|
collection,
|
|
|
|
|
|
Some(url),
|
|
|
|
|
|
FinalizeMode::ConvertLazy { lazy_pk },
|
|
|
|
|
|
)
|
|
|
|
|
|
.await
|
2025-10-17 19:48:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-19 17:26:06 +02:00
|
|
|
|
/// Ajoute un fichier à partir d'un flux asynchrone.
|
|
|
|
|
|
///
|
2025-10-25 17:14:24 +02:00
|
|
|
|
/// Cette méthode utilise le même système d'identifiants basé sur le contenu que `add_from_url`.
|
|
|
|
|
|
/// Elle lit les 512 premiers octets du flux pour calculer l'identifiant, puis reconstitue
|
|
|
|
|
|
/// le flux complet pour l'ingestion.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Workflow
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 1. Lit les 512 premiers octets du reader
|
|
|
|
|
|
/// 2. Calcule le pk en hashant (SHA256) ces premiers octets
|
|
|
|
|
|
/// 3. Vérifie si le fichier existe déjà dans le cache
|
|
|
|
|
|
/// 4. Si oui : update timestamp et retour rapide
|
|
|
|
|
|
/// 5. Si non : reconstitue le reader (header + reste) et lance l'ingestion
|
2025-10-19 17:26:06 +02:00
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
2025-11-02 15:06:27 +01:00
|
|
|
|
/// * `source_uri` - Identifiant logique optionnel du flux (pour traçabilité dans la DB). Si None, l'origin_url ne sera pas sauvegardée.
|
2025-10-19 17:26:06 +02:00
|
|
|
|
/// * `reader` - Flux asynchrone fournissant les données
|
|
|
|
|
|
/// * `length` - Taille attendue (si connue)
|
|
|
|
|
|
/// * `collection` - Collection optionnelle à laquelle appartient l'élément
|
2025-10-25 17:14:24 +02:00
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
///
|
|
|
|
|
|
/// La clé primaire (pk) du fichier dans le cache, calculée à partir du contenu
|
2025-10-19 17:26:06 +02:00
|
|
|
|
pub async fn add_from_reader<R>(
|
|
|
|
|
|
&self,
|
2025-11-02 15:06:27 +01:00
|
|
|
|
source_uri: Option<&str>,
|
2025-12-12 16:36:31 +00:00
|
|
|
|
reader: R,
|
2025-10-19 17:26:06 +02:00
|
|
|
|
length: Option<u64>,
|
|
|
|
|
|
collection: Option<&str>,
|
|
|
|
|
|
) -> Result<String>
|
|
|
|
|
|
where
|
|
|
|
|
|
R: AsyncRead + Send + Unpin + 'static,
|
|
|
|
|
|
{
|
2025-11-14 10:43:53 +01:00
|
|
|
|
self.add_from_reader_with_pk(source_uri, reader, length, collection, None)
|
|
|
|
|
|
.await
|
2025-11-07 06:23:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Ajoute un fichier à partir d'un flux avec un pk explicite optionnel.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Si `explicit_pk` est fourni, utilise ce pk au lieu de le calculer à partir du contenu.
|
|
|
|
|
|
/// Ceci est utile quand plusieurs fichiers ont le même header mais doivent être cachés séparément
|
|
|
|
|
|
/// (par exemple, des fichiers FLAC avec le même format mais du contenu différent).
|
|
|
|
|
|
pub async fn add_from_reader_with_pk<R>(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
source_uri: Option<&str>,
|
|
|
|
|
|
mut reader: R,
|
|
|
|
|
|
length: Option<u64>,
|
|
|
|
|
|
collection: Option<&str>,
|
|
|
|
|
|
explicit_pk: Option<String>,
|
|
|
|
|
|
) -> Result<String>
|
|
|
|
|
|
where
|
|
|
|
|
|
R: AsyncRead + Send + Unpin + 'static,
|
|
|
|
|
|
{
|
2025-11-07 07:24:19 +00:00
|
|
|
|
// 1. Lire EXACTEMENT 1024 octets (ou EOF si fichier plus petit)
|
|
|
|
|
|
// Utilise read_exact_or_eof qui boucle jusqu'à avoir tous les octets demandés
|
|
|
|
|
|
let header = crate::download::read_exact_or_eof(&mut reader, 1024)
|
2025-10-25 17:14:24 +02:00
|
|
|
|
.await
|
2025-11-07 07:24:19 +00:00
|
|
|
|
.map_err(|e| anyhow!("Failed to read header bytes: {}", e))?;
|
2025-10-19 17:26:06 +02:00
|
|
|
|
|
2025-11-07 07:27:20 +00:00
|
|
|
|
// 2. Calculer le pk selon la taille du fichier
|
|
|
|
|
|
// - Fichiers >= 1024 octets (FLAC): skip header (512 premiers octets), utilise octets 512-1024
|
|
|
|
|
|
// - Fichiers < 1024 octets (images, petits fichiers): utilise TOUT le contenu
|
2025-11-07 06:23:35 +00:00
|
|
|
|
let pk = if let Some(explicit) = explicit_pk {
|
|
|
|
|
|
explicit
|
|
|
|
|
|
} else {
|
2025-11-07 07:27:20 +00:00
|
|
|
|
let pk_bytes = if header.len() >= 1024 {
|
|
|
|
|
|
// Gros fichier (>= 1024 octets): skip les 512 premiers (header FLAC)
|
2025-11-07 06:23:35 +00:00
|
|
|
|
&header[512..]
|
|
|
|
|
|
} else {
|
2025-11-07 07:27:20 +00:00
|
|
|
|
// Petit fichier (< 1024 octets): utiliser TOUT le contenu
|
2025-11-07 06:23:35 +00:00
|
|
|
|
&header[..]
|
|
|
|
|
|
};
|
|
|
|
|
|
crate::cache_trait::pk_from_content_header(pk_bytes)
|
|
|
|
|
|
};
|
2025-11-02 15:06:27 +01:00
|
|
|
|
if let Some(uri) = source_uri {
|
|
|
|
|
|
tracing::debug!("Computed pk {} for source_uri {}", pk, uri);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tracing::debug!("Computed pk {} from reader", pk);
|
|
|
|
|
|
}
|
2025-10-25 17:14:24 +02:00
|
|
|
|
|
2025-11-06 08:09:33 +00:00
|
|
|
|
// 3. Vérifier si le fichier est déjà en cache ET complet
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
if self.check_cached_and_complete(&pk).await? {
|
|
|
|
|
|
tracing::debug!("File with pk {} already in cache, updating timestamp", pk);
|
|
|
|
|
|
self.db.update_hit(&pk)?;
|
|
|
|
|
|
return Ok(pk);
|
2025-10-25 17:14:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. Vérifier si un download est déjà en cours pour ce pk
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
if let Some(pk) = self.check_ongoing_download(&pk).await? {
|
2025-11-06 08:05:41 +00:00
|
|
|
|
return Ok(pk);
|
2025-10-19 17:26:06 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-25 17:14:24 +02:00
|
|
|
|
// 5. Reconstituer le reader complet (header + reste)
|
|
|
|
|
|
use std::io::Cursor;
|
|
|
|
|
|
let header_reader = Cursor::new(header);
|
|
|
|
|
|
let full_reader = header_reader.chain(reader);
|
|
|
|
|
|
|
|
|
|
|
|
// 6. Lancer l'ingestion avec transformer
|
|
|
|
|
|
tracing::debug!("Starting ingestion for pk {} from reader", pk);
|
2025-10-26 13:05:46 +01:00
|
|
|
|
let file_path = self.get_file_path(&pk);
|
2025-10-19 17:26:06 +02:00
|
|
|
|
let transformer = self.transformer_factory.as_ref().map(|factory| factory());
|
2025-10-25 17:14:24 +02:00
|
|
|
|
let download = ingest_with_transformer(&file_path, full_reader, length, transformer);
|
2025-10-19 17:26:06 +02:00
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
let mut downloads = self.downloads.write().await;
|
|
|
|
|
|
downloads.insert(pk.clone(), download.clone());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
// Finaliser avec prébuffering et nettoyage
|
2025-12-17 10:10:56 +01:00
|
|
|
|
self.finalize_download(
|
|
|
|
|
|
&pk,
|
|
|
|
|
|
download,
|
|
|
|
|
|
collection,
|
|
|
|
|
|
source_uri,
|
|
|
|
|
|
FinalizeMode::InsertNew,
|
|
|
|
|
|
)
|
|
|
|
|
|
.await
|
2025-10-19 17:26:06 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-17 10:10:56 +01:00
|
|
|
|
/// Ajoute un fichier local au cache en copiant son contenu.
|
2025-10-17 19:48:53 +02:00
|
|
|
|
pub async fn add_from_file(&self, path: &str, collection: Option<&str>) -> Result<String> {
|
|
|
|
|
|
let canonical_path = std::fs::canonicalize(path)?;
|
|
|
|
|
|
let file_url = format!("file://{}", canonical_path.display());
|
2025-10-19 17:26:06 +02:00
|
|
|
|
let length = tokio::fs::metadata(&canonical_path)
|
|
|
|
|
|
.await
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.map(|m| m.len());
|
|
|
|
|
|
let reader = tokio::fs::File::open(&canonical_path).await?;
|
2025-10-25 17:14:24 +02:00
|
|
|
|
|
2025-11-02 15:06:27 +01:00
|
|
|
|
self.add_from_reader(Some(&file_url), reader, length, collection)
|
2025-10-19 17:26:06 +02:00
|
|
|
|
.await
|
2025-10-12 21:39:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-17 10:10:56 +01:00
|
|
|
|
/// Enregistre une référence vers un fichier déjà présent sur le disque sans duplication.
|
|
|
|
|
|
pub async fn register_local_file_reference(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
pk: &str,
|
|
|
|
|
|
source_path: &Path,
|
|
|
|
|
|
collection: Option<&str>,
|
|
|
|
|
|
origin_url: Option<&str>,
|
|
|
|
|
|
extra_metadata: Option<&[(String, Value)]>,
|
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
|
let cache_path = self.get_file_path(pk);
|
|
|
|
|
|
if cache_path.exists() {
|
|
|
|
|
|
if let Err(err) = tokio::fs::remove_file(&cache_path).await {
|
|
|
|
|
|
if err.kind() != std::io::ErrorKind::NotFound {
|
|
|
|
|
|
return Err(err.into());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
link_file(source_path, &cache_path)
|
|
|
|
|
|
.map_err(|e| anyhow!("Failed to link local file into cache: {}", e))?;
|
|
|
|
|
|
|
|
|
|
|
|
let completion_marker = self.get_completion_marker_path(pk);
|
|
|
|
|
|
if completion_marker.exists() {
|
|
|
|
|
|
let _ = std::fs::remove_file(&completion_marker);
|
|
|
|
|
|
}
|
|
|
|
|
|
std::fs::write(&completion_marker, "")
|
|
|
|
|
|
.map_err(|e| anyhow!("Failed to create completion marker for local file: {}", e))?;
|
|
|
|
|
|
|
|
|
|
|
|
self.db.add(pk, None, collection)?;
|
|
|
|
|
|
if let Some(url) = origin_url {
|
|
|
|
|
|
self.db.set_origin_url(pk, url)?;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(entries) = extra_metadata {
|
|
|
|
|
|
for (key, value) in entries {
|
|
|
|
|
|
self.db.set_a_metadata(pk, key, value.clone())?;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Err(e) = self.enforce_limit().await {
|
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
|
"Error enforcing cache limit after local file registration (pk={}): {}",
|
|
|
|
|
|
pk,
|
|
|
|
|
|
e
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 09:18:48 +01:00
|
|
|
|
pub async fn delete_item(&self, pk: &str) -> Result<()> {
|
|
|
|
|
|
// Vérifie l'existence pour signaler une erreur explicite si l'entrée est absente
|
|
|
|
|
|
self.db.get(pk, false)?;
|
|
|
|
|
|
|
|
|
|
|
|
// Oublie un téléchargement en cours pour cette clé
|
|
|
|
|
|
self.downloads.write().await.remove(pk);
|
|
|
|
|
|
|
|
|
|
|
|
// Supprime chaque fichier {pk}.{qualifier}.{ext} (ignorer si déjà absent)
|
|
|
|
|
|
for path in self.get_file_paths(pk)? {
|
|
|
|
|
|
if let Err(err) = tokio::fs::remove_file(&path).await {
|
|
|
|
|
|
if err.kind() != std::io::ErrorKind::NotFound {
|
|
|
|
|
|
return Err(err.into());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Efface l’entrée de la base (les métadonnées partent via ON DELETE CASCADE)
|
|
|
|
|
|
self.db.delete(pk)?;
|
|
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
pub async fn delete_collection(&self, collection: &str) -> Result<()> {
|
|
|
|
|
|
let entries = self.db.get_by_collection(collection, false)?;
|
|
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
let mut downloads = self.downloads.write().await;
|
|
|
|
|
|
for entry in &entries {
|
|
|
|
|
|
downloads.remove(&entry.pk);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for entry in &entries {
|
|
|
|
|
|
for path in self.get_file_paths(&entry.pk)? {
|
|
|
|
|
|
if let Err(err) = tokio::fs::remove_file(&path).await {
|
|
|
|
|
|
if err.kind() != std::io::ErrorKind::NotFound {
|
|
|
|
|
|
return Err(err.into());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.db.delete_collection(collection)?;
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
|
/// Récupère le chemin d'un fichier dans le cache
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire du fichier
|
|
|
|
|
|
pub async fn get(&self, pk: &str) -> Result<PathBuf> {
|
2025-10-26 09:18:48 +01:00
|
|
|
|
self.db.get(pk, false)?;
|
2025-10-12 21:39:20 +02:00
|
|
|
|
self.db.update_hit(pk)?;
|
|
|
|
|
|
|
2025-10-26 13:05:46 +01:00
|
|
|
|
let file_path = self.get_file_path(pk);
|
2025-10-12 21:39:20 +02:00
|
|
|
|
if file_path.exists() {
|
|
|
|
|
|
Ok(file_path)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Err(anyhow!("File not found"))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 13:05:46 +01:00
|
|
|
|
/// Récupère une métadonnée précise pour une entrée du cache.
|
|
|
|
|
|
pub async fn get_a_metadata(&self, pk: &str, key: &str) -> Result<Option<Value>> {
|
|
|
|
|
|
Ok(self.db.get_metadata_value(pk, key)?)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Récupère une métadonnée en tant que chaîne, si disponible.
|
|
|
|
|
|
pub async fn get_a_metadata_as_string(&self, pk: &str, key: &str) -> Result<Option<String>> {
|
|
|
|
|
|
match self.get_a_metadata(pk, key).await? {
|
|
|
|
|
|
Some(Value::String(s)) => Ok(Some(s)),
|
|
|
|
|
|
Some(Value::Null) | None => Ok(None),
|
|
|
|
|
|
Some(other) => bail!("metadata '{key}' for pk '{pk}' is not a string (found {other})"),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Récupère une métadonnée en tant que nombre JSON (`serde_json::Number`).
|
|
|
|
|
|
pub async fn get_a_metadata_as_number(&self, pk: &str, key: &str) -> Result<Option<Number>> {
|
|
|
|
|
|
match self.get_a_metadata(pk, key).await? {
|
|
|
|
|
|
Some(Value::Number(n)) => Ok(Some(n)),
|
|
|
|
|
|
Some(Value::Null) | None => Ok(None),
|
|
|
|
|
|
Some(other) => bail!("metadata '{key}' for pk '{pk}' is not a number (found {other})"),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Récupère une métadonnée en tant que booléen.
|
|
|
|
|
|
pub async fn get_a_metadata_as_bool(&self, pk: &str, key: &str) -> Result<Option<bool>> {
|
|
|
|
|
|
match self.get_a_metadata(pk, key).await? {
|
|
|
|
|
|
Some(Value::Bool(b)) => Ok(Some(b)),
|
|
|
|
|
|
Some(Value::Null) | None => Ok(None),
|
|
|
|
|
|
Some(other) => bail!("metadata '{key}' for pk '{pk}' is not a boolean (found {other})"),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-26 09:18:48 +01:00
|
|
|
|
pub async fn touch(&self, pk: &str) -> Result<()> {
|
|
|
|
|
|
self.db.update_hit(pk)?;
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 22:16:31 +01:00
|
|
|
|
/// Épingle un item pour le protéger de l'éviction LRU
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Un item épinglé ne peut pas être supprimé automatiquement par la politique LRU
|
|
|
|
|
|
/// et ne compte pas dans la limite du cache.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire de l'item à épingler
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Retourne une erreur si l'item a un TTL défini (incompatibilité métier)
|
|
|
|
|
|
pub async fn pin(&self, pk: &str) -> Result<()> {
|
|
|
|
|
|
self.db.pin(pk).map_err(|e| anyhow!(e))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Désépingle un item pour le rendre à nouveau éligible à l'éviction LRU
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire de l'item à désépingler
|
|
|
|
|
|
pub async fn unpin(&self, pk: &str) -> Result<()> {
|
|
|
|
|
|
self.db.unpin(pk).map_err(|e| anyhow!(e))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Vérifie si un item est épinglé
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire de l'item
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
///
|
|
|
|
|
|
/// `true` si l'item est épinglé, `false` sinon
|
|
|
|
|
|
pub async fn is_pinned(&self, pk: &str) -> Result<bool> {
|
|
|
|
|
|
self.db.is_pinned(pk).map_err(|e| anyhow!(e))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Définit le TTL (Time To Live) d'un item
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire de l'item
|
|
|
|
|
|
/// * `expires_at` - Date/heure d'expiration au format RFC3339
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Errors
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Retourne une erreur si l'item est épinglé (incompatibilité métier)
|
|
|
|
|
|
pub async fn set_ttl(&self, pk: &str, expires_at: &str) -> Result<()> {
|
|
|
|
|
|
self.db.set_ttl(pk, expires_at).map_err(|e| anyhow!(e))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Supprime le TTL d'un item
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire de l'item
|
|
|
|
|
|
pub async fn clear_ttl(&self, pk: &str) -> Result<()> {
|
|
|
|
|
|
self.db.clear_ttl(pk).map_err(|e| anyhow!(e))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
|
/// Récupère tous les fichiers d'une collection
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `collection` - Identifiant de la collection
|
|
|
|
|
|
pub async fn get_collection(&self, collection: &str) -> Result<Vec<PathBuf>> {
|
2025-10-26 09:18:48 +01:00
|
|
|
|
let entries = self.db.get_by_collection(collection, false)?;
|
2025-10-12 21:39:20 +02:00
|
|
|
|
let mut paths = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
for entry in entries {
|
2025-10-26 13:05:46 +01:00
|
|
|
|
let path = self.get_file_path(&entry.pk);
|
2025-10-12 21:39:20 +02:00
|
|
|
|
if path.exists() {
|
|
|
|
|
|
paths.push(path);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(paths)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Supprime tous les fichiers et entrées du cache
|
|
|
|
|
|
pub async fn purge(&self) -> Result<()> {
|
2025-10-17 14:36:39 +02:00
|
|
|
|
let mut entries = tokio::fs::read_dir(&self.dir).await?;
|
2025-10-12 21:39:20 +02:00
|
|
|
|
while let Some(entry) = entries.next_entry().await? {
|
2025-10-17 14:36:39 +02:00
|
|
|
|
if entry.path().is_file() && entry.path() != self.dir.join("cache.db") {
|
2025-10-12 21:39:20 +02:00
|
|
|
|
tokio::fs::remove_file(entry.path()).await?;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 14:36:39 +02:00
|
|
|
|
self.db
|
|
|
|
|
|
.purge()
|
|
|
|
|
|
.map_err(|e| anyhow!("Database error: {}", e))
|
2025-10-12 21:39:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Consolide le cache en supprimant les orphelins et en re-téléchargeant les fichiers manquants
|
2025-11-07 05:43:25 +00:00
|
|
|
|
///
|
|
|
|
|
|
/// Cette fonction :
|
|
|
|
|
|
/// - Supprime les entrées DB sans fichiers (ou re-télécharge si URL disponible)
|
|
|
|
|
|
/// - Supprime les fichiers sans marker de complétion et leurs entrées DB
|
|
|
|
|
|
/// - Supprime les fichiers sans entrées DB correspondantes
|
2025-10-12 21:39:20 +02:00
|
|
|
|
pub async fn consolidate(&self) -> Result<()> {
|
|
|
|
|
|
// Récupérer la liste des entrées à traiter
|
2025-10-26 09:18:48 +01:00
|
|
|
|
let entries = self.db.get_all(false)?;
|
2025-10-12 21:39:20 +02:00
|
|
|
|
|
2025-11-07 05:43:25 +00:00
|
|
|
|
// Supprimer les entrées sans fichiers correspondants OU sans marker de complétion
|
2025-10-12 21:39:20 +02:00
|
|
|
|
for entry in entries {
|
2025-10-26 13:05:46 +01:00
|
|
|
|
let file_path = self.get_file_path(&entry.pk);
|
2025-11-07 05:43:25 +00:00
|
|
|
|
let completion_marker = self.get_completion_marker_path(&entry.pk);
|
2025-10-26 09:18:48 +01:00
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
|
if !file_path.exists() {
|
2025-11-07 05:43:25 +00:00
|
|
|
|
// Fichier manquant, essayer de re-télécharger
|
2025-10-26 09:18:48 +01:00
|
|
|
|
match self.db.get_origin_url(&entry.pk)? {
|
|
|
|
|
|
Some(url) => {
|
|
|
|
|
|
if let Err(err) = self.add_from_url(&url, entry.collection.as_deref()).await
|
|
|
|
|
|
{
|
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
|
"Unable to redownload missing file for {}: {}",
|
|
|
|
|
|
entry.pk,
|
|
|
|
|
|
err
|
|
|
|
|
|
);
|
|
|
|
|
|
self.db.delete(&entry.pk)?;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
None => {
|
2025-10-12 21:39:20 +02:00
|
|
|
|
self.db.delete(&entry.pk)?;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-11-07 05:43:25 +00:00
|
|
|
|
} else if !completion_marker.exists() {
|
|
|
|
|
|
// Fichier existe mais pas de marker de complétion -> fichier incomplet
|
|
|
|
|
|
tracing::warn!(
|
|
|
|
|
|
"Removing incomplete file {} (no completion marker)",
|
|
|
|
|
|
entry.pk
|
|
|
|
|
|
);
|
|
|
|
|
|
let _ = tokio::fs::remove_file(&file_path).await;
|
|
|
|
|
|
self.db.delete(&entry.pk)?;
|
2025-10-12 21:39:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Supprimer les fichiers sans entrées DB correspondantes
|
2025-10-17 14:36:39 +02:00
|
|
|
|
let mut dir_entries = tokio::fs::read_dir(&self.dir).await?;
|
2025-10-12 21:39:20 +02:00
|
|
|
|
while let Some(entry) = dir_entries.next_entry().await? {
|
|
|
|
|
|
let path = entry.path();
|
2025-10-17 14:36:39 +02:00
|
|
|
|
if path.is_file() && path != self.dir.join("cache.db") {
|
2025-10-12 21:39:20 +02:00
|
|
|
|
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
|
2025-11-07 05:43:25 +00:00
|
|
|
|
// Ignorer les fichiers .complete
|
|
|
|
|
|
if file_name.ends_with(".complete") {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 14:36:39 +02:00
|
|
|
|
// Format attendu: {pk}.{qualifier}.{EXT}
|
|
|
|
|
|
// On extrait le pk (première partie avant le premier point)
|
|
|
|
|
|
if let Some(pk) = file_name.split('.').next() {
|
2025-10-26 09:18:48 +01:00
|
|
|
|
if self.db.get(pk, false).is_err() {
|
2025-11-07 05:43:25 +00:00
|
|
|
|
tracing::debug!("Removing orphan file: {}", file_name);
|
|
|
|
|
|
tokio::fs::remove_file(&path).await?;
|
|
|
|
|
|
// Supprimer aussi le marker de complétion s'il existe
|
|
|
|
|
|
let completion_marker = self.get_completion_marker_path(pk);
|
|
|
|
|
|
let _ = tokio::fs::remove_file(&completion_marker).await;
|
2025-10-17 14:36:39 +02:00
|
|
|
|
}
|
2025-10-12 21:39:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 19:48:53 +02:00
|
|
|
|
/// Récupère l'objet Download pour un pk donné (si en cours)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire du fichier
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Some(Download) si le téléchargement est en cours, None sinon
|
|
|
|
|
|
pub async fn get_download(&self, pk: &str) -> Option<Arc<Download>> {
|
|
|
|
|
|
let downloads = self.downloads.read().await;
|
|
|
|
|
|
downloads.get(pk).cloned()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-07 13:10:35 +00:00
|
|
|
|
/// Vérifie si le téléchargement/ingestion d'un fichier est complètement terminé
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Cette méthode vérifie l'existence du fichier marker de complétion (.complete)
|
|
|
|
|
|
/// qui est créé uniquement quand le fichier est complètement écrit et fermé.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Utile pour différencier:
|
|
|
|
|
|
/// - EOF temporaire : fichier encore en cours d'écriture (retourne false)
|
|
|
|
|
|
/// - EOF réel : fichier complètement écrit (retourne true)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire du fichier
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
///
|
|
|
|
|
|
/// `true` si le fichier est complètement écrit (marker existe), `false` sinon
|
|
|
|
|
|
pub fn is_download_complete(&self, pk: &str) -> bool {
|
|
|
|
|
|
let completion_marker = self.get_completion_marker_path(pk);
|
|
|
|
|
|
completion_marker.exists()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 19:48:53 +02:00
|
|
|
|
/// Retourne la taille actuelle téléchargée (source)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Si le download est en cours, retourne la taille téléchargée.
|
|
|
|
|
|
/// Sinon, retourne la taille du fichier sur disque.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire du fichier
|
|
|
|
|
|
pub async fn current_size(&self, pk: &str) -> Option<u64> {
|
|
|
|
|
|
if let Some(download) = self.get_download(pk).await {
|
|
|
|
|
|
Some(download.current_size().await)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Fichier terminé, lire la taille du fichier
|
2025-10-26 13:05:46 +01:00
|
|
|
|
let file_path = self.get_file_path(pk);
|
2025-10-17 19:48:53 +02:00
|
|
|
|
if file_path.exists() {
|
|
|
|
|
|
std::fs::metadata(file_path).ok().map(|m| m.len())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Retourne la taille des données transformées
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Si le download est en cours, retourne la taille transformée.
|
|
|
|
|
|
/// Sinon, retourne la taille du fichier sur disque.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire du fichier
|
|
|
|
|
|
pub async fn transformed_size(&self, pk: &str) -> Option<u64> {
|
|
|
|
|
|
if let Some(download) = self.get_download(pk).await {
|
|
|
|
|
|
Some(download.transformed_size().await)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Fichier terminé, lire la taille du fichier
|
2025-10-26 13:05:46 +01:00
|
|
|
|
let file_path = self.get_file_path(pk);
|
2025-10-17 19:48:53 +02:00
|
|
|
|
if file_path.exists() {
|
|
|
|
|
|
std::fs::metadata(file_path).ok().map(|m| m.len())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Retourne la taille attendue du fichier (si disponible)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire du fichier
|
|
|
|
|
|
pub async fn expected_size(&self, pk: &str) -> Option<u64> {
|
|
|
|
|
|
if let Some(download) = self.get_download(pk).await {
|
|
|
|
|
|
download.expected_size().await
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Fichier terminé, la taille finale est la taille du fichier
|
|
|
|
|
|
self.transformed_size(pk).await
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 00:10:51 +01:00
|
|
|
|
/// Retourne les métadonnées de transformation (si disponibles)
|
|
|
|
|
|
pub async fn transform_metadata(&self, pk: &str) -> Option<crate::download::TransformMetadata> {
|
|
|
|
|
|
if let Some(download) = self.get_download(pk).await {
|
|
|
|
|
|
download.transform_metadata().await
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 19:48:53 +02:00
|
|
|
|
/// Indique si le téléchargement est terminé
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire du fichier
|
|
|
|
|
|
pub async fn is_finished(&self, pk: &str) -> bool {
|
|
|
|
|
|
if let Some(download) = self.get_download(pk).await {
|
|
|
|
|
|
download.finished().await
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Pas dans la map = terminé (ou n'existe pas)
|
2025-10-26 13:05:46 +01:00
|
|
|
|
self.get_file_path(pk).exists()
|
2025-10-17 19:48:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Attend qu'un fichier atteigne au moins une taille minimale
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire du fichier
|
|
|
|
|
|
/// * `min_size` - Taille minimale attendue en bytes
|
|
|
|
|
|
pub async fn wait_until_min_size(&self, pk: &str, min_size: u64) -> Result<()> {
|
|
|
|
|
|
if let Some(download) = self.get_download(pk).await {
|
2025-10-19 13:42:29 +02:00
|
|
|
|
download
|
|
|
|
|
|
.wait_until_min_size(min_size)
|
|
|
|
|
|
.await
|
2025-10-17 19:48:53 +02:00
|
|
|
|
.map_err(|e| anyhow!("Download error: {}", e))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Déjà terminé ou n'existe pas
|
2025-10-26 13:05:46 +01:00
|
|
|
|
if self.get_file_path(pk).exists() {
|
2025-10-17 19:48:53 +02:00
|
|
|
|
Ok(())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Err(anyhow!("File not found"))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Attend que le téléchargement soit complètement terminé
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Arguments
|
|
|
|
|
|
///
|
|
|
|
|
|
/// * `pk` - Clé primaire du fichier
|
|
|
|
|
|
pub async fn wait_until_finished(&self, pk: &str) -> Result<()> {
|
|
|
|
|
|
if let Some(download) = self.get_download(pk).await {
|
2025-10-19 13:42:29 +02:00
|
|
|
|
download
|
|
|
|
|
|
.wait_until_finished()
|
|
|
|
|
|
.await
|
2025-10-17 19:48:53 +02:00
|
|
|
|
.map_err(|e| anyhow!("Download error: {}", e))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Déjà terminé ou n'existe pas
|
2025-10-26 13:05:46 +01:00
|
|
|
|
if self.get_file_path(pk).exists() {
|
2025-10-17 19:48:53 +02:00
|
|
|
|
Ok(())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Err(anyhow!("File not found"))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-12 21:39:20 +02:00
|
|
|
|
/// Retourne le répertoire du cache
|
2025-10-17 14:36:39 +02:00
|
|
|
|
pub fn cache_dir(&self) -> &Path {
|
|
|
|
|
|
&self.dir
|
2025-10-12 21:39:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 22:15:00 +02:00
|
|
|
|
/// Construit le chemin complet d'un fichier dans le cache avec le param par défaut
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Format: `{pk}.{default_param}.{extension}`
|
2025-10-26 13:05:46 +01:00
|
|
|
|
pub fn get_file_path(&self, pk: &str) -> PathBuf {
|
|
|
|
|
|
self.get_file_path_with_qualifier(pk, C::default_param())
|
2025-10-17 22:15:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Construit le chemin d'un fichier dans le cache avec un qualificatif
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Format: `{pk}.{qualifier}.{extension}`
|
2025-10-26 13:05:46 +01:00
|
|
|
|
pub fn get_file_path_with_qualifier(&self, pk: &str, qualifier: &str) -> PathBuf {
|
2025-10-19 13:42:29 +02:00
|
|
|
|
self.dir
|
|
|
|
|
|
.join(format!("{}.{}.{}", pk, qualifier, C::file_extension()))
|
2025-10-17 22:15:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-26 09:18:48 +01:00
|
|
|
|
/// Retourne tous les chemins de fichiers stockés pour une clé donnée,
|
|
|
|
|
|
/// quel que soit le qualifier.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Format: `{pk}.*.{extension}`
|
|
|
|
|
|
pub fn get_file_paths(&self, pk: &str) -> Result<Vec<PathBuf>> {
|
|
|
|
|
|
let mut paths = Vec::new();
|
|
|
|
|
|
let prefix = format!("{pk}.");
|
|
|
|
|
|
let expected_ext = C::file_extension();
|
|
|
|
|
|
|
|
|
|
|
|
for entry in std::fs::read_dir(&self.dir)? {
|
|
|
|
|
|
let entry = entry?;
|
|
|
|
|
|
let path = entry.path();
|
|
|
|
|
|
|
|
|
|
|
|
if !path.is_file() {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let file_name = match entry.file_name().into_string() {
|
|
|
|
|
|
Ok(name) => name,
|
|
|
|
|
|
Err(_) => continue, // nom de fichier non UTF-8 : on l’ignore
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if !file_name.starts_with(&prefix) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !file_name.ends_with(expected_ext) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
paths.push(path);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(paths)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 14:36:39 +02:00
|
|
|
|
/// 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())
|
2025-10-12 21:39:20 +02:00
|
|
|
|
}
|
2025-10-17 22:15:00 +02:00
|
|
|
|
|
|
|
|
|
|
/// Applique la politique d'éviction LRU (Least Recently Used)
|
|
|
|
|
|
///
|
2026-01-15 22:16:31 +01:00
|
|
|
|
/// Si le nombre d'entrées non épinglées dépasse la limite configurée, supprime
|
2025-10-17 22:15:00 +02:00
|
|
|
|
/// les entrées les plus anciennes (moins récemment utilisées).
|
|
|
|
|
|
///
|
2026-01-15 22:16:31 +01:00
|
|
|
|
/// Les items épinglés sont exclus du comptage et ne peuvent pas être supprimés.
|
|
|
|
|
|
///
|
2025-10-17 22:15:00 +02:00
|
|
|
|
/// Cette méthode :
|
2026-01-15 22:16:31 +01:00
|
|
|
|
/// 1. Compte le nombre d'entrées non épinglées
|
|
|
|
|
|
/// 2. Si > limit, récupère les N entrées les plus anciennes (non épinglées)
|
2025-10-17 22:15:00 +02:00
|
|
|
|
/// 3. Supprime ces entrées de la DB et leurs fichiers du disque
|
2026-01-15 22:16:31 +01:00
|
|
|
|
/// 4. Supprime également les items expirés (TTL dépassé)
|
2025-10-17 22:15:00 +02:00
|
|
|
|
///
|
|
|
|
|
|
/// # Returns
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Le nombre d'entrées supprimées
|
|
|
|
|
|
pub async fn enforce_limit(&self) -> Result<usize> {
|
2026-01-15 22:16:31 +01:00
|
|
|
|
let mut total_removed = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 1. Supprimer d'abord les items expirés (TTL dépassé)
|
|
|
|
|
|
let expired_entries = self.db.get_expired()?;
|
|
|
|
|
|
for entry in expired_entries {
|
|
|
|
|
|
if let Ok(paths) = self.get_file_paths(&entry.pk) {
|
|
|
|
|
|
for path in paths {
|
|
|
|
|
|
let _ = tokio::fs::remove_file(path).await;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Err(e) = self.db.delete(&entry.pk) {
|
|
|
|
|
|
tracing::warn!("Error deleting expired entry {} from DB: {}", entry.pk, e);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
total_removed += 1;
|
|
|
|
|
|
tracing::debug!(
|
|
|
|
|
|
"Removed expired item {} (TTL: {:?})",
|
|
|
|
|
|
entry.pk,
|
|
|
|
|
|
entry.ttl_expires_at
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. Compter seulement les items non épinglés
|
|
|
|
|
|
let count = self.db.count_unpinned()?;
|
2025-10-17 22:15:00 +02:00
|
|
|
|
|
|
|
|
|
|
if count <= self.limit {
|
2026-01-15 22:16:31 +01:00
|
|
|
|
if total_removed > 0 {
|
|
|
|
|
|
tracing::info!("Cache cleanup: removed {} expired entries", total_removed);
|
|
|
|
|
|
}
|
|
|
|
|
|
return Ok(total_removed);
|
2025-10-17 22:15:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 22:16:31 +01:00
|
|
|
|
// 3. Supprimer les plus vieux items non épinglés si nécessaire
|
2025-10-17 22:15:00 +02:00
|
|
|
|
let to_remove = count - self.limit;
|
|
|
|
|
|
let old_entries = self.db.get_oldest(to_remove)?;
|
|
|
|
|
|
|
2026-01-15 22:16:31 +01:00
|
|
|
|
let mut lru_removed = 0;
|
2025-10-17 22:15:00 +02:00
|
|
|
|
for entry in old_entries {
|
Revue de code complète et amélioration des trois crates de cache
## Corrections de bugs
- **CRITIQUE**: Correction du bug SQL dans `pmocache/src/db.rs:get_oldest()`
- La requête référençait des colonnes inexistantes (`source_url`, `metadata_json`)
- Corrigé pour utiliser les bonnes colonnes de la table `asset` (`id`)
## Refactoring et simplifications
- **Factorisation majeure** dans `pmocache/src/cache.rs`:
- Extraction de 3 méthodes helpers pour éliminer ~90 lignes de code dupliqué
entre `add_from_url()` et `add_from_reader()`:
- `check_cached_and_complete()`: vérification cache et intégrité
- `check_ongoing_download()`: gestion des téléchargements en cours
- `finalize_download()`: finalisation avec prébuffering et nettoyage
- Les deux méthodes sont maintenant beaucoup plus lisibles et maintenables
- **Simplification** de `enforce_limit()`:
- Utilisation de `get_file_paths()` au lieu d'itérations manuelles complexes
- Suppression des boucles imbriquées pour une logique plus claire
- **Correction** d'import manquant: ajout de `AsyncReadExt` dans `cache.rs`
## Tests complets ajoutés
### pmocache (27 tests)
- `tests/test_db.rs`: 24 tests couvrant toutes les opérations DB
- CRUD de base (add, get, delete, purge)
- Gestion des métadonnées (tous types JSON)
- Collections (get_by_collection, delete_collection)
- LRU et éviction (get_oldest, count)
- URLs d'origine (set_origin_url, get_origin_url)
- Indexation par (collection, id)
- `tests/test_cache.rs`: 16 tests d'intégration du cache
- Ajout depuis fichier, reader, URL
- Déduplication basée sur contenu
- Collections et gestion
- Éviction LRU automatique
- Purge et consolidation
- Métadonnées et touch
- Prébuffering et téléchargements
### pmoaudiocache (4 tests)
- `tests/test_cache.rs`: Tests spécifiques audio
- Création et configuration
- Collections d'albums
- Éviction LRU avec limite
### pmocovers (6 tests)
- `tests/test_cache.rs`: Tests de cache d'images
- Conversion WebP automatique
- Déduplication d'images identiques
- Gestion de collections
- Éviction LRU
- `tests/test_webp.rs`: Tests du module WebP
- Encodage WebP depuis différents formats
- Redimensionnement carré avec préservation du ratio
- Génération et mise en cache de variantes
- Tests avec différentes tailles (portrait, landscape, carré)
## Améliorations de la couverture
- Passage de **0 test** à **37 tests** au total
- Ajout de `tempfile = "3"` comme dev-dependency dans `pmocache/Cargo.toml`
- Couverture des cas nominaux et des cas limites
- Tests d'intégration et unitaires
## Préservation des APIs
- ✅ Aucune API publique n'a été modifiée ou cassée
- ✅ Toutes les fonctions helpers sont privées (non exposées)
- ✅ Les signatures publiques restent identiques
- ✅ Rétrocompatibilité totale garantie
2025-11-06 08:43:11 +00:00
|
|
|
|
// Utiliser get_file_paths() pour obtenir tous les fichiers de cette entrée
|
|
|
|
|
|
if let Ok(paths) = self.get_file_paths(&entry.pk) {
|
|
|
|
|
|
for path in paths {
|
|
|
|
|
|
let _ = tokio::fs::remove_file(path).await;
|
2025-10-17 22:15:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Supprimer de la base de données
|
|
|
|
|
|
if let Err(e) = self.db.delete(&entry.pk) {
|
|
|
|
|
|
tracing::warn!("Error deleting entry {} from DB: {}", entry.pk, e);
|
|
|
|
|
|
} else {
|
2026-01-15 22:16:31 +01:00
|
|
|
|
lru_removed += 1;
|
2025-10-17 22:15:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 22:16:31 +01:00
|
|
|
|
total_removed += lru_removed;
|
|
|
|
|
|
|
|
|
|
|
|
if total_removed > 0 {
|
2025-10-19 13:42:29 +02:00
|
|
|
|
tracing::info!(
|
2026-01-15 22:16:31 +01:00
|
|
|
|
"LRU eviction: removed {} old entries (unpinned cache size: {} -> {})",
|
|
|
|
|
|
lru_removed,
|
2025-10-19 13:42:29 +02:00
|
|
|
|
count,
|
2026-01-15 22:16:31 +01:00
|
|
|
|
count - lru_removed
|
2025-10-19 13:42:29 +02:00
|
|
|
|
);
|
2025-10-17 22:15:00 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-15 22:16:31 +01:00
|
|
|
|
Ok(total_removed)
|
2025-10-17 22:15:00 +02:00
|
|
|
|
}
|
2025-12-12 16:36:31 +00:00
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// LAZY PK SUPPORT - Methods
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/// S'abonne aux events du cache (lazy downloads, etc.)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Retourne un receiver pour écouter les events. Chaque abonné reçoit
|
|
|
|
|
|
/// une copie indépendante des events.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # Example
|
|
|
|
|
|
///
|
|
|
|
|
|
/// ```rust,no_run
|
2025-12-17 10:10:56 +01:00
|
|
|
|
/// use pmocache::{Cache, CacheConfig, CacheEvent};
|
|
|
|
|
|
///
|
|
|
|
|
|
/// struct AudioConfig;
|
|
|
|
|
|
/// impl CacheConfig for AudioConfig {
|
|
|
|
|
|
/// fn file_extension() -> &'static str { "flac" }
|
|
|
|
|
|
/// }
|
|
|
|
|
|
///
|
|
|
|
|
|
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
2025-12-12 16:36:31 +00:00
|
|
|
|
/// let cache = Cache::<AudioConfig>::new("./cache", 1000)?;
|
|
|
|
|
|
/// let mut rx = cache.subscribe_events();
|
|
|
|
|
|
///
|
|
|
|
|
|
/// tokio::spawn(async move {
|
|
|
|
|
|
/// while let Ok(event) = rx.recv().await {
|
|
|
|
|
|
/// match event {
|
|
|
|
|
|
/// CacheEvent::LazyDownloaded { lazy_pk, real_pk } => {
|
|
|
|
|
|
/// println!("Lazy {} → Real {}", lazy_pk, real_pk);
|
|
|
|
|
|
/// }
|
|
|
|
|
|
/// _ => {}
|
|
|
|
|
|
/// }
|
|
|
|
|
|
/// }
|
|
|
|
|
|
/// });
|
2025-12-17 10:10:56 +01:00
|
|
|
|
/// # Ok(())
|
|
|
|
|
|
/// # }
|
2025-12-12 16:36:31 +00:00
|
|
|
|
/// ```
|
|
|
|
|
|
pub fn subscribe_events(&self) -> broadcast::Receiver<CacheEvent> {
|
|
|
|
|
|
self.served_tx
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.expect("Cache event channel not initialized")
|
|
|
|
|
|
.subscribe()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Broadcast un event quand un lazy PK est téléchargé
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Cette méthode est appelée après qu'un fichier lazy a été téléchargé
|
|
|
|
|
|
/// et son real pk calculé. Elle permet aux playlists de commuter leurs PK.
|
|
|
|
|
|
pub async fn broadcast_lazy_downloaded(&self, lazy_pk: &str, real_pk: &str) {
|
|
|
|
|
|
if let Some(tx) = &self.served_tx {
|
|
|
|
|
|
let event = CacheEvent::LazyDownloaded {
|
|
|
|
|
|
lazy_pk: lazy_pk.to_string(),
|
|
|
|
|
|
real_pk: real_pk.to_string(),
|
|
|
|
|
|
};
|
|
|
|
|
|
// Ignorer l'erreur si pas d'abonnés
|
|
|
|
|
|
let _ = tx.send(event);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 15:05:35 +01:00
|
|
|
|
/// Garantit l'existence d'une entrée lazy spécifique.
|
|
|
|
|
|
pub async fn ensure_lazy_entry(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
lazy_pk: &str,
|
|
|
|
|
|
collection: Option<&str>,
|
|
|
|
|
|
origin_url: Option<&str>,
|
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
|
if let Ok(true) = self.db.has_lazy_entry(lazy_pk) {
|
|
|
|
|
|
self.db.update_hit_by_lazy_pk(lazy_pk)?;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
self.db.add_lazy(lazy_pk, None, collection)?;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(url) = origin_url {
|
|
|
|
|
|
self.db.set_origin_url_for_lazy(lazy_pk, url)?;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Récupère auprès du provider les métadonnées/couvertures associées.
|
|
|
|
|
|
pub async fn fetch_lazy_provider_data(&self, lazy_pk: &str) -> Result<LazyEntryRemoteData> {
|
|
|
|
|
|
if let Some(provider) = self.provider_for_lazy_pk(lazy_pk) {
|
|
|
|
|
|
let metadata = provider.metadata(lazy_pk).await?;
|
|
|
|
|
|
let cover_url = provider.cover_url(lazy_pk).await?;
|
2025-12-17 10:10:56 +01:00
|
|
|
|
Ok(LazyEntryRemoteData {
|
|
|
|
|
|
metadata,
|
|
|
|
|
|
cover_url,
|
|
|
|
|
|
})
|
2025-12-15 15:05:35 +01:00
|
|
|
|
} else {
|
|
|
|
|
|
Ok(LazyEntryRemoteData::default())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Résout l'URL d'origine pour un lazy PK, via la DB ou un provider.
|
|
|
|
|
|
pub async fn resolve_lazy_url(&self, lazy_pk: &str) -> Result<String> {
|
|
|
|
|
|
if let Ok(Some(url)) = self.db.get_origin_url(lazy_pk) {
|
|
|
|
|
|
return Ok(url);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(provider) = self.provider_for_lazy_pk(lazy_pk) {
|
|
|
|
|
|
return provider.get_url(lazy_pk).await;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bail!("No origin URL or lazy provider registered for {}", lazy_pk);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Télécharge un lazy PK en résolvant automatiquement son URL.
|
|
|
|
|
|
pub async fn download_lazy(&self, lazy_pk: &str, collection: Option<&str>) -> Result<String> {
|
|
|
|
|
|
let (existing_pk, _lazy_sec, existing_collection) = self
|
|
|
|
|
|
.db
|
|
|
|
|
|
.get_entry_by_pk_or_lazy_pk(lazy_pk)?
|
|
|
|
|
|
.ok_or_else(|| anyhow!("Lazy pk {} not found in DB", lazy_pk))?;
|
|
|
|
|
|
|
|
|
|
|
|
let url = self.resolve_lazy_url(lazy_pk).await?;
|
|
|
|
|
|
let collection = collection.or(existing_collection.as_deref());
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(real_pk) = existing_pk {
|
|
|
|
|
|
if real_pk != lazy_pk && self.check_cached_and_complete(&real_pk).await? {
|
|
|
|
|
|
return Ok(real_pk);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.download_lazy_from_url(lazy_pk, &url, collection).await
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Ajoute une URL sans lancer immédiatement le téléchargement.
|
2025-12-12 16:36:31 +00:00
|
|
|
|
pub async fn add_from_url_deferred(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
url: &str,
|
|
|
|
|
|
collection: Option<&str>,
|
|
|
|
|
|
) -> Result<String> {
|
|
|
|
|
|
if let Ok(Some((pk_opt, lazy_pk_opt))) = self.db.get_entry_by_url(url) {
|
|
|
|
|
|
if let Some(pk) = pk_opt {
|
|
|
|
|
|
self.db.update_hit(&pk)?;
|
|
|
|
|
|
if let Some(lpk) = lazy_pk_opt {
|
|
|
|
|
|
return Ok(lpk);
|
|
|
|
|
|
}
|
|
|
|
|
|
return Ok(pk);
|
|
|
|
|
|
} else if let Some(lpk) = lazy_pk_opt {
|
|
|
|
|
|
self.db.update_hit_by_lazy_pk(&lpk)?;
|
|
|
|
|
|
return Ok(lpk);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-15 15:05:35 +01:00
|
|
|
|
let lazy_pk = format!("L:{}", generate_lazy_pk(url));
|
2025-12-15 11:18:58 +01:00
|
|
|
|
if let Ok(true) = self.db.has_lazy_entry(&lazy_pk) {
|
2025-12-12 16:36:31 +00:00
|
|
|
|
bail!("Lazy PK collision for URL: {}", url);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-17 10:10:56 +01:00
|
|
|
|
self.ensure_lazy_entry(&lazy_pk, collection, Some(url))
|
|
|
|
|
|
.await?;
|
2025-12-12 16:36:31 +00:00
|
|
|
|
tracing::debug!("Created new lazy pk {} for URL {}", lazy_pk, url);
|
|
|
|
|
|
Ok(lazy_pk)
|
|
|
|
|
|
}
|
2025-10-12 21:39:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-17 10:10:56 +01:00
|
|
|
|
fn link_file(source: &Path, destination: &Path) -> std::io::Result<()> {
|
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
|
{
|
|
|
|
|
|
use std::os::unix::fs::symlink;
|
|
|
|
|
|
symlink(source, destination)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(windows)]
|
|
|
|
|
|
{
|
|
|
|
|
|
use std::os::windows::fs::symlink_file;
|
|
|
|
|
|
symlink_file(source, destination)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(any(unix, windows)))]
|
|
|
|
|
|
{
|
|
|
|
|
|
std::fs::hard_link(source, destination)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 14:36:39 +02:00
|
|
|
|
/// Implémentation du trait FileCache pour Cache
|
2025-12-17 15:15:10 +01:00
|
|
|
|
impl<C: CacheConfig + 'static> FileCache<C> for Cache<C> {
|
2025-10-17 19:48:53 +02:00
|
|
|
|
fn get_cache_dir(&self) -> &Path {
|
|
|
|
|
|
self.cache_dir()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn get_database(&self) -> Arc<DB> {
|
|
|
|
|
|
self.db.clone()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 14:36:39 +02:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 19:48:53 +02:00
|
|
|
|
async fn add_from_file(&self, path: &str, collection: Option<&str>) -> Result<String> {
|
|
|
|
|
|
self.add_from_file(path, collection).await
|
2025-10-17 14:36:39 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2025-10-12 21:39:20 +02:00
|
|
|
|
}
|