file.service.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import { GenericCrudService } from '@/server/utils/generic-crud.service';
  2. import { DataSource } from 'typeorm';
  3. import { File } from './file.entity';
  4. import { MinioService } from './minio.service';
  5. // import { AppError } from '@/server/utils/errorHandler';
  6. import { v4 as uuidv4 } from 'uuid';
  7. import { logger } from '@/server/utils/logger';
  8. export class FileService extends GenericCrudService<File> {
  9. private readonly minioService: MinioService;
  10. constructor(dataSource: DataSource) {
  11. super(dataSource, File);
  12. this.minioService = new MinioService();
  13. }
  14. /**
  15. * 创建文件记录并生成预签名上传URL
  16. */
  17. async createFile(data: Partial<File>) {
  18. try {
  19. // 生成唯一文件存储路径
  20. const fileKey = `${data.uploadUserId}/${uuidv4()}-${data.name}`;
  21. // 生成MinIO上传策略
  22. const uploadPolicy = await this.minioService.generateUploadPolicy(fileKey);
  23. // 准备文件记录数据
  24. const fileData = {
  25. ...data,
  26. path: fileKey,
  27. uploadTime: new Date(),
  28. createdAt: new Date(),
  29. updatedAt: new Date()
  30. };
  31. // 保存文件记录到数据库
  32. const savedFile = await this.create(fileData as File);
  33. // 返回文件记录和上传策略
  34. return {
  35. file: savedFile,
  36. uploadPolicy
  37. };
  38. } catch (error) {
  39. logger.error('Failed to create file:', error);
  40. throw new Error('文件创建失败');
  41. }
  42. }
  43. /**
  44. * 删除文件记录及对应的MinIO文件
  45. */
  46. async deleteFile(id: number) {
  47. try {
  48. // 获取文件记录
  49. const file = await this.getById(id);
  50. if (!file) {
  51. throw new Error('文件不存在');
  52. }
  53. // 验证文件是否存在于MinIO
  54. const fileExists = await this.minioService.objectExists(this.minioService.bucketName, file.path);
  55. if (!fileExists) {
  56. logger.error(`File not found in MinIO: ${this.minioService.bucketName}/${file.path}`);
  57. // 仍然继续删除数据库记录,但记录警告日志
  58. } else {
  59. // 从MinIO删除文件
  60. await this.minioService.deleteObject(this.minioService.bucketName, file.path);
  61. }
  62. // 从数据库删除记录
  63. await this.delete(id);
  64. return true;
  65. } catch (error) {
  66. logger.error('Failed to delete file:', error);
  67. throw new Error('文件删除失败');
  68. }
  69. }
  70. /**
  71. * 获取文件访问URL
  72. */
  73. async getFileUrl(id: number) {
  74. const file = await this.getById(id);
  75. if (!file) {
  76. throw new Error('文件不存在');
  77. }
  78. return this.minioService.getFileUrl(this.minioService.bucketName, file.path);
  79. }
  80. /**
  81. * 创建多部分上传策略
  82. */
  83. async createMultipartUploadPolicy(data: Partial<File>, partCount: number) {
  84. try {
  85. // 生成唯一文件存储路径
  86. const fileKey = `${data.uploadUserId}/${uuidv4()}-${data.name}`;
  87. // 初始化多部分上传
  88. const uploadId = await this.minioService.createMultipartUpload(
  89. this.minioService.bucketName,
  90. fileKey
  91. );
  92. // 生成各部分上传URL
  93. const uploadUrls = await this.minioService.generateMultipartUploadUrls(
  94. this.minioService.bucketName,
  95. fileKey,
  96. uploadId,
  97. partCount
  98. );
  99. // 准备文件记录数据
  100. const fileData = {
  101. ...data,
  102. path: fileKey,
  103. uploadTime: new Date(),
  104. createdAt: new Date(),
  105. updatedAt: new Date()
  106. };
  107. // 保存文件记录到数据库
  108. const savedFile = await this.create(fileData as File);
  109. // 返回文件记录和上传策略
  110. return {
  111. file: savedFile,
  112. uploadId,
  113. uploadUrls,
  114. bucket: this.minioService.bucketName,
  115. key: fileKey
  116. };
  117. } catch (error) {
  118. logger.error('Failed to create multipart upload policy:', error);
  119. throw new Error('创建多部分上传策略失败');
  120. }
  121. }
  122. /**
  123. * 完成分片上传
  124. */
  125. async completeMultipartUpload(data: {
  126. uploadId: string;
  127. bucket: string;
  128. key: string;
  129. parts: Array<{ PartNumber: number; ETag: string }>;
  130. }) {
  131. try {
  132. // 完成MinIO分片上传
  133. const result = await this.minioService.completeMultipartUpload(
  134. data.bucket,
  135. data.key,
  136. data.uploadId,
  137. data.parts.map(part => ({ PartNumber: part.PartNumber, ETag: part.ETag }))
  138. );
  139. // 查找文件记录并更新
  140. const file = await this.repository.findOneBy({ path: data.key });
  141. if (!file) {
  142. throw new Error('文件记录不存在');
  143. }
  144. // 更新文件大小等信息
  145. file.size = result.size;
  146. file.updatedAt = new Date();
  147. await this.repository.save(file);
  148. // 生成文件访问URL
  149. const url = this.minioService.getFileUrl(data.bucket, data.key);
  150. return {
  151. fileId: file.id,
  152. url,
  153. key: data.key,
  154. size: result.size
  155. };
  156. } catch (error) {
  157. logger.error('Failed to complete multipart upload:', error);
  158. throw new Error('完成分片上传失败');
  159. }
  160. }
  161. }