ADR-020 — Périmètre de forge-mvc-files : primitives de stockage média génériques¶
Statut¶
Proposé — Forge 1.0.0-beta.x.
Décision de direction (option B ci-dessous). L'ADR fixe le périmètre cible de
forge-mvc-files; l'exécution est progressive et non urgente (les opt-ins déjà livrés ne sont pas refondus avant la 1.0).
Date¶
2026-06-05
Contexte¶
Depuis ADR-019, forge-mvc-files détient le pipeline d'upload générique
extrait du core : écriture disque anti-traversal, save_upload/SavedUpload,
service de fichiers serve_media_file (HTTP Range), suppression, rate-limit.
La validation pure (extension/MIME/taille, UploadError) reste dans le core
(core.forms.upload_validation), réexportée par files — le core ne peut pas
dépendre d'un opt-in (ADR-004).
forge-mvc-files est présenté comme l'upload générique de Forge. Mais l'audit
des dépendances révèle qu'il n'est pas la fondation commune annoncée :
| Module | S'appuie sur forge-mvc-files ? |
Modèle de stockage |
|---|---|---|
forge-mvc-images |
Oui (dépendance déclarée + imports réels) | nom assaini + variantes (même forme que le « document » de files) |
forge-mvc-video |
Non (0 dépendance, 0 import) | propre storage UUID, multi-rendition (originals/mp4/posters), transcodage |
forge-mvc-audio |
Non (0 dépendance, 0 import) | propre storage UUID, transcodage |
Il existe donc deux approches de stockage parallèles dans Forge :
- le pipeline générique core→files (utilisé par images) ;
- des stockages dédiés UUID (video, audio), indépendants.
Pourquoi cette divergence ? Elle est principielle, pas accidentelle :
- video/audio adoptent un chemin 100% UUID (le nom utilisateur n'apparaît
jamais — anti-traversal par construction), plus strict que le modèle
« nom assaini conservé » de
save_upload; - ils produisent plusieurs fichiers dérivés par upload (source + MP4/MP3 + poster), dans un sous-arbre dédié ;
- ils ont un cycle de vie (transcodage ffmpeg, états
uploaded→processing→ready) hors périmètre de files.
À noter : le service HTTP Range est déjà mutualisé dans le core
(Response.file) — files et le http.py de video y délèguent tous deux. La
duplication réelle n'est donc pas dans le service, mais dans l'écriture sûre
et les helpers anti-traversal, ré-implémentés côté video/audio.
Le résultat : la promesse « files = socle média générique » n'est tenue qu'à moitié, et chaque nouvel opt-in média risque de re-diverger (tension avec le principe 11, « une seule façon officielle »).
Décision¶
Option B — forge-mvc-files devient une boîte à primitives de stockage média
génériques sur laquelle tout opt-in média (image, video, audio, futurs)
compose sa propre disposition.
- files reste un opt-in, hors du core (confirmation d'ADR-019/ADR-004 : le core n'a pas à traiter de fichiers).
- files expose des primitives composables, pas un pipeline imposé :
- écriture disque sûre, y compris un mode UUID (nom utilisateur absent du chemin) en plus du mode « nom assaini » actuel ;
- helpers anti-traversal / chemin (
secure_filename,is_safe_media_path,normalize_media_path,save_bytes) ; - un appui pour les dispositions multi-rendition (un média = plusieurs fichiers dérivés sous une racine) ;
- service Range (mince enveloppe sur la primitive core), rate-limit, suppression.
save_uploadreste la convenance « document » bâtie sur ces primitives — c'est elle qu'images réutilise pour le chemin document. Elle n'est plus présentée comme « la » façon de stocker un média, mais comme une façade.- Règle pérenne : tout nouvel opt-in média s'appuie sur les primitives de files (et on vérifie que files expose ce dont il a besoin).
- L'API des primitives est conçue contre les 3 clients réels (image, video, audio) déjà existants — on extrait la forme commune observée, on n'invente pas une abstraction spéculative.
Hors périmètre¶
- Pas de big-bang : video et audio (déjà livrés, video publié) ne sont pas refondus en urgence. Leur repointage sur les primitives de files est opportuniste et post-1.0.
- files ne devient pas un framework configurable à options (anti principe 8) : des primitives, pas une usine.
- files n'absorbe pas la validation de domaine (codecs/ffprobe), le transcodage, ni aucun état/BDD — cela reste propriété des opt-ins métier.
- On ne force pas video/audio sur le modèle « nom assaini » : ce serait un recul de sécurité par rapport à leur chemin UUID.
Conséquences¶
Positives¶
- Sécurité à un seul endroit : l'écriture sûre / anti-traversal écrite, auditée et corrigée une fois, partagée par tous les médias.
- Clarté conceptuelle : « files = la fondation média » devient vrai, pas un slogan à moitié tenu.
- Extensibilité : un nouvel opt-in média assemble au lieu de réinventer.
- Cohérence (principe 11) : une seule façon officielle de stocker/servir un fichier en bas niveau.
Coûts / ruptures¶
- L'API de files grandit (mode UUID, appui multi-rendition) — à garder minimale et justifiée par les 3 clients réels.
- video et audio gagneront (à terme) une dépendance sur files qu'ils n'ont pas aujourd'hui.
- Effort de repointage sur du code livré/publié (video b13) → assumé progressif, post-1.0.
- Gain de dé-duplication modéré : le service Range est déjà core ; l'essentiel partagé est l'écriture sûre + helpers. La valeur est cohérence + sécurité + extensibilité, pas un dégraissage massif.
- Risque : sur-généraliser files en framework. Garde-fou : revue stricte au regard du principe 8 à chaque ajout de primitive.
Plan d'exécution (tickets)¶
- Cet ADR — fixe la direction (option B).
FILES-PRIMITIVES-*— exposer proprement le mode UUID + l'appui multi-rendition, validés contre la forme de image/video/audio, sans modifier encore aucun consommateur.- Repointage opportuniste de video puis audio sur les primitives de files (post-1.0, tickets dédiés, suite verte à chaque étape).
- Règle de contribution : documenter que tout nouvel opt-in média s'appuie
sur files (à refléter dans
docs/philosophy/module-author-guide.mdet la page médias).
Alternatives écartées¶
- (A) Statu quo — files = pipeline document unique. Laisse video/audio divergents et la promesse « socle générique » à moitié fausse ; la dérive se répète au prochain opt-in média. Écarté.
- Forcer video/audio à utiliser
save_upload. Les mettrait sur le modèle « nom assaini conservé », un recul de sécurité vs leur chemin UUID par construction. Écarté. - Réintégrer un stockage commun dans le core. Contredit ADR-004 et le principe 8 (le core n'a pas à traiter de fichiers). Écarté.
- Tout laisser tel quel et l'assumer. Deux stockages parallèles non documentés = dette implicite ; au minimum la frontière doit être actée — ce que fait cet ADR. Écarté au profit de la convergence dirigée par B.
Références¶
- ADR-004 — Périmètre du core minimal strict (le core ne dépend pas d'un opt-in).
- ADR-018 — Extraction du traitement d'image (
forge-mvc-images). - ADR-019 — Extraction de l'upload générique (
forge-mvc-files). - Charte v2 — principe 8 (noyau minimal, briques opt-in) et principe 11 (une seule façon officielle de faire chaque chose).