```
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:
@ -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)
|
||||
|
@ -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
1
internal/handlers/vfs.go
Normal file
@ -0,0 +1 @@
|
||||
package handlers
|
Reference in New Issue
Block a user