file.service.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  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. await this.minioService.deleteObject(this.minioService.bucketName, file.path);
  55. // 从数据库删除记录
  56. await this.delete(id);
  57. return true;
  58. } catch (error) {
  59. logger.error('Failed to delete file:', error);
  60. throw new Error('文件删除失败');
  61. }
  62. }
  63. /**
  64. * 获取文件访问URL
  65. */
  66. async getFileUrl(id: number) {
  67. const file = await this.getById(id);
  68. if (!file) {
  69. throw new Error('文件不存在');
  70. }
  71. return this.minioService.getFileUrl(this.minioService.bucketName, file.path);
  72. }
  73. /**
  74. * 创建多部分上传策略
  75. */
  76. async createMultipartUploadPolicy(data: Partial<File>, partCount: number) {
  77. try {
  78. // 生成唯一文件存储路径
  79. const fileKey = `${data.uploadUserId}/${uuidv4()}-${data.name}`;
  80. // 初始化多部分上传
  81. const uploadId = await this.minioService.createMultipartUpload(
  82. this.minioService.bucketName,
  83. fileKey
  84. );
  85. // 生成各部分上传URL
  86. const uploadUrls = await this.minioService.generateMultipartUploadUrls(
  87. this.minioService.bucketName,
  88. fileKey,
  89. uploadId,
  90. partCount
  91. );
  92. // 准备文件记录数据
  93. const fileData = {
  94. ...data,
  95. path: fileKey,
  96. uploadTime: new Date(),
  97. createdAt: new Date(),
  98. updatedAt: new Date()
  99. };
  100. // 保存文件记录到数据库
  101. const savedFile = await this.create(fileData as File);
  102. // 返回文件记录和上传策略
  103. return {
  104. file: savedFile,
  105. uploadId,
  106. uploadUrls,
  107. bucket: this.minioService.bucketName,
  108. key: fileKey
  109. };
  110. } catch (error) {
  111. logger.error('Failed to create multipart upload policy:', error);
  112. throw new Error('创建多部分上传策略失败');
  113. }
  114. }
  115. /**
  116. * 完成分片上传
  117. */
  118. async completeMultipartUpload(data: {
  119. uploadId: string;
  120. bucket: string;
  121. key: string;
  122. parts: Array<{ PartNumber: number; ETag: string }>;
  123. }) {
  124. try {
  125. // 完成MinIO分片上传
  126. const result = await this.minioService.completeMultipartUpload(
  127. data.bucket,
  128. data.key,
  129. data.uploadId,
  130. data.parts.map(part => ({ PartNumber: part.PartNumber, ETag: part.ETag }))
  131. );
  132. // 查找文件记录并更新
  133. const file = await this.repository.findOneBy({ path: data.key });
  134. if (!file) {
  135. throw new Error('文件记录不存在');
  136. }
  137. // 更新文件大小等信息
  138. file.size = result.size;
  139. file.updatedAt = new Date();
  140. await this.repository.save(file);
  141. // 生成文件访问URL
  142. const url = this.minioService.getFileUrl(data.bucket, data.key);
  143. return {
  144. fileId: file.id,
  145. url,
  146. key: data.key,
  147. size: result.size
  148. };
  149. } catch (error) {
  150. logger.error('Failed to complete multipart upload:', error);
  151. throw new Error('完成分片上传失败');
  152. }
  153. }
  154. }