【鸿蒙开发】第四十八章 Core File Kit(文件基础服务)

目录

1 Core File Kit概述

1.1 Kit使用场景

1.2 能力范围

1.3 亮点/特征

1.4 框架原理

1.4.1 应用文件访问框架

1.4.2 用户文件访问框架

2 应用文件

2.1 应用文件概述

 2.2 应用沙箱目录

2.2.1 应用沙箱目录与应用沙箱路径

2.2.2 应用文件目录与应用文件路径

2.2.3 应用沙箱路径和真实物理路径的对应关系

2.3 应用文件访问与管理 

2.3.1 应用文件访问(ArkTS)

2.3.2 接口说明

2.3.3 开发示例

1、新建并读写一个文件

2、读取文件内容并写入到另一个文件

3、以流的形式读写文件

4、查看文件列表

5、使用文件流

6、使用文件哈希流

2.4 应用文件分享

2.4.1 应用可分享目录

2.4.2 文件URI规范

2.4.3 分享文件给其他应用

2.4.4 使用其他应用分享的文件

2.5 应用数据备份恢复

2.5.1 应用数据备份恢复概述

2.5.2 应用接入数据备份恢复

1、接口说明

2、约束与限制

3、开发步骤

4、元数据资源配置文件说明

3 用户文件

3.1 用户文件概述

3.1.1 用户文件存储位置

1、内置存储

2、外置存储

3.2 用户文件uri介绍

3.2.1 uri的类型

3.2.2 文档类uri

1、文档类uri介绍

2、文档类uri获取方式

3、文档类uri的使用方式

3.2.3 媒体文件uri

1、媒体文件uri介绍

2、媒体文件uri获取方式

3、媒体文件uri的使用方式

3.3 选择用户文件

3.3.1 选择图片或视频类文件

3.3.2 选择文档类文件

3.4 保存用户文件

3.4.1 保存图片或视频类文件

3.4.2 保存文档类文件

4 分布式文件系统

4.1 分布式文件系统概述

4.1.1 分布式文件系统架构

4.2 设置分布式文件数据等级

4.2.1 接口说明

4.2.2 开发示例

4.3 跨设备文件访问

4.3.1 开发步骤

4.4 跨设备文件拷贝

4.4.1 开发步骤


1 Core File Kit概述

在Core File Kit套件中,按文件所有者的不同,有如下文件分类模型,其示意图如下面文件分类模型示意图:

  • 应用文件:文件所有者为应用,包括应用安装文件、应用资源文件、应用缓存文件等。

  • 用户文件:文件所有者为登录到该终端设备的用户,包括用户私有的图片、视频、音频、文档等。

  • 系统文件:与应用和用户无关的其它文件,包括公共库、设备文件、系统资源文件等。这类文件不需要开发者进行文件管理,本文不展开介绍。

按文件系统管理的文件存储位置(数据源位置)的不同,有如下文件系统分类模型:

  • 本地文件系统:提供本地设备或外置存储设备(如U盘、移动硬盘)的文件访问能力。本地文件系统是最基本的文件系统,本文不展开介绍。

  • 分布式文件系统:提供跨设备的文件访问能力。所谓跨设备,指文件不一定存储在本地设备或外置存储设备,而是通过计算机网络与其它分布式设备相连。

图1 文件分类模型示意图

1.1 Kit使用场景

Core File Kit常见的使用场景:

  • 应用文件访问和文件分享。
  • 应用数据备份恢复。
  • 选择与保存用户文件。
  • 跨设备的文件访问和分享能力。

1.2 能力范围

  • 支持对应用文件进行查看、创建、读写、删除、移动、复制、获取属性等访问操作。
  • 支持应用文件上传到网络服务器和网络服务器下载网络资源文件到本地应用文件目录。
  • 支持获取当前应用的存储空间大小、指定文件系统的剩余空间大小和指定文件系统的总空间大小。
  • 支持应用分享文件给其它应用和使用其它应用分享的文件。
  • 支持应用接入数据备份恢复,在接入后,应用可通过修改配置文件定制备份恢复框架的行为,包括是否允许备份恢复、备份哪些数据。
  • 提供用户文件访问框架,用于开发者访问和管理用户文件。例如选择与保存用户文件。
  • 支持跨设备的文件访问和拷贝能力。

1.3 亮点/特征

  • 沙箱隔离:

    访问和管理应用文件,对于每个应用,系统会在内部存储空间映射出一个专属的“应用沙箱目录”,它是“应用文件目录”与一部分系统文件(应用运行必需的少量系统文件)所在的目录组成的集合。有以下优点:

    • 隔离性:应用沙箱提供了一个完全隔离的环境,使用户可以安全地访问应用文件。
    • 安全性:应用沙箱限制了应用可见的数据的最小范围,保护了应用文件的安全。

  • 应用分享:

    应用之间可以通过分享URI(Uniform Resource Identifier)或文件描述符FD(File Descriptor)的方式,进行文件共享。有以下优点:

    • 便携性:应用之间进行文件分享,省去了用户在多个应用间切换的麻烦,简化了操作步骤,提高了效率。
    • 高效性:应用间的文件分享能够更快地完成文件的传输,减少了因多次跳转和等待而浪费的时间。
    • 数据一致性:应用间的文件分享能够确保数据的完整性和一致性,避免数据在传输过程中出现损坏或丢失的情况。
    • 安全性:应用间的文件分享可以确保文件的安全性,避免文件被非法获取或篡改。同时,通过文件授权访问的方式,可以进一步增强文件的安全性。

1.4 框架原理

1.4.1 应用文件访问框架

应用文件访问框架是通过基础文件操作接口(ohos.file.fs)实现。开发者无需了解内部实现,基础文件操作接口功能详情请参考接口说明

1.4.2 用户文件访问框架

用户文件访问框架(File Access Framework)是一套提供给开发者访问和管理用户文件的基础框架。该框架依托于HarmonyOS的ExtensionAbility组件机制,提供了一套统一访问用户文件的方法和接口。

图2 用户文件访问框架示意图

  • 各类系统应用或三方应用(即图中的文件访问客户端)若需访问用户文件,如选择一张照片或保存多个文档等,可以通过拉起“文件选择器应用”来实现。

  • FilePicker:系统预置应用,提供文件访问客户端选择和保存文件的能力,且不需要配置任何权限。FilePicker的使用指导请参见选择用户文件

  • FileManager:对于设备开发者,还可以按需开发自己的文件选择器或文件管理器应用。该功能不向三方应用开放。

  • File Access Framework(用户文件访问框架)的主要功能模块如下:

    • File Access Helper:提供给文件管理器和文件选择器访问用户文件的API接口。
    • File Access ExtensionAbility:提供文件访问框架能力,由内卡文件管理服务UserFileManager和外卡文件管理服务ExternalFileManager组成,实现对应的文件访问功能。
      • UserFileManager:内卡文件管理服务,基于File Access ExtensionAbility框架实现,用于管理内置存储设备上的文件。
      • ExternalFileManager:外卡文件管理服务,基于File Access ExtensionAbility框架实现,用于管理外置存储设备上的文件。

2 应用文件

2.1 应用文件概述

应用文件:文件所有者为应用,包括应用安装文件、应用资源文件、应用缓存文件等。

  • 设备上应用所使用及存储的数据,以文件、键值对、数据库等形式保存在一个应用专属的目录内。该专属目录我们称为“应用文件目录”,该目录下所有数据以不同的文件格式存放,这些文件即应用文件。

  • “应用文件目录”与一部分系统文件(应用运行必须使用的系统文件)所在的目录组成了一个集合,该集合称为“应用沙箱目录”,代表应用可见的所有目录范围。因此“应用文件目录”是在“应用沙箱目录”内的。

  • 系统文件及其目录对于应用是只读的;应用仅能保存文件到“应用文件目录”下,根据目录的使用规范和注意事项来选择将数据保存到不同的子目录中。

 2.2 应用沙箱目录

应用沙箱是一种以安全防护为目的的隔离机制,避免数据受到恶意路径穿越访问。在这种沙箱的保护机制下,应用可见的目录范围即为“应用沙箱目录”。

  • 对于每个应用,系统会在内部存储空间映射出一个专属的“应用沙箱目录”,它是“应用文件目录”与一部分系统文件(应用运行必需的少量系统文件)所在的目录组成的集合。

  • 应用沙箱限制了应用可见的数据范围。在“应用沙箱目录”中,应用仅能看到自己的应用文件以及少量的系统文件(应用运行必需的少量系统文件)。因此,本应用的文件也不为其他应用可见,从而保护了应用文件的安全。

  • 应用可以在“应用文件目录”下保存和处理自己的应用文件;系统文件及其目录对于应用是只读的;而应用若需访问用户文件,则需要通过特定API同时经过用户的相应授权才能进行。

