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

View File

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

View File

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

View File

@ -227,6 +227,99 @@ func (v *Vfs) GetChildren(parentID uint64) ([]VfsDirEntry, error) {
return dirEntrys, nil 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 // GetParentID 根据父路径查找父节点 ID
// parentPath 应该是 ParsePathComponents 的第一个返回值 // parentPath 应该是 ParsePathComponents 的第一个返回值
func (v *Vfs) GetParentID(parentPath string) (uint64, error) { func (v *Vfs) GetParentID(parentPath string) (uint64, error) {
@ -400,6 +493,7 @@ func ParsePathComponents(pathStr string) (parentPath, nodeName string, nodeType
// MoveToPath 将节点移动到指定路径 // MoveToPath 将节点移动到指定路径
func (v *Vfs) MoveToPath(node *VfsNode, destPath string) error { func (v *Vfs) MoveToPath(node *VfsNode, destPath string) error {
// FIXME: 路径和权限的检查
// 1. 解析目标路径 // 1. 解析目标路径
parentPath, nodeName, _, err := ParsePathComponents(destPath) parentPath, nodeName, _, err := ParsePathComponents(destPath)
if err != nil { if err != nil {
@ -459,12 +553,12 @@ func (v *Vfs) DeleteVFSNode(p *VfsNode) error {
return err return err
} }
func (v *Vfs) GetVFSNode(p *VfsNode) *VfsNode { func (v *Vfs) GetVFSNode(ID uint64) *VfsNode {
node := &VfsNode{} node := &VfsNode{}
err := v.DB.QueryRow(` err := v.DB.QueryRow(`
SELECT id, name, parent_id, type, created_at, updated_at SELECT id, name, parent_id, type, created_at, updated_at
FROM vfs_nodes FROM vfs_nodes
WHERE id = ?`, p.ID).Scan( WHERE id = ?`, ID).Scan(
&node.ID, &node.Name, &node.ParentID, &node.Type, &node.CreatedAt, &node.UpdatedAt) &node.ID, &node.Name, &node.ParentID, &node.Type, &node.CreatedAt, &node.UpdatedAt)
if err != nil { if err != nil {

View File

@ -99,29 +99,51 @@ func (v *VfsImpl) DeleteVFSNode(ctx context.Context, request api.DeleteVFSNodeRe
}, nil }, nil
} }
case models.VfsNodeTypeDirectory: 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 { if request.Params.Op != nil && *request.Params.Op == api.Recursive {
return api.DeleteVFSNode400JSONResponse{ if sericeIDs, err := v.vfs.DeleteNodeRecursively(node.ID); err != nil {
ParameterErrorJSONResponse: api.ParameterErrorJSONResponse{ return api.DeleteVFSNode500JSONResponse{
Errtype: api.ErrorErrtypeConflictError, ServerInternalErrorJSONResponse: api.ServerInternalErrorJSONResponse{
Message: "Directory is not empty", Errtype: api.ErrorErrtypeInternalServerError,
}, Message: "Failed to delete node: " + err.Error(),
}, nil },
}, 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: default:
return api.DeleteVFSNode400JSONResponse{ return api.DeleteVFSNode400JSONResponse{
ParameterErrorJSONResponse: api.ParameterErrorJSONResponse{ ParameterErrorJSONResponse: api.ParameterErrorJSONResponse{