import { GenericCrudService } from '@/server/utils/generic-crud.service'; import { DataSource } from 'typeorm'; import { File } from './file.entity'; import { MinioService } from './minio.service'; // import { AppError } from '@/server/utils/errorHandler'; import { v4 as uuidv4 } from 'uuid'; import { logger } from '@/server/utils/logger'; export class FileService extends GenericCrudService { private readonly minioService: MinioService; constructor(dataSource: DataSource) { super(dataSource, File); this.minioService = new MinioService(); } /** * 创建文件记录并生成预签名上传URL */ async createFile(data: Partial) { try { // 生成唯一文件存储路径 const fileKey = `${data.uploadUserId}/${uuidv4()}-${data.name}`; // 生成MinIO上传策略 const uploadPolicy = await this.minioService.generateUploadPolicy(fileKey); // 准备文件记录数据 const fileData = { ...data, path: fileKey, uploadTime: new Date(), createdAt: new Date(), updatedAt: new Date() }; // 保存文件记录到数据库 const savedFile = await this.create(fileData as File); // 返回文件记录和上传策略 return { file: savedFile, uploadPolicy }; } catch (error) { logger.error('Failed to create file:', error); throw new Error('文件创建失败'); } } /** * 删除文件记录及对应的MinIO文件 */ async deleteFile(id: number) { try { // 获取文件记录 const file = await this.getById(id); if (!file) { throw new Error('文件不存在'); } // 验证文件是否存在于MinIO const fileExists = await this.minioService.objectExists(this.minioService.bucketName, file.path); if (!fileExists) { logger.error(`File not found in MinIO: ${this.minioService.bucketName}/${file.path}`); // 仍然继续删除数据库记录,但记录警告日志 } else { // 从MinIO删除文件 await this.minioService.deleteObject(this.minioService.bucketName, file.path); } // 从数据库删除记录 await this.delete(id); return true; } catch (error) { logger.error('Failed to delete file:', error); throw new Error('文件删除失败'); } } /** * 获取文件访问URL */ async getFileUrl(id: number) { const file = await this.getById(id); if (!file) { throw new Error('文件不存在'); } return this.minioService.getFileUrl(this.minioService.bucketName, file.path); } /** * 创建多部分上传策略 */ async createMultipartUploadPolicy(data: Partial, partCount: number) { try { // 生成唯一文件存储路径 const fileKey = `${data.uploadUserId}/${uuidv4()}-${data.name}`; // 初始化多部分上传 const uploadId = await this.minioService.createMultipartUpload( this.minioService.bucketName, fileKey ); // 生成各部分上传URL const uploadUrls = await this.minioService.generateMultipartUploadUrls( this.minioService.bucketName, fileKey, uploadId, partCount ); // 准备文件记录数据 const fileData = { ...data, path: fileKey, uploadTime: new Date(), createdAt: new Date(), updatedAt: new Date() }; // 保存文件记录到数据库 const savedFile = await this.create(fileData as File); // 返回文件记录和上传策略 return { file: savedFile, uploadId, uploadUrls, bucket: this.minioService.bucketName, key: fileKey }; } catch (error) { logger.error('Failed to create multipart upload policy:', error); throw new Error('创建多部分上传策略失败'); } } /** * 完成分片上传 */ async completeMultipartUpload(data: { uploadId: string; bucket: string; key: string; parts: Array<{ PartNumber: number; ETag: string }>; }) { try { // 完成MinIO分片上传 const result = await this.minioService.completeMultipartUpload( data.bucket, data.key, data.uploadId, data.parts.map(part => ({ PartNumber: part.PartNumber, ETag: part.ETag })) ); // 查找文件记录并更新 const file = await this.repository.findOneBy({ path: data.key }); if (!file) { throw new Error('文件记录不存在'); } // 更新文件大小等信息 file.size = result.size; file.updatedAt = new Date(); await this.repository.save(file); // 生成文件访问URL const url = this.minioService.getFileUrl(data.bucket, data.key); return { fileId: file.id, url, key: data.key, size: result.size }; } catch (error) { logger.error('Failed to complete multipart upload:', error); throw new Error('完成分片上传失败'); } } }