feat(bookmark): 添加文件夹树结构导入导出接口

新增 `/bookmarks/v1/folder/serial` 接口,支持文件夹树结构的压缩导出与解压导入。
同时完善了相关响应结构体定义,如 ImportResponse 等。

refactor(bookmark): 重命名配置文件并调整字段命名

将 `config/api.yaml` 重命名为 `config/bookmark.yaml`,并统一将 parent_path_id 字段
更名为 parent_id。此外,更新 API 鉴权头名称为 X-BookMark-Token。

feat(bookmark): 实现文件夹挂载管理功能

新增以下三个接口用于管理文件夹挂载:
- GET `/bookmarks/v1/folder/{id}/mount` 获取挂载信息
- POST `/bookmarks/v1/folder/{id}/mount` 挂载文件夹
- DELETE `/bookmarks/v1/folder/{id}/mount` 取消挂载

新增相关结构体定义:MountResponse、MountInfo。

feat(vfs): 初始化虚拟文件系统 API 配置

新增 `config/vfs.yaml` 和 `config/vfs_cfg.yaml` 配置文件,定义 VFS 相关接口和代码生成规则。
接口包括文件/目录的创建、读取、更新和删除操作,并引入新的安全头 X-VFS-Token。

chore(config): 忽略 data 目录并更新生成路径

.gitignore 中新增忽略 data/ 目录。同时更新 bookmark 和 vfs 的代码生成输出路径分别为
`./gen/bookmarks/gen.go` 和 `./gen/vfs/gen.go`。

chore(deps): 引入 casbin、gopsutil 等依赖库

