{T}

文件系统

Electron 提供了多种方式来访问和操作文件系统,包括 Node.js 的 fs 模块和 Electron 的 dialog 模块。

系统架构

code
┌─────────────────────────────────────────────────────────┐
│                   文件系统访问层                         │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐ │
│  │  Dialog API  │  │   fs 模块    │  │   path 模块  │ │
│  ├──────────────┤  ├──────────────┤  ├──────────────┤ │
│  │ showOpenDialo│  │ readFile     │  │ join         │ │
│  │ showSaveDialo│  │ writeFile    │  │ resolve      │ │
│  │ showMessageBox│  │ mkdir        │  │ dirname      │ │
│  └──────────────┘  │ readdir      │  │ basename     │ │
│                    │ unlink       │  │ extname      │ │
│                    └──────────────┘  └──────────────┘ │
│                                                         │
├─────────────────────────────────────────────────────────┤
│                   安全访问层                             │
│  • IPC 通信           • 权限验证      • 路径过滤       │
│  • 沙箱隔离           • 文件类型检查  • 大小限制       │
└─────────────────────────────────────────────────────────┘

Dialog API 完整说明

文件对话框参数

showOpenDialog(options)

打开文件/目录选择对话框。

参数类型默认值说明
titleString-对话框标题
defaultPathString-默认打开路径
buttonLabelString-确认按钮文本
filtersFileFilter[]-文件类型过滤器
propertiesString[]-对话框属性
messageString-macOS: 对话框消息
securityScopedBookmarksBooleanfalsemacOS: 安全范围书签

properties 可选值

属性说明
openFile允许选择文件
openDirectory允许选择目录
multiSelections允许多选
showHiddenFiles显示隐藏文件
createDirectorymacOS: 允许创建目录
promptToCreateWindows: 提示创建不存在的文件
noResolveAliasesmacOS: 不解析别名
treatPackageAsDirectorymacOS: 将包视为目录
dontAddToRecentWindows: 不添加到最近文件列表

showSaveDialog(options)

保存文件对话框,参数与 showOpenDialog 类似。

showMessageBox(options)

消息对话框。

参数类型默认值说明
typeString'none'消息类型: 'none', 'info', 'error', 'question', 'warning'
buttonsString[]['OK']按钮文本数组
defaultIdNumber0默认选中按钮的索引
cancelIdNumber0取消按钮的索引
titleString-对话框标题
messageString-消息内容
detailString-详细信息
checkboxLabelString-复选框文本
checkboxCheckedBooleanfalse复选框默认状态
iconNativeImage-对话框图标
noLinkBooleanfalse不使用链接样式

文件对话框

打开文件

javascript
const { dialog, app } = require('electron')

// 选择单个文件
async function openFile() {
  const result = await dialog.showOpenDialog({
    title: '选择文件',
    defaultPath: app.getPath('documents'),
    filters: [
      { name: '文本文件', extensions: ['txt', 'md'] },
      { name: '图片', extensions: ['jpg', 'png', 'gif'] },
      { name: '所有文件', extensions: ['*'] }
    ],
    properties: ['openFile']
  })
  
  if (!result.canceled && result.filePaths.length > 0) {
    return result.filePaths[0]
  }
  return null
}

// 选择多个文件
async function openFiles() {
  const result = await dialog.showOpenDialog({
    title: '选择文件',
    properties: ['openFile', 'multiSelections']
  })
  
  return result.canceled ? [] : result.filePaths
}

// 选择目录
async function selectDirectory() {
  const result = await dialog.showOpenDialog({
    title: '选择目录',
    properties: ['openDirectory', 'createDirectory']
  })
  
  if (!result.canceled && result.filePaths.length > 0) {
    return result.filePaths[0]
  }
  return null
}

保存文件

javascript
async function saveFile(defaultName = 'untitled.txt') {
  const result = await dialog.showSaveDialog({
    title: '保存文件',
    defaultPath: defaultName,
    filters: [
      { name: '文本文件', extensions: ['txt'] },
      { name: 'Markdown', extensions: ['md'] },
      { name: '所有文件', extensions: ['*'] }
    ],
    properties: ['createDirectory']
  })
  
  if (!result.canceled && result.filePath) {
    return result.filePath
  }
  return null
}

