L'upload de fichiers dans Forge (forge-mvc-files)¶
Ce document explique ce que fait l'opt-in forge-mvc-files, ce qu'il expose, et comment on s'en sert.
forge-mvc-files détient le pipeline d'upload générique : valider, écrire sur disque de façon sécurisée, servir un fichier en streaming, le supprimer, limiter le débit d'upload.
C'est le socle des médias : forge-mvc-images, forge-mvc-video et forge-mvc-audio s'appuient dessus.
1. Rôle du module
Recevoir un fichier d'un formulaire et le stocker en toute sécurité demande plusieurs étapes : valider l'extension, le type MIME et la taille, neutraliser le nom (anti-traversal), écrire sur disque, puis savoir le re-servir.
L'opt-in regroupe ce pipeline derrière une API simple : save_upload pour entrer un fichier, serve_media_file pour le ressortir.
La validation pure (extension, MIME, taille) reste dans le cœur (core.forms.upload_validation, ADR-019) et est réexportée ici : le cœur ne peut pas dépendre d'un opt-in (ADR-004).
2. Installation
Prérequis : activez le venv du projet
Quelle que soit la source, installez dans le venv du projet :
Lancé hors d'un venv, pip vise le Python système (Debian 12+, Ubuntu 23.04+),
protégé par PEP 668. Il refuse alors d'installer, pour ne pas écraser les paquets
gérés par apt, et affiche externally-managed-environment.
Le venv de projet créé par forge new n'a pas ce verrou.
Installer le paquet¶
B. Depuis Git (avant-garde)¶
Cœur puis opt-in depuis git, dans le venv du projet (l'opt-in trouve le cœur git déjà en place, sans version publiée sur PyPI) :
3. Mise en service
Installer le paquet ne suffit pas à le rendre opérationnel.
Voici les gestes propres à forge-mvc-files, dans l'ordre.
Ils déclinent la procédure canonique, Rendre un opt-in opérationnel : les cinq
points.
1. L'épingler¶
Dans requirements.txt, à la même version ou au même commit que forge-mvc.
Sans cette ligne, l'opt-in n'existe que sur votre machine.
2. L'inscrire¶
L'opt-in est inscrit dans optins/registry.py (ADR-061), ce qui le rend visible du
projet.
--apply est obligatoire : sans lui, la commande simule et n'écrit rien.
3. Poser ce dont il a besoin¶
Cet opt-in n'apporte aucune table, mais il a tout de même une initialisation :
Elle crée storage/uploads/ et ses sous-dossiers images/, documents/ et tmp/.
Ne pas avoir de tables ne veut pas dire n'avoir rien à faire.
4. Le brancher là où il agit¶
Il s'importe dans le code qui s'en sert. Il n'y a ni route à monter ni middleware
à poser.
5. Le prouver¶
Puis un premier usage réel.
Un opt-in installé, inscrit et provisionné qu'aucun code n'appelle n'est pas
opérationnel : il est seulement présent.
4. Désinstallation
opt-in:disable est l'inverse d'enable : il dé-inscrit du registre (le code n'était pas câblé), sans toucher au paquet.
forge opt-in:remove files affiche la commande pip uninstall sans l'exécuter.
5. Commandes
forge-mvc-files ajoute ces commandes :
| Commande | Rôle | Exemple |
|---|---|---|
upload:init |
Initialise les dossiers de stockage d'upload. | forge upload:init |
media:init |
Initialise les dossiers de stockage média. | forge media:init |
6. Vue d'ensemble rapide
| Élément | Valeur |
|---|---|
| Paquet | forge-mvc-files |
| Module | forge_mvc_files |
| Catégorie | Médias et fichiers (ADR-055) |
| Couche | opt-in (brique optionnelle), socle des médias |
| Dépend de | forge-mvc (validation et exceptions restent au cœur) |
| API publique | save_upload, SavedUpload, serve_media_file, delete_upload, delete_media_file, primitives de stockage, rate-limit |
| Objet renvoyé | SavedUpload (métadonnées du fichier écrit) |
| Service HTTP | serve_media_file (streaming, HTTP Range) |
| Racine de stockage | variable d'environnement UPLOAD_ROOT (défaut storage/uploads) |
| Exceptions | UploadError et ses sous-classes (extension, MIME, taille, stockage) |
| Décisions d'architecture | ADR-019 (extraction), ADR-020 (primitives) |
| Installation | pip install --pre forge-mvc-files |
7. Schémas UML
Les deux schémas suivants montrent deux vues complémentaires de l'opt-in.
Le diagramme de classe montre l'API d'upload, les primitives de stockage et la hiérarchie d'exceptions.
Le diagramme de séquence montre l'enregistrement d'un upload puis sa relecture.
5.1 Diagramme de classe¶
Le diagramme de classe montre que save_upload valide (via le cœur), écrit via les primitives de stockage, et renvoie un SavedUpload ; serve_media_file produit une Response du cœur.
classDiagram
direction LR
class files {
<<module>>
+save_upload(file, category) SavedUpload
+serve_media_file(path, root, request) Response
+delete_upload(path) bool
+get_upload_path(filename, category) Path
+upload_root() Path
}
class SavedUpload {
<<dataclass>>
+str filename
+str original_name
+str path
+str category
+int size
+str mime_type
+dict variants
}
class storage {
<<module>>
+secure_filename(name) str
+save_bytes(data, ...) str
+is_safe_media_path(path) bool
}
class UploadError {
<<exception>>
}
class Response {
+int status
+body
}
files --> storage : écrit via
files --> SavedUpload : renvoie
files --> Response : serve_media_file
files ..> UploadError : peut lever
À retenir :
save_uploadest l'entrée unique : valider, sécuriser, écrire ;- il renvoie un
SavedUpload(nom stocké, chemin, taille, MIME) ; serve_media_fileressort le fichier enResponse, avec HTTP Range ;- toute anomalie d'upload lève une sous-classe de
UploadError.
5.2 Diagramme de séquence¶
Le diagramme de séquence montre un upload de formulaire puis l'affichage du média.
sequenceDiagram
actor Navigateur
participant Ctrl as Contrôleur
participant Files as forge_mvc_files
participant Core as Validation (cœur)
participant Disk as Disque (UPLOAD_ROOT)
Navigateur->>Ctrl: POST formulaire (multipart, fichier)
Ctrl->>Files: save_upload(file, category="images")
Files->>Core: valide extension / MIME / taille
Files->>Files: secure_filename (anti-traversal)
Files->>Disk: écrit les octets
Files-->>Ctrl: SavedUpload (path, size, mime_type)
Navigateur->>Ctrl: GET /media/<path>
Ctrl->>Files: serve_media_file(path, request=request)
Files->>Disk: lit le fichier (par plage si Range)
Files-->>Ctrl: Response (streaming, 200 ou 206)
Ctrl-->>Navigateur: le média
À retenir :
- la validation s'appuie sur le cœur, mais l'écriture vit dans l'opt-in ;
- le nom de fichier est neutralisé avant écriture (anti-traversal) ;
serve_media_filerépond206 Partial Contentsi la requête envoie unRange;- le chemin servi est validé pour rester sous la racine de stockage.
8. API publique
| Élément | Signature | Rôle |
|---|---|---|
save_upload |
save_upload(file, category="documents") -> SavedUpload |
valide, sécurise et écrit un upload |
SavedUpload |
dataclass | filename, original_name, path, category, size, mime_type, variants |
serve_media_file |
serve_media_file(path, *, root=None, request=None) -> Response |
sert un fichier (streaming, HTTP Range) |
delete_upload |
delete_upload(path) -> bool |
supprime un fichier uploadé |
delete_media_file |
delete_media_file(path, *, root=None, variants=False) -> dict |
supprime un média et ses variantes |
get_upload_path |
get_upload_path(filename, category="documents") -> Path |
chemin disque d'un fichier |
upload_root |
upload_root() -> Path |
racine de stockage (UPLOAD_ROOT) |
| primitives stockage | secure_filename, save_bytes, is_safe_media_path, normalize_media_path, delete_file |
briques bas niveau (ADR-020) |
| rate-limit | is_upload_rate_limited, record_upload_attempt |
limiter le débit d'upload |
| exceptions | UploadError, UploadInvalidExtensionError, UploadInvalidMimeTypeError, UploadTooLargeError, UploadStorageError |
erreurs d'upload (réexportées du cœur) |
category range le fichier dans un sous-dossier (documents, images...).
file est un objet d'upload duck-typé (champ multipart, fichier Python, wrapper applicatif).
9. Contextes d'utilisation
| Besoin | Élément |
|---|---|
| Enregistrer un fichier reçu | save_upload(file, category=...) |
| Servir un fichier | serve_media_file(path, request=request) |
| Supprimer un fichier | delete_upload(path) |
| Connaître la racine de stockage | upload_root() ou UPLOAD_ROOT |
| Sécuriser un nom de fichier | secure_filename(name) |
| Limiter les uploads abusifs | is_upload_rate_limited(...) |
| Gérer un upload refusé | intercepter UploadError |
10. Exemples d'utilisation
8.1 Enregistrer un upload depuis un contrôleur¶
from core.http.request import Request
from core.http.response import Response
from forge_mvc_files import save_upload, UploadError
def upload(request: Request) -> Response:
file = request.file("document")
try:
saved = save_upload(file, category="documents")
except UploadError as exc:
return Response.text(f"Upload refusé : {exc}", status=400)
return Response.text(f"Reçu : {saved.path} ({saved.size} octets)")
8.2 Servir un fichier (avec HTTP Range)¶
from forge_mvc_files import serve_media_file
def media(request: Request) -> Response:
path = request.route("path")
return serve_media_file(path, request=request)
En passant request, le service honore l'en-tête Range et répond 206 Partial Content quand le client demande une plage.
Aide-mémoire
Deux verbes pour le cycle de vie d'un fichier :
save_uploadpour entrer (valide, sécurise, écrit) ;serve_media_filepour sortir (streaming, Range).
11. Sécurité, stockage et validation
Les noms de fichiers fournis par le navigateur ne sont jamais utilisés tels quels : secure_filename neutralise les chemins (anti-traversal).
Le chemin servi par serve_media_file est validé pour rester sous UPLOAD_ROOT (is_safe_media_path).
Ne jamais faire confiance au nom client
Le nom d'origine est conservé dans SavedUpload.original_name pour l'affichage, mais le nom de stockage est toujours sécurisé.
Contrôlez extension, type MIME, taille et emplacement final (c'est ce que fait save_upload).
Validation au cœur, écriture dans l'opt-in
La validation pure (extension, MIME, taille) vit dans core.forms.upload_validation et est réexportée ici (ADR-019).
L'écriture, le service et le rate-limit vivent dans l'opt-in.
Le cœur ne dépend pas de forge-mvc-files (ADR-004).
Configuration du stockage
La racine de stockage est UPLOAD_ROOT (défaut storage/uploads).
Seul upload_max_size reste une config du cœur (ADR-032) ; le reste est lu par l'opt-in depuis l'environnement.
Voir aussi¶
- Upload générique (manager.py) : détail de
save_upload/serve_media_file. - Primitives de stockage (storage.py) : anti-traversal, écriture bas niveau (ADR-020).
- Rate-limit d'upload (rate_limit.py) : limiter le débit.
- Welcome-Files : parcours d'apprentissage.