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

    }

相关推荐
梦想不只是梦与想1 分钟前
鸿蒙AGC设备管理:设备注册(一)
harmonyos·设备管理·appgallery
熊猫钓鱼>_>2 小时前
鸿蒙ArkUI全手势操作实战指南:6大基础手势从原理到落地避坑
人工智能·深度学习·华为·架构·harmonyos·arkui·tapgesture
2501_919749034 小时前
华为鸿蒙记录怀孕APP—小羊怀孕
华为·harmonyos·鸿蒙
YM52e5 小时前
鸿蒙 ArkTS 实战|网络常用漫剧主角名称分类表:26 位主角 8 大分类 + 搜索筛选
学习·华为·harmonyos
小雨青年6 小时前
【HarmonyOS 7 悬浮页签深度实战】02 从普通底栏到第一个悬浮页签
华为·harmonyos
whyutianict_vv6 小时前
从零到上架6款APP:AI鸿蒙全栈智能体开发5个月实战复盘(ArkTS/DevEco/AGC全流程)
人工智能·个人开发·harmonyos
OH_TPC7 小时前
HarmonyOS APP开发---"启动秀"应用引导页App,需要用到这个库
harmonyos
见山是山-见水是水9 小时前
围绕HTTP 网络请求实战构建原生体验:设计取舍、实现与排错
网络·网络协议·http·华为·harmonyos
见山是山-见水是水9 小时前
拆解网络缓存策略设计:原生鸿蒙页面的实现路径与调试方法
http·缓存·华为·harmonyos
蓝速科技10 小时前
蓝速鸿蒙信创终端长效流畅性深度评测
arm开发·华为·harmonyos