feat(vfs): 增强虚拟文件系统删除和查询功能

- 新增递归删除目录及子项的功能,支持 recursive 和 force 操作模式
- 支持通过路径获取文件或目录内容,并完善参数说明和示例
- 完善 VFS API 文档,包括操作模式、权限描述和响应内容
- 优化 GetVFSNode 方法,改为通过 ID 查询节点信息
- 修复部分路径和权限检查的逻辑注释
This commit is contained in:
zzy
2025-09-27 22:23:30 +08:00
parent cf47ef66c3
commit a3033bbf67
5 changed files with 178 additions and 38 deletions

View File

@ -24,6 +24,7 @@ paths:
schema:
type: integer
format: int64
post:
summary: 创建书签
description: 在文件夹下创建一个书签
@ -79,7 +80,7 @@ paths:
$ref: '#/components/responses/Forbidden'
'500':
$ref: '#/components/responses/ServerInternalError'
put:
summary: 更新书签
description: 更新指定id的书签

View File

@ -88,7 +88,7 @@ paths:
description: 请求参数错误
'401':
description: 认证失败
delete:
summary: 删除用户
description: 删除用户
@ -100,7 +100,7 @@ paths:
description: 用户注销成功
'401':
description: 认证失败
/auth/user/{username}/info:
parameters:
- name: username
@ -175,7 +175,7 @@ components:
type: string
email:
type: string
ChangePasswordRequest:
type: object
required:

View File

@ -73,13 +73,17 @@ paths:
/vfs/v1/files:
parameters:
- name: path
in: query
required: true
description: 文件系统路径,例如 "documents/readme.txt" 或 "services/mysql.service" 或 "folder/"
schema:
type: string
example: "readme.txt"
- name: path
in: query
required: true
description: |
文件系统路径,例如:
- "/documents/readme.txt" (文件路径)
- "/services/sql.bk.api" (服务文件)
- "/folder/" (目录路径,以/结尾)
schema:
type: string
example: "/documents/readme.txt"
get:
summary: 读取文件或列出目录
@ -129,6 +133,7 @@ paths:
text/plain:
schema:
type: string
description: 文件内容, 或者服务内容
responses:
'201':
description: 创建成功
@ -158,11 +163,17 @@ paths:
parameters:
- name: op
in: query
description: 操作模式
description: |
更新操作模式:
- move: 移动文件或目录到新位置
- rename: 重命名文件或目录
- change: 修改文件内容或目录属性
- copy: 复制文件或目录到新位置
required: true
schema:
type: string
enum: [move, rename, change, copy]
example: "rename"
requestBody:
required: true
content:
@ -197,6 +208,19 @@ paths:
description: 删除指定路径的文件或目录
operationId: deleteVFSNode
tags: [vfs]
parameters:
- name: op
in: query
description: |
删除操作模式:
- recursive: 递归删除目录及其所有内容
- force: 强制删除,忽略只读等保护属性
不指定时执行普通删除操作
required: false
schema:
type: string
enum: [recursive, force]
example: "recursive"
responses:
'204':
description: 删除成功
@ -307,11 +331,10 @@ components:
$ref: '#/components/schemas/VFSNodeType'
permissions:
type: string
description: 权限信息,如 "rw"
description: 权限信息,如 "rwo" (读 写 拥有)
required:
- name
- type
- modified
Error:
type: object
@ -349,4 +372,4 @@ components:
example: "传递的第一个参数错误"
required:
- errtype
- message
- message

View File

