Aller au contenu

PD-43 — Plan d'implémentation : Service upload OVH S3 multipart streaming

Ce document est un artefact dérivé de la spécification canonique PD-43-specification.md. Toute divergence avec la spécification constitue une anomalie à corriger dans ce plan.


1. Découpage en composants

1.1 Architecture modulaire

flowchart TB
    subgraph API["API Layer (NestJS)"]
        UC[UploadController]
        AG[AuthGuard]
        TIG[TenantIsolationGuard]
    end

    subgraph Domain["Domain Layer"]
        US[UploadSession<br/>Aggregate Root]
        UP[UploadPart<br/>Value Object]
        UVR[UploadValidationRules<br/>Value Object]
    end

    subgraph Application["Application Layer"]
        USS[UploadSessionService]
        HSS[HashStreamService]
        QS[QuotaService]
        IV[IntegrityVerifier]
        CS[ConfigService]
    end

    subgraph Infrastructure["Infrastructure Layer"]
        S3A[S3MultipartAdapter<br/>OVH S3]
        SR[SessionRepository<br/>PostgreSQL]
        AL[AuditLogger]
    end

    API --> Domain
    Domain --> Application
    Application --> Infrastructure

1.2 Description des composants

Composant Responsabilité Fichier(s) cible
UploadController Exposition REST des endpoints (init, upload part, complete, abort, status) src/upload/upload.controller.ts
AuthGuard Vérification authentification JWT src/auth/guards/auth.guard.ts
TenantIsolationGuard Isolation tenant + propriétaire de session src/upload/guards/tenant-isolation.guard.ts
UploadSession Aggregate root : état de session, transitions, invariants métier src/upload/domain/upload-session.entity.ts
UploadPart Value Object : numéro, taille, ETag S3, hash partiel src/upload/domain/upload-part.vo.ts
UploadValidationRules Value Object : règles de validation (tailles, limites) src/upload/domain/validation-rules.vo.ts
UploadSessionService Orchestration des flux (init, part, complete, abort) src/upload/services/upload-session.service.ts
HashStreamService Calcul SHA3-256 incrémental sur stream src/upload/services/hash-stream.service.ts
QuotaService Vérification et mise à jour des quotas tenant src/upload/services/quota.service.ts
IntegrityVerifier Vérification intégrité S3 post-complétion src/upload/services/integrity-verifier.service.ts
ConfigService Paramètres de validation consultables src/upload/services/upload-config.service.ts
S3MultipartAdapter Abstraction AWS SDK S3 pour OVH src/upload/infrastructure/s3-multipart.adapter.ts
SessionRepository Persistance sessions + parts (PostgreSQL) src/upload/infrastructure/session.repository.ts
AuditLogger Journalisation sans contenu sensible src/upload/infrastructure/audit-logger.ts

2. Flux techniques

2.1 Flux N1 — Initialiser un upload multipart

sequenceDiagram
    participant C as Client
    participant UC as UploadController
    participant US as UploadSessionService
    participant S3 as S3MultipartAdapter

    C->>UC: POST /uploads
    UC->>UC: AuthGuard.canActivate()
    UC->>UC: TenantIsolationGuard
    UC->>US: initSession(dto)
    US->>US: QuotaService.check()
    US->>S3: createMultipartUpload()
    S3-->>US: { uploadId }
    US->>US: SessionRepository.save()
    US-->>UC: { sessionId, rules, expiresAt }
    UC-->>C: 201 Created

Données persistées (UploadSession):

{
  id: UUID,
  tenantId: UUID,
  ownerId: UUID,
  s3UploadId: string,
  s3Key: string,
  bucket: string,
  status: 'IN_PROGRESS',
  mimeType: string,
  expectedSize?: number,
  validationRules: UploadValidationRules,
  parts: [],
  hashState: null, // État SHA3-256 sérialisé
  createdAt: Date,
  expiresAt: Date,
}

2.2 Flux N2 — Uploader une part (streaming)

sequenceDiagram
    participant C as Client
    participant UC as UploadController
    participant US as UploadSessionService
    participant S3 as S3MultipartAdapter

    C->>UC: PUT /uploads/:id/parts/:partNumber (Stream)
    UC->>UC: Guards (Auth, Tenant, Owner)
    UC->>US: uploadPart(id, partNum, stream)
    US->>US: SessionRepository.findById()
    US->>US: validatePartRules() + idempotence check

    rect rgb(240, 248, 255)
        Note over US,S3: Streaming Pipeline
        US->>US: PassThrough Transform
        US->>US: HashStreamService (SHA3-256 incr.)
        US->>S3: S3.uploadPart(stream)
    end

    S3-->>US: { ETag, size }
    US->>US: updatePartAndHashState()
    US-->>UC: { partNumber, etag, size }
    UC-->>C: 200 OK

