Текстовий редактор на 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 секунд. Наш досвід — понад 10 років у розробці десктоп-додатків, понад 40 виконаних проектів. Замовте аудит вашого додатку — виявимо вразливості за 1 день.
Орієнтовні терміни
| Етап | Час |
|---|---|
| Аналіз і проектування | 1 день |
| Реалізація базового доступу (читання/запис/діалоги) | 2-3 дні |
| Моніторинг і бінарні файли | 1-2 дні |
| Тестування і налагодження | 1-2 дні |
| Інтеграція в існуючий проект | від 1 дня |
Вартість розраховується індивідуально. Зв'яжіться з нами, щоб отримати консультацію. Замовте розробку файлового доступу — оцінимо проект за один день.