@ -227,6 +227,99 @@ func (v *Vfs) GetChildren(parentID uint64) ([]VfsDirEntry, error) {
return dirEntrys, nil
}
func (v *Vfs) GetChildrenID(parentID uint64, recursive bool) (ids []uint64, services []uint64, err error) {
if recursive {
// 递归获取所有子项ID
err := v.getChildrenIDRecursive(parentID, &ids, &services)
if err != nil {
return nil, nil, err
}
} else {
// 只获取直接子项ID
rows, err := v.DB.Query("SELECT id FROM vfs_nodes WHERE parent_id = ?", parentID)
if err != nil {
return nil, nil, err
}
defer rows.Close()
for rows.Next() {
var id uint64
if err := rows.Scan(&id); err != nil {
return nil, nil, err
}
ids = append(ids, id)
}
}
return ids, services, nil
}
// DeleteNodeRecursively 递归删除节点及其所有子项
func (v *Vfs) DeleteNodeRecursively(nodeID uint64) ([]uint64, error) {
// 获取所有需要删除的ID包括自己和所有子项
idsToDelete := []uint64{nodeID}
// 获取所有子项ID
childIDs, sericeIDs, err := v.GetChildrenID(nodeID, true)
if err != nil {
return nil, err
}
idsToDelete = append(idsToDelete, childIDs...)
// 构建删除语句
if len(idsToDelete) == 0 {
return nil, nil
}
// 创建占位符
placeholders := make([]string, len(idsToDelete))
args := make([]any, len(idsToDelete))
for i, id := range idsToDelete {
placeholders[i] = "?"
args[i] = id
}
query := fmt.Sprintf("DELETE FROM vfs_nodes WHERE id IN (%s)", strings.Join(placeholders, ","))
_, err = v.DB.Exec(query, args...)
return sericeIDs, err
}
// getChildrenIDRecursive 递归获取所有子项ID的辅助函数
func (v *Vfs) getChildrenIDRecursive(parentID uint64, ids *[]uint64, services *[]uint64) error {
// 获取当前层级的子项
rows, err := v.DB.Query("SELECT id, type FROM vfs_nodes WHERE parent_id = ?", parentID)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id uint64
var nodeType VfsNodeType
if err := rows.Scan(&id, &nodeType); err != nil {
return err
}
// 将当前节点ID添加到列表中
*ids = append(*ids, id)
// 如果是目录,递归获取其子项
switch nodeType {
case VfsNodeTypeDirectory:
err := v.getChildrenIDRecursive(id, ids, services)
if err != nil {
return err
}
case VfsNodeTypeService:
*services = append(*services, id)
}
}
return nil
}
// GetParentID 根据父路径查找父节点 ID
// parentPath 应该是 ParsePathComponents 的第一个返回值
func (v *Vfs) GetParentID(parentPath string) (uint64, error) {
@ -400,6 +493,7 @@ func ParsePathComponents(pathStr string) (parentPath, nodeName string, nodeType
// MoveToPath 将节点移动到指定路径
func (v *Vfs) MoveToPath(node *VfsNode, destPath string) error {
// FIXME: 路径和权限的检查
// 1. 解析目标路径
parentPath, nodeName, _, err := ParsePathComponents(destPath)
if err != nil {
@ -459,12 +553,12 @@ func (v *Vfs) DeleteVFSNode(p *VfsNode) error {
return err
}
func (v *Vfs) GetVFSNode(p *VfsNode) *VfsNode {
func (v *Vfs) GetVFSNode(ID uint64) *VfsNode {
node := &VfsNode{}
err := v.DB.QueryRow(`
SELECT id, name, parent_id, type, created_at, updated_at
FROM vfs_nodes
WHERE id = ?`, p.ID).Scan(
WHERE id = ?`, ID).Scan(
&node.ID, &node.Name, &node.ParentID, &node.Type, &node.CreatedAt, &node.UpdatedAt)
if err != nil {

View File

@ -99,29 +99,51 @@ func (v *VfsImpl) DeleteVFSNode(ctx context.Context, request api.DeleteVFSNodeRe
}, nil
}
case models.VfsNodeTypeDirectory:
// 检查目录是否为空
children, err := v.vfs.GetChildren(node.ID)
if err != nil {
return api.DeleteVFSNode500JSONResponse{
ServerInternalErrorJSONResponse: api.ServerInternalErrorJSONResponse{
Errtype: api.ErrorErrtypeInternalServerError,
Message: "Failed to get directory children: " + err.Error(),
},
}, nil
}
if len(children) != 0 {
return api.DeleteVFSNode400JSONResponse{
ParameterErrorJSONResponse: api.ParameterErrorJSONResponse{
Errtype: api.ErrorErrtypeConflictError,
Message: "Directory is not empty",
},
}, nil
if request.Params.Op != nil && *request.Params.Op == api.Recursive {
if sericeIDs, err := v.vfs.DeleteNodeRecursively(node.ID); err != nil {
return api.DeleteVFSNode500JSONResponse{
ServerInternalErrorJSONResponse: api.ServerInternalErrorJSONResponse{
Errtype: api.ErrorErrtypeInternalServerError,
Message: "Failed to delete node: " + err.Error(),
},
}, nil
} else {
for _, serviceID := range sericeIDs {
// 对于服务类型节点,通过代理删除
_, err := v.StrictProxy2Service(http.MethodDelete, nil, v.vfs.GetVFSNode(serviceID))
if err != nil {
return api.DeleteVFSNode500JSONResponse{
ServerInternalErrorJSONResponse: api.ServerInternalErrorJSONResponse{
Errtype: api.ErrorErrtypeServiceProxyError,
Message: err.Error(),
},
}, nil
}
}
return api.DeleteVFSNode204Response{}, nil
}
} else {
// 检查目录是否为空
children, err := v.vfs.GetChildren(node.ID)
if err != nil {
return api.DeleteVFSNode500JSONResponse{
ServerInternalErrorJSONResponse: api.ServerInternalErrorJSONResponse{
Errtype: api.ErrorErrtypeInternalServerError,
Message: "Failed to get directory children: " + err.Error(),
},
}, nil
}
if len(children) != 0 {
return api.DeleteVFSNode400JSONResponse{
ParameterErrorJSONResponse: api.ParameterErrorJSONResponse{
Errtype: api.ErrorErrtypeConflictError,
Message: "Directory is not empty",
},
}, nil
}
}
case models.VfsNodeTypeFile:
// TODO
// 文件类型节点可以直接删除
fallthrough
default:
return api.DeleteVFSNode400JSONResponse{
ParameterErrorJSONResponse: api.ParameterErrorJSONResponse{