鸿蒙应用开发:文件下载、选择、打开

文件下载

项目业务需求,一般会有将网络文件下载到沙盒或者公共目录的场景。而文件下载的第一步,是获取下载文件的目标地址选择。

按照官方文档的建议,代码如下:

import { BusinessError, Callback } from '@ohos.base'
import { fileIo, fileUri } from '@kit.CoreFileKit';
import picker from '@ohos.file.picker';
import fs from '@ohos.file.fs';
import { FWHttp, NetResponse } from '@fw/net';


export interface FileGetDownloadPathOptionsExtra {
  /**
   * 1:表示使用公共目录 0:App沙盒空间
   */
  external?: number
  /**
   * true在公共目录下面新建一个appid对应的文件
   */
  bind?: boolean
  /**
   * true重复下载某个文件不做覆盖替换,而是添加文件顺序号共存。
   * bind==false时不支持该参数,因为公共目录是使用picker访问,bind==false时picker会控制文件名是否重复。
   */
  repeat?: boolean
}

/**
 * 文件下载参数
 */
export interface FileGetDownloadPathOptions {
  /**
   * 网络下载路径
   */
  downloadUrl: string
  /**
   * 文件名称
   *
   * 若为undefined,则取downloadUrl最后一段path为文件名
   */
  fileName?: string
  // /**
  //  * 信任主机
  //  */
  // trustAllHosts?: string
  /**
   * 成功回调
   */
  successCallback?: Callback<Record<string, string>>
  /**
   * 失败回调
   */
  failCallback?: Callback<string>
  /**
   * 扩展参数
   */
  extra?: FileGetDownloadPathOptionsExtra
}

export class File {

  static getUrlFileName(url: string): string {
    if (!url) {
      return'';
    }
    let index = url.lastIndexOf('/');
    if (index === -1) {
      return'';
    }
    url = url.substring(index + 1);
    index = url.indexOf('?');
    if (index !== -1) {
      url = url.substring(0, index);
    }
    return url;
  }

  static getNoneExistFileName(path: string, fileName: string): string {
    let newFileName = fileName;
    let filePath = path + "/" + newFileName;
    let fileExist = fileIo.accessSync(filePath);
    let num = 1;
    while (fileExist) {
      if (fileName.indexOf('.') > -1) {
        let index = fileName.lastIndexOf('.');
        newFileName = fileName.substring(0, index) + "_" + num + "." + fileName.substring(index + 1);
      } else {
        newFileName = fileName + "_" + num;
      }

      filePath = path + "/" + newFileName;
      fileExist = fileIo.accessSync(filePath);
      num++;
    }
    return filePath;
  }

  static async getDownloadPath(options: FileGetDownloadPathOptions) {
    try {
      let DOWNLOAD_TO_PATH = FWFileUtil.getFilesDirPath(`download`); // 默认取沙盒文件路径

      let filename = options.fileName ?? ''
      if (options.fileName == undefined) {
        filename = File.getUrlFileName(options.downloadUrl)
      }

      if (options.extra?.external === 1) {
        // 获取目录
        const documentSaveOptions = new picker.DocumentSaveOptions(); // 创建文件管理器选项实例
        if (options.extra?.bind) {
          documentSaveOptions.pickerMode = picker.DocumentPickerMode.DOWNLOAD
        } else {
          documentSaveOptions.newFileNames = [filename]; // 保存文件名
        }

        let uriString = ''
        const documentViewPicker = new picker.DocumentViewPicker(); // 创建文件选择器实例
        let documentSaveResult = await documentViewPicker.save(documentSaveOptions)
        let uri = new fileUri.FileUri(documentSaveResult[0]);
        if (options.extra?.bind) {
          if (options.extra?.repeat) {
            uriString = File.getNoneExistFileName(uri.path, filename)
          } else {
            uriString = uri.path + '/' + filename;
          }
        } else {
          uriString = uri.path
        }

        DOWNLOAD_TO_PATH = uriString
      } else {
        if (options.extra?.repeat) {
          DOWNLOAD_TO_PATH = File.getNoneExistFileName(DOWNLOAD_TO_PATH, filename)
        } else {
          DOWNLOAD_TO_PATH = FWFileUtil.getFilesDirPath(DOWNLOAD_TO_PATH, filename)
          if (fileIo.accessSync(DOWNLOAD_TO_PATH)) {
            fileIo.unlinkSync(DOWNLOAD_TO_PATH);
          }
        }
      }

      if (options.successCallback != undefined) {
        options.successCallback({nativeURL: DOWNLOAD_TO_PATH})
      }

      // const res: NetResponse<ArrayBuffer> = await FWHttp.getInstance().download(options.downloadUrl)
      // let file = fs.openSync(DOWNLOAD_TO_PATH, fs.OpenMode.WRITE_ONLY | fs.OpenMode.CREATE)
      // fs.writeSync(file.fd, res.result)
      //
      // if (options.successCallback != undefined) {
      //   options.successCallback({nativeURL: DOWNLOAD_TO_PATH})
      // }

    } catch (e) {
      if (options.failCallback != undefined) {
        options.failCallback('获取下载路径失败,原因:' + e.message)
      }
    }
  }