下图展示了应用沙箱下,应用可访问的文件范围和方式

图1 应用沙箱文件访问关系图

2.2.1 应用沙箱目录与应用沙箱路径

在应用沙箱保护机制下,应用无法获知除自身应用文件目录之外的其他应用或用户的数据目录位置及存在。同时,所有应用的目录可见范围均经过权限隔离与文件路径挂载隔离,形成了独立的路径视图,屏蔽了实际物理路径:

  • 如下图所示,在普通应用(也称三方应用)视角下,不仅可见的目录与文件数量限制了范围,并且可见的目录与文件路径也与系统进程等其他进程看到的不同。我们将普通应用视角下看到的“应用沙箱目录”下某个文件或某个具体目录的路径,称为“应用沙箱路径”。

  • 开发者在应用开发调试时,可能需要向应用沙箱下推送一些文件以期望在应用内访问或测试。可以通过DevEco Studio向应用安装路径中放入目标文件,详见应用安装资源访问

  • 实际物理路径与沙箱路径并非1:1的映射关系,沙箱路径总是少于系统进程视角可见的物理路径。部分调试进程视角下的物理路径在对应的应用沙箱目录下没有对应路径。

图2 应用沙箱路径(不同权限与角色的进程下可见的文件路径不同)

2.2.2 应用文件目录与应用文件路径

如前文所述,“应用沙箱目录”内分为两类:应用文件目录和系统文件目录。

系统文件目录对应用的可见范围由HarmonyOS系统预置。

在此主要介绍应用文件目录,如下图所示。应用文件目录下某个文件或某个具体目录的路径称为应用文件路径。应用文件目录下的各个文件路径,具备不同的属性和特征。

图3 应用文件目录结构图

说明

  • 禁止直接使用上图中四级目录之前的目录名组成的路径字符串,否则可能导致后续应用版本因应用文件路径变化导致不兼容问题。
  • 应通过Context属性获取应用文件路径,包括但不限于上图中绿色背景的路径。 Context上下文获取及上述应用文件路径的获取,详见应用上下文Context

1. 一级目录data/:代表应用文件目录。

2. 二级目录storage/:代表本应用持久化文件目录。

3. 三级目录el1/~el5/:代表不同文件加密类型。

  • EL1(Encryption Level 1):
  • 保护设备上的所有文件的基础安全能力。在设备开机后,不需要用户先完成身份认证即可访问EL1保护的文件。如无特殊必要,不推荐使用该方式。
  • 如果直接窃取设备存储介质上的密文,攻击者无法脱机进行解密。
  • EL2(Encryption Level 2):
  • 在EL1的基础上,增加首次认证后的文件保护能力。设备开机后,用户在通过首次认证后,通过EL2能力保护的文件才能被访问。此后只要设备没有关机,通过EL2能力保护的文件一直可被访问。推荐应用默认使用该方式。
  • 如果在关机后丢失手机,则攻击者无法读取通过EL2能力保护的文件。
  • EL3(Encryption Level 3):
  • 与EL4整体能力类似,但和EL4的区别是,在锁屏下可创建新的文件,但无法读取。如无特殊必要,无需使用该方式。
  • EL4(Encryption Level 4):
  • 在EL2的基础上,增加设备锁屏时的文件保护能力。在用户锁屏时,通过EL4能力保护的数据将无法被访问。如无特殊必要,无需使用该方式。
  • 如果设备在锁屏状态下被盗,攻击者无法读取通过EL4能力保护的文件。
  • EL5(Encryption Level 5):
  • 在EL2的基础上,增加设备锁屏时的文件保护能力。在用户锁屏后,满足一定条件时,通过EL5能力保护的数据将无法被访问,但可以继续创建和读写新的文件。如无特殊必要,无需使用该方式。
  • 默认情况下不会生成EL5的相关目录,需要配置访问E类加密数据库的相关权限,详见E类加密数据库的使用

说明

  • 应用如无特殊需要,应将数据存放在el2加密目录下,以尽可能保证数据安全。但是对于某些场景,一些应用文件需要在用户首次认证前就可被访问,例如时钟、闹铃、壁纸等,此时应用需要将这些文件存放到设备级加密区(el1)。
  • 通过监听COMMON_EVENT_USER_UNLOCKED事件感知当前用户首次认证完成。
  • 切换应用文件加密类型目录的方法请参见获取和修改加密分区

4. 四级、五级目录

通过ApplicationContext可以获取distributedfiles目录或base下的files、cache、preferences、temp等目录的应用文件路径,应用全局信息可以存放在这些目录下。

通过UIAbilityContext、AbilityStageContext、ExtensionContext可以获取HAP级别应用文件路径。HAP信息可以存放在这些目录下,存放在此目录的文件会跟随HAP的卸载而删除,不会影响App级别目录下的文件。在开发态,一个应用包含一个或者多个HAP,详见Stage模型应用程序包结构

应用文件路径具体说明及生命周期如下表所示。

表1 应用文件路径详细说明

目录名Context属性名称类型说明
bundlebundleCodeDir安装文件路径

应用安装后的App的HAP资源包所在目录;随应用卸载而清理。

不能拼接路径访问资源文件,请使用资源管理接口访问资源。

可以用于存储应用的代码资源数据,主要包括应用安装的HAP资源包、可重复使用的库文件以及插件资源等。此路径下存储的代码资源数据可以被用于动态加载。

baseNA本设备文件路径应用在本设备上存放持久化数据的目录,子目录包含files/、cache/、temp/和haps/;随应用卸载而清理。
databasedatabaseDir数据库路径

应用在el2加密条件下存放通过分布式数据库服务操作的文件目录;随应用卸载而清理。

仅用于保存应用的私有数据库数据,主要包括数据库文件等。此路径下仅适用于存储分布式数据库相关文件数据。

distributedfilesdistributedFilesDir分布式文件路径

应用在el2加密条件下存放分布式文件的目录,应用将文件放入该目录可分布式跨设备直接访问;随应用卸载而清理。

可以用于保存应用分布式场景下的数据,主要包括应用多设备共享文件、应用多设备备份文件、应用多设备群组协助文件。此路径下存储这些数据,使得应用更加适合多设备使用场景。

filesfilesDir应用通用文件路径

应用在本设备内部存储上通用的存放默认长期保存的文件路径;随应用卸载而清理。

可以用于保存应用的任何私有数据,主要包括用户持久性文件、图片、媒体文件以及日志文件等。此路径下存储这些数据,使得数据保持私有、安全且持久有效。

cachecacheDir应用缓存文件路径

应用在本设备内部存储上用于缓存下载的文件或可重新生成的缓存文件的路径,应用cache目录大小超过配额或者系统空间达到一定条件,自动触发清理该目录下文件;用户通过系统空间管理类应用也可能触发清理该目录。应用需判断文件是否仍存在,决策是否需重新缓存该文件;随应用卸载而清理。

可以用于保存应用的缓存数据,主要包括离线数据、图片缓存、数据库备份以及临时文件等。此路径下存储的数据可能会被系统自动清理,因此不要存储重要数据。

preferencespreferencesDir应用首选项文件路径

应用在本设备内部存储上通过数据库API存储配置类或首选项的目录;随应用卸载而清理。详见通过用户首选项实现数据持久化

可以用于保存应用的首选项数据,主要包括应用首选项文件以及配置文件等。此路径下仅适用于存储小量数据。

temptempDir应用临时文件路径

应用在本设备内部存储上仅在应用运行期间产生和需要的文件,应用退出后即清理。

可以用于保存应用的临时生成的数据,主要包括数据库缓存、图片缓存、临时日志文件、以及下载的应用安装包文件等。此路径下存储使用后即可删除的数据。

2.2.3 应用沙箱路径和真实物理路径的对应关系

在应用沙箱路径下读写文件,经过映射转换,实际读写的是真实物理路径中的应用文件,应用沙箱路径与真实物理路径对应关系如下表所示。

其中<USERID>为当前用户ID,从100开始递增,<EXTENSIONPATH>为moduleName-extensionName。应用是否以Extension独立沙箱运行可参考ExtensionAbility组件

应用沙箱路径物理路径
/data/storage/el1/bundle

应用安装包目录:

/data/app/el1/bundle/public/<PACKAGENAME>

/data/storage/el1/base

应用el1级别加密数据目录:

- 非独立沙箱运行的应用:/data/app/el1/<USERID>/base/<PACKAGENAME>

- 以独立沙箱运行的Extension应用: /data/app/el1/<USERID>/base/+extension-<EXTENSIONPATH>+<PACKAGENAME>

