refactor(bookmark): 重构书签服务入口文件并整合用户权限功能

将 bookmark.go 重命名为 main.go,并调整包引用路径。将 bookmarks 和 user_np
两个模块的处理逻辑合并到同一个服务中,统一注册路由。同时更新了相关 API
的引用路径,确保生成代码与内部实现正确绑定。

此外,移除了独立的 user_np 服务入口文件,其功能已整合至 bookmark 服务中。

配置文件中调整了 user_np 和 vfs 服务的端口及部分接口定义,完善了用户
相关操作的路径参数和请求体结构。
This commit is contained in:
zzy
2025-09-25 09:50:35 +08:00
parent 1e81e603de
commit 24f238f377
23 changed files with 1173 additions and 601 deletions

View File

@ -1,12 +1,12 @@
// internal/handlers/note_link.go
package handlers
package bookmarks
import (
"net/http"
api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/bookmarks"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/bookmarks/models"
"github.com/gin-gonic/gin"
_ "github.com/mattn/go-sqlite3"
"gorm.io/driver/sqlite"
@ -34,7 +34,6 @@ func AuthMiddleware() api.MiddlewareFunc {
// 提取 API Key
apiKey := c.GetHeader("X-BookMark-Token")
return
// 验证 API Key您需要实现这个逻辑
if apiKey == "" || !validateApiKey(apiKey) {
c.JSON(http.StatusUnauthorized, api.Error{
@ -55,26 +54,6 @@ func NewBookMarkPermission() (*api.GinServerOptions, error) {
}, nil
}
func bookmarkReq2Model(req api.BookmarkRequest) models.Bookmark {
return models.Bookmark{
Name: req.Name,
Link: req.Link,
Detail: req.Detail,
Description: req.Description,
}
}
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,
CreatedAt: bookmark.CreatedAt,
}
}
func NewBookMarks(dbPath string) (*BookMarksImpl, error) {
var err error
var db *gorm.DB
@ -236,5 +215,25 @@ func (b *BookMarksImpl) UpdateBookmark(c *gin.Context, id int64) {
c.JSON(http.StatusOK, response)
}
func bookmarkReq2Model(req api.BookmarkRequest) models.Bookmark {
return models.Bookmark{
Name: req.Name,
Link: req.Link,
Detail: req.Detail,
Description: req.Description,
}
}
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,
CreatedAt: bookmark.CreatedAt,
}
}
// Make sure we conform to ServerInterface
var _ api.ServerInterface = (*BookMarksImpl)(nil)

View File

@ -0,0 +1,4 @@
package bookmarks
type Config struct {
}

View File

@ -0,0 +1,38 @@
// internal/models/user_np.go
package models
import (
"time"
"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"`
}
// 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
}

View File

@ -1,19 +1,24 @@
// internal/handlers/user_np.go
package handlers
package bookmarks
import (
"context"
"fmt"
"net/http"
api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/user_np"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models"
client "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/bookmarks/models"
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type UserNPImpl struct {
db *gorm.DB
db *gorm.DB
client *client.ClientWithResponses
vfsToken string
}
func NewUserNP(dbPath string) (*UserNPImpl, error) {
@ -29,8 +34,62 @@ func NewUserNP(dbPath string) (*UserNPImpl, error) {
if err != nil {
return nil, err
}
client, err := client.NewClientWithResponses("http://localhost:8080/api")
if err != nil {
return nil, err
}
return &UserNPImpl{db: db}, nil
return &UserNPImpl{
db: db,
client: client,
vfsToken: "random_token",
}, nil
}
func (u *UserNPImpl) RegisterVFSService(username, token string) (*string, error) {
ctx := context.Background()
reqs, err := u.client.CreateUserWithResponse(ctx, username, func(ctx context.Context, req *http.Request) error {
req.Header.Set("X-VFS-Token", token)
return nil
})
if err != nil {
return nil, err
}
if reqs.StatusCode() != http.StatusCreated {
return nil, fmt.Errorf("failed to register vfs service: %s", reqs.Status())
}
tokenHeader := reqs.HTTPResponse.Header.Get("X-VFS-Token")
if tokenHeader == "" {
return nil, fmt.Errorf("X-VFS-Token header is missing")
}
return &tokenHeader, nil
}
func (u *UserNPImpl) UnregisterVFSService(username, token string) error {
ctx := context.Background()
reqs, err := u.client.DeleteUserWithResponse(ctx, username, func(ctx context.Context, req *http.Request) error {
req.Header.Set("X-VFS-Token", token)
return nil
})
if err != nil {
return err
}
if reqs.StatusCode() == http.StatusNoContent {
return nil
}
if reqs.JSON404 != nil {
return fmt.Errorf("用户不存在 %s", reqs.JSON404.Message)
}
if reqs.JSON500 != nil {
return fmt.Errorf("服务器错误 %s", reqs.JSON500.Message)
}
return fmt.Errorf("未知错误")
}
// PostAuthLogin 用户登录
@ -54,19 +113,8 @@ func (u *UserNPImpl) PostAuthLogin(c *gin.Context) {
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,
Token: user.Token,
UserId: &user.ID,
})
}
@ -101,7 +149,14 @@ func (u *UserNPImpl) PostAuthRegister(c *gin.Context) {
// 保存到数据库
if err := u.db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "用户创建失败"})
return
}
if token, err := u.RegisterVFSService(req.Username, u.vfsToken); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "无法生成访问令牌"})
u.db.Delete(&user)
} else {
user.Token = token
u.db.Save(&user)
}
c.JSON(http.StatusCreated, nil)
@ -116,13 +171,6 @@ func (u *UserNPImpl) PutAuthPassword(c *gin.Context) {
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()})
@ -131,7 +179,7 @@ func (u *UserNPImpl) PutAuthPassword(c *gin.Context) {
// 查找用户
var user models.UserNP
if err := u.db.Where("username = ?", username).First(&user).Error; err != nil {
if err := u.db.Where("username = ?", req.UserName).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户不存在"})
return
}
@ -154,17 +202,6 @@ func (u *UserNPImpl) PutAuthPassword(c *gin.Context) {
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)
}

View File

@ -1,354 +0,0 @@
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
}
}

View File

@ -1,100 +0,0 @@
// 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
}

View File

@ -2,8 +2,10 @@
package models
import (
"crypto/rand"
"database/sql"
"errors"
"fmt"
"path"
"time"
@ -32,6 +34,11 @@ type VfsNode struct {
UpdatedAt time.Time
}
type VfsUser struct {
Name string
Token string
}
type VfsDirEntry struct {
ID uint64
Name string
@ -45,7 +52,6 @@ func NewVfs(dbPath string) (*Vfs, error) {
return nil, err
}
// 创建表
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS vfs_nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -60,9 +66,146 @@ func NewVfs(dbPath string) (*Vfs, error) {
return nil, err
}
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS vfs_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
token TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`)
if err != nil {
return nil, err
}
err = createInitialDirectories(db)
if err != nil {
return nil, err
}
return &Vfs{DB: db}, nil
}
// 创建初始目录结构
func createInitialDirectories(db *sql.DB) error {
var err error
// 创建 /home 目录
_, err = db.Exec(`
INSERT OR IGNORE INTO vfs_nodes (name, parent_id, type, created_at, updated_at)
VALUES ('home', 0, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
VfsNodeTypeDirectory)
if err != nil {
return err
}
// 创建 /public 目录
_, err = db.Exec(`
INSERT OR IGNORE INTO vfs_nodes (name, parent_id, type, created_at, updated_at)
VALUES ('public', 0, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
VfsNodeTypeDirectory)
if err != nil {
return err
}
return nil
}
func generateToken() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
// fallback to time-based token
return fmt.Sprintf("%x", time.Now().UnixNano())
}
return fmt.Sprintf("%x", bytes)
}
// 添加用户相关操作
func (v *Vfs) CreateUser(username string) (string, error) {
// 生成随机token
token := generateToken()
// 检查用户是否已存在
_, err := v.GetUserByName(username)
if err == nil {
return "", errors.New("user already exists")
}
// 插入用户(不使用事务)
_, err = v.DB.Exec("INSERT INTO vfs_users (name, token) VALUES (?, ?)", username, token)
if err != nil {
return "", err
}
// 使用 defer 确保出错时能清理已创建的用户
defer func() {
if err != nil {
v.DB.Exec("DELETE FROM vfs_users WHERE name = ? AND token = ?", username, token)
}
}()
// 确保 /home 目录存在
homeDir, getHomeErr := v.GetNodeByPath("/home/")
if getHomeErr != nil {
// 如果 /home 不存在,创建它
homeDir = &VfsNode{
Name: "home",
ParentID: 0,
Type: VfsNodeTypeDirectory,
}
if createHomeErr := v.CreateVFSNode(homeDir); createHomeErr != nil {
err = createHomeErr
return "", err
}
}
// 创建用户目录 /home/username
userDir := &VfsNode{
Name: username,
ParentID: homeDir.ID,
Type: VfsNodeTypeDirectory,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if createDirErr := v.CreateVFSNode(userDir); createDirErr != nil {
err = createDirErr
return "", err
}
return token, nil
}
func (v *Vfs) DeleteUser(username string) error {
// TODO: 递归删除用户相关文件
// 这里暂时只删除用户记录
_, err := v.DB.Exec("DELETE FROM vfs_users WHERE name = ?", username)
return err
}
func (v *Vfs) GetUserByToken(token string) (*VfsUser, error) {
user := &VfsUser{}
err := v.DB.QueryRow("SELECT name, token FROM vfs_users WHERE token = ?", token).
Scan(&user.Name, &user.Token)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.New("user not found")
}
return nil, err
}
return user, nil
}
func (v *Vfs) GetUserByName(username string) (*VfsUser, error) {
user := &VfsUser{}
err := v.DB.QueryRow("SELECT name, token FROM vfs_users WHERE name = ?", username).
Scan(&user.Name, &user.Token)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.New("user not found")
}
return nil, err
}
return user, 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)
@ -253,6 +396,47 @@ func ParsePathComponents(pathStr string) (parentPath, nodeName string, nodeType
return parentPath, nodeName, nodeType, nil
}
// MoveToPath 将节点移动到指定路径
func (v *Vfs) MoveToPath(node *VfsNode, destPath string) error {
// 1. 解析目标路径
parentPath, nodeName, _, err := ParsePathComponents(destPath)
if err != nil {
return fmt.Errorf("invalid destination path: %w", err)
}
// 2. 查找目标父节点
parentID, err := v.GetParentID(parentPath)
if err != nil {
return fmt.Errorf("failed to find parent directory '%s': %w", parentPath, err)
}
// 3. 检查目标位置是否已存在同名节点
_, err = v.GetNodeByParentIDAndName(parentID, nodeName)
if err == nil {
return fmt.Errorf("destination path '%s' already exists", destPath)
}
if err.Error() != "node not found" {
return fmt.Errorf("error checking destination path: %w", err)
}
// 4. 更新节点的父节点ID和名称
node.ParentID = parentID
node.Name = nodeName
node.UpdatedAt = time.Now()
// 5. 更新数据库中的节点信息
_, err = v.DB.Exec(`
UPDATE vfs_nodes
SET name = ?, parent_id = ?, updated_at = ?
WHERE id = ?`,
node.Name, node.ParentID, node.UpdatedAt, node.ID)
if err != nil {
return fmt.Errorf("failed to update node: %w", err)
}
return nil
}
func (v *Vfs) CreateVFSNode(p *VfsNode) error {
_, err := v.DB.Exec(`
INSERT INTO vfs_nodes (name, parent_id, type, created_at, updated_at)

View File

@ -3,7 +3,7 @@ package models_test
import (
"testing"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/models"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs/models"
)
func TestParsePathComponents(t *testing.T) {

360
internal/vfs/vfs.go Normal file
View File

@ -0,0 +1,360 @@
package vfs
import (
"log"
"net/http"
"os"
"path/filepath"
"sync"
api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs/models"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
fileadapter "github.com/casbin/casbin/v2/persist/file-adapter"
"github.com/gin-gonic/gin"
)
type VfsImpl struct {
vfs *models.Vfs
enfocer *casbin.Enforcer
config VFSConfig
proxyTable []*ProxyEntry // 动态代理表
proxyMutex sync.RWMutex // 保护代理表的读写锁
}
func NewVfsHandler(config *Config) (*VfsImpl, error) {
var err error
vfs, err := models.NewVfs(config.VFS.DbPath)
if err != nil {
return nil, err
}
var policyPath = config.VFS.PolicyPath
// 检查策略文件是否存在,如果不存在则创建
if _, err := os.Stat(policyPath); os.IsNotExist(err) {
// 确保目录存在
dir := filepath.Dir(policyPath)
if err := os.MkdirAll(dir, 0755); err != nil {
log.Fatalf("error: failed to create policy directory: %s", err)
}
// 创建空的策略文件
file, err := os.Create(policyPath)
if err != nil {
log.Fatalf("error: failed to create policy file: %s", err)
}
file.Close()
log.Printf("Created policy file: %s", policyPath)
}
a := fileadapter.NewAdapter(policyPath)
if a == nil {
log.Fatalf("error: adapter: %s", err)
}
m, err := model.NewModelFromString(CasbinModel)
if err != nil {
log.Fatalf("error: model: %s", err)
}
e, err := casbin.NewEnforcer(m, a)
if err != nil {
return nil, err
}
log.Printf("Admin Token: %s", config.VFS.AdminToken)
log.Printf("Register Token: %s", config.VFS.RegisterToken)
return &VfsImpl{
vfs: vfs,
enfocer: e,
config: config.VFS,
proxyTable: make([]*ProxyEntry, 0),
proxyMutex: sync.RWMutex{},
}, 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
}
// CreateVFSNode implements api.ServerInterface.
func (v *VfsImpl) CreateVFSNode(c *gin.Context, params api.CreateVFSNodeParams) {
if !v.CheckPermissionMiddleware(c, params.Path) {
return
}
// 解析路径组件
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) {
// Rollback
err := v.vfs.DeleteVFSNode(node)
if err != nil {
// FIXME: 需要解决这种原子性
panic("Maybe Consistency Error")
}
return
}
}
// 返回创建成功的节点
c.JSON(http.StatusCreated, api.VFSNodeResponse{
Name: node.Name,
Type: ModelType2ResponseType(node.Type),
CreatedAt: node.CreatedAt,
UpdatedAt: node.UpdatedAt,
})
}
// GetVFSNode implements api.ServerInterface.
func (v *VfsImpl) GetVFSNode(c *gin.Context, params api.GetVFSNodeParams) {
if !v.CheckPermissionMiddleware(c, params.Path) {
return
}
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 valid node type",
})
}
}
// DeleteVFSNode implements api.ServerInterface.
func (v *VfsImpl) DeleteVFSNode(c *gin.Context, params api.DeleteVFSNodeParams) {
if !v.CheckPermissionMiddleware(c, params.Path) {
return
}
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.VfsNodeTypeService:
if !v.Proxy2Service(c, node) {
return
}
case models.VfsNodeTypeDirectory:
if children, err := v.vfs.GetChildren(node.ID); err != nil || len(children) != 0 {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "error",
Message: "the folder is not empty",
})
return
}
default:
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "error",
Message: "node type not supported",
})
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)
}
// UpdateVFSNode implements api.ServerInterface.
func (v *VfsImpl) UpdateVFSNode(c *gin.Context, params api.UpdateVFSNodeParams) {
if !v.CheckPermissionMiddleware(c, params.Path) {
return
}
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
}
switch params.Op {
case api.Rename:
if req == "" {
c.JSON(http.StatusBadRequest, api.Error{
Errtype: "ParameterError",
Message: "Invalid request parameters",
})
return
}
// FIXME: 对于service,后缀属性需要强制保留
if err := v.vfs.UpdateVFSNode(node); err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "error",
Message: err.Error(),
})
return
}
case api.Change:
if node.Type != models.VfsNodeTypeFile {
c.JSON(http.StatusBadRequest, api.Error{
Errtype: "ParameterError",
Message: "Invalid request parameters, node type must be a service",
})
return
}
if !v.Proxy2Service(c, node) {
return
}
case api.Move:
// FIXME: 需要添加权限控制
v.vfs.MoveToPath(node, req)
case api.Copy:
fallthrough
default:
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "error",
Message: "op type not supported",
})
return
}
}
// CreateUser implements api.ServerInterface.
func (v *VfsImpl) CreateUser(c *gin.Context, username string) {
token := c.GetHeader("X-VFS-Token")
if token != v.config.RegisterToken || token != v.config.AdminToken {
c.JSON(http.StatusForbidden, api.Error{
Errtype: "AccessDenied",
Message: "Access denied",
})
return
}
token, err := v.vfs.CreateUser(username)
if err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "CreateUserError",
Message: err.Error(),
})
return
}
_, err = v.enfocer.AddRoleForUser(username, "user")
if err != nil {
log.Printf("Failed to add role for user %s: %v", username, err)
}
v.enfocer.SavePolicy()
// 根据API文档token应该通过响应头返回
c.Header("X-VFS-Token", token)
c.Status(http.StatusCreated)
}
// DeleteUser implements api.ServerInterface.
func (v *VfsImpl) DeleteUser(c *gin.Context, username string) {
token := c.GetHeader("X-VFS-Token")
user, err := v.vfs.GetUserByToken(token)
if err != nil || user.Name != username {
c.JSON(http.StatusForbidden, api.Error{
Errtype: "AccessDenied",
Message: "Access denied",
})
return
}
err = v.vfs.DeleteUser(username)
if err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, api.Error{
Errtype: "UserNotFoundError",
Message: "User not found",
})
return
}
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "DeleteUserError",
Message: err.Error(),
})
return
}
// 根据API文档删除成功返回204状态码
c.Status(http.StatusNoContent)
}
// Make sure we conform to ServerInterface
var _ api.ServerInterface = (*VfsImpl)(nil)

89
internal/vfs/vfs_auth.go Normal file
View File

@ -0,0 +1,89 @@
package vfs
import (
_ "embed"
"net/http"
"strings"
api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs/models"
"github.com/gin-gonic/gin"
)
//go:embed vfs_model.conf
var CasbinModel string
func NewVfsPermission() (*api.GinServerOptions, error) {
return &api.GinServerOptions{
Middlewares: []api.MiddlewareFunc{VfsMiddleware()},
}, nil
}
func (v *VfsImpl) CheckPermission(token, path, action string) (bool, error) {
// 根据 token 获取用户信息
user, err := v.vfs.GetUserByToken(token)
if err != nil {
// 匿名用户
user = &models.VfsUser{Name: "", Token: ""}
}
// 特殊处理admin 用户拥有所有权限
if token == v.config.AdminToken && len(token) != 0 { // admin 用户拥有所有权限
return true, nil
}
// 允许任何人读取 public 目录
if strings.HasPrefix(path, "/public") && action == "GET" {
return true, nil
}
// 如果是普通用户访问自己的主目录,则允许
if user.Name != "" && strings.HasPrefix(path, "/home/"+user.Name) {
return true, nil
}
// 构造 Casbin 请求
// 对于普通用户,需要将策略中的 {{username}} 替换为实际用户名
obj := path
sub := user.Name
// 使用 Casbin 检查权限
allowed, err := v.enfocer.Enforce(sub, obj, action)
if err != nil {
return false, err
}
return allowed, nil
}
func (v *VfsImpl) CheckPermissionMiddleware(c *gin.Context, path string) bool {
token := c.GetHeader("X-VFS-Token")
allowed, err := v.CheckPermission(token, path, c.Request.Method)
// log.Println("CheckPermission:", allowed, err)
if err != nil {
c.JSON(http.StatusInternalServerError, api.Error{
Errtype: "PermissionCheckError",
Message: "Failed to check permission: " + err.Error(),
})
return false
}
if !allowed {
c.JSON(http.StatusForbidden, api.Error{
Errtype: "AccessDenied",
Message: "Access denied",
})
return false
}
return true
}
func VfsMiddleware() api.MiddlewareFunc {
return func(c *gin.Context) {
// 检查当前请求是否需要认证
if _, exists := c.Get(api.ApiKeyAuthScopes); exists {
// 提取 API Key
apiKey := c.GetHeader("X-VFS-Token")
c.Set(api.ApiKeyAuthScopes, apiKey)
}
c.Next()
}
}

View File

@ -0,0 +1,75 @@
// internal/handlers/vfs/vfs_config.go
package vfs
import (
"fmt"
"log"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
VFS VFSConfig `mapstructure:"vfs"`
Services ServicesConfig `mapstructure:"services"`
}
type ServerConfig struct {
Address string `mapstructure:"address"`
Mode string `mapstructure:"mode"`
}
type VFSConfig struct {
DbPath string `mapstructure:"db_path"`
PolicyPath string `mapstructure:"policy_path"`
AdminToken string `mapstructure:"admin_token"`
RegisterToken string `mapstructure:"regiser_token"`
Debug bool `mapstructure:"debug"`
}
type ServicesConfig struct {
Bookmark BookmarkServiceConfig `mapstructure:"bookmark"`
}
type BookmarkServiceConfig struct {
URL string `mapstructure:"url"`
}
func genrateRandomToken(length int) string {
// 生成随机字符串
return "random_token"
}
// LoadConfig 加载配置
func LoadConfig() (*Config, error) {
viper.SetConfigName("vfs_config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")
viper.AddConfigPath("./config")
viper.AddConfigPath("config")
// 设置默认值
viper.SetDefault("server.address", "localhost:8080")
viper.SetDefault("server.mode", "release")
viper.SetDefault("vfs.db_path", "./data/vfs.sqlite3")
viper.SetDefault("vfs.policy_path", "./data/policy.csv")
viper.SetDefault("vfs.debug", false)
viper.SetDefault("services.bookmark.url", "http://localhost:8081/api")
viper.SetDefault("vfs.admin_token", genrateRandomToken(64))
viper.SetDefault("vfs.regiser_token", genrateRandomToken(64))
// 自动读取环境变量
viper.AutomaticEnv()
// 读取配置文件
if err := viper.ReadInConfig(); err != nil {
log.Printf("Warning: unable to read config file: %v. Using defaults and environment variables.", err)
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("unable to decode into struct: %v", err)
}
return &config, nil
}

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

155
internal/vfs/vfs_service.go Normal file
View File

@ -0,0 +1,155 @@
package vfs
import (
"net/http"
"strings"
api "git.zzyxyz.com/zzy/zzyxyz_go_api/gen/vfs"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs/models"
"github.com/gin-gonic/gin"
)
// 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
MatchExt string
Proxy ServiceProxy // 对应的代理实现
}
// 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.MatchExt == 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
}
}

View File

@ -8,8 +8,8 @@ import (
"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"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs"
"git.zzyxyz.com/zzy/zzyxyz_go_api/internal/vfs/models"
"github.com/gin-gonic/gin"
)
@ -17,15 +17,18 @@ type VfsBookMarkService struct {
client *api.ClientWithResponses
}
func NewVfsBookMarkService(serverURL string) (*VfsBookMarkService, error) {
func NewVfsBookMarkService(serverURL string) (*vfs.ProxyEntry, error) {
client, err := api.NewClientWithResponses(serverURL)
if err != nil {
return nil, err
}
return &VfsBookMarkService{
client: client,
}, nil
ret := vfs.ProxyEntry{
Name: "bookmark",
MatchExt: "bk",
Proxy: &VfsBookMarkService{client: client},
}
return &ret, nil
}
// Create implements ServiceProxy.
@ -157,4 +160,4 @@ func (v *VfsBookMarkService) Update(c *gin.Context, servicePath string, node *mo
return fmt.Errorf("unknown error")
}
var _ handlers.ServiceProxy = (*VfsBookMarkService)(nil)
var _ vfs.ServiceProxy = (*VfsBookMarkService)(nil)