# 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