消息对话框

javascript
const { dialog } = require('electron')

// 确认对话框
async function confirmAction(message) {
  const result = await dialog.showMessageBox({
    type: 'question',
    buttons: ['取消', '确定'],
    defaultId: 1,
    cancelId: 0,
    title: '确认',
    message: message
  })
  
  return result.response === 1  // true: 确定, false: 取消
}

// 错误提示
async function showError(title, message) {
  await dialog.showMessageBox({
    type: 'error',
    title: title,
    message: message
  })
}

// 带复选框的对话框
async function showConfirmWithCheckbox() {
  const result = await dialog.showMessageBox({
    type: 'question',
    buttons: ['取消', '确定'],
    message: '确定要执行此操作吗?',
    checkboxLabel: '不再提示',
    checkboxChecked: false
  })
  
  return {
    confirmed: result.response === 1,
    dontAskAgain: result.checkboxChecked
  }
}

文件读写

Node.js fs 模块 API

常用方法

方法同步版本异步版本Promise 版本说明
读取文件readFileSyncreadFilefs.promises.readFile读取文件内容
写入文件writeFileSyncwriteFilefs.promises.writeFile写入文件内容
追加文件appendFileSyncappendFilefs.promises.appendFile追加内容到文件
创建目录mkdirSyncmkdirfs.promises.mkdir创建目录
读取目录readdirSyncreaddirfs.promises.readdir读取目录内容
删除文件unlinkSyncunlinkfs.promises.unlink删除文件
删除目录rmdirSyncrmdirfs.promises.rmdir删除空目录
复制文件copyFileSynccopyFilefs.promises.copyFile复制文件
移动文件renameSyncrenamefs.promises.rename移动/重命名文件
文件状态statSyncstatfs.promises.stat获取文件信息
检查存在existsSync检查文件是否存在

同步读写

javascript
const fs = require('fs')
const path = require('path')

// 读取文件
function readFile(filePath) {
  try {
    const content = fs.readFileSync(filePath, 'utf-8')
    return { success: true, content }
  } catch (error) {
    console.error('读取文件失败:', error.message)
    return { success: false, error: error.message }
  }
}

// 写入文件
function writeFile(filePath, content) {
  try {
    fs.writeFileSync(filePath, content, 'utf-8')
    return { success: true }
  } catch (error) {
    console.error('写入文件失败:', error.message)
    return { success: false, error: error.message }
  }
}

// 读取 JSON 文件
function readJSON(filePath) {
  try {
    const content = fs.readFileSync(filePath, 'utf-8')
    return { success: true, data: JSON.parse(content) }
  } catch (error) {
    console.error('读取 JSON 失败:', error.message)
    return { success: false, error: error.message }
  }
}

// 写入 JSON 文件
function writeJSON(filePath, data, indent = 2) {
  try {
    const content = JSON.stringify(data, null, indent)
    fs.writeFileSync(filePath, content, 'utf-8')
    return { success: true }
  } catch (error) {
    console.error('写入 JSON 失败:', error.message)
    return { success: false, error: error.message }
  }
}

异步读写(推荐)

javascript
const fs = require('fs/promises')

// 读取文件
async function readFileAsync(filePath) {
  try {
    const content = await fs.readFile(filePath, 'utf-8')
    return { success: true, content }
  } catch (error) {
    console.error('读取文件失败:', error.message)
    return { success: false, error: error.message }
  }
}

// 写入文件
async function writeFileAsync(filePath, content) {
  try {
    await fs.writeFile(filePath, content, 'utf-8')
    return { success: true }
  } catch (error) {
    console.error('写入文件失败:', error.message)
    return { success: false, error: error.message }
  }
}

// 追加内容
async function appendFileAsync(filePath, content) {
  try {
    await fs.appendFile(filePath, content, 'utf-8')
    return { success: true }
  } catch (error) {
    console.error('追加文件失败:', error.message)
    return { success: false, error: error.message }
  }
}

