rawfile目录下文件夹如何复制到沙箱目录并读写
=========================

#### 问题现象

如何实现rawfile目录下文件夹复制到沙箱目录，并支持读写功能？

#### 背景知识

应用资源文件目录分为在base目录、限定词目录、rawfile目录、resfile目录。而rawfile目录、resfile目录通常存放其他类型文件（例如txt文件、db文件等等），原始文件形式保存。rawfile目录和resfile目录同在[资源目录](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/resource-categories-and-access#资源目录)下，目录中的资源文件会被直接打包进应用，不经过编译，也不会分配资源ID。二者的主要不同之处在于访问方式：

*   resfile目录：应用安装后，resfile资源会被解压到应用沙箱路径，通过Context属性resourceDir获取到resfile资源目录后，可通过文件路径以只读权限访问。
*   rawfile目录：通过文件路径和文件名进行访问（"$rawfile('filename')"）；或者通过Context获取[resourceManager](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-resource-manager)后，调用资源管理接口访问。

应用沙箱是一种以安全防护为目的的隔离机制，避免数据受到恶意路径穿越访问。在这种沙箱的保护机制下，应用可见的目录范围即为“[沙箱目录](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/app-sandbox-directory)”。通常rawfile/resfile目录下文件/文件夹无法直接在应用中读写，需要复制到应用沙箱下才可以读写。

#### 解决方案

假设rawfile目录下存在apps文件夹（路径：rawfile/apps），需将其复制到沙箱目录。由于rawfile目录下的文件夹无法跟随应用安装解压到沙箱目录，若需要将整个文件夹拷贝到沙箱目录，需要进行递归遍历文件夹等操作，不推荐。因此目前有以下两种推荐方案：

*   **方案一：rawfile目录下的文件夹直接复制到resfile目录下（resfile/apps），然后再复制到沙箱。**由于resfile下文件夹随应用安装解压到应用沙箱目录，但是仅能以只读方式访问，所以也需要将其文件复制到沙箱目录下进行读写。
    1.  通过context.resourceDir获取resfile目录，context.filesDir获取沙箱目录。
    2.  使用[fs.copyDirSync()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-file-fs#fscopydirsync10)把resfile/apps直接复制到沙箱目录。
*   **方案二：把需要复制的目录压缩成zip，复制zip并解压到沙箱目录。**
    1.  通过[getRawFd()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-resource-manager#getrawfd9)获取rawfile/apps.zip所在hap包的descriptor信息（示例代码中的参数data）。
    2.  使用buffer将rawfile/apps.zip文件内容复制到沙箱临时文件路径（示例代码中的参数filepath）。
    3.  使用[zlib.decompressFile()](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-zlib#zlibdecompressfile9)解压zip文件至沙箱通用文件路径（示例代码中的参数sandboxPath）。

完整示例参考如下：

import { fileIo } from '@kit.CoreFileKit';
import { zlib } from '@kit.BasicServicesKit';
import type { common } from '@kit.AbilityKit';
import type { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct RawfileToSandbox {
  context \= this.getUIContext().getHostContext() as common.UIAbilityContext;

  build() {
    Column() {
      _//_ _方案一：rawfile目录下的文件夹直接复制到resfile目录下（resfile/apps），然后再复制到沙箱。_
      Button('复制resFile目录下文件夹到沙箱目录')
        .height(100)
        .width('50%')
        .onClick(() \=\> {
          try {
            let srcPath \= this.context.resourceDir \+ '/apps/';
            let destPath \= this.context.filesDir \+ '/apps/';
            _//_ _判断文件夹是否存在_
            if (!fileIo.accessSync(destPath)) {
              fileIo.mkdirSync(destPath);
            }
            fileIo.copyDirSync(srcPath, destPath, 0);
          } catch (error) {
            let err: BusinessError \= error as BusinessError;
            console.error(\`copy directory failed with error message: ${JSON.stringify(err)}\`);
          }
        });

    _  // 方案二：把需要复制的目录压缩成zip，复制zip并解压到沙箱目录。_
      Button('复制zip到沙箱，并解压zip')
        .height(100)
        .width('50%')
        .onClick(async () \=\> {
 _         // 通过fd来进行拷贝，避免文件过大的内存占用问题_
          _// data.fd是hap包的fd，data.offset表示目标文件在hap包中的偏移，data.length表示目标文件的长度_
          this.context.resourceManager.getRawFd('apps.zip', async (err, data) \=\> {
            try {
              let sandboxPath \= this.context.filesDir;
              console.info(\`沙箱路径：${sandboxPath}\`);
              let filePath \= this.context.tempDir \+ '/bfapps.zip';
              console.info(\`压缩文件路径：${filePath}\`);
              let dest \= fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ\_WRITE);
              let bufsize \= 4096;
              let buf \= new ArrayBuffer(bufsize);
              let off \= 0;
              let readLen \= 0;

              _// 把rawfile压缩包文件内容复制到沙箱路径_
              let len \= fileIo.readSync(data.fd, buf, { offset: data.offset \+ off, length: bufsize });
              while (len) {
                fileIo.writeSync(dest.fd, buf, { offset: off, length: len });
                readLen += len;
                if (readLen \>\= data.length) {
                  break;
                }
                off += len;
                if ((data.length \- readLen) < bufsize) {
                  bufsize \= data.length \- readLen;
                }
                len \= fileIo.readSync(data.fd, buf, { offset: data.offset \+ off, length: bufsize });
              }
              fileIo.closeSync(dest.fd);

             _ // 对沙箱路径下的压缩文件进行解压_
              await zlib.decompressFile(filePath, sandboxPath);
              this.context.resourceManager.closeRawFd('apps.zip');
            } catch (e) {
              console.error(\`failed, error = ${JSON.stringify(e)}\`);
            }
          });
        });

    }
    .height(300)
    .width('100%')
    .justifyContent(FlexAlign.SpaceAround);
  }
}

#### 常见FAQ

Q：zlib.decompressFile()的参数inFile和outFile可以取到项目内的路径吗？

A：inFile和outFile文件路径必须为沙箱路径。

Q：为什么context.getApplicationContext().resourceDir返回值是空字符串？

A：通过ApplicationContext获取的是应用级别的应用文件路径，这其中不包括resourceDir，详情参考[获取应用文件路径](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/application-context-stage#获取应用文件路径)。

#### 总结

|    | 
方案一

 | 

方案二

 |
| :-- | :-- | :-- |
| 

关键接口

 | 

fs.mkdirSync、 fs.copyDirSync

 | 

resourceManager.getRawFd、 fs.openSync、fs.readSync、fs.writeSync、zlib.decompressFile

 |
| 

使用场景

 | 

适用于文件夹下嵌套层级少，子目录、子文件小的情况，因为copyDirSync直接拷贝文件夹会造成应用开销大，占用运行内存

 | 

适用于文件夹下嵌套层级多，子目录、子文件大的情况，该方案利用了压缩、解压，buffer循环读取可以减少应用开销、占用内存较少

 |

综上所述，需要读写rawfile文件夹需要先复制到应用沙箱，两种方案需要根据开发者具体场景进行选择。