UNPKG

@cloudbase/storage

Version:
687 lines (623 loc) 20.1 kB
import { constants, utils, helpers } from '@cloudbase/utilities' import { ICloudbase } from '@cloudbase/types' import { ICloudbaseComponent } from '@cloudbase/types/component' import { ICloudbaseFileMetaDataRes, ICloudbaseFileInfo, ICloudbaseUploadFileParams, ICloudbaseUploadFileResult, ICloudbaseGetUploadMetadataParams, ICloudbaseDeleteFileParams, ICloudbaseDeleteFileResult, ICloudbaseGetTempFileURLResult, ICloudbaseGetTempFileURLParams, ICloudbaseDownloadFileResult, ICloudbaseDownloadFileParams, ICloudbaseCopyFileParams, ICloudbaseCopyFileResult, } from '@cloudbase/types/storage' declare const cloudbase: ICloudbase enum EUploadMethod { put = 'put', post = 'post', } const { getSdkName, ERRORS, COMMUNITY_SITE_URL } = constants const { isArray, isString, isPalinObject, execCallback } = utils const { catchErrorsDecorator } = helpers const COMPONENT_NAME = 'storage' function basename(path) { const lastSlashIndex = path.lastIndexOf('/') if (lastSlashIndex === -1) return path return path.slice(lastSlashIndex + 1) } const storageGateWay = { getUploadInfo: async ( request, params: { path: string; headers?: Record<string, string> }, customReqOpts: ICloudbaseUploadFileParams['customReqOpts'], ) => { let res = await request.gateWay( { path: 'storages', name: 'get-objects-upload-info', data: [ { objectId: params.path, ...(params.headers ? { signedHeader: params.headers } : {}), }, ], }, customReqOpts, ) const data = res.data?.[0] || {} res = { ...res, data: { ...(data.code ? { ...data } : {}), authorization: data.authorization, token: data.token, url: data.uploadUrl, fileId: data.cloudObjectId, cosFileId: data.cloudObjectMeta, download_url: data.downloadUrl, }, } return res }, getDownLoadInfo: async ( request, params: { convertedFileList: Array<{ fileid: string }> }, customReqOpts: ICloudbaseUploadFileParams['customReqOpts'], ) => { let res = await request.gateWay( { path: 'storages', name: 'get-objects-download-info', data: params.convertedFileList.map((v: any) => ({ cloudObjectId: v.fileid })), }, customReqOpts, ) res = { ...res, data: { download_list: res.data?.map(v => ({ code: v.code || 'SUCCESS', message: v.message, fileid: v.cloudObjectId, download_url: v.downloadUrl, fileID: v.cloudObjectId, tempFileURL: v.downloadUrl, })), }, } return res }, delete: async ( request, params: { fileList: Array<string> }, customReqOpts: ICloudbaseUploadFileParams['customReqOpts'], ) => { let res = await request.gateWay( { path: 'storages', name: 'delete-objects', data: params.fileList.map(v => ({ cloudObjectId: v })), }, customReqOpts, ) res = { ...res, data: { delete_list: res.data?.map(v => ({ code: v.code || 'SUCCESS', fileID: v.cloudObjectId, message: v.message, })), }, } return res }, copyFile: async ( request, params: { convertedFileList: Array<{ src_path: string; dst_path: string; overwrite: boolean; remove_original: boolean }> }, customReqOpts: ICloudbaseUploadFileParams['customReqOpts'], ) => { let res = await request.gateWay( { path: 'storages', name: 'copy-objects', data: params.convertedFileList.map((v: any) => ({ srcPath: v.src_path, dstPath: v.dst_path, overwrite: v.overwrite, removeOriginal: v.remove_original, })), }, customReqOpts, ) res = { ...res, data: { copy_list: res.data?.map(v => ({ code: v.code || 'SUCCESS', fileID: v.cloudObjectId, message: v.message, })), }, } return res }, } class CloudbaseStorage { public isGateWay() { // @ts-ignore const { config } = this const endPointMode = config.endPointMode || 'CLOUD_API' return endPointMode === 'GATEWAY' } @catchErrorsDecorator({ customInfo: { className: 'Cloudbase', methodName: 'uploadFile', }, title: '上传文件失败', messages: [ '请确认以下各项:', ' 1 - 调用 uploadFile() 的语法或参数是否正确', ' 2 - 当前域名是否在安全域名列表中:https://console.cloud.tencent.com/tcb/env/safety', ' 3 - 云存储安全规则是否限制了当前登录状态访问', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async uploadFile( params: ICloudbaseUploadFileParams, callback?: Function, ): Promise<ICloudbaseUploadFileResult> { const { cloudPath, filePath, onUploadProgress, method = 'put', headers = {}, fileContent } = params if (!isString(cloudPath) || (!filePath && !fileContent)) { throw new Error(JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${COMPONENT_NAME}.uploadFile] invalid params`, }),) } const uploadMethod = { put: EUploadMethod.put, post: EUploadMethod.post }[method.toLocaleLowerCase()] || EUploadMethod.put const action = 'storage.getUploadMetadata' // @ts-ignore const { request } = this const metaDataParam: { path: string method: EUploadMethod headers?: Record<any, any> } = { path: cloudPath, method: uploadMethod, } if (uploadMethod === EUploadMethod.put) { metaDataParam.headers = headers } let metaData: ICloudbaseFileMetaDataRes = {} as any if (this.isGateWay()) { metaData = await storageGateWay.getUploadInfo(request, metaDataParam, params.customReqOpts) } else { metaData = await request.send(action, metaDataParam, params.customReqOpts) } const { data: { url, authorization, token, fileId, cosFileId, download_url: downloadUrl }, requestId, } = metaData if (metaData.data?.code) { return execCallback( callback, new Error(`[${getSdkName()}][${ERRORS.OPERATION_FAIL}][${COMPONENT_NAME}]:${metaData.data}`), ) } const commonParams = { url, file: filePath, name: cloudPath, onUploadProgress, fileContent, fileId, requestId, } const putParams = { ...commonParams, method: EUploadMethod.put, headers: { ...headers, authorization, 'x-cos-meta-fileid': cosFileId, 'x-cos-security-token': token, }, } const postParams = { ...commonParams, method: EUploadMethod.post, data: { key: cloudPath, signature: authorization, 'x-cos-meta-fileid': cosFileId, success_action_status: '201', 'x-cos-security-token': token, }, } const uploadConfig = { [EUploadMethod.put]: { params: putParams, isSuccess: (code: number) => code >= 200 && code < 300, }, [EUploadMethod.post]: { params: postParams, isSuccess: (code: number) => code === 201, }, } const res = await request.upload(uploadConfig[uploadMethod].params) if (uploadConfig[uploadMethod].isSuccess(res.statusCode)) { return execCallback(callback, null, { fileID: fileId, download_url: downloadUrl, requestId, }) } return execCallback( callback, new Error(`[${getSdkName()}][${ERRORS.OPERATION_FAIL}][${COMPONENT_NAME}]:${res.data}`), ) } @catchErrorsDecorator({ customInfo: { className: 'Cloudbase', methodName: 'getUploadMetadata', }, title: '获取上传元信息失败', messages: [ '请确认以下各项:', ' 1 - 调用 getUploadMetadata() 的语法或参数是否正确', ' 2 - 当前域名是否在安全域名列表中:https://console.cloud.tencent.com/tcb/env/safety', ' 3 - 云存储安全规则是否限制了当前登录状态访问', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async getUploadMetadata(params: ICloudbaseGetUploadMetadataParams, callback?: Function): Promise<any> { const { cloudPath } = params if (!isString(cloudPath)) { throw new Error(JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${COMPONENT_NAME}.getUploadMetadata] invalid cloudPath`, }),) } // @ts-ignore const { request } = this const action = 'storage.getUploadMetadata' try { let metaData: any = {} if (this.isGateWay()) { metaData = await storageGateWay.getUploadInfo(request, { path: cloudPath }, params.customReqOpts) } else { metaData = await request.send( action, { path: cloudPath, }, params.customReqOpts, ) } return execCallback(callback, null, metaData) } catch (err) { return execCallback(callback, err) } } @catchErrorsDecorator({ customInfo: { className: 'Cloudbase', methodName: 'deleteFile', }, title: '删除文件失败', messages: [ '请确认以下各项:', ' 1 - 调用 deleteFile() 的语法或参数是否正确', ' 2 - 当前域名是否在安全域名列表中:https://console.cloud.tencent.com/tcb/env/safety', ' 3 - 云存储安全规则是否限制了当前登录状态访问', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async deleteFile( params: ICloudbaseDeleteFileParams, callback?: Function, ): Promise<ICloudbaseDeleteFileResult> { const { fileList } = params if (!fileList || !isArray(fileList) || fileList.length === 0) { throw new Error(JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${COMPONENT_NAME}.deleteFile] fileList must not be empty`, }),) } for (const fileId of fileList) { if (!fileId || !isString(fileId)) { throw new Error(JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${COMPONENT_NAME}.deleteFile] fileID must be string`, }),) } } const action = 'storage.batchDeleteFile' // @ts-ignore const { request } = this let res: any = {} if (this.isGateWay()) { res = await storageGateWay.delete(request, { fileList }, params.customReqOpts) } else { res = await request.send( action, { fileid_list: fileList, }, params.customReqOpts, ) } if (res.code) { return execCallback(callback, null, res) } const data = { fileList: res.data.delete_list, requestId: res.requestId, } return execCallback(callback, null, data) } @catchErrorsDecorator({ customInfo: { className: 'Cloudbase', methodName: 'getTempFileURL', }, title: '获取文件下载链接', messages: [ '请确认以下各项:', ' 1 - 调用 getTempFileURL() 的语法或参数是否正确', ' 2 - 当前域名是否在安全域名列表中:https://console.cloud.tencent.com/tcb/env/safety', ' 3 - 云存储安全规则是否限制了当前登录状态访问', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async getTempFileURL( params: ICloudbaseGetTempFileURLParams, callback?: Function, ): Promise<ICloudbaseGetTempFileURLResult> { const { fileList } = params if (!fileList || !isArray(fileList) || fileList.length === 0) { throw new Error(JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${COMPONENT_NAME}.getTempFileURL] fileList must not be empty`, }),) } const convertedFileList = [] for (const file of fileList) { if (isPalinObject(file)) { if ( !Object.prototype.hasOwnProperty.call(file, 'fileID') || !Object.prototype.hasOwnProperty.call(file, 'maxAge') ) { throw new Error(JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${COMPONENT_NAME}.getTempFileURL] file info must include fileID and maxAge`, }),) } convertedFileList.push({ fileid: (file as ICloudbaseFileInfo).fileID, max_age: (file as ICloudbaseFileInfo).maxAge, }) } else if (isString(file)) { convertedFileList.push({ fileid: file, }) } else { throw new Error(JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${COMPONENT_NAME}.getTempFileURL] invalid fileList`, }),) } } const action = 'storage.batchGetDownloadUrl' // @ts-ignore const { request } = this let res: any = {} if (this.isGateWay()) { res = await storageGateWay.getDownLoadInfo(request, { convertedFileList }, params.customReqOpts) } else { res = await request.send(action, { file_list: convertedFileList }, params.customReqOpts) } if (res.code) { return execCallback(callback, null, res) } return execCallback(callback, null, { fileList: res.data.download_list, requestId: res.requestId, }) } @catchErrorsDecorator({ customInfo: { className: 'Cloudbase', methodName: 'downloadFile', }, title: '下载文件失败', messages: [ '请确认以下各项:', ' 1 - 调用 downloadFile() 的语法或参数是否正确', ' 2 - 当前域名是否在安全域名列表中:https://console.cloud.tencent.com/tcb/env/safety', ' 3 - 云存储安全规则是否限制了当前登录状态访问', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async downloadFile( params: ICloudbaseDownloadFileParams, callback?: Function, ): Promise<ICloudbaseDownloadFileResult> { const { fileID } = params if (!isString(fileID)) { throw new Error(JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${COMPONENT_NAME}.getTempFileURL] fileID must be string`, }),) } const tmpUrlRes = await this.getTempFileURL.call(this, { fileList: [ { fileID, maxAge: 600, }, ], customReqOpts: params.customReqOpts, }) const res = tmpUrlRes.fileList[0] if (res.code !== 'SUCCESS') { return execCallback(callback, res) } // @ts-ignore const { request } = this const tmpUrl = encodeURI(res.download_url) const result = await request.download({ url: tmpUrl, tempFilePath: params.tempFilePath }) return execCallback(callback, null, result) } @catchErrorsDecorator({ customInfo: { className: 'Cloudbase', methodName: 'copyFile', }, title: '批量复制文件', messages: [ '请确认以下各项:', ' 1 - 调用 copyFile() 的语法或参数是否正确', ' 2 - 当前域名是否在安全域名列表中:https://console.cloud.tencent.com/tcb/env/safety', ' 3 - 云存储安全规则是否限制了当前登录状态访问', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async copyFile(params: ICloudbaseCopyFileParams, callback?: Function): Promise<ICloudbaseCopyFileResult> { const { fileList } = params if (!fileList || !isArray(fileList) || fileList.length === 0) { throw new Error(JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${COMPONENT_NAME}.copyFile] fileList must not be empty`, }),) } const convertedFileList = [] for (const file of fileList) { const { srcPath, dstPath } = file if (!srcPath || !dstPath || typeof srcPath !== 'string' || typeof dstPath !== 'string') { throw new Error(JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${COMPONENT_NAME}.copyFile] srcPath and dstPath may not be empty`, }),) } if (srcPath === dstPath) { throw new Error(JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${COMPONENT_NAME}.copyFile] srcPath and dstPath can not be the same`, }),) } if (basename(srcPath) !== basename(dstPath)) { throw new Error(JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${COMPONENT_NAME}.copyFile] srcPath and dstPath file name must be the same`, }),) } convertedFileList.push({ src_path: srcPath, dst_path: dstPath, overwrite: file.overwrite, remove_original: file.removeOriginal, }) } const action = 'storage.batchCopyFile' // @ts-ignore const { request } = this let res: any = {} if (this.isGateWay()) { res = await storageGateWay.copyFile(request, { convertedFileList }, params.customReqOpts) } else { res = await request.send(action, { file_list: convertedFileList }, params.customReqOpts) } if (res.code) { return execCallback(callback, null, res) } return execCallback(callback, null, { fileList: res.data.copy_list, requestId: res.requestId, }) } public async getFileInfo(params: ICloudbaseGetTempFileURLParams): Promise<ICloudbaseGetTempFileURLResult> { const fileInfo = await this.getTempFileURL(params) if (fileInfo?.fileList && fileInfo?.fileList?.length > 0) { const fileList = await Promise.all(fileInfo.fileList.map(async (item: ICloudbaseGetTempFileURLResult['fileList'][0]) => { if (item.code !== 'SUCCESS') { return { code: item.code, fileID: item.fileID, tempFileURL: item.tempFileURL, } } try { // @ts-ignore const { request } = this const res = await request.fetch({ url: item.tempFileURL, method: 'HEAD' }) // eslint-disable-next-line radix const fileSize = parseInt(res.headers['content-length']) || 0 const contentType = res.headers['content-type'] || '' const fileInfo = { code: item.code, fileID: item.fileID, tempFileURL: item.tempFileURL, cloudId: item.fileID, fileName: item.fileID.split('/').pop(), contentType, mime: contentType.split(';')[0].trim(), size: fileSize, } return fileInfo } catch (e) { return { code: 'FETCH_FILE_INFO_ERROR', fileID: item.fileID, tempFileURL: item.tempFileURL, } } }),) return { fileList, requestId: fileInfo.requestId, } } return { fileList: [], requestId: fileInfo.requestId, } } } const cloudbaseStorage = new CloudbaseStorage() const component: ICloudbaseComponent = { name: COMPONENT_NAME, entity: { uploadFile: cloudbaseStorage.uploadFile, deleteFile: cloudbaseStorage.deleteFile, getTempFileURL: cloudbaseStorage.getTempFileURL, downloadFile: cloudbaseStorage.downloadFile, getUploadMetadata: cloudbaseStorage.getUploadMetadata, copyFile: cloudbaseStorage.copyFile, getFileInfo: cloudbaseStorage.getFileInfo, isGateWay: cloudbaseStorage.isGateWay, }, } try { cloudbase.registerComponent(component) } catch (e) {} export function registerStorage(app: Pick<ICloudbase, 'registerComponent'>) { try { app.registerComponent(component) } catch (e) { console.warn(e) } } try { (window as any).registerStorage = registerStorage } catch (e) {}