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 |
Backend — ExportService | 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. |
App — ExportOrchestrator | 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. |
App — PvproofAssembler | 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é |