openharmony北向开发基础之应用访问公共目录

无法读取文件沙箱路径 - 问题分析与解决方案

问题现象

应用尝试访问以下公共目录路径时失败:

目录 路径
桌面 /data/service/el2/100/hmdfs/account/files/Docs/Desktop
下载 /data/service/el2/100/hmdfs/account/files/Docs/Download
文档 /data/service/el2/100/hmdfs/account/files/Docs/Documents
图片 /data/service/el2/100/hmdfs/account/files/Photo
视频 /data/service/el2/100/hmdfs/account/files/Videos
音频 /data/service/el2/100/hmdfs/account/files/Audio

日志表现为:

  • statSync → Error: No such file or directory
  • mkdirSync → Error: Permission denied
  • Environment.getUserDocumentDir() → Error: The device doesn't support this api

根本原因

权限未授予时,OHOS 沙箱机制会让这些系统公共目录对应用完全"不可见"。

通过 hdc shell(root 权限)可以正常看到这些目录和文件,证明路径在设备上真实存在。问题在于应用进程未获得对应权限,导致沙箱视图中这些路径被隐藏。

解决方案

1. module.json5 声明权限

json 复制代码
"requestPermissions": [
  { "name": "ohos.permission.READ_WRITE_DOCUMENTS_DIRECTORY" },
  { "name": "ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY" },
  { "name": "ohos.permission.READ_WRITE_DESKTOP_DIRECTORY" },
  { "name": "ohos.permission.READ_IMAGEVIDEO" },
  { "name": "ohos.permission.WRITE_IMAGEVIDEO" },
  { "name": "ohos.permission.READ_AUDIO" },
  { "name": "ohos.permission.WRITE_AUDIO" }
]

2. 运行时先申请权限,再访问路径

typescript 复制代码
// 应用启动时一次性申请所有权限
const allGranted = await CommonDirectoryUtil.ensureAllPermissions(this.context);

// 权限授予后,路径就能正常获取
const desktop  = CommonDirectoryUtil.getDesktopPath();
const docs     = CommonDirectoryUtil.getDocumentsPath();
const download = CommonDirectoryUtil.getDownloadsPath();
const photo    = CommonDirectoryUtil.getPhotoPath();
const videos   = CommonDirectoryUtil.getVideosPath();
const audio    = CommonDirectoryUtil.getAudioPath();

3. 工具类接口说明

方法 说明
ensureAllPermissions(context) 一次性申请所有公共目录权限,返回是否全部授予
ensureDocumentPermission(context) 单独申请 Documents 权限
ensureDownloadPermission(context) 单独申请 Download 权限
getDesktopPath() 获取桌面目录路径
getDocumentsPath() 获取文档目录路径
getDownloadsPath() 获取下载目录路径
getPhotoPath() 获取图片目录路径
getVideosPath() 获取视频目录路径
getAudioPath() 获取音频目录路径
resetCache() 清除缓存路径,权限变更后调用
diagnose(context) 输出完整诊断信息到沙箱文件

4. 路径解析优先级

  1. Environment API(鸿蒙Next 标准设备支持)
  2. 候选路径探测statSync 检测目录是否在沙箱视图中可见)

