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

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация доступа к файловой системе в десктоп-приложении
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы

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

Этапы разработки

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

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

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

Работа с файловой системой — одно из главных преимуществ десктоп-приложения перед веб-версией. Electron даёт полный доступ через Node.js API; Tauri — через Rust-команды с явными разрешениями. Разберём оба варианта.

Electron: прямой доступ через Node.js

Весь файловый ввод-вывод выполняется в main process. Renderer запрашивает операции через IPC.

// main/fs-service.js
const fs = require('fs/promises');
const fsSync = require('fs');
const path = require('path');
const { app, dialog, BrowserWindow } = 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 файлов

// main/preload.js — регистрируем в contextBridge
onFileDrop: (callback) => {
  ipcRenderer.on('files:dropped', (_, files) => callback(files));
  return () => ipcRenderer.removeAllListeners('files:dropped');
}
// renderer — обработка drop
const dropZone = document.getElementById('drop-zone');

dropZone.addEventListener('dragover', (e) => {
  e.preventDefault();
  e.stopPropagation();
  dropZone.classList.add('dragging');
});

dropZone.addEventListener('dragleave', () => {
  dropZone.classList.remove('dragging');
});

dropZone.addEventListener('drop', async (e) => {
  e.preventDefault();
  dropZone.classList.remove('dragging');

  // В Electron webContents drag-n-drop работает нативно
  // e.dataTransfer.files — реальные пути к файлам
  const files = Array.from(e.dataTransfer.files).map(f => ({
    name: f.name,
    path: f.path, // Electron добавляет .path — полный путь
    size: f.size,
    type: f.type
  }));

  for (const file of files) {
    const content = await window.electronAPI.fs.readFile(file.path);
    handleFile(content);
  }
});

Tauri: файловая система через плагин

# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2"
// src-tauri/capabilities/default.json
{
  "permissions": [
    "fs:allow-read-text-file",
    "fs:allow-write-text-file",
    "fs:allow-read-dir",
    "fs:allow-create-dir",
    "fs:allow-remove-file",
    "fs:allow-copy-file",
    "fs:allow-stat",
    "fs:allow-watch",
    "fs:scope-app-data-recursive",
    "fs:scope-document-recursive",
    "fs:scope-download-recursive",
    "dialog:allow-open",
    "dialog:allow-save"
  ]
}
// renderer/api/fs.ts — Tauri
import { readTextFile, writeTextFile, readDir, watch, BaseDirectory } from '@tauri-apps/plugin-fs';
import { open, save } from '@tauri-apps/plugin-dialog';

export async function openAndReadFile() {
  const selected = await open({
    multiple: false,
    filters: [{ name: 'Text', extensions: ['txt', 'md', 'json'] }]
  });

  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;
}

// Чтение с использованием BaseDirectory (относительно папки приложения)
export async function readConfig() {
  return readTextFile('config.json', { baseDir: BaseDirectory.AppData });
}

export async function writeConfig(config: object) {
  await writeTextFile('config.json', JSON.stringify(config, null, 2), {
    baseDir: BaseDirectory.AppData
  });
}

// Слежение за файлом
export async function watchFile(path: string, onChange: () => void) {
  const stopWatching = await watch(path, () => onChange());
  return stopWatching; // вызвать для прекращения слежения
}

Работа с двоичными файлами

// Electron — чтение бинарного файла
ipcMain.handle('fs:readBinary', async (event, filePath) => {
  const buffer = await fs.readFile(filePath); // Buffer
  // Передаём как ArrayBuffer через IPC
  return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
});

// Renderer — работа с ArrayBuffer
const arrayBuffer = await window.electronAPI.fs.readBinary(path);
const uint8 = new Uint8Array(arrayBuffer);

// Для изображений — конвертируем в blob URL
const blob = new Blob([uint8]);
const url = URL.createObjectURL(blob);
imgElement.src = url;

Временные файлы и очистка

// main/temp-manager.js
const fs = require('fs/promises');
const path = require('path');
const os = require('os');
const { app } = require('electron');

const tempDir = path.join(app.getPath('temp'), 'myapp-temp');

async function createTempFile(content, ext = 'tmp') {
  await fs.mkdir(tempDir, { recursive: true });
  const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`;
  const filePath = path.join(tempDir, filename);
  await fs.writeFile(filePath, content);
  return filePath;
}

async function cleanupTemp() {
  try {
    const files = await fs.readdir(tempDir);
    const now = Date.now();

    await Promise.all(files.map(async (file) => {
      const filePath = path.join(tempDir, file);
      const stat = await fs.stat(filePath);
      // Удаляем файлы старше 1 часа
      if (now - stat.mtimeMs > 3600000) {
        await fs.unlink(filePath);
      }
    }));
  } catch {}
}

// Очистка при выходе
app.on('before-quit', async () => {
  await fs.rm(tempDir, { recursive: true, force: true });
});

Важный момент для обоих фреймворков: никогда не доверяйте путям, пришедшим от renderer без валидации в main process. Path traversal (../../etc/passwd) — реальный вектор атаки в Electron-приложениях, которые рендерят удалённый контент.