Warum die Dateiverarbeitung wichtig ist
Server lesen und schreiben ständig Dateien: Konfigurationen, Uploads, Logs, Caches, generierte Berichte und Build-Outputs. Das node:fs-Modul von Node.js ist die entsprechende Schnittstelle, und die Art und Weise, wie Sie es einsetzen, entscheidet darüber, ob Ihr Server reaktionsfähig bleibt und Ihre Daten sicher sind.
Drei Dinge sind dabei besonders wichtig: Nutzen Sie die Promise API, damit der Event Loop nicht blockiert wird. Erstellen Sie Pfade sicher, damit Benutzereingaben nicht aus einem Verzeichnis ausbrechen können. Und verwenden Sie Streams für große Dateien, damit der Speicherverbrauch nicht mit der Dateigröße ansteigt.
Dateien lesen und schreiben
Die Promise-API ist der Standard für den Anwendungscode.
// 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);
Übergeben Sie eine Kodierung wie "utf8", um einen String zu erhalten; lassen Sie diese weg, um ein Buffer mit rohen Bytes zu erhalten. writeFile ersetzt die Datei, während appendFile Inhalte hinzufügt. Keine der beiden Methoden erstellt fehlende Verzeichnisse, rufen Sie daher bei Bedarf zuerst mkdir auf.
Verzeichnisse
// 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 sorgt dafür, dass mkdir und rm auf gesamten Verzeichnisbäumen operieren, und withFileTypes gibt Einträge zurück, die ohne zusätzliche stat-Aufrufe geprüft werden können. Verwenden Sie stat, um die Existenz, Größe und Zeitstempel zu prüfen, aber beachten Sie, dass es Symlinks folgt; lstat tut dies nicht.
Pfade
Das node:path Modul ermöglicht die portable Manipulation von Pfaden.
// 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
Verwenden Sie path.join und path.resolve anstelle von String-Konkatenation sowie import.meta.dirname (oder import.meta.url in älteren Versionen), um Dateien relativ zum aktuellen Modul zu lokalisieren. Unter Windows verwendet path Backslashes, weshalb eine manuelle Konkatenation zu Fehlern führt.
Sicherheit: Path Traversal
Die häufigste Dateivulnerabilität besteht darin, dass Benutzereingaben das beabsichtigte Verzeichnis verlassen können.
// 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;
}
Lösen Sie die Eingabe gegen eine feste Basis auf und überprüfen Sie anschließend, ob das Ergebnis immer noch innerhalb dieser Basis liegt. Validieren Sie zudem Dateinamen, lehnen Sie Null-Bytes ab und geben Sie niemals rohe Dateisystemfehler an Clients weiter. Weitere Informationen finden Sie im Web Security Guide.
Atomare Schreibvorgänge
Wenn es während eines Schreibvorgangs zu einem Absturz kommt, kann ein Reader eine halb geschriebene Datei sehen. Schreiben Sie Zustände und Konfigurationen stattdessen in eine temporäre Datei und benennen Sie diese anschließend an die Zielposition um.
// 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 ist auf demselben Dateisystem atomar, sodass Reader immer entweder die alte Datei oder die vollständig neue Datei sehen.
Große Dateien streamen
Verwenden Sie bei allem, was groß werden könnte, Streams anstelle von Buffering.
// stream.js
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
await pipeline(
createReadStream("big.csv"),
createWriteStream("copy.csv"),
);
Streaming hält den Speicherverbrauch konstant und ermöglicht es Ihnen, mit der Verarbeitung zu beginnen, noch bevor die gesamte Datei eingelesen wurde. Der Streams guide behandelt Transforms, Backpressure und den Object Mode.
Dateien überwachen
// watch.js
import { watch } from "node:fs";
const watcher = watch("./config", { recursive: true }, (event, filename) => {
console.log(event, filename);
});
process.on("SIGINT", () => watcher.close());
Watcher eignen sich ideal für Dev-Server und das Neuladen von Konfigurationen. Nutze Debouncing für Event-Bursts, behandle Löschvorgänge sowie Umbenennungen und schließe den Watcher immer ab, damit der Prozess beendet werden kann.
Best Practices
- Verwenden Sie
node:fs/promisesim Anwendungscode. - Lesen und schreiben Sie mit einer expliziten Kodierung oder erwarten Sie einen Buffer.
- Erstellen Sie Pfade mit
node:pathund lösen Sie diese relativ zu einer bekannten Basis auf. - Schützen Sie sich vor Path Traversal bei jedem vom Benutzer bereitgestellten Dateinamen.
- Streamen Sie große Dateien; puffern Sie nur das, was Sie als Ganzes benötigen.
- Schreiben Sie Zustandsdateien atomar mithilfe einer temporären Datei und einem anschließenden Umbenennen.
- Schließen Sie Watcher und File Handles, damit Prozesse sauber beendet werden können.
Häufige Fehler
- Verwendung von
readFileSyncin einem Request-Handler. - Konkatenierung von Benutzereingaben in Pfaden.
- Die Annahme, dass ein Verzeichnis existiert, bevor geschrieben wird.
- Einlesen riesiger Dateien in den Arbeitsspeicher, was unter Last zu Abstürzen führt.
- Ausgeben von rohen Dateisystemfehlern und das damit verbundene Leaken von Pfaden.
- Vergessen, Watcher oder Streams zu schließen.
Wie geht es weiter?
Die Dateiverarbeitung gehört zu den täglichen Aufgaben auf dem Server. Vertiefe dein Wissen mit Streams, verstehe das Scheduling im Event Loop Guide und sichere deine Pfade mit dem Web Security Guide ab.