veza/veza-backend-api/docs/UPLOAD_ASYNC.md

364 lines
8.4 KiB
Markdown
Raw Normal View History

2025-12-16 16:23:49 +00:00
# Upload I/O Asynchrone — Documentation
**Date**: 2025-01-27
**Status**: ✅ **IMPLEMENTED** - MOD-P2-008
---
## Vue d'ensemble
L'upload de fichiers audio utilise maintenant une **sémantique asynchrone** avec réponse `202 Accepted`. La copie fichier (`io.Copy`) se fait en arrière-plan dans une goroutine suivie, permettant au handler HTTP de répondre immédiatement.
---
## Sémantique HTTP
### Endpoint: `POST /api/v1/tracks`
**Réponse**: `202 Accepted`
**Headers**:
```
Location: /api/v1/tracks/{track_id}/status
```
**Body**:
```json
{
"success": true,
"data": {
"track_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "uploading",
"status_url": "/api/v1/tracks/550e8400-e29b-41d4-a716-446655440000/status",
"message": "Upload initiated, file is being saved in background"
}
}
```
---
## Flux d'Exécution
### 1. Handler (`UploadTrack`)
1. Validation du fichier (ClamAV, format, quota)
2. Création du Track en DB avec `Status=Uploading` **immédiatement**
3. Lancement de la copie fichier en **goroutine** (`copyFileAsync`)
4. Réponse **202 Accepted** avec `track_id`
### 2. Goroutine Asynchrone (`copyFileAsync`)
1. Création d'un contexte avec timeout (5 minutes)
2. Ouverture du fichier source (`fileHeader.Open()`)
3. Création du fichier destination (`os.Create`)
4. Copie avec `io.Copy`
5. Mise à jour du Status:
- `Processing` si succès
- `Failed` si erreur
6. Nettoyage automatique en cas d'échec (`os.Remove`)
---
## Suivi de Progression
### Endpoint: `GET /api/v1/tracks/{id}/status`
**Réponse**: `200 OK`
**Body**:
```json
{
"success": true,
"data": {
"track_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "processing",
"progress": 100,
"message": "File uploaded, processing..."
}
}
```
**Status possibles**:
- `uploading`: Fichier en cours de copie
- `processing`: Fichier copié, traitement en cours
- `completed`: Track prêt
- `failed`: Échec (upload ou traitement)
---
## Gestion d'Erreurs
### Erreurs Synchrones (avant goroutine)
- **Validation échouée**: `400 Bad Request`
- **Quota dépassé**: `403 Forbidden`
- **ClamAV unavailable**: `503 Service Unavailable`
- **Virus détecté**: `422 Unprocessable Entity`
### Erreurs Asynchrones (dans goroutine)
- **Erreur de copie**: Status → `Failed`, fichier nettoyé
- **Timeout (5 min)**: Status → `Failed`, fichier nettoyé
- **Contexte annulé**: Status → `Failed`, fichier nettoyé
**Nettoyage automatique**: Le fichier est supprimé (`os.Remove`) en cas d'échec.
---
## Traçabilité
### Logs
Tous les logs incluent:
- `track_id`: UUID du track
- `user_id`: UUID de l'utilisateur
- `request_id`: ID de requête (si disponible via context)
**Exemples**:
```
INFO Track upload initiated (async) track_id=... user_id=... filename=...
INFO Track status updated track_id=... status=processing message=...
INFO Track file copied successfully (async) track_id=... bytes_written=...
```
### Request ID
Le `request_id` est propagé via le contexte:
```go
ctx := c.Request.Context() // Contient request_id du middleware
track, err := service.UploadTrack(ctx, userID, fileHeader)
```
---
## Exemples cURL
### 1. Upload d'un fichier
```bash
curl -X POST http://localhost:8080/api/v1/tracks \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@audio.mp3" \
-v
```
**Réponse**:
```
HTTP/1.1 202 Accepted
Location: /api/v1/tracks/550e8400-e29b-41d4-a716-446655440000/status
{
"success": true,
"data": {
"track_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "uploading",
"status_url": "/api/v1/tracks/550e8400-e29b-41d4-a716-446655440000/status",
"message": "Upload initiated, file is being saved in background"
}
}
```
### 2. Vérifier le statut
```bash
curl -X GET http://localhost:8080/api/v1/tracks/550e8400-e29b-41d4-a716-446655440000/status \
-H "Authorization: Bearer YOUR_TOKEN"
```
**Réponse** (pendant upload):
```json
{
"success": true,
"data": {
"track_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "uploading",
"progress": 0,
"message": "Upload started"
}
}
```
**Réponse** (après copie):
```json
{
"success": true,
"data": {
"track_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "processing",
"progress": 100,
"message": "File uploaded, processing..."
}
}
```
**Réponse** (échec):
```json
{
"success": true,
"data": {
"track_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"progress": 0,
"message": "Failed to save file: ..."
}
}
```
---
## Polling Recommandé
### Stratégie Simple
```javascript
async function uploadAndWait(trackId) {
const maxAttempts = 60; // 5 minutes max (5s * 60)
const interval = 5000; // 5 secondes
for (let i = 0; i < maxAttempts; i++) {
const response = await fetch(`/api/v1/tracks/${trackId}/status`);
const data = await response.json();
if (data.data.status === 'completed') {
return data.data;
}
if (data.data.status === 'failed') {
throw new Error(data.data.message);
}
await sleep(interval);
}
throw new Error('Upload timeout');
}
```
### Stratégie avec Exponential Backoff
```javascript
async function uploadAndWaitWithBackoff(trackId) {
let interval = 1000; // 1 seconde initial
const maxInterval = 30000; // 30 secondes max
const maxAttempts = 120; // ~10 minutes max
for (let i = 0; i < maxAttempts; i++) {
const response = await fetch(`/api/v1/tracks/${trackId}/status`);
const data = await response.json();
if (data.data.status === 'completed') {
return data.data;
}
if (data.data.status === 'failed') {
throw new Error(data.data.message);
}
await sleep(interval);
interval = Math.min(interval * 1.5, maxInterval); // Exponential backoff
}
throw new Error('Upload timeout');
}
```
---
## Configuration
### Timeout de Copie
**Valeur par défaut**: 5 minutes
**Modification**: `internal/core/track/service.go`
```go
copyCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
```
### Répertoire d'Upload
**Variable d'environnement**: `UPLOAD_DIR` (optionnel)
**Valeur par défaut**: `uploads/tracks`
---
## Tests
### Tests Unitaires
```bash
go test ./internal/core/track -v -run TestUploadTrack_Async
```
**Tests inclus**:
- `TestUploadTrack_Async_Success`: Upload réussi, vérification Status
- `TestUploadTrack_Async_Interruption`: Gestion interruption (contexte)
- `TestUploadTrack_Async_ErrorHandling`: Gestion erreurs
- `TestCopyFileAsync_ContextCancellation`: Annulation directe
---
## Limitations et Notes
### Limitations Actuelles
1. **Pas de progression détaillée**: Le `progress` dans `GetUploadStatus` n'est pas mis à jour pendant la copie (reste à 0 jusqu'à 100)
2. **Timeout fixe**: 5 minutes (non configurable via env)
3. **Pas de retry automatique**: Si la copie échoue, le Track reste en `Failed`
### Améliorations Futures (Optionnel)
1. **Progression détaillée**: Utiliser `io.TeeReader` pour suivre les bytes copiés
2. **Retry automatique**: Relancer la copie en cas d'erreur réseau
3. **Webhooks**: Notifier le client quand l'upload est terminé
4. **Chunked upload**: Pour très gros fichiers (>100MB)
---
## Cohérence avec l'Architecture
### Avantages
-**Cohérent** avec `GetUploadStatus` existant
-**Cohérent** avec `Track.Status` (Uploading, Processing, Completed, Failed)
-**Traçabilité complète** (logs + request_id)
-**Nettoyage automatique** en cas d'échec
-**Support cancellation** (context)
### Intégration
- ✅ Utilise le système de Status existant
- ✅ Compatible avec le traitement asynchrone (streaming, metadata)
- ✅ Pas de changement breaking pour les clients (juste nouveau status code)
---
## Dépannage
### Upload reste en "uploading"
**Cause**: Goroutine bloquée ou timeout non atteint
**Solution**: Vérifier les logs, attendre 5 minutes max
### Fichier créé mais Status=Failed
**Cause**: Erreur après copie (validation, DB, etc.)
**Solution**: Vérifier les logs pour le message d'erreur
### Status=Failed immédiatement
**Cause**: Erreur lors de l'ouverture du fichier source
**Solution**: Vérifier que le fichier est valide et accessible
---
## Références
- [HTTP 202 Accepted](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202)
- [Async Processing Pattern](https://restfulapi.net/asynchronous-operations-in-rest/)
- [Context Package](https://pkg.go.dev/context)
---
**Dernière mise à jour**: 2025-01-27
**Maintenu par**: Veza Backend Team