346 lines
10 KiB
Markdown
346 lines
10 KiB
Markdown
|
|
# 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
|
||
|
|
```
|