Реалізація доступу до файлової системи в десктоп-додатку

Ваше десктоп-застосунок може гальмувати під час роботи з великими файлами або стикатися з уразливостями в доступі до файлової системи. Ми розробляємо надійні рішення для Electron і Tauri, забезпечуючи безпечний і швидкий обмін даними. Наша команда реалізує проєкт під ключ — від аудиту до впровадження, з подальшою підтримкою.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Реалізація доступу до файлової системи в десктоп-додатку
Середній
~2-3 дні
Часті запитання

Текстовий редактор на 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 у розробці)

Процес і обсяг роботи

  1. Аналітика — вивчаємо ваш поточний додаток, вимоги до файлового доступу, визначаємо вразливості. Створюємо карту потоку даних.
  2. Проектування — розробляємо схему IPC або permissions, обираємо бібліотеки, проектуємо API.
  3. Реалізація — пишемо код з модульним тестуванням (jest для Node.js, vitest для Tauri). Використовуємо Repository pattern для ізоляції бізнес-логіки.
  4. Тестування — перевіряємо на реальних файлах (до 10 ГБ), імітуємо атаки path traversal, тестуємо на Windows, macOS, Linux.
  5. Деплой — збираємо бінарники, налаштовуємо 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 дня

Вартість розраховується індивідуально. Зв'яжіться з нами, щоб отримати консультацію. Замовте розробку файлового доступу — оцінимо проект за один день.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1142
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060