Pfade in Electron sind doppelt heikel: Plattform-Unterschiede (/ vs. \) und User-spezifische Verzeichnisse (~/Library/... vs. %APPDATA%\...). Das path-Modul aus Node abstrahiert die Trennzeichen, app.getPath liefert die richtigen User-Verzeichnisse. Hier alle Standard-Pfade und Patterns.
Standard-Pfade von app.getPath
| Key | macOS | Windows | Linux |
|---|---|---|---|
userData | ~/Library/Application Support/<App> | %APPDATA%\<App>\ | ~/.config/<App>/ |
home | ~ | C:\Users\<user> | ~ |
appData | ~/Library/Application Support/ | %APPDATA% | ~/.config/ |
temp | /tmp/ | %TEMP% | /tmp/ |
downloads | ~/Downloads/ | %USERPROFILE%\Downloads\ | ~/Downloads/ |
documents | ~/Documents/ | ~\Documents\ | ~/Documents/ |
desktop | ~/Desktop/ | ~\Desktop\ | ~/Desktop/ |
pictures | ~/Pictures/ | ~\Pictures\ | ~/Pictures/ |
music | ~/Music/ | ~\Music\ | ~/Music/ |
videos | ~/Movies/ | ~\Videos\ | ~/Videos/ |
logs | ~/Library/Logs/<App>/ | %APPDATA%\<App>\logs\ | ~/.config/<App>/logs/ |
exe | App-Executable | App-Executable | App-Executable |
import { app } from 'electron';
const userData = app.getPath('userData');
const downloads = app.getPath('downloads');
const logsDir = app.getPath('logs');userData — der App-spezifische Speicher
userData ist der wichtigste Pfad: hier speichert deine App alles, was zur App gehört (Settings, SQLite-DB, Cache, Logs).
import path from 'node:path';
import { app } from 'electron';
import fs from 'node:fs/promises';
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
await fs.writeFile(settingsPath, JSON.stringify(settings));userData wird automatisch nach productName aus package.json benannt — My App → Ordner My App. Setze productName sorgfältig (mit Spaces, sauber capitalisiert), denn er taucht in Pfaden und Menüs auf.
Mit app.setPath('userData', '/custom/path') (vor app.ready) kann man's überschreiben — selten nötig.
path-Modul — plattformsicheres Joinen
import path from 'node:path';
// path.join — gibt richtigen Trenner pro Plattform
const file = path.join(app.getPath('userData'), 'cache', 'image.png');
// macOS/Linux: '/Users/anna/Library/Application Support/MyApp/cache/image.png'
// Windows: 'C:\\Users\\anna\\AppData\\Roaming\\MyApp\\cache\\image.png'
// path.resolve — absolut, inkl. cwd
const abs = path.resolve('cache/image.png');
// path.dirname / basename / extname
path.dirname('/foo/bar/baz.txt'); // '/foo/bar'
path.basename('/foo/bar/baz.txt'); // 'baz.txt'
path.extname('/foo/bar/baz.txt'); // '.txt'
// Plattform-spezifische Constants
path.sep; // '/' auf macOS/Linux, '\\' auf Windows
path.delimiter; // ':' vs. ';'Niemals Pfade per String-Konkatenation bauen (a + '/' + b) — auf Windows brichst du das. path.join ist der einzige sichere Weg.
Pfad-Traversal verhindern
Wenn der Renderer einen Pfad mitschickt, kann er Path-Traversal versuchen (../../etc/passwd). Schutz:
const ROOT = app.getPath('userData');
function safeUserDataPath(input) {
// resolve + Whitelist
const target = path.resolve(ROOT, input);
if (!target.startsWith(ROOT + path.sep)) {
throw new Error('Path traversal detected');
}
return target;
}
ipcMain.handle('files:read', async (_event, name) => {
const p = safeUserDataPath(name);
return fs.readFile(p, 'utf-8');
});path.resolve löst .. und . auf — danach prüfen, dass der Pfad noch im erlaubten Bereich liegt. Mit path.sep als Suffix verhindert false-positives wie /data/userdata matched /data/userda.
Pfade aus dem App-Bundle
In Production sind Assets im App-Bundle gepackt — typisch in app.asar. Pfade dorthin baust du relativ zur __dirname oder app.getAppPath():
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Asset im selben Ordner wie main.js
const iconPath = path.join(__dirname, 'assets', 'icon.png');
// App-Root (wo package.json liegt)
const appRoot = app.getAppPath();
const templatePath = path.join(appRoot, 'templates', 'invoice.html');app.getAppPath() liefert den App-Root — bei electron-builder typisch <install>/resources/app.asar/. In Dev: das Projekt-Verzeichnis.
Spezialpfad: process.resourcesPath
Für Assets, die NICHT in der app.asar liegen (z. B. Native Binaries, große Files):
// electron-builder „extraResources" liegen in resources/
const ffmpegPath = path.join(process.resourcesPath, 'ffmpeg');In electron-builder.yml:
extraResources:
- from: 'binaries/ffmpeg'
to: 'ffmpeg'Das landet in resources/ffmpeg — über process.resourcesPath zugänglich.
Besonderheiten
NIEMALS Pfade per String-Konkatenation bauen.
path + '/' + name bricht auf Windows. Immer path.join(path, name). Bei jeder Code-Review der Klassiker, der gefunden werden muss.
userData ist DER Pfad für App-Daten.
Settings, SQLite-Datenbank, Cache, Logs — alles in app.getPath('userData'). NICHT in __dirname (read-only nach Packaging) und NICHT in app.getPath('home') (User würde Müll sehen).
productName bestimmt den userData-Ordner.
package.json productName: 'My App' → Ordner heißt My App. Bei späterer Umbenennung verlieren User ihre Settings, weil der neue Name einen neuen Ordner anlegt. Migration vorher planen.
Path-Traversal-Schutz mit resolve + startsWith.
path.resolve(root, userInput) löst .. auf. Danach startsWith(root + path.sep) prüft, dass das Resultat im erlaubten Bereich liegt. Sonst: kompromittierter Renderer kann beliebige Dateien lesen.
app.getPath braucht app.ready.
Vor app.whenReady().then(...) darf getPath für manche Keys nicht aufgerufen werden — nur die System-Pfade (home, temp) sind früh verfügbar, userData etc. braucht ggf. die Init.
process.resourcesPath für Native Binaries.
Wenn deine App externe Tools (ffmpeg, eigene Binaries) braucht: in extraResources packen, mit process.resourcesPath zugreifen. NICHT in app.asar — dort sind sie als Read-Only und nicht direkt ausführbar.