Aller au contenu

PD-286 — Export multi-volumes pour dossiers > 1 GB

1. Contexte

Un dossier probatoire peut contenir plusieurs vidéos de 500 MB. Avec le hard limit actuel de 1 GB (MAX_BACKEND_EXPORT_BYTES = 1_073_741_824 dans export.constants.ts), un dossier de 2 vidéos + screenshots est inexportable : le backend renvoie 413 et l'app propose de réduire la sélection. C'est inacceptable — on ne peut pas demander à un utilisateur de retirer des preuves d'un dossier de plainte.

Détecté par la pipeline de cohérence inter-EB (Phase 2, décision D4) lors de l'analyse du conflit storage.export_max_bytes entre PD-283 (500 MB) et PD-85 (100 MB).

2. Besoin

En tant qu' utilisateur ProbatioVault, je veux que l'export de mon dossier probatoire fonctionne quelle que soit la taille totale, afin de pouvoir déposer un dossier complet (plainte, partage, archivage) sans être limité par une contrainte technique invisible.

3. Périmètre fonctionnel

3.1 Ce qui est inclus

Composant Changement
BackendExportService Au lieu de rejeter 413 quand totalSize > maxExportBytes, le service split le manifest en N volumes. Chaque volume contient un sous-ensemble de preuves dont la taille cumulée <= VOLUME_MAX_BYTES.
Backend — Réponse API La réponse ComplaintFileResponseDto évolue pour supporter N volumes : chaque volume a ses propres signed URLs, son manifest partiel, et des métadonnées de volume (index, total, hash partiel).
Backend — Manifest multi-volumes Le manifest racine référence les N volumes (index, taille, hash). Chaque volume contient un manifest partiel listant ses preuves.
AppExportOrchestrator L'orchestrateur détecte une réponse multi-volumes, télécharge séquentiellement chaque volume, et assemble le .pvproof final de manière transparente.
AppPvproofAssembler L'assembleur accepte les entrées de N volumes et produit un unique fichier .pvproof. Le pvproof.json final contient les métadonnées de tous les volumes assemblés.
App — UX Aucun changement visible. L'utilisateur voit un seul export, une seule barre de progression. Le multi-volumes est entièrement transparent.

3.2 Ce qui est exclu

  • Pas de configuration utilisateur ou admin du seuil de split (contrainte technique hardcodée)
  • Pas de changement sur le format .pvproof final (le résultat reste un seul fichier)
  • Pas de changement sur les guards (OidcJwtAuthGuard, PremiumGuard) ni le rate limiting
  • Pas de modification de la pipeline de validation des preuves (ProofValidatorPipeline)

4. Seuil de volume

Le seuil par volume est fixé à 768 MB (VOLUME_MAX_BYTES = 805_306_368).

Justification : - Hard limit backend actuel : 1 GB (1_073_741_824 bytes) - 768 MB laisse ~25% de marge pour le manifest, les métadonnées JSON, la chronologie et les overhead ZIP par volume - Suffisamment grand pour minimiser le nombre de volumes dans les cas courants (2 vidéos de 500 MB = 2 volumes seulement) - Compatible avec la recommandation mobile de 500 MB (MAX_MOBILE_EXPORT dans la description PD-286) : chaque volume reste sous le seuil mobile de 1 GB

La constante est définie dans export.constants.ts comme variable globale, figée au build.

5. Architecture de la solution

5.1 Backend — Split du manifest

ExportService.execute()
  ├── Steps 1-4 : inchangés (ownership, load, validate, size check)
  ├── Step 5 : SIZE CHECK MODIFIÉ
  │   ├── totalSize <= VOLUME_MAX_BYTES → single volume (comportement actuel)
  │   └── totalSize > VOLUME_MAX_BYTES → multi-volume split
  │       ├── Partitionner validProofs en N groupes (bin-packing par taille)
  │       ├── Chaque groupe <= VOLUME_MAX_BYTES
  │       └── Générer manifest + signed URLs par volume
  ├── Steps 6-9 : adaptés pour N volumes
  └── Step 10 : réponse avec volumes[]

5.2 App — Assemblage transparent

ExportOrchestrator.execute()
  ├── Appel API backend → response
  ├── IF response.volumes.length === 1
  │   └── Flux actuel inchangé
  ├── IF response.volumes.length > 1
  │   ├── Pour chaque volume (séquentiel) :
  │   │   ├── Télécharger les fichiers (signed URLs du volume)
  │   │   ├── Vérifier intégrité (hash partiel)
  │   │   └── Ajouter les entrées au PvproofAssembler
  │   ├── Générer pvproof.json avec metadata multi-volumes
  │   └── Finaliser le .pvproof unique
  └── Notification + cleanup

5.3 Contrat API (évolution ComplaintFileResponseDto)

// Nouveau : réponse multi-volumes
interface ComplaintFileResponseDto {
  exportId: string;
  // Champs existants conservés pour rétrocompatibilité (volume unique)
  manifest?: ExportManifest;
  chronology?: Chronology;
  signedUrls?: SignedUrlDto[];
  // Nouveau : tableau de volumes (présent si multi-volumes)
  volumes?: ExportVolumeDto[];
  // Communs
  guideUrl: string;
  readmeVerification: string;
  rejectedProofs: RejectedProofDto[];
  minorEvidence?: MinorEvidenceDto;
}

