无法读取文件沙箱路径 - 问题分析与解决方案
问题现象
应用尝试访问以下公共目录路径时失败:
| 目录 | 路径 |
|---|---|
| 桌面 | /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 directorymkdirSync→ Error: Permission deniedEnvironment.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. 路径解析优先级
- Environment API(鸿蒙Next 标准设备支持)
- 候选路径探测 (
statSync检测目录是否在沙箱视图中可见)
关键结论
-
这些路径不是应用沙箱路径 ,而是系统公共目录
-
hdc shell 能访问是因为它以 system 权限运行
-
应用必须先获得权限,这些路径才会在应用的沙箱文件系统视图中"出现"
-
之前代码失败是因为在未授权的情况下就去探测路径
/**
- 公共目录路径工具类
- 提供 Desktop / Documents / Download / Photo / Videos / Audio 目录路径解析和权限申请
- 路径解析优先级:
-
- Environment API(鸿蒙Next)
-
- 候选路径探测(需权限授予后才可见)
- 重要:必须先调用 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; }}