Mécanisme de streaming avec hash incrémental et vérification d'idempotence:

// Pseudo-code du flux streaming
async uploadPart(sessionId: string, partNumber: number, inputStream: Readable): Promise<PartResult> {
  const session = await this.sessionRepository.findById(sessionId);
  const existingPart = session.parts.find(p => p.number === partNumber);

  // Buffer temporaire pour calcul hash de vérification (taille limitée)
  // Note: Pour respecter le streaming, on utilise un hash incrémental sur le flux entrant
  const incomingHashStream = this.hashStreamService.createPartHash();
  const passThrough = new PassThrough();

  // Tee du stream: un vers hash vérification, un vers S3 (ou buffer si part existante)
  inputStream.pipe(passThrough);
  inputStream.pipe(incomingHashStream);

  // Attendre le hash complet du contenu entrant
  const incomingContentHash = await incomingHashStream.finalizePartHash();

  // === VÉRIFICATION IDEMPOTENCE (SPEC §4 Règles d'idempotence) ===
  if (existingPart) {
    if (existingPart.contentHash === incomingContentHash) {
      // Re-soumission identique → idempotence stricte (acceptée sans effet)
      // Le stream passThrough est drainé mais non traité
      await drainStream(passThrough);
      return existingPart;
    } else {
      // Re-soumission avec contenu DIFFÉRENT → REJET EXPLICITE (E4)
      await drainStream(passThrough);
      throw new UploadPartConflictException({
        code: 'UPLOAD_PART_CONFLICT',
        message: 'Part number already exists with different content',
        details: { sessionId, partNumber },
      });
    }
  }

  // === NOUVELLE PART ===
  // Création du hash stream incrémental pour le hash global
  const globalHashStream = this.hashStreamService.createIncrementalHash(session.hashState);

  // Upload vers S3 avec hash global en parallèle
  // Note: On doit re-streamer le contenu car il a été consommé pour le hash de vérification
  // Alternative: utiliser un buffer si part < seuil, sinon stocker temporairement
  const s3Result = await this.s3Adapter.uploadPart({
    bucket: session.bucket,
    key: session.s3Key,
    uploadId: session.s3UploadId,
    partNumber,
    body: passThrough,
    contentHash: incomingContentHash, // Pour vérification S3
  });

  // Mise à jour de l'état hash global
  globalHashStream.update(/* bytes de la part */);
  session.hashState = globalHashStream.getSerializedState();

  // Mise à jour session avec part
  session.addPart({
    number: partNumber,
    etag: s3Result.ETag,
    size: s3Result.size,
    contentHash: incomingContentHash, // Hash stocké pour futures vérifications idempotence
  });

  await this.sessionRepository.save(session);

  return session.parts.find(p => p.number === partNumber);
}

Point critique — Streaming vs vérification d'idempotence:

Le calcul du hash du contenu entrant AVANT de décider si c'est un doublon pose un défi pour le streaming pur. Deux stratégies:

