Implementing File System Access in Desktop Applications
File system access is one of the main advantages of desktop apps over web. Electron provides full access via Node.js; Tauri through Rust commands with explicit permissions.
Electron: File System Service
// main/fs-service.js
const fs = require('fs/promises');
const path = require('path');
const { app, dialog } = require('electron');
class FileSystemService {
async openFileDialog(win, options = {}) {
const result = await dialog.showOpenDialog(win, {
properties: ['openFile'],
filters: options.filters ?? [{ name: 'All Files', extensions: ['*'] }],
...options
});
if (result.canceled) return null;
return this.readFile(result.filePaths[0]);
}
async openFolderDialog(win) {
const result = await dialog.showOpenDialog(win, {
properties: ['openDirectory']
});
if (result.canceled) return null;
return result.filePaths[0];
}
async readFile(filePath) {
const stat = await fs.stat(filePath);
if (stat.size > 50 * 1024 * 1024) {
throw new Error(`File too large: ${(stat.size / 1024 / 1024).toFixed(1)} MB`);
}
const content = await fs.readFile(filePath, 'utf-8');
return {
path: filePath,
name: path.basename(filePath),
ext: path.extname(filePath).slice(1),
content,
size: stat.size,
modified: stat.mtimeMs
};
}
async saveFile(win, content, currentPath = null) {
let savePath = currentPath;
if (!savePath) {
const result = await dialog.showSaveDialog(win, {
defaultPath: path.join(app.getPath('documents'), 'untitled.txt')
});
if (result.canceled) return null;
savePath = result.filePath;
}
await fs.writeFile(savePath, content, 'utf-8');
return savePath;
}
async listDirectory(dirPath, options = {}) {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
const items = await Promise.all(
entries
.filter(e => options.showHidden || !e.name.startsWith('.'))
.map(async (entry) => {
const fullPath = path.join(dirPath, entry.name);
try {
const stat = await fs.stat(fullPath);
return {
name: entry.name,
path: fullPath,
isDirectory: entry.isDirectory(),
size: entry.isFile() ? stat.size : 0,
modified: stat.mtimeMs
};
} catch {
return null;
}
})
);
return items.filter(Boolean).sort((a, b) => {
if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
return a.name.localeCompare(b.name);
});
}
watchFile(filePath, callback) {
const watcher = require('fs').watch(filePath, (eventType) => {
callback({ eventType, path: filePath });
});
return () => watcher.close();
}
getAppPaths() {
return {
userData: app.getPath('userData'),
documents: app.getPath('documents'),
downloads: app.getPath('downloads'),
temp: app.getPath('temp'),
home: app.getPath('home')
};
}
}
module.exports = new FileSystemService();
Drag & Drop Files
// renderer — handling drop
const dropZone = document.getElementById('drop-zone');
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragging');
});
dropZone.addEventListener('drop', async (e) => {
e.preventDefault();
dropZone.classList.remove('dragging');
const files = Array.from(e.dataTransfer.files).map(f => ({
name: f.name,
path: f.path, // Electron adds .path
size: f.size,
type: f.type
}));
for (const file of files) {
const content = await window.electronAPI.fs.readFile(file.path);
handleFile(content);
}
});
Tauri: File System via Plugin
# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2"
// renderer/api/fs.ts
import { readTextFile, writeTextFile, readDir } from '@tauri-apps/plugin-fs';
import { open, save } from '@tauri-apps/plugin-dialog';
export async function openAndReadFile() {
const selected = await open({
multiple: false,
filters: [{ name: 'Text', extensions: ['txt', 'md', 'json'] }]
});
if (!selected) return null;
const content = await readTextFile(selected as string);
return { path: selected as string, content };
}
export async function saveToFile(content: string, currentPath?: string) {
const filePath = currentPath ?? await save({
filters: [{ name: 'Text', extensions: ['txt'] }]
});
if (!filePath) return null;
await writeTextFile(filePath as string, content);
return filePath;
}
Security note: Never trust paths from renderer without validation in main process. Path traversal (../../etc/passwd) is a real attack vector in Electron apps rendering remote content.