/data/storage/el2/base

应用el2级别加密数据目录:

- 非独立沙箱运行的应用:/data/app/el2/<USERID>/base/<PACKAGENAME>

- 以独立沙箱运行的Extension应用: /data/app/el2/<USERID>/base/+extension-<EXTENSIONPATH>+<PACKAGENAME>

/data/storage/el1/database

应用el1级别加密数据库目录:

- 非独立沙箱运行的应用:/data/app/el1/<USERID>/database/<PACKAGENAME>

- 以独立沙箱运行的Extension应用:/data/app/el1/<USERID>/database/+extension-<EXTENSIONPATH>+<PACKAGENAME>

/data/storage/el2/database

应用el2级别加密数据库目录:

- 非独立沙箱运行的应用:/data/app/el2/<USERID>/database/<PACKAGENAME>

- 以独立沙箱运行的Extension应用:/data/app/el2/<USERID>/database/+extension-<EXTENSIONPATH>+<PACKAGENAME>

/data/storage/el2/distributedfiles/mnt/hmdfs/<USERID>/account/merge_view/data/<PACKAGENAME>

2.3 应用文件访问与管理 

2.3.1 应用文件访问(ArkTS)

应用需要对应用文件目录下的应用文件进行查看、创建、读写、删除、移动、复制、获取属性等访问操作,下文介绍具体方法。

2.3.2 接口说明

通过基础文件操作接口(ohos.file.fs)实现应用文件访问能力,主要功能如下表所示。

表1 基础文件操作接口功能

接口名功能接口类型支持同步支持异步
access检查文件是否存在方法
close关闭文件方法
copyFile复制文件方法
createStream基于文件路径打开文件流方法
listFile列出文件夹下所有文件名方法
mkdir创建目录方法
moveFile移动文件方法
open打开文件方法
read从文件读取数据方法
rename重命名文件或文件夹方法
rmdir删除整个目录方法
stat获取文件详细属性信息方法
unlink删除单个文件方法
write将数据写入文件方法
Stream.close关闭文件流方法
Stream.flush刷新文件流方法
Stream.write将数据写入流文件方法
Stream.read从流文件读取数据方法
File.fd获取文件描述符属性--
OpenMode设置文件打开标签属性--
Filter设置文件过滤配置项类型--

注意

使用基础文件操作接口时,耗时较长的操作,例如:read、write等,建议使用异步接口,避免应用崩溃。

2.3.3 开发示例

在对应用文件开始访问前,需要获取应用文件路径。以从UIAbilityContext获取HAP级别的文件路径为例进行说明,UIAbilityContext的获取方式请参见获取UIAbility的上下文信息

下面介绍几种常用操作示例。

1、新建并读写一个文件

以下示例代码演示了如何新建一个文件并对其读写。

// pages/xxx.ets
import { fileIo as fs, ReadOptions } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { buffer } from '@kit.ArkTS';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;

function createFile(): void {
  // 文件不存在时创建并打开文件,文件存在时打开文件
  let file = fs.openSync(filesDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  // 写入一段内容至文件
  let writeLen = fs.writeSync(file.fd, "Try to write str.");
  console.info("The length of str is: " + writeLen);
  // 创建一个大小为1024字节的ArrayBuffer对象,用于存储从文件中读取的数据
  let arrayBuffer = new ArrayBuffer(1024);
  // 设置读取的偏移量和长度
  let readOptions: ReadOptions = {
    offset: 0,
    length: arrayBuffer.byteLength
  };
  // 读取文件内容到ArrayBuffer对象中,并返回实际读取的字节数
  let readLen = fs.readSync(file.fd, arrayBuffer, readOptions);
  // 将ArrayBuffer对象转换为Buffer对象,并转换为字符串输出
  let buf = buffer.from(arrayBuffer, 0, readLen);
  console.info("the content of file: " + buf.toString());
  // 关闭文件
  fs.closeSync(file);
}
2、读取文件内容并写入到另一个文件

以下示例代码演示了如何从一个文件读写内容到另一个文件。

// pages/xxx.ets
import { fileIo as fs, ReadOptions, WriteOptions } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;

function readWriteFile(): void {
  // 打开文件
  let srcFile = fs.openSync(filesDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  let destFile = fs.openSync(filesDir + '/destFile.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  // 读取源文件内容并写入至目的文件
  let bufSize = 4096;
  let readSize = 0;
  let buf = new ArrayBuffer(bufSize);
  let readOptions: ReadOptions = {
    offset: readSize,
    length: bufSize
  };
  let readLen = fs.readSync(srcFile.fd, buf, readOptions);
  while (readLen > 0) {
    readSize += readLen;
    let writeOptions: WriteOptions = {
      length: readLen
    };
    fs.writeSync(destFile.fd, buf, writeOptions);
    readOptions.offset = readSize;
    readLen = fs.readSync(srcFile.fd, buf, readOptions);
  }
  // 关闭文件
  fs.closeSync(srcFile);
  fs.closeSync(destFile);
}

说明

使用读写接口时,需注意可选项参数offset的设置。对于已存在且读写过的文件,文件偏移指针默认在上次读写操作的终止位置。

3、以流的形式读写文件

以下示例代码演示了如何使用流接口读取test.txt的文件内容并写入到destFile.txt文件中。

// pages/xxx.ets
import { fileIo as fs, ReadOptions } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;

async function readWriteFileWithStream(): Promise<void> {
  // 创建并打开输入文件流
  let inputStream = fs.createStreamSync(filesDir + '/test.txt', 'r+');
  // 创建并打开输出文件流
  let outputStream = fs.createStreamSync(filesDir + '/destFile.txt', "w+");

  let bufSize = 4096;
  let readSize = 0;
  let buf = new ArrayBuffer(bufSize);
  let readOptions: ReadOptions = {
    offset: readSize,
    length: bufSize
  };
  // 以流的形式读取源文件内容并写入到目标文件
  let readLen = await inputStream.read(buf, readOptions);
  readSize += readLen;
  while (readLen > 0) {
    const writeBuf = readLen < bufSize ? buf.slice(0, readLen) : buf;
    await outputStream.write(writeBuf);
    readOptions.offset = readSize;
    readLen = await inputStream.read(buf, readOptions);
    readSize += readLen;
  }
  // 关闭文件流
  inputStream.closeSync();
  outputStream.closeSync();
}
4、查看文件列表

以下示例代码演示了如何查看文件列表。

import { fileIo as fs, Filter, ListFileOptions } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;

// 查看文件列表
function getListFile(): void {
  let listFileOption: ListFileOptions = {
    recursion: false,
    listNum: 0,
    filter: {
      suffix: [".png", ".jpg", ".txt"],
      displayName: ["test*"],
      fileSizeOver: 0,
      lastModifiedAfter: new Date(0).getTime()
    }
  };
  let files = fs.listFileSync(filesDir, listFileOption);
  for (let i = 0; i < files.length; i++) {
    console.info(`The name of file: ${files[i]}`);
  }
}
5、使用文件流

以下示例代码演示了如何使用文件可读流,文件可写流。

// pages/xxx.ets
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;

function copyFileWithReadable(): void {
  // 创建文件可读流
  const rs = fs.createReadStream(`${filesDir}/read.txt`);
  // 创建文件可写流
  const ws = fs.createWriteStream(`${filesDir}/write.txt`);
  // 暂停模式拷贝文件。在拷贝数据时,将原始数据暂停,然后将数据复制到另一个位置,适用于对数据完整性和一致性要求较高的场景
  rs.on('readable', () => {
    const data = rs.read();
    if (!data) {
      return;
    }
    ws.write(data);
  });
}

function copyFileWithData(): void {
  // 创建文件可读流
  const rs = fs.createReadStream(`${filesDir}/read.txt`);
  // 创建文件可写流
  const ws = fs.createWriteStream(`${filesDir}/write.txt`);
  // 流动模式拷贝文件。数据的读取和写入是同时进行的,不需要暂停原始数据的访问,适用于对数据实时性要求较高的场景
  rs.on('data', (emitData) => {
    const data = emitData?.data;
    if (!data) {
      return;
    }
    ws.write(data as Uint8Array);
  });
}
6、使用文件哈希流

哈希流是一种数据传输和存储技术,可以将任意长度的数据转换为固定长度的哈希值来验证数据的完整性和一致性。以下代码演示了如何使用文件哈希处理接口(ohos.file.hash)来处理文件哈希流。

// pages/xxx.ets
import { fileIo as fs } from '@kit.CoreFileKit';
import { hash } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;

function hashFileWithStream() {
  const filePath = `${filesDir}/test.txt`;
  // 创建文件可读流
  const rs = fs.createReadStream(filePath);
  // 创建哈希流
  const hs = hash.createHash('sha256');
  rs.on('data', (emitData) => {
    const data = emitData?.data;
    hs.update(new Uint8Array(data?.split('').map((x: string) => x.charCodeAt(0))).buffer);
  });
  rs.on('close', async () => {
    const hashResult = hs.digest();
    const fileHash = await hash.hash(filePath, 'sha256');
    console.info(`hashResult: ${hashResult}, fileHash: ${fileHash}`);
  });
}

2.4 应用文件分享

应用文件分享是应用之间通过分享URI(Uniform Resource Identifier)或文件描述符FD(File Descriptor)的方式,进行文件共享的过程。

2.4.1 应用可分享目录

沙箱路径物理路径说明
/data/storage/el1/base/data/app/el1/<currentUserId>/base/<PackageName>应用el1级别加密数据目录。
/data/storage/el2/base/data/app/el2/<currentUserId>/base/<PackageName>应用el2级别加密数据目录。
/data/storage/el2/distributedfiles/mnt/hmdfs/<currentUserId>/account/device_view/<networkId>/data/<PackageName>应用el2加密级别有账号分布式数据融合目录。

2.4.2 文件URI规范

文件URI的格式为:

格式为file://<bundleName>/<path>

  • file:文件URI的标志。

  • bundleName:该文件资源的属主。

  • path:文件资源在应用沙箱中的路径。

2.4.3 分享文件给其他应用

在分享文件给其他应用前,需要先获取应用文件路径

1. 获取文件在应用沙箱中的路径,并转换为文件URI。

import { UIAbility } from '@kit.AbilityKit';
import { fileUri } from '@kit.CoreFileKit';
import { window } from '@kit.ArkUI';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    // 获取文件的沙箱路径
    let pathInSandbox = this.context.filesDir + "/test1.txt";
    // 将沙箱路径转换为uri
    let uri = fileUri.getUriFromPath(pathInSandbox);
    // 获取的uri为"file://com.example.demo/data/storage/el2/base/files/test1.txt"
  }
}

2. 设置获取文件的权限以及选择要分享的应用。

分享文件给其他应用需要使用startAbility接口,将获取到的URI填充在want的参数URI中,标注URI的文件类型,type字段可参考want属性,并通过设置want的flag来设置对应的读写权限,action字段配置为"ohos.want.action.sendData"表示进行应用文件分享,开发示例如下。

import { fileUri } from '@kit.CoreFileKit';
import { window } from '@kit.ArkUI';
import { wantConstant } from '@kit.AbilityKit';
import { UIAbility } from '@kit.AbilityKit';
import { Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage) {
    // 获取文件沙箱路径
    let filePath = this.context.filesDir + '/test1.txt';
    // 将沙箱路径转换为uri
    let uri = fileUri.getUriFromPath(filePath);
    let want: Want  = {
      // 配置被分享文件的读写权限,例如对被分享应用进行读写授权
      flags: wantConstant.Flags.FLAG_AUTH_WRITE_URI_PERMISSION | wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION,
      // 配置分享应用的隐式拉起规则
      action: 'ohos.want.action.sendData',
      uri: uri,
      type: 'text/plain'
    }
    this.context.startAbility(want)
      .then(() => {
        console.info('Invoke getCurrentBundleStats succeeded.');
      })
      .catch((err: BusinessError) => {
        console.error(`Invoke startAbility failed, code is ${err.code}, message is ${err.message}`);
      });
  }
  // ...
}

