---
title: "GridFS depuis Bun, sans cérémonie"
description: "Pourquoi j'ai écrit bun-gridfs-storage, ce que l'ancien moteur Multer faisait mal sous Bun, et les quatre décisions qui ont rendu les uploads ennuyeux à nouveau."
date: 2026-08-30
tags: ["bun", "mongodb", "fichiers"]
language: fr
canonical: https://aissamirhir.com/fr/blog/gridfs-from-bun
source: aissamirhir.com
---
Chaque produit que j'ai livré ces quatre dernières années stocke des fichiers quelque part, et chacun a commencé par la même conversation : stockage objet, ou la base que nous faisons déjà tourner ? Pour Keforo, la réponse a été MongoDB. Les fichiers étaient petits, ils appartenaient à des documents qui vivaient déjà là, et une seule sauvegarde couvrait tout. Cette décision était facile. Faire fonctionner les uploads sous Bun ne l'était pas.

## Ce qui a cassé

L'écosystème Node a une réponse à cela : `multer` analyse le corps multipart, et un moteur de stockage décide où va chaque fichier. `multer-gridfs-storage` était ce moteur pour MongoDB, avec des années de production derrière lui.

Sous Bun, il tombait à deux endroits. Sa gestion des flux supposait des internes de Node que l'implémentation de Bun ne respecte pas, et son câblage d'`EventEmitter` émettait avant que les écouteurs soient attachés. Les uploads restaient suspendus, ou se résolvaient avec un enregistrement de fichier qui ne pointait vers rien. Le paquet n'avait pas connu de version depuis longtemps, et je ne voulais pas d'un fork avec des correctifs que personne d'autre n'exécuterait.

J'ai donc écrit un moteur de stockage qui fait exactement une chose et la fait de la même façon sur les deux runtimes. C'est devenu [bun-gridfs-storage](https://www.npmjs.com/package/bun-gridfs-storage).

## Ce qu'un moteur de stockage doit vraiment faire

Le contrat de Multer est petit. Un moteur reçoit la requête et le flux du fichier entrant, l'écrit quelque part, puis rappelle avec ce qui doit se retrouver sur `req.file`. Pour GridFS, cela veut dire ouvrir un flux d'upload sur un bucket et y diriger le flux.

```typescript title="upload.ts"
import { BunGridFSStorage } from 'bun-gridfs-storage'
import multer from 'multer'
import mongoose from 'mongoose'

await mongoose.connect(process.env.MONGO_URL!)

const storage = new BunGridFSStorage({
  db: mongoose.connection.db,
  file: (req, file) => ({
    filename: `${Date.now()}-${file.originalname}`,
    bucketName: 'uploads',
  }),
})

const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 } })

app.post('/upload', upload.single('file'), (req, res) => {
  res.json({ file: req.file })
})
```

Tout le reste du paquet existe pour rendre cela ennuyeux en production. Quatre décisions ont fait l'essentiel du travail.

## 1. La base peut arriver en retard

Le moteur de stockage est généralement construit au chargement du module, bien avant que la connexion soit ouverte. Plutôt que d'imposer un ordre de démarrage, `db` accepte soit un `Db`, soit une `Promise<Db>`. Les uploads qui arrivent avant que la promesse soit résolue l'attendent.

```typescript
const storage = new BunGridFSStorage({
  db: getDb(),
  file: (req, file) => ({ filename: file.originalname, bucketName: 'uploads' }),
})

async function getDb() {
  if (mongoose.connection.readyState === 1) return mongoose.connection.db
  return new Promise((resolve, reject) => {
    mongoose.connection.once('open', () => resolve(mongoose.connection.db))
    mongoose.connection.once('error', reject)
  })
}
```

## 2. Un seul rappel décide de tout pour un fichier

Nom de fichier, bucket, taille de chunk, type de contenu et métadonnées viennent tous du même rappel `file`, qui peut être asynchrone. C'est là qu'une vraie application met l'identifiant de l'utilisateur, le tenant, ou un hash de contenu calculé ailleurs.

```typescript
const storage = new BunGridFSStorage({
  db: mongoose.connection.db,
  file: async (req, file) => ({
    filename: `${crypto.randomUUID()}-${file.originalname}`,
    bucketName: 'uploads',
    chunkSize: 255 * 1024,
    contentType: file.mimetype,
    metadata: {
      userId: req.user.id,
      originalName: file.originalname,
    },
  }),
})
```

> [!NOTE]
> La taille de chunk par défaut est de 255 Ko, qui est aussi la valeur par défaut de MongoDB. Des chunks plus grands signifient moins de documents par fichier mais plus de mémoire par upload en cours. Je n'ai jamais eu besoin de la changer ; l'option existe parce que quelqu'un en aura besoin.

## 3. Des événements, pas des logs

Le moteur étend `EventEmitter` et émet `connection`, `file`, `streamError` et `connectionFailed`. Cela semble anodin jusqu'au jour où il faut compter les uploads échoués par tenant, ou lancer une analyse antivirus à l'instant où un fichier arrive, sans toucher au gestionnaire de route.

```typescript
storage.on('file', (file) => metrics.increment('uploads', { bucket: file.bucketName }))
storage.on('streamError', (error, config) => log.error({ error, config }, 'upload failed'))
```

## 4. Bun d'abord, Node quand même

Les tests tournent sous `bun test`, et le build livre CommonJS et ESM. Node 18 et suivants fonctionnent sans changement. Je ne veux pas de deux histoires d'upload dans une même base de code parce qu'un service tourne sur un autre runtime.

## Relire les fichiers

Téléchargements et suppressions passent directement par le bucket du driver. Le moteur l'expose pour que vous n'en construisiez jamais un second.

```typescript
app.get('/files/:name', (req, res) => {
  const bucket = storage.getBucket()
  if (!bucket) return res.status(503).send('storage not ready')
  bucket
    .openDownloadStreamByName(req.params.name)
    .on('error', () => res.status(404).end())
    .pipe(res)
})
```

## Quand GridFS est la mauvaise réponse

Si vos fichiers sont volumineux, publics ou servis à de nombreux lecteurs, un CDN devant un stockage objet fera mieux à chaque fois, et pour moins cher. GridFS mérite sa place quand les fichiers sont privés, de taille modeste, liés à des documents que vous interrogez déjà, et que vous voulez une seule sauvegarde, un seul modèle de contrôle d'accès et une seule chaîne de connexion. C'était le cas de chaque upload Keforo, et c'est pour cela que le paquet existe.