关键结论

  • 这些路径不是应用沙箱路径 ,而是系统公共目录

  • hdc shell 能访问是因为它以 system 权限运行

  • 应用必须先获得权限,这些路径才会在应用的沙箱文件系统视图中"出现"

  • 之前代码失败是因为在未授权的情况下就去探测路径

    /**

    • 公共目录路径工具类
    • 提供 Desktop / Documents / Download / Photo / Videos / Audio 目录路径解析和权限申请
    • 路径解析优先级:
      1. Environment API(鸿蒙Next)
      1. 候选路径探测(需权限授予后才可见)
    • 重要:必须先调用 ensureAllPermissions() 授权,再调用 getXxxPath() 获取路径
      */
      import { fileIo as fs } from '@kit.CoreFileKit';
      import { Environment } from '@kit.CoreFileKit';
      import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';

    const TAG = '[CommonDirUtil]';

    const BASE_DOCS = '/data/service/el2/100/hmdfs/account/files/Docs';
    const BASE_FILES = '/data/service/el2/100/hmdfs/account/files';

    // Documents 候选路径(按优先级排列)
    const DOCUMENT_PATH_CANDIDATES: string[] = [
    BASE_DOCS + '/Documents',
    '/storage/Users/currentUser/Documents',
    '/storage/media/100/local/files/Docs/Documents',
    ];

    // Download 候选路径(按优先级排列)
    const DOWNLOAD_PATH_CANDIDATES: string[] = [
    BASE_DOCS + '/Download',
    '/storage/Users/currentUser/Download',
    '/storage/media/100/local/files/Docs/Download',
    ];

    // Desktop 候选路径
    const DESKTOP_PATH_CANDIDATES: string[] = [
    BASE_DOCS + '/Desktop',
    '/storage/Users/currentUser/Desktop',
    ];

    // Photo 候选路径
    const PHOTO_PATH_CANDIDATES: string[] = [
    BASE_FILES + '/Photo',
    '/storage/Users/currentUser/Photo',
    '/storage/media/100/local/files/Photo',
    ];

    // Videos 候选路径
    const VIDEOS_PATH_CANDIDATES: string[] = [
    BASE_FILES + '/Videos',
    '/storage/Users/currentUser/Videos',
    '/storage/media/100/local/files/Videos',
    ];

    // Audio 候选路径
    const AUDIO_PATH_CANDIDATES: string[] = [
    BASE_FILES + '/Audio',
    '/storage/Users/currentUser/Audio',
    '/storage/media/100/local/files/Audio',
    ];

    // 访问公共目录所需的全部权限
    const ALL_PERMISSIONS: Permissions[] = [
    'ohos.permission.READ_WRITE_DOCUMENTS_DIRECTORY' as Permissions,
    'ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY' as Permissions,
    'ohos.permission.READ_WRITE_DESKTOP_DIRECTORY' as Permissions,
    'ohos.permission.READ_IMAGEVIDEO' as Permissions,
    'ohos.permission.WRITE_IMAGEVIDEO' as Permissions,
    'ohos.permission.READ_AUDIO' as Permissions,
    'ohos.permission.WRITE_AUDIO' as Permissions,
    ];

    export class CommonDirectoryUtil {
    private static documentsPath: string = '';
    private static downloadsPath: string = '';
    private static desktopPath: string = '';
    private static photoPath: string = '';
    private static videosPath: string = '';
    private static audioPath: string = '';

    复制代码
    /**
     * 清除缓存的路径,下次调用时重新探测
     * 在权限授予后调用,以便用新权限重新解析路径
     */
    static resetCache(): void {
      console.info(TAG + ' resetCache');
      CommonDirectoryUtil.documentsPath = '';
      CommonDirectoryUtil.downloadsPath = '';
      CommonDirectoryUtil.desktopPath = '';
      CommonDirectoryUtil.photoPath = '';
      CommonDirectoryUtil.videosPath = '';
      CommonDirectoryUtil.audioPath = '';
    }
    
    /**
     * 一次性申请所有公共目录相关权限
     * 必须在获取路径之前调用,授权后路径才在应用沙箱视图中可见
     */
    static async ensureAllPermissions(context: common.UIAbilityContext): Promise<boolean> {
      try {
        const tokenId = context.applicationInfo.accessTokenId;
        const atManager = abilityAccessCtrl.createAtManager();
    
        const needRequest: Permissions[] = [];
        for (const perm of ALL_PERMISSIONS) {
          try {
            const status = atManager.checkAccessTokenSync(tokenId, perm);
            if (status !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
              needRequest.push(perm);
            }
          } catch (e) {
            console.warn(TAG + ' checkAccessToken 异常 ' + perm + ': ' + e);
            needRequest.push(perm);
          }
        }
    
        if (needRequest.length === 0) {
          console.info(TAG + ' 所有权限已授予');
          return true;
        }
    
        console.info(TAG + ' 需要申请权限: ' + needRequest.join(', '));
        const result = await atManager.requestPermissionsFromUser(context, needRequest);
        let allGranted = true;
        for (let i = 0; i < result.authResults.length; i++) {
          if (result.authResults[i] !== 0) {
            console.error(TAG + ' 权限被拒绝: ' + needRequest[i]);
            allGranted = false;
          }
        }
    
        CommonDirectoryUtil.resetCache();
        return allGranted;
      } catch (e) {
        console.error(TAG + ' ensureAllPermissions 异常: ' + e);
        return false;
      }
    }
    
    /**
     * 获取 Documents 目录路径
     */
    static getDocumentsPath(): string {
      if (CommonDirectoryUtil.documentsPath) {
        return CommonDirectoryUtil.documentsPath;
      }
    
      try {
        const dynamicPath = Environment.getUserDocumentDir();
        console.info(TAG + ' Environment.getUserDocumentDir() = ' + dynamicPath);
        if (dynamicPath && dynamicPath.length > 0) {
          CommonDirectoryUtil.documentsPath = dynamicPath;
          return dynamicPath;
        }
      } catch (e) {
        console.warn(TAG + ' Environment.getUserDocumentDir() 不支持: ' + e);
      }
    
      const detected = CommonDirectoryUtil.detectPath(DOCUMENT_PATH_CANDIDATES);
      if (detected) {
        CommonDirectoryUtil.documentsPath = detected;
        return detected;
      }
    
      console.error(TAG + ' Documents 路径不可用,请确认已调用 ensureAllPermissions()');
      return '';
    }
    
    /**
     * 获取 Download 目录路径
     */
    static getDownloadsPath(): string {
      if (CommonDirectoryUtil.downloadsPath) {
        return CommonDirectoryUtil.downloadsPath;
      }
    
      try {
        const dynamicPath = Environment.getUserDownloadDir();
        console.info(TAG + ' Environment.getUserDownloadDir() = ' + dynamicPath);
        if (dynamicPath && dynamicPath.length > 0) {
          CommonDirectoryUtil.downloadsPath = dynamicPath;
          return dynamicPath;
        }
      } catch (e) {
        console.warn(TAG + ' Environment.getUserDownloadDir() 不支持: ' + e);
      }
    
      const detected = CommonDirectoryUtil.detectPath(DOWNLOAD_PATH_CANDIDATES);
      if (detected) {
        CommonDirectoryUtil.downloadsPath = detected;
        return detected;
      }
    
      console.error(TAG + ' Download 路径不可用,请确认已调用 ensureAllPermissions()');
      return '';
    }
    
    /**
     * 获取 Desktop 桌面目录路径
     */
    static getDesktopPath(): string {
      if (CommonDirectoryUtil.desktopPath) {
        return CommonDirectoryUtil.desktopPath;
      }
    
      const detected = CommonDirectoryUtil.detectPath(DESKTOP_PATH_CANDIDATES);
      if (detected) {
        CommonDirectoryUtil.desktopPath = detected;
        return detected;
      }
    
      console.error(TAG + ' Desktop 路径不可用,请确认已调用 ensureAllPermissions()');
      return '';
    }
    
    /**
     * 获取 Photo 图片目录路径
     */
    static getPhotoPath(): string {
      if (CommonDirectoryUtil.photoPath) {
        return CommonDirectoryUtil.photoPath;
      }
    
      const detected = CommonDirectoryUtil.detectPath(PHOTO_PATH_CANDIDATES);
      if (detected) {
        CommonDirectoryUtil.photoPath = detected;
        return detected;
      }
    
      console.error(TAG + ' Photo 路径不可用,请确认已调用 ensureAllPermissions()');
      return '';
    }
    
    /**
     * 获取 Videos 视频目录路径
     */
    static getVideosPath(): string {
      if (CommonDirectoryUtil.videosPath) {
        return CommonDirectoryUtil.videosPath;
      }
    
      const detected = CommonDirectoryUtil.detectPath(VIDEOS_PATH_CANDIDATES);
      if (detected) {
        CommonDirectoryUtil.videosPath = detected;
        return detected;
      }
    
      console.error(TAG + ' Videos 路径不可用,请确认已调用 ensureAllPermissions()');
      return '';
    }
    
    /**
     * 获取 Audio 音频目录路径
     */
    static getAudioPath(): string {
      if (CommonDirectoryUtil.audioPath) {
        return CommonDirectoryUtil.audioPath;
      }
    
      const detected = CommonDirectoryUtil.detectPath(AUDIO_PATH_CANDIDATES);
      if (detected) {
        CommonDirectoryUtil.audioPath = detected;
        return detected;
      }
    
      console.error(TAG + ' Audio 路径不可用,请确认已调用 ensureAllPermissions()');
      return '';
    }
    
    /**
     * 申请 Documents 目录读写权限
     */
    static async ensureDocumentPermission(context: common.UIAbilityContext): Promise<boolean> {
      return CommonDirectoryUtil.requestSinglePermission(context, 'ohos.permission.READ_WRITE_DOCUMENTS_DIRECTORY' as Permissions);
    }
    
    /**
     * 申请 Download 目录读写权限
     */
    static async ensureDownloadPermission(context: common.UIAbilityContext): Promise<boolean> {
      return CommonDirectoryUtil.requestSinglePermission(context, 'ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY' as Permissions);
    }
    
    private static async requestSinglePermission(context: common.UIAbilityContext, permission: Permissions): Promise<boolean> {
      try {
        const tokenId = context.applicationInfo.accessTokenId;
        const atManager = abilityAccessCtrl.createAtManager();
        const granted = atManager.checkAccessTokenSync(tokenId, permission);
        if (granted === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
          return true;
        }
        const result = await atManager.requestPermissionsFromUser(context, [permission]);
        const isGranted = result.authResults[0] === 0;
        if (isGranted) {
          CommonDirectoryUtil.resetCache();
        }
        return isGranted;
      } catch {
        return false;
      }
    }
    
    /**
     * 候选路径探测:statSync 检测目录是否可见
     */
    private static detectPath(candidates: string[]): string {
      for (const candidate of candidates) {
        try {
          if (fs.statSync(candidate).isDirectory()) {
            console.info(TAG + ' statSync 命中: ' + candidate);
            return candidate;
          }
        } catch (e) {
          console.warn(TAG + ' statSync 失败: ' + candidate + ' -> ' + e);
        }
      }
      return '';
    }
    
    /**
     * 诊断方法:将权限状态和路径探测详情写入沙箱文件
     * 沙箱路径始终可写,文件位置: context.filesDir/path_diagnosis.txt
     */
    static diagnose(context: common.UIAbilityContext): string {
      const lines: string[] = [];
      lines.push('===== 公共目录路径诊断 =====');
      lines.push('时间: ' + new Date().toISOString());
      lines.push('');
    
      // 权限状态
      lines.push('--- 权限状态 ---');
      try {
        const atManager = abilityAccessCtrl.createAtManager();
        const tokenId = context.applicationInfo.accessTokenId;
        for (const perm of ALL_PERMISSIONS) {
          try {
            const status = atManager.checkAccessTokenSync(tokenId, perm);
            lines.push(perm + ': ' + status + ' (0=GRANTED)');
          } catch (e) {
            lines.push(perm + ': 检查异常 ' + e);
          }
        }
      } catch (e) {
        lines.push('权限检查异常: ' + e);
      }
      lines.push('');
    
      // 逐条探测所有候选路径
      lines.push('--- 路径探测详情 ---');
      const allCandidates = [
        ...DESKTOP_PATH_CANDIDATES,
        ...DOCUMENT_PATH_CANDIDATES,
        ...DOWNLOAD_PATH_CANDIDATES,
        ...PHOTO_PATH_CANDIDATES,
        ...VIDEOS_PATH_CANDIDATES,
        ...AUDIO_PATH_CANDIDATES,
      ];
      for (const path of allCandidates) {
        lines.push('路径: ' + path);
        try {
          const stat = fs.statSync(path);
          lines.push('  statSync: isDir=' + stat.isDirectory() + ', size=' + stat.size);
        } catch (e) {
          lines.push('  statSync 失败: ' + e);
        }
        try {
          const accessible = fs.accessSync(path);
          lines.push('  accessSync: ' + accessible);
        } catch (e) {
          lines.push('  accessSync 失败: ' + e);
        }
        lines.push('');
      }
    
      // 最终解析结果
      lines.push('--- 解析结果 ---');
      lines.push('Desktop 路径: ' + (CommonDirectoryUtil.desktopPath || '(空)'));
      lines.push('Documents 路径: ' + (CommonDirectoryUtil.documentsPath || '(空)'));
      lines.push('Download 路径: ' + (CommonDirectoryUtil.downloadsPath || '(空)'));
      lines.push('Photo 路径: ' + (CommonDirectoryUtil.photoPath || '(空)'));
      lines.push('Videos 路径: ' + (CommonDirectoryUtil.videosPath || '(空)'));
      lines.push('Audio 路径: ' + (CommonDirectoryUtil.audioPath || '(空)'));
      lines.push('');
    
      const result = lines.join('\n');
    
      // 写入沙箱
      try {
        const diagPath = context.filesDir + '/path_diagnosis.txt';
        const file = fs.openSync(diagPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC);
        fs.writeSync(file.fd, result);
        fs.closeSync(file);
        console.info(TAG + ' 诊断文件已写入: ' + diagPath);
      } catch (e) {
        console.error(TAG + ' 诊断文件写入失败: ' + e);
      }
    
      return result;
    }

    }

