feat(vfs): 增强虚拟文件系统删除和查询功能
- 新增递归删除目录及子项的功能,支持 recursive 和 force 操作模式 - 支持通过路径获取文件或目录内容,并完善参数说明和示例 - 完善 VFS API 文档,包括操作模式、权限描述和响应内容 - 优化 GetVFSNode 方法,改为通过 ID 查询节点信息 - 修复部分路径和权限检查的逻辑注释
This commit is contained in:
@ -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的书签
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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{
|
||||
|
Reference in New Issue
Block a user