feat(bookmark): 初始化书签服务并配置路由与权限控制
新增书签服务主程序,使用 Gin 框架搭建 HTTP 服务器,并注册书签相关接口处理器。 配置 CORS 中间件以支持跨域请求。服务监听端口为 8081。 feat(user_np): 初始化用户权限服务并注册认证接口 新增用户权限服务主程序,使用 Gin 框架搭建 HTTP 服务器,并注册登录、注册及修改密码等接口处理器。 服务监听端口为 8082。 refactor(config): 重构 OpenAPI 配置文件结构并拆分模块 将原有合并的 OpenAPI 配置文件按功能模块拆分为 bookmark 和 user_np 两个独立目录, 分别管理各自的 server、client 及 API 定义文件,便于后续维护和扩展。 refactor(vfs): 调整虚拟文件系统 API 接口路径与参数定义 更新 VFS API 配置文件,修改部分接口路径及参数结构, 如将文件路径参数由 path 转为 query 参数,并优化响应结构体定义。
This commit is contained in:
40
cmd/bookmark/bookmark.go
Normal file
40
cmd/bookmark/bookmark.go
Normal file
@ -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))
|
||||
}
|
29
cmd/user_np/user_np.go
Normal file
29
cmd/user_np/user_np.go
Normal file
@ -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))
|
||||
}
|
@ -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
|
213
config/bookmark/bookmark.yaml
Normal file
213
config/bookmark/bookmark.yaml
Normal file
@ -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
|
5
config/bookmark/client.yaml
Normal file
5
config/bookmark/client.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
# yaml-language-server: ...
|
||||
package: api
|
||||
generate:
|
||||
client: true
|
||||
output: ./gen/bookmarks/client.go
|
@ -3,4 +3,4 @@ package: api
|
||||
generate:
|
||||
gin-server: true
|
||||
models: true
|
||||
output: ./gen/vfs/gen.go
|
||||
output: ./gen/bookmarks/server.go
|
7
config/user_np/server.yaml
Normal file
7
config/user_np/server.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
# yaml-language-server: ...
|
||||
package: api
|
||||
generate:
|
||||
gin-server: true
|
||||
models: true
|
||||
# strict-server: true
|
||||
output: ./gen/user_np/server.go
|
127
config/user_np/user_np.yaml
Normal file
127
config/user_np/user_np.yaml
Normal file
@ -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
|
5
config/vfs/client.yaml
Normal file
5
config/vfs/client.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
# yaml-language-server: ...
|
||||
package: api
|
||||
generate:
|
||||
client: true
|
||||
output: ./gen/vfs/client.go
|
@ -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
|
@ -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"
|
19
go.mod
19
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
|
||||
|
43
go.sum
43
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=
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
172
internal/handlers/user_np.go
Normal file
172
internal/handlers/user_np.go
Normal file
@ -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)
|
@ -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
|
||||
}
|
||||
}
|
||||
|
160
internal/handlers/vfs_driver/vfs_bookmark.go
Normal file
160
internal/handlers/vfs_driver/vfs_bookmark.go
Normal file
@ -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)
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
100
internal/models/user_np.go
Normal file
100
internal/models/user_np.go
Normal file
@ -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
|
||||
}
|
291
internal/models/vfs.go
Normal file
291
internal/models/vfs.go
Normal file
@ -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
|
||||
}
|
129
internal/models/vfs_test.go
Normal file
129
internal/models/vfs_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
86
magefile.go
86
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")
|
||||
}
|
||||
|
45
main.go
45
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) {
|
||||
// 直接修改请求路径实现映射
|
||||
|
Reference in New Issue
Block a user