前言

在将 Abricotine 适配到鸿蒙 PC 平台时,文件系统操作遇到了严重的权限问题:files.createDir() 函数在用户数据目录创建文件夹时失败,抛出 EPERM(权限不足)错误,导致文件操作失败,应用功能受限。

本文将深入分析 HarmonyOS 文件系统权限模型,提供完整的权限检查和降级策略,确保应用在权限受限的情况下仍能正常工作,实现优雅的降级处理。

关键词:鸿蒙PC、Electron适配、文件系统权限、createDir、权限检查、降级策略、错误处理
在这里插入图片描述

目录

  1. 问题现象与错误分析
  2. 根本原因深度分析
  3. HarmonyOS 文件系统权限模型详解
  4. 权限检查与降级策略
  5. 完整实现方案
  6. 最佳实践与注意事项
  7. 常见问题解答
  8. 总结与展望

欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/

问题现象与错误分析

1.1 错误信息

文件操作时,控制台出现以下错误:

[HarmonyOS Files] Permission denied creating directory: /storage/emulated/0/Documents/export_files
Error: EPERM: operation not permitted, mkdir '/storage/emulated/0/Documents/export_files'

错误特征

  • files.createDir() 调用失败
  • ❌ 错误代码:EPERM(权限不足)
  • ❌ 无法创建文件夹
  • ❌ 依赖文件夹创建的功能失败

1.2 问题影响

这个错误会导致:

  • 导出功能失败:无法创建导出文件夹
  • 文件操作失败:依赖文件夹创建的操作失败
  • 用户体验差:功能受限,用户无法正常使用
  • 应用崩溃风险:如果错误处理不当,可能导致应用崩溃

根本原因深度分析

2.1 HarmonyOS 权限模型

根据 HarmonyOS 文件访问权限文档,HarmonyOS 采用严格的权限模型:

权限级别

  • 应用沙箱目录:完全访问权限
  • ⚠️ 用户数据目录:需要用户授权
  • 系统目录:完全禁止

文件夹创建权限

  • ✅ 可以在应用沙箱目录创建文件夹
  • ⚠️ 在用户数据目录创建文件夹需要权限
  • ❌ 在系统目录创建文件夹完全禁止

2.2 createDir 函数分析

原代码

// files.js
createDir: function (target) {
  var parsedPath = parsePath(target)
  var destDir = parsedPath.extname ? parsedPath.dirname : target
  try {
    mkdirp.sync(destDir)  // ❌ 可能抛出 EPERM 错误
  } catch (e) {
    throw e  // ❌ 直接抛出错误,导致应用崩溃
  }
  return destDir
}

问题分析

  • 直接调用 mkdirp.sync(),不检查权限
  • 权限错误时直接抛出异常
  • 没有降级处理机制

HarmonyOS 文件系统权限模型详解

3.1 目录权限级别

目录类型 路径示例 创建文件夹权限 说明
应用沙箱 /data/storage/.../files/ ✅ 允许 应用完全控制
用户文档 /storage/emulated/0/Documents/ ⚠️ 需要权限 用户数据目录
下载目录 /storage/emulated/0/Download/ ⚠️ 需要权限 用户数据目录
系统目录 /system/ ❌ 禁止 系统保护目录

3.2 权限申请机制

根据 HarmonyOS 权限申请文档,应用需要申请权限:

// module.json5
{
  "requestPermissions": [
    {
      "name": "ohos.permission.READ_MEDIA",
      "reason": "读取用户文件",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    },
    {
      "name": "ohos.permission.WRITE_MEDIA",
      "reason": "写入用户文件",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    }
  ]
}

但即使申请了权限,某些操作仍可能失败


权限检查与降级策略

4.1 权限检查函数

// utils/permission-checker.js

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

/**
 * 检查是否有权限创建目录
 */
function canCreateDirectory(dirPath) {
  try {
    // 规范化路径
    const normalizedPath = path.normalize(dirPath)
  
    // 检查父目录是否存在且可写
    const parentDir = path.dirname(normalizedPath)
  
    if (!fs.existsSync(parentDir)) {
      // 父目录不存在,尝试创建父目录
      return canCreateDirectory(parentDir)
    }
  
    // 尝试创建测试目录
    const testDir = path.join(normalizedPath, '.permission-test-' + Date.now())
    fs.mkdirSync(testDir, { recursive: true })
  
    // 清理测试目录
    fs.rmdirSync(testDir)
  
    return true
  } catch (error) {
    if (error.code === 'EPERM' || error.code === 'EACCES') {
      return false
    }
    // 其他错误(如目录已存在)也返回 true
    return true
  }
}

module.exports = { canCreateDirectory }

4.2 降级策略设计

策略1:静默失败

  • 创建失败时不抛出错误
  • 返回 nullfalse
  • 调用方检查返回值

策略2:使用备选目录

  • 如果用户目录失败,使用应用沙箱目录
  • 提示用户文件保存在应用目录

策略3:功能降级

  • 如果无法创建文件夹,跳过相关功能
  • 确保其他功能正常

完整实现方案

5.1 createDir 函数优化

// files.js

const { canCreateDirectory } = require('./utils/permission-checker')

