Просмотр исходного кода

✨ feat(files): implement file management API endpoints

- add delete file endpoint to remove files by ID
- add get file URL endpoint to retrieve file access links
- add upload policy endpoint to generate file upload credentials
- refactor file routes structure to use modular route handlers
- implement authentication middleware for all file operations
- add OpenAPI documentation for all file endpoints
- handle error cases with appropriate HTTP status codes
yourname 8 месяцев назад
Родитель
Сommit
37a737077c

+ 61 - 0
src/server/api/files/[id]/delete.ts

@@ -0,0 +1,61 @@
+import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
+import { FileService } from '@/server/modules/files/file.service';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+// 删除文件路由
+const deleteFileRoute = createRoute({
+  method: 'delete',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.string().openapi({
+        param: { name: 'id', in: 'path' },
+        example: 'FILE123456',
+        description: '文件ID'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '文件删除成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            success: z.boolean().openapi({ example: true }),
+            message: z.string().openapi({ example: '文件删除成功' })
+          })
+        }
+      }
+    },
+    404: {
+      description: '文件不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 创建文件服务实例
+const fileService = new FileService(AppDataSource);
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(deleteFileRoute, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    await fileService.deleteFile(id);
+    return c.json({ success: true, message: '文件删除成功' }, 200);
+  } catch (error) {
+    const message = error instanceof Error ? error.message : '文件删除失败';
+    const code = (error instanceof Error && error.message === '文件不存在') ? 404 : 500;
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 63 - 0
src/server/api/files/[id]/get-url.ts

@@ -0,0 +1,63 @@
+import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
+import { FileService } from '@/server/modules/files/file.service';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+// 获取文件URL路由
+const getFileUrlRoute = createRoute({
+  method: 'get',
+  path: '/url',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.string().openapi({
+        param: { name: 'id', in: 'path' },
+        example: 'FILE123456',
+        description: '文件ID'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '获取文件URL成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            url: z.string().url().openapi({
+              description: '文件访问URL',
+              example: 'https://minio.example.com/bucket/file-key'
+            })
+          })
+        }
+      }
+    },
+    404: {
+      description: '文件不存在',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 创建文件服务实例
+const fileService = new FileService(AppDataSource);
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(getFileUrlRoute, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    const url = await fileService.getFileUrl(id);
+    return c.json({ url }, 200);
+  } catch (error) {
+    const message = error instanceof Error ? error.message : '获取文件URL失败';
+    const code = (error instanceof Error && error.message === '文件不存在') ? 404 : 500;
+    return c.json({ code, message }, code);
+  }
+});
+
+export default app;

+ 11 - 14
src/server/api/files/index.ts

@@ -1,16 +1,13 @@
-import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
-import { File } from '@/server/modules/files/file.entity';
-import { FileSchema, CreateFileDto, UpdateFileDto } from '@/server/modules/files/file.entity';
-import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { OpenAPIHono } from '@hono/zod-openapi';
+import uploadPolicyRoute from './upload-policy/post';
+import getUrlRoute from './[id]/get-url';
+import deleteRoute from './[id]/delete';
+import { AuthContext } from '@/server/types/context';
 
-const fileRoutes = createCrudRoutes({
-  entity: File,
-  createSchema: CreateFileDto,
-  updateSchema: UpdateFileDto,
-  getSchema: FileSchema,
-  listSchema: FileSchema,
-  searchFields: ['name', 'type', 'description'],
-  middleware: [authMiddleware]
-});
+// 创建路由实例并聚合所有子路由
+const app = new OpenAPIHono<AuthContext>()
+  .route('/upload-policy', uploadPolicyRoute)
+  .route('/{id}/url', getUrlRoute)
+  .route('/{id}', deleteRoute);
 
-export default fileRoutes;
+export default app;

+ 84 - 0
src/server/api/files/upload-policy/post.ts

@@ -0,0 +1,84 @@
+import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi';
+import { FileService } from '@/server/modules/files/file.service';
+import { FileSchema, CreateFileDto, File } from '@/server/modules/files/file.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { AuthContext } from '@/server/types/context';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+// 创建文件上传策略路由
+const createUploadPolicyRoute = createRoute({
+  method: 'post',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: CreateFileDto }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '生成文件上传策略成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            file: FileSchema,
+            uploadPolicy: z.object({
+              'x-amz-algorithm': z.string(),
+              'x-amz-credential': z.string(),
+              'x-amz-date': z.string(),
+              'x-amz-security-token': z.string().optional(),
+              policy: z.string(),
+              'x-amz-signature': z.string(),
+              host: z.string(),
+              key: z.string(),
+              bucket: z.string()
+            })
+          })
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
+    500: {
+      description: '服务器错误',
+      content: { 'application/json': { schema: ErrorSchema } }
+    }
+  }
+});
+
+// 创建文件服务实例
+const fileService = new FileService(AppDataSource);
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(createUploadPolicyRoute, async (c) => {
+  try {
+    const data = await c.req.json();
+    const result = await fileService.createFile(data);
+    // 手动转换日期类型并处理可选字段
+    const formattedFile = {
+      ...result.file,
+      description: result.file.description ?? null,
+      type: result.file.type ?? null,
+      size: result.file.size ?? null,
+      lastUpdated: result.file.lastUpdated ? result.file.lastUpdated.toISOString() : null,
+      createdAt: result.file.createdAt.toISOString(),
+      uploadTime: result.file.uploadTime.toISOString()
+    };
+    
+    const typedResult = {
+      file: formattedFile,
+      uploadPolicy: result.uploadPolicy
+    };
+    return c.json(typedResult, 200);
+  } catch (error) {
+    const message = error instanceof Error ? error.message : '生成上传策略失败';
+    return c.json({ code: 500, message }, 500);
+  }
+});
+
+export default app;