diff --git a/config/bookmark/bookmark.yaml b/config/bookmark/bookmark.yaml index 736bd72..f9ffb0a 100644 --- a/config/bookmark/bookmark.yaml +++ b/config/bookmark/bookmark.yaml @@ -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的书签 diff --git a/config/user_np/user_np.yaml b/config/user_np/user_np.yaml index 4301ca7..d7a5000 100644 --- a/config/user_np/user_np.yaml +++ b/config/user_np/user_np.yaml @@ -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: diff --git a/config/vfs/vfs.yaml b/config/vfs/vfs.yaml index 9c50555..1bc40dd 100644 --- a/config/vfs/vfs.yaml +++ b/config/vfs/vfs.yaml @@ -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 \ No newline at end of file + - message diff --git a/internal/vfs/models/vfs.go b/internal/vfs/models/vfs.go index b823965..6890272 100644 --- a/internal/vfs/models/vfs.go +++ b/internal/vfs/models/vfs.go @@ -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 { diff --git a/internal/vfs/vfs_impl.go b/internal/vfs/vfs_impl.go index d37de06..0d8dff8 100644 --- a/internal/vfs/vfs_impl.go +++ b/internal/vfs/vfs_impl.go @@ -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{