113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||
import type {
|
||
IOssProvider,
|
||
OssPutObjectInput,
|
||
OssPutObjectResult,
|
||
OssUploadTokenInput,
|
||
OssUploadTokenResult,
|
||
} from './oss.interface';
|
||
import { buildOssObjectKey, resolveOssUploadDir } from './oss.key.util';
|
||
import { createAliyunOssClient, resolveOssUploadHost } from './oss.aliyun.client';
|
||
|
||
const DEFAULT_EXPIRE_SECONDS = 15 * 60;
|
||
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
||
|
||
@Injectable()
|
||
export class OssAliyunProvider implements IOssProvider {
|
||
private readonly accessKeyId = process.env.OSS_ACCESS_KEY_ID ?? '';
|
||
private readonly accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET ?? '';
|
||
private readonly bucket = process.env.OSS_BUCKET ?? '';
|
||
private readonly region = process.env.OSS_REGION ?? 'oss-cn-hangzhou';
|
||
private readonly cdnBase = process.env.OSS_CDN_BASE ?? '';
|
||
private readonly endpoint = process.env.OSS_ENDPOINT ?? '';
|
||
private readonly uploadPrefix = (process.env.OSS_UPLOAD_PREFIX ?? 'uploads').replace(/\/$/, '');
|
||
private readonly expireSeconds = Number(process.env.OSS_UPLOAD_EXPIRE_SECONDS ?? DEFAULT_EXPIRE_SECONDS);
|
||
private readonly maxUploadBytes = Number(process.env.OSS_MAX_UPLOAD_BYTES ?? DEFAULT_MAX_BYTES);
|
||
private readonly authorizationV4 = process.env.OSS_AUTHORIZATION_V4 === 'true';
|
||
private client: ReturnType<typeof createAliyunOssClient> | null = null;
|
||
|
||
isEnabled() {
|
||
return !!(this.accessKeyId && this.accessKeySecret && this.bucket);
|
||
}
|
||
|
||
private assertConfigured() {
|
||
if (this.isEnabled()) return;
|
||
throw new InternalServerErrorException(
|
||
'OSS 未配置:请设置 OSS_ACCESS_KEY_ID、OSS_ACCESS_KEY_SECRET、OSS_BUCKET(及 OSS_REGION)',
|
||
);
|
||
}
|
||
|
||
private getClient() {
|
||
this.assertConfigured();
|
||
if (!this.client) {
|
||
this.client = createAliyunOssClient({
|
||
accessKeyId: this.accessKeyId,
|
||
accessKeySecret: this.accessKeySecret,
|
||
bucket: this.bucket,
|
||
region: this.region,
|
||
endpoint: this.endpoint || undefined,
|
||
authorizationV4: this.authorizationV4,
|
||
});
|
||
}
|
||
return this.client;
|
||
}
|
||
|
||
buildPublicUrl(ossKey: string) {
|
||
const key = ossKey.replace(/^\//, '');
|
||
const client = this.getClient();
|
||
return client.generateObjectUrl(key, this.cdnBase || undefined);
|
||
}
|
||
|
||
getUploadToken(dto: OssUploadTokenInput): OssUploadTokenResult {
|
||
const client = this.getClient();
|
||
const ossKey = buildOssObjectKey(this.uploadPrefix, dto.bizType, dto.fileName);
|
||
const expireAt = new Date(Date.now() + this.expireSeconds * 1000);
|
||
const host = resolveOssUploadHost(this.bucket, this.region);
|
||
const keyPrefix = resolveOssUploadDir(this.uploadPrefix, dto.bizType);
|
||
|
||
const policy = {
|
||
expiration: expireAt.toISOString(),
|
||
conditions: [
|
||
['content-length-range', 0, this.maxUploadBytes],
|
||
['eq', '$bucket', this.bucket],
|
||
['starts-with', '$key', keyPrefix],
|
||
],
|
||
};
|
||
|
||
const signed = client.calculatePostSignature(policy);
|
||
|
||
return {
|
||
bucket: this.bucket,
|
||
region: this.region,
|
||
ossKey,
|
||
url: this.buildPublicUrl(ossKey),
|
||
mock: false,
|
||
expireAt: expireAt.toISOString(),
|
||
mediaType: dto.mediaType,
|
||
bizType: dto.bizType,
|
||
host,
|
||
policy: signed.policy,
|
||
signature: signed.Signature,
|
||
accessKeyId: signed.OSSAccessKeyId,
|
||
};
|
||
}
|
||
|
||
async putObject(input: OssPutObjectInput): Promise<OssPutObjectResult> {
|
||
const client = this.getClient();
|
||
const ossKey = buildOssObjectKey(this.uploadPrefix, input.bizType, input.fileName);
|
||
await client.put(ossKey, input.buffer, {
|
||
mime: input.mimeType || 'application/octet-stream',
|
||
headers: {
|
||
'Content-Disposition': 'inline',
|
||
},
|
||
});
|
||
return {
|
||
bucket: this.bucket,
|
||
region: this.region,
|
||
ossKey,
|
||
url: this.buildPublicUrl(ossKey),
|
||
mock: false,
|
||
};
|
||
}
|
||
}
|