oak-general-business/src/utils/cos/s3.ts

149 lines
5.0 KiB
TypeScript

import { EntityDict } from '../../oak-app-domain';
import { assert } from 'oak-domain/lib/utils/assert';
import { Cos, UploadFn, UploadToAspect } from "../../types/Cos";
import { OpSchema } from '../../oak-app-domain/ExtraFile/Schema';
import { S3UploadInfo } from '../../types/Upload';
import { S3CosConfig, Protocol } from '../../types/Config';
import { OakUploadException } from '../../types/Exception';
import { OakNetworkException } from 'oak-domain/lib/types/Exception';
import { chunkUpload } from './common';
export default class S3 implements Cos<EntityDict> {
name = 's3';
autoInform(): boolean {
return false;
}
protected getConfig(application: Partial<EntityDict['application']['Schema']>) {
const { system } = application;
const { config } = system!;
const s3Config = config.Cos?.s3;
assert(s3Config);
const { accessKey, endpoint, defaultBucket } = s3Config;
const account = config.Account?.s3?.find(
(ele) => ele.accessKey === accessKey
);
assert(account);
return {
config: s3Config,
account,
endpoint,
defaultBucket,
};
}
protected formKey(extraFile: Partial<OpSchema>) {
const { id, extension, objectId } = extraFile;
assert(objectId);
return `extraFile/${objectId}${extension ? '.' + extension : ''}`;
}
async upload(
options: {
extraFile: OpSchema,
uploadFn: UploadFn,
file: string | File,
uploadToAspect?: UploadToAspect,
getPercent?: Function
// 分片上传时使用
parallelism?: number // 并行线程数
retryTimes?: number // 重试次数
retryDelay?: number // 重试间隔,单位毫秒
onChunkSuccess?: (chunkInfo: EntityDict['extraFile']['Schema']['chunkInfo']) => Promise<void> // 每个分片上传成功的回调
}
) {
const { extraFile, uploadFn, file, getPercent, parallelism, retryTimes, retryDelay, onChunkSuccess } = options;
const uploadMeta = extraFile.uploadMeta! as S3UploadInfo;
if (extraFile.enableChunkedUpload) {
return chunkUpload({
extraFile,
uploadFn,
file,
getPercent,
parallelism: parallelism,
retryTimes: retryTimes,
retryDelay: retryDelay,
onChunkSuccess: onChunkSuccess,
});
} else {
let response;
try {
// S3 使用预签名 URL 直接上传,不需要额外的 formData
response = await uploadFn(
file,
'file',
uploadMeta.uploadUrl,
{},
true,
getPercent,
extraFile.id,
"PUT"
);
} catch (err) {
throw new OakNetworkException('网络异常,请求失败');
}
let isSuccess = false;
if (process.env.OAK_PLATFORM === 'wechatMp') {
// 小程序端上传
if (response.errMsg === 'uploadFile:ok') {
const statusCode = response.statusCode;
isSuccess = statusCode === 200 || statusCode === 204;
}
} else {
isSuccess = response.status === 200 || response.status === 204;
}
if (isSuccess) {
return;
}
}
throw new OakUploadException('文件上传S3失败');
}
composeFileUrl(
options: {
application: Partial<EntityDict['application']['Schema']>,
extraFile: Partial<EntityDict['extraFile']['OpSchema']>,
style?: string,
},
) {
const { application, extraFile, style } = options;
const { config: s3CosConfig, endpoint } = this.getConfig(application);
if (s3CosConfig) {
let bucket = (
s3CosConfig.buckets as S3CosConfig['buckets']
).find((ele) => ele.name === extraFile.bucket!);
if (bucket) {
const { domain, protocol, pathStyle } = bucket;
let protocol2 = protocol;
if (protocol instanceof Array) {
const index = (protocol as Protocol[]).includes('https:')
? protocol.findIndex((ele) => ele === 'https:')
: 0;
protocol2 = protocol[index];
}
const key = this.formKey(extraFile);
// 如果使用 pathStyle (Minio 常用)
if (pathStyle && endpoint) {
return `${protocol2}//${domain}/${bucket.name}/${key}${style ? style : ''}`;
}
// 否则使用虚拟主机风格 (AWS S3 默认)
return `${protocol2}//${domain}/${key}${style ? style : ''}`;
}
}
return '';
}
}