This commit is contained in:
2025-09-14 16:47:35 +02:00
parent cde04c9917
commit 84bbcd3a06
4 changed files with 463 additions and 17 deletions

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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": {

View File

@@ -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))
}