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

    }

相关推荐
ShallowLin1 小时前
【HarmonyOS闯关习题】——HarmonyOS介绍
华为·harmonyos
爱吃大芒果1 小时前
声明式 UI 进阶剖析:复杂长列表懒加载与视图模型 (ViewModel) 的内存优化策略
ui·华为·harmonyos
坚果的博客2 小时前
Flutter 开发鸿蒙 6 应用,祝贺六一儿童节 [特殊字符]
flutter·华为·harmonyos
yuegu7772 小时前
HarmonyOS应用<节气通>开发第3篇:首页开发(下)——动态内容实现
华为·harmonyos
想你依然心痛2 小时前
HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与HMAF的“芯界智脑“——PC端AI智能体沉浸式芯片设计与EDA验证工作台
人工智能·华为·ar·harmonyos·智能体
前端不太难2 小时前
鸿蒙游戏 HUD 如何设计?
游戏·状态模式·harmonyos
Swift社区2 小时前
HarmonyOS鸿蒙三方库移植:选 vcpkg 还是 lycium_plusplus?两种“框架化”方案对比
华为·harmonyos
互联网散修2 小时前
鸿蒙实战:图片编辑器——高性能纹理马赛克画笔
华为·编辑器·harmonyos·纹理马赛克
特立独行的猫a3 小时前
鸿蒙 PC 平台 Rust 语言第三方库与应用移植全景指南
华为·rust·harmonyos·三方库·鸿蒙pc