鸿蒙应用下载文件保存到本地文件目录(使用安全控件SaveButton和picker.DocumentViewPicker)

922 阅读2分钟

接上一篇文章juejin.cn/post/742918…

在Android开发中会遇到下载应用包等文件到本地存储卡中,在Harmony应用中实现这个需求的思路是

  1. 首先权限上,采用安全控件SaveButton的方式跳过申请受限开放权限ohos.permission.WRITE_IMAGEVIDEO;
  2. 本地存储卡的路径,使用picker.DocumentViewPicker获取;
  3. 使用request.agent.create创建一个Task,将网络文件下载到当前应用的沙箱目录下;
  4. 将沙箱目录中下载完成的文件拷贝到本地存储卡的路径中。

工具类的代码DownloadFileNew.ets如下(配上了注释)

import { common } from '@kit.AbilityKit';
import fs, { ReadOptions, WriteOptions } from '@ohos.file.fs';
import { BusinessError, request } from '@kit.BasicServicesKit';
import { fileIo, picker } from '@kit.CoreFileKit';

let TAG= "DownloadFileNew"
export class DownloadFileNew{

  /**
   * 获取应用沙箱存储目录 统一一下,方便APP设置页面删除
   * @returns
   */
  static getLocalCacheDir():string {
    let cache_dir_name = "appfiles";
    let cache_dir = getContext().cacheDir+"/"+cache_dir_name;
    console.info(`createCacheDir cache_dir: ${cache_dir}`);
    let existAppCacheDir = fileIo.accessSync(cache_dir,fileIo.AccessModeType.EXIST)
    if (!existAppCacheDir) {
      fileIo.mkdirSync(cache_dir, true);
    }
    return cache_dir
  }