createDir: function (target) {
  var parsedPath = parsePath(target)
  var destDir = parsedPath.extname ? parsedPath.dirname : target
  
  // ⚠️ HarmonyOS: 检查目录是否已存在
  if (fs.existsSync && fs.existsSync(destDir)) {
    try {
      var stat = fs.statSync(destDir)
      if (stat.isDirectory()) {
        return destDir  // 目录已存在,直接返回
      }
    } catch (e) {
      // 如果检查失败,继续尝试创建
    }
  }
  
  try {
    // ⚠️ HarmonyOS: 检查权限
    if (!canCreateDirectory(destDir)) {
      console.warn('[HarmonyOS Files] No permission to create directory:', destDir)
      return null  // 返回 null,不抛出错误
    }
  
    // 尝试创建目录
    mkdirp.sync(destDir)
    return destDir
  } catch (e) {
    // ⚠️ HarmonyOS: 权限错误时,不抛出异常,而是返回 null
    if (e.code === "EEXIST") {
      // 目录已存在,正常情况
      return destDir
    } else if (e.code === "EPERM" || e.errno === -1) {
      // HarmonyOS 权限错误,记录但不抛出异常
      console.error('[HarmonyOS Files] Permission denied creating directory:', destDir, e.message)
      return null
    } else {
      // 其他错误,记录但不抛出异常(避免阻止导出)
      console.error('[HarmonyOS Files] Error creating directory:', destDir, e.message)
      return null
    }
  }
}

5.2 调用方适配

// export-html.js

// 创建导出文件夹
var createdExportFolder = files.createDir(exportFolder)

if (!createdExportFolder) {
  console.warn('[HarmonyOS Export] ⚠️ Failed to create export folder (permission denied)')
  console.warn('[HarmonyOS Export] Using fallback strategy')
  
  // 降级策略:使用应用沙箱目录
  const appDataDir = app.getPath('userData')
  const fallbackExportFolder = path.join(appDataDir, 'exports', folderBasename + '_files')
  
  createdExportFolder = files.createDir(fallbackExportFolder)
  
  if (createdExportFolder) {
    console.log('[HarmonyOS Export] Using fallback export folder:', fallbackExportFolder)
    exportFolder = fallbackExportFolder
  } else {
    console.error('[HarmonyOS Export] Fallback folder creation also failed')
    // 继续执行,但不复制图片
  }
}

5.3 错误处理最佳实践

// files.js

createDir: function (target, options = {}) {
  const { silent = true, fallback = null } = options
  
  var parsedPath = parsePath(target)
  var destDir = parsedPath.extname ? parsedPath.dirname : target
  
  try {
    // 检查是否已存在
    if (fs.existsSync && fs.existsSync(destDir)) {
      const stat = fs.statSync(destDir)
      if (stat.isDirectory()) {
        return destDir
      }
    }
  
    // 检查权限
    if (!canCreateDirectory(destDir)) {
      if (fallback) {
        // 使用备选目录
        return this.createDir(fallback, options)
      }
    
      if (silent) {
        return null
      } else {
        throw new Error(`Permission denied: cannot create directory ${destDir}`)
      }
    }
  
    // 创建目录
    mkdirp.sync(destDir)
    return destDir
  } catch (e) {
    if (e.code === "EEXIST") {
      return destDir
    }
  
    if (silent) {
      console.error('[HarmonyOS Files] Error creating directory:', destDir, e.message)
      return null
    } else {
      throw e
    }
  }
}

最佳实践与注意事项

6.1 权限检查最佳实践

推荐做法

// ✅ 好:检查权限后再操作
if (canCreateDirectory(dir)) {
  files.createDir(dir)
} else {
  // 降级处理
}

// ❌ 不好:直接操作,可能失败
files.createDir(dir)  // 可能抛出异常

6.2 错误处理最佳实践

推荐做法

// ✅ 好:静默失败,返回 null
const result = files.createDir(dir)
if (!result) {
  // 降级处理
}

// ❌ 不好:直接抛出异常
files.createDir(dir)  // 可能抛出异常,导致应用崩溃

6.3 降级策略最佳实践

推荐做法

// ✅ 好:多重降级策略
let exportFolder = userSelectedFolder

if (!files.createDir(exportFolder)) {
  // 降级1:使用应用沙箱目录
  exportFolder = app.getPath('userData')
  
  if (!files.createDir(exportFolder)) {
    // 降级2:跳过文件夹创建,直接导出
    exportFolder = null
  }
}

常见问题解答

Q1: 为什么不能创建文件夹?

A: HarmonyOS 文件系统权限限制,应用不能在用户数据目录直接创建文件夹,需要用户授权或使用应用沙箱目录。

Q2: 如何提高创建成功率?

A:

  1. 检查权限后再操作
  2. 使用备选目录
  3. 实现降级策略
  4. 完善的错误处理

Q3: 降级策略会影响用户体验吗?

A: 不会。降级策略确保功能可用,即使权限不足也能部分工作,用户体验更好。


总结与展望

8.1 核心要点总结

通过本文的深入分析,我们了解到:

  1. 权限检查的重要性:检查权限后再操作,避免不必要的错误
  2. 降级策略的价值:确保功能在权限受限时仍能部分工作
  3. 错误处理的最佳实践:静默失败,返回 null,调用方处理

8.2 技术价值

这个解决方案不仅解决了文件系统权限问题,还带来了以下好处:

  • 更好的兼容性:支持各种权限情况
  • 更完善的错误处理:降级策略确保功能可用
  • 更好的用户体验:即使权限不足也能部分工作

相关资源

Node.js 官方文档

HarmonyOS 官方文档

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