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 | 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 { 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, }; } }