Текстовый редактор на 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 дня |
Стоимость рассчитывается индивидуально. Свяжитесь с нами, чтобы получить консультацию. Закажите разработку файлового доступа — оценим проект за один день.