interface ExportVolumeDto {
  volumeIndex: number;       // 0-based
  totalVolumes: number;
  signedUrls: SignedUrlDto[];
  manifest: ExportManifest;  // manifest partiel de ce volume
  estimatedBytes: number;
  integrityHash: string;     // SHA3-256 du manifest partiel
}

6. Invariants

ID Invariant Justification
INV-286-01 Chaque volume <= VOLUME_MAX_BYTES (768 MB) Contrainte technique mobile + backend
INV-286-02 L'union des preuves de tous les volumes === l'ensemble des preuves validées Aucune preuve ne doit être perdue dans le split
INV-286-03 Le fichier .pvproof final est identique quel que soit le nombre de volumes Transparence totale pour l'utilisateur
INV-286-04 L'intégrité de chaque volume est vérifiable indépendamment (hash partiel) Détection d'erreur de téléchargement par volume
INV-286-05 La rétrocompatibilité API est maintenue (exports < 768 MB = réponse inchangée) Pas de breaking change pour les clients existants
INV-286-06 Audit WORM fail-closed pour chaque volume (extension INV-85-05) Traçabilité complète y compris multi-volumes
INV-286-07 Le pvproof.json final contient volumes_count et assembled_from si multi-volumes Traçabilité de l'assemblage

7. Critères d'acceptation

ID Critère Vérification
CA-286-01 Un export de 2 GB (4 vidéos de 500 MB) produit 3 volumes de ~680 MB chacun Test d'intégration backend
CA-286-02 Un export de 500 MB (< seuil) produit un volume unique (rétrocompatibilité) Test de non-régression
CA-286-03 L'app assemble 3 volumes en un seul .pvproof sans intervention utilisateur Test E2E app
CA-286-04 La barre de progression reflète l'avancement global (tous volumes) Review UX
CA-286-05 Si un volume échoue au téléchargement, l'export entier échoue avec erreur explicite Test d'erreur app
CA-286-06 L'audit WORM contient volumes_count et les hashes de chaque volume Test d'intégration audit
CA-286-07 Le manifest racine référence les N volumes avec index/hash/taille Test unitaire backend
CA-286-08 Le bin-packing ne sépare jamais les documents d'une même preuve entre volumes Test unitaire backend

8. Risques et mitigations

Risque Impact Mitigation
Mémoire mobile lors de l'assemblage multi-volumes Crash OOM sur appareils < 4 GB RAM Assemblage en streaming (PvproofAssembler utilise déjà fflate en streaming)
Expiration des signed URLs entre volume 1 et volume N Échec de téléchargement du dernier volume TTL des signed URLs >> temps de téléchargement total. TTL actuel = configurable (INV-85-04)
Taille totale sans limite théorique Abus (export de 100 GB) Ajouter un hard limit global (MAX_TOTAL_EXPORT_BYTES, ex: 10 GB) — scope PD-286
Rétrocompatibilité avec clients existants Casse des intégrations Réponse avec volumes optionnel, champs legacy préservés pour single-volume

9. Arbitrages et décisions

# Décision Justification
D1 Seuil à 768 MB (pas 500 MB) Minimise le nombre de volumes. Un dossier de 1.5 GB = 2 volumes au lieu de 3
D2 Bin-packing par preuve entière (pas par fichier) Une preuve (vidéo + metadata + hash) est une unité atomique. La séparer casserait la vérification d'intégrité
D3 Téléchargement séquentiel (pas parallèle) des volumes Évite les pics mémoire sur mobile. La bande passante est le bottleneck, pas la latence
D4 Hard limit global de 10 GB (MAX_TOTAL_EXPORT_BYTES) Protection contre abus. Aucun dossier de plainte réaliste ne dépasse 10 GB
D5 Le multi-volumes s'applique à TOUS les types d'export (pas seulement plainte) Clarification PO : le split est une contrainte technique transverse
D6 Flow gaps PD-101 (jpeg, pending_seal) ignorés Vérification formelle step 0 : PD-101 (upload) produit des états que PD-286 (export) ne consomme pas — domaines orthogonaux, pas de contradiction. Score cohérence : 98.8/100

10. Stories liées

Story Relation
PD-85 Export dossier plainte (fonctionnalité d'origine) — INV-85-07 modifié
PD-283 Assemblage ZIP streaming .pvproof — PvproofAssembler réutilisé
PD-101 Upload Mobile AES-GCM — pattern chunking/streaming applicable

11. Learnings injectés

Source Learning Application
PD-283 (Gate 8, RESERVE) purgeStale() doit être appelé proactivement au démarrage du flux export Appeler purgeStale au début de l'assemblage multi-volumes dans l'app
PD-283 (Gate 8, RESERVE) Integrity hash must be verified functionally (not just format-validated) Vérifier le hash de chaque volume après téléchargement, pas juste le format
PD-101 (Gate 8, GO) FILE-LEVEL encryption avant chunking élimine risque nonce-reuse Le split par preuve entière (D2) préserve cette propriété