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







