Connecteurs de stockage de fichiers
Résumé. Les fichiers vivent derrière le port FileStorage du kernel. Le kit livre trois
connecteurs tenus à une même suite de contrat (fileStorageContract de
@ddd-ts-kit/testing/contracts) : la mémoire du processus pour les tests, un répertoire local pour
le développement, et un bucket compatible S3 (@ddd-ts-kit/s3, sur le SDK AWS v3 officiel) pour
un déploiement, qui couvre AWS S3, MinIO, OVHcloud, Scaleway, Cloudflare R2 et Garage. Google Cloud
Storage et Azure Blob Storage s'atteignent en écrivant un adaptateur qui passe la même suite.
Choisissez le connecteur S3 pour tout ce qui est déployé ; gardez le répertoire pour la machine
d'un développeur.
Les connecteurs
Section intitulée « Les connecteurs »Les cas d'usage connaissent un seul port, FileStorage ; la plateforme lie un connecteur au
démarrage, et une seule suite de contrat tient chaque connecteur au même comportement. Changer de
connecteur ne change aucune ligne de la base non plus : l'agrégat garde un StoredFile (clé, taille, type de
média, empreinte), qui ne nomme ni fournisseur, ni bucket, ni URL.
InMemoryFileStorage |
LocalFileStorage |
S3FileStorage (@ddd-ts-kit/s3) |
|
|---|---|---|---|
| Pour | les tests, les outils | le développement, une machine | tout déploiement |
| Où est le contenu | une map dans le processus | un répertoire, métadonnées dans .metadata/ |
un bucket, sous un préfixe facultatif |
| Flux | en entrée, puis gardé en mémoire | en entrée et en sortie, une écriture renommée à sa place | envoi multipart en parties de 8 Mio, une seule en mémoire ; un flux en sortie |
| Liens signés | jetons auxquels répond signedFileRoutes |
jetons auxquels répond signedFileRoutes |
présignés par le service |
| Plusieurs instances | non | non | oui |
| Empreinte gardée | oui | oui | répondue par put, non gardée |
| Copie, déplacement | en mémoire | système de fichiers | CopyObject, 5 Gio par objet |
Lié par init |
dans les tests et les outils, quel que soit stack.storage |
stack.storage: local |
stack.storage: s3, MinIO dans la stack compose |
La ligne « Liens signés » est la différence qui compte le plus une fois déployé. La mémoire et le
répertoire sont des stockages servis (ServedFileStorage) : un lien qu'ils signent pointe vers
l'application, qui doit y répondre, si bien que chaque octet passe par l'API. Un bucket répond à
ses propres liens présignés : l'API ne fait que signer, sans aucune requête au bucket, et le client
télécharge ou envoie directement vers le stockage.
init écrit les fichiers de la plateforme pour la stack qu'on lui a donnée ; côte à côte, les
deux choix et les routes qui répondent aux liens d'un stockage servi :
import { signedFileRoutes, type Route } from '@ddd-ts-kit/http';import { LocalFileStorage, ServedFileStorage, type FileStorage } from '@ddd-ts-kit/kernel';import { S3FileStorage, s3ClientOf, type S3Settings } from '@ddd-ts-kit/s3';
// platform/files/open-files.ts avec stack.storage: local (FILES_DIRECTORY, FILES_PUBLIC_URL, FILES_SIGNING_SECRET)export function openDirectory(config: { readonly directory: string; readonly publicUrl: string; readonly signingSecret?: string;}): FileStorage { const { directory, publicUrl, signingSecret } = config;
// sans secret, un secret est tiré à chaque démarrage, et les liens meurent avec le processus return new LocalFileStorage({ directory, baseUrl: publicUrl, ...(signingSecret === undefined ? {} : { secret: signingSecret }), });}
// platform/files/open-files.ts avec stack.storage: s3 (les variables S3_*)export function openBucket(config: S3Settings & { readonly bucket: string }): FileStorage { const { bucket, ...settings } = config;
return new S3FileStorage({ client: s3ClientOf(settings), bucket });}
// platform/files/file-routes.ts : GET et PUT /files?token= pour un stockage servi, rien pour un bucketexport function fileRoutes(storage: FileStorage): readonly Route[] { return storage instanceof ServedFileStorage ? signedFileRoutes(storage) : [];}Fournisseurs du protocole S3
Section intitulée « Fournisseurs du protocole S3 »| Fournisseur | Endpoint | Remarques |
|---|---|---|
| AWS S3 | aucun | la référence ; région du bucket |
| MinIO | http://localhost:9000 en développement |
adressage par chemin (path-style) ; le projet amont a cessé de publier des images en 2025 : le pipeline du kit et sa stack compose exécutent pgsty/minio, construit à partir des mêmes sources, épinglé par digest |
| OVHcloud Object Storage | https://s3.<region>.io.cloud.ovh.net |
hébergement européen |
| Scaleway Object Storage | https://s3.<region>.scw.cloud |
hébergement européen |
| Cloudflare R2 | https://<account>.r2.cloudflarestorage.com |
région auto ; pas de frais de sortie |
| Garage | le vôtre | auto-hébergé, faible empreinte |
s3ClientOf(settings) construit un client pour n'importe lequel d'entre eux : adressage par chemin
avec un endpoint, et des sommes de contrôle seulement quand une opération en exige une, puisque le
comportement par défaut des SDK récents (une somme de contrôle sur chaque requête) est refusé par
plusieurs fournisseurs qui ne sont pas AWS et lierait un lien d'envoi signé à un corps vide.
Passer d'un fournisseur à un autre change l'environnement, pas le code. La plateforme d'un projet
généré avec --storage s3 lit cinq variables ; ce qui change, c'est l'endpoint et la région (le
bucket et sa clé d'accès sont créés par le code d'infrastructure de chacun) :
# AWS S3 : pas d'endpoint, la région du bucketS3_REGION=eu-west-3
# MinIO de la stack compose, en développementS3_ENDPOINT=http://localhost:9000S3_REGION=us-east-1
# OVHcloud Object Storage, GravelinesS3_ENDPOINT=https://s3.gra.io.cloud.ovh.netS3_REGION=gra
# Scaleway Object Storage, ParisS3_ENDPOINT=https://s3.fr-par.scw.cloudS3_REGION=fr-par
# Cloudflare R2S3_ENDPOINT=https://<account>.r2.cloudflarestorage.comS3_REGION=auto
# et, dans tous les casS3_BUCKET=acme-filesS3_ACCESS_KEY_ID=...S3_SECRET_ACCESS_KEY=...Ce que fait put du contenu en route vers un bucket : il est compté et haché au fil du flux,
envoyé en parties, et refusé au-delà de sa limite sans rien laisser derrière lui.
Critères de choix
Section intitulée « Critères de choix »- Plus d'une instance, ou un conteneur qui est remplacé : un stockage objet. Un répertoire est perdu avec sa machine et n'est pas partagé.
- Où les données peuvent vivre : un fournisseur dans la région que nomment vos obligations ; le bucket, son chiffrement, son versionnement et son cycle de vie sont du code d'infrastructure, pas du code applicatif.
- Sortie de données : des fichiers souvent téléchargés coûtent chez un fournisseur qui facture la sortie ; R2 ne la facture pas.
- Auto-hébergement : MinIO ou Garage, chacun avec sa charge d'exploitation.
Ce que le kit prend en charge et a vérifié
Section intitulée « Ce que le kit prend en charge et a vérifié »- La suite de contrat s'exécute sur
InMemoryFileStorageetLocalFileStoragedans l'exécution unitaire, et surS3FileStoragecontre un vrai serveur MinIO dans le job d'intégration du pipeline (pipeline/github/scripts/start-minio.sh, un service du job GitLab) : aller-retour avec taille, type de média et empreinte, six mébioctets transmis en flux sur plusieurs parties, une écriture refusée au-delà de sa limite qui laisse l'objet précédent et aucune partie derrière elle, des clés qui pourraient sortir de leur place refusées, copie, déplacement, listage, des liens signés qui expirent, qui refusent une signature modifiée ou un autre type de média. init --storage local|s3câble la plateforme :platform/files/lie les ports, enregistre la purge horaire des fichiers mis en attente, monte les routes qui répondent aux liens d'un stockage servi et le lecteur d'envois ;generate file-uploadécrit le premier envoi.- Non vérifié : les autres fournisseurs du tableau, qui parlent le même protocole ; un fournisseur
dont l'envoi présigné ignore le
content-typesigné.
Un autre fournisseur : Google Cloud Storage, Azure Blob Storage
Section intitulée « Un autre fournisseur : Google Cloud Storage, Azure Blob Storage »Un connecteur est une classe de la plateforme (ou un package à vous) qui étend FileStorage et
passe la suite de contrat contre le vrai service ou son émulateur :
// platform/files/gcs-file-storage.ts : put, écrit comme S3FileStorage l'écritexport class GcsFileStorage extends FileStorage { constructor(private readonly bucket: Bucket) { super(); }
async put(key: string, content: FileSource, options: PutOptions): Promise<StoredFile> { const mediaType = mediaTypeOf(options.mediaType); // compte et hache le contenu au passage, et échoue avec FILE_TOO_LARGE au-delà de maxBytes const inspection = new FileInspection(options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes }); const upload = this.bucket.file(assertFileKey(key)).createWriteStream({ contentType: mediaType, resumable: true });
await pipeline(content, inspection, upload);
return { key, size: inspection.size, mediaType, checksum: inspection.checksum }; }
// get, stat, delete, copy, move, list, signedDownloadUrl (URL signée v4), signedUploadUrl (v4, content-type signé)}FileInspection, assertFileKey, assertExpiry, mediaTypeOf et contentDisposition sont
exportés par @ddd-ts-kit/kernel pour les connecteurs : les règles que partagent tous les
connecteurs sont écrites une seule fois. Ensuite, la suite de contrat s'exécute contre l'émulateur,
dans les specs d'intégration :
import { beforeEach, describe, expect, it } from 'vitest';import type { FileStorage } from '@ddd-ts-kit/kernel';import { fileStorageContract } from '@ddd-ts-kit/testing/contracts';
// platform/files/gcs-file-storage.ts, le connecteur ci-dessusdeclare const GcsFileStorage: new (bucketName: string) => FileStorage;// un bucket de fake-gcs-server (Azurite pour Azure), neuf pour chaque testdeclare function freshBucket(): string;
// test/integration/gcs-file-storage.contract.spec.tsfileStorageContract( { describe, it, expect, beforeEach }, { name: 'GcsFileStorage', createStorage: () => new GcsFileStorage(freshBucket()), // les liens d'un vrai service expirent selon sa propre horloge : la suite attend réellement elapse: (ms) => new Promise<void>((resolve) => setTimeout(resolve, ms)), },);Un connecteur dont les liens pointent vers l'application (un stockage servi) passe aussi
follow: followServed(storage), pour que la suite suive ses liens par openSigned et
acceptSigned au lieu de fetch ; le kit exécute ainsi ses connecteurs mémoire et répertoire,
avec une FixedClock avancée par elapse.
Ce à quoi la suite tient un connecteur : une écriture refusée (FILE_TOO_LARGE) ne laisse rien de
nouveau et garde le contenu précédent, donc écrivez dans un objet temporaire ou abandonnez
l'envoi ; un fichier absent répond undefined à get et stat, et FILE_NOT_FOUND à copy et
move ; une clé invalide est refusée avant toute requête (assertFileKey) ; les liens durent au
plus une heure (assertExpiry) ; un lien d'envoi est lié à son type de média ; un lien de
téléchargement est une pièce jointe, nommée ou non. Azure signe avec une SAS dont le
content-type ne peut pas être lié : vérifiez le type à FileStaging.confirm, par lequel passe de
toute façon chaque envoi direct.
Limites, et quand ne pas l'utiliser
Section intitulée « Limites, et quand ne pas l'utiliser »- Aucun connecteur ne fait d'une écriture une partie d'une transaction de base de données :
FileStagingordonne le travail, la purge nettoie. - Le répertoire local et le connecteur mémoire répondent à leurs liens par l'application : avec
Express, les routes sont montées avant
express.json(); derrière NestJS, son propre analyseur de corps lit d'abord un fichier JSON envoyé par un lien. - Les fichiers qui ne doivent pas être supprimés pendant une période (conservation légale) ont besoin du verrouillage d'objet (object lock) sur le bucket, réglé par le code d'infrastructure.
- La diffusion de contenu vers de nombreux lecteurs (images d'un site public) revient à un CDN devant le bucket, pas aux liens signés de l'application.
Recommandation selon la situation
Section intitulée « Recommandation selon la situation »| Situation | Choix |
|---|---|
| Un nouveau projet | init (répertoire local), puis init --force --storage s3 avant le premier déploiement |
| Déployé sur AWS | S3FileStorage, sans endpoint |
| Hébergement européen exigé | OVHcloud ou Scaleway par S3FileStorage |
| Beaucoup de téléchargements | Cloudflare R2 par S3FileStorage |
| Auto-hébergé | MinIO (pgsty/minio) ou Garage par S3FileStorage |
| Google Cloud ou Azure seulement | un adaptateur à vous tenu à fileStorageContract |
- SDK AWS pour JavaScript v3,
@aws-sdk/client-s3,@aws-sdk/lib-storage,@aws-sdk/s3-request-presigner: 3.1150.0, vérifié avecnpm viewle 2026-10-12. - Serveur MinIO, image
pgsty/minio:RELEASE.2026-08-04T00-00-00Z(sha256:b6bfe7239bfc83fb90d31612d9704d86039dd714f7904b3f1ad68f211e602372) ;minio/miniorépondait « denied » sur Docker Hub et aucune étiquette sur Quay le 2026-10-12. - busboy 1.6.0, l'analyseur multipart de
@ddd-ts-kit/http/uploads.
Voir aussi
Section intitulée « Voir aussi »- Stockage de fichiers : le concept, l'ordre de cohérence, la sécurité.
- Persistance : ce qui va, lui, dans la base de données.
@ddd-ts-kit/s3.