diff --git a/cmd/bookmark/bookmark.go b/cmd/bookmark/bookmark.go new file mode 100644 index 0000000..1c93add --- /dev/null +++ b/cmd/bookmark/bookmark.go @@ -0,0 +1,40 @@ +package main + +import ( + "log" + + bookmarks "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func main() { + gin.SetMode(gin.ReleaseMode) + router := gin.Default() + + // 允许所有源的示例配置 + config := cors.Config{ + AllowAllOrigins: true, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"}, + } + router.Use(cors.New(config)) + + api_router := router.Group("/api") + { + // create a type that satisfies the `api.ServerInterface`, + // which contains an implementation of every operation from the generated code + if server, err := handlers.NewBookMarks("./data/bookmark.sqlite3"); err != nil { + log.Fatal("Failed to create bookmarks server:", err) + } else if permission, err := handlers.NewBookMarkPermission(); err != nil || permission == nil { + log.Fatal("Failed to create bookmarks permission:", err) + } else { + bookmarks.RegisterHandlersWithOptions(api_router, server, *permission) + } + } + + var listener = "localhost:8081" + log.Printf("Starting server at http://%s", listener) + log.Fatal(router.Run(listener)) +} diff --git a/cmd/user_np/user_np.go b/cmd/user_np/user_np.go new file mode 100644 index 0000000..8b42e7b --- /dev/null +++ b/cmd/user_np/user_np.go @@ -0,0 +1,29 @@ +package main + +import ( + "log" + + user_np "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/user_np" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers" + "github.com/gin-gonic/gin" +) + +func main() { + // gin.SetMode(gin.ReleaseMode) + router := gin.Default() + + api_router := router.Group("/api") + { + // create a type that satisfies the `api.ServerInterface`, + // which contains an implementation of every operation from the generated code + if server, err := handlers.NewUserNP("./data/user_np.sqlite3"); err != nil { + log.Fatal("Failed to create user_np server:", err) + } else { + user_np.RegisterHandlers(api_router, server) + } + } + + var listener = "localhost:8082" + log.Printf("Starting server at http://%s", listener) + log.Fatal(router.Run(listener)) +} diff --git a/config/bookmark.yaml b/config/bookmark.yaml deleted file mode 100644 index e53af61..0000000 --- a/config/bookmark.yaml +++ /dev/null @@ -1,775 +0,0 @@ -openapi: '3.0.3' -info: - title: zzyxyz_api - description: API服务 - version: '1.0' -servers: - - url: http://localhost:8080/api - description: 开发环境 - - url: https://api.zzyxyz.com/api - description: 生产环境 -tags: - - name: folder - description: 文件夹相关操作 - - name: data - description: 书签相关操作 -security: - - ApiKeyAuth: [] - -paths: - /bookmarks/v1/folder/serial: - get: - summary: 导出文件夹树结构 - description: 递归导出整个文件夹树结构并压缩返回 - operationId: exportFolderTree - tags: [folder] - responses: - '200': - description: 压缩的文件夹树数据 - content: - application/octet-stream: - schema: - type: string - format: binary - '500': - $ref: '#/components/responses/ServerInternalError' - - post: - summary: 导入文件夹树结构 - description: 上传并解压文件夹树结构数据,重建文件系统 - operationId: importFolderTree - tags: [folder] - requestBody: - required: true - content: - application/octet-stream: - schema: - type: string - format: binary - responses: - '200': - description: 导入成功 - content: - application/json: - schema: - $ref: '#/components/schemas/ImportResponse' - '400': - description: 数据格式错误 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - $ref: '#/components/responses/ServerInternalError' - - /bookmarks/v1/folder: - post: - summary: 创建文件夹 - description: 创建一个存储书签或者文件夹的文件夹 - operationId: createFolder - tags: [folder] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/FolderRequest' - responses: - '201': - description: 创建成功的文件夹 - content: - application/json: - schema: - $ref: '#/components/schemas/FolderResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - $ref: '#/components/responses/ServerInternalError' - - /bookmarks/v1/folder/{id}: - get: - summary: 获取文件夹基础信息 - description: 获取文件夹基础信息不包含内容,只有元数据 - operationId: getFolderInfo - tags: [folder] - parameters: - - name: id - in: path - required: true - example: 1 - schema: - type: integer - format: int64 - responses: - '200': - description: 文件夹详情 - content: - application/json: - schema: - $ref: '#/components/schemas/FolderResponse' - '404': - description: 文件夹不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - put: - summary: 更新文件夹 - description: 修改文件夹的元数据(包括修改名称和移动文件夹) - operationId: updateFolder - tags: [folder] - parameters: - - name: id - in: path - example: 1 - required: true - schema: - type: integer - format: int64 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/FolderRequest' - responses: - '200': - description: 更新后的文件夹 - content: - application/json: - schema: - $ref: '#/components/schemas/FolderResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '404': - description: 文件夹不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - $ref: '#/components/responses/ServerInternalError' - - delete: - summary: 删除文件夹 - description: 删除文件夹(文件夹不能有内容) - operationId: deleteFolder - tags: [folder] - parameters: - - name: id - in: path - example: 1 - required: true - schema: - type: integer - format: int64 - responses: - '204': - description: 删除成功 - '400': - description: 文件夹不为空,无法删除 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '404': - description: 文件夹不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - $ref: '#/components/responses/ServerInternalError' - - /bookmarks/v1/folder/{id}/mount: - get: - summary: 获取文件夹挂载信息 - description: 获取指定文件夹的所有挂载信息 - operationId: getFolderMounts - tags: [folder] - parameters: - - name: id - in: path - required: true - schema: - type: integer - format: int64 - description: 文件夹ID - responses: - '200': - description: 挂载信息列表 - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/MountInfo' - '404': - description: 文件夹不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - $ref: '#/components/responses/ServerInternalError' - - post: - summary: 挂载文件夹 - description: 将指定文件夹挂载到目标位置 - operationId: mountFolder - tags: [folder] - parameters: - - name: id - in: path - required: true - schema: - type: integer - format: int64 - description: 要挂载的文件夹ID - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - target_folder_id: - type: integer - format: int64 - description: 目标文件夹ID(挂载点) - mount_name: - type: string - description: 挂载后的显示名称(可选,默认使用原文件夹名) - required: - - target_folder_id - responses: - '200': - description: 挂载成功 - content: - application/json: - schema: - $ref: '#/components/schemas/MountResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '404': - description: 文件夹不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '409': - description: 挂载冲突(如循环挂载、重复挂载等) - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - $ref: '#/components/responses/ServerInternalError' - - delete: - summary: 取消挂载文件夹 - description: 取消指定文件夹的挂载关系 - operationId: unmountFolder - tags: [folder] - parameters: - - name: id - in: path - required: true - schema: - type: integer - format: int64 - description: 要取消挂载的文件夹ID - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - mount_point_id: - type: integer - format: int64 - description: 挂载点ID(从哪个位置取消挂载) - required: - - mount_point_id - responses: - '200': - description: 取消挂载成功 - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '404': - description: 挂载关系不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - $ref: '#/components/responses/ServerInternalError' - - /bookmarks/v1/folder/{id}/content: - get: - summary: 获取文件夹的内容 - description: 只获取当前文件夹的内容,不会递归搜索 - operationId: getFolderContent - tags: [folder] - parameters: - - name: id - in: path - example: 1 - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: 文件夹子节点列表 - content: - application/json: - schema: - $ref: '#/components/schemas/FolderContentResponse' - '404': - description: 文件夹不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - delete: - summary: 删除文件夹的内容 - description: 删除文件夹的内容(危险操作) - operationId: deleteFolderContent - tags: [folder] - parameters: - - name: id - in: path - required: true - schema: - type: integer - format: int64 - - name: mode - in: query - description: 删除模式 - required: false - schema: - type: string - enum: [onlyContent, onlyEmptyFolder, onlyFolder, all] - responses: - '200': - description: 删除成功 - '500': - $ref: '#/components/responses/ServerInternalError' - - /bookmarks/v1/data: - post: - summary: 创建书签 - description: 在文件夹下创建一个书签 - operationId: createBookmark - tags: [data] - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BookmarkRequest' - responses: - '201': - description: 创建成功的书签 - content: - application/json: - schema: - $ref: '#/components/schemas/BookmarkResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - $ref: '#/components/responses/ServerInternalError' - - /bookmarks/v1/data/{id}: - get: - summary: 获取书签详情 - description: 通过id获取书签内容 - operationId: getBookmark - tags: [data] - parameters: - - name: id - in: path - example: 1 - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: 书签详情 - content: - application/json: - schema: - $ref: '#/components/schemas/BookmarkResponse' - '404': - description: 书签不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - put: - summary: 更新书签 - description: 更新指定id的书签 - operationId: updateBookmark - tags: [data] - parameters: - - name: id - in: path - example: 1 - required: true - schema: - type: integer - format: int64 - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BookmarkRequest' - responses: - '200': - description: 更新后的书签 - content: - application/json: - schema: - $ref: '#/components/schemas/BookmarkResponse' - '400': - description: 请求参数错误 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '404': - description: 书签不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - $ref: '#/components/responses/ServerInternalError' - - delete: - summary: 删除书签 - description: 删除指定id的书签 - operationId: deleteBookmark - tags: [data] - parameters: - - name: id - in: path - example: 1 - required: true - schema: - type: integer - format: int64 - responses: - '204': - description: 删除成功 - '404': - description: 书签不存在 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - $ref: '#/components/responses/ServerInternalError' - -components: - securitySchemes: - ApiKeyAuth: - type: apiKey - in: header - name: X-BookMark-Token - responses: - ServerInternalError: - description: 服务器内部错误 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - Unauthorized: - description: 未授权 - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - schemas: - ImportResponse: - type: object - properties: - imported_folders: - type: integer - description: 导入的文件夹数量 - imported_bookmarks: - type: integer - description: 导入的书签数量 - duration: - type: string - description: 导入耗时 - required: - - imported_folders - - imported_bookmarks - - duration - - MountResponse: - type: object - properties: - id: - type: integer - format: int64 - description: 挂载关系ID - source_folder_id: - type: integer - format: int64 - description: 源文件夹ID - target_folder_id: - type: integer - format: int64 - description: 目标文件夹ID(挂载点) - mount_name: - type: string - description: 挂载显示名称 - created_at: - type: string - format: date-time - description: 挂载时间 - required: - - id - - source_folder_id - - target_folder_id - - mount_name - - created_at - - MountInfo: - type: object - properties: - id: - type: integer - format: int64 - description: 挂载关系ID - source_folder: - $ref: '#/components/schemas/FolderBriefResponse' - target_folder: - $ref: '#/components/schemas/FolderBriefResponse' - mount_name: - type: string - description: 挂载显示名称 - is_mounted_here: - type: boolean - description: true表示此文件夹被挂载到target_folder,false表示此文件夹挂载了source_folder - created_at: - type: string - format: date-time - description: 挂载时间 - required: - - id - - source_folder - - target_folder - - mount_name - - is_mounted_here - - created_at - - FolderRequest: - description: 文件夹请求结构体 - type: object - properties: - name: - type: string - minLength: 1 - maxLength: 255 - description: 文件夹名称 - example: 测试名称 - parent_id: - type: integer - format: int64 - description: 父文件夹ID 若为空则自动创建在用户根目录下 - example: null - required: - - name - - BookmarkRequest: - type: object - properties: - name: - type: string - minLength: 1 - maxLength: 255 - description: 书签名称 - example: 测试名称 - link: - type: string - description: 书签链接 - example: /swagger/index.html - detail: - type: string - description: 书签详情链接 - description: - type: string - description: 书签描述 - parent_id: - type: integer - format: int64 - description: 父文件夹ID 若为空则自动创建在用户根目录下 - example: null - required: - - name - - FolderResponse: - type: object - description: 文件夹响应结构体 - properties: - id: - type: integer - format: int64 - description: 文件夹ID - name: - type: string - description: 文件夹名称 - parent_id: - type: integer - format: int64 - description: 父文件夹ID - created_at: - type: string - format: date-time - description: 创建时间 - updated_at: - type: string - format: date-time - description: 更新时间 - sub_folder_count: - type: integer - description: 子文件夹数量 - bookmark_count: - type: integer - description: 书签数量 - required: - - id - - name - - created_at - - updated_at - - sub_folder_count - - bookmark_count - - BookmarkResponse: - type: object - description: 书签相应结构体 - properties: - id: - type: integer - format: int64 - description: 书签ID - name: - type: string - description: 书签名称 - link: - type: string - description: 书签链接 - detail: - type: string - description: 书签详情链接 - description: - type: string - description: 书签描述 - parent_id: - type: integer - format: int64 - description: 父文件夹ID - created_at: - type: string - format: date-time - description: 创建时间 - updated_at: - type: string - format: date-time - description: 更新时间 - required: - - id - - name - - parent_id - - created_at - - updated_at - - FolderContentResponse: - type: object - properties: - sub_folders: - type: array - items: - $ref: '#/components/schemas/FolderBriefResponse' - description: 子文件夹列表 - bookmarks: - type: array - items: - $ref: '#/components/schemas/BookmarkBriefResponse' - description: 书签列表 - required: - - sub_folders - - bookmarks - - FolderBriefResponse: - type: object - properties: - id: - type: integer - format: int64 - description: 文件夹ID - name: - type: string - description: 文件夹名称 - required: - - id - - name - - BookmarkBriefResponse: - type: object - properties: - id: - type: integer - format: int64 - description: 书签ID - name: - type: string - description: 书签名称 - required: - - id - - name - - Error: - type: object - description: 错误信息 - properties: - errtype: - type: string - example: "ParameterError" - description: 错误类型 - message: - example: "传递的第一个参数错误" - type: string - description: 错误信息 - required: - - errtype - - message diff --git a/config/bookmark/bookmark.yaml b/config/bookmark/bookmark.yaml new file mode 100644 index 0000000..ed7b9c0 --- /dev/null +++ b/config/bookmark/bookmark.yaml @@ -0,0 +1,213 @@ +openapi: '3.0.3' +info: + title: zzyxyz_api + description: API服务 + version: '1.0' +servers: + - url: http://localhost:8081/api + description: 开发环境 + - url: https://api.zzyxyz.com/api + description: 生产环境 +tags: + - name: data + description: 书签相关操作 +security: + - ApiKeyAuth: [] + +paths: + /bookmarks/v1/data/{id}: + parameters: + - name: id + in: path + example: 1 + required: true + schema: + type: integer + format: int64 + post: + summary: 创建书签 + description: 在文件夹下创建一个书签 + operationId: createBookmark + tags: [data] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BookmarkRequest' + responses: + '201': + description: 创建成功的书签 + content: + application/json: + schema: + $ref: '#/components/schemas/BookmarkResponse' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/ServerInternalError' + + get: + summary: 获取书签详情 + description: 通过id获取书签内容 + operationId: getBookmark + tags: [data] + responses: + '200': + description: 书签详情 + content: + application/json: + schema: + $ref: '#/components/schemas/BookmarkResponse' + '404': + description: 书签不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + put: + summary: 更新书签 + description: 更新指定id的书签 + operationId: updateBookmark + tags: [data] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BookmarkRequest' + responses: + '200': + description: 更新后的书签 + content: + application/json: + schema: + $ref: '#/components/schemas/BookmarkResponse' + '400': + description: 请求参数错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: 书签不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/ServerInternalError' + + delete: + summary: 删除书签 + description: 删除指定id的书签 + operationId: deleteBookmark + tags: [data] + responses: + '204': + description: 删除成功 + '404': + description: 书签不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/ServerInternalError' + +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-BookMark-Token + responses: + ServerInternalError: + description: 服务器内部错误 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + Unauthorized: + description: 未授权 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + schemas: + BookmarkRequest: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 255 + description: 书签名称 + example: 测试名称 + link: + type: string + description: 书签链接 + example: /swagger/index.html + detail: + type: string + description: 书签详情链接 + description: + type: string + description: 书签描述 + required: + - name + + BookmarkResponse: + type: object + description: 书签相应结构体 + properties: + id: + type: integer + format: int64 + description: 书签ID + name: + type: string + description: 书签名称 + link: + type: string + description: 书签链接 + detail: + type: string + description: 书签详情链接 + description: + type: string + description: 书签描述 + created_at: + type: string + format: date-time + description: 创建时间 + updated_at: + type: string + format: date-time + description: 更新时间 + required: + - id + - name + - created_at + - updated_at + + Error: + type: object + description: 错误信息 + properties: + errtype: + type: string + example: "ParameterError" + description: 错误类型 + message: + example: "传递的第一个参数错误" + type: string + description: 错误信息 + required: + - errtype + - message diff --git a/config/bookmark/client.yaml b/config/bookmark/client.yaml new file mode 100644 index 0000000..7cb09ed --- /dev/null +++ b/config/bookmark/client.yaml @@ -0,0 +1,5 @@ +# yaml-language-server: ... +package: api +generate: + client: true +output: ./gen/bookmarks/client.go diff --git a/config/vfs_cfg.yaml b/config/bookmark/server.yaml similarity index 71% rename from config/vfs_cfg.yaml rename to config/bookmark/server.yaml index f22a237..c63069e 100644 --- a/config/vfs_cfg.yaml +++ b/config/bookmark/server.yaml @@ -3,4 +3,4 @@ package: api generate: gin-server: true models: true -output: ./gen/vfs/gen.go +output: ./gen/bookmarks/server.go diff --git a/config/user.yaml b/config/user.yaml deleted file mode 100644 index e69de29..0000000 diff --git a/config/user_np/server.yaml b/config/user_np/server.yaml new file mode 100644 index 0000000..5ebe25b --- /dev/null +++ b/config/user_np/server.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: ... +package: api +generate: + gin-server: true + models: true + # strict-server: true +output: ./gen/user_np/server.go diff --git a/config/user_np/user_np.yaml b/config/user_np/user_np.yaml new file mode 100644 index 0000000..75a303b --- /dev/null +++ b/config/user_np/user_np.yaml @@ -0,0 +1,127 @@ +openapi: '3.0.3' +info: + title: zzyxyz_user_np_api + description: 用户节点权限相关操作(user_name and password) + version: '1.0' +servers: + - url: http://localhost:8082/api + description: 开发环境 + - url: https://api.zzyxyz.com/api + description: 生产环境 +security: + - ApiKeyAuth: [] + +paths: + /auth/login: + post: + summary: 用户登录 + description: 使用用户名和密码进行登录 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + '200': + description: 登录成功 + content: + application/json: + schema: + $ref: '#/components/schemas/LoginResponse' + '400': + description: 请求参数错误 + '401': + description: 认证失败 + + /auth/register: + post: + summary: 用户注册 + description: 创建新用户账户 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RegisterRequest' + responses: + '201': + description: 注册成功 + '400': + description: 请求参数错误 + '409': + description: 用户名已存在 + + /auth/password: + put: + summary: 修改密码 + description: 修改已登录用户的密码 + security: + - ApiKeyAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ChangePasswordRequest' + responses: + '200': + description: 密码修改成功 + '400': + description: 请求参数错误 + '401': + description: 认证失败 + +components: + schemas: + LoginRequest: + type: object + required: + - username + - password + properties: + username: + type: string + password: + type: string + + LoginResponse: + type: object + properties: + token: + type: string + expires_in: + type: integer + user_id: + type: integer + format: int64 + + RegisterRequest: + type: object + required: + - username + - password + properties: + username: + type: string + password: + type: string + email: + type: string + + ChangePasswordRequest: + type: object + required: + - old_password + - new_password + properties: + old_password: + type: string + new_password: + type: string + + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: Authorization diff --git a/config/vfs/client.yaml b/config/vfs/client.yaml new file mode 100644 index 0000000..9924e99 --- /dev/null +++ b/config/vfs/client.yaml @@ -0,0 +1,5 @@ +# yaml-language-server: ... +package: api +generate: + client: true +output: ./gen/vfs/client.go diff --git a/config/bookmark_cfg.yaml b/config/vfs/server.yaml similarity index 62% rename from config/bookmark_cfg.yaml rename to config/vfs/server.yaml index 033f57b..941c886 100644 --- a/config/bookmark_cfg.yaml +++ b/config/vfs/server.yaml @@ -3,4 +3,5 @@ package: api generate: gin-server: true models: true -output: ./gen/bookmarks/gen.go + # strict-server: true +output: ./gen/vfs/server.go diff --git a/config/vfs.yaml b/config/vfs/vfs.yaml similarity index 71% rename from config/vfs.yaml rename to config/vfs/vfs.yaml index 91c1321..e7c68eb 100644 --- a/config/vfs.yaml +++ b/config/vfs/vfs.yaml @@ -4,7 +4,7 @@ info: description: 虚拟文件系统API服务 version: '1.0' servers: - - url: http://localhost:8080/api + - url: http://localhost:8083/api description: 开发环境 - url: https://api.zzyxyz.com/api description: 生产环境 @@ -15,10 +15,48 @@ security: - ApiKeyAuth: [] paths: - /vfs/v1/files/{path}: + /vfs/v1/users: + post: + summary: 创建用户 + description: 创建一个用户 + operationId: createUser + tags: [vfs] + requestBody: + required: true + content: + application/json: + schema: + type: string + description: 用户名 + responses: + '201': + description: 创建成功 + content: + application/json: + schema: + type: string + description: X-VFS-TOKEN + delete: + summary: 删除用户 + description: 删除一个用户 + operationId: deleteUser + tags: [vfs] + responses: + '204': + description: 删除成功 + '404': + description: 用户不存在 + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + $ref: '#/components/responses/ServerInternalError' + + /vfs/v1/files: parameters: - name: path - in: path + in: query required: true description: 文件系统路径,例如 "documents/readme.txt" 或 "services/mysql.service" 或 "folder/" schema: @@ -77,6 +115,10 @@ paths: responses: '201': description: 创建成功 + content: + application/json: + schema: + $ref: '#/components/schemas/VFSNodeResponse' '400': description: 请求参数错误 content: @@ -86,7 +128,7 @@ paths: '500': $ref: '#/components/responses/ServerInternalError' - put: + patch: summary: 修改文件或修改目录 description: 修改/移动/重命名 已存在的 文件/目录 operationId: updateVFSNode @@ -101,19 +143,13 @@ paths: new_name: type: string description: 新名称(用于重命名) - new_path: - type: string - description: 新路径(用于移动) - content: - type: string - description: 新内容(用于修改文件内容) - anyOf: - - required: [new_name] - - required: [new_path] - - required: [content] responses: '200': description: 操作成功 + content: + application/json: + schema: + $ref: '#/components/schemas/VFSNodeResponse' '400': description: 请求参数错误 content: @@ -160,6 +196,32 @@ components: schema: $ref: '#/components/schemas/Error' schemas: + VFSNodeType: + type: string + description: 节点类型 + enum: [file, directory, service] + + VFSNodeResponse: + type: object + properties: + name: + type: string + description: 节点名称 + type: + $ref: '#/components/schemas/VFSNodeType' + created_at: + type: string + format: date-time + description: 创建时间 + updated_at: + type: string + format: date-time + description: 更新时间 + required: + - name + - type + - created_at + - updated_at VFSDirectoryEntry: type: object properties: @@ -167,13 +229,7 @@ components: type: string description: 条目名称 type: - type: string - enum: [file, directory, service] - description: 条目类型 - modified: - type: string - format: date-time - description: 修改时间 + $ref: '#/components/schemas/VFSNodeType' permissions: type: string description: 权限信息,如 "rw" diff --git a/go.mod b/go.mod index 8cec322..352ec67 100644 --- a/go.mod +++ b/go.mod @@ -4,35 +4,31 @@ go 1.25.1 require ( github.com/gin-gonic/gin v1.10.1 - github.com/magefile/mage v1.15.0 github.com/mattn/go-sqlite3 v1.14.32 github.com/oapi-codegen/runtime v1.1.2 - github.com/stretchr/testify v1.11.1 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.30.5 ) +require github.com/gin-contrib/cors v1.7.6 + require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.14.1 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect - github.com/casbin/casbin/v2 v2.127.0 // indirect - github.com/casbin/govaluate v1.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/getkin/kin-openapi v0.132.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.22.0 // indirect github.com/go-openapi/swag/jsonname v0.24.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.5.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -40,7 +36,6 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -51,19 +46,13 @@ require ( github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/arch v0.21.0 // indirect - golang.org/x/crypto v0.42.0 // indirect + golang.org/x/crypto v0.42.0 golang.org/x/mod v0.28.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.17.0 // indirect diff --git a/go.sum b/go.sum index 8d60da7..e2bb4c4 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,13 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= -github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= -github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/casbin/casbin/v2 v2.127.0 h1:UGK3uO/8cOslnNqFUJ4xzm/bh+N+o45U7cSolaFk38c= -github.com/casbin/casbin/v2 v2.127.0/go.mod h1:n4uZK8+tCMvcD6EVQZI90zKAok8iHAvEypcMJVKhGF0= -github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= -github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -33,12 +26,12 @@ github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIp github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM= github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU= github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k= @@ -56,7 +49,8 @@ github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -70,9 +64,6 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -101,10 +92,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= -github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -147,16 +134,10 @@ github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= -github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= -github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= -github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= @@ -165,21 +146,14 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKk github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= @@ -187,8 +161,6 @@ github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2W github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw= golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -200,7 +172,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -218,22 +189,17 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -245,7 +211,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= diff --git a/internal/handlers/bookmark.go b/internal/handlers/bookmark.go index 9c5011c..62b92b9 100644 --- a/internal/handlers/bookmark.go +++ b/internal/handlers/bookmark.go @@ -3,16 +3,10 @@ package handlers import ( - "crypto/rand" - "encoding/hex" - "fmt" - "log" "net/http" - "net/url" api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks" "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models" - "github.com/casbin/casbin/v2" "github.com/gin-gonic/gin" _ "github.com/mattn/go-sqlite3" "gorm.io/driver/sqlite" @@ -23,27 +17,9 @@ type BookMarksImpl struct { db *gorm.DB } -// GetFolderMounts implements api.ServerInterface. -func (b *BookMarksImpl) GetFolderMounts(c *gin.Context, id int64) { - panic("unimplemented") -} - -// MountFolder implements api.ServerInterface. -func (b *BookMarksImpl) MountFolder(c *gin.Context, id int64) { - panic("unimplemented") -} - -// UnmountFolder implements api.ServerInterface. -func (b *BookMarksImpl) UnmountFolder(c *gin.Context, id int64) { - panic("unimplemented") -} - -const forlder_root_id = 1 - -var enforcer *casbin.Enforcer var adminToken *string -func validateApiKey(url *url.URL, apiKey string) bool { +func validateApiKey(apiKey string) bool { if adminToken != nil && apiKey == *adminToken { return true } @@ -58,8 +34,9 @@ func AuthMiddleware() api.MiddlewareFunc { // 提取 API Key apiKey := c.GetHeader("X-BookMark-Token") + return // 验证 API Key(您需要实现这个逻辑) - if apiKey == "" || !validateApiKey(c.Request.URL, apiKey) { + if apiKey == "" || !validateApiKey(apiKey) { c.JSON(http.StatusUnauthorized, api.Error{ Errtype: "Unauthorized", Message: "Invalid or missing API key", @@ -73,56 +50,17 @@ func AuthMiddleware() api.MiddlewareFunc { } func NewBookMarkPermission() (*api.GinServerOptions, error) { - // 一行代码生成安全的随机token - token := make([]byte, 16) - rand.Read(token) // 忽略错误处理以简化代码(生产环境建议处理) - tokenStr := hex.EncodeToString(token) - adminToken = &tokenStr - log.Printf("Admin API Token (for Swagger testing): %s", *adminToken) - - if e, err := casbin.NewEnforcer("./config/model.conf", ".data/policy.csv"); err == nil { - log.Fatalf("Failed to create casbin enforcer: %v", err) - } else { - enforcer = e - } - return &api.GinServerOptions{ Middlewares: []api.MiddlewareFunc{AuthMiddleware()}, }, nil } -func (b *BookMarksImpl) GetFolderDefaultRoot(folderID *int64) (*models.Folder, error) { - var db *gorm.DB = b.db - var real_root_id int64 = forlder_root_id - - // 设置默认父文件夹ID为根目录 - parentID := real_root_id - if folderID != nil && *folderID != 0 { - parentID = *folderID - } - - // 检查文件夹是否存在(Find 不会在找不到记录时返回错误) - var parentFolder models.Folder - result := db.Limit(1).Find(&parentFolder, parentID) - if result.Error != nil { - return nil, result.Error - } - - // 检查是否找到了记录 - if result.RowsAffected == 0 { - return nil, nil - } - - return &parentFolder, nil -} - -func bookmarkReq2Model(req api.BookmarkRequest, parentID int64) models.Bookmark { +func bookmarkReq2Model(req api.BookmarkRequest) models.Bookmark { return models.Bookmark{ Name: req.Name, Link: req.Link, Detail: req.Detail, Description: req.Description, - ParentID: parentID, } } @@ -133,28 +71,10 @@ func bookmarkModel2Res(bookmark models.Bookmark) api.BookmarkResponse { Link: bookmark.Link, Detail: bookmark.Detail, Description: bookmark.Description, - ParentId: bookmark.ParentID, CreatedAt: bookmark.CreatedAt, } } -func folderReq2Model(req api.FolderRequest, parentID *int64) models.Folder { - return models.Folder{ - Name: req.Name, - ParentID: parentID, - } -} - -func folderModel2Res(folder models.Folder) api.FolderResponse { - return api.FolderResponse{ - Id: folder.ID, - Name: folder.Name, - ParentId: folder.ParentID, - CreatedAt: folder.CreatedAt, - UpdatedAt: folder.UpdatedAt, - } -} - func NewBookMarks(dbPath string) (*BookMarksImpl, error) { var err error var db *gorm.DB @@ -164,35 +84,27 @@ func NewBookMarks(dbPath string) (*BookMarksImpl, error) { } // 自动迁移表结构 - err = db.AutoMigrate(&models.Folder{}, &models.Bookmark{}) + err = db.AutoMigrate(&models.Bookmark{}) if err != nil { return nil, err } - - // 创建根文件夹(如果不存在) - var rootFolder models.Folder - result := db.Limit(1).Find(&rootFolder, forlder_root_id) - if result.Error != nil { - return nil, result.Error - } - - if result.RowsAffected == 0 { - // 根文件夹不存在,创建它 - rootFolder = models.Folder{ - ID: forlder_root_id, - Name: "Root", - ParentID: nil, // 根目录指向NULL - } - if err := db.Create(&rootFolder).Error; err != nil { - return nil, fmt.Errorf("failed to create root folder: %w", err) - } - } - return &BookMarksImpl{db: db}, nil } +func (b *BookMarksImpl) FindBMFromExternalID(externalID int64) (models.Bookmark, error) { + var db = b.db + var bookmark models.Bookmark + + // 使用ExternalID查询书签 + if err := db.Where("external_id = ?", externalID).First(&bookmark).Error; err != nil { + return bookmark, err + } + + return bookmark, nil +} + // CreateBookmark implements api.ServerInterface. -func (b *BookMarksImpl) CreateBookmark(c *gin.Context) { +func (b *BookMarksImpl) CreateBookmark(c *gin.Context, id int64) { var db = b.db var req api.BookmarkRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -203,19 +115,27 @@ func (b *BookMarksImpl) CreateBookmark(c *gin.Context) { return } - var parentID int64 - if folder, err := b.GetFolderDefaultRoot(req.ParentId); err != nil || folder == nil { - c.JSON(http.StatusNotFound, api.Error{ - Errtype: "NotFoundError", - Message: "Parent folder not found", + // 检查外部ID是否已经存在 + var existingBookmark models.Bookmark + result := db.Where("external_id = ?", id).First(&existingBookmark) + if result.Error == nil { + // ExternalID已存在,返回冲突错误 + c.JSON(http.StatusConflict, api.Error{ + Errtype: "ConflictError", + Message: "Bookmark with this External ID already exists", + }) + return + } else if result.Error != gorm.ErrRecordNotFound { + // 数据库查询出错 + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "DatabaseError", + Message: "Database query error", }) return - } else { - parentID = folder.ID } - bookmark := bookmarkReq2Model(req, parentID) - + bookmark := bookmarkReq2Model(req) + bookmark.ExternalID = id // 设置外部ID if err := db.Create(&bookmark).Error; err != nil { c.JSON(http.StatusInternalServerError, api.Error{ Errtype: "DatabaseError", @@ -228,61 +148,13 @@ func (b *BookMarksImpl) CreateBookmark(c *gin.Context) { c.JSON(http.StatusCreated, response) } -// CreateFolder implements api.ServerInterface. -func (b *BookMarksImpl) CreateFolder(c *gin.Context) { - var db = b.db - var req api.FolderRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, api.Error{ - Errtype: "ParameterError", - Message: "Invalid request parameters", - }) - return - } - - var parentID int64 - if folder, err := b.GetFolderDefaultRoot(req.ParentId); err != nil || folder == nil { - c.JSON(http.StatusNotFound, api.Error{ - Errtype: "NotFoundError", - Message: "Parent folder not found", - }) - return - } else { - parentID = folder.ID - } - - // 创建文件夹 - folder := folderReq2Model(req, &parentID) - - if err := db.Create(&folder).Error; err != nil { - c.JSON(http.StatusInternalServerError, api.Error{ - Errtype: "DatabaseError", - Message: "Failed to create folder", - }) - return - } - - response := folderModel2Res(folder) - c.JSON(http.StatusCreated, response) -} - // DeleteBookmark implements api.ServerInterface. func (b *BookMarksImpl) DeleteBookmark(c *gin.Context, id int64) { var db = b.db var bookmark models.Bookmark - // 查询书签是否存在 - if err := db.First(&bookmark, id).Error; err != nil { - // FIXME maybe use 204 means already deleted status is same as delete - c.JSON(http.StatusNotFound, api.Error{ - Errtype: "NotFoundError", - Message: "Bookmark not found", - }) - return - } - - // 删除书签 - if err := db.Delete(&bookmark).Error; err != nil { + // 使用ExternalID删除书签 + if err := db.Where("external_id = ?", id).Delete(&bookmark).Error; err != nil { c.JSON(http.StatusInternalServerError, api.Error{ Errtype: "DatabaseError", Message: "Failed to delete bookmark", @@ -293,72 +165,13 @@ func (b *BookMarksImpl) DeleteBookmark(c *gin.Context, id int64) { c.Status(http.StatusNoContent) } -// DeleteFolder implements api.ServerInterface. -func (b *BookMarksImpl) DeleteFolder(c *gin.Context, id int64) { - var db = b.db - - var folder models.Folder - - // 查询文件夹是否存在 - if err := db.First(&folder, id).Error; err != nil { - c.JSON(http.StatusNotFound, api.Error{ - Errtype: "NotFoundError", - Message: "Folder not found", - }) - return - } - - if folder.ParentID == nil { - c.JSON(http.StatusBadRequest, api.Error{ - Errtype: "ParameterError", - Message: "Cannot delete root folder", - }) - return - } - - // 检查文件夹是否为空 - var subFolderCount int64 - db.Model(&models.Folder{}).Where("parent_path_id = ?", id).Count(&subFolderCount) - - var bookmarkCount int64 - db.Model(&models.Bookmark{}).Where("parent_path_id = ?", id).Count(&bookmarkCount) - - // 如果文件夹不为空,拒绝删除 - if subFolderCount > 0 || bookmarkCount > 0 { - c.JSON(http.StatusBadRequest, api.Error{ - Errtype: "ParameterError", - Message: "Cannot delete non-empty folder, please delete contents first", - }) - return - } - - // 删除空文件夹 - if err := db.Delete(&folder).Error; err != nil { - c.JSON(http.StatusInternalServerError, api.Error{ - Errtype: "DatabaseError", - Message: "Failed to delete folder", - }) - return - } - - c.Status(http.StatusNoContent) -} - -// DeleteFolderContent implements api.ServerInterface. -func (b *BookMarksImpl) DeleteFolderContent(c *gin.Context, id int64, params api.DeleteFolderContentParams) { - c.JSON(http.StatusNotImplemented, api.Error{ - Errtype: "error", - Message: "Not implemented", - }) -} - // GetBookmark implements api.ServerInterface. func (b *BookMarksImpl) GetBookmark(c *gin.Context, id int64) { var db = b.db var bookmark models.Bookmark - // 查询书签 - if err := db.First(&bookmark, id).Error; err != nil { + // 使用ExternalID查询书签 + if err := db.Where("external_id = ?", id).First(&bookmark).Error; err != nil { c.JSON(http.StatusNotFound, api.Error{ Errtype: "NotFoundError", Message: "Bookmark not found", @@ -371,91 +184,6 @@ func (b *BookMarksImpl) GetBookmark(c *gin.Context, id int64) { c.JSON(http.StatusOK, response) } -// GetFolderContent implements api.ServerInterface. -func (b *BookMarksImpl) GetFolderContent(c *gin.Context, id int64) { - var db = b.db - - // 检查文件夹是否存在 - var folder models.Folder - if err := db.First(&folder, id).Error; err != nil { - c.JSON(http.StatusNotFound, api.Error{ - Errtype: "NotFoundError", - Message: "Folder not found", - }) - return - } - - // 查询子文件夹 - var subFolders []models.Folder - db.Where("parent_path_id = ?", id).Find(&subFolders) - - // 查询书签 - var bookmarks []models.Bookmark - db.Where("parent_path_id = ?", id).Find(&bookmarks) - - // 构造子文件夹响应列表 - var subFolderResponses []api.FolderBriefResponse - for _, f := range subFolders { - subFolderResponses = append(subFolderResponses, api.FolderBriefResponse{ - Id: f.ID, - Name: f.Name, - }) - } - - // 构造书签响应列表 - var bookmarkResponses []api.BookmarkBriefResponse - for _, bm := range bookmarks { - bookmarkResponses = append(bookmarkResponses, api.BookmarkBriefResponse{ - Id: bm.ID, - Name: bm.Name, - }) - } - - // 构造响应 - response := api.FolderContentResponse{ - SubFolders: subFolderResponses, - Bookmarks: bookmarkResponses, - } - - c.JSON(http.StatusOK, response) -} - -// GetFolderInfo implements api.ServerInterface. -func (b *BookMarksImpl) GetFolderInfo(c *gin.Context, id int64) { - var db = b.db - var folder models.Folder - - // 查询文件夹 - if err := db.First(&folder, id).Error; err != nil { - c.JSON(http.StatusNotFound, api.Error{ - Errtype: "NotFoundError", - Message: "Folder not found", - }) - return - } - - // 统计子文件夹数量 - var subFolderCount int64 - db.Model(&models.Folder{}).Where("parent_path_id = ?", id).Count(&subFolderCount) - - // 统计书签数量 - var bookmarkCount int64 - db.Model(&models.Bookmark{}).Where("parent_path_id = ?", id).Count(&bookmarkCount) - - // 构造响应 - response := api.FolderResponse{ - Id: folder.ID, - Name: folder.Name, - ParentId: folder.ParentID, - CreatedAt: folder.CreatedAt, - UpdatedAt: folder.UpdatedAt, - SubFolderCount: int(subFolderCount), - BookmarkCount: int(bookmarkCount), - } - - c.JSON(http.StatusOK, response) -} - // UpdateBookmark implements api.ServerInterface. func (b *BookMarksImpl) UpdateBookmark(c *gin.Context, id int64) { var db = b.db @@ -470,9 +198,9 @@ func (b *BookMarksImpl) UpdateBookmark(c *gin.Context, id int64) { return } - // 查找要更新的书签 + // 查找要更新的书签(使用ExternalID) var bookmark models.Bookmark - if err := db.First(&bookmark, id).Error; err != nil { + if err := db.Where("external_id = ?", id).First(&bookmark).Error; err != nil { c.JSON(http.StatusNotFound, api.Error{ Errtype: "NotFoundError", Message: "Bookmark not found", @@ -494,19 +222,6 @@ func (b *BookMarksImpl) UpdateBookmark(c *gin.Context, id int64) { bookmark.Description = req.Description } - // 更新父文件夹ID(如果提供且有效) - if req.ParentId != nil && *req.ParentId != 0 { - var parentFolder models.Folder - if err := db.First(&parentFolder, *req.ParentId).Error; err != nil { - c.JSON(http.StatusNotFound, api.Error{ - Errtype: "NotFoundError", - Message: "Parent folder not found", - }) - return - } - bookmark.ParentID = *req.ParentId - } - // 保存更新 if err := db.Save(&bookmark).Error; err != nil { c.JSON(http.StatusInternalServerError, api.Error{ @@ -521,88 +236,5 @@ func (b *BookMarksImpl) UpdateBookmark(c *gin.Context, id int64) { c.JSON(http.StatusOK, response) } -// UpdateFolder implements api.ServerInterface. -func (b *BookMarksImpl) UpdateFolder(c *gin.Context, id int64) { - var db = b.db - var req api.FolderRequest - - // 绑定请求参数 - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, api.Error{ - Errtype: "ParameterError", - Message: "Invalid request parameters", - }) - return - } - - // 查找要更新的文件夹 - var folder models.Folder - if err := db.First(&folder, id).Error; err != nil { - c.JSON(http.StatusNotFound, api.Error{ - Errtype: "NotFoundError", - Message: "Folder not found", - }) - return - } - - // 更新文件夹名称(如果提供) - if req.Name != "" { - folder.Name = req.Name - } - - // 更新父文件夹ID(如果提供且有效) - if req.ParentId != nil && *req.ParentId != 0 { - // 不能将文件夹设置为自己的子文件夹 - if *req.ParentId == id { - c.JSON(http.StatusBadRequest, api.Error{ - Errtype: "ParameterError", - Message: "Cannot set folder as its own parent", - }) - return - } - - var parentFolder models.Folder - if err := db.First(&parentFolder, *req.ParentId).Error; err != nil { - c.JSON(http.StatusNotFound, api.Error{ - Errtype: "NotFoundError", - Message: "Parent folder not found", - }) - return - } - folder.ParentID = req.ParentId - } - - // 保存更新 - if err := db.Save(&folder).Error; err != nil { - c.JSON(http.StatusInternalServerError, api.Error{ - Errtype: "DatabaseError", - Message: "Failed to update folder", - }) - return - } - - // 统计子文件夹数量 - var subFolderCount int64 - db.Model(&models.Folder{}).Where("parent_path_id = ?", id).Count(&subFolderCount) - - // 统计书签数量 - var bookmarkCount int64 - db.Model(&models.Bookmark{}).Where("parent_path_id = ?", id).Count(&bookmarkCount) - - // 构造响应 - response := folderModel2Res(folder) - c.JSON(http.StatusOK, response) -} - -// ExportFolderTree implements api.ServerInterface. -func (b *BookMarksImpl) ExportFolderTree(c *gin.Context) { - panic("unimplemented") -} - -// ImportFolderTree implements api.ServerInterface. -func (b *BookMarksImpl) ImportFolderTree(c *gin.Context) { - panic("unimplemented") -} - // Make sure we conform to ServerInterface var _ api.ServerInterface = (*BookMarksImpl)(nil) diff --git a/internal/handlers/bookmark_test.go b/internal/handlers/bookmark_test.go deleted file mode 100644 index 4469231..0000000 --- a/internal/handlers/bookmark_test.go +++ /dev/null @@ -1,310 +0,0 @@ -// internal/handlers/bookmark_test.go -package handlers_test - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks" - "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers" - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" -) - -type BookmarkTestSuite struct { - suite.Suite - server *gin.Engine - bookmarks *handlers.BookMarksImpl - testDBPath string -} - -func (suite *BookmarkTestSuite) SetupSuite() { - // 使用内存数据库进行测试 - suite.testDBPath = ":memory:" - - // 设置Gin为测试模式 - gin.SetMode(gin.TestMode) - - var err error - suite.bookmarks, err = handlers.NewBookMarks(suite.testDBPath) - assert.NoError(suite.T(), err) - - suite.server = gin.New() - api.RegisterHandlers(suite.server, suite.bookmarks) -} - -func (suite *BookmarkTestSuite) TestCreateBookmark() { - // 准备测试数据 - detail := "Test detail" - description := "Test description" - link := "https://example.com" - request := api.BookmarkRequest{ - Name: "Test Bookmark", - Detail: &detail, - Description: &description, - Link: &link, - } - - // 将请求转换为JSON - jsonData, _ := json.Marshal(request) - req, _ := http.NewRequest("POST", "/bookmarks/v1/data", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - - // 发送请求 - resp := httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - // 验证响应 - assert.Equal(suite.T(), http.StatusCreated, resp.Code) - - var response api.BookmarkResponse - err := json.Unmarshal(resp.Body.Bytes(), &response) - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), request.Name, response.Name) - assert.Equal(suite.T(), *request.Detail, *response.Detail) - assert.Equal(suite.T(), *request.Description, *response.Description) - assert.Equal(suite.T(), int64(1), response.ParentId) // 默认根目录 -} - -func (suite *BookmarkTestSuite) TestGetBookmark() { - // 先创建一个书签 - detail := "Test detail for get" - description := "Test description for get" - link := "https://example.com/get" - request := api.BookmarkRequest{ - Name: "Test Get Bookmark", - Detail: &detail, - Description: &description, - Link: &link, - } - - jsonData, _ := json.Marshal(request) - req, _ := http.NewRequest("POST", "/bookmarks/v1/data", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - assert.Equal(suite.T(), http.StatusCreated, resp.Code) - var createdBookmark api.BookmarkResponse - err := json.Unmarshal(resp.Body.Bytes(), &createdBookmark) - assert.NoError(suite.T(), err) - - // 获取创建的书签 - req, _ = http.NewRequest("GET", fmt.Sprintf("/bookmarks/v1/data/%d", createdBookmark.Id), nil) - resp = httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - assert.Equal(suite.T(), http.StatusOK, resp.Code) - - var response api.BookmarkResponse - err = json.Unmarshal(resp.Body.Bytes(), &response) - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), createdBookmark.Id, response.Id) - assert.Equal(suite.T(), request.Name, response.Name) -} - -func (suite *BookmarkTestSuite) TestCreateFolder() { - // 准备测试数据 - request := api.FolderRequest{ - Name: "Test Folder", - } - - // 将请求转换为JSON - jsonData, _ := json.Marshal(request) - req, _ := http.NewRequest("POST", "/bookmarks/v1/folder", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - - // 发送请求 - resp := httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - // 验证响应 - assert.Equal(suite.T(), http.StatusCreated, resp.Code) - - var response api.FolderResponse - err := json.Unmarshal(resp.Body.Bytes(), &response) - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), request.Name, response.Name) - assert.Equal(suite.T(), int64(1), response.ParentId) // 默认根目录 -} - -func (suite *BookmarkTestSuite) TestGetFolderInfo() { - // 先创建一个文件夹 - request := api.FolderRequest{ - Name: "Test Get Folder", - } - - jsonData, _ := json.Marshal(request) - req, _ := http.NewRequest("POST", "/bookmarks/v1/folder", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - assert.Equal(suite.T(), http.StatusCreated, resp.Code) - var createdFolder api.FolderResponse - err := json.Unmarshal(resp.Body.Bytes(), &createdFolder) - assert.NoError(suite.T(), err) - - // 获取创建的文件夹信息 - req, _ = http.NewRequest("GET", fmt.Sprintf("/bookmarks/v1/folder/%d", createdFolder.Id), nil) - resp = httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - assert.Equal(suite.T(), http.StatusOK, resp.Code) - - var response api.FolderResponse - err = json.Unmarshal(resp.Body.Bytes(), &response) - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), createdFolder.Id, response.Id) - assert.Equal(suite.T(), request.Name, response.Name) - assert.Equal(suite.T(), 0, response.BookmarkCount) - assert.Equal(suite.T(), 0, response.SubFolderCount) -} - -func (suite *BookmarkTestSuite) TestGetFolderContent() { - // 创建一个文件夹 - folderRequest := api.FolderRequest{ - Name: "Content Test Folder", - } - - folderJson, _ := json.Marshal(folderRequest) - req, _ := http.NewRequest("POST", "/bookmarks/v1/folder", bytes.NewBuffer(folderJson)) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - assert.Equal(suite.T(), http.StatusCreated, resp.Code) - var createdFolder api.FolderResponse - err := json.Unmarshal(resp.Body.Bytes(), &createdFolder) - assert.NoError(suite.T(), err) - - // 在该文件夹中创建一个书签 - detail := "Bookmark in folder" - description := "Test bookmark in folder" - link := "https://example.com/folder" - bookmarkRequest := api.BookmarkRequest{ - Name: "Folder Bookmark", - Detail: &detail, - Description: &description, - Link: &link, - ParentId: &createdFolder.Id, - } - - bookmarkJson, _ := json.Marshal(bookmarkRequest) - req, _ = http.NewRequest("POST", "/bookmarks/v1/data", bytes.NewBuffer(bookmarkJson)) - req.Header.Set("Content-Type", "application/json") - resp = httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - assert.Equal(suite.T(), http.StatusCreated, resp.Code) - - // 获取文件夹内容 - req, _ = http.NewRequest("GET", fmt.Sprintf("/bookmarks/v1/folder/%d/content", createdFolder.Id), nil) - resp = httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - assert.Equal(suite.T(), http.StatusOK, resp.Code) - - var response api.FolderContentResponse - err = json.Unmarshal(resp.Body.Bytes(), &response) - assert.NoError(suite.T(), err) - assert.Len(suite.T(), response.Bookmarks, 1) - assert.Equal(suite.T(), "Folder Bookmark", response.Bookmarks[0].Name) - assert.Len(suite.T(), response.SubFolders, 0) -} - -func (suite *BookmarkTestSuite) TestUpdateBookmark() { - // 先创建一个书签 - detail := "Original detail" - description := "Original description" - link := "https://example.com/original" - request := api.BookmarkRequest{ - Name: "Original Bookmark", - Detail: &detail, - Description: &description, - Link: &link, - } - - jsonData, _ := json.Marshal(request) - req, _ := http.NewRequest("POST", "/bookmarks/v1/data", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - assert.Equal(suite.T(), http.StatusCreated, resp.Code) - var createdBookmark api.BookmarkResponse - err := json.Unmarshal(resp.Body.Bytes(), &createdBookmark) - assert.NoError(suite.T(), err) - - // 更新书签 - newName := "Updated Bookmark" - newDetail := "Updated detail" - updateRequest := api.BookmarkRequest{ - Name: newName, - Detail: &newDetail, - } - - updateJson, _ := json.Marshal(updateRequest) - req, _ = http.NewRequest("PUT", fmt.Sprintf("/bookmarks/v1/data/%d", createdBookmark.Id), bytes.NewBuffer(updateJson)) - req.Header.Set("Content-Type", "application/json") - resp = httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - assert.Equal(suite.T(), http.StatusOK, resp.Code) - - var response api.BookmarkResponse - err = json.Unmarshal(resp.Body.Bytes(), &response) - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), newName, response.Name) - assert.Equal(suite.T(), newDetail, *response.Detail) - // FIXME 确保更新时间发生了变化 时钟粒度不足 - // assert.True(suite.T(), response.UpdatedAt.After(response.CreatedAt) || response.UpdatedAt.Equal(response.CreatedAt)) -} - -func (suite *BookmarkTestSuite) TestUpdateFolder() { - // 先创建一个文件夹 - request := api.FolderRequest{ - Name: "Original Folder", - } - - jsonData, _ := json.Marshal(request) - req, _ := http.NewRequest("POST", "/bookmarks/v1/folder", bytes.NewBuffer(jsonData)) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - assert.Equal(suite.T(), http.StatusCreated, resp.Code) - var createdFolder api.FolderResponse - err := json.Unmarshal(resp.Body.Bytes(), &createdFolder) - assert.NoError(suite.T(), err) - - // 更新文件夹 - newName := "Updated Folder" - updateRequest := api.FolderRequest{ - Name: newName, - } - - updateJson, _ := json.Marshal(updateRequest) - req, _ = http.NewRequest("PUT", fmt.Sprintf("/bookmarks/v1/folder/%d", createdFolder.Id), bytes.NewBuffer(updateJson)) - req.Header.Set("Content-Type", "application/json") - resp = httptest.NewRecorder() - suite.server.ServeHTTP(resp, req) - - assert.Equal(suite.T(), http.StatusOK, resp.Code) - - var response api.FolderResponse - err = json.Unmarshal(resp.Body.Bytes(), &response) - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), newName, response.Name) - // 确保更新时间发生了变化 - assert.True(suite.T(), response.UpdatedAt.After(response.CreatedAt) || response.UpdatedAt.Equal(response.CreatedAt)) -} - -func TestBookmarkTestSuite(t *testing.T) { - suite.Run(t, new(BookmarkTestSuite)) -} diff --git a/internal/handlers/user_np.go b/internal/handlers/user_np.go new file mode 100644 index 0000000..0b09240 --- /dev/null +++ b/internal/handlers/user_np.go @@ -0,0 +1,172 @@ +// internal/handlers/user_np.go + +package handlers + +import ( + "net/http" + + api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/user_np" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models" + "github.com/gin-gonic/gin" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type UserNPImpl struct { + db *gorm.DB +} + +func NewUserNP(dbPath string) (*UserNPImpl, error) { + var err error + var db *gorm.DB + db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{}) + if err != nil { + return nil, err + } + + // 自动迁移表结构 + err = db.AutoMigrate(&models.UserNP{}) + if err != nil { + return nil, err + } + + return &UserNPImpl{db: db}, nil +} + +// PostAuthLogin 用户登录 +func (u *UserNPImpl) PostAuthLogin(c *gin.Context) { + var req api.PostAuthLoginJSONRequestBody + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 查找用户 + var user models.UserNP + if err := u.db.Where("username = ?", req.Username).First(&user).Error; err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) + return + } + + // 验证密码 + if !user.CheckPassword(req.Password) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"}) + return + } + + // 生成JWT token + token, err := user.GenerateSimpleJWT() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "无法生成访问令牌"}) + return + } + + // 更新用户token字段(可选) + user.Token = &token + u.db.Save(&user) + + c.JSON(http.StatusOK, api.LoginResponse{ + Token: &token, + UserId: &user.ID, + }) +} + +// PostAuthRegister 用户注册 +func (u *UserNPImpl) PostAuthRegister(c *gin.Context) { + var req api.PostAuthRegisterJSONRequestBody + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 检查用户名是否已存在 + var existingUser models.UserNP + if err := u.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil { + c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在"}) + return + } + + // 创建新用户 + user := models.UserNP{ + Username: req.Username, + Email: req.Email, + } + + // 加密密码 + if err := user.HashPassword(req.Password); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "密码处理失败"}) + return + } + + // 保存到数据库 + if err := u.db.Create(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "用户创建失败"}) + return + } + + c.JSON(http.StatusCreated, nil) +} + +// PutAuthPassword 修改密码 +func (u *UserNPImpl) PutAuthPassword(c *gin.Context) { + // 获取Authorization头中的token + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "缺少访问令牌"}) + return + } + + // 验证token并获取用户名 + username, err := models.CheckSimpleJWT(authHeader) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + var req api.PutAuthPasswordJSONRequestBody + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 查找用户 + var user models.UserNP + if err := u.db.Where("username = ?", username).First(&user).Error; err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"}) + return + } + + // 验证旧密码 + if !user.CheckPassword(req.OldPassword) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "旧密码错误"}) + return + } + + // 加密新密码 + if err := user.HashPassword(req.NewPassword); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "密码处理失败"}) + return + } + + // 保存到数据库 + if err := u.db.Save(&user).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "密码更新失败"}) + return + } + + // 生成新的JWT token + token, err := user.GenerateSimpleJWT() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "无法生成新的访问令牌"}) + return + } + + // 更新用户token字段 + user.Token = &token + u.db.Save(&user) + + c.JSON(http.StatusOK, nil) +} + +// Make sure we conform to ServerInterface +var _ api.ServerInterface = (*UserNPImpl)(nil) diff --git a/internal/handlers/vfs.go b/internal/handlers/vfs.go index 5ac8282..8bb8afb 100644 --- a/internal/handlers/vfs.go +++ b/internal/handlers/vfs.go @@ -1 +1,354 @@ package handlers + +import ( + "log" + "net/http" + "strings" + "sync" + + api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models" + "github.com/gin-gonic/gin" +) + +// // 一行代码生成安全的随机token +// token := make([]byte, 16) +// rand.Read(token) // 忽略错误处理以简化代码(生产环境建议处理) +// tokenStr := hex.EncodeToString(token) +// adminToken = &tokenStr +// log.Printf("Admin API Token (for Swagger testing): %s", *adminToken) + +// if e, err := casbin.NewEnforcer("./config/model.conf", ".data/policy.csv"); err == nil { +// log.Fatalf("Failed to create casbin enforcer: %v", err) +// } else { +// enforcer = e +// } + +// ServiceProxy 服务代理接口 +type ServiceProxy interface { + // Get 从后端服务获取数据 + Get(c *gin.Context, servicePath string, node *models.VfsNode) (any, error) + + // Create 在后端服务创建资源 + Create(c *gin.Context, servicePath string, node *models.VfsNode, data []byte) (string, error) // 返回创建的资源ID + + // Update 更新后端服务资源 + Update(c *gin.Context, servicePath string, node *models.VfsNode, data []byte) error + + // Delete 删除后端服务资源 + Delete(c *gin.Context, servicePath string, node *models.VfsNode) error + + // GetName 获取代理名称 + GetName() string +} + +// ProxyEntry 代理表条目 +type ProxyEntry struct { + Name string + Proxy ServiceProxy // 对应的代理实现 +} + +type VfsImpl struct { + vfs *models.Vfs + proxyTable []*ProxyEntry // 动态代理表 + proxyMutex sync.RWMutex // 保护代理表的读写锁 +} + +func NewVfsHandler(vfs models.Vfs) (*VfsImpl, error) { + return &VfsImpl{ + vfs: &vfs, + proxyTable: make([]*ProxyEntry, 0), + proxyMutex: sync.RWMutex{}, + }, nil +} + +// CreateUser implements api.ServerInterface. +func (v *VfsImpl) CreateUser(c *gin.Context) { + panic("unimplemented") +} + +// DeleteUser implements api.ServerInterface. +func (v *VfsImpl) DeleteUser(c *gin.Context) { + panic("unimplemented") +} + +// CreateVFSNode implements api.ServerInterface. +func (v *VfsImpl) CreateVFSNode(c *gin.Context, params api.CreateVFSNodeParams) { + // 解析路径组件 + parentPath, nodeName, nodeType, err := models.ParsePathComponents(params.Path) + if err != nil { + c.JSON(http.StatusBadRequest, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } + + // 创建节点 + node, err := v.vfs.CreateNodeByComponents(parentPath, nodeName, nodeType) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "CreateNodeByComponents", + Message: err.Error(), + }) + return + } + if nodeType == models.VfsNodeTypeService { + if !v.Proxy2Service(c, node) { + v.vfs.DeleteVFSNode(node) + return + } + } + + // 返回创建成功的节点 + c.JSON(http.StatusCreated, api.VFSNodeResponse{ + Name: node.Name, + Type: ModelType2ResponseType(node.Type), + CreatedAt: node.CreatedAt, + UpdatedAt: node.UpdatedAt, + }) +} + +// DeleteVFSNode implements api.ServerInterface. +func (v *VfsImpl) DeleteVFSNode(c *gin.Context, params api.DeleteVFSNodeParams) { + node, err := v.vfs.GetNodeByPath(params.Path) + if err != nil { + c.JSON(http.StatusNotFound, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } + + if node.Type == models.VfsNodeTypeService { + if !v.Proxy2Service(c, node) { + return + } + } + + if err := v.vfs.DeleteVFSNode(node); err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } + c.JSON(http.StatusNoContent, nil) +} + +// GetVFSNode implements api.ServerInterface. +func (v *VfsImpl) GetVFSNode(c *gin.Context, params api.GetVFSNodeParams) { + node, err := v.vfs.GetNodeByPath(params.Path) + if err != nil { + c.JSON(http.StatusNotFound, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } + + switch node.Type { + case models.VfsNodeTypeDirectory: + if entries, err := v.vfs.GetChildren(node.ID); err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } else { + var responseEntries []api.VFSDirectoryEntry + for _, entry := range entries { + responseEntries = append(responseEntries, api.VFSDirectoryEntry{ + Name: entry.Name, + Type: ModelType2ResponseType(entry.Type), + }) + } + c.JSON(http.StatusOK, responseEntries) + return + } + case models.VfsNodeTypeService: + v.Proxy2Service(c, node) + default: + c.JSON(http.StatusBadRequest, api.Error{ + Errtype: "error", + Message: "Not a directory", + }) + } + +} + +// UpdateVFSNode implements api.ServerInterface. +func (v *VfsImpl) UpdateVFSNode(c *gin.Context, params api.UpdateVFSNodeParams) { + var req api.UpdateVFSNodeJSONRequestBody + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, api.Error{ + Errtype: "ParameterError", + Message: "Invalid request parameters", + }) + return + } + + node, err := v.vfs.GetNodeByPath(params.Path) + if err != nil { + c.JSON(http.StatusNotFound, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } + if req.NewName != nil { + node.Name = *req.NewName + } + + // FIXME + if node.Type == models.VfsNodeTypeService { + if !v.Proxy2Service(c, node) { + return + } + } + + // TODO + if err := v.vfs.UpdateVFSNode(node); err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: err.Error(), + }) + return + } +} + +// Make sure we conform to ServerInterface +var _ api.ServerInterface = (*VfsImpl)(nil) + +func ModelType2ResponseType(nodeType models.VfsNodeType) api.VFSNodeType { + var reponseType api.VFSNodeType + switch nodeType { + case models.VfsNodeTypeFile: + reponseType = api.File + case models.VfsNodeTypeDirectory: + reponseType = api.Directory + case models.VfsNodeTypeService: + reponseType = api.Service + } + return reponseType +} + +// FindProxyByServiceName 根据服务节点名称查找对应的代理 +func (v *VfsImpl) FindProxyByServiceName(serviceName string) ServiceProxy { + v.proxyMutex.RLock() + defer v.proxyMutex.RUnlock() + + if serviceName == "" { + return nil + } + + // 根据服务名称匹配前缀 + for _, entry := range v.proxyTable { + if entry.Name == serviceName { + return entry.Proxy + } + } + + return nil +} + +func (v *VfsImpl) RegisterProxy(entry *ProxyEntry) { + v.proxyMutex.Lock() + defer v.proxyMutex.Unlock() + + v.proxyTable = append(v.proxyTable, entry) +} + +// Proxy2Service 通用服务代理处理函数 +func (v *VfsImpl) Proxy2Service(c *gin.Context, node *models.VfsNode) bool { + exts := strings.Split(node.Name, ".") + var serviceName = exts[1] + log.Println("Proxy2Service: ", serviceName) + // 查找对应的代理 + proxy := v.FindProxyByServiceName(serviceName) + if proxy == nil { + c.JSON(http.StatusNotImplemented, api.Error{ + Errtype: "error", + Message: "Service proxy not found for: " + serviceName, + }) + return false + } + + // 根据HTTP方法调用相应的代理方法 + switch c.Request.Method { + case http.MethodGet: + result, err := proxy.Get(c, serviceName, node) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: "Failed to get service data: " + err.Error(), + }) + return false + } + + c.JSON(http.StatusOK, result) + return true + case http.MethodPost: + // 读取请求体数据 + data, err := c.GetRawData() + if err != nil { + c.JSON(http.StatusBadRequest, api.Error{ + Errtype: "error", + Message: "Failed to read request data: " + err.Error(), + }) + return false + } + + resourceID, err := proxy.Create(c, serviceName, node, data) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: "Failed to create service resource: " + err.Error(), + }) + return false + } + + c.JSON(http.StatusCreated, gin.H{"resource_id": resourceID}) + return true + case http.MethodPut, http.MethodPatch: + // 读取请求体数据 + data, err := c.GetRawData() + if err != nil { + c.JSON(http.StatusBadRequest, api.Error{ + Errtype: "error", + Message: "Failed to read request data: " + err.Error(), + }) + return false + } + + err = proxy.Update(c, serviceName, node, data) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: "Failed to update service resource: " + err.Error(), + }) + return false + } + + c.JSON(http.StatusOK, gin.H{"message": "Updated successfully"}) + return true + case http.MethodDelete: + err := proxy.Delete(c, serviceName, node) + if err != nil { + c.JSON(http.StatusInternalServerError, api.Error{ + Errtype: "error", + Message: "Failed to delete service resource: " + err.Error(), + }) + return false + } + + c.JSON(http.StatusNoContent, nil) + return true + default: + c.JSON(http.StatusMethodNotAllowed, api.Error{ + Errtype: "error", + Message: "Method not allowed", + }) + return false + } +} diff --git a/internal/handlers/vfs_driver/vfs_bookmark.go b/internal/handlers/vfs_driver/vfs_bookmark.go new file mode 100644 index 0000000..d9ae729 --- /dev/null +++ b/internal/handlers/vfs_driver/vfs_bookmark.go @@ -0,0 +1,160 @@ +// internal/handlers/vfs_driver/vfs_bookmark.go + +package vfsdriver + +import ( + "context" + "encoding/json" + "fmt" + + api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models" + "github.com/gin-gonic/gin" +) + +type VfsBookMarkService struct { + client *api.ClientWithResponses +} + +func NewVfsBookMarkService(serverURL string) (*VfsBookMarkService, error) { + client, err := api.NewClientWithResponses(serverURL) + if err != nil { + return nil, err + } + + return &VfsBookMarkService{ + client: client, + }, nil +} + +// Create implements ServiceProxy. +func (v *VfsBookMarkService) Create(c *gin.Context, servicePath string, node *models.VfsNode, data []byte) (string, error) { + ctx := context.Background() + + // 解析传入的数据为 BookmarkRequest + var req api.BookmarkRequest + if err := json.Unmarshal(data, &req); err != nil { + return "", err + } + + // 调用 bookmark 服务创建书签 + resp, err := v.client.CreateBookmarkWithResponse(ctx, int64(node.ID), req) + if err != nil { + return "", err + } + + // 处理响应 + if resp.JSON201 != nil { + result, err := json.Marshal(resp.JSON201) + if err != nil { + return "", err + } + return string(result), nil + } + + // 处理错误情况 + if resp.JSON400 != nil { + return "", fmt.Errorf("bad request: %s", resp.JSON400.Message) + } + + if resp.JSON500 != nil { + return "", fmt.Errorf("server error: %s", resp.JSON500.Message) + } + + return "", fmt.Errorf("unknown error") +} + +// Delete implements ServiceProxy. +func (v *VfsBookMarkService) Delete(c *gin.Context, servicePath string, node *models.VfsNode) error { + ctx := context.Background() + + // 调用 bookmark 服务删除书签 + resp, err := v.client.DeleteBookmarkWithResponse(ctx, int64(node.ID)) + if err != nil { + return err + } + + // 处理响应 + if resp.StatusCode() == 204 { + return nil + } + + // 处理错误情况 + if resp.JSON404 != nil { + return fmt.Errorf("not found: %s", resp.JSON404.Message) + } + + if resp.JSON500 != nil { + return fmt.Errorf("server error: %s", resp.JSON500.Message) + } + + return fmt.Errorf("unknown error") +} + +// Get implements ServiceProxy. +func (v *VfsBookMarkService) Get(c *gin.Context, servicePath string, node *models.VfsNode) (any, error) { + ctx := context.Background() + + // 调用 bookmark 服务获取书签 + resp, err := v.client.GetBookmarkWithResponse(ctx, int64(node.ID)) + if err != nil { + return nil, err + } + + // 处理响应 + if resp.JSON200 != nil { + return resp.JSON200, nil + } + + // 处理错误情况 + if resp.JSON404 != nil { + return nil, fmt.Errorf("not found: %s", resp.JSON404.Message) + } + + return nil, fmt.Errorf("unknown error") +} + +// GetName implements ServiceProxy. +func (v *VfsBookMarkService) GetName() string { + return "bookmark" +} + +// Update implements ServiceProxy. +func (v *VfsBookMarkService) Update(c *gin.Context, servicePath string, node *models.VfsNode, data []byte) error { + ctx := context.Background() + + // 解析传入的数据为 BookmarkRequest + var req api.BookmarkRequest + if err := json.Unmarshal(data, &req); err != nil { + return err + } + + // 调用 bookmark 服务更新书签 + resp, err := v.client.UpdateBookmarkWithResponse(ctx, int64(node.ID), req) + if err != nil { + return err + } + + // 处理响应 + if resp.JSON200 != nil { + return nil + } + + // 处理错误情况 + if resp.JSON400 != nil { + return fmt.Errorf("bad request: %s", resp.JSON400.Message) + } + + if resp.JSON404 != nil { + return fmt.Errorf("not found: %s", resp.JSON404.Message) + } + + if resp.JSON500 != nil { + return fmt.Errorf("server error: %s", resp.JSON500.Message) + } + + return fmt.Errorf("unknown error") +} + +var _ handlers.ServiceProxy = (*VfsBookMarkService)(nil) diff --git a/internal/models/bookmark.go b/internal/models/bookmark.go index 51a5383..452ab5c 100644 --- a/internal/models/bookmark.go +++ b/internal/models/bookmark.go @@ -6,50 +6,14 @@ import ( "gorm.io/gorm" ) -// Bookmark 书签结构体 type Bookmark struct { - ID int64 `json:"id" gorm:"primaryKey"` + ID int64 `json:"id" gorm:"primaryKey;autoIncrement"` + ExternalID int64 `json:"external_id" gorm:"uniqueIndex;not null"` Name string `json:"name" gorm:"not null;index;size:255"` Link *string `json:"link" gorm:"type:url"` Detail *string `json:"detail" gorm:"type:text"` Description *string `json:"description" gorm:"type:text"` - ParentID int64 `json:"parent_id" gorm:"not null;index"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` } - -// Folder 文件夹结构体 -type Folder struct { - ID int64 `json:"id" gorm:"primaryKey"` - Name string `json:"name" gorm:"not null;index;size:255"` - ParentID *int64 `json:"parent_id" gorm:"index"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` -} - -// IsValidParent 检查父文件夹ID是否有效 -func (f *Folder) IsValidParent(db *gorm.DB, parentID int64) bool { - // 检查父文件夹是否存在且未被删除 - if err := db.Where("id = ?", parentID).First(&Folder{}).Error; err != nil { - return false - } - - // 防止循环引用(不能将文件夹设置为自己的子文件夹) - if parentID == f.ID { - return false - } - - return true -} - -// IsValidParent 检查书签的父文件夹ID是否有效 -func (b *Bookmark) IsValidParent(db *gorm.DB, parentID int64) bool { - // 检查父文件夹是否存在且未被删除 - if err := db.Where("id = ?", parentID).First(&Folder{}).Error; err != nil { - return false - } - - return true -} diff --git a/internal/models/user.go b/internal/models/user.go deleted file mode 100644 index 0493026..0000000 --- a/internal/models/user.go +++ /dev/null @@ -1,15 +0,0 @@ -package models - -type PasswordUser struct { - ID int64 - Username string - Password string - BookMarkUserID int64 -} - -type BookMarkUser struct { - ID int64 - RootFolderID int64 - Token string - Data *string -} diff --git a/internal/models/user_np.go b/internal/models/user_np.go new file mode 100644 index 0000000..9d06579 --- /dev/null +++ b/internal/models/user_np.go @@ -0,0 +1,100 @@ +// internal/models/user_np.go + +package models + +import ( + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +// UserNP 用户名密码认证模型 +type UserNP struct { + ID int64 `json:"id" gorm:"primaryKey"` + Username string `json:"username" gorm:"not null;index;size:255;unique"` + Password string `json:"password" gorm:"not null;size:255"` + Email *string `json:"email" gorm:"type:text;unique"` + Token *string `json:"token" gorm:"type:text"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` +} + +// JWTSecret JWT签名密钥(在实际应用中应该从环境变量或配置文件中读取) +var JWTSecret = []byte("your-secret-key-change-in-production") + +// SimpleClaims 简单的JWT声明结构体 +type SimpleClaims struct { + Username string `json:"username"` + jwt.RegisteredClaims +} + +// HashPassword 对密码进行哈希处理 +func (u *UserNP) HashPassword(password string) error { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + if err != nil { + return err + } + u.Password = string(bytes) + return nil +} + +// CheckPassword 验证密码 +func (u *UserNP) CheckPassword(providedPassword string) bool { + err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(providedPassword)) + return err == nil +} + +// GenerateSimpleJWT 生成简单的JWT Token +func (u *UserNP) GenerateSimpleJWT() (string, error) { + expirationTime := time.Now().Add(24 * time.Hour) + + claims := &SimpleClaims{ + Username: u.Username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expirationTime), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "zzyxyz_user_np_api", + Subject: u.Username, + ID: string(rune(u.ID)), // 将用户ID作为JWT ID + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString(JWTSecret) + if err != nil { + return "", err + } + + return tokenString, nil +} + +// CheckSimpleJWT 验证JWT Token +func CheckSimpleJWT(tokenString string) (string, error) { + claims := &SimpleClaims{} + + // 解析token + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return JWTSecret, nil + }) + + if err != nil { + // 检查是否是token过期错误 + if errors.Is(err, jwt.ErrTokenExpired) { + return "", errors.New("token已过期") + } + return "", errors.New("无效的token") + } + + // 验证token有效性 + if !token.Valid { + return "", errors.New("无效的token") + } + + // 返回用户名 + return claims.Username, nil +} diff --git a/internal/models/vfs.go b/internal/models/vfs.go new file mode 100644 index 0000000..6c6a0ff --- /dev/null +++ b/internal/models/vfs.go @@ -0,0 +1,291 @@ +// vfs.go +package models + +import ( + "database/sql" + "errors" + "path" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +type Vfs struct { + DB *sql.DB +} + +type VfsNodeType int + +const ( + VfsNodeTypeFile VfsNodeType = iota + VfsNodeTypeService + VfsNodeTypeDirectory + VfsNodeTypeSymlink +) + +type VfsNode struct { + ID uint64 + Name string + ParentID uint64 + Type VfsNodeType + CreatedAt time.Time + UpdatedAt time.Time +} + +type VfsDirEntry struct { + ID uint64 + Name string + Type VfsNodeType +} + +// NewVfs 创建新的 VFS 实例 +func NewVfs(dbPath string) (*Vfs, error) { + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, err + } + + // 创建表 + _, err = db.Exec(` + CREATE TABLE IF NOT EXISTS vfs_nodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + parent_id INTEGER, + type INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(parent_id, name) + )`) + if err != nil { + return nil, err + } + + return &Vfs{DB: db}, nil +} + +// GetChildrenID 获取目录下所有子项的 ID +func (v *Vfs) GetChildren(parentID uint64) ([]VfsDirEntry, error) { + rows, err := v.DB.Query("SELECT id, name, type FROM vfs_nodes WHERE parent_id = ?", parentID) + if err != nil { + return nil, err + } + defer rows.Close() + + var dirEntrys []VfsDirEntry + for rows.Next() { + var entry VfsDirEntry + if err := rows.Scan(&entry.ID, &entry.Name, &entry.Type); err != nil { + return nil, err + } + dirEntrys = append(dirEntrys, entry) + } + + return dirEntrys, nil +} + +// GetParentID 根据父路径查找父节点 ID +// parentPath 应该是 ParsePathComponents 的第一个返回值 +func (v *Vfs) GetParentID(parentPath string) (uint64, error) { + // 根目录特殊处理 + if parentPath == "/" || parentPath == "" { + return 0, nil + } + + // 递归查找父路径ID + // 先解析父路径的父路径和节点名 + grandParentPath, parentName, _, err := ParsePathComponents(parentPath) + if err != nil { + return 0, err + } + + // 递归获取祖父节点ID + grandParentID, err := v.GetParentID(grandParentPath) + if err != nil { + return 0, err + } + + // 在祖父节点下查找父节点 + var parentID uint64 + err = v.DB.QueryRow("SELECT id FROM vfs_nodes WHERE parent_id = ? AND name = ?", + grandParentID, parentName).Scan(&parentID) + + if err != nil { + if err == sql.ErrNoRows { + return 0, errors.New("parent path not found") + } + return 0, err + } + + return parentID, nil +} + +// GetNodeByPath 根据完整路径查找节点 +func (v *Vfs) GetNodeByPath(fullPath string) (*VfsNode, error) { + // 根目录特殊处理 + if path.Clean(fullPath) == "/" { + return &VfsNode{ + ID: 0, + Name: "/", + ParentID: 0, + Type: VfsNodeTypeDirectory, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, nil + } + + // 使用 ParsePathComponents 解析路径 + parentPath, nodeName, nodeType, err := ParsePathComponents(fullPath) + if err != nil { + return nil, err + } + + // 获取父节点ID + parentID, err := v.GetParentID(parentPath) + if err != nil { + return nil, err + } + + // 根据 parentID, nodeName, nodeType 查找或创建节点 + node, err := v.GetNodeByParentIDAndName(parentID, nodeName) + if err != nil { + // 如果节点不存在,可以选择创建它或者返回错误 + // 这里根据你的需求决定是返回错误还是创建节点 + return nil, err + } + + if node.Type != nodeType { + return nil, errors.New("node type mismatch") + } + + return node, nil +} + +// GetNodeByParentIDAndName 根据父ID和名称查找节点 +func (v *Vfs) GetNodeByParentIDAndName(parentID uint64, name string) (*VfsNode, error) { + node := &VfsNode{} + err := v.DB.QueryRow(` + SELECT id, name, parent_id, type, created_at, updated_at + FROM vfs_nodes + WHERE parent_id = ? AND name = ?`, parentID, name).Scan( + &node.ID, &node.Name, &node.ParentID, &node.Type, &node.CreatedAt, &node.UpdatedAt) + + if err != nil { + if err == sql.ErrNoRows { + return nil, errors.New("node not found") + } + return nil, err + } + + return node, nil +} + +// CreateNodeByComponents 根据路径组件创建节点 +func (v *Vfs) CreateNodeByComponents(parentPath, nodeName string, nodeType VfsNodeType) (*VfsNode, error) { + // 获取父节点ID + parentID, err := v.GetParentID(parentPath) + if err != nil { + return nil, err + } + + // 创建新节点 + node := &VfsNode{ + Name: nodeName, + ParentID: parentID, + Type: nodeType, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + // 保存到数据库 + result, err := v.DB.Exec(` + INSERT INTO vfs_nodes (name, parent_id, type, created_at, updated_at) + VALUES (?, ?, ?, ?, ?)`, + node.Name, node.ParentID, node.Type, node.CreatedAt, node.UpdatedAt) + + if err != nil { + return nil, err + } + + // 获取插入的ID + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + node.ID = uint64(id) + return node, nil +} + +func ParsePathComponents(pathStr string) (parentPath, nodeName string, nodeType VfsNodeType, err error) { + abspath := path.Clean(pathStr) + if !path.IsAbs(abspath) { + return "", "", 0, errors.New("path must be absolute") + } + + // 特殊处理根路径 + if abspath == "/" { + return "", "", VfsNodeTypeDirectory, nil + } + + // 判断是文件还是目录 + nodeType = VfsNodeTypeFile + // 如果原始路径以 / 结尾,则为目录 + if len(pathStr) > 1 && pathStr[len(pathStr)-1] == '/' { + nodeType = VfsNodeTypeDirectory + } + if nodeType == VfsNodeTypeFile && path.Ext(pathStr) == ".api" { + nodeType = VfsNodeTypeService + } + + // 分割路径 + parentPath, nodeName = path.Split(abspath) + + // 清理父路径 + if parentPath != "/" && parentPath != "" { + parentPath = path.Clean(parentPath) + } + + // 处理特殊情况 + if parentPath == "." { + parentPath = "/" + } + + return parentPath, nodeName, nodeType, nil +} + +func (v *Vfs) CreateVFSNode(p *VfsNode) error { + _, err := v.DB.Exec(` + INSERT INTO vfs_nodes (name, parent_id, type, created_at, updated_at) + VALUES (?, ?, ?, ?, ?)`, + p.Name, p.ParentID, p.Type, time.Now(), time.Now()) + return err +} + +func (v *Vfs) DeleteVFSNode(p *VfsNode) error { + _, err := v.DB.Exec("DELETE FROM vfs_nodes WHERE id = ?", p.ID) + return err +} + +func (v *Vfs) GetVFSNode(p *VfsNode) *VfsNode { + node := &VfsNode{} + err := v.DB.QueryRow(` + SELECT id, name, parent_id, type, created_at, updated_at + FROM vfs_nodes + WHERE id = ?`, p.ID).Scan( + &node.ID, &node.Name, &node.ParentID, &node.Type, &node.CreatedAt, &node.UpdatedAt) + + if err != nil { + return nil + } + + return node +} + +func (v *Vfs) UpdateVFSNode(p *VfsNode) error { + _, err := v.DB.Exec(` + UPDATE vfs_nodes + SET name = ?, parent_id = ?, type = ?, updated_at = ? + WHERE id = ?`, + p.Name, p.ParentID, p.Type, time.Now(), p.ID) + return err +} diff --git a/internal/models/vfs_test.go b/internal/models/vfs_test.go new file mode 100644 index 0000000..3e97613 --- /dev/null +++ b/internal/models/vfs_test.go @@ -0,0 +1,129 @@ +package models_test + +import ( + "testing" + + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models" +) + +func TestParsePathComponents(t *testing.T) { + tests := []struct { + name string + pathStr string + wantParent string + wantName string + wantNodeType models.VfsNodeType + wantErr bool + }{ + { + name: "directory path with trailing slash", + pathStr: "/home/", + wantParent: "/", + wantName: "home", + wantNodeType: models.VfsNodeTypeDirectory, + wantErr: false, + }, + { + name: "file path without extension", + pathStr: "/home", + wantParent: "/", + wantName: "home", + wantNodeType: models.VfsNodeTypeFile, + wantErr: false, + }, + { + name: "file path with extension", + pathStr: "/home.txt", + wantParent: "/", + wantName: "home.txt", + wantNodeType: models.VfsNodeTypeFile, + wantErr: false, + }, + { + name: "nested directory path with trailing slash", + pathStr: "/home/user/", + wantParent: "/home", + wantName: "user", + wantNodeType: models.VfsNodeTypeDirectory, + wantErr: false, + }, + { + name: "nested file path", + pathStr: "/home/user/file.txt", + wantParent: "/home/user", + wantName: "file.txt", + wantNodeType: models.VfsNodeTypeFile, + wantErr: false, + }, + { + name: "deep nested directory path with trailing slash", + pathStr: "/home/user/documents/", + wantParent: "/home/user", + wantName: "documents", + wantNodeType: models.VfsNodeTypeDirectory, + wantErr: false, + }, + { + name: "deep nested file path", + pathStr: "/home/user/documents/file.txt", + wantParent: "/home/user/documents", + wantName: "file.txt", + wantNodeType: models.VfsNodeTypeFile, + wantErr: false, + }, + { + name: "relative path should error", + pathStr: ".", + wantErr: true, + }, + { + name: "relative path should error", + pathStr: "home.txt", + wantErr: true, + }, + { + name: "relative path should error", + pathStr: "home/user/", + wantErr: true, + }, + { + name: "path with multiple slashes", + pathStr: "//home//user//", + wantParent: "/home", + wantName: "user", + wantNodeType: models.VfsNodeTypeDirectory, + wantErr: false, + }, + { + name: "path with multiple slashes and file", + pathStr: "//home//user//file.txt", + wantParent: "/home/user", + wantName: "file.txt", + wantNodeType: models.VfsNodeTypeFile, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parent, name, nodeType, err := models.ParsePathComponents(tt.pathStr) + + if (err != nil) != tt.wantErr { + t.Errorf("ParsePathComponents() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + if parent != tt.wantParent { + t.Errorf("ParsePathComponents() parent = %v, want %v", parent, tt.wantParent) + } + if name != tt.wantName { + t.Errorf("ParsePathComponents() name = %v, want %v", name, tt.wantName) + } + if nodeType != tt.wantNodeType { + t.Errorf("ParsePathComponents() nodeType = %v, want %v", nodeType, tt.wantNodeType) + } + } + }) + } +} diff --git a/magefile.go b/magefile.go index 3ea2357..8521973 100644 --- a/magefile.go +++ b/magefile.go @@ -7,76 +7,64 @@ import ( "fmt" "os" "os/exec" - - "github.com/magefile/mage/mg" // mg contains helpful utility functions, like Deps + // mg contains helpful utility functions, like Deps ) // Default target to run when none is specified // If not set, running mage will list available targets // var Default = Build -// A build step that requires additional params, or platform specific steps for example -// Build builds the application for multiple platforms. -func Build() error { - mg.Deps(InstallDeps) +func BuildAll() error { + services := []struct { + Name string + Path string + }{ + {"openapi", "."}, + {"bookmark", "./cmd/bookmark"}, + {"user_np", "./cmd/user_np"}, + } - // Define target platforms platforms := []struct { OS string Arch string }{ {"linux", "amd64"}, - {"linux", "arm64"}, - {"darwin", "amd64"}, - {"darwin", "arm64"}, {"windows", "amd64"}, } - for _, p := range platforms { - fmt.Printf("Building for %s/%s...\n", p.OS, p.Arch) + for _, service := range services { + for _, p := range platforms { + fmt.Printf("Building service %s for %s/%s...\n", service.Name, p.OS, p.Arch) - // Set environment variables for cross-compilation - env := append(os.Environ(), - fmt.Sprintf("GOOS=%s", p.OS), - fmt.Sprintf("GOARCH=%s", p.Arch)) + // 设置环境变量 + env := append(os.Environ(), + fmt.Sprintf("GOOS=%s", p.OS), + fmt.Sprintf("GOARCH=%s", p.Arch)) - // Determine output name - outputName := fmt.Sprintf("./bin/zzyxyz_go_api-%s-%s", p.OS, p.Arch) - if p.OS == "windows" { - outputName += ".exe" - } + // 确定输出名称 + outputName := fmt.Sprintf("./bin/%s/%s-%s-%s", p.OS, service.Name, p.OS, p.Arch) + if p.OS == "windows" { + outputName += ".exe" + } - // Run build command - cmd := exec.Command("go", "build", "-o", outputName, ".") - cmd.Env = env + // // 执行构建命令 + // cmd := exec.Command("go", "build", "-o", outputName, service.Path) + // cmd.Env = env - if err := cmd.Run(); err != nil { - return err + // 使用 release 模式构建并显示链接信息 + cmd := exec.Command("go", "build", + "-x", + "-ldflags", "-s -w -extldflags -static", // 去除调试信息,减小体积 + "-v", // 显示编译过程中的包信息 + "-o", outputName, + service.Path) + cmd.Env = env + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to build %s: %w", service.Name, err) + } } } return nil } - -// A custom install step if you need your bin someplace other than go/bin -func Install() error { - mg.Deps(Build) - // fmt.Println("Installing...") - // return os.Rename("./MyApp", "/usr/bin/MyApp") - return nil -} - -// Manage your deps, or running package managers. -func InstallDeps() error { - fmt.Println("Installing Deps...") - // TODO downloads swagger-ui-5.29.0 /dist/* - // cmd := exec.Command("go", "get", "github.com/stretchr/piglatin") - // return cmd.Run() - return nil -} - -// Clean up after yourself -func Clean() { - fmt.Println("Cleaning...") - // os.RemoveAll("MyApp") -} diff --git a/main.go b/main.go index 158672f..7f3ded1 100644 --- a/main.go +++ b/main.go @@ -2,17 +2,24 @@ package main import ( "embed" + "fmt" "log" "net/http" - bookmarks "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks" + vfs "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs" "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers" + vfsdriver "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers/vfs_driver" + "git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models" "github.com/gin-gonic/gin" ) -//go:generate go tool oapi-codegen -config config/bookmark_cfg.yaml config/bookmark.yaml -//go:generate go tool oapi-codegen -config config/vfs_cfg.yaml config/vfs.yaml +//go:generate go tool oapi-codegen -config config/bookmark/client.yaml config/bookmark/bookmark.yaml +//go:generate go tool oapi-codegen -config config/bookmark/server.yaml config/bookmark/bookmark.yaml +//go:generate go tool oapi-codegen -config config/vfs/server.yaml config/vfs/vfs.yaml +//go:generate go tool oapi-codegen -config config/vfs/server.yaml config/vfs/vfs.yaml +//go:generate go tool oapi-codegen -config config/user_np/server.yaml config/user_np/user_np.yaml + //go:embed config/* dist/* var staticFiles embed.FS @@ -28,24 +35,38 @@ func main() { api_router := router.Group("/api") { - // create a type that satisfies the `api.ServerInterface`, - // which contains an implementation of every operation from the generated code - if server, err := handlers.NewBookMarks("./data/bookmark.sqlite3"); err != nil { + if vfsImpl, err := models.NewVfs("./data/vfs.sqlite3"); err != nil { + log.Fatal("Failed to create vfs server:", err) + } else if server, err := handlers.NewVfsHandler(*vfsImpl); err != nil { log.Fatal("Failed to create bookmarks server:", err) - } else if permission, err := handlers.NewBookMarkPermission(); err != nil || permission == nil { - log.Fatal("Failed to create bookmarks permission:", err) } else { - bookmarks.RegisterHandlersWithOptions(api_router, server, *permission) + vfs.RegisterHandlers(api_router, server) + // 示例:在你的服务初始化代码中 + bookmarkService, err := vfsdriver.NewVfsBookMarkService("http://localhost:8081/api") // 替换为实际的 bookmark 服务地址 + if err != nil { + log.Fatal("Failed to create bookmark service client:", err) + } + server.RegisterProxy(&handlers.ProxyEntry{Name: "bk", Proxy: bookmarkService}) } - handlers.TodoHandler(api_router) } // FIXME 可能有更好的方式实现这个代码 // 提供嵌入的静态文件访问 - OpenAPI YAML 文件和 dist 目录 - router.GET("/config/*filepath", func(ctx *gin.Context) { + router.GET("/config/*filename", func(ctx *gin.Context) { + // 直接修改请求路径实现映射 + r := ctx.Request + originalPath := r.URL.Path + + filename := ctx.Param("filename") + filepath := fmt.Sprintf("/config/%s/%s.yaml", filename, filename) + r.URL.Path = filepath + fs := http.FileServer(http.FS(staticFiles)) - fs.ServeHTTP(ctx.Writer, ctx.Request) + fs.ServeHTTP(ctx.Writer, r) + + // 恢复原始路径 + r.URL.Path = originalPath }) router.GET("/swagger/*filepath", func(ctx *gin.Context) { // 直接修改请求路径实现映射