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

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)