import { assert } from 'oak-domain/lib/utils/assert'; import ALiYun from './aliyun'; import { ALiYunSDK } from 'oak-external-sdk'; import { OakPreConditionUnsetException } from 'oak-domain/lib/types/Exception'; import { stsAssumeRole } from 'oak-external-sdk/lib/service/ali/sts'; import { OakException } from 'oak-domain/lib/types'; export default class ALiYunBackend extends ALiYun { getConfigAndInstance(application) { const { config, account } = this.getConfig(application); const instance = ALiYunSDK.getInstance(account.accessKeyId, account.accessKeySecret); return { config, instance, account, }; } async composeFileUrlBackend(options) { const { application, extraFile, context, style } = options; const { config: aliyunCosConfig } = this.getConfig(application); if (aliyunCosConfig) { let bucket = aliyunCosConfig.buckets.find((ele) => ele.name === extraFile.bucket); if (bucket) { const { domain, protocol } = bucket; let protocol2 = protocol; if (protocol instanceof Array) { // protocol存在https: 说明域名有证书 const index = protocol.includes('https:') ? protocol.findIndex((ele) => ele === 'https:') : 0; protocol2 = protocol[index]; } return `${protocol2}//${domain}/${this.formKey(extraFile)}${style ? style : ''}`; } } return ''; } async formUploadMeta(application, extraFile) { const { bucket, enableChunkedUpload } = extraFile; // 构造文件上传所需的key const key = this.formKey(extraFile); const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application); const { buckets } = aliyunCosConfig; let bucket2 = bucket; if (!bucket2) { const { defaultBucket } = aliyunCosConfig; bucket2 = defaultBucket; } assert(bucket2); const b = buckets.find((ele) => ele.name === bucket2); assert(b, `${bucket2}不是一个有效的桶配置`); Object.assign(extraFile, { bucket: bucket2, uploadMeta: !enableChunkedUpload ? instance.getUploadInfo(bucket2, b.zone, key) : {}, }); } async checkWhetherSuccess(application, extraFile) { const key = this.formKey(extraFile); const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application); const b = aliyunCosConfig.buckets.find((ele) => ele.name === extraFile.bucket); assert(b, `extraFile中的bucket名称在阿里云配置中找不到「${extraFile.bucket}」`); try { const result = await instance.isExistObject(extraFile.bucket, b.zone, key); return result; } catch (err) { throw err; } } async removeFile(application, extraFile) { const key = this.formKey(extraFile); const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application); const b = aliyunCosConfig.buckets.find((ele) => ele.name === extraFile.bucket); assert(b, `extraFile中的bucket名称在阿里云配置中找不到「${extraFile.bucket}」`); try { await instance.removeFile(extraFile.bucket, b.zone, key); } catch (err) { throw err; } } async composeChunkUploadInfo(application, extraFile, context) { const key = this.formKey(extraFile); const { instance, config: aliyunCosConfig, account } = this.getConfigAndInstance(application); let useSts = true; let stsInfo = {}; if (!account.stsEndpoint || !account.roleArn || !account.roleSessionName) { useSts = false; console.warn("阿里云Cos配置中缺少sts相关配置,无法使用sts方式上传分片,将使用账号授权进行上传,可能存在安全风险,请检查确保不会暴露accessKey"); } else { try { const res = await stsAssumeRole({ accessKeyId: account.accessKeyId, accessKeySecret: account.accessKeySecret, endpoint: account.stsEndpoint, roleArn: account.roleArn, roleSessionName: account.roleSessionName, }); stsInfo = { stsToken: res.Credentials.SecurityToken, accessKeyId: res.Credentials.AccessKeyId, accessKeySecret: res.Credentials.AccessKeySecret, }; } catch (err) { console.error("Failed to assume role for STS:", err); throw new OakPreConditionUnsetException("获取阿里云STS临时凭证失败,请检查配置是否正确", 'extraFile'); } } // 大部分校验都在formUploadMeta中完成,这里可以不多做判断了 const b = aliyunCosConfig.buckets.find((ele) => ele.name === extraFile.bucket); const preInit = await instance.prepareMultipartUpload(extraFile.bucket, b.zone, key, extraFile.chunkInfo?.partCount, { timeout: 30 * 1000, // 30 seconds ...(useSts ? stsInfo : {}), expires: 3 * 24 * 60 * 60, // 3 days }); return { uploadId: preInit.uploadId, chunkSize: extraFile.chunkInfo?.chunkSize, partCount: preInit.parts.length, parts: preInit.parts.map((part) => ({ partNumber: part.partNumber, uploadUrl: part.uploadUrl, formData: {}, // 阿里云不需要额外的formData })), }; } /** * 完成分片上传后的合并操作 */ async mergeChunkedUpload(application, extraFile, parts, context) { const key = this.formKey(extraFile); const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application); const b = aliyunCosConfig.buckets.find((ele) => ele.name === extraFile.bucket); assert(b, `extraFile中的bucket名称在阿里云配置中找不到「${extraFile.bucket}」`); assert(extraFile.chunkInfo?.uploadId, 'extraFile缺少chunkInfo.uploadId,无法完成分片上传的合并操作'); assert(parts.length > 0, 'parts不能为空,无法完成分片上传的合并操作'); try { await instance.completeMultipartUpload(extraFile.bucket, b.zone, key, extraFile.chunkInfo.uploadId, parts.map(part => ({ number: part.partNumber, etag: part.etag, }))); } catch (err) { throw new OakException('合并分片上传失败' + 'extraFile' + err); } } async abortMultipartUpload(application, extraFile, context) { const key = this.formKey(extraFile); const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application); const b = aliyunCosConfig.buckets.find((ele) => ele.name === extraFile.bucket); assert(b, `extraFile中的bucket名称在阿里云配置中找不到「${extraFile.bucket}」`); assert(extraFile.chunkInfo?.uploadId, 'extraFile缺少chunkInfo.uploadId,无法中止分片上传操作'); await instance.abortMultipartUpload(extraFile.bucket, b.zone, key, extraFile.chunkInfo.uploadId); } async listMultipartUploads(application, extraFile, context) { const key = this.formKey(extraFile); const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application); const b = aliyunCosConfig.buckets.find((ele) => ele.name === extraFile.bucket); assert(b, `extraFile中的bucket名称在阿里云配置中找不到「${extraFile.bucket}」`); assert(extraFile.chunkInfo?.uploadId, 'extraFile缺少chunkInfo.uploadId,无法列出分片上传信息'); return await instance.listParts(extraFile.bucket, b.zone, key, extraFile.chunkInfo.uploadId); } async presignFile(methods, application, extraFile, context) { const key = this.formKey(extraFile); const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application); const b = aliyunCosConfig.buckets.find((ele) => ele.name === extraFile.bucket); assert(b, `extraFile中的bucket名称在阿里云配置中找不到「${extraFile.bucket}」`); return await instance.presignObjectUrl(methods, extraFile.bucket, b.zone, key, { expires: 24 * 60 * 60, // 1 day }); } /** * 对一段文件的分片上传进行预签名 * @param extraFileId extraFile的id * @param from 起始partNumber * @param to 结束partNumber */ async presignMultiPartUpload(application, extraFile, from, to, context) { const key = this.formKey(extraFile); const { instance, config: aliyunCosConfig } = this.getConfigAndInstance(application); const b = aliyunCosConfig.buckets.find((ele) => ele.name === extraFile.bucket); assert(b, `extraFile中的bucket名称在阿里云配置中找不到「${extraFile.bucket}」`); const res = await instance.presignMulti(extraFile.bucket, b.zone, key, extraFile.chunkInfo.uploadId, from, to, { expires: 24 * 60 * 60, // 1 day }); return res; } async prepareChunkedUpload(application, extraFile, context) { const key = this.formKey(extraFile); const { instance, config: aliyunCosConfig, account } = this.getConfigAndInstance(application); let useSts = true; let stsInfo = {}; if (!account.stsEndpoint || !account.roleArn || !account.roleSessionName) { useSts = false; console.warn("阿里云Cos配置中缺少sts相关配置,无法使用sts方式上传分片,将使用账号授权进行上传,可能存在安全风险,请检查确保不会暴露accessKey"); } else { try { const res = await stsAssumeRole({ accessKeyId: account.accessKeyId, accessKeySecret: account.accessKeySecret, endpoint: account.stsEndpoint, roleArn: account.roleArn, roleSessionName: account.roleSessionName, }); stsInfo = { stsToken: res.Credentials.SecurityToken, accessKeyId: res.Credentials.AccessKeyId, accessKeySecret: res.Credentials.AccessKeySecret, }; } catch (err) { console.error("Failed to assume role for STS:", err); throw new OakPreConditionUnsetException("获取阿里云STS临时凭证失败,请检查配置是否正确", 'extraFile'); } } // 大部分校验都在formUploadMeta中完成,这里可以不多做判断了 const b = aliyunCosConfig.buckets.find((ele) => ele.name === extraFile.bucket); const preInit = await instance.initiateMultipartUpload(extraFile.bucket, b.zone, key, { timeout: 30 * 1000, // 30 seconds ...(useSts ? stsInfo : {}), }); return { uploadId: preInit.uploadId, }; } }