ADR-019 : Extraction de l'upload générique hors du core : forge-mvc-files¶
Statut¶
Acceptée, Forge 1.0.0-beta.x. Extraction livrée : forge-mvc-files est publié.
Décision validée sur le principe (« sortir complètement l'upload du core »).
Périmètre figé ci-dessous ; exécution par étapes (chantier multi-tickets).
Date¶
2026-06-04
Contexte¶
Le core de Forge embarque aujourd'hui tout le pipeline d'upload générique dans
core/uploads/ :
manager.py,save_upload,SavedUpload,serve_media_file,
delete_upload,delete_media_file,get_upload_path,upload_root,
_read_upload;storage.py, écriture disque uuid/horodatée, anti-traversal
(normalize_media_path,media_path_to_storage_path,is_safe_media_path,
save_bytes,delete_file) ;validators.py,validate_upload_metadata,validate_extension,
validate_mime_type,validate_size;exceptions.py, hiérarchieUploadError;rate_limit.py,is_upload_rate_limited,record_upload_attempt.
Après l'extraction du traitement d'image (ADR-018), l'upload générique reste
le dernier gros bloc applicatif logé dans le noyau. Un framework web peut
parfaitement exister sans upload de fichiers : c'est une brique applicative,
pas un fondement. Le garder dans le core contredit le principe de noyau
minimal (charte principe 8, ADR-004) et laisse deux façons d'aborder les
fichiers (core + opt-ins médias), en tension avec le principe 11 (« une seule
façon officielle »).
Couplage existant à dénouer¶
core/forms/fields.py(dans le core) importecore.uploads.exceptions
(UploadError) etcore.uploads.validators(validate_extension,
validate_mime_type,validate_size) pour validerFileField/ImageField.
⚠️ Le core ne peut pas dépendre d'un opt-in (ADR-004), voir la décision.forge-mvc-imagesdépend decore.uploads(save_upload,_read_upload,
validate_upload_metadata,SavedUpload,storage) → dépendra de
forge-mvc-files(inversion de dépendance).- Générateurs
make:crud/ pages publiques :
cli/entities/crud/controller_builder.pyémet
from core.uploads import save_upload(fichiers documents). cli/assets/uploads.py(init_upload_storage) etcli/_support/help_dispatch.py.- Starter
cli/starters/data/file-upload/…(contrôleur d'exemple). - ~34 fichiers de tests importent
core.uploads. - Docs :
docs/reference/api.md,reference.md,reference-schema.md,
features/media.md,deployment/production-limits.md,
deployment/production-security.md,philosophy/security.md,
starters/welcome-forge/avance/file-upload.md,ADR-004,CLAUDE.md(§3). forge-mvc-audio(livré) possède son propre stockage, non impacté.rate_limitd'upload est upload-spécifique (l'auth/MFA a son propre
rate-limit danscore.security.hashing) → part sans accroc.
Décision¶
Créer un opt-in forge-mvc-files (forge_mvc_files) qui devient l'unique
propriétaire de l'upload générique. Le pipeline d'I/O quitte le core :
- Déplacé vers
forge-mvc-files:manager(save_upload,SavedUpload,
serve_media_file,delete_upload,delete_media_file,get_upload_path,
upload_root,_read_upload),storage(écriture/anti-traversal),
rate_limitd'upload. - Reste dans le core (relocalisé hors de
core/uploads/, vers
core/forms/ou un util de validation générique) : les validators
purs (validate_extension,validate_mime_type,validate_size) et la
hiérarchie d'exceptionsUploadError, uniquement parce que
core/forms/fields.py(FileField) en dépend et que le core ne peut pas
dépendre d'un opt-in (ADR-004). Ce sont des contrôles purs (chaîne/entier),
sans I/O, ils ne contredisent pas le noyau minimal.forge-mvc-files
réutilise ces validators du core.
Le core ne fait donc plus aucune écriture/lecture de fichier uploadé :
save_upload, serve_media_file, le storage et le rate-limit d'upload
disparaissent du noyau. FileField continue de valider (métadonnées) mais
l'écriture d'un fichier devient l'affaire de forge-mvc-files.
Conformément à la convention pré-1.0, l'extraction se fait sans alias
déprécié : les imports core.uploads du pipeline sont supprimés ; importer
depuis forge_mvc_files.
Hors périmètre¶
- Aucune nouvelle fonctionnalité d'upload : on déplace le comportement existant.
forge-mvc-images,forge-mvc-audio,forge-mvc-videone changent pas de
comportement (seule la dépendance d'images verscore.uploadsest repointée).- Les validators génériques + exceptions restent dans le core (voir décision).
Conséquences¶
Positives¶
- Noyau minimal réel (principe 8, ADR-004) : le core n'écrit plus de fichier
uploadé. - Une seule façon officielle (principe 11) :
forge-mvc-filesdétient
l'upload générique ; les modules médias (images/audio/vidéo) se branchent
au-dessus ou à côté, sans doublon avec le core. - Cohérence avec ADR-018 (extraction image) : même trajectoire d'extraction.
Coûts / ruptures¶
- Toute application qui uploade un fichier devra installer
forge-mvc-files
(changement de contrat, assumé, philosophie opt-in explicite). - Inversion de dépendance :
forge-mvc-imagesdépendra deforge-mvc-files. make:crudgénère désormaisfrom forge_mvc_files import save_uploadpour les
champs fichier ; tests de génération à mettre à jour.- ~34 fichiers de tests + ~10 docs à repointer ;
CLAUDE.md(§3) etADR-004
(périmètre) à amender (mainteneur, fichiers protégés par le hook). - Garde-fous packaging/perimètre à inverser (le core ne contient plus d'upload).
Plan d'exécution (tickets)¶
FILES-PKG-SCAFFOLD-001, squelettepackages/forge-mvc-files
(pyproject+forge_mvc_files/__init__), enregistrement opt-in (catalogue,
classifier, CI, release-policy, contrats de tests).FILES-VALIDATORS-KEEP-001, relocaliser les validators purs + exceptions
UploadErrorhors decore/uploads/(verscore/forms/oucore/validation),
repointercore/forms/fields.py. Le core garde la validation, pas l'I/O.FILES-MOVE-PIPELINE-001, déplacermanager+storage+rate_limit
d'upload versforge_mvc_files; le core ne réexporte plus l'upload.FILES-IMAGES-REPOINT-001,forge-mvc-imagesdépend deforge-mvc-files
(importscore.uploads→forge_mvc_files).FILES-CLI-RENAME-001, générateursmake:crud/pages publiques +
cli/assets/uploads.py+ starterfile-upload→forge_mvc_files.FILES-DOCS-PERIMETER-001, docs (api.md,production-*,security.md,
file-upload.md…), ADR-004 et CLAUDE.md §3 (mainteneur).CORE-DROP-UPLOADS-001, suppression decore/uploads/, inversion des
garde-fous périmètre + test d'absence.
L'ordre garantit que la suppression de core/uploads/ (étape 7) vient après
le repointage de tous les importeurs, pour ne jamais casser la collecte.
Alternatives écartées¶
- Upload extrait mais
serve_media_filegardé dans le core : compromis
rejeté, laisse deux endroits pour les fichiers (principe 11 moins net) et
garde de l'I/O fichier dans le noyau. - Couche
forge-mvc-filespar-dessuscore.uploads: rejeté, laisserait
deux façons d'uploader (core + opt-in), exactement ce que le principe 11
proscrit. - Statu quo : rejeté, l'upload reste un bloc applicatif dans le noyau.
Références¶
- Charte principes 8 (noyau minimal), 11 (une seule façon officielle).
- ADR-004 (périmètre du core minimal strict), à amender.
- ADR-005 (packaging hybride monorepo + multi-distributions PyPI).
- ADR-018 (extraction du traitement d'image, même trajectoire).