Aller au contenu

ADR-032 : Périmètre de la configuration upload (seul upload_max_size est du core)

Statut

Acceptée et mise en œuvre (ticket UPLOAD-CONFIG-DECOUPLE-001).
Seul upload_max_size reste dans le cœur ; le reste de la config upload est lu par les opt-ins forge-mvc-files et forge-mvc-images depuis l'environnement.

Complète ADR-031 (même patron de découplage), prolonge ADR-019 / ADR-020 (extraction de l'upload) et renforce ADR-004 ainsi que le principe 8 (noyau minimal, briques opt-in).


Contexte

Le squelette nu (env/example) porte un bloc upload de quatre variables, et le registre core.forge déclare une cinquième clé :

UPLOAD_MAX_SIZE, UPLOAD_ROOT, UPLOAD_ALLOWED_EXTENSIONS, UPLOAD_ALLOWED_MIME_TYPES
(+ upload_max_image_pixels, défaut codé en dur dans core/forge.py, hors env)

Examen précis des consommateurs réels :

Clé Lue par Le core la consomme ?
upload_max_size core/http/request.py (borne le corps multipart) oui
upload_root forge-mvc-files uniquement non
upload_allowed_extensions forge-mvc-files uniquement non
upload_allowed_mime_types forge-mvc-files uniquement non
upload_max_image_pixels forge-mvc-images uniquement non

Point décisif : le FileField du core (core/forms/fields.py) prend allowed_extensions, max_size, allowed_mime_types en paramètres (défaut None) et ne valide que si on les lui passe.
Il ne lit jamais le registre.
La validation par champ est donc explicite côté application, et les clés upload_allowed_* du registre ne servent qu'au flux de sauvegarde de forge-mvc-files.

Conséquence : dans un projet nu (sans opt-in), quatre des cinq variables ne sont lues par rien.
Leur présence dans env/example laisse croire que l'upload est géré et configuré, alors que le stockage/service relève de forge-mvc-files (ADR-019/020) et la validation par champ est explicite.
C'est de la connaissance d'opt-in qui fuite dans le noyau et le squelette nu.

upload_max_size est différent : le noyau lit cette valeur pour décider combien d'octets il accepte sur une requête multipart, avant tout opt-in.
C'est un contrôle anti-DoS intrinsèque à la couche HTTP.
On ne peut pas rendre le core ignorant d'une limite de corps multipart, puisque c'est lui qui lit les octets.


Décision

Seul upload_max_size reste un réglage du core.
Les quatre autres clés upload quittent le noyau et le squelette nu, vers les opt-ins qui les consomment.

Clé Avant Après
upload_max_size core registre + env/example inchangé (contrôle HTTP du noyau)
upload_root core registre + env/example lu par forge-mvc-files via os.getenv (défaut storage/uploads)
upload_allowed_extensions core registre + env/example idem forge-mvc-files (défaut sobre)
upload_allowed_mime_types core registre + env/example idem forge-mvc-files
upload_max_image_pixels core registre (hors env) lu par forge-mvc-images via os.getenv("UPLOAD_MAX_IMAGE_PIXELS") (défaut 24000000)

forge-mvc-files continue de lire upload_max_size depuis core.forge.get (valeur légitimement détenue par le noyau), mais lit ses propres clés (upload_root, extensions, MIME) depuis l'environnement.


Conséquences

Positives :

  • Le noyau ne déclare plus de config de stockage/validation d'upload qu'il ne consomme pas ; il ne garde que la limite de corps multipart, qui lui revient.
  • Le projet nu ne porte plus de variables trompeuses suggérant un upload « géré » alors qu'aucun opt-in n'est installé.
  • forge-mvc-files et forge-mvc-images possèdent leur configuration.
  • upload_max_image_pixels devient réellement configurable via l'environnement (aujourd'hui la docstring le prétend, à tort : aucune variable d'env n'existe).

À assumer :

  • Rupture interne pré-1.0 de core.forge.configure (retrait des kwargs upload_root, upload_allowed_extensions, upload_allowed_mime_types, upload_max_image_pixels).
    Pas d'alias, à signaler au CHANGELOG.md.
  • forge doctor : le check check_prod_security lit UPLOAD_ALLOWED_EXTENSIONS pour avertir d'uploads non restreints.
    Dans un projet nu sans forge-mvc-files, ce contrôle perd son sens et doit être conditionné à la présence de l'opt-in.
  • Provisioning : le bloc d'upload de forge-mvc-files (et la variable image de forge-mvc-images) est documenté par l'opt-in, ajouté à env/dev à l'installation, jamais écrit silencieusement (principe 9).

Mise en œuvre (ticket UPLOAD-CONFIG-DECOUPLE-001)

  1. Gardes d'abord : upload_root / upload_allowed_* / upload_max_image_pixels absents de core/forge.py ; projet nu sans ces variables ; upload_max_size conservé partout.
  2. forge-mvc-files : lecture os.getenv pour upload_root, upload_allowed_extensions, upload_allowed_mime_types (défauts propres) ; continue de lire upload_max_size depuis le core.
  3. forge-mvc-images : os.getenv("UPLOAD_MAX_IMAGE_PIXELS", "24000000") ; idem le starter image-safety.
  4. Core : retirer les quatre slots de core/forge.py (garder upload_max_size) et upload_root de _PATH_KEYS ; retirer le threading correspondant de core/app/app_factory.py (garder upload_max_size).
  5. Squelette nu + dogfood racine : retirer UPLOAD_ROOT, UPLOAD_ALLOWED_EXTENSIONS, UPLOAD_ALLOWED_MIME_TYPES de env/example, config.py, app.py (garder UPLOAD_MAX_SIZE).
  6. forge doctor : conditionner le check upload-restreint à la présence de forge-mvc-files.
  7. Documentation : docs/features/media.md / doc files et images documentent le bloc env à ajouter ; mise à jour des parcours welcome concernés.

Validations :

python -m pytest -x -q
python -m compileall -q .
ruff check .
mkdocs build --strict
git diff --check

Alternatives rejetées

Découpler aussi upload_max_size.
Rejetée : le noyau lit cette valeur dans core/http/request.py pour borner le corps d'une requête multipart, avant tout opt-in.
C'est le parseur du core qui lit les octets, il doit donc connaître le plafond.
Un opt-in ne peut pas s'injecter dans le parsing.
Le seul découplage possible serait un renommage cosmétique (max_multipart_body_size), sans gain réel et au prix de la lisibilité de UPLOAD_MAX_SIZE.

Conserver les cinq clés dans le core comme un « contrat upload ».
Rejetée : c'est le même argument « le core déclare, l'opt-in applique » écarté pour le mail (ADR-031).
Quatre des cinq clés n'ont aucun consommateur dans le core.


Charte appliquée

Principe 3 (refuser la magie cachée), principe 4 / ADR-004 (périmètre du core), principe 8 (noyau minimal), principe 9 (provisioning sans écriture invisible), principe 10 (API publique claire).