api-server/src/infrastructure/storage/storage.service.ts
wangdl a621bd169d
Some checks failed
Deploy API Server / build-and-unit (push) Successful in 41s
Deploy API Server / current-integration (push) Successful in 33s
Deploy API Server / backward-compat (push) Successful in 0s
Deploy API Server / deploy (push) Failing after 57s
feat: 文档清理 + RAG Worker 迁移 + 批量操作 + HTTP 日志 + 学习会话修复
- 删除根 docs/ 中的 audit/issue 文档
- 删除 api-server/docs/ 中的 16 个审核/里程碑文档
- rag-worker/ 从 api-server 迁出为独立项目
- 新增 admin 导入批量重新解析、按知识库筛选、后端排序
- 新增 source 软删除标记,防止已删 source 重新解析
- 修复 upload-url 500(重复 UploadSession 创建 + 缺 sizeBytes)
- 修复 loadContextByScope 读 chunks 而非依赖 textLength
- 修复 MaterialReadingProgress.create() increment 语法错误
- 新增 LoggingInterceptor 记录所有 HTTP 请求
- 新增学习会话批量删除接口
- enrichWithNames 增加 email 回退
- 全局异常过滤器和 name-resolver 增强

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-01 22:44:36 +08:00

140 lines
4.0 KiB
TypeScript

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<string>('storage.driver', 'cos')) as 'cos' | 'local';
this.logger.log(`Storage driver: ${this.driver}`);
}
getUploadPath(filename: string): string {
const basePath = this.configService.get<string>(
'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<string>('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<UploadUrlResult> {
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<UploadUrlResult> {
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<VerifiedUpload> {
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<string> {
return this.driver === 'local'
? this.local.generateDownloadUrl(objectKey, expiresInSeconds)
: this.cos.generateDownloadUrl(objectKey, expiresInSeconds);
}
async deleteObject(objectKey: string): Promise<void> {
if (this.driver === 'local') {
await this.local.deleteObject(objectKey);
} else {
await this.cos.deleteObject(objectKey);
}
}
async getObject(objectKey: string): Promise<Buffer | null> {
return this.driver === 'local'
? this.local.getObject(objectKey)
: this.cos.getObject(objectKey);
}
async healthCheck(): Promise<boolean> {
return this.driver === 'local'
? this.local.healthCheck()
: this.cos.healthCheck();
}
}