veza/veza-backend-api/docs/UPLOAD_ASYNC.md
2025-12-16 11:23:49 -05:00

8.4 KiB

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:

{
  "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:

{
  "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:

ctx := c.Request.Context() // Contient request_id du middleware
track, err := service.UploadTrack(ctx, userID, fileHeader)

Exemples cURL

1. Upload d'un fichier

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

curl -X GET http://localhost:8080/api/v1/tracks/550e8400-e29b-41d4-a716-446655440000/status \
  -H "Authorization: Bearer YOUR_TOKEN"

Réponse (pendant upload):

{
  "success": true,
  "data": {
    "track_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "uploading",
    "progress": 0,
    "message": "Upload started"
  }
}

Réponse (après copie):

{
  "success": true,
  "data": {
    "track_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "processing",
    "progress": 100,
    "message": "File uploaded, processing..."
  }
}

Réponse (échec):

{
  "success": true,
  "data": {
    "track_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "failed",
    "progress": 0,
    "message": "Failed to save file: ..."
  }
}

Polling Recommandé

Stratégie Simple

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

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

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

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


Dernière mise à jour: 2025-01-27
Maintenu par: Veza Backend Team