图1 效果示意图:

2.4.4 使用其他应用分享的文件

被分享应用需要在module.json5配置文件的actions标签的值配置为"ohos.want.action.sendData",表示接收应用分享文件,配置uris字段,表示接收URI的类型,即只接收其他应用分享该类型的URI,如下表示本应用只接收scheme为file,类型为txt的文件,示例如下。

{
  "module": {
    //...
    "abilities": [
      {
        //...
        "skills": [
          {
            //...
            "actions": [
              "ohos.want.action.sendData"
            ],
            "uris": [
              {
                "scheme": "file",
                "type": "text/plain"
              }
           ]
          }
        ]
      }
    ]
  }
}

被分享方的UIAbility被启动后,可以在其onCreate()或者onNewWant回调中获取传入的want参数信息。

通过接口want的参数获取分享文件的URI,获取文件URI后通过fs.open接口打开文件,获取对应的file对象后,可对文件进行读写操作。

// xxx.ets
import { fileIo as fs } from '@kit.CoreFileKit';
import { Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

function getShareFile() {
  try {
    let want: Want = {}; // 此处实际使用时应该修改为获取到的分享方传递过来的want信息

    // 从want信息中获取uri字段
    let uri = want.uri;
    if (uri == null || uri == undefined) {
      console.info('uri is invalid');
      return;
    }
    try {
      // 根据需要对被分享文件的URI进行相应操作。例如读写的方式打开URI获取file对象
      let file = fs.openSync(uri, fs.OpenMode.READ_WRITE);
      console.info('open file successfully!');
    } catch (err) {
      let error: BusinessError = err as BusinessError;
      console.error(`Invoke openSync failed, code is ${error.code}, message is ${error.message}`);
    }
  } catch (error) {
    let err: BusinessError = error as BusinessError;
    console.error(`Invoke openSync failed, code is ${err.code}, message is ${err.message}`);
  }
}

2.5 应用数据备份恢复

2.5.1 应用数据备份恢复概述

BackupExtensionAbilityStage模型中扩展组件ExtensionAbility的派生类,用于提供备份及恢复应用数据的能力。它是一种无界面的扩展组件,随着备份恢复任务的启动而运行,随着备份恢复任务的结束而退出。

不同应用所需实现的场景不同,分为:

  • 应用接入数据备份恢复:应用均可以接入数据备份恢复,在接入后,应用可通过修改配置文件定制备份恢复框架的行为,包括是否允许备份恢复、备份哪些数据。

    应用本身无法触发数据的备份和恢复,仅能进行备份恢复的配置。

  • 应用触发数据备份恢复(仅对系统应用开放)

2.5.2 应用接入数据备份恢复

应用接入数据备份恢复需要通过BackupExtensionAbility实现。

BackupExtensionAbility,是Stage模型中扩展组件ExtensionAbility的派生类。开发者可以通过修改配置文件定制备份恢复框架的行为,包括是否允许备份恢复,备份哪些文件等。

1、接口说明

备份恢复扩展能力API的接口使用指导请参见BackupExtensionAbility API参考BackupExtensionContext API参考

2、约束与限制
  • 当备份恢复时,所有待备份文件及目录的路径不得超过4095字节,否则将导致未定义行为。
  • 当备份目录时,应用进程必须拥有读取该目录及其所有子目录的权限(DAC中的r),否则将导致备份失败。
  • 当备份文件时,应用进程必须拥有搜索该文件所有祖父级目录的权限(DAC中的x),否则将导致备份失败。
  • 当备份恢复时,所有待备份恢复的文件及目录不支持相对路径(../)和软链接。
3、开发步骤

1. 在应用配置文件module.json5中注册extensionAbilities相关配置

新增"extensionAbilities"字段,其中注册类型"type"设置为"backup",元数据信息"metadata"新增一个"name"为"ohos. extension. backup"的条目。

BackupExtensionAbility配置文件示例:

{
    "extensionAbilities": [
        {
            "description": "$string:ServiceExtAbility",
            "icon": "$media:icon",
            "name": "BackupExtensionAbility",
            "type": "backup",
            "exported": false,
            "metadata": [
                {
                    "name": "ohos.extension.backup",
                    "resource": "$profile:backup_config"
                }
            ],
            // 在BackupExtension.ets文件里自定义继承BackupExtensionAbility,重写其中的onBackup/onBackupEx和
            // onRestore/onRestoreEx方法,推荐使用onBackupEx/onRestoreEx。
            // 如果没有特殊要求可以空实现,则备份恢复服务会按照统一的备份恢复数据规则进行备份恢复。
            "srcEntry": "./ets/BackupExtension/BackupExtension.ets", 
        }      
    ]
}

2. 新增元数据资源配置文件

在元数据资源配置文件中,定义备份恢复时需要传输的文件。元数据资源配置文件名称需要与module.json5中"metadata.resource"例如"backup_config.json"名称保持一致,其保存位置在工程的resources/base/profile文件夹下。

元数据资源配置文件示例:

{
    "allowToBackupRestore": true,
    "includes": [
        "/data/storage/el2/base/files/users/"
    ],
    "excludes": [
        "/data/storage/el2/base/files/users/hidden/"
    ],
    "fullBackupOnly": false,
    "restoreDeps": ""
}

3. 在BackupExtension.ets文件中自定义类继承的BackupExtensionAbility,通过重写其onBackup/onBackupEx和onRestore/onRestoreEx方法,使其达到在备份预加工应用数据或者在恢复阶段加工待恢复文件。

如果没有特殊要求可以空实现,则备份恢复服务会按照统一的备份恢复数据规则进行备份恢复。

下面的示例展示了一个空实现的BackupExtension.ets文件:

//onBackup && onRestore
import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit';
import {hilog} from '@kit.PerformanceAnalysisKit';

const TAG = `FileBackupExtensionAbility`;
export default class BackupExtension extends  BackupExtensionAbility {
  //onBackup
  async onBackup ()   {
    hilog.info(0x0000, TAG, `onBackup ok`);
  }
  //onRestore
  async onRestore (bundleVersion : BundleVersion) {
    hilog.info(0x0000, TAG, `onRestore ok ${JSON.stringify(bundleVersion)}`);
    hilog.info(0x0000, TAG, `onRestore end`);
  }
}
//onBackupEx && onRestoreEx
import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit';

interface ErrorInfo {
  type: string,
  errorCode: number,
  errorInfo: string
}

class BackupExt extends BackupExtensionAbility {
  //onBackupEx
  async onBackupEx(backupInfo: string): Promise<string> {
    console.log(`onBackupEx ok`);
    let errorInfo: ErrorInfo = {
      type: "ErrorInfo",
      errorCode: 0,
      errorInfo: "app diy error info"       
    }
    return JSON.stringify(errorInfo);
  }

  // onRestoreEx
  async onRestoreEx(bundleVersion : BundleVersion, restoreInfo: string): Promise<string> {
    console.log(`onRestoreEx ok ${JSON.stringify(bundleVersion)}`);
    let errorInfo: ErrorInfo = {
      type: "ErrorInfo",
      errorCode: 0,
      errorInfo: "app diy error info"
    }
    return JSON.stringify(errorInfo);
  }
}
4、元数据资源配置文件说明
属性名称数据类型必填含义
allowToBackupRestore布尔值是否允许备份恢复,默认为false。
includes字符串数组

应用沙箱中需要备份的文件和目录。

当模式串以非/开始时,表示一个相对于根路径的相对路径。

includes支持的路径清单列表如下述代码段内容所示,当配置includes时请确保配置路径范围包含在其中。

当includes已配置时,备份恢复框架会采用开发者配置的模式串,否则将会采用下述代码段内容作为默认值。

excludes字符串数组

includes中无需备份的例外项。格式同includes。

在配置excludes时,请确保其范围在includes的子集中。

当excludes已配置时,备份恢复框架会采用开发者配置的模式串,否则将会采用空数组作为默认值。

fullBackupOnly布尔值是否使用应用默认恢复目录,默认值为false。当值为true时,恢复数据时会通过临时路径进行缓存,临时路径可通过backupDir获取。当值为false或者不配置该字段时,恢复数据会以'/'为根目录解压数据。
restoreDeps字符串不推荐使用,应用恢复时依赖其他应用数据,默认值为"",需要配置依赖应用名称。当前仅支持最多一个依赖项。配置的依赖仅在一次恢复任务上下文生效,如果一次恢复任务中没有检测到依赖应用,则忽略该依赖描述继续执行恢复任务。依赖应用未恢复或者恢复失败都会导致本应用恢复失败
extraInfojson串额外信息可通过该字段传递。

说明

1、有关fullBackupOnly字段的说明

  • 当fullBackupOnly为false时,恢复数据会以 / 为根目录解压数据,同路径下的同名文件会被覆盖。
  • 当fullBackupOnly为true时,恢复数据会以临时目录为根目录解压数据,开发者需要在OnRestore/OnRestoreEx内自行实现恢复数据的逻辑,进行最终的恢复。

includes支持的路径清单列表如下:

{
    "includes": [
    "data/storage/el1/database/",
    "data/storage/el1/base/files/",
    "data/storage/el1/base/preferences/",
    "data/storage/el1/base/haps/*/files/",
    "data/storage/el1/base/haps/*/preferences/",
    "data/storage/el2/database/",
    "data/storage/el2/base/files/",
    "data/storage/el2/base/preferences/",
    "data/storage/el2/base/haps/*/files/",
    "data/storage/el2/base/haps/*/preferences/",
    "data/storage/el2/distributedfiles/",
    "data/storage/el5/database/",
    "data/storage/el5/base/files/",
    "data/storage/el5/base/preferences/",
    "data/storage/el5/base/haps/*/files/",
    "data/storage/el5/base/haps/*/preferences/"
    ]
}

3 用户文件

3.1 用户文件概述

用户文件:登录到该终端设备的用户所拥有的文件,包括用户私有的图片、视频、音频、文档等。

  1. 用户文件存放在用户目录下,归属于该设备上登录的用户。

  2. 用户文件存储位置主要分为内置存储外置存储

  3. 应用对用户文件的创建、访问、删除等行为,需要提前获取用户授权,或由用户操作完成。

3.1.1 用户文件存储位置

1、内置存储

内置存储,是指用户文件存储在终端设备的内部存储设备(空间)上。内置存储设备无法被移除。内置存储的用户文件主要有:

  • 用户特有的文件:这部分文件归属于登录该设备的用户,不同用户登录后,仅可看到该用户自己的文件。

    按照这些文件的特征/属性,以及用户/应用的使用习惯,可分为:

    • 图片/视频类媒体文件

      所具有的特征包括拍摄时间、地点、旋转角度、文件宽高等信息,以媒体文件的形式存储在系统中,通常是以所有文件、相册的形式对外呈现,不会展示其在系统中存储的具体位置。

    • 音频类媒体文件

      所具有的特征包括所属专辑、音频创作者、持续时间等信息,以媒体文件的形式存储在系统中,通常会以所有文件、专辑、作家等形式对外部呈现,不会展示其在系统中存储的具体位置。

    • 其他文件(统称为文档类文件)

      以普通文件的形式存储在系统中,该类文件既包括普通的文本文件、压缩文件等,又包括以普通文件形式存储的图片/视频、音频文件,该类文件通常是以目录树的形式对外展示。

  • 多用户共享的文件:用户可以通过将文件放在共享文件区,实现多个用户之间文件的共享访问。

    共享文件区的文件,也是以普通文件的形式存储在系统中,以目录树的形式对外展示。

2、外置存储

外置存储,是指用户文件存储在外置可插拔设备上(如SD卡、U盘等)。外置存储设备上的文件,和内置存储设备共享区文件一样,可以被所有登录到系统中的用户看到。

外置存储设备具备可插拔属性,因此系统提供了设备插拔事件的监听及挂载功能,用于管理外置存储设备,该部分功能仅对系统应用开放。

外置存储设备上的文件,全部以普通文件的形式呈现,和内置存储设备上的文档类文件一样,采用目录树的形式对外展示。

3.2 用户文件uri介绍

用户文件uri是文件的唯一标识,在对用户文件进行访问与修改等操作时往往都会使用到uri,不建议开发者解析uri中的片段用于业务代码开发,不同类型的uri使用方式将在下文详细介绍。

3.2.1 uri的类型

uri类型可以归纳为文档类uri和媒体文件uri两类

  • 文档类uri:由picker拉起文件管理器选择或保存返回,以及通过fileAccess模块获取。具体获取方式参见文档类uri获取方式
  • 媒体文件uri:由picker通过拉起图库选择图片或者视频返回,通过photoAccessHelper模块获取图片或者视频文件的uri,以及通过userFileManager模块获取图片、视频或者音频文件的uri。具体获取方式参见媒体文件uri获取方式

3.2.2 文档类uri

1、文档类uri介绍

文档类uri的格式类型为:

'file://docs/storage/Users/currentUser/<relative_path>/test.txt'

其中各个字段表示的含义为:

uri字段说明
'file://docs/storage/Users/currentUser/'文件管理器的根目录。
'<relative_path>/'文件在根目录下的相对路径。例如:'Download/'和'Documents/'。
'test.txt'用户文件系统中存储的文件名,支持的文件类型为文件管理器支持的所有类型,以文件管理器为准。例如txt、jpg、mp4和mp3等格式的文件。
2、文档类uri获取方式
  1. 通过DocumentViewPicker接口选择或保存文件,返回选择或保存的文件uri。

  2. 通过AudioViewPicker接口选择或保存文件,返回选择或保存的文件uri。

3、文档类uri的使用方式

normal等级的应用使用此类uri的方式只能通过fs模块进行进一步处理,其他模块使用此uri是会报没有权限的错误。示例代码参见picker中的选择文档类文件保存文档类文件

3.2.3 媒体文件uri

1、媒体文件uri介绍

媒体文件uri的格式类型为:

图片uri格式

  • 'file://media/Photo/<id>/IMG_datetime_0001/displayName.jpg'

视频uri格式

  • 'file://media/Photo/<id>/VID_datetime_0001/displayName.mp4'

音频uri格式

  • 'file://media/Audio/<id>/AUD_datetime_0001/displayName.mp3'

其中各个字段表示的含义为:

uri字段说明
'file://media'表示这个uri是媒体文件。
'Photo'表示这个uri是媒体文件中的图片或者视频类文件。
'Audio'表示这个uri是媒体文件中的音频类文件。
'<id>'表示在数据库中多个表中处理后的值,并不是指表中的file_id列,注意请不要使用此id去数据库中查询具体文件。
'IMG_datetime_0001'表示图片文件在用户文件系统中存储的文件名去掉后缀剩下的部分。
'VID_datetime_0001'表示视频文件在用户文件系统中存储的文件名去掉后缀剩下的部分。
'AUD_datetime_0001'表示音频文件在用户文件系统中存储的文件名去掉后缀剩下的部分。
2、媒体文件uri获取方式
  1. 通过PhotoAccessHelper的PhotoViewPicker选择媒体文件,返回选择的媒体文件文件的uri。

  2. 通过photoAccessHelper模块中的getAssetscreateAsset接口获取媒体文件对应文件的uri。

3、媒体文件uri的使用方式

以下为PhotoKeys中支持临时授权方式可以读取的信息:

名称说明
URI'uri'文件uri。
PHOTO_TYPE'media_type'媒体文件类型。
DISPLAY_NAME'display_name'显示名字。
SIZE'size'文件大小。
DATE_ADDED'date_added'文件创建时的Unix时间戳(单位:秒)。
DATE_MODIFIED'date_modified'文件修改时的Unix时间戳(单位:秒)。修改文件名不会改变此值,当文件内容发生修改时才会更新。
DURATION'duration'持续时间(单位:毫秒)。
WIDTH'width'图片宽度(单位:像素)。
HEIGHT'height'图片高度(单位:像素)。
DATE_TAKEN'date_taken'拍摄时的Unix时间戳(单位:秒)。
ORIENTATION'orientation'图片文件的方向。
TITLE'title'文件标题。

下面为通过临时授权方式使用媒体文件uri进行获取缩略图和读取文件部分信息的示例代码:

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { dataSharePredicates } from '@kit.ArkData';

// 定义一个uri数组,用于接收PhotoViewPicker选择图片返回的uri
let uris: Array<string> = [];
const context = getContext(this);

// 调用PhotoViewPicker.select选择图片
async function photoPickerGetUri() {
  try {  
    let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
    PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
    PhotoSelectOptions.maxSelectNumber = 1;
    let photoPicker = new photoAccessHelper.PhotoViewPicker();
    photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => {
      console.info('PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(PhotoSelectResult));
      uris = PhotoSelectResult.photoUris;
    }).catch((err: BusinessError) => {
      console.error('PhotoViewPicker.select failed with err: ' + JSON.stringify(err));
    });
  } catch (error) {
    let err: BusinessError = error as BusinessError;
    console.error('PhotoViewPicker failed with err: ' + JSON.stringify(err));
  }
}

async function uriGetAssets() {
try {
    let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
    let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();
    // 配置查询条件,使用PhotoViewPicker选择图片返回的uri进行查询
    predicates.equalTo('uri', uris[0]);
    let fetchOption: photoAccessHelper.FetchOptions = {
      fetchColumns: [photoAccessHelper.PhotoKeys.WIDTH, photoAccessHelper.PhotoKeys.HEIGHT, photoAccessHelper.PhotoKeys.TITLE, photoAccessHelper.PhotoKeys.DURATION],
      predicates: predicates
    };
    let fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> = await phAccessHelper.getAssets(fetchOption);
    // 得到uri对应的PhotoAsset对象,读取文件的部分信息
    const asset: photoAccessHelper.PhotoAsset = await fetchResult.getFirstObject();
    console.info('asset displayName: ', asset.displayName);
    console.info('asset uri: ', asset.uri);
    console.info('asset photoType: ', asset.photoType);
    console.info('asset width: ', asset.get(photoAccessHelper.PhotoKeys.WIDTH));
    console.info('asset height: ', asset.get(photoAccessHelper.PhotoKeys.HEIGHT));
    console.info('asset title: ' + asset.get(photoAccessHelper.PhotoKeys.TITLE));
    // 获取缩略图
    asset.getThumbnail((err, pixelMap) => {
      if (err == undefined) {
        console.info('getThumbnail successful ' + JSON.stringify(pixelMap));
      } else {
        console.error('getThumbnail fail', err);
      }
    });
  } catch (error){
    console.error('uriGetAssets failed with err: ' + JSON.stringify(error));
  }
}

3.3 选择用户文件

用户需要分享文件、保存图片、视频等用户文件时,开发者可以通过系统预置的文件选择器(FilePicker),实现该能力。通过Picker访问相关文件,将拉起对应的应用,引导用户完成界面操作,接口本身无需申请权限。picker获取的uri只具有临时权限,获取持久化权限需要通过FilePicker设置永久授权方式获取。

根据用户文件的常见类型,选择器(FilePicker)分别提供以下选项:

  • PhotoViewPicker:适用于图片或视频类型文件的选择与保存(该接口在后续版本不再演进)。请使用PhotoAccessHelper的PhotoViewPicker来选择图片文件。请使用安全控件保存媒体库资源

  • DocumentViewPicker:适用于文件类型文件的选择与保存。DocumentViewPicker对接的选择资源来自于FilePicker, 负责文件类型的资源管理,文件类型不区分后缀,比如浏览器下载的图片、文档等,都属于文件类型。

  • AudioViewPicker:适用于音频类型文件的选择与保存。AudioViewPicker目前对接的选择资源来自于FilePicker。

3.3.1 选择图片或视频类文件

PhotoViewPicker在后续版本不再演进,请PhotoAccessHelper的PhotoViewPicker来选择图片文件。

3.3.2 选择文档类文件

1. 导入选择器模块和基础文件API模块。

import  { picker } from '@kit.CoreFileKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

2. 创建文件类型、文件选择选项实例。

const documentSelectOptions = new picker.DocumentSelectOptions();
// 选择文档的最大数目(可选)。
documentSelectOptions.maxSelectNumber = 5;
// 指定选择的文件或者目录路径(可选)。
documentSelectOptions.defaultFilePathUri = "file://docs/storage/Users/currentUser/test";
// 选择文件的后缀类型['后缀类型描述|后缀类型'](可选,不传该参数,默认不过滤,即显示所有文件),若选择项存在多个后缀名,则每一个后缀名之间用英文逗号进行分隔(可选),后缀类型名不能超过100。此外2in1设备支持通过通配符方式['所有文件(*.*)|.*'],表示为显示所有文件,手机暂不支持该配置。
 documentSelectOptions.fileSuffixFilters = ['图片(.png, .jpg)|.png,.jpg', '文档|.txt', '视频|.mp4', '.pdf']; 
//选择是否对指定文件或目录授权,true为授权,当为true时,defaultFilePathUri为必选参数,拉起文管授权界面;false为非授权(默认为false),拉起常规文管界面(可选),仅支持2in1设备。
documentSelectOptions.authMode = false;
//批量授权模式,默认为false(非批量授权模式)。当multAuthMode为true时为批量授权模式。当multAuthMode为true时,只有multiUriArray参数生效,其他参数不生效。仅支持手机设备。
documentSelectOptions.multiAuthMode = false;
//需要传入批量授权的uri数组(仅支持文件,文件夹不生效)。配合multAuthMode使用。当multAuthMode为false时,配置该参数不生效。仅支持手机设备。
documentSelectOptions.multiAuthMode = ["file://docs/storage/Users/currentUser/test", "file://docs/storage/Users/currentUser/2test"];
//开启聚合视图模式,支持拉起文件管理应用的聚合视图。默认为DEFAULT,表示该参数不生效,非聚合视图。当该参数置为非DEFAULT时,其他参数不生效。仅支持手机设备。
documentSelectOptions.mergeMode = picker.MergeTypeMode.DEFAULT;

3. 创建文件选择器DocumentViewPicker实例。调用select()接口拉起FilePicker应用界面进行文件选择。

let uris: Array<string> = [];
let context = getContext(this) as common.Context; // 请确保 getContext(this) 返回结果为 UIAbilityContext
// 创建文件选择器实例
const documentViewPicker = new picker.DocumentViewPicker(context);
documentViewPicker.select(documentSelectOptions).then((documentSelectResult: Array<string>) => {
  //文件选择成功后,返回被选中文档的uri结果集。
  uris = documentSelectResult;
  console.info('documentViewPicker.select to file succeed and uris are:' + uris);
}).catch((err: BusinessError) => {
  console.error(`Invoke documentViewPicker.select failed, code is ${err.code}, message is ${err.message}`);
})

注意

1、使用picker获取的select()返回的uri权限是临时只读权限,待退出应用后台后,获取的临时权限就会失效。

2、如果想要获取持久化权限(仅在2in1设备上生效),请参考文件持久化授权访问

3、开发者可以根据结果集中uri做进一步的处理。建议定义一个全局变量保存uri。

4、如有获取元数据需求,可以通过基础文件API文件URI根据uri获取部分文件属性信息,比如文件大小、访问时间、修改时间、文件名、文件路径等。

let uri: string = '';
//这里需要注意接口权限参数是fs.OpenMode.READ_ONLY。
let file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
console.info('file fd: ' + file.fd);
  • 通过fd使用fs.readSync接口读取这个文件内的数据。
let buffer = new ArrayBuffer(4096);
let readLen = fs.readSync(file.fd, buffer);
console.info('readSync data to file succeed and buffer size is:' + readLen);
//读取完成后关闭fd。
fs.closeSync(file);

3.4 保存用户文件

在从网络下载文件到本地或将已有用户文件另存为新的文件路径等场景下,需要使用FilePicker提供的保存用户文件的能力。需关注以下关键点:

权限说明

  • 通过Picker获取的URI默认只具备临时读写权限,临时授权在应用退出后台自动失效。
  • 获取持久化权限需要通过FilePicker设置永久授权方式获取。(仅限2in1设备)。
  • 使用picker对音频、图片、视频、文档类文件的保存操作无需申请权限

系统隔离说明

  • 通过Picker保存的文件存储在用户指定的目录。此类文件与图库管理的资源隔离,无法在图库中看到。
  • 若开发者需要保存图片、视频资源到图库,可使用用户无感的安全控件进行保存

3.4.1 保存图片或视频类文件

PhotoViewPicker在后续版本不再演进,建议使用Media Library Kit(媒体文件管理服务)中能力来保存媒体库资源

如果开发场景无法调用安全控件进行图片、视频保存,可使用相册管理模块PhotoAccessHelper.showAssetsCreationDialog接口进行保存操作。

3.4.2 保存文档类文件

1. 模块导入。

import { picker } from '@kit.CoreFileKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';

2. 配置保存选项。

// 创建文件管理器选项实例。
const documentSaveOptions = new picker.DocumentSaveOptions();
// 保存文件名(可选)。 默认为空。
documentSaveOptions.newFileNames = ["DocumentViewPicker01.txt"];
// 保存文件类型['后缀类型描述|后缀类型'],选择所有文件:'所有文件(*.*)|.*'(可选) ,如果选择项存在多个后缀(最大限制100个过滤后缀),默认选择第一个。如果不传该参数,默认无过滤后缀。
documentSaveOptions.fileSuffixChoices = ['文档|.txt', '.pdf']; 

3 创建文件选择器DocumentViewPicker实例。调用save()接口拉起FilePicker界面进行文件保存。

let uris: Array<string> = [];
// 请确保 getContext(this) 返回结果为 UIAbilityContext
let context = getContext(this) as common.Context;
const documentViewPicker = new picker.DocumentViewPicker(context);
documentViewPicker.save(documentSaveOptions).then((documentSaveResult: Array<string>) => {
  uris = documentSaveResult;
  console.info('documentViewPicker.save to file succeed and uris are:' + uris);
}).catch((err: BusinessError) => {
  console.error(`Invoke documentViewPicker.save failed, code is ${err.code}, message is ${err.message}`);
})

4. 待界面从FilePicker返回后,使用基础文件API的fs.openSync接口,通过URI打开这个文件得到文件描述符(fd)。

const uri = '';
//这里需要注意接口权限参数是fs.OpenMode.READ_WRITE。
let file = fs.openSync(uri, fs.OpenMode.READ_WRITE);
console.info('file fd: ' + file.fd);

5. 通过(fd)使用基础文件API的fs.writeSync接口对这个文件进行编辑修改,编辑修改完成后关闭(fd)。

let writeLen: number = fs.writeSync(file.fd, 'hello, world');
console.info('write data to file succeed and size is:' + writeLen);
fs.closeSync(file);

4 分布式文件系统

4.1 分布式文件系统概述

分布式文件系统(hmdfs,HarmonyOS Distributed File System)提供跨设备的文件访问能力,适用于如下场景:

  • 两台设备组网,用户可以利用一台设备上的编辑软件编辑另外一台设备上的文档。

  • 平板保存的音乐,车载系统直接可见并可播放。

  • 户外拍摄的照片,回家打开平板直接访问原设备拍摄的照片。

hmdfs在分布式软总线动态组网的基础上,为网络上各个设备结点提供一个全局一致的访问视图,支持开发者通过基础文件系统的接口进行读写访问,具有高性能、低延时等优点。

4.1.1 分布式文件系统架构

  • distributedfile_daemon:主要负责设备上线监听、通过软总线建立链路,并根据分布式的设备安全等级执行不同的数据流转策略。

  • hmdfs:实现在内核的网络文件系统,包括缓存管理、文件访问、元数据管理和冲突管理等。

    • 缓存管理
      • 设备分布式组网后,hmdfs提供文件的互访能力,但不会主动进行文件数据传输和拷贝。如果应用需要将数据保存到本地,需主动拷贝。
      • hmdfs保证Close-to-Open的一致性,即一端写关闭后,另外一端可以读取到最新数据,不保证文件内容的实时一致性。
      • 数据在远端写入,但是由于网络原因未及时回刷,文件系统会在下次网络接入时回刷本地,但是如果远端已修改则无法回刷。
    • 文件访问
      • 文件访问接口与本地一致(ohos.file.fs)。
      • 如果文件在本地,则堆叠访问本地文件系统。
      • 如果文件在其他设备,则同步网络访问远端设备文件。

        说明

        symlink:不支持。

    • 元数据管理
      • 分布式组网下,文件一端创建、删除、修改,另一端可以“立即”查看到最新文件,看到速度取决于网络情况。
      • 远端设备离线后,该设备数据将不再在本端设备呈现。但由于设备离线的感知具有延迟,可能会造成部分消息4s超时,因此开发者需要考虑接口的网络超时或一些文件虽然可以看到,但实际设备可能已离线的场景。
    • 冲突处理
      • 本地与远端冲突 ,远端文件被重命名,看到的同名文件是本地同名文件,远端文件被重命名。
      • 远端多个设备冲突,以接入本设备ID为顺序,显示设备ID小的同名文件,其他文件被依次重命名。
      • 如果组网场景,目录树下已经有远端文件,创建同名文件,提示文件已存在。
      • 冲突文件显示_conflict_dev后依次加id,id从1自动递增。
      • 同名目录之间仅融合不存在冲突,文件和远端目录同名冲突,远端目录后缀加_remote_directory。

4.2 设置分布式文件数据等级

不同设备本身的安全能力差异较大,一些小的嵌入式设备安全能力远弱于平板等设备类型。用户或者应用不同的文件数据有不同安全诉求,例如个人的健康信息和银行卡信息等不期望被弱设备读取。因此,HarmonyOS提供一套完整的数据分级、设备分级标准,并针对不同设备制定不同的数据流转策略,具体规则请参见数据、设备安全分级

4.2.1 接口说明

API详细介绍请参见ohos.file.securityLabel

表1 设置文件数据等级

接口名功能接口类型支持同步支持异步
setSecurityLabel设置文件安全标签方法
getSecurityLabel获取文件安全标签方法

注意

  1. 对于不满足安全等级的文件,跨设备仍然可以看到该文件,但是无权限打开访问该文件。

  2. 分布式文件系统的数据等级默认为S3,应用可以主动设置文件的安全等级。

4.2.2 开发示例

1. 获取通用文件沙箱路径,并设置数据等级标签。示例中的context的获取方式请参见获取UIAbility的上下文信息

import { securityLabel } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import { fileIo as fs } from '@kit.CoreFileKit';

// 获取需要设备数据等级的文件沙箱路径
let context = getContext(this) as common.UIAbilityContext; // 获取UIAbilityContext信息
let pathDir = context.filesDir;
let filePath = pathDir + '/test.txt';

//打开文件
let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
// 设置文件的数据等级为s0
securityLabel.setSecurityLabel(filePath, 's0').then(() => {
  console.info('Succeeded in setSecurityLabeling.');
  fs.closeSync(file);
}).catch((err: BusinessError) => {
  console.error(`Failed to setSecurityLabel. Code: ${err.code}, message: ${err.message}`);
});

4.3 跨设备文件访问

分布式文件系统为应用提供了跨设备文件访问的能力,开发者在两个设备安装同一应用时,通过基础文件接口,可跨设备读写另一个设备该应用分布式文件路径(/data/storage/el2/distributedfiles/)下的文件。例如:多设备数据流转的场景,设备组网互联之后,设备A上的应用可访问设备B同应用分布式路径下的文件,当期望应用文件被其他设备访问时,只需将文件移动到分布式文件路径即可。

4.3.1 开发步骤

1. 完成分布式组网。

  • 将需要跨设备访问的两个设备登录同一账号,保证设备蓝牙和Wi-Fi功能开启,蓝牙无需互连,Wi-Fi无需接入同一个局域网。

2. 访问跨设备文件。

  • 同一应用不同设备之间实现跨设备文件访问,只需要将对应的文件放在应用沙箱的分布式文件路径即可。
  • 设备A上在分布式路径下创建测试文件,并写入内容。示例中的context的获取方式请参见获取UIAbility的上下文信息
import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

let context = getContext(this) as common.UIAbilityContext; // 获取设备A的UIAbilityContext信息
let pathDir: string = context.distributedFilesDir;
// 获取分布式目录的文件路径
let filePath: string = pathDir + '/test.txt';

try {
  // 在分布式目录下创建文件
  let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  console.info('Succeeded in createing.');
  // 向文件中写入内容
  fs.writeSync(file.fd, 'content');
  // 关闭文件
  fs.closeSync(file.fd);
} catch (error) {
  let err: BusinessError = error as BusinessError;
  console.error(`Failed to openSync / writeSync / closeSync. Code: ${err.code}, message: ${err.message}`);
} 

 设备B主动向设备A发起建链,建链成功后设备B可在分布式路径下读取测试文件。

import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { buffer } from '@kit.ArkTS';
import { distributedDeviceManager } from '@kit.DistributedServiceKit'

// 通过分布式设备管理的接口获取设备A的networkId信息
let dmInstance = distributedDeviceManager.createDeviceManager("com.example.hap");
let deviceInfoList: Array<distributedDeviceManager.DeviceBasicInfo> = dmInstance.getAvailableDeviceListSync();
let networkId = deviceInfoList[0].networkId;

// 定义访问公共文件目录的回调
let listeners : fs.DfsListeners = {
  onStatus: (networkId: string, status: number): void => {
    console.info('Failed to access public directory');
  }
}

// 访问并挂载公共文件目录
fs.connectDfs(networkId, listeners).then(() => {
  console.info("Success to connectDfs");
  let context = getContext(); // 获取设备B的UIAbilityContext信息
  let pathDir: string = context.distributedFilesDir;
  // 获取分布式目录的文件路径
  let filePath: string = pathDir + '/test.txt';

  try {
    // 打开分布式目录下的文件
    let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE);
    // 定义接收读取数据的缓存
    let arrayBuffer = new ArrayBuffer(4096);
    // 读取文件的内容,返回值是读取到的字节个数
    class Option {
        public offset: number = 0;
        public length: number = 0;
    }
    let option = new Option();
    option.length = arrayBuffer.byteLength;
    let num = fs.readSync(file.fd, arrayBuffer, option);
    // 打印读取到的文件数据
    let buf = buffer.from(arrayBuffer, 0, num);
    console.info('read result: ' + buf.toString());
  } catch (error) {
    let err: BusinessError = error as BusinessError;
    console.error(`Failed to openSync / readSync. Code: ${err.code}, message: ${err.message}`);
  }
}).catch((error: BusinessError) => {
  let err: BusinessError = error as BusinessError;
  console.error(`Failed to connectDfs Code: ${err.code}, message: ${err.message}`);
});

