api-server/src/modules/files/files.service.ts
WangDL 6d7cbffc3b
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 3m0s
feat: COS 对象存储接入 — CosStorageProvider + FilesModule
- 安装 cos-nodejs-sdk-v5,封装 CosStorageProvider(upload/download/delete/healthCheck)
- 重写 StorageService,新增 createUploadUrl/verifyUpload/getDownloadUrl/deleteObject
- 创建 FilesModule:POST /files/upload-url, POST /files/complete, GET /files/:id, DELETE /files/:id
- UploadedFile 新增 objectKey/bucket 字段
- 对象键格式 {userId}/{YYYYMM}/{sanitizedName}.{ext}
- 接入文件类型校验(ALLOWED_FILE_TYPES)+ 上传限流(10次/小时/用户)
- 配置文件 cos.longde.cloud → zhixi-1259685406 / ap-guangzhou

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 22:30:14 +08:00

75 lines
2.2 KiB
TypeScript

import {
Injectable,
NotFoundException,
ForbiddenException,
} from '@nestjs/common';
import { FilesRepository } from './files.repository';
import { StorageService } from '../../infrastructure/storage/storage.service';
import { CosStorageProvider } from '../../infrastructure/storage/cos-storage.provider';
import { CreateUploadUrlDto, CompleteUploadDto } from './dto';
@Injectable()
export class FilesService {
constructor(
private readonly repository: FilesRepository,
private readonly storage: StorageService,
private readonly cos: CosStorageProvider,
) {}
async requestUploadUrl(userId: string, dto: CreateUploadUrlDto) {
return this.storage.createUploadUrl(userId, {
filename: dto.filename,
mimeType: dto.mimeType,
sizeBytes: dto.sizeBytes,
});
}
async confirmUpload(userId: string, dto: CompleteUploadDto) {
const info = await this.storage.verifyUpload(dto.objectKey);
const parts = dto.objectKey.split('/');
const originalFilename = parts[parts.length - 1];
return this.repository.create({
userId,
filename: originalFilename,
mimeType: info.contentType,
storagePath: dto.objectKey,
objectKey: dto.objectKey,
bucket: this.cos.getBucket(),
sizeBytes: info.size,
checksum: dto.checksum,
});
}
async getFile(userId: string, fileId: string) {
const file = await this.repository.findById(fileId);
if (!file) {
throw new NotFoundException('文件不存在');
}
if (file.userId !== userId) {
throw new ForbiddenException('无权访问该文件');
}
const downloadUrl = await this.storage.getDownloadUrl(file.objectKey!);
return { file, downloadUrl };
}
async deleteFile(userId: string, fileId: string) {
const file = await this.repository.findById(fileId);
if (!file) {
throw new NotFoundException('文件不存在');
}
if (file.userId !== userId) {
throw new ForbiddenException('无权操作该文件');
}
await this.storage.deleteObject(file.objectKey!);
await this.repository.delete(fileId);
}
async findByUserId(userId: string) {
return this.repository.findByUserId(userId);
}
}