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

Ваше десктоп-приложение может тормозить при работе с большими файлами или сталкиваться с уязвимостями в доступе к файловой системе. Мы разрабатываем надёжные решения для 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 секунд. Закажите аудит вашего приложения — выявим уязвимости за 1 день.

Ориентировочные сроки

Этап Время
Анализ и проектирование 1 день
Реализация базового доступа (чтение/запись/диалоги) 2-3 дня
Мониторинг и бинарные файлы 1-2 дня
Тестирование и отладка 1-2 дня
Интеграция в существующий проект от 1 дня

Стоимость рассчитывается индивидуально. Свяжитесь с нами, чтобы получить консультацию. Закажите разработку файлового доступа — оценим проект за один день.

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

Последние работы

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