2
0

file.service.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  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.getPresignedFileUrl(this.minioService.bucketName, file.path);
  79. }
  80. /**
  81. * 获取文件下载URL(带Content-Disposition头)
  82. */
  83. async getFileDownloadUrl(id: number) {
  84. const file = await this.getById(id);
  85. if (!file) {
  86. throw new Error('文件不存在');
  87. }
  88. const url = await this.minioService.getPresignedFileDownloadUrl(
  89. this.minioService.bucketName,
  90. file.path,
  91. file.name
  92. );
  93. return {
  94. url,
  95. filename: file.name
  96. };
  97. }
  98. /**
  99. * 创建多部分上传策略
  100. */
  101. async createMultipartUploadPolicy(data: Partial<File>, partCount: number) {
  102. try {
  103. // 生成唯一文件存储路径
  104. const fileKey = `${data.uploadUserId}/${uuidv4()}-${data.name}`;
  105. // 初始化多部分上传
  106. const uploadId = await this.minioService.createMultipartUpload(
  107. this.minioService.bucketName,
  108. fileKey
  109. );
  110. // 生成各部分上传URL
  111. const uploadUrls = await this.minioService.generateMultipartUploadUrls(
  112. this.minioService.bucketName,
  113. fileKey,
  114. uploadId,
  115. partCount
  116. );
  117. // 准备文件记录数据
  118. const fileData = {
  119. ...data,
  120. path: fileKey,
  121. uploadTime: new Date(),
  122. createdAt: new Date(),
  123. updatedAt: new Date()
  124. };
  125. // 保存文件记录到数据库
  126. const savedFile = await this.create(fileData as File);
  127. // 返回文件记录和上传策略
  128. return {
  129. file: savedFile,
  130. uploadId,
  131. uploadUrls,
  132. bucket: this.minioService.bucketName,
  133. key: fileKey
  134. };
  135. } catch (error) {
  136. logger.error('Failed to create multipart upload policy:', error);
  137. throw new Error('创建多部分上传策略失败');
  138. }
  139. }
  140. /**
  141. * 完成分片上传
  142. */
  143. async completeMultipartUpload(data: {
  144. uploadId: string;
  145. bucket: string;
  146. key: string;
  147. parts: Array<{ partNumber: number; etag: string }>;
  148. }) {
  149. try {
  150. logger.db('Starting multipart upload completion:', {
  151. uploadId: data.uploadId,
  152. bucket: data.bucket,
  153. key: data.key,
  154. partsCount: data.parts.length
  155. });
  156. // 完成MinIO分片上传 - 注意格式转换
  157. const result = await this.minioService.completeMultipartUpload(
  158. data.bucket,
  159. data.key,
  160. data.uploadId,
  161. data.parts.map(part => ({ PartNumber: part.partNumber, ETag: part.etag }))
  162. );
  163. // 查找文件记录并更新
  164. const file = await this.repository.findOneBy({ path: data.key });
  165. if (!file) {
  166. throw new Error('文件记录不存在');
  167. }
  168. // 更新文件大小等信息
  169. file.size = result.size;
  170. file.updatedAt = new Date();
  171. await this.repository.save(file);
  172. // 生成文件访问URL
  173. const url = this.minioService.getFileUrl(data.bucket, data.key);
  174. logger.db('Multipart upload completed successfully:', {
  175. fileId: file.id,
  176. size: result.size,
  177. key: data.key
  178. });
  179. return {
  180. fileId: file.id,
  181. url,
  182. key: data.key,
  183. size: result.size
  184. };
  185. } catch (error) {
  186. logger.error('Failed to complete multipart upload:', error);
  187. throw new Error('完成分片上传失败');
  188. }
  189. }
  190. }