Stratégie Avantage Inconvénient
A. Hash streaming puis comparaison Streaming préservé Contenu consommé, doit être bufferisé ou re-streamé
B. Buffer si part existante Simple Mémoire si part volumineuse + part existante
C. Rejet systématique si part existe Pas de buffer Non conforme spec (pas d'idempotence)

Recommandation: Stratégie A avec stockage temporaire sur disque si part > 10MB et part existante détectée. Sinon buffer mémoire.

2.3 Flux N3 — Compléter l'upload

sequenceDiagram
    participant C as Client
    participant UC as UploadController
    participant US as UploadSessionService
    participant S3 as S3MultipartAdapter
    participant IV as IntegrityVerifier

    C->>UC: POST /uploads/:id/complete
    UC->>UC: Guards
    UC->>US: completeSession(id)
    US->>US: validateCompletion() (status, expiry, parts)
    US->>S3: completeMultipartUpload()
    S3-->>US: { ETag, ChecksumSHA256 }
    US->>IV: verify()
    IV-->>US: OK
    US->>US: finalizeHash() (SHA3-256 final)
    US->>US: session.complete(hash)
    US-->>UC: { objectKey, size, hash, status: COMPLETED }
    UC-->>C: 200 OK

2.4 Flux N4 — Abandonner un upload

sequenceDiagram
    participant C as Client
    participant UC as UploadController
    participant US as UploadSessionService
    participant S3 as S3MultipartAdapter

    C->>UC: DELETE /uploads/:id
    UC->>UC: Guards
    UC->>US: abortSession(id)
    US->>US: validateAbort() (status not terminal)
    US->>S3: abortMultipartUpload()
    S3-->>US: { success }
    US->>US: session.abort()
    US-->>UC: { status: ABORTED }
    UC-->>C: 200 OK

3. Mapping invariants → mécanismes

# Invariant Mécanisme technique Composant
INV-1 Aucun contenu en clair journalisé AuditLogger filtre les champs sensibles. Pattern de logging structuré avec whitelist (id, size, hash, status). Intercepteur global NestJS. AuditLogger, LoggingInterceptor
INV-2 Streaming obligatoire Utilisation de PassThrough streams. Pas de .buffer() ni .toArray(). Body parser désactivé pour routes upload. Chunk size configurable. UploadController, S3MultipartAdapter
INV-3 Empreinte sur octets exacts Hash SHA3-256 calculé via HashStreamService en mode incrémental. État sérialisé entre parts. Finalisation à la complétion. HashStreamService
INV-4 Session complétable une seule fois Machine à états dans UploadSession. Transition IN_PROGRESS → COMPLETED unique. Vérification état avant completeMultipartUpload(). UploadSession (FSM)
INV-5 Session abortable si non terminale Méthode canAbort() dans aggregate. Garde sur transition → ABORTED. UploadSession (FSM)
INV-6 Authentification/autorisation AuthGuard (JWT), TenantIsolationGuard (tenant match), SessionOwnerGuard (owner match). Guards NestJS
INV-7 Règles validation déterministes UploadValidationRules VO immuable. Chargé à l'init depuis ConfigService. Stocké avec session. Paramètres publiés via endpoint /config et versionnés (cf. §3.1). UploadValidationRules, ConfigService, UploadController
INV-8 État cohérent en cas d'erreur Transaction DB autour des opérations critiques. Rollback si S3 échoue. Pattern Saga pour complétion. SessionRepository (transactions)
INV-9 Algorithme hash normatif unique SHA3-256, hex, sur concaténation canonique. Hardcodé, non configurable. Documenté. HashStreamService
INV-10 Vérification intégrité cryptographique Double vérification: (1) SHA256 checksum S3 via x-amz-checksum-sha256, (2) Comparaison hash applicatif vs recalcul si nécessaire. IntegrityVerifier
INV-11 Idempotence des opérations Part: hash content → si identique, retour immédiat. Complete/Abort: si état terminal, retour état sans action. UploadSessionService
INV-12 Isolement tenant/propriétaire Clé composite (tenantId, sessionId) en DB. Guard vérifie user.tenantId === session.tenantId && user.id === session.ownerId. TenantIsolationGuard, SessionOwnerGuard

3.1 Publication et consultabilité des paramètres normatifs (Spec §4, CA12, CA14)

La spécification exige que les paramètres de validation soient explicitement définis, consultables et publiés avant toute opération d'upload.

Endpoint de consultation

GET /api/v1/uploads/config
Authorization: Bearer <token>

Réponse:

interface UploadConfigResponse {
  // Identifiant unique de cette configuration (pour traçabilité)
  configVersion: string;           // ex: "v2024.1.15-tenant-premium"

  // Contexte d'application
  context: {
    tenantId: string;
    planId: string;                // ex: "premium", "standard"
    effectiveAt: string;           // ISO 8601
  };

  // Paramètres de validation des parts (Spec §4 Règles normatives)
  partConstraints: {
    minPartSize: number;           // bytes, ex: 5242880 (5MB)
    maxPartSize: number;           // bytes, ex: 104857600 (100MB)
    maxPartCount: number;          // ex: 10000
  };

  // Paramètres de validation fichier
  fileConstraints: {
    maxFileSize: number;           // bytes, ex: 5497558138880 (5TB)
  };

  // Paramètres de session
  sessionConstraints: {
    maxDuration: number;           // seconds, ex: 86400 (24h)
  };

  // Quotas applicables au tenant/plan
  quotas: {
    maxCumulativeSize: number | null;    // bytes, null = illimité
    maxConcurrentUploads: number | null; // null = illimité
    maxUploadsPerDay: number | null;     // null = illimité
  };

  // Référence aux valeurs par défaut normatives (Annexe D spec)
  normativeDefaults: {
    documentationUrl: string;      // Lien vers doc versionnée
    specVersion: string;           // ex: "PD-43-v1.0"
  };
}

Mécanismes de garantie (CA12, CA14)

Exigence Mécanisme
Paramètres identiques pour contexte donné ConfigService charge la config au démarrage et la cache par (tenantId, planId). Invalidation uniquement sur redéploiement ou event explicite.
Consultable pour audit/tests Endpoint GET /config retourne les valeurs actives. Accessible aux tests automatisés.
Traçabilité des changements configVersion unique. Table config_audit_log enregistre chaque changement avec timestamp, author, diff.
Déviations documentées Si valeurs ≠ Annexe D, champ deviations[] ajouté à la réponse avec justification.

Stockage et versioning

-- Table de configuration par tenant/plan
CREATE TABLE upload_configs (
    id UUID PRIMARY KEY,
    tenant_id UUID NOT NULL,
    plan_id VARCHAR(64) NOT NULL,
    config_version VARCHAR(128) NOT NULL,
    min_part_size BIGINT NOT NULL,
    max_part_size BIGINT NOT NULL,
    max_part_count INT NOT NULL,
    max_file_size BIGINT NOT NULL,
    max_session_duration INT NOT NULL,
    quota_max_cumulative_size BIGINT,
    quota_max_concurrent_uploads INT,
    quota_max_uploads_per_day INT,
    effective_at TIMESTAMPTZ NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_tenant_plan_version UNIQUE (tenant_id, plan_id, config_version)
);

-- Audit log des changements
CREATE TABLE config_audit_log (
    id UUID PRIMARY KEY,
    config_id UUID NOT NULL REFERENCES upload_configs(id),
    changed_by VARCHAR(256) NOT NULL,
    changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    previous_values JSONB,
    new_values JSONB,
    reason VARCHAR(512)
);

Injection dans la session

À l'initialisation d'une session (Flux N1), les règles actives sont :

  1. Chargées depuis ConfigService.getActiveConfig(tenantId, planId)
  2. Copiées dans UploadSession.validationRules (snapshot immuable)
  3. Utilisées pour toutes les validations de cette session

Cela garantit que les règles restent constantes pendant toute la durée de vie de la session, même si la configuration globale change entre-temps.


4. Gestion des erreurs

4.1 Codes d'erreur normalisés

Code HTTP Description Conditions
UPLOAD_NOT_AUTHENTICATED 401 Requête non authentifiée Token JWT absent ou invalide
UPLOAD_NOT_AUTHORIZED 403 Accès refusé Tenant ou owner mismatch
UPLOAD_SESSION_NOT_FOUND 404 Session inconnue ID invalide ou supprimé
UPLOAD_SESSION_EXPIRED 410 Session expirée expiresAt < now()
UPLOAD_SESSION_TERMINAL 409 Session en état terminal Status ∈
UPLOAD_PART_INVALID 400 Part invalide Numéro hors plage, taille invalide
UPLOAD_PART_CONFLICT 409 Conflit de part Re-soumission contenu différent
UPLOAD_COMPLETION_INCOMPLETE 400 Parts manquantes Trous dans séquence ou parts absentes
UPLOAD_QUOTA_EXCEEDED 429 Quota dépassé Limites tenant/plan atteintes
UPLOAD_INTEGRITY_FAILED 500 Échec intégrité Hash mismatch post-complétion
UPLOAD_S3_ERROR 502 Erreur stockage S3 Erreur OVH S3 non récupérable

4.2 Format de réponse d'erreur

interface UploadErrorResponse {
  code: string;           // Code stable (ex: UPLOAD_PART_INVALID)
  message: string;        // Message non sensible
  details?: {
    sessionId?: string;   // ID session si applicable
    partNumber?: number;  // Numéro part si applicable
    constraint?: string;  // Règle violée si applicable
  };
  timestamp: string;      // ISO 8601
  requestId: string;      // Correlation ID
}

4.3 Stratégies de récupération

Erreur Stratégie client Action serveur
UPLOAD_S3_ERROR (part) Retry avec backoff Aucune part enregistrée
UPLOAD_S3_ERROR (complete) Retry possible Session reste IN_PROGRESS
UPLOAD_INTEGRITY_FAILED Nouveau upload complet Session → FAILED, cleanup S3
UPLOAD_SESSION_EXPIRED Nouvel init Cleanup S3 async
Timeout réseau Retry requête Idempotence garantie

5. Impacts sécurité

5.1 Matrice des menaces et mitigations

Menace Impact Mitigation Composant
Injection de contenu malveillant Stockage de malware Hors périmètre (antimalware externe). Logs pour forensics. N/A
Accès non autorisé à session Fuite de données, corruption Guards multi-niveaux (Auth, Tenant, Owner). Pas d'énumération d'ID. Guards
Déni de service par upload massif Ressources épuisées Quotas, rate limiting, timeout sessions. QuotaService, Config
Fuite de contenu dans logs Exposition données Whitelist de champs loggables. Audit logger dédié. AuditLogger
Man-in-the-middle Interception données TLS obligatoire. HSTS. Infra (hors scope)
Replay attack Upload dupliqué Idempotence + session unique. Expiration. UploadSession
Corruption silencieuse S3 Intégrité compromise Vérification checksum cryptographique. IntegrityVerifier

5.2 Logging sécurisé

Champs autorisés dans les logs: - sessionId, tenantId, userId - partNumber, partSize, totalSize - status, errorCode - hash (empreinte finale) - timestamp, requestId, duration

Champs interdits: - Contenu du fichier (bytes) - Noms de fichiers (potentiellement sensibles) - Headers contenant des tokens (masqués) - Stack traces avec paramètres

5.3 Contrôle d'accès

// Hiérarchie des guards
@UseGuards(AuthGuard)           // 1. Authentification valide
@UseGuards(TenantIsolationGuard) // 2. Appartenance au tenant de la session
@UseGuards(SessionOwnerGuard)    // 3. Propriétaire de la session (par défaut)

Règle de délégation (future): - Si politique de délégation activée → SessionOwnerGuard vérifie aussi les délégations explicites - Absence de politique = owner only (comportement par défaut normatif)


6. Hypothèses techniques

HT-1. Stack technique backend

  • Framework: NestJS (TypeScript)
  • Runtime: Node.js 20+ (support streams natif)
  • ORM: TypeORM avec PostgreSQL
  • SDK S3: AWS SDK v3 (compatible OVH)

HT-2. Capacités OVH S3

  • Support complet de l'API Multipart Upload S3
  • Support des checksums via x-amz-checksum-sha256 ou équivalent
  • Limites OVH: 10 000 parts max, 5GB par part, 5TB par objet

HT-3. Persistance de l'état de hash

  • L'état interne SHA3-256 (Keccak) peut être sérialisé/désérialisé
  • Utilisation de js-sha3 ou implémentation custom avec état exportable
  • Stockage état en base (BYTEA PostgreSQL) entre parts

HT-4. Streams Node.js

  • PassThrough permet de dupliquer un stream pour hash + S3
  • Backpressure géré automatiquement par pipeline
  • Chunk size par défaut: 64KB (configurable)

HT-5. Transactions et concurrence

  • PostgreSQL supporte les transactions ACID
  • Pas de concurrence sur même session (single owner)
  • Lock optimiste via version column si nécessaire

HT-6. Configuration OVH S3

  • Credentials via variables d'environnement ou Vault
  • Endpoint OVH: s3.<region>.cloud.ovh.net
  • Bucket pré-créé et configuré (hors scope)

7. Points de vigilance

PV-1. Sérialisation état SHA3-256

Risque: La plupart des implémentations SHA3 ne permettent pas de sérialiser l'état intermédiaire.

Mitigation: - Option A: Implémenter SHA3-256 avec état exportable (Keccak sponge state) - Option B: Stocker hash de chaque part + recalcul final via XOR ou tree hash - Option C: Utiliser bibliothèque supportant getState()/setState() (ex: implémentation custom)

Recommandation: Option A avec implémentation Keccak pure JS modifiée.

PV-2. Cohérence état en cas de crash

Risque: Crash entre upload S3 et persistance DB = état incohérent.

Mitigation: 1. Transaction DB autour de chaque opération 2. Pattern "write-ahead": persister intention avant action S3 3. Job de réconciliation périodique (list S3 parts vs DB)

PV-3. Vérification intégrité S3

Risque: OVH S3 peut ne pas supporter tous les checksums natifs AWS.

Mitigation: 1. Vérifier support ChecksumSHA256 sur OVH 2. Fallback: télécharger et recalculer hash après complétion (coûteux) 3. Alternative: stocker MD5 parts (ETag) + vérifier cohérence liste

Recommandation: Tester en environnement OVH. Prévoir fallback.

PV-4. Gestion mémoire streaming

Risque: Accumulation de buffers si backpressure mal géré.

Mitigation: 1. Utiliser pipeline() qui gère backpressure 2. Limiter highWaterMark des streams 3. Monitorer heap usage en production

PV-5. Expiration et nettoyage

Risque: Sessions expirées avec parts orphelines sur S3 = coûts.

Mitigation: 1. Job CRON de nettoyage sessions expirées 2. Appel AbortMultipartUpload S3 pour chaque session expirée 3. Lifecycle rule S3 en backup (abort incomplete uploads > 7j)

PV-6. Taille des parts et performance

Risque: Parts trop petites = overhead réseau. Parts trop grandes = risque timeout.

Mitigation: 1. Taille minimale recommandée: 5MB (sauf dernière part) 2. Taille maximale: 100MB (compromis) 3. Documentation des contraintes pour clients

PV-7. Ordre des parts pour hash

Risque: Hash calculé dans mauvais ordre = empreinte incorrecte.

Mitigation: 1. Stocker parts avec numéro explicite 2. À la complétion, trier parts par numéro 3. Concaténer hashs partiels dans l'ordre (si tree hash) ou recalculer

PV-8. Rollback complétion S3

Risque: S3 CompleteMultipartUpload n'est pas annulable.

Mitigation: 1. Vérifier intégrité AVANT complétion impossible (parts non accessibles) 2. Vérifier APRÈS complétion, marquer FAILED si KO 3. Objet S3 existant mais non référencé = orphelin (cleanup batch)


Annexe A — Schéma de données

-- Table principale des sessions
CREATE TABLE upload_sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    owner_id UUID NOT NULL,
    s3_upload_id VARCHAR(256) NOT NULL,
    s3_bucket VARCHAR(128) NOT NULL,
    s3_key VARCHAR(512) NOT NULL,
    status VARCHAR(32) NOT NULL DEFAULT 'IN_PROGRESS',
    mime_type VARCHAR(128),
    expected_size BIGINT,
    final_size BIGINT,
    final_hash VARCHAR(64), -- SHA3-256 hex
    hash_state BYTEA, -- État Keccak sérialisé
    validation_rules JSONB NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    completed_at TIMESTAMPTZ,

    CONSTRAINT chk_status CHECK (status IN ('IN_PROGRESS', 'COMPLETED', 'ABORTED', 'FAILED', 'EXPIRED'))
);