  /**
   * 下载文件到本地存储卡的下载等文件夹下
   * @param context
   * @param filePath
   * @param callback
   */
  public static downloadImagToDir(context:common.UIAbilityContext,filePath:string,callback:DownloadCallback){
    console.info(`${TAG} downloadImagToDir imagePath: ${filePath}`);
    if (!filePath.startsWith("http://")&&!filePath.startsWith("https://")) {
      callback.onError()
      return
    }
    let defaultName = `${Date.now().toString()}.dmg`
    //1.获取本地存储卡的下载等文件夹 考虑用户体验友好,先让用户选择下载路径,再下载
    DownloadFileNew.selectDocumentPath(`${defaultName}`).then(destFilePath =>{
      if (!destFilePath||destFilePath.length<=0) {
        console.info(`${TAG} downloadImagToDir 用户取消了`);
        callback.onError()
        return
      }
      //2.获取下载到的应用沙箱目录
      let localCacheDir:string = DownloadFileNew.getLocalCacheDir()
      let srcFilePath =`${localCacheDir}/${defaultName}`
      //使用request.agent.create下载文件到沙箱目录
      let config:request.agent.Config = {
        action: request.agent.Action.DOWNLOAD,
        url: filePath,
        method: 'GET',
        title: 'download',
        mode: request.agent.Mode.BACKGROUND,
        retry: true,
        network: request.agent.Network.ANY,
        saveas: srcFilePath,//这里要注意目录 API文档和官方文档都有详细说明
        overwrite: true
      }
      try {
        let startTime = 0
        //this.downloadTask = await request.agent.create(context,config)
        request.agent.create(context,config).then(downloadTask=>{
          if (callback.onStart) {
            callback.onStart()
          }
          downloadTask.on("progress",(progress: request.agent.Progress)=>{
            console.info(`${TAG} downloadImagToDir progress = ${JSON.stringify(progress)}`);
            if (progress) {
              callback.inProgress(progress.processed,progress.sizes[0])
            }
          })
          downloadTask.on("completed",(progress: request.agent.Progress)=>{
            console.info(`${TAG} downloadImagToDir completed time=${Date.now()-startTime}`);
            //deleteTask();
            if (downloadTask&&downloadTask.config.saveas) {
              try {
                let srcFilePath = downloadTask.config.saveas
                downloadTask.off('progress');
                downloadTask.off('completed');
                downloadTask.off('failed');
                request.agent.remove(downloadTask.tid);
                //3.将下载到应用沙箱目录中的文件 写到用户选择的存储卡中destFilePath
                DownloadFileNew.saveToDestFilePath(srcFilePath,destFilePath).then(r=>{
                  if (r) {
                    callback.onSuccess(destFilePath)
                  }else {
                    callback.onError()
                  }
                })
              } catch (err) {
                console.info(`${TAG} downloadImagToDir completed fail, err= ${JSON.stringify(err)}`);
                callback.onError()
              }
            }else {
              console.info(`${TAG} downloadImagToDir completed fail, onError downloadTask==null `);
              callback.onError()
            }
          })
          downloadTask.on("failed", async (progress)=>{
            let taskInfo = await request.agent.show(downloadTask!.tid);
            console.info(`${TAG} downloadImagToDir failed, resean = ${taskInfo.reason}, faults = ${JSON.stringify(taskInfo.faults)}`);
            callback.onError()
          })
          downloadTask.on('pause',  async (progress: request.agent.Progress) => {
            if (downloadTask) {
              let taskInfo = await request.agent.show(downloadTask.tid);
              console.info(`${TAG} pause,  resean = ${taskInfo.reason}, progress = ${progress.processed}, faults = ${JSON.stringify(taskInfo.faults)}`);
            }
          })
          downloadTask.on('resume',async (progress: request.agent.Progress) => {
            if (downloadTask) {
              let taskInfo = await request.agent.show(downloadTask.tid);
              console.info(`${TAG} resume,  resean = ${taskInfo.reason}, progress = ${progress.processed}, faults = ${JSON.stringify(taskInfo.faults)}`)
            }
          })
          startTime = Date.now()
          downloadTask.start()
        })
      } catch (err) {
        console.info(`${TAG} task  err, err  = ${JSON.stringify(err)}`)
        callback.onError()
      }
    }).catch((err:Error)=>{
      console.info(`${TAG} task  err, err111  = ${JSON.stringify(err)}`)
      callback.onError()
    })
  }

  /**
   * 获取本地存储卡的下载等文件夹
   * @param defaultName 默认文件名称(包含后缀名),用户可以在选择弹窗中修改
   * @returns
   */
  static async selectDocumentPath(defaultName:string): Promise<string>{
    try {
      let DocumentSaveOptions = new picker.DocumentSaveOptions();
      DocumentSaveOptions.newFileNames = [defaultName];
      let documentPicker = new picker.DocumentViewPicker();
      return documentPicker.save(DocumentSaveOptions).then(async (documentSaveResult) => {
        console.info(`${TAG} DocumentViewPicker.save successfully, DocumentSaveResult uri:= ${JSON.stringify(documentSaveResult)}`)
        let destFile =await fs.open(documentSaveResult[0], fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
        console.info(`${TAG} start destFile: ${destFile.path}`);
        return destFile.path
      }).catch((err:BusinessError) => {
        console.info(`${TAG} DocumentViewPicker.save failed with err:  ${JSON.stringify(err)}`);
        return ""
      });
    } catch (err) {
      console.info(`${TAG} DocumentViewPicker failed with err:  ${JSON.stringify(err)}`);
      return ""
    }
  }

  /**
   * 拷贝文件 从沙箱目录中到目标文件中
   * @param srcFilePath 沙箱目录中文件
   * @param destFilePath
   * @returns
   */
  static async saveToDestFilePath(srcFilePath:string,destFilePath:string){
    console.info(`${TAG} saveToDestFilePath srcFilePath= ${srcFilePath},destFilePath= ${destFilePath}`);
    try {
      let bufSize = 4096;
      let readSize = 0;
      let srcFile  = fs.openSync(srcFilePath, fs.OpenMode.READ_WRITE);
      console.info(`${TAG} saveToDestFilePath srcFile: ${srcFile.path} size=`+ DownloadFileNew.bytesToMB(fs.statSync(srcFile.path).size));
      let destFile = fs.openSync(destFilePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
      console.info(`${TAG} saveToDestFilePath destFile: ${destFile.path}`);
      let buf = new ArrayBuffer(bufSize);
      let readOptions: ReadOptions = {
        offset: readSize,
        length: bufSize
      };

      let startTime = Date.now()
      let readLen = fs.readSync(srcFile.fd, buf, readOptions);
      console.info(`${TAG} readSync readLen: ${readLen}`);
      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);
      }
      console.info(`${TAG} write successfully 1111111 time=${Date.now()-startTime}`);
      //fs.unlinkSync(srcFile.path) //删除沙箱目录中的临时文件
      fs.closeSync(srcFile);
      fs.closeSync(destFile);
      console.info(`${TAG} close successfully 2222222 time=${Date.now()-startTime}`);
      return true
    } catch (err) {
      console.info(`${TAG} saveToDestFilePath failed with err: ${JSON.stringify(err)}`);
      return false
    }
  }

