@react-native-ohos/react-native-document-picker
Version:
A react native interface to access documents from dropbox, google drive, iCloud...
199 lines (181 loc) • 6.52 kB
text/typescript
/*
* Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file
*/
import { TM } from './generated/ts';
import { RNOHLogger, TurboModule } from '@rnoh/react-native-openharmony/ts';
import { PickerOptions } from './FileTypes';
import picker from '@ohos.file.picker';
import fileuri from '@ohos.file.fileuri';
import fs, { ReadOptions } from '@ohos.file.fs';
import { BusinessError } from '@kit.BasicServicesKit';
import util from '@ohos.util';
import mime from "mime";
const CANCELED_ERROR_CODE = 'DOCUMENT_PICKER_CANCELED';
const LOGGER_NAME = 'DocumentPicker';
export class DocumentPickerTurboModule extends TurboModule implements TM.RNDocumentPicker.Spec {
private documentPicker: picker.DocumentViewPicker = new picker.DocumentViewPicker();
public logger: RNOHLogger;
constructor(ctx) {
super(ctx);
this.logger = this.ctx.logger.clone(LOGGER_NAME)
}
/*
* 获取常量,暂未用到
*/
getConstants(): Object {
return {};
}
/*
* 获取要选择文件的后缀
*
* @param types 文件后缀列表 eg: [ '.jpeg .jpg .png', '.txt', '.zip .gz' ]
* @return 文件后缀列表 [ '.jpeg', '.jpg', '.png', '.txt', '.zip', '.gz' ]
*/
private getPickerFileSuffix(types: PickerOptions['type']): string[] {
let suffixList: string[] = [];
if (Array.isArray(types)) {
for (const suffix of types) {
suffixList.push(...suffix.split(' '));
}
}
return suffixList;
}
/*
* 获取文件大小
*
* @param uri 文件路径
* @return 返回文件大小(byte)
*/
private async getFileSize(uri: string): Promise<number> {
const stat = await fs.stat(uri);
return stat.size;
}
/*
* 有传入的copyTo选项,每次会新建UUID目录且文件拷贝到该目录下, dir参数指定是在哪个目录下。
*
* @param sourceUri 源文件uri
* @param dir 目标文件夹
*/
private async copyFileToLocalStorage(sourceUri: fileuri.FileUri, dir: PickerOptions['copyTo']): Promise<string> {
const dirPath = dir === 'cachesDirectory' ?
this.ctx.uiAbilityContext.cacheDir :
this.ctx.uiAbilityContext.filesDir;
const destUUIdDir = dirPath + '/' + util.generateRandomUUID();
const destFilePath = `${destUUIdDir}/${sourceUri.name ?? new Date().getTime()}`;
await fs.mkdir(destUUIdDir, true);
await copyFile(sourceUri.path, destFilePath);
return destFilePath;
}
/*
* 根据picker拿到的uri获取文件更多信息:文件大小、文件名、 mimetype等
*/
private async parseFileByFileUri(uri: string,
copyToDir?: PickerOptions['copyTo']): Promise<TM.RNDocumentPicker.DocumentPickerResponse> {
const fUri: fileuri.FileUri = new fileuri.FileUri(uri);
const fileSize = await this.getFileSize(fUri.path);
const filename = fUri.name;
const fileExt = filename.substring(filename.lastIndexOf('.'));
const fileMimeType = mime.getType(fileExt);
let result: TM.RNDocumentPicker.DocumentPickerResponse;
result = {
uri: fUri.path,
type: fileMimeType,
name: filename,
size: fileSize,
fileCopyUri: null,
};
try {
if (copyToDir) {
result.fileCopyUri = await this.copyFileToLocalStorage(fUri, copyToDir);
};
} catch (err) {
let e: BusinessError = err;
result.copyError = `${e.code} ${e.message}`;
}
return result;
}
/*
* 根据传入的选择参数调用 documentPicker
*/
async pick(options: PickerOptions): Promise<TM.RNDocumentPicker.DocumentPickerResponse[]> {
try {
const pickerOpt = new picker.DocumentSelectOptions();
if (canIUse('SystemCapability.FileManagement.UserFileService.FolderSelection')) {
pickerOpt.selectMode = picker.DocumentSelectMode.FILE;
pickerOpt.fileSuffixFilters = this.getPickerFileSuffix(options.type);
};
// 单选
if (!options.allowMultiSelection) {
pickerOpt.maxSelectNumber = 1;
};
const pickerRes = await this.documentPicker.select(pickerOpt);
// 选择为空:用户取消了
if (!pickerRes.length) {
throw new PickCancelError()
};
const parseRes = await Promise.allSettled(
pickerRes.map(uri => this.parseFileByFileUri(uri, options.copyTo))
);
return parseRes.map(v => v.status === 'fulfilled' && v.value);
} catch (err) {
this.logger.info(`${err.code} ${err.message}`);
if (err instanceof PickCancelError) {
console.log(JSON.stringify(err));
throw err;
}
}
}
async releaseSecureAccess(_uris: string[]): Promise<void> {
return;
}
async pickDirectory(): Promise<TM.RNDocumentPicker.DirectoryPickerResponse> {
const pickerOpt = new picker.DocumentSelectOptions();
if (canIUse('SystemCapability.FileManagement.UserFileService.FolderSelection')) {
pickerOpt.selectMode = picker.DocumentSelectMode.FOLDER;
};
const pickerRes = await this.documentPicker.select(pickerOpt);
return { uri: pickerRes[0] }
}
public __onDestroy__(): void {
this.logger.info('RNDocumentPick destroy!');
}
}
/*
* 流式读写 使用系统fs.copyFile会报错
*
* @param source 原文件沙箱路径
* @param dest 目标沙箱路径
*/
async function copyFile(source: string, dest: string): Promise<void> {
// 打开文件流
let inputStream = fs.createStreamSync(source, 'r+');
let outputStream = fs.createStreamSync(dest, "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) {
await outputStream.write(buf);
readOptions.offset = readSize;
readLen = await inputStream.read(buf, readOptions);
readSize += readLen;
}
// 关闭文件流
inputStream.closeSync();
outputStream.closeSync();
}
/*
* 取消选择抛出错误, 目前rn框架不支持抛出 message以外的属性
*/
class PickCancelError extends Error {
// code: string = CANCELED_ERROR_CODE;
message: string = CANCELED_ERROR_CODE
}