3. B设备访问跨设备文件完成,断开链路。

import { BusinessError } from '@kit.BasicServicesKit';
import { distributedDeviceManager } from '@kit.DistributedServiceKit'
import { fileIo as fs } from '@kit.CoreFileKit';

// 获取设备A的networkId
let dmInstance = distributedDeviceManager.createDeviceManager("com.example.hap");
let deviceInfoList: Array<distributedDeviceManager.DeviceBasicInfo> = dmInstance.getAvailableDeviceListSync();
let networkId = deviceInfoList[0].networkId;

// 取消公共文件目录挂载
fs.disconnectDfs(networkId).then(() => {
  console.info("Success to disconnectDfs");
}).catch((error: BusinessError) => {
  let err: BusinessError = error as BusinessError;
  console.error(`Failed to disconnectDfs Code: ${err.code}, message: ${err.message}`)
})

4.4 跨设备文件拷贝

分布式文件系统为应用提供了跨设备文件拷贝的能力,开发者在跨设备跨应用进行文件拷贝时,通过基础文件接口,可跨设备跨应用拷贝文件。例如:多设备数据流转的场景,设备组网互联之后,设备A上的应用可在复制时,将A设备的沙箱文件,拷贝到A设备的分布式路径下,设备B在粘贴的时候,从设备B的分布式路径下,将文件拷贝到对应的沙箱文件中。