// 读取 JSON 文件
async function readJSONAsync(filePath) {
  try {
    const content = await fs.readFile(filePath, 'utf-8')
    return { success: true, data: JSON.parse(content) }
  } catch (error) {
    console.error('读取 JSON 失败:', error.message)
    return { success: false, error: error.message }
  }
}

// 写入 JSON 文件
async function writeJSONAsync(filePath, data, indent = 2) {
  try {
    const content = JSON.stringify(data, null, indent)
    await fs.writeFile(filePath, content, 'utf-8')
    return { success: true }
  } catch (error) {
    console.error('写入 JSON 失败:', error.message)
    return { success: false, error: error.message }
  }
}

文件监听

使用 fs.watch

javascript
const fs = require('fs')

// 监听单个文件
function watchFile(filePath, callback) {
  const watcher = fs.watch(filePath, (eventType, filename) => {
    console.log(`文件 ${filename} 发生了 ${eventType} 事件`)
    callback(eventType, filename)
  })
  
  return {
    close: () => watcher.close()
  }
}

// 监听目录
function watchDirectory(dirPath, callback) {
  const watcher = fs.watch(dirPath, { recursive: false }, (eventType, filename) => {
    console.log(`目录中的 ${filename} 发生了 ${eventType} 事件`)
    callback(eventType, filename)
  })
  
  return {
    close: () => watcher.close()
  }
}

使用 chokidar(推荐)

chokidar 是一个跨平台的文件监听库,解决了 fs.watch 的许多问题。

bash
npm install chokidar
javascript
const chokidar = require('chokidar')

// 监听文件或目录
function watch(path, options = {}) {
  const defaultOptions = {
    ignored: /(^|[\/\\])\../,  // 忽略点文件
    persistent: true,
    ignoreInitial: false,      // 是否忽略初始扫描
    awaitWriteFinish: {
      stabilityThreshold: 2000,  // 文件稳定时间(毫秒)
      pollInterval: 100          // 轮询间隔
    }
  }
  
  const watcher = chokidar.watch(path, { ...defaultOptions, ...options })
  
  watcher
    .on('add', path => console.log(`文件添加: ${path}`))
    .on('change', path => console.log(`文件修改: ${path}`))
    .on('unlink', path => console.log(`文件删除: ${path}`))
    .on('addDir', path => console.log(`目录添加: ${path}`))
    .on('unlinkDir', path => console.log(`目录删除: ${path}`))
    .on('error', error => console.log(`监听错误: ${error}`))
    .on('ready', () => console.log('初始扫描完成,开始监听'))
  
  return watcher
}

// 监听多个路径
const watcher = chokidar.watch([
  '/path/to/file1',
  '/path/to/file2',
  '/path/to/directory'
])

// 取消监听
watcher.close()

文件操作

创建目录

javascript
const fs = require('fs')