  static readonly separator: string = '/';

  /**
   * 获取文件目录下的文件夹路径或文件路径。
   * @param dirPath 文件路径,支持完整路径 和 相对路径(download/wps/doc)。dirPath传空表示根目录
   * @param fileName 文件名(test.text)
   * @returns
   */
  static getFilesDirPath(dirPath: string, fileName?: string): string {
    let filePath = getContext().filesDir; //根目录
    if (dirPath) {
      if (dirPath.startsWith(filePath)) { //路径中包含根目录,是完整路径。
        filePath = dirPath;
      } else { //路径中不包含根目录,拼接成完整路径。
        filePath = filePath + File.separator + dirPath;
      }
      if (!File.accessSync(filePath)) {
        File.mkdirSync(filePath) //如果文件夹不存在就创建
      }
    }
    if (fileName) {
      filePath = filePath + File.separator + fileName;
    }
    return filePath;
  }

  /**
   * 检查文件是否存在,以同步方法。
   * @param path 文件应用沙箱路径。
   * @returns
   */
  static accessSync(path: string): boolean {
    return fs.accessSync(path);
  }

  /**
   * 创建目录以同步方法,当recursion指定为true,可多层级创建目录。
   * @param path 目录的应用沙箱路径。
   * @param recursion 是否多层级创建目录。recursion指定为true时,可多层级创建目录。recursion指定为false时,仅可创建单层目录。
   */
  static mkdirSync(path: string, recursion: boolean = true) {
    if (recursion) {
      fs.mkdirSync(path, recursion);
    } else {
      fs.mkdirSync(path);
    }
  }
}
  1. 获取沙盒路径不涉及隐私问题,权限问题,所以没有什么特殊点;
  2. 核心在于获取公共目录地址;按照官方文档,要使用DocumentViewPicker.save()方法,而其返回值是FileUri格式的字符串;例如:file://docs/storage/Users/currentUser/Download/component-Library.zip
  3. @ohos.file.fs库中,大部分方法都是要求传入path格式的字符串;因此需要使用FileUri类进行解析后获得path;例如:/storage/Users/currentUser/Download/component-Library.zip
  4. 具体的下载逻辑一般依赖项目中集成的网络框架,这里暂不包含具体下载逻辑,通过callback回传下载地址url;

文件选择

项目业务需求中,有选择文件的需求。

选择文件,系统提供了DocumentViewPicker.select()方法。

import { fileUri, picker } from '@kit.CoreFileKit'

export interface FileChooserOpenOptions {
  defaultFilePathUri?: string
  success: Callback<Record<string, string>>
  failure: Callback<string>
}

export class FileChooser {
  static async open(options: FileChooserOpenOptions) {
    try {
      const documentViewPicker = new picker.DocumentViewPicker(); // 创建文件选择器实例
      const documentSelectOptions = new picker.DocumentSelectOptions(); // 创建文件管理器选项实例
      documentSelectOptions.defaultFilePathUri = options.defaultFilePathUri
      let documentSelectResult = await documentViewPicker.select(documentSelectOptions)
      if (documentSelectResult.length > 0) {
        let uri = new fileUri.FileUri(documentSelectResult[0]);
        if (options.success != undefined) {
          options.success({
            'path': uri.path,
            'name': uri.name
          })
        }
      } else {
        if (options.failure != undefined) {
          options.failure(`未选择文件`)
        }
      }
    } catch (e) {
      if (options.failure != undefined) {
        options.failure(e.message)
      }
    }
  }
}

DocumentSelectOptions中有一个defaultFilePathUri参数,该参数支持沙盒路径;
因此当传入沙盒路径uri时,就可以在系统弹窗中选择指定沙盒路径下的文件;
不传入该参数时,默认是选择公共目录下的文件。
需要注意的是,该参数要求是uri格式的。

文件打开

项目业务需求中,有将文件使用外部应用打开的需求,比如Word/Excel/pdf文件使用外部应用打开等场景。

import { fileUri } from '@kit.CoreFileKit';
import { common, Want, wantConstant } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

export interface FileOpener2OpenOptionsCallbackContext {
  /**
   * 成功回调
   */
  success?: Callback<string>
  /**
   * 失败回调
   */
  error?: Callback<string>
}

export interface FileOpener2OpenOptions {
  fileName: string
  contentType: string
  callbackContext: FileOpener2OpenOptionsCallbackContext
}

export class FileOpener2 {
  static open(context: common.UIAbilityContext, options: FileOpener2OpenOptions) {
    // 获取文件沙箱路径
    let filePath = options.fileName;
    // 将沙箱路径转换为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: options.contentType
    }
    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}`);
      });
  }
}

根据官方文档,打开第三方应用需要使用context.startAbility()方法。
其参数为Want类型:

    {
      // 配置被分享文件的读写权限,例如对被分享应用进行读写授权
      flags: wantConstant.Flags.FLAG_AUTH_WRITE_URI_PERMISSION | wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION,
      // 配置分享应用的隐式拉起规则
      action: 'ohos.want.action.sendData',
      uri: uri,
      type: options.contentType
    }

其中,uri和type分别是文件地址uri字符串和contentType,都需要传,否则调用第三方打开会失败。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