4.4.1 开发步骤

1. 完成分布式组网。

  • 首先将需要进行跨设备访问的设备连接到同一局域网中,同账号认证完成组网。

2. 拷贝跨设备文件。 同一应用不同设备之间实现跨设备文件拷贝,只需要将对应的文件放在应用沙箱的分布式文件路径即可。

  • 将A设备的待拷贝沙箱文件,拷贝到A设备的分布式路径下。

 

import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileUri } from '@kit.CoreFileKit';

let context = getContext(this) as common.UIAbilityContext; // 获取设备A的UIAbilityContext信息
let pathDir: string = context.filesDir;
let distributedPathDir: string = context.distributedFilesDir;
// 待拷贝文件沙箱路径
let filePath: string = pathDir + '/src.txt';

try {
 // 文件不存在时,需要创建文件并写入内容
 let file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
 fs.writeSync(file.fd, 'Create file success');
 fs.closeSync(file);
} catch (error) {
 console.error(`Failed to createFile. Code: ${error.code}, message: ${error.message}`);
}

// 获取待拷贝文件uri
let srcUri = fileUri.getUriFromPath(filePath);

// 将待拷贝的沙箱文件,拷贝到分布式目录下
let destUri: string = fileUri.getUriFromPath(distributedPathDir + '/src.txt');