// 创建单层目录
function createDir(dirPath) {
  try {
    if (!fs.existsSync(dirPath)) {
      fs.mkdirSync(dirPath)
    }
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
}

// 创建多层目录(推荐)
function createDirRecursive(dirPath) {
  try {
    fs.mkdirSync(dirPath, { recursive: true })
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
}

// 异步版本
async function createDirAsync(dirPath, recursive = true) {
  try {
    await fs.promises.mkdir(dirPath, { recursive })
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
}

删除文件/目录

javascript
const fs = require('fs')

// 删除文件
function deleteFile(filePath) {
  try {
    if (fs.existsSync(filePath)) {
      fs.unlinkSync(filePath)
    }
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
}

// 删除空目录
function deleteEmptyDir(dirPath) {
  try {
    fs.rmdirSync(dirPath)
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
}

// 删除非空目录(递归删除)
function deleteDir(dirPath) {
  try {
    fs.rmSync(dirPath, { recursive: true, force: true })
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
}

// 异步版本
async function deleteDirAsync(dirPath) {
  try {
    await fs.promises.rm(dirPath, { recursive: true, force: true })
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
}

复制/移动文件

javascript
const fs = require('fs')

// 复制文件
function copyFile(src, dest) {
  try {
    // 确保目标目录存在
    const destDir = path.dirname(dest)
    if (!fs.existsSync(destDir)) {
      fs.mkdirSync(destDir, { recursive: true })
    }
    
    fs.copyFileSync(src, dest)
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
}

// 复制目录(递归)
function copyDirectory(src, dest) {
  try {
    fs.cpSync(src, dest, { recursive: true })
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
}

// 移动/重命名文件
function moveFile(oldPath, newPath) {
  try {
    // 确保目标目录存在
    const newDir = path.dirname(newPath)
    if (!fs.existsSync(newDir)) {
      fs.mkdirSync(newDir, { recursive: true })
    }
    
    fs.renameSync(oldPath, newPath)
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
}

获取文件信息

javascript
const fs = require('fs')

function getFileInfo(filePath) {
  try {
    const stats = fs.statSync(filePath)
    
    return {
      success: true,
      info: {
        exists: true,
        isFile: stats.isFile(),
        isDirectory: stats.isDirectory(),
        size: stats.size,
        sizeFormatted: formatFileSize(stats.size),
        created: stats.birthtime,
        modified: stats.mtime,
        accessed: stats.atime,
        permissions: stats.mode,
        owner: stats.uid,
        group: stats.gid
      }
    }
  } catch (error) {
    if (error.code === 'ENOENT') {
      return { success: true, info: { exists: false } }
    }
    return { success: false, error: error.message }
  }
}

// 格式化文件大小
function formatFileSize(bytes) {
  if (bytes === 0) return '0 Bytes'
  const k = 1024
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
}

遍历目录

同步遍历

javascript
const fs = require('fs')
const path = require('path')

// 遍历目录(深度优先)
function walkDir(dir, callback) {
  try {
    const files = fs.readdirSync(dir)
    
    for (const file of files) {
      const filePath = path.join(dir, file)
      const stats = fs.statSync(filePath)
      
      if (stats.isDirectory()) {
        walkDir(filePath, callback)
      } else {
        callback(filePath, stats)
      }
    }
  } catch (error) {
    console.error(`遍历目录失败: ${dir}`, error.message)
  }
}

// 获取所有文件列表
function getAllFiles(dir, fileList = []) {
  const files = fs.readdirSync(dir)
  
  for (const file of files) {
    const filePath = path.join(dir, file)
    const stats = fs.statSync(filePath)
    
    if (stats.isDirectory()) {
      getAllFiles(filePath, fileList)
    } else {
      fileList.push({
        path: filePath,
        name: file,
        size: stats.size,
        modified: stats.mtime
      })
    }
  }
  
  return fileList
}

异步遍历(推荐)

javascript
const fs = require('fs/promises')
const path = require('path')

// 异步遍历目录
async function walkDirAsync(dir, callback) {
  try {
    const files = await fs.readdir(dir)
    
    for (const file of files) {
      const filePath = path.join(dir, file)
      const stats = await fs.stat(filePath)
      
      if (stats.isDirectory()) {
        await walkDirAsync(filePath, callback)
      } else {
        await callback(filePath, stats)
      }
    }
  } catch (error) {
    console.error(`遍历目录失败: ${dir}`, error.message)
  }
}

// 获取所有文件列表(异步)
async function getAllFilesAsync(dir) {
  const results = []
  const files = await fs.readdir(dir)
  
  for (const file of files) {
    const filePath = path.join(dir, file)
    const stats = await fs.stat(filePath)
    
    if (stats.isDirectory()) {
      const subResults = await getAllFilesAsync(filePath)
      results.push(...subResults)
    } else {
      results.push({
        path: filePath,
        name: file,
        size: stats.size,
        modified: stats.mtime
      })
    }
  }
  
  return results
}

路径处理

path 模块 API

javascript
const path = require('path')

// 路径拼接
const fullPath = path.join('/foo', 'bar', 'baz.txt')
// 结果: '/foo/bar/baz.txt'

// 解析绝对路径
const resolved = path.resolve('foo', 'bar', 'baz.txt')
// 结果: '/当前工作目录/foo/bar/baz.txt'

// 获取目录名
const dir = path.dirname('/foo/bar/baz.txt')
// 结果: '/foo/bar'

// 获取文件名
const base = path.basename('/foo/bar/baz.txt')
// 结果: 'baz.txt'

// 获取文件名(不含扩展名)
const name = path.basename('/foo/bar/baz.txt', '.txt')
// 结果: 'baz'

// 获取扩展名
const ext = path.extname('/foo/bar/baz.txt')
// 结果: '.txt'

// 解析路径
const parsed = path.parse('/foo/bar/baz.txt')
// 结果: { root: '/', dir: '/foo/bar', base: 'baz.txt', ext: '.txt', name: 'baz' }

// 格式化路径
const formatted = path.format({
  dir: '/foo/bar',
  base: 'baz.txt'
})
// 结果: '/foo/bar/baz.txt'

// 规范化路径
const normalized = path.normalize('/foo/bar/../baz/./qux')
// 结果: '/foo/baz/qux'

// 相对路径
const relative = path.relative('/foo/bar', '/foo/baz/qux')
// 结果: '../baz/qux'

跨平台路径处理

javascript
// 路径分隔符
console.log(path.sep)  // Windows: '\\', macOS/Linux: '/'

// 路径定界符
console.log(path.delimiter)  // Windows: ';', macOS/Linux: ':'

// 跨平台路径拼接
const dataPath = path.join(
  app.getPath('userData'),
  'data',
  'config.json'
)

// 处理不同平台的路径格式
function normalizePath(filePath) {
  return filePath.replace(/[\\/]/g, path.sep)
}

应用数据目录

获取系统路径

javascript
const { app } = require('electron')

// 常用路径
const paths = {
  home: app.getPath('home'),           // 用户主目录
  appData: app.getPath('appData'),     // 应用数据目录
  userData: app.getPath('userData'),   // 用户数据目录
  temp: app.getPath('temp'),           // 临时文件目录
  desktop: app.getPath('desktop'),     // 桌面
  documents: app.getPath('documents'), // 文档目录
  downloads: app.getPath('downloads'), // 下载目录
  music: app.getPath('music'),         // 音乐目录
  pictures: app.getPath('pictures'),   // 图片目录
  videos: app.getPath('videos'),       // 视频目录
  logs: app.getPath('logs'),           // 日志目录
  exe: app.getPath('exe')              // 应用可执行文件路径
}

console.log('应用数据目录:', paths.userData)
console.log('文档目录:', paths.documents)

管理应用数据

javascript
const { app } = require('electron')
const path = require('path')
const fs = require('fs')

// 获取应用数据目录
function getAppDataDir() {
  return app.getPath('userData')
}

// 确保数据目录存在
function ensureDataDir(subDir = '') {
  const dataDir = path.join(getAppDataDir(), 'data', subDir)
  
  if (!fs.existsSync(dataDir)) {
    fs.mkdirSync(dataDir, { recursive: true })
  }
  
  return dataDir
}

// 获取配置文件路径
function getConfigPath(configName = 'config.json') {
  return path.join(getAppDataDir(), configName)
}

// 加载配置
function loadConfig(defaultConfig = {}) {
  const configPath = getConfigPath()
  
  try {
    if (fs.existsSync(configPath)) {
      const content = fs.readFileSync(configPath, 'utf-8')
      return { ...defaultConfig, ...JSON.parse(content) }
    }
  } catch (error) {
    console.error('加载配置失败:', error.message)
  }
  
  return defaultConfig
}

// 保存配置
function saveConfig(config) {
  const configPath = getConfigPath()
  
  try {
    fs.writeFileSync(
      configPath,
      JSON.stringify(config, null, 2),
      'utf-8'
    )
    return true
  } catch (error) {
    console.error('保存配置失败:', error.message)
    return false
  }
}

临时文件处理

创建临时文件

javascript
const fs = require('fs')
const path = require('path')
const { app } = require('electron')
const crypto = require('crypto')

// 获取临时目录
function getTempDir() {
  return app.getPath('temp')
}

// 创建临时文件
function createTempFile(prefix = 'temp-', extension = '.tmp') {
  const tempDir = getTempDir()
  const fileName = prefix + crypto.randomBytes(8).toString('hex') + extension
  const tempPath = path.join(tempDir, fileName)
  
  return tempPath
}

// 创建临时目录
function createTempDir(prefix = 'temp-') {
  const tempDir = getTempDir()
  const dirName = prefix + crypto.randomBytes(8).toString('hex')
  const tempPath = path.join(tempDir, dirName)
  
  fs.mkdirSync(tempPath, { recursive: true })
  
  return tempPath
}

// 使用临时文件
async function withTempFile(callback) {
  const tempPath = createTempFile()
  
  try {
    const result = await callback(tempPath)
    return result
  } finally {
    // 清理临时文件
    try {
      fs.unlinkSync(tempPath)
    } catch (error) {
      console.error('清理临时文件失败:', error.message)
    }
  }
}

// 使用临时目录
async function withTempDir(callback) {
  const tempPath = createTempDir()
  
  try {
    const result = await callback(tempPath)
    return result
  } finally {
    // 清理临时目录
    try {
      fs.rmSync(tempPath, { recursive: true, force: true })
    } catch (error) {
      console.error('清理临时目录失败:', error.message)
    }
  }
}

大文件处理

流式读写

javascript
const fs = require('fs')
const path = require('path')

// 大文件复制(使用流)
function copyLargeFile(src, dest, onProgress) {
  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(src)
    const writeStream = fs.createWriteStream(dest)
    let copied = 0
    const total = fs.statSync(src).size
    
    readStream.on('data', (chunk) => {
      copied += chunk.length
      if (onProgress) {
        onProgress(copied, total)
      }
    })
    
    readStream.on('error', reject)
    writeStream.on('error', reject)
    writeStream.on('finish', resolve)
    
    readStream.pipe(writeStream)
  })
}

// 分块读取大文件
function readLargeFileInChunks(filePath, chunkSize = 1024 * 1024) {
  const stats = fs.statSync(filePath)
  const fileSize = stats.size
  const chunks = []
  
  for (let offset = 0; offset < fileSize; offset += chunkSize) {
    const fd = fs.openSync(filePath, 'r')
    const buffer = Buffer.alloc(Math.min(chunkSize, fileSize - offset))
    fs.readSync(fd, buffer, 0, buffer.length, offset)
    fs.closeSync(fd)
    chunks.push(buffer)
  }
  
  return Buffer.concat(chunks)
}

// 行读取器(逐行读取大文件)
function readLines(filePath, callback) {
  return new Promise((resolve, reject) => {
    const readStream = fs.createReadStream(filePath, { encoding: 'utf-8' })
    const readline = require('readline')
    
    const rl = readline.createInterface({
      input: readStream,
      crlfDelay: Infinity
    })
    
    rl.on('line', (line) => {
      callback(line)
    })
    
    rl.on('close', resolve)
    rl.on('error', reject)
  })
}

渲染进程访问文件

通过 IPC 通信

javascript
// main.js
const { ipcMain, dialog } = require('electron')
const fs = require('fs')

// 打开文件对话框
ipcMain.handle('dialog:openFile', async (event, options = {}) => {
  const result = await dialog.showOpenDialog({
    properties: ['openFile'],
    ...options
  })
  return result.canceled ? null : result.filePaths[0]
})

// 保存文件对话框
ipcMain.handle('dialog:saveFile', async (event, options = {}) => {
  const result = await dialog.showSaveDialog(options)
  return result.canceled ? null : result.filePath
})

// 读取文件
ipcMain.handle('file:read', async (event, filePath) => {
  try {
    // 验证文件路径安全性
    if (!isPathSafe(filePath)) {
      throw new Error('文件路径不安全')
    }
    
    const content = await fs.promises.readFile(filePath, 'utf-8')
    return { success: true, content }
  } catch (error) {
    return { success: false, error: error.message }
  }
})

// 写入文件
ipcMain.handle('file:write', async (event, filePath, content) => {
  try {
    // 验证文件路径安全性
    if (!isPathSafe(filePath)) {
      throw new Error('文件路径不安全')
    }
    
    await fs.promises.writeFile(filePath, content, 'utf-8')
    return { success: true }
  } catch (error) {
    return { success: false, error: error.message }
  }
})

// 获取文件信息
ipcMain.handle('file:stat', async (event, filePath) => {
  try {
    const stats = await fs.promises.stat(filePath)
    return {
      success: true,
      info: {
        isFile: stats.isFile(),
        isDirectory: stats.isDirectory(),
        size: stats.size,
        modified: stats.mtime
      }
    }
  } catch (error) {
    return { success: false, error: error.message }
  }
})

// 路径安全检查
function isPathSafe(filePath) {
  const normalized = path.normalize(filePath)
  
  // 检查是否在允许的目录内
  const allowedDirs = [
    app.getPath('documents'),
    app.getPath('downloads'),
    app.getPath('desktop')
  ]
  
  return allowedDirs.some(dir => normalized.startsWith(dir))
}
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('fileAPI', {
  // 对话框
  openFile: (options) => ipcRenderer.invoke('dialog:openFile', options),
  saveFile: (options) => ipcRenderer.invoke('dialog:saveFile', options),
  
  // 文件操作
  readFile: (path) => ipcRenderer.invoke('file:read', path),
  writeFile: (path, content) => ipcRenderer.invoke('file:write', path, content),
  statFile: (path) => ipcRenderer.invoke('file:stat', path)
})
javascript
// renderer.js
async function openAndReadFile() {
  // 打开文件选择对话框
  const filePath = await window.fileAPI.openFile({
    filters: [
      { name: '文本文件', extensions: ['txt', 'md'] }
    ]
  })
  
  if (filePath) {
    // 读取文件内容
    const result = await window.fileAPI.readFile(filePath)
    
    if (result.success) {
      console.log('文件内容:', result.content)
    } else {
      console.error('读取失败:', result.error)
    }
  }
}

async function saveFile(content) {
  const filePath = await window.fileAPI.saveFile({
    defaultPath: 'untitled.txt',
    filters: [
      { name: '文本文件', extensions: ['txt'] }
    ]
  })
  
  if (filePath) {
    const result = await window.fileAPI.writeFile(filePath, content)
    
    if (result.success) {
      console.log('文件已保存')
    } else {
      console.error('保存失败:', result.error)
    }
  }
}

错误处理

常见错误代码

javascript
// 错误代码对照表
const errorCodes = {
  'ENOENT': '文件或目录不存在',
  'EISDIR': '目标是一个目录',
  'ENOTDIR': '目标不是一个目录',
  'EACCES': '权限不足',
  'EEXIST': '文件已存在',
  'ENOSPC': '磁盘空间不足',
  'EMFILE': '打开的文件过多',
  'ELOOP': '符号链接层级过深',
  'ENAMETOOLONG': '文件名过长'
}

// 错误处理函数
function handleFileError(error) {
  const message = errorCodes[error.code] || error.message
  
  switch (error.code) {
    case 'ENOENT':
      console.error('文件不存在:', message)
      break
    case 'EACCES':
      console.error('权限不足:', message)
      break
    case 'ENOSPC':
      console.error('磁盘空间不足')
      break
    default:
      console.error('文件操作错误:', message)
  }
}

安全错误处理

javascript
// 安全的文件操作包装器
async function safeFileOperation(operation, ...args) {
  try {
    const result = await operation(...args)
    return { success: true, result }
  } catch (error) {
    // 记录错误但不暴露敏感信息
    console.error('文件操作失败:', error.code)
    
    // 返回用户友好的错误信息
    return {
      success: false,
      error: errorCodes[error.code] || '操作失败'
    }
  }
}

性能优化

1. 使用异步操作

javascript
// ❌ 避免:同步操作会阻塞主线程
const content = fs.readFileSync('large-file.txt', 'utf-8')

// ✅ 推荐:异步操作不会阻塞
const content = await fs.promises.readFile('large-file.txt', 'utf-8')

2. 批量操作

javascript
// ❌ 避免:多次单独操作
for (const file of files) {
  await fs.promises.stat(file)
}

// ✅ 推荐:批量处理
const stats = await Promise.all(
  files.map(file => fs.promises.stat(file))
)

3. 流式处理大文件

javascript
// ❌ 避免:一次性读取大文件
const content = fs.readFileSync('large-file.txt', 'utf-8')

// ✅ 推荐:使用流处理
const readStream = fs.createReadStream('large-file.txt', { encoding: 'utf-8' })
readStream.on('data', (chunk) => {
  // 处理数据块
})

安全注意事项

1. 路径验证

javascript
// 验证路径是否在允许范围内
function isPathAllowed(filePath, allowedDirs) {
  const resolved = path.resolve(filePath)
  return allowedDirs.some(dir => resolved.startsWith(path.resolve(dir)))
}

// 防止路径遍历攻击
function sanitizePath(filePath) {
  const normalized = path.normalize(filePath)
  
  // 检查是否包含 ..
  if (normalized.includes('..')) {
    throw new Error('路径包含非法字符')
  }
  
  return normalized
}

2. 文件类型验证

javascript
// 验证文件扩展名
function isAllowedExtension(filePath, allowedExtensions) {
  const ext = path.extname(filePath).toLowerCase()
  return allowedExtensions.includes(ext)
}

// 读取文件魔术数字验证类型
function getFileType(filePath) {
  const fd = fs.openSync(filePath, 'r')
  const buffer = Buffer.alloc(8)
  fs.readSync(fd, buffer, 0, 8, 0)
  fs.closeSync(fd)
  
  // 检查文件头
  if (buffer.toString('hex', 0, 4) === '89504e47') return 'png'
  if (buffer.toString('hex', 0, 2) === 'ffd8') return 'jpeg'
  if (buffer.toString('hex', 0, 4) === '47494638') return 'gif'
  // ... 其他格式
  
  return 'unknown'
}

3. 文件大小限制

javascript
const MAX_FILE_SIZE = 10 * 1024 * 1024  // 10MB

async function readFileWithLimit(filePath) {
  const stats = await fs.promises.stat(filePath)
  
  if (stats.size > MAX_FILE_SIZE) {
    throw new Error(`文件过大,最大允许 ${MAX_FILE_SIZE / 1024 / 1024}MB`)
  }
  
  return fs.promises.readFile(filePath, 'utf-8')
}

常见问题解答

1. 如何处理文件路径中的中文字符?

javascript
const path = require('path')

// Electron 会自动处理中文路径
// 确保使用 utf-8 编码读写文件
const content = fs.readFileSync(filePath, 'utf-8')
fs.writeFileSync(filePath, content, 'utf-8')

2. 如何监听文件变化?

javascript
// 使用 chokidar 库(推荐)
const chokidar = require('chokidar')

const watcher = chokidar.watch('/path/to/file', {
  awaitWriteFinish: {
    stabilityThreshold: 2000,
    pollInterval: 100
  }
})

watcher.on('change', (path) => {
  console.log('文件已修改:', path)
})

3. 如何实现文件拖放?

javascript
// 主进程
mainWindow.webContents.on('will-navigate', (event, url) => {
  event.preventDefault()
})

// 渲染进程
document.addEventListener('drop', (event) => {
  event.preventDefault()
  
  const files = event.dataTransfer.files
  for (const file of files) {
    console.log('拖放的文件:', file.path)
  }
})

document.addEventListener('dragover', (event) => {
  event.preventDefault()
})

4. 如何处理文件编码问题?

javascript
const jschardet = require('jschardet')
const iconv = require('iconv-lite')

// 检测文件编码
function detectEncoding(buffer) {
  const detected = jschardet.detect(buffer)
  return detected.encoding || 'utf-8'
}

// 读取文件并自动检测编码
function readFileWithEncoding(filePath) {
  const buffer = fs.readFileSync(filePath)
  const encoding = detectEncoding(buffer)
  
  if (encoding === 'utf-8') {
    return buffer.toString('utf-8')
  }
  
  return iconv.decode(buffer, encoding)
}

5. 如何实现文件搜索功能?

javascript
async function searchFiles(dir, pattern) {
  const results = []
  const files = await fs.promises.readdir(dir)
  
  for (const file of files) {
    const filePath = path.join(dir, file)
    const stats = await fs.promises.stat(filePath)
    
    if (stats.isDirectory()) {
      const subResults = await searchFiles(filePath, pattern)
      results.push(...subResults)
    } else if (file.includes(pattern)) {
      results.push(filePath)
    }
  }
  
  return results
}

参考链接