...
This commit is contained in:
@@ -17,6 +17,23 @@ pub mut:
|
||||
parent_id u32 // Parent directory ID (0 for root)
|
||||
}
|
||||
|
||||
// DirectoryContents represents the contents of a directory
|
||||
pub struct DirectoryContents {
|
||||
pub mut:
|
||||
directories []FsDir
|
||||
files []FsFile
|
||||
symlinks []FsSymlink
|
||||
}
|
||||
|
||||
// ListContentsOptions defines options for listing directory contents
|
||||
@[params]
|
||||
pub struct ListContentsOptions {
|
||||
pub mut:
|
||||
recursive bool
|
||||
include_patterns []string // File/directory name patterns to include (e.g. *.py, doc*)
|
||||
exclude_patterns []string // File/directory name patterns to exclude
|
||||
}
|
||||
|
||||
// we only keep the parents, not the children, as children can be found by doing a query on parent_id, we will need some smart hsets to make this fast enough and efficient
|
||||
|
||||
pub struct DBFsDir {
|
||||
@@ -147,6 +164,173 @@ pub fn (mut self DBFsDir) list_by_filesystem(fs_id u32) ![]FsDir {
|
||||
return dirs
|
||||
}
|
||||
|
||||
// Get directory by absolute path
|
||||
pub fn (mut self DBFsDir) get_by_absolute_path(fs_id u32, path string) !FsDir {
|
||||
// Normalize path (remove trailing slashes, handle empty path)
|
||||
normalized_path := if path == '' || path == '/' { '/' } else { path.trim_right('/') }
|
||||
|
||||
if normalized_path == '/' {
|
||||
// Special case for root directory
|
||||
dirs := self.list_by_filesystem(fs_id)!
|
||||
for dir in dirs {
|
||||
if dir.parent_id == 0 {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
return error('Root directory not found for filesystem ${fs_id}')
|
||||
}
|
||||
|
||||
// Split path into components
|
||||
components := normalized_path.trim_left('/').split('/')
|
||||
|
||||
// Start from the root directory
|
||||
mut current_dir_id := u32(0)
|
||||
mut dirs := self.list_by_filesystem(fs_id)!
|
||||
|
||||
// Find root directory
|
||||
for dir in dirs {
|
||||
if dir.parent_id == 0 {
|
||||
current_dir_id = dir.id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if current_dir_id == 0 {
|
||||
return error('Root directory not found for filesystem ${fs_id}')
|
||||
}
|
||||
|
||||
// Navigate through path components
|
||||
for component in components {
|
||||
found := false
|
||||
for dir in dirs {
|
||||
if dir.parent_id == current_dir_id && dir.name == component {
|
||||
current_dir_id = dir.id
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return error('Directory "${component}" not found in path "${normalized_path}"')
|
||||
}
|
||||
|
||||
// Update dirs for next iteration
|
||||
dirs = self.list_children(current_dir_id)!
|
||||
}
|
||||
|
||||
return self.get(current_dir_id)!
|
||||
}
|
||||
|
||||
// Create a directory by absolute path, creating parent directories as needed
|
||||
pub fn (mut self DBFsDir) create_path(fs_id u32, path string) !u32 {
|
||||
// Normalize path
|
||||
normalized_path := if path == '' || path == '/' { '/' } else { path.trim_right('/') }
|
||||
|
||||
if normalized_path == '/' {
|
||||
// Special case for root directory
|
||||
dirs := self.list_by_filesystem(fs_id)!
|
||||
for dir in dirs {
|
||||
if dir.parent_id == 0 {
|
||||
return dir.id
|
||||
}
|
||||
}
|
||||
|
||||
// Create root directory if it doesn't exist
|
||||
mut root_dir := self.new(
|
||||
name: 'root'
|
||||
fs_id: fs_id
|
||||
parent_id: 0
|
||||
description: 'Root directory'
|
||||
)!
|
||||
return self.set(root_dir)!
|
||||
}
|
||||
|
||||
// Split path into components
|
||||
components := normalized_path.trim_left('/').split('/')
|
||||
|
||||
// Start from the root directory
|
||||
mut current_dir_id := u32(0)
|
||||
mut dirs := self.list_by_filesystem(fs_id)!
|
||||
|
||||
// Find or create root directory
|
||||
for dir in dirs {
|
||||
if dir.parent_id == 0 {
|
||||
current_dir_id = dir.id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if current_dir_id == 0 {
|
||||
// Create root directory
|
||||
mut root_dir := self.new(
|
||||
name: 'root'
|
||||
fs_id: fs_id
|
||||
parent_id: 0
|
||||
description: 'Root directory'
|
||||
)!
|
||||
current_dir_id = self.set(root_dir)!
|
||||
}
|
||||
|
||||
// Navigate/create through path components
|
||||
for component in components {
|
||||
found := false
|
||||
for dir in dirs {
|
||||
if dir.parent_id == current_dir_id && dir.name == component {
|
||||
current_dir_id = dir.id
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// Create this directory component
|
||||
mut new_dir := self.new(
|
||||
name: component
|
||||
fs_id: fs_id
|
||||
parent_id: current_dir_id
|
||||
description: 'Directory created as part of path ${normalized_path}'
|
||||
)!
|
||||
current_dir_id = self.set(new_dir)!
|
||||
}
|
||||
|
||||
// Update directory list for next iteration
|
||||
dirs = self.list_children(current_dir_id)!
|
||||
}
|
||||
|
||||
return current_dir_id
|
||||
}
|
||||
|
||||
// Delete a directory by absolute path
|
||||
pub fn (mut self DBFsDir) delete_by_path(fs_id u32, path string) ! {
|
||||
dir := self.get_by_absolute_path(fs_id, path)!
|
||||
self.delete(dir.id)!
|
||||
}
|
||||
|
||||
// Move a directory using source and destination paths
|
||||
pub fn (mut self DBFsDir) move_by_path(fs_id u32, source_path string, dest_path string) !u32 {
|
||||
// Get the source directory
|
||||
source_dir := self.get_by_absolute_path(fs_id, source_path)!
|
||||
|
||||
// For the destination, we need the parent directory
|
||||
dest_dir_path := dest_path.all_before_last('/')
|
||||
dest_dir_name := dest_path.all_after_last('/')
|
||||
|
||||
dest_parent_dir := if dest_dir_path == '' || dest_dir_path == '/' {
|
||||
// Moving to the root
|
||||
self.get_by_absolute_path(fs_id, '/')!
|
||||
} else {
|
||||
self.get_by_absolute_path(fs_id, dest_dir_path)!
|
||||
}
|
||||
|
||||
// First rename if the destination name is different
|
||||
if source_dir.name != dest_dir_name {
|
||||
self.rename(source_dir.id, dest_dir_name)!
|
||||
}
|
||||
|
||||
// Then move to the new parent
|
||||
return self.move(source_dir.id, dest_parent_dir.id)!
|
||||
}
|
||||
|
||||
// Get children of a directory
|
||||
pub fn (mut self DBFsDir) list_children(dir_id u32) ![]FsDir {
|
||||
child_ids := self.db.redis.hkeys('fsdir:children:${dir_id}')!
|
||||
@@ -205,3 +389,91 @@ pub fn (mut self DBFsDir) move(id u32, new_parent_id u32) !u32 {
|
||||
// Save with new parent
|
||||
return self.set(dir)!
|
||||
}
|
||||
|
||||
// List contents of a directory with filtering capabilities
|
||||
pub fn (mut self DBFsDir) list_contents(fs_factory &FsFactory, dir_id u32, opts ListContentsOptions) !DirectoryContents {
|
||||
mut result := DirectoryContents{}
|
||||
|
||||
// Helper function to check if name matches include/exclude patterns
|
||||
matches_pattern := fn (name string, patterns []string) bool {
|
||||
if patterns.len == 0 {
|
||||
return true // No patterns means include everything
|
||||
}
|
||||
|
||||
for pattern in patterns {
|
||||
if pattern.contains('*') {
|
||||
prefix := pattern.all_before('*')
|
||||
suffix := pattern.all_after('*')
|
||||
|
||||
if prefix == '' && suffix == '' {
|
||||
return true // Pattern is just "*"
|
||||
} else if prefix == '' {
|
||||
if name.ends_with(suffix) {
|
||||
return true
|
||||
}
|
||||
} else if suffix == '' {
|
||||
if name.starts_with(prefix) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if name.starts_with(prefix) && name.ends_with(suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else if name == pattern {
|
||||
return true // Exact match
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if item should be included based on patterns
|
||||
should_include := fn (name string, include_patterns []string, exclude_patterns []string) bool {
|
||||
// First apply include patterns (if empty, include everything)
|
||||
if !matches_pattern(name, include_patterns) && include_patterns.len > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Then apply exclude patterns
|
||||
if matches_pattern(name, exclude_patterns) && exclude_patterns.len > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Get directories, files, and symlinks in the current directory
|
||||
dirs := self.list_children(dir_id)!
|
||||
for dir in dirs {
|
||||
if should_include(dir.name, opts.include_patterns, opts.exclude_patterns) {
|
||||
result.directories << dir
|
||||
}
|
||||
|
||||
// If recursive, process subdirectories
|
||||
if opts.recursive {
|
||||
sub_contents := self.list_contents(fs_factory, dir.id, opts)!
|
||||
result.directories << sub_contents.directories
|
||||
result.files << sub_contents.files
|
||||
result.symlinks << sub_contents.symlinks
|
||||
}
|
||||
}
|
||||
|
||||
// Get files in the directory
|
||||
files := fs_factory.fs_file.list_by_directory(dir_id)!
|
||||
for file in files {
|
||||
if should_include(file.name, opts.include_patterns, opts.exclude_patterns) {
|
||||
result.files << file
|
||||
}
|
||||
}
|
||||
|
||||
// Get symlinks in the directory
|
||||
symlinks := fs_factory.fs_symlink.list_by_parent(dir_id)!
|
||||
for symlink in symlinks {
|
||||
if should_include(symlink.name, opts.include_patterns, opts.exclude_patterns) {
|
||||
result.symlinks << symlink
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ pub fn start(args ServerArgs) ! {
|
||||
openrpc_handler.register_procedure_handle('fs_dir_rename', fs_dir_rename)
|
||||
openrpc_handler.register_procedure_handle('fs_dir_list_by_filesystem', fs_dir_list_by_filesystem)
|
||||
openrpc_handler.register_procedure_handle('fs_dir_has_children', fs_dir_has_children)
|
||||
openrpc_handler.register_procedure_handle('fs_dir_list_contents', fs_dir_list_contents)
|
||||
|
||||
// Register fs_file procedures
|
||||
openrpc_handler.register_procedure_handle('fs_file_get', fs_file_get)
|
||||
|
||||
@@ -981,6 +981,97 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fs_dir_list_contents",
|
||||
"summary": "List directory contents with filtering",
|
||||
"description": "List files, directories, and symlinks in a directory with optional filtering and recursion",
|
||||
"params": [
|
||||
{
|
||||
"name": "dir_id",
|
||||
"description": "ID of directory to list contents for",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"description": "Absolute path to the directory",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fs_id",
|
||||
"description": "ID of filesystem (required when using path)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "recursive",
|
||||
"description": "Whether to list contents recursively",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "include",
|
||||
"description": "Patterns to include (e.g. ['*.py', 'doc*'])",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exclude",
|
||||
"description": "Patterns to exclude (e.g. ['*~', '.git'])",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "contents",
|
||||
"description": "Directory contents with files, directories, and symlinks",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Directory"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/File"
|
||||
}
|
||||
},
|
||||
"symlinks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Symlink"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
|
||||
@@ -8,7 +8,9 @@ import freeflowuniverse.herolib.hero.herofs
|
||||
@[params]
|
||||
pub struct FSDirGetArgs {
|
||||
pub mut:
|
||||
id u32 @[required]
|
||||
id u32
|
||||
path string // Allow getting a directory by path
|
||||
fs_id u32 // Required when using path
|
||||
}
|
||||
|
||||
@[params]
|
||||
@@ -17,6 +19,7 @@ pub mut:
|
||||
name string @[required]
|
||||
fs_id u32 @[required]
|
||||
parent_id u32
|
||||
path string // Allow creating directories by path
|
||||
description string
|
||||
metadata map[string]string
|
||||
}
|
||||
@@ -24,14 +27,19 @@ pub mut:
|
||||
@[params]
|
||||
pub struct FSDirDeleteArgs {
|
||||
pub mut:
|
||||
id u32 @[required]
|
||||
id u32
|
||||
path string // Allow deleting a directory by path
|
||||
fs_id u32 // Required when using path
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct FSDirMoveArgs {
|
||||
pub mut:
|
||||
id u32 @[required]
|
||||
parent_id u32 @[required]
|
||||
id u32
|
||||
parent_id u32
|
||||
source_path string // Allow moving using paths
|
||||
dest_path string
|
||||
fs_id u32 // Required when using paths
|
||||
}
|
||||
|
||||
@[params]
|
||||
@@ -53,13 +61,32 @@ pub mut:
|
||||
id u32 @[required]
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct FSDirListContentsArgs {
|
||||
pub mut:
|
||||
dir_id u32
|
||||
path string // Allow listing contents by path
|
||||
fs_id u32 // Required when using path
|
||||
recursive bool
|
||||
include []string // Patterns to include
|
||||
exclude []string // Patterns to exclude
|
||||
}
|
||||
|
||||
pub fn fs_dir_get(request Request) !Response {
|
||||
payload := jsonrpc.decode_payload[FSDirGetArgs](request.params) or {
|
||||
return jsonrpc.invalid_params
|
||||
}
|
||||
|
||||
mut fs_factory := herofs.new()!
|
||||
dir := fs_factory.fs_dir.get(payload.id)!
|
||||
|
||||
// Handle either path-based or ID-based retrieval
|
||||
mut dir := if payload.path != '' && payload.fs_id > 0 {
|
||||
fs_factory.fs_dir.get_by_absolute_path(payload.fs_id, payload.path)!
|
||||
} else if payload.id > 0 {
|
||||
fs_factory.fs_dir.get(payload.id)!
|
||||
} else {
|
||||
return jsonrpc.invalid_params_with_msg("Either id or both path and fs_id must be provided")
|
||||
}
|
||||
|
||||
return jsonrpc.new_response(request.id, json.encode(dir))
|
||||
}
|
||||
@@ -70,17 +97,25 @@ pub fn fs_dir_set(request Request) !Response {
|
||||
}
|
||||
|
||||
mut fs_factory := herofs.new()!
|
||||
mut dir_obj := fs_factory.fs_dir.new(
|
||||
name: payload.name
|
||||
fs_id: payload.fs_id
|
||||
parent_id: payload.parent_id
|
||||
description: payload.description
|
||||
metadata: payload.metadata
|
||||
)!
|
||||
|
||||
mut dir_id := u32(0)
|
||||
|
||||
// Handle path-based creation
|
||||
if payload.path != '' {
|
||||
dir_id = fs_factory.fs_dir.create_path(payload.fs_id, payload.path)!
|
||||
} else {
|
||||
// Handle traditional creation
|
||||
mut dir_obj := fs_factory.fs_dir.new(
|
||||
name: payload.name
|
||||
fs_id: payload.fs_id
|
||||
parent_id: payload.parent_id
|
||||
description: payload.description
|
||||
metadata: payload.metadata
|
||||
)!
|
||||
dir_id = fs_factory.fs_dir.set(dir_obj)!
|
||||
}
|
||||
|
||||
id := fs_factory.fs_dir.set(dir_obj)!
|
||||
|
||||
return new_response_u32(request.id, id)
|
||||
return new_response_u32(request.id, dir_id)
|
||||
}
|
||||
|
||||
pub fn fs_dir_delete(request Request) !Response {
|
||||
@@ -89,7 +124,15 @@ pub fn fs_dir_delete(request Request) !Response {
|
||||
}
|
||||
|
||||
mut fs_factory := herofs.new()!
|
||||
fs_factory.fs_dir.delete(payload.id)!
|
||||
|
||||
// Handle either path-based or ID-based deletion
|
||||
if payload.path != '' && payload.fs_id > 0 {
|
||||
fs_factory.fs_dir.delete_by_path(payload.fs_id, payload.path)!
|
||||
} else if payload.id > 0 {
|
||||
fs_factory.fs_dir.delete(payload.id)!
|
||||
} else {
|
||||
return jsonrpc.invalid_params_with_msg("Either id or both path and fs_id must be provided")
|
||||
}
|
||||
|
||||
return new_response_true(request.id)
|
||||
}
|
||||
@@ -107,7 +150,15 @@ pub fn fs_dir_move(request Request) !Response {
|
||||
}
|
||||
|
||||
mut fs_factory := herofs.new()!
|
||||
fs_factory.fs_dir.move(payload.id, payload.parent_id)!
|
||||
|
||||
// Handle either path-based or ID-based move
|
||||
if payload.source_path != '' && payload.dest_path != '' && payload.fs_id > 0 {
|
||||
fs_factory.fs_dir.move_by_path(payload.fs_id, payload.source_path, payload.dest_path)!
|
||||
} else if payload.id > 0 && payload.parent_id > 0 {
|
||||
fs_factory.fs_dir.move(payload.id, payload.parent_id)!
|
||||
} else {
|
||||
return jsonrpc.invalid_params_with_msg("Either id and parent_id, or source_path, dest_path and fs_id must be provided")
|
||||
}
|
||||
|
||||
return new_response_true(request.id)
|
||||
}
|
||||
@@ -144,3 +195,34 @@ pub fn fs_dir_has_children(request Request) !Response {
|
||||
|
||||
return jsonrpc.new_response(request.id, json.encode(has_children))
|
||||
}
|
||||
|
||||
// New method to list directory contents with filters
|
||||
pub fn fs_dir_list_contents(request Request) !Response {
|
||||
payload := jsonrpc.decode_payload[FSDirListContentsArgs](request.params) or {
|
||||
return jsonrpc.invalid_params
|
||||
}
|
||||
|
||||
mut fs_factory := herofs.new()!
|
||||
|
||||
// Get directory ID either directly or from path
|
||||
mut dir_id := if payload.path != '' && payload.fs_id > 0 {
|
||||
dir := fs_factory.fs_dir.get_by_absolute_path(payload.fs_id, payload.path)!
|
||||
dir.id
|
||||
} else if payload.dir_id > 0 {
|
||||
payload.dir_id
|
||||
} else {
|
||||
return jsonrpc.invalid_params_with_msg("Either dir_id or both path and fs_id must be provided")
|
||||
}
|
||||
|
||||
// Create options struct
|
||||
opts := herofs.ListContentsOptions{
|
||||
recursive: payload.recursive
|
||||
include_patterns: payload.include
|
||||
exclude_patterns: payload.exclude
|
||||
}
|
||||
|
||||
// List contents with filters
|
||||
contents := fs_factory.fs_dir.list_contents(&fs_factory, dir_id, opts)!
|
||||
|
||||
return jsonrpc.new_response(request.id, json.encode(contents))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user