Текстовый редактор на Electron несколько секунд подвисает при открытии файла больше 50 МБ. Пользователи жалуются на лаги при перетаскивании файлов. Причина — неоптимизированный IPC и отсутствие предварительной проверки размера. Оба фреймворка — Electron и Tauri — имеют свои нюансы. Разберёмся, как правильно реализовать доступ к файловой системе в десктоп-приложении, чтобы таких проблем не возникало.
Проблемы, которые решаем
Path traversal — когда renderer отправляет путь вида ../../etc/passwd. В Electron эту проблему решают проверкой на стороне main process. В Tauri — разграничением scope в capabilities/default.json. В одном из проектов мы обнаружили, что клиент передавал абсолютные пути без валидации — исправили за час, но могли потерять данные. Подробнее о Path traversal на Wikipedia.
Неэффективный IPC при больших файлах — передача через ipcRenderer.invoke файлов размером 100+ МБ блокирует рендерер. Решение — использовать стримы или передавать ArrayBuffer через SharedArrayBuffer. В Electron появилась поддержка MessagePort для передачи больших объёмов данных без копирования.
Сложности с мониторингом изменений — fs.watch в Node.js нестабилен на macOS — пропускает события при большом количестве изменений. Альтернатива — polling с интервалом 1000 мс или использование chokidar с настройкой awaitWriteFinish. Для Tauri — плагин tauri-plugin-fs с watch, который использует inotify на Linux и FSEvents на macOS.
Как обеспечить безопасность файлового доступа?
В Electron весь файловый ввод-вывод выполняется в main process. Renderer запрашивает операции через IPC. Это предотвращает прямые атаки из UI. Используйте contextBridge для изоляции — не экспортируйте из preload ничего, кроме безопасных методов. В Tauri модель разрешений ещё строже: каждое действие (чтение, запись, удаление) нужно задекларировать в capabilities/default.json. Укажите scope на конкретные каталоги: app-data, documents, downloads. Никогда не давайте доступ ко всей файловой системе.
Пример настройки permissions в Tauri
{
"permissions": [
"fs:allow-read-text-file",
"fs:allow-write-text-file",
"fs:allow-read-dir",
"fs:allow-watch",
"fs:scope-app-data-recursive",
"fs:scope-document-recursive",
"dialog:allow-open",
"dialog:allow-save"
]
}
Реализация в Electron
Классический подход — класс FileSystemService в main process. Он использует fs/promises и fs для работы с файлами, а также dialog для открытия/сохранения через нативный интерфейс. Важно: предупреждайте пользователя о файлах >50 MB, чтобы не повесить процесс. Для списка файлов используйте фильтр скрытых и сортировку: папки первыми, затем по алфавиту.
// main/fs-service.js
const fs = require('fs/promises');
const fsSync = require('fs');
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 || result.filePaths.length === 0) 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);
let stat;
try { stat = await fs.stat(fullPath); } catch { return null; }
return { name: entry.name, path: fullPath, isDirectory: entry.isDirectory(), size: entry.isFile() ? stat.size : 0, modified: stat.mtimeMs, ext: entry.isFile() ? path.extname(entry.name).slice(1) : null };
}));
return items.filter(Boolean).sort((a, b) => { if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1; return a.name.localeCompare(b.name); });
}
async copyDirectory(src, dest) {
await fs.mkdir(dest, { recursive: true });
const entries = await fs.readdir(src, { withFileTypes: true });
await Promise.all(entries.map(entry => {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
return entry.isDirectory() ? this.copyDirectory(srcPath, destPath) : fs.copyFile(srcPath, destPath);
}));
}
watchFile(filePath, callback) {
const watcher = fsSync.watch(filePath, { persistent: false }, (eventType) => callback({ eventType, path: filePath }));
return () => watcher.close();
}
watchDirectory(dirPath, callback) {
const watcher = fsSync.watch(dirPath, { recursive: true, persistent: false }, (eventType, filename) => {
if (filename) callback({ eventType, path: path.join(dirPath, filename), filename });
});
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 реализуется через обработку события drop в renderer с получением f.path (Electron добавляет это свойство). Чтобы не перегружать main process, передавайте через IPC только метаданные, а чтение выполняйте по запросу.
Реализация в Tauri
Подключаем плагины tauri-plugin-fs и tauri-plugin-dialog. Все операции с файлами требуют явных разрешений в capabilities/default.json.
# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2"
Пример чтения и записи:
// renderer/api/fs.ts
import { readTextFile, writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs';
import { open, save } from '@tauri-apps/plugin-dialog';
export async function openAndReadFile() {
const selected = await open({ multiple: false });
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;
}
Для бинарных файлов используйте readFile и writeFile из того же плагина — они работают с Uint8Array.
Почему Tauri быстрее Electron?
Tauri компилирует бинарник с Rust и использует системный WebView, а не встроенный Chromium. Размер приложения в 2-3 раза меньше, а потребление памяти — до 70% ниже. Однако Tauri требует объявления всех операций в capabilities, что делает код безопаснее, но немного увеличивает время настройки.
Сравнение Electron и Tauri для файлового доступа
| Характеристика | Electron | Tauri |
|---|---|---|
| API | Node.js fs (полный доступ) | tauri-plugin-fs (ограниченный scope) |
| Безопасность | Требуется явное разделение процессов | Модель разрешений из коробки |
| Размер приложения | >100 MB (включая Chromium) | <10 MB |
| Производительность | Средняя (из-за Chromium) | Высокая (нативный код) |
| Работа с бинарными файлами | Через Buffer и ArrayBuffer | Через Uint8Array, конвертация |
| Мониторинг файлов | fs.watch / fs.watchFile | tauri-plugin-fs watch |
| Кросс-платформенность | Windows, macOS, Linux | Windows, macOS, Linux (iOS, Android в разработке) |
Процесс и объём работы
- Аналитика — изучаем ваше текущее приложение, требования к файловому доступу, определяем уязвимости. Создаём карту потока данных.
- Проектирование — разрабатываем схему IPC или permissions, выбираем библиотеки, проектируем API.
- Реализация — пишем код с модульным тестированием (jest для Node.js, vitest для Tauri). Используем Repository pattern для изоляции бизнес-логики.
- Тестирование — проверяем на реальных файлах (до 10 ГБ), имитируем атаки path traversal, тестируем на Windows, macOS, Linux.
- Деплой — собираем бинарники, настраиваем auto-update, документируем API.
В работе используем: Electron 28+, Tauri 2, TypeScript, ESLint, Prettier, Husky.
Кейс: для одного стартапа мы реализовали файловый менеджер на Electron с возможностью открытия файлов до 4 ГБ без подвисаний. Решение — чанковая передача через IPC с прогресс-баром и прерыванием по требованию. Время загрузки файла 500 МБ сократилось с 12 до 3 секунд. Закажите аудит вашего приложения — выявим уязвимости за 1 день.
Ориентировочные сроки
| Этап | Время |
|---|---|
| Анализ и проектирование | 1 день |
| Реализация базового доступа (чтение/запись/диалоги) | 2-3 дня |
| Мониторинг и бинарные файлы | 1-2 дня |
| Тестирование и отладка | 1-2 дня |
| Интеграция в существующий проект | от 1 дня |
Стоимость рассчитывается индивидуально. Свяжитесь с нами, чтобы получить консультацию. Закажите разработку файлового доступа — оценим проект за один день.