go.mod 中新增 casbin 权限控制、gopsutil 系统监控等相关依赖。
```
This commit is contained in:
zzy
2025-09-23 01:33:50 +08:00
parent 6e513dbeb8
commit 60d6628b0d
15 changed files with 696 additions and 79 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
dist/
gen/
bin/
data/
*.exe
*.sqlite3

View File

@ -17,6 +17,51 @@ 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: 创建文件夹
@ -145,6 +190,139 @@ paths:
'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: 获取文件夹的内容
@ -324,7 +502,7 @@ components:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
name: X-BookMark-Token
responses:
ServerInternalError:
description: 服务器内部错误
@ -339,6 +517,81 @@ components:
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
@ -349,11 +602,11 @@ components:
maxLength: 255
description: 文件夹名称
example: 测试名称
parent_path_id:
parent_id:
type: integer
format: int64
description: 父文件夹ID 若为空则自动创建在用户根目录下
example: 1
example: null
required:
- name
@ -376,11 +629,11 @@ components:
description:
type: string
description: 书签描述
parent_path_id:
parent_id:
type: integer
format: int64
description: 父文件夹ID 若为空则自动创建在用户根目录下
example: 1
example: null
required:
- name
@ -395,7 +648,7 @@ components:
name:
type: string
description: 文件夹名称
parent_path_id:
parent_id:
type: integer
format: int64
description: 父文件夹ID
@ -416,7 +669,6 @@ components:
required:
- id
- name
- parent_path_id
- created_at
- updated_at
- sub_folder_count
@ -442,7 +694,7 @@ components:
description:
type: string
description: 书签描述
parent_path_id:
parent_id:
type: integer
format: int64
description: 父文件夹ID
@ -457,7 +709,7 @@ components:
required:
- id
- name
- parent_path_id
- parent_id
- created_at
- updated_at

6
config/bookmark_cfg.yaml Normal file
View File

@ -0,0 +1,6 @@
# yaml-language-server: ...
package: api
generate:
gin-server: true
models: true
output: ./gen/bookmarks/gen.go

14
config/model.conf Normal file
View File

@ -0,0 +1,14 @@
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act, eft
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

0
config/user.yaml Normal file
View File

199
config/vfs.yaml Normal file
View File

@ -0,0 +1,199 @@
openapi: '3.0.3'
info:
title: zzyxyz_vfs_api
description: 虚拟文件系统API服务
version: '1.0'
servers:
- url: http://localhost:8080/api
description: 开发环境
- url: https://api.zzyxyz.com/api
description: 生产环境
tags:
- name: vfs
description: 虚拟文件系统相关操作
security:
- ApiKeyAuth: []
paths:
/vfs/v1/files/{path}:
parameters:
- name: path
in: path
required: true
description: 文件系统路径,例如 "documents/readme.txt" 或 "services/mysql.service" 或 "folder/"
schema:
type: string
example: "readme.txt"
get:
summary: 读取文件或列出目录
description: 获取指定路径的文件内容或目录列表
operationId: getVFSNode
tags: [vfs]
parameters:
- name: op
in: query
required: false
description: 操作类型, list表示列出目录内容, 不指定则读取文件内容
schema:
type: string
enum: [list]
responses:
'200':
description: 文件内容或目录列表
content:
application/json:
schema:
oneOf:
- type: string
description: 文件内容
- type: array
items:
$ref: '#/components/schemas/VFSDirectoryEntry'
description: 目录条目列表
'404':
description: 路径不存在
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/ServerInternalError'
post:
summary: 创建文件或目录
description: 创建文件或目录
operationId: createVFSNode
tags: [vfs]
requestBody:
required: true
content:
application/json:
schema:
type: string
description: 文件内容(仅当type为file时有效)
responses:
'201':
description: 创建成功
'400':
description: 请求参数错误
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/ServerInternalError'
put:
summary: 修改文件或修改目录
description: 修改/移动/重命名 已存在的 文件/目录
operationId: updateVFSNode
tags: [vfs]
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
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: 操作成功
'400':
description: 请求参数错误
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/ServerInternalError'
delete:
summary: 删除文件或目录
description: 删除指定路径的文件或目录
operationId: deleteVFSNode
tags: [vfs]
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-VFS-Token
responses:
ServerInternalError:
description: 服务器内部错误
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: 未授权
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
VFSDirectoryEntry:
type: object
properties:
name:
type: string
description: 条目名称
type:
type: string
enum: [file, directory, service]
description: 条目类型
modified:
type: string
format: date-time
description: 修改时间
permissions:
type: string
description: 权限信息,如 "rw"
required:
- name
- type
- modified
Error:
type: object
description: 错误信息
properties:
errtype:
type: string
example: "ParameterError"
description: 错误类型
message:
example: "传递的第一个参数错误"
type: string
description: 错误信息
required:
- errtype
- message

View File

@ -3,4 +3,4 @@ package: api
generate:
gin-server: true
models: true
output: ./gen/api/gen.go
output: ./gen/vfs/gen.go

10
go.mod
View File

@ -14,15 +14,19 @@ require (
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
@ -36,6 +40,7 @@ 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
@ -47,11 +52,16 @@ require (
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/mod v0.28.0 // indirect

37
go.sum
View File

@ -1,13 +1,20 @@
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=
@ -30,6 +37,8 @@ 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=
@ -47,6 +56,7 @@ 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/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=
@ -60,6 +70,9 @@ 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=
@ -88,6 +101,8 @@ 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=
@ -132,10 +147,16 @@ 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=
@ -144,14 +165,21 @@ 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=
@ -159,6 +187,8 @@ 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=
@ -170,6 +200,7 @@ 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=
@ -187,17 +218,22 @@ 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=
@ -209,6 +245,7 @@ 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=

View File

@ -3,11 +3,16 @@
package handlers
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"net/http"
"net/url"
"git.zzyxyz.com/zzy/zzyxyz_go_api/gen/api"
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"
@ -18,13 +23,79 @@ 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 {
if adminToken != nil && apiKey == *adminToken {
return true
}
return false
}
func AuthMiddleware() api.MiddlewareFunc {
return func(c *gin.Context) {
// 检查当前请求是否需要认证
if _, exists := c.Get(api.ApiKeyAuthScopes); exists {
// 提取 API Key
apiKey := c.GetHeader("X-BookMark-Token")
// 验证 API Key您需要实现这个逻辑
if apiKey == "" || !validateApiKey(c.Request.URL, apiKey) {
c.JSON(http.StatusUnauthorized, api.Error{
Errtype: "Unauthorized",
Message: "Invalid or missing API key",
})
c.Abort()
return
}
}
c.Next()
}
}
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为根目录(1)
// 设置默认父文件夹ID为根目录
parentID := real_root_id
if folderID != nil && *folderID != 0 {
parentID = *folderID
@ -47,40 +118,40 @@ func (b *BookMarksImpl) GetFolderDefaultRoot(folderID *int64) (*models.Folder, e
func bookmarkReq2Model(req api.BookmarkRequest, parentID int64) models.Bookmark {
return models.Bookmark{
Name: req.Name,
Link: req.Link,
Detail: req.Detail,
Description: req.Description,
ParentPathID: parentID,
Name: req.Name,
Link: req.Link,
Detail: req.Detail,
Description: req.Description,
ParentID: parentID,
}
}
func bookmarkModel2Res(bookmark models.Bookmark) api.BookmarkResponse {
return api.BookmarkResponse{
Id: bookmark.ID,
Name: bookmark.Name,
Link: bookmark.Link,
Detail: bookmark.Detail,
Description: bookmark.Description,
ParentPathId: bookmark.ParentPathID,
CreatedAt: bookmark.CreatedAt,
Id: bookmark.ID,
Name: bookmark.Name,
Link: bookmark.Link,
Detail: bookmark.Detail,
Description: bookmark.Description,
ParentId: bookmark.ParentID,
CreatedAt: bookmark.CreatedAt,
}
}
func folderReq2Model(req api.FolderRequest, parentID int64) models.Folder {
func folderReq2Model(req api.FolderRequest, parentID *int64) models.Folder {
return models.Folder{
Name: req.Name,
ParentPathID: parentID,
Name: req.Name,
ParentID: parentID,
}
}
func folderModel2Res(folder models.Folder) api.FolderResponse {
return api.FolderResponse{
Id: folder.ID,
Name: folder.Name,
ParentPathId: folder.ParentPathID,
CreatedAt: folder.CreatedAt,
UpdatedAt: folder.UpdatedAt,
Id: folder.ID,
Name: folder.Name,
ParentId: folder.ParentID,
CreatedAt: folder.CreatedAt,
UpdatedAt: folder.UpdatedAt,
}
}
@ -108,9 +179,9 @@ func NewBookMarks(dbPath string) (*BookMarksImpl, error) {
if result.RowsAffected == 0 {
// 根文件夹不存在,创建它
rootFolder = models.Folder{
ID: forlder_root_id,
Name: "Root",
ParentPathID: -1, // 根目录指向自己
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)
@ -133,7 +204,7 @@ func (b *BookMarksImpl) CreateBookmark(c *gin.Context) {
}
var parentID int64
if folder, err := b.GetFolderDefaultRoot(req.ParentPathId); err != nil || folder == nil {
if folder, err := b.GetFolderDefaultRoot(req.ParentId); err != nil || folder == nil {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "NotFoundError",
Message: "Parent folder not found",
@ -170,7 +241,7 @@ func (b *BookMarksImpl) CreateFolder(c *gin.Context) {
}
var parentID int64
if folder, err := b.GetFolderDefaultRoot(req.ParentPathId); err != nil || folder == nil {
if folder, err := b.GetFolderDefaultRoot(req.ParentId); err != nil || folder == nil {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "NotFoundError",
Message: "Parent folder not found",
@ -181,7 +252,7 @@ func (b *BookMarksImpl) CreateFolder(c *gin.Context) {
}
// 创建文件夹
folder := folderReq2Model(req, parentID)
folder := folderReq2Model(req, &parentID)
if err := db.Create(&folder).Error; err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
@ -237,7 +308,7 @@ func (b *BookMarksImpl) DeleteFolder(c *gin.Context, id int64) {
return
}
if folder.ID == folder.ParentPathID {
if folder.ParentID == nil {
c.JSON(http.StatusBadRequest, api.Error{
Errtype: "ParameterError",
Message: "Cannot delete root folder",
@ -375,7 +446,7 @@ func (b *BookMarksImpl) GetFolderInfo(c *gin.Context, id int64) {
response := api.FolderResponse{
Id: folder.ID,
Name: folder.Name,
ParentPathId: folder.ParentPathID,
ParentId: folder.ParentID,
CreatedAt: folder.CreatedAt,
UpdatedAt: folder.UpdatedAt,
SubFolderCount: int(subFolderCount),
@ -424,16 +495,16 @@ func (b *BookMarksImpl) UpdateBookmark(c *gin.Context, id int64) {
}
// 更新父文件夹ID如果提供且有效
if req.ParentPathId != nil && *req.ParentPathId != 0 {
if req.ParentId != nil && *req.ParentId != 0 {
var parentFolder models.Folder
if err := db.First(&parentFolder, *req.ParentPathId).Error; err != nil {
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.ParentPathID = *req.ParentPathId
bookmark.ParentID = *req.ParentId
}
// 保存更新
@ -480,9 +551,9 @@ func (b *BookMarksImpl) UpdateFolder(c *gin.Context, id int64) {
}
// 更新父文件夹ID如果提供且有效
if req.ParentPathId != nil && *req.ParentPathId != 0 {
if req.ParentId != nil && *req.ParentId != 0 {
// 不能将文件夹设置为自己的子文件夹
if *req.ParentPathId == id {
if *req.ParentId == id {
c.JSON(http.StatusBadRequest, api.Error{
Errtype: "ParameterError",
Message: "Cannot set folder as its own parent",
@ -491,14 +562,14 @@ func (b *BookMarksImpl) UpdateFolder(c *gin.Context, id int64) {
}
var parentFolder models.Folder
if err := db.First(&parentFolder, *req.ParentPathId).Error; err != nil {
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.ParentPathID = *req.ParentPathId
folder.ParentID = req.ParentId
}
// 保存更新
@ -523,5 +594,15 @@ func (b *BookMarksImpl) UpdateFolder(c *gin.Context, id int64) {
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)

View File

@ -9,7 +9,7 @@ import (
"net/http/httptest"
"testing"
"git.zzyxyz.com/zzy/zzyxyz_go_api/gen/api"
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"
@ -68,7 +68,7 @@ func (suite *BookmarkTestSuite) TestCreateBookmark() {
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.ParentPathId) // 默认根目录
assert.Equal(suite.T(), int64(1), response.ParentId) // 默认根目录
}
func (suite *BookmarkTestSuite) TestGetBookmark() {
@ -130,7 +130,7 @@ func (suite *BookmarkTestSuite) TestCreateFolder() {
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.ParentPathId) // 默认根目录
assert.Equal(suite.T(), int64(1), response.ParentId) // 默认根目录
}
func (suite *BookmarkTestSuite) TestGetFolderInfo() {
@ -188,11 +188,11 @@ func (suite *BookmarkTestSuite) TestGetFolderContent() {
description := "Test bookmark in folder"
link := "https://example.com/folder"
bookmarkRequest := api.BookmarkRequest{
Name: "Folder Bookmark",
Detail: &detail,
Description: &description,
Link: &link,
ParentPathId: &createdFolder.Id,
Name: "Folder Bookmark",
Detail: &detail,
Description: &description,
Link: &link,
ParentId: &createdFolder.Id,
}
bookmarkJson, _ := json.Marshal(bookmarkRequest)

1
internal/handlers/vfs.go Normal file
View File

@ -0,0 +1 @@
package handlers

View File

@ -8,25 +8,25 @@ import (
// Bookmark 书签结构体
type Bookmark struct {
ID int64 `json:"id" gorm:"primaryKey"`
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"`
ParentPathID int64 `json:"parent_path_id" gorm:"index;not null;default:1"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
ID int64 `json:"id" gorm:"primaryKey"`
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"`
ParentPathID int64 `json:"parent_path_id" gorm:"index;not null;default:1"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
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是否有效

View File

@ -1 +1,15 @@
package models
type PasswordUser struct {
ID int64
Username string
Password string
BookMarkUserID int64
}
type BookMarkUser struct {
ID int64
RootFolderID int64
Token string
Data *string
}

20
main.go
View File

@ -5,15 +5,15 @@ import (
"log"
"net/http"
"git.zzyxyz.com/zzy/zzyxyz_go_api/gen/api"
bookmarks "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/handlers"
"github.com/gin-gonic/gin"
)
//go:generate go tool oapi-codegen -config config/cfg.yaml config/api.yaml
//go:embed config/api.yaml dist/*
//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:embed config/* dist/*
var staticFiles embed.FS
func main() {
@ -30,10 +30,12 @@ func main() {
{
// create a type that satisfies the `api.ServerInterface`,
// which contains an implementation of every operation from the generated code
if server, err := handlers.NewBookMarks("user.sqlite3"); err != nil {
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 {
api.RegisterHandlers(api_router, server)
bookmarks.RegisterHandlersWithOptions(api_router, server, *permission)
}
handlers.TodoHandler(api_router)
@ -41,9 +43,9 @@ func main() {
// FIXME 可能有更好的方式实现这个代码
// 提供嵌入的静态文件访问 - OpenAPI YAML 文件和 dist 目录
router.GET("/swagger.yaml", func(c *gin.Context) {
file, _ := staticFiles.ReadFile("config/api.yaml")
c.Data(http.StatusOK, "application/x-yaml", file)
router.GET("/config/*filepath", func(ctx *gin.Context) {
fs := http.FileServer(http.FS(staticFiles))
fs.ServeHTTP(ctx.Writer, ctx.Request)
})
router.GET("/swagger/*filepath", func(ctx *gin.Context) {
// 直接修改请求路径实现映射