相关推荐
Georgewu1 小时前
【HarmonyOS 7】鸿蒙应用开发如何屏蔽剪切板
harmonyos
谷子在生长1 天前
纯血鸿蒙自定义弹窗最佳实践:从「到处复制」到「一行调用」
前端·harmonyos
小魔女千千鱼1 天前
把 Go 塞进鸿蒙PC:windows上用 c-shared 跑 2048
harmonyos
TrisighT1 天前
Electron 跑在鸿蒙 PC 上,单窗口和多窗口内存差 800MB?我抓了 5 组数据
性能优化·electron·harmonyos
TrisighT2 天前
AI写埋点代码,35%覆盖率坑惨运营
harmonyos·arkts·arkui
Junerver5 天前
把 DevEco Code 的 HarmonyOS 开发能力装进口袋——harmonyos-dev-skill
harmonyos
程序猿追6 天前
那个右下角的小数字怎么“卡”住我打字——我用 HarmonyOS 自己写了一个字数限制输入框
pytorch·华为·harmonyos
古德new6 天前
鸿蒙PC使用electron迁移:Joplin Electron 桌面适配全记录
华为·electron·harmonyos
世人万千丶6 天前
桌面便签小应用 - HarmonyOS ArkUI 开发实战-TextArea与Flex布局-PC版本
华为·harmonyos·鸿蒙·鸿蒙系统