Ajout de fonctionnalité de download asynchrone au pmocache
This commit is contained in:
345
pmocache/DOWNLOAD_MODULE.md
Normal file
345
pmocache/DOWNLOAD_MODULE.md
Normal file
@@ -0,0 +1,345 @@
|
||||
# Module Download
|
||||
|
||||
Module de téléchargement asynchrone avec support de transformation de stream.
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Le module `download` permet de télécharger des fichiers depuis une URL en tâche de fond avec :
|
||||
- Suivi de la progression en temps réel
|
||||
- Support de transformations de stream (conversion, compression, etc.)
|
||||
- API non-bloquante avec attentes conditionnelles
|
||||
- Gestion d'erreurs robuste
|
||||
|
||||
## API
|
||||
|
||||
### Types principaux
|
||||
|
||||
#### `Download`
|
||||
Objet représentant un téléchargement en cours, partagé via `Arc<Download>`.
|
||||
|
||||
**Méthodes:**
|
||||
- `filename() -> &Path` - Retourne le chemin du fichier de destination
|
||||
- `wait_until_min_size(size: u64) -> Result<(), String>` - Attend que le fichier atteigne une taille minimale
|
||||
- `wait_until_finished() -> Result<(), String>` - Attend la fin complète du téléchargement
|
||||
- `open() -> io::Result<File>` - Ouvre le fichier pour lecture
|
||||
- `pos() -> u64` - Position de lecture actuelle
|
||||
- `set_pos(pos: u64)` - Définit la position de lecture
|
||||
- `expected_size() -> Option<u64>` - Taille attendue du fichier source (via Content-Length)
|
||||
- `current_size() -> u64` - Taille actuellement téléchargée (source)
|
||||
- `transformed_size() -> u64` - Taille des données transformées écrites
|
||||
- `finished() -> bool` - Indique si le téléchargement est terminé
|
||||
- `error() -> Option<String>` - Retourne l'erreur éventuelle
|
||||
|
||||
#### `StreamTransformer`
|
||||
Type pour une fonction de transformation de stream.
|
||||
|
||||
```rust
|
||||
pub type StreamTransformer = Box<
|
||||
dyn FnOnce(
|
||||
reqwest::Response,
|
||||
tokio::fs::File,
|
||||
Arc<dyn Fn(u64) + Send + Sync>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send>>
|
||||
+ Send,
|
||||
>;
|
||||
```
|
||||
|
||||
**Paramètres:**
|
||||
1. `reqwest::Response` - La réponse HTTP avec le stream de données
|
||||
2. `tokio::fs::File` - Le fichier de destination ouvert en écriture
|
||||
3. `Arc<dyn Fn(u64) + Send + Sync>` - Callback pour mettre à jour la progression (taille transformée)
|
||||
|
||||
**Retour:**
|
||||
- `Future<Output = Result<(), String>>` - Future qui se résout quand la transformation est terminée
|
||||
|
||||
### Fonctions
|
||||
|
||||
#### `download(filename, url) -> Arc<Download>`
|
||||
Télécharge un fichier sans transformation.
|
||||
|
||||
```rust
|
||||
use pmocache::download::download;
|
||||
|
||||
let dl = download("/tmp/file.dat", "https://example.com/file.dat");
|
||||
dl.wait_until_finished().await?;
|
||||
```
|
||||
|
||||
#### `download_with_transformer(filename, url, transformer) -> Arc<Download>`
|
||||
Télécharge un fichier avec une transformation optionnelle du stream.
|
||||
|
||||
```rust
|
||||
use pmocache::download::{download_with_transformer, StreamTransformer};
|
||||
|
||||
let transformer: StreamTransformer = Box::new(|response, mut file, update_progress| {
|
||||
Box::pin(async move {
|
||||
// Votre logique de transformation ici
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
let dl = download_with_transformer("/tmp/output.dat", "https://example.com/input.dat", Some(transformer));
|
||||
```
|
||||
|
||||
## Exemples d'utilisation
|
||||
|
||||
### 1. Téléchargement simple
|
||||
|
||||
```rust
|
||||
use pmocache::download::download;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let dl = download("/tmp/rust.html", "https://www.rust-lang.org/");
|
||||
|
||||
println!("Téléchargement démarré...");
|
||||
|
||||
// Attendre au moins 1KB
|
||||
dl.wait_until_min_size(1024).await?;
|
||||
println!("Au moins 1KB téléchargés");
|
||||
|
||||
// Attendre la fin
|
||||
dl.wait_until_finished().await?;
|
||||
println!("Terminé! Taille: {} bytes", dl.current_size().await);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Transformation en majuscules
|
||||
|
||||
```rust
|
||||
use pmocache::download::{download_with_transformer, StreamTransformer};
|
||||
use futures_util::StreamExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
fn uppercase_transformer() -> StreamTransformer {
|
||||
Box::new(|response, mut file, update_progress| {
|
||||
Box::pin(async move {
|
||||
let mut stream = response.bytes_stream();
|
||||
let mut total = 0u64;
|
||||
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
let chunk = chunk_result.map_err(|e| e.to_string())?;
|
||||
|
||||
// Transformer en majuscules
|
||||
let uppercase: Vec<u8> = chunk
|
||||
.iter()
|
||||
.map(|&b| b.to_ascii_uppercase())
|
||||
.collect();
|
||||
|
||||
file.write_all(&uppercase).await.map_err(|e| e.to_string())?;
|
||||
|
||||
total += uppercase.len() as u64;
|
||||
update_progress(total);
|
||||
}
|
||||
|
||||
file.flush().await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let transformer = uppercase_transformer();
|
||||
let dl = download_with_transformer("/tmp/UPPERCASE.txt", "https://example.com/text.txt", Some(transformer));
|
||||
|
||||
dl.wait_until_finished().await.unwrap();
|
||||
println!("Fichier converti en majuscules!");
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Compression GZIP à la volée
|
||||
|
||||
```rust
|
||||
use pmocache::download::{download_with_transformer, StreamTransformer};
|
||||
use futures_util::StreamExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use async_compression::tokio::write::GzipEncoder;
|
||||
|
||||
fn gzip_transformer() -> StreamTransformer {
|
||||
Box::new(|response, file, update_progress| {
|
||||
Box::pin(async move {
|
||||
let mut encoder = GzipEncoder::new(file);
|
||||
let mut stream = response.bytes_stream();
|
||||
let mut total = 0u64;
|
||||
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
let chunk = chunk_result.map_err(|e| e.to_string())?;
|
||||
encoder.write_all(&chunk).await.map_err(|e| e.to_string())?;
|
||||
|
||||
total += chunk.len() as u64;
|
||||
update_progress(total);
|
||||
}
|
||||
|
||||
encoder.shutdown().await.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Conversion d'image (concept)
|
||||
|
||||
```rust
|
||||
// Exemple conceptuel de conversion WebP
|
||||
// (nécessiterait une bibliothèque de traitement d'images)
|
||||
|
||||
fn webp_transformer() -> StreamTransformer {
|
||||
Box::new(|response, mut file, update_progress| {
|
||||
Box::pin(async move {
|
||||
// 1. Télécharger l'image en mémoire
|
||||
let bytes = response.bytes().await.map_err(|e| e.to_string())?;
|
||||
|
||||
// 2. Décoder l'image source
|
||||
let img = image::load_from_memory(&bytes)
|
||||
.map_err(|e| format!("Failed to decode image: {}", e))?;
|
||||
|
||||
// 3. Encoder en WebP
|
||||
let mut webp_data = Vec::new();
|
||||
let encoder = webp::Encoder::from_image(&img)
|
||||
.map_err(|e| format!("Failed to create WebP encoder: {}", e))?;
|
||||
let webp = encoder.encode(75.0); // Qualité 75%
|
||||
webp_data.extend_from_slice(&*webp);
|
||||
|
||||
// 4. Écrire le résultat
|
||||
file.write_all(&webp_data).await.map_err(|e| e.to_string())?;
|
||||
file.flush().await.map_err(|e| e.to_string())?;
|
||||
|
||||
update_progress(webp_data.len() as u64);
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Utilisation
|
||||
let transformer = webp_transformer();
|
||||
let dl = download_with_transformer(
|
||||
"/tmp/image.webp",
|
||||
"https://example.com/image.jpg",
|
||||
Some(transformer)
|
||||
);
|
||||
```
|
||||
|
||||
### 5. Conversion audio (concept)
|
||||
|
||||
```rust
|
||||
// Exemple conceptuel de conversion MP3 -> FLAC
|
||||
// (nécessiterait des bibliothèques audio comme symphonia)
|
||||
|
||||
fn mp3_to_flac_transformer() -> StreamTransformer {
|
||||
Box::new(|response, mut file, update_progress| {
|
||||
Box::pin(async move {
|
||||
// 1. Télécharger le MP3 en mémoire
|
||||
let mp3_bytes = response.bytes().await.map_err(|e| e.to_string())?;
|
||||
|
||||
// 2. Décoder le MP3
|
||||
let cursor = std::io::Cursor::new(mp3_bytes);
|
||||
let mp3_decoder = minimp3::Decoder::new(cursor);
|
||||
|
||||
let mut samples = Vec::new();
|
||||
let mut sample_rate = 0;
|
||||
let mut channels = 0;
|
||||
|
||||
for frame in mp3_decoder {
|
||||
let frame = frame.map_err(|e| format!("MP3 decode error: {:?}", e))?;
|
||||
if sample_rate == 0 {
|
||||
sample_rate = frame.sample_rate;
|
||||
channels = frame.channels;
|
||||
}
|
||||
samples.extend_from_slice(&frame.data);
|
||||
}
|
||||
|
||||
// 3. Encoder en FLAC
|
||||
let mut flac_encoder = claxon::FlacEncoder::new(
|
||||
&mut file,
|
||||
sample_rate,
|
||||
channels as u32,
|
||||
16, // bits per sample
|
||||
).map_err(|e| format!("FLAC encoder error: {:?}", e))?;
|
||||
|
||||
for sample in samples {
|
||||
flac_encoder.write_sample(sample as i32)
|
||||
.map_err(|e| format!("FLAC write error: {:?}", e))?;
|
||||
}
|
||||
|
||||
flac_encoder.finish()
|
||||
.map_err(|e| format!("FLAC finalize error: {:?}", e))?;
|
||||
|
||||
file.flush().await.map_err(|e| e.to_string())?;
|
||||
|
||||
// Note: on ne peut pas facilement connaître la taille finale avant d'avoir tout encodé
|
||||
// Pour un suivi précis, il faudrait encoder par chunks
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Cas d'usage dans PMOMusic
|
||||
|
||||
### 1. Cache audio avec conversion
|
||||
```rust
|
||||
// Télécharger du MP3 et le convertir en FLAC pour le cache
|
||||
let transformer = mp3_to_flac_transformer();
|
||||
let dl = download_with_transformer(
|
||||
cache_path,
|
||||
audio_url,
|
||||
Some(transformer)
|
||||
);
|
||||
```
|
||||
|
||||
### 2. Cache d'images avec WebP
|
||||
```rust
|
||||
// Télécharger une image et la convertir en WebP
|
||||
let transformer = webp_transformer();
|
||||
let dl = download_with_transformer(
|
||||
cover_cache_path,
|
||||
cover_url,
|
||||
Some(transformer)
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Streaming progressif
|
||||
```rust
|
||||
// Commencer à lire le fichier dès qu'on a assez de données
|
||||
let dl = download(audio_path, stream_url);
|
||||
|
||||
// Attendre au moins 256KB pour commencer la lecture
|
||||
dl.wait_until_min_size(256 * 1024).await?;
|
||||
|
||||
// Ouvrir le fichier et commencer à lire pendant que le téléchargement continue
|
||||
let file = dl.open()?;
|
||||
// ... lecture du fichier
|
||||
```
|
||||
|
||||
## Notes d'implémentation
|
||||
|
||||
### Thread safety
|
||||
- Tous les objets sont thread-safe via `Arc` et `RwLock`
|
||||
- Le téléchargement s'exécute dans un `tokio::spawn` séparé
|
||||
- Les callbacks de progression utilisent `Arc<dyn Fn>` pour être partagés
|
||||
|
||||
### Gestion des erreurs
|
||||
- Les erreurs sont capturées et stockées dans l'état
|
||||
- `wait_until_*` retourne l'erreur si elle existe
|
||||
- Le téléchargement est marqué comme terminé même en cas d'erreur
|
||||
|
||||
### Performance
|
||||
- Téléchargement par chunks (stream)
|
||||
- Transformation à la volée sans buffer intermédiaire complet (selon le transformer)
|
||||
- Mise à jour de la progression asynchrone via spawn
|
||||
|
||||
## Dépendances
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12", features = ["stream"] }
|
||||
futures-util = "0.3"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
|
||||
# Optionnel selon les transformers utilisés
|
||||
async-compression = "0.4" # Pour GZIP
|
||||
image = "0.24" # Pour images
|
||||
webp = "0.2" # Pour WebP
|
||||
```
|
||||
Reference in New Issue
Block a user