Przeglądaj źródła

✨ feat(files): add file management features with MinIO integration

- add uuid and @types/uuid dependencies for unique file naming
- implement createFile method to generate upload policies and save file records
- add deleteFile method to remove files from both database and MinIO
- implement getFileUrl method to retrieve file access URLs
- add deleteObject method in MinioService for object deletion

♻️ refactor(minio): enhance MinIO service functionality

- add deleteObject method to handle object removal from MinIO storage
- improve error logging for object deletion operations
yourname 8 miesięcy temu
rodzic
commit
bb849a2b2a

+ 3 - 1
package.json

@@ -20,6 +20,7 @@
     "@tanstack/react-query": "^5.77.2",
     "@types/bcrypt": "^5.0.2",
     "@types/jsonwebtoken": "^9.0.9",
+    "@types/uuid": "^10.0.0",
     "antd": "^5.26.0",
     "axios": "^1.9.0",
     "bcrypt": "^6.0.0",
@@ -41,7 +42,8 @@
     "react-router-dom": "^7.6.1",
     "react-toastify": "^11.0.5",
     "reflect-metadata": "^0.2.2",
-    "typeorm": "^0.3.24"
+    "typeorm": "^0.3.24",
+    "uuid": "^11.1.0"
   },
   "devDependencies": {
     "@types/debug": "^4.1.12",

+ 11 - 0
pnpm-lock.yaml

@@ -44,6 +44,9 @@ importers:
       '@types/jsonwebtoken':
         specifier: ^9.0.9
         version: 9.0.9
+      '@types/uuid':
+        specifier: ^10.0.0
+        version: 10.0.0
       antd:
         specifier: ^5.26.0
         version: 5.26.0(moment@2.30.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -110,6 +113,9 @@ importers:
       typeorm:
         specifier: ^0.3.24
         version: 0.3.24(babel-plugin-macros@3.1.0)(ioredis@5.6.1)(mysql2@3.14.1)(reflect-metadata@0.2.2)
+      uuid:
+        specifier: ^11.1.0
+        version: 11.1.0
     devDependencies:
       '@types/debug':
         specifier: ^4.1.12
@@ -1300,6 +1306,9 @@ packages:
   '@types/three@0.177.0':
     resolution: {integrity: sha512-/ZAkn4OLUijKQySNci47lFO+4JLE1TihEjsGWPUT+4jWqxtwOPPEwJV1C3k5MEx0mcBPCdkFjzRzDOnHEI1R+A==}
 
+  '@types/uuid@10.0.0':
+    resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
+
   '@types/webxr@0.5.22':
     resolution: {integrity: sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==}
 
@@ -3874,6 +3883,8 @@ snapshots:
       fflate: 0.8.2
       meshoptimizer: 0.18.1
 
+  '@types/uuid@10.0.0': {}
+
   '@types/webxr@0.5.22': {}
 
   '@vitejs/plugin-react@4.5.2(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(yaml@2.8.0))':

+ 79 - 0
src/server/modules/files/file.service.ts

@@ -1,9 +1,88 @@
 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<File> {
+  private readonly minioService: MinioService;
+
   constructor(dataSource: DataSource) {
     super(dataSource, File);
+    this.minioService = new MinioService();
+  }
+
+  /**
+   * 创建文件记录并生成预签名上传URL
+   */
+  async createFile(data: Partial<File>) {
+    try {
+      // 生成唯一文件ID和存储路径
+      const fileId = `FILE${Date.now()}${Math.floor(Math.random() * 1000)}`;
+      const fileKey = `${data.uploadUserId}/${uuidv4()}-${data.name}`;
+      
+      // 生成MinIO上传策略
+      const uploadPolicy = await this.minioService.generateUploadPolicy(fileKey);
+      
+      // 准备文件记录数据
+      const fileData = {
+        ...data,
+        id: fileId,
+        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: string) {
+    try {
+      // 获取文件记录
+      const file = await this.getById(id);
+      if (!file) {
+        throw new Error('文件不存在');
+      }
+      
+      // 从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: string) {
+    const file = await this.getById(id);
+    if (!file) {
+      throw new Error('文件不存在');
+    }
+    
+    return this.minioService.getFileUrl(this.minioService.bucketName, file.path);
   }
 }

+ 11 - 0
src/server/modules/files/minio.service.ts

@@ -157,4 +157,15 @@ export class MinioService {
       throw error;
     }
   }
+
+  // 删除文件
+  async deleteObject(bucketName: string, objectName: string) {
+    try {
+      await this.client.removeObject(bucketName, objectName);
+      logger.db(`Deleted object: ${bucketName}/${objectName}`);
+    } catch (error) {
+      logger.error(`Failed to delete object ${bucketName}/${objectName}:`, error);
+      throw error;
+    }
+  }
 }