try {
 // 将沙箱路径下的文件拷贝到分布式路径下
 fs.copy(srcUri, destUri).then(()=>{
   console.info("Succeeded in copying---. ");
   console.info("src: " + srcUri + "dest: " + destUri);
 }).catch((error: BusinessError)=>{
   let err: BusinessError = error as BusinessError;
   console.info(`Failed to copy. Code: ${err.code}, message: ${err.message}`);
 })
} catch (error) {
 console.error(`Failed to getData. Code: ${error.code}, message: ${error.message}`);
}

B设备在获取A端沙箱文件时,从B设备的分布式路径下将对应的文件拷贝走,以此完成跨设备拷贝。

import { fileIo as fs } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileUri } from '@kit.CoreFileKit';

let context = getContext(this) as common.UIAbilityContext; // 获取设备B的UIAbilityContext信息
let pathDir: string = context.filesDir;
let distributedPathDir: string = context.distributedFilesDir;
// 待拷贝文件的目标沙箱路径
let filePath: string = pathDir + '/dest.txt';

// 获取目标路径uri
let destUri = fileUri.getUriFromPath(filePath);

// 获取分布式路径下的源文件
let srcUri: string = fileUri.getUriFromPath(distributedPathDir + '/src.txt');

// 定义拷贝回调
let progressListener: fs.ProgressListener = (progress: fs.Progress) => {
  console.info(`progressSize: ${progress.processedSize}, totalSize: ${progress.totalSize}`);
};
let options: fs.CopyOptions = {
  "progressListener" : progressListener
}

try {
 // 将分布式路径下的文件拷贝到其他沙箱路径下
 fs.copy(srcUri, destUri, options).then(()=>{
   console.info("Succeeded in copying of paste. ");
   console.info("src: " + srcUri + "dest: " + destUri); // file://com.example.myapplication/data/storage/el2/distributedfiles/src.txt
 }).catch((error: BusinessError)=>{
   let err: BusinessError = error as BusinessError;
   console.info(`Failed to copy. Code: ${err.code}, message: ${err.message}`);
 })
} catch (error) {
 console.error(`Failed to copy. Code: ${error.code}, message: ${error.message}`);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值