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