-- Table des parts
CREATE TABLE upload_parts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id UUID NOT NULL REFERENCES upload_sessions(id) ON DELETE CASCADE,
    part_number INT NOT NULL,
    size BIGINT NOT NULL,
    etag VARCHAR(128) NOT NULL,
    content_hash VARCHAR(64), -- Hash SHA3-256 de cette part seule
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_session_part UNIQUE (session_id, part_number),
    CONSTRAINT chk_part_number CHECK (part_number >= 1 AND part_number <= 10000)
);

-- Index pour requêtes fréquentes
CREATE INDEX idx_sessions_tenant ON upload_sessions(tenant_id);
CREATE INDEX idx_sessions_owner ON upload_sessions(owner_id);
CREATE INDEX idx_sessions_status ON upload_sessions(status) WHERE status = 'IN_PROGRESS';
CREATE INDEX idx_sessions_expires ON upload_sessions(expires_at) WHERE status = 'IN_PROGRESS';

Annexe B — Contrat API (OpenAPI summary)

Méthode Endpoint Description
POST /api/v1/uploads Initialiser une session multipart
PUT /api/v1/uploads/{sessionId}/parts/{partNumber} Uploader une part (streaming)
POST /api/v1/uploads/{sessionId}/complete Compléter l'upload
DELETE /api/v1/uploads/{sessionId} Abandonner l'upload
GET /api/v1/uploads/{sessionId} Consulter état et parts
GET /api/v1/uploads/config Consulter règles de validation

Annexe C — Dépendances NPM

{
  "@aws-sdk/client-s3": "^3.x",
  "@aws-sdk/lib-storage": "^3.x",
  "js-sha3": "^0.9.x",
  "uuid": "^9.x",
  "class-validator": "^0.14.x",
  "class-transformer": "^0.5.x"
}

Annexe D — Valeurs par défaut normatives (référence spec)

Paramètre Valeur par défaut Remarque
Taille min part 5 MB Sauf dernière part
Taille max part 100 MB Configurable par plan
Nombre max parts 10 000 Limite S3
Taille max fichier 5 TB Limite S3
Durée session 24h Configurable par plan
Quota taille/tenant Illimité Configurable par plan
Quota uploads/tenant Illimité Configurable par plan