Pourquoi la gestion des fichiers est cruciale
Les serveurs lisent et écrivent des fichiers en permanence : configurations, uploads, logs, caches, rapports générés et sorties de build. Le module node:fs de Node.js en est l’interface, et la manière dont vous l’utilisez détermine si votre serveur reste réactif et si vos données sont sécurisées.
Trois points sont essentiels. Utilisez l’API de promesses pour ne pas bloquer l’event loop. Construisez vos chemins de fichiers de manière sécurisée pour éviter que les entrées utilisateur ne puissent sortir d’un répertoire. Et utilisez les streams pour les fichiers volumineux afin que la consommation mémoire ne grimpe pas avec la taille du fichier.
Lire et écrire des fichiers
L’API basée sur les promesses est le standard pour le code applicatif.
// files.js
import { readFile, writeFile, appendFile } from "node:fs/promises";
// text
const text = await readFile("notes.md", "utf8");
await writeFile("notes.md", text + "\nnew line\n", "utf8");
await appendFile("app.log", "event\n", "utf8");
// binary
const image = await readFile("photo.png"); // Buffer
await writeFile("copy.png", image);
Passez un encodage tel que "utf8" pour obtenir une chaîne de caractères ; omettez-le pour obtenir un Buffer d’octets bruts. writeFile remplace le fichier, tandis que appendFile ajoute du contenu à celui-ci. Aucun des deux ne crée les répertoires manquants, appelez donc mkdir au préalable si nécessaire.
Répertoires
// dirs.js
import { mkdir, readdir, rm, rename, stat } from "node:fs/promises";
await mkdir("data/cache", { recursive: true });
const entries = await readdir("data", { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) console.log("dir", entry.name);
}
const info = await stat("data/cache");
console.log(info.size, info.mtime);
await rename("data/tmp.json", "data/final.json");
await rm("data/old", { recursive: true, force: true });
recursive: true permet à mkdir et rm d’opérer sur des arbres complets, et withFileTypes renvoie des entrées que vous pouvez tester sans appels stat supplémentaires. Utilisez stat pour vérifier l’existence, la taille et les horodatages, mais gardez à l’esprit qu’il suit les liens symboliques ; lstat ne le fait pas.
Chemins (Paths)
Le module node:path gère la manipulation des chemins de manière portable.
// paths.js
import path from "node:path";
const file = path.join("uploads", "avatars", "ada.png");
const absolute = path.resolve("uploads", "ada.png");
const name = path.basename(file); // ada.png
const ext = path.extname(file); // .png
const dir = path.dirname(file); // uploads/avatars
Utilisez path.join et path.resolve plutôt que la concaténation de chaînes, et import.meta.dirname (ou import.meta.url sur les versions plus anciennes) pour localiser des fichiers relativement au module actuel. Sous Windows, path utilise des barres obliques inverses (backslashes), c’est pourquoi la concaténation manuelle pose problème.
Sécurité : path traversal
La vulnérabilité de fichier la plus courante consiste à laisser une entrée utilisateur s’échapper du répertoire prévu.
// safe-path.js
import path from "node:path";
const BASE = "/srv/uploads";
function safePath(name) {
const target = path.resolve(BASE, name);
if (target !== BASE && !target.startsWith(BASE + path.sep)) {
throw new Error("Invalid path");
}
return target;
}
Résolvez l’entrée par rapport à une base fixe, puis vérifiez que le résultat se trouve toujours à l’intérieur de celle-ci. Validez également les noms de fichiers, rejetez les octets nuls et n’exposez jamais les erreurs brutes du système de fichiers aux clients. Consultez le guide de sécurité Web.
Écritures atomiques
Si un plantage survient en cours d’écriture, un lecteur peut se retrouver avec un fichier partiellement écrit. Pour l’état et la configuration, écrivez dans un fichier temporaire puis renommez-le pour remplacer le fichier original.
// atomic.js
import { writeFile, rename } from "node:fs/promises";
import { randomUUID } from "node:crypto";
const tmp = `/srv/state/.${randomUUID()}.tmp`;
await writeFile(tmp, JSON.stringify(state), "utf8");
await rename(tmp, "/srv/state/current.json");
rename est atomique sur un même système de fichiers, ainsi les lecteurs voient toujours soit l’ancien fichier, soit le nouveau dans son intégralité.
Streaming de fichiers volumineux
Pour tout contenu potentiellement volumineux, privilégiez le streaming plutôt que la mise en mémoire tampon (buffering).
// stream.js
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
await pipeline(
createReadStream("big.csv"),
createWriteStream("copy.csv"),
);
Le streaming permet de maintenir une consommation mémoire stable et vous permet de commencer le traitement avant même que l’intégralité du fichier ne soit lue. Le guide sur les Streams traite des transformations, de la backpressure et du mode objet.
Surveiller les fichiers
// watch.js
import { watch } from "node:fs";
const watcher = watch("./config", { recursive: true }, (event, filename) => {
console.log(event, filename);
});
process.on("SIGINT", () => watcher.close());
Les watchers sont idéaux pour les serveurs de développement et le rechargement de configurations. Utilisez le debounce pour gérer les rafales d’événements, gérez les suppressions et les renommages, et pensez à toujours fermer le watcher pour permettre au processus de s’arrêter.
Bonnes pratiques
- Utilisez
node:fs/promisesdans le code de l’application. - Lisez et écrivez avec un encodage explicite, ou attendez-vous à recevoir un Buffer.
- Construisez vos chemins avec
node:pathet résolvez-les par rapport à une base connue. - Protégez-vous contre les attaques de type “path traversal” sur tout nom de fichier fourni par l’utilisateur.
- Utilisez des flux (streams) pour les fichiers volumineux ; ne mettez en mémoire tampon (buffer) que ce dont vous avez besoin intégralement.
- Écrivez les fichiers d’état de manière atomique en utilisant un fichier temporaire puis en le renommant.
- Fermez les watchers et les descripteurs de fichiers (file handles) pour que les processus puissent s’arrêter proprement.
Erreurs courantes
- Utiliser
readFileSyncdans un gestionnaire de requêtes. - Concaténer des entrées utilisateur dans des chemins de fichiers.
- Supposer qu’un répertoire existe avant d’écrire.
- Lire un fichier volumineux en mémoire, provoquant un plantage lors d’une forte charge.
- Exposer des erreurs brutes du système de fichiers et divulguer des chemins.
- Oublier de fermer les watchers ou les streams.
Et après ?
La manipulation de fichiers est une tâche quotidienne côté serveur. Allez plus loin avec les Streams, comprenez l’ordonnancement dans le guide sur l’Event Loop, et sécurisez vos chemins d’accès avec le guide sur la sécurité Web.