Datei-Operationen sind eine der Hauptaufgaben im Main-Prozess. Node bringt das fs/promises-Modul mit — modern, async, sauber. Hier die wichtigsten Operationen für Electron-Apps und das Pattern, mit dem der Renderer sicher Datei-Zugriffe anfordert.
Standard-Operationen
import fs from 'node:fs/promises';
// Datei lesen
const text = await fs.readFile('/path/to/file.txt', 'utf-8');
const buf = await fs.readFile('/path/to/binary.bin'); // Buffer
// Datei schreiben
await fs.writeFile('/path/to/out.txt', 'Hallo Welt');
await fs.writeFile('/path/to/out.json', JSON.stringify(data, null, 2));
// Existenz prüfen
try {
await fs.access('/path/to/file');
// existiert
} catch {
// nicht da
}
// Verzeichnis erstellen (rekursiv)
await fs.mkdir('/path/to/nested/dir', { recursive: true });
// Verzeichnis lesen
const entries = await fs.readdir('/path/to/dir');
const detailed = await fs.readdir('/path/to/dir', { withFileTypes: true });
// Datei löschen
await fs.unlink('/path/to/file');
// Verzeichnis löschen (rekursiv)
await fs.rm('/path/to/dir', { recursive: true });
// Datei-Info
const stat = await fs.stat('/path/to/file');
console.log(stat.size, stat.mtime);fs/promises ist Node-Standard — die gleichen APIs, die in Server-Apps und CLI-Tools verwendet werden.
Renderer fragt, Main führt aus
Der Renderer hat keinen direkten Zugriff. Pattern: IPC-Bridge, mit Validierung im Main.
import { app, ipcMain } from 'electron';
import fs from 'node:fs/promises';
import path from 'node:path';
const ALLOWED_ROOT = app.getPath('userData');
function isAllowed(p) {
const resolved = path.resolve(p);
return resolved.startsWith(ALLOWED_ROOT);
}
ipcMain.handle('files:read', async (_event, p) => {
if (!isAllowed(p)) throw new Error('Path not allowed');
return fs.readFile(p, 'utf-8');
});
ipcMain.handle('files:write', async (_event, p, content) => {
if (!isAllowed(p)) throw new Error('Path not allowed');
return fs.writeFile(p, content);
});Pflicht-Punkt: immer Pfad-Validierung. Der Renderer ist nicht trusted — eine kompromittierte Webseite könnte sonst /etc/passwd lesen oder Logout-Skripte schreiben.
Streams für große Dateien
import fs from 'node:fs'; // Stream-API ist im klassischen 'fs', nicht 'fs/promises'
ipcMain.handle('files:read-large', async (event, p) => {
return new Promise((resolve, reject) => {
const stream = fs.createReadStream(p, { encoding: 'utf-8' });
let bytesRead = 0;
stream.on('data', (chunk) => {
bytesRead += chunk.length;
event.sender.send('files:progress', { bytesRead });
});
stream.on('end', () => resolve('ok'));
stream.on('error', reject);
});
});Bei mehreren Hundert MB lohnt sich Streaming — nicht das ganze File auf einmal in den Speicher laden. Mit event.sender.send parallel Progress-Events pushen.
File-Watching
import { watch } from 'node:fs';
const watcher = watch('/path/to/dir', { recursive: true }, (event, filename) => {
console.log(`${event}: ${filename}`);
// event: 'rename' oder 'change'
mainWindow.webContents.send('files:changed', { event, filename });
});
// Cleanup
app.on('before-quit', () => {
watcher.close();
});Plattform-Unterschiede:
- macOS/Linux:
recursive: truefunktioniert (Linux ab Node 20) - Windows:
recursive: truefunktioniert von jeher
Für robustere File-Watcher: chokidar als Bibliothek — abstrahiert Plattform-Eigenheiten.
Atomares Speichern
Wenn deine App eine wichtige Datei speichert (Settings, Dokumente), ist atomares Schreiben Pflicht — sonst riskiert ein Crash mitten im Schreiben eine korrupte Datei.
async function atomicWrite(target, content) {
const tmp = target + '.tmp.' + process.pid;
await fs.writeFile(tmp, content);
await fs.rename(tmp, target);
}
await atomicWrite(configPath, JSON.stringify(settings));Pattern: erst in eine .tmp-Datei schreiben, dann atomar umbenennen. rename ist auf den meisten Filesystems atomar — falls der Prozess in der Mitte stirbt, ist entweder noch die alte oder schon die neue Datei da, nie eine halbe.
Bibliothek dafür: write-file-atomic — dasselbe Pattern als robust getestetes Modul.
Encoding-Eigenheiten
// Default: Buffer (raw bytes)
const buf = await fs.readFile('/path/file.txt');
// Mit Encoding: String
const str = await fs.readFile('/path/file.txt', 'utf-8');
// Klassische Encodings
await fs.readFile('/path/legacy.txt', 'latin1'); // Windows-1252-artig
await fs.readFile('/path/legacy.txt', 'utf16le'); // UTF-16 LE
// BOM beachten — Node entfernt es nicht automatisch
if (str.charCodeAt(0) === 0xfeff) {
return str.slice(1); // BOM weg
}UTF-8 ist heute Standard. Aber bei Windows-spezifischen Tools landest du manchmal in UTF-16 LE oder ANSI — dann explizit Encoding angeben.
Interessantes
fs/promises ist der moderne Default.
import fs from 'node:fs/promises' — alles async, Promise-based. Das alte Callback-API (fs.readFile(path, cb)) gibt's noch, ist aber für neue Apps nicht mehr empfohlen.
Pfad-Validation NICHT im Preload — IMMER im Main.
Renderer-Inputs sind nicht trusted. Validation gehört in den Main-Handler, vor jedem fs-Aufruf. path.resolve plus startsWith(ALLOWED_ROOT) ist das Standard-Pattern.
Atomares Schreiben für wichtige Dateien.
Settings, Dokumente, Konfig — niemals direkt mit writeFile über die alte Datei. Erst .tmp, dann rename. Schützt vor Korruption bei Crash mitten im Schreiben.
Streams für große Dateien — sonst Memory-Spike.
readFile von 1 GB lädt 1 GB in RAM. Mit createReadStream chunkweise — Memory bleibt klein. Bei jedem File-Operation über ~50 MB lohnt's sich.
fs.watch ist plattformabhängig.
Auf Linux war recursive: true lange unpraktikabel. Ab Node 20 funktioniert's, aber mit Performance-Charakteristiken. Für robustes File-Watching plattformübergreifend: chokidar-Library.
Default-Encoding ist Buffer, nicht String.
Ohne Encoding-Argument bekommst du einen Buffer. Beim Logging oder direkten String-Operations: 'utf-8' mitgeben. Sonst: explizit mit .toString('utf-8') konvertieren.