import { Injectable, BadRequestException, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { CosStorageProvider } from './cos-storage.provider'; import { LocalStorageProvider } from './local-storage.provider'; import { sanitizeFilename, validateFileUpload, } from '../../common/utils/security.util'; export interface CreateUploadUrlInput { filename: string; mimeType: string; sizeBytes: number; objectKey?: string; } export interface UploadUrlResult { uploadUrl: string; objectKey: string; bucket: string; region: string; expiresIn: number; } export interface VerifiedUpload { size: number; etag: string; contentType: string; } @Injectable() export class StorageService { private readonly logger = new Logger(StorageService.name); private readonly driver: 'cos' | 'local'; constructor( private readonly cos: CosStorageProvider, private readonly local: LocalStorageProvider, private readonly configService: ConfigService, ) { this.driver = (configService.get('storage.driver', 'cos')) as 'cos' | 'local'; this.logger.log(`Storage driver: ${this.driver}`); } getUploadPath(filename: string): string { const basePath = this.configService.get( 'storage.localPath', './uploads', ); return `${basePath}/${filename}`; } generateObjectKey(userId: string, originalFilename: string, uploadId?: string): string { const date = new Date(); const yearMonth = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}`; const safeName = sanitizeFilename(originalFilename); const env = this.configService.get('app.nodeEnv', 'development'); const prefix = env === 'production' ? '' : 'dev/'; return uploadId ? `${prefix}${userId}/${yearMonth}/${uploadId}-${safeName}` : `${prefix}${userId}/${yearMonth}/${safeName}`; } async createUploadUrl( userId: string, input: CreateUploadUrlInput, expiresIn = 3600, ): Promise { validateFileUpload(input.mimeType, input.sizeBytes); const objectKey = input.objectKey ?? this.generateObjectKey(userId, input.filename); return this.createUploadUrlForObject(objectKey, expiresIn); } async createUploadUrlForObject( objectKey: string, expiresIn = 3600, ): Promise { if (this.driver === 'local') { const result = await this.local.generateUploadUrl(objectKey, expiresIn); return { uploadUrl: result.uploadUrl, objectKey: result.objectKey, bucket: result.bucket, region: result.region, expiresIn, }; } const result = await this.cos.generateUploadUrl(objectKey, expiresIn); return { uploadUrl: result.uploadUrl, objectKey: result.objectKey, bucket: result.bucket, region: result.region, expiresIn, }; } async verifyUpload(objectKey: string): Promise { const info = this.driver === 'local' ? await this.local.headObject(objectKey) : await this.cos.headObject(objectKey); if (!info) { throw new BadRequestException('文件未被上传到存储服务'); } return info; } async getDownloadUrl( objectKey: string, expiresInSeconds?: number, ): Promise { return this.driver === 'local' ? this.local.generateDownloadUrl(objectKey, expiresInSeconds) : this.cos.generateDownloadUrl(objectKey, expiresInSeconds); } async deleteObject(objectKey: string): Promise { if (this.driver === 'local') { await this.local.deleteObject(objectKey); } else { await this.cos.deleteObject(objectKey); } } async getObject(objectKey: string): Promise { return this.driver === 'local' ? this.local.getObject(objectKey) : this.cos.getObject(objectKey); } async healthCheck(): Promise { return this.driver === 'local' ? this.local.healthCheck() : this.cos.healthCheck(); } }