  public static bytesToMB(bytes:number){
    if(bytes>=1000){
      bytes=bytes/1000.0;
      if(bytes>=1000){
        bytes=bytes/1000.0;
        if(bytes>=1000){
          bytes=bytes/1000.0;
          return bytes+" GB";
        }else{
          return bytes+" MB";
        }
      }else{
        return bytes+" KB";
      }
    }else{
      return bytes+" Byte";
    }
  }
}

/**
 * 下载文件的返回结果
 */
interface DownloadCallback {
  onError: () => void;
  onStart?: () => void;
  inProgress: (progress: number, total: number) => void;
  onSuccess: (file: string) => void;
}

调用侧逻辑代码

import { common } from '@kit.AbilityKit';
import { DownloadFileNew } from './DownloadFileNew';
import { Decimal } from '@kit.ArkTS';

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';
  //网上自己找一个大的文件 
  fileUri = "https://4980bb6043e153a06db675f2cd6ce1f7.dlied1.cdntips.net/dldir1.qq.com/weixin/mac/WeChatMac.dmg?mkey=lego_ztc&f=00&sche_type=7&cip=118.113.101.171&proto=https"
  @State fileDownHint: string = '0%';
  @State saveButtonOptions: SaveButtonOptions = {
    icon: SaveIconStyle.FULL_FILLED,
    text: SaveDescription.SAVE_FILE,
    buttonType: ButtonType.Capsule
  } // 设置安全控件按钮属性

  build() {
    Column() {
      Text(`${this.fileDownHint}`)
        .textOverflow({ overflow: TextOverflow.None })
        .fontSize(20)
        .textAlign(TextAlign.Center)
        .fontColor(Color.Black)
        .width("100%")
        .margin(40)
        .fontWeight(FontWeight.Bold)
      SaveButton(this.saveButtonOptions)
        .width(100)
        .height(50)
        .onClick(async (event, result: SaveButtonOnClickResult) => {
          if (result == SaveButtonOnClickResult.SUCCESS) {
            DownloadFileNew.downloadImagToDir(getContext(this) as common.UIAbilityContext, this.fileUri, {
              onError: () => {
              },
              inProgress: (progress, total) => {
                let progressDecimal: Decimal = new Decimal(progress/total*100);
                let progressFixed = progressDecimal.toFixed(2)
                this.fileDownHint = `下载中:${progressFixed}%`
              },
              onSuccess: (file) => {
                this.fileDownHint = `下载完成100%,文件地址:${file}`
              }
            })
          }
        })
    }
    .height('100%')
  }
}

效果图

image.png image.png image.png image.png

image.png image.png