From 43ae67a070203defdd3a10ec8b1c715f8d7129bd Mon Sep 17 00:00:00 2001 From: despiegk Date: Mon, 15 Sep 2025 08:02:44 +0200 Subject: [PATCH] ... --- lib/hero/db/core_methods.v | 4 +- lib/hero/herofs/fs.v | 17 +- lib/hero/herofs/fs_tools_find.v | 91 +++-- lib/hero/herofs/fs_tools_find_test.v | 521 +++++++++++++++++++++++++++ 4 files changed, 604 insertions(+), 29 deletions(-) create mode 100644 lib/hero/herofs/fs_tools_find_test.v diff --git a/lib/hero/db/core_methods.v b/lib/hero/db/core_methods.v index 2adb0dd4..93f2deb5 100644 --- a/lib/hero/db/core_methods.v +++ b/lib/hero/db/core_methods.v @@ -3,9 +3,8 @@ module db import freeflowuniverse.herolib.data.ourtime import freeflowuniverse.herolib.data.encoder -pub fn (mut self DB) set[T](obj_ T) !u32 { +pub fn (mut self DB) set[T](mut obj T) ! { // Get the next ID - mut obj := obj_ if obj.id == 0 { obj.id = self.new_id()! } @@ -40,7 +39,6 @@ pub fn (mut self DB) set[T](obj_ T) !u32 { obj.dump(mut e)! // println('set: after dump, e.data.len: ${e.data.len}') self.redis.hset(self.db_name[T](), obj.id.str(), e.data.bytestr())! - return obj.id } // return the data, cannot return the object as we do not know the type diff --git a/lib/hero/herofs/fs.v b/lib/hero/herofs/fs.v index 7273aab2..d95715ab 100644 --- a/lib/hero/herofs/fs.v +++ b/lib/hero/herofs/fs.v @@ -79,12 +79,19 @@ pub fn (mut self DBFs) new(args FsArg) !Fs { return o } -pub fn (mut self DBFs) set(o Fs) !u32 { +pub fn (mut self DBFs) set(o_ Fs) !u32 { + mut o := o_ + if o.root_dir_id == 0 { + // If no root directory is set, create one + mut root_dir := self.factory.fs_dir.new( + name: 'root' + fs_id: o.id + parent_id: 0 // Root has no parent + )! + root_dir := self.factory.fs_dir.set(root_dir)! + // Update the filesystem with the new root directory ID + } id := self.db.set[Fs](o)! - - // Store name -> id mapping for lookups - self.db.redis.hset('fs:names', o.name, id.str())! - return id } diff --git a/lib/hero/herofs/fs_tools_find.v b/lib/hero/herofs/fs_tools_find.v index e6e7c94a..b78d7c66 100644 --- a/lib/hero/herofs/fs_tools_find.v +++ b/lib/hero/herofs/fs_tools_find.v @@ -69,16 +69,33 @@ pub fn (mut self FsTools) find(start_path string, opts FindOptions) ![]FindResul // - Symlinks: Symbolic links in the current directory (handled according to opts.follow_symlinks) // - Directories: Subdirectories of the current directory (recursed into according to opts.recursive) fn (mut self FsTools) find_recursive(dir_id u32, current_path string, opts FindOptions, mut results []FindResult, current_depth int) ! { + println('DEBUG: find_recursive called with dir_id=${dir_id}, current_path="${current_path}", current_depth=${current_depth}') + // Check depth limit if opts.max_depth >= 0 && current_depth > opts.max_depth { + println('DEBUG: Max depth reached, returning') return } // Get current directory info current_dir := self.factory.fs_dir.get(dir_id)! + println('DEBUG: Got directory "${current_dir.name}" with ${current_dir.files.len} files, ${current_dir.directories.len} directories, ${current_dir.symlinks.len} symlinks') // Check if current directory matches search criteria - if should_include(current_dir.name, opts.include_patterns, opts.exclude_patterns) { + // Only include the directory if it's not the root directory + if current_path != '/' + && should_include(current_dir.name, opts.include_patterns, opts.exclude_patterns) { + println('DEBUG: Including directory "${current_dir.name}" in results') + results << FindResult{ + result_type: .directory + id: dir_id + path: current_path + } + } + + // Always include the root directory + if current_path == '/' { + println('DEBUG: Including root directory "${current_dir.name}" in results') results << FindResult{ result_type: .directory id: dir_id @@ -88,9 +105,11 @@ fn (mut self FsTools) find_recursive(dir_id u32, current_path string, opts FindO // Get files in current directory for file_id in current_dir.files { + println('DEBUG: Processing file ID ${file_id}') file := self.factory.fs_file.get(file_id)! if should_include(file.name, opts.include_patterns, opts.exclude_patterns) { file_path := join_path(current_path, file.name) + println('DEBUG: Including file "${file.name}" in results') results << FindResult{ result_type: .file id: file.id @@ -117,10 +136,20 @@ fn (mut self FsTools) find_recursive(dir_id u32, current_path string, opts FindO if self.factory.fs_file.exist(symlink.target_id)! { target_file := self.factory.fs_file.get(symlink.target_id)! target_file_path := join_path(current_path, target_file.name) - results << FindResult{ - result_type: .file - id: target_file.id - path: target_file_path + // Check if we've already added this file to avoid duplicates + mut found := false + for result in results { + if result.id == target_file.id && result.result_type == .file { + found = true + break + } + } + if !found { + results << FindResult{ + result_type: .file + id: target_file.id + path: target_file_path + } } } else { // dangling symlink, just add the symlink itself @@ -132,14 +161,24 @@ fn (mut self FsTools) find_recursive(dir_id u32, current_path string, opts FindO if self.factory.fs_dir.exist(symlink.target_id)! { target_dir := self.factory.fs_dir.get(symlink.target_id)! target_dir_path := join_path(current_path, target_dir.name) - results << FindResult{ - result_type: .directory - id: target_dir.id - path: target_dir_path + // Check if we've already added this directory to avoid duplicates + mut found := false + for result in results { + if result.id == target_dir.id && result.result_type == .directory { + found = true + break + } } - if opts.recursive { - self.find_recursive(symlink.target_id, target_dir_path, opts, mut - results, current_depth + 1)! + if !found { + results << FindResult{ + result_type: .directory + id: target_dir.id + path: target_dir_path + } + if opts.recursive { + self.find_recursive(symlink.target_id, target_dir_path, + opts, mut results, current_depth + 1)! + } } } else { // dangling symlink, just add the symlink itself @@ -151,21 +190,27 @@ fn (mut self FsTools) find_recursive(dir_id u32, current_path string, opts FindO } for dir_id2 in current_dir.directories { + println('DEBUG: Found child directory ID ${dir_id2} in directory ${dir_id}') subdir := self.factory.fs_dir.get(dir_id2)! if should_include(subdir.name, opts.include_patterns, opts.exclude_patterns) { subdir_path := join_path(current_path, subdir.name) - results << FindResult{ - result_type: .directory - id: subdir.id - path: subdir_path - } - - // Process subdirectories if recursive - if opts.recursive { - self.find_recursive(dir_id, subdir_path, opts, mut results, current_depth + 1)! + // Include child directories in results when not recursive + // When recursive, the directory will be included in the results when find_recursive is called on it + if !opts.recursive { + println('DEBUG: Including directory "${subdir.name}" in results') + results << FindResult{ + result_type: .directory + id: subdir.id + path: subdir_path + } + } else { + println('DEBUG: Processing directory "${subdir.name}"') + self.find_recursive(dir_id2, subdir_path, opts, mut results, current_depth + 1)! } } } + + println('DEBUG: find_recursive finished with ${results.len} results') } // get_dir_by_absolute_path resolves an absolute path to a directory ID @@ -181,11 +226,15 @@ fn (mut self FsTools) find_recursive(dir_id u32, current_path string, opts FindO // dir := tools.get_dir_by_absolute_path('/home/user/documents')! // ``` pub fn (mut self FsTools) get_dir_by_absolute_path(path string) !FsDir { + println('DEBUG: get_dir_by_absolute_path called with path "${path}"') normalized_path_ := normalize_path(path) + println('DEBUG: normalized_path_ = "${normalized_path_}"') // Handle root directory case if normalized_path_ == '/' { + println('DEBUG: Handling root directory case') fs := self.factory.fs.get(self.fs_id)! + println('DEBUG: fs.root_dir_id = ${fs.root_dir_id}') return self.factory.fs_dir.get(fs.root_dir_id)! } diff --git a/lib/hero/herofs/fs_tools_find_test.v b/lib/hero/herofs/fs_tools_find_test.v new file mode 100644 index 00000000..ac9c47fa --- /dev/null +++ b/lib/hero/herofs/fs_tools_find_test.v @@ -0,0 +1,521 @@ +module herofs + +import freeflowuniverse.herolib.hero.db + +fn test_basic_find() { + println('Testing FsTools find functionality...') + + // Initialize the HeroFS factory + mut fs_factory := new()! + println('HeroFS factory initialized') + + // Create a new filesystem + mut my_fs := fs_factory.fs.new( + name: 'test_filesystem_find' + description: 'Filesystem for testing FsTools find functionality' + quota_bytes: 1024 * 1024 * 1024 // 1GB quota + )! + + // Save the filesystem to get an ID + fs_id := fs_factory.fs.set(my_fs)! + println('Created test filesystem with ID: ${fs_id}') + + // Create root directory + mut root_dir := fs_factory.fs_dir.new( + name: 'root' + fs_id: fs_id + parent_id: 0 // Root has no parent + description: 'Root directory for testing find' + )! + + // Save the root directory + root_dir_id := fs_factory.fs_dir.set(root_dir)! + println('Created root directory with ID: ${root_dir_id}') + + // Update the filesystem with the root directory ID + println('DEBUG: Before update, my_fs.root_dir_id = ${my_fs.root_dir_id}') + println('DEBUG: Before update, my_fs.id = ${my_fs.id}') + my_fs.root_dir_id = root_dir_id + my_fs.id = fs_id // Set the ID to ensure we update the existing object + println('DEBUG: Setting my_fs.root_dir_id to ${root_dir_id}') + mut fs_id2 := fs_factory.fs.set(my_fs)! + println('DEBUG: After update, fs_id2 = ${fs_id2}') + println('DEBUG: After update, my_fs.root_dir_id = ${my_fs.root_dir_id}') + + // Retrieve the updated filesystem object + my_fs = fs_factory.fs.get(fs_id)! + println('DEBUG: After retrieval, fs.root_dir_id = ${my_fs.root_dir_id}') + + // Create test directories + mut dir1 := fs_factory.fs_dir.new( + name: 'documents' + fs_id: fs_id + parent_id: root_dir_id + description: 'Documents directory' + )! + dir1_id := fs_factory.fs_dir.set(dir1)! + + mut dir2 := fs_factory.fs_dir.new( + name: 'images' + fs_id: fs_id + parent_id: root_dir_id + description: 'Images directory' + )! + dir2_id := fs_factory.fs_dir.set(dir2)! + + mut dir3 := fs_factory.fs_dir.new( + name: 'subdir' + fs_id: fs_id + parent_id: dir1_id + description: 'Subdirectory in documents' + )! + dir3_id := fs_factory.fs_dir.set(dir3)! + + // Update parent directories with their children + // Update root_dir to include dir1 and dir2 + println('DEBUG: Updating root_dir with children') + root_dir.directories = [dir1_id, dir2_id] + root_dir.id = root_dir_id // Set the ID to ensure we update the existing object + mut root_dir_id2 := fs_factory.fs_dir.set(root_dir)! + println('DEBUG: root_dir updated with ID ${root_dir_id2}') + + // Update dir1 to include dir3 + println('DEBUG: Updating dir1 with children') + dir1.directories = [dir3_id] + dir1.id = dir1_id // Set the ID to ensure we update the existing object + mut dir1_id2 := fs_factory.fs_dir.set(dir1)! + println('DEBUG: dir1 updated with ID ${dir1_id2}') + + // Create test blobs for files + mut test_blob1 := fs_factory.fs_blob.new( + data: 'This is test content for file 1'.bytes() + )! + blob1_id := fs_factory.fs_blob.set(test_blob1)! + println('Created test blob with ID: ${blob1_id}') + + mut test_blob2 := fs_factory.fs_blob.new( + data: 'This is test content for file 2'.bytes() + )! + blob2_id := fs_factory.fs_blob.set(test_blob2)! + println('Created test blob with ID: ${blob2_id}') + + mut test_blob3 := fs_factory.fs_blob.new( + data: 'This is test content for file 3'.bytes() + )! + blob3_id := fs_factory.fs_blob.set(test_blob3)! + println('Created test blob with ID: ${blob3_id}') + + // Create test files + mut file1 := fs_factory.fs_file.new( + name: 'document.txt' + fs_id: fs_id + directories: [dir1_id] + blobs: [blob1_id] + description: 'Text document' + mime_type: .txt + )! + file1_id := fs_factory.fs_file.set(file1)! + + mut file2 := fs_factory.fs_file.new( + name: 'image.png' + fs_id: fs_id + directories: [dir2_id] + blobs: [blob2_id] + description: 'PNG image' + mime_type: .png + )! + file2_id := fs_factory.fs_file.set(file2)! + + mut file3 := fs_factory.fs_file.new( + name: 'subfile.txt' + fs_id: fs_id + directories: [dir3_id] + blobs: [blob3_id] + description: 'Text file in subdirectory' + mime_type: .txt + )! + file3_id := fs_factory.fs_file.set(file3)! + + // Create symlinks + mut symlink1 := fs_factory.fs_symlink.new( + name: 'doc_link.txt' + fs_id: fs_id + parent_id: root_dir_id + target_id: file1_id + target_type: .file + description: 'Symlink to document.txt' + )! + symlink1_id := fs_factory.fs_symlink.set(symlink1)! + + mut symlink2 := fs_factory.fs_symlink.new( + name: 'images_link' + fs_id: fs_id + parent_id: root_dir_id + target_id: dir2_id + target_type: .directory + description: 'Symlink to images directory' + )! + symlink2_id := fs_factory.fs_symlink.set(symlink2)! + + // Update directories with their children + // Update dir1 to include dir3 and file1 + dir1.directories = [dir3_id] + dir1.files = [file1_id] + fs_factory.fs_dir.set(dir1)! + + // Update dir2 to include file2 + dir2.files = [file2_id] + fs_factory.fs_dir.set(dir2)! + + // Update dir3 to include file3 + dir3.files = [file3_id] + dir3.id = dir3_id // Set the ID to ensure we update the existing object + fs_factory.fs_dir.set(dir3)! + + // Update root_dir to include dir1, dir2, symlink1, symlink2 + root_dir.directories = [dir1_id, dir2_id] + root_dir.symlinks = [symlink1_id, symlink2_id] + fs_factory.fs_dir.set(root_dir)! + + println('Created test directory structure:') + println('- root (ID: ${root_dir_id})') + println(' - documents (ID: ${dir1_id})') + println(' - subdir (ID: ${dir3_id})') + println(' - subfile.txt (ID: ${file3_id})') + println(' - document.txt (ID: ${file1_id})') + println(' - images (ID: ${dir2_id})') + println(' - image.png (ID: ${file2_id})') + println(' - doc_link.txt (ID: ${symlink1_id}) -> document.txt') + println(' - images_link (ID: ${symlink2_id}) -> images') + + // Create FsTools instance + mut fs_tools := fs_factory.fs_tools(fs_id) + + // Test basic find from root + println('\nTesting basic find from root...') + mut results := fs_tools.find('/', FindOptions{ + recursive: true + })! + + // Should find all items + assert results.len == 8 + println('✓ Found all 8 items in recursive search') + + // Check that we found the expected items + mut found_items := map[string]FSItemType{} + for result in results { + found_items[result.path] = result.result_type + } + + assert found_items['/'] == .directory + assert found_items['/documents'] == .directory + assert found_items['/images'] == .directory + assert found_items['/documents/subdir'] == .directory + assert found_items['/documents/document.txt'] == .file + assert found_items['/images/image.png'] == .file + assert found_items['/documents/subdir/subfile.txt'] == .file + assert found_items['/doc_link.txt'] == .symlink + assert found_items['/images_link'] == .symlink + println('✓ All items found with correct paths and types') + + // Test non-recursive find from root + println('\nTesting non-recursive find from root...') + results = fs_tools.find('/', FindOptions{ + recursive: false + })! + + // Should only find items directly in root + assert results.len == 5 + println('✓ Found 5 items in non-recursive search') + + // Check that we found the expected items + found_items = map[string]FSItemType{} + for result in results { + found_items[result.path] = result.result_type + } + + assert found_items['/'] == .directory + assert found_items['/documents'] == .directory + assert found_items['/images'] == .directory + assert '/documents/subdir' !in found_items + println('✓ Non-recursive search only found direct children') + + // Test find with include patterns + println('\nTesting find with include patterns...') + results = fs_tools.find('/', FindOptions{ + recursive: true + include_patterns: ['*.txt'] + })! + + // Should find only .txt files + assert results.len == 2 + println('✓ Found 2 .txt files with include pattern') + + found_items = map[string]FSItemType{} + for result in results { + found_items[result.path] = result.result_type + } + + assert found_items['/documents/document.txt'] == .file + assert found_items['/documents/subdir/subfile.txt'] == .file + println('✓ Include pattern correctly filtered results') + + // Test find with exclude patterns + println('\nTesting find with exclude patterns...') + results = fs_tools.find('/', FindOptions{ + recursive: true + exclude_patterns: ['*.png'] + })! + + // Should find all items except the .png file + assert results.len == 7 + println('✓ Found 7 items excluding .png files') + + found_items = map[string]FSItemType{} + for result in results { + found_items[result.path] = result.result_type + } + + assert '/images/image.png' !in found_items + assert found_items['/images'] == .directory + println('✓ Exclude pattern correctly filtered results') + + // Test find with max_depth + println('\nTesting find with max_depth...') + results = fs_tools.find('/', FindOptions{ + recursive: true + max_depth: 1 + })! + + // Should find root and its direct children only + assert results.len == 6 + println('✓ Found 6 items with max_depth=1') + + found_items = map[string]FSItemType{} + for result in results { + found_items[result.path] = result.result_type + } + + assert found_items['/'] == .directory + assert found_items['/documents'] == .directory + assert found_items['/images'] == .directory + assert '/documents/subdir' !in found_items + assert '/documents/subdir/subfile.txt' !in found_items + println('✓ Max depth correctly limited search depth') + + // Test find from subdirectory + println('\nTesting find from subdirectory...') + results = fs_tools.find('/documents', FindOptions{ + recursive: true + })! + + // Should find items in /documents and its subdirectories + assert results.len == 4 + println('✓ Found 4 items in subdirectory search') + + found_items = map[string]FSItemType{} + for result in results { + found_items[result.path] = result.result_type + } + + assert found_items['/documents'] == .directory + assert found_items['/documents/document.txt'] == .file + assert found_items['/documents/subdir'] == .directory + assert found_items['/documents/subdir/subfile.txt'] == .file + assert '/' !in found_items + println('✓ Subdirectory search correctly rooted at /documents') + + println('\nFsTools find basic test completed successfully!') +} + +fn test_symlink_find() { + println('\nTesting FsTools find with symlinks...') + + // Initialize the HeroFS factory + mut fs_factory := new()! + + // Create a new filesystem + mut my_fs := fs_factory.fs.new( + name: 'test_filesystem_symlink_find' + description: 'Filesystem for testing FsTools find with symlinks' + quota_bytes: 1024 * 1024 * 1024 // 1GB quota + )! + + // Save the filesystem to get an ID + fs_id := fs_factory.fs.set(my_fs)! + + // Create root directory + mut root_dir := fs_factory.fs_dir.new( + name: 'root' + fs_id: fs_id + parent_id: 0 // Root has no parent + description: 'Root directory for testing symlink find' + )! + + // Save the root directory + root_dir_id := fs_factory.fs_dir.set(root_dir)! + + // Update the filesystem with the root directory ID + my_fs.root_dir_id = root_dir_id + my_fs.id = fs_id // Set the ID to ensure we update the existing object + fs_factory.fs.set(my_fs)! + + // Retrieve the updated filesystem object + my_fs = fs_factory.fs.get(fs_id)! + + // Create test directory + mut dir1 := fs_factory.fs_dir.new( + name: 'target_dir' + fs_id: fs_id + parent_id: root_dir_id + description: 'Target directory for symlink' + )! + dir1_id := fs_factory.fs_dir.set(dir1)! + + // Create test blob + mut test_blob := fs_factory.fs_blob.new( + data: 'Symlink test content'.bytes() + )! + blob_id := fs_factory.fs_blob.set(test_blob)! + + // Create test file + mut file1 := fs_factory.fs_file.new( + name: 'target_file.txt' + fs_id: fs_id + directories: [dir1_id] + blobs: [blob_id] + description: 'Target file for symlink' + mime_type: .txt + )! + file1_id := fs_factory.fs_file.set(file1)! + + // Update dir1 with file1 + dir1.files = [file1_id] + dir1.id = dir1_id // Set the ID to ensure we update the existing object + fs_factory.fs_dir.set(dir1)! + + // Create symlinks + mut symlink1 := fs_factory.fs_symlink.new( + name: 'file_link.txt' + fs_id: fs_id + parent_id: root_dir_id + target_id: file1_id + target_type: .file + description: 'Symlink to target_file.txt' + )! + symlink1_id := fs_factory.fs_symlink.set(symlink1)! + + mut symlink2 := fs_factory.fs_symlink.new( + name: 'dir_link' + fs_id: fs_id + parent_id: root_dir_id + target_id: dir1_id + target_type: .directory + description: 'Symlink to target_dir' + )! + symlink2_id := fs_factory.fs_symlink.set(symlink2)! + + // Update root_dir with dir1 and symlinks + root_dir.directories = [dir1_id] + root_dir.symlinks = [symlink1_id, symlink2_id] + root_dir.id = root_dir_id // Set the ID to ensure we update the existing object + fs_factory.fs_dir.set(root_dir)! + + // Create FsTools instance + mut fs_tools := fs_factory.fs_tools(fs_id) + + // Test find without following symlinks + println('Testing find without following symlinks...') + mut results := fs_tools.find('/', FindOptions{ + recursive: true + follow_symlinks: false + })! + + // Should find root, target_dir, symlinks, and target_file.txt + assert results.len == 5 + println('✓ Found 5 items without following symlinks') + + mut found_items := map[string]FSItemType{} + for result in results { + found_items[result.path] = result.result_type + } + + assert found_items['/'] == .directory + assert found_items['/target_dir'] == .directory + assert found_items['/file_link.txt'] == .symlink + assert found_items['/dir_link'] == .symlink + assert found_items['/target_dir/target_file.txt'] == .file + println('✓ Symlinks found as symlinks when follow_symlinks=false') + + // Test find with following symlinks + println('Testing find with following symlinks...') + results = fs_tools.find('/', FindOptions{ + recursive: true + follow_symlinks: true + })! + + // Should find root, target_dir, and target_file.txt (but not the symlinks themselves) + assert results.len == 3 + println('✓ Found 3 items when following symlinks') + + found_items = map[string]FSItemType{} + for result in results { + found_items[result.path] = result.result_type + } + + assert found_items['/'] == .directory + assert found_items['/target_dir'] == .directory + assert found_items['/target_dir/target_file.txt'] == .file + assert '/file_link.txt' !in found_items + assert '/dir_link' !in found_items + println('✓ Symlinks followed correctly when follow_symlinks=true') + + println('FsTools find symlink test completed successfully!') +} + +fn test_find_edge_cases() { + println('\nTesting FsTools find edge cases...') + + // Initialize the HeroFS factory + mut fs_factory := new()! + + // Create a new filesystem + mut my_fs := fs_factory.fs.new( + name: 'test_filesystem_find_edge' + description: 'Filesystem for testing FsTools find edge cases' + quota_bytes: 1024 * 1024 * 1024 // 1GB quota + )! + + // Save the filesystem to get an ID + fs_id := fs_factory.fs.set(my_fs)! + + // Create root directory + mut root_dir := fs_factory.fs_dir.new( + name: 'root' + fs_id: fs_id + parent_id: 0 // Root has no parent + description: 'Root directory for testing find edge cases' + )! + + // Save the root directory + root_dir_id := fs_factory.fs_dir.set(root_dir)! + + // Update the filesystem with the root directory ID + my_fs.root_dir_id = root_dir_id + fs_factory.fs.set(my_fs)! + + // Create FsTools instance + mut fs_tools := fs_factory.fs_tools(fs_id) + + // Test find with non-existent path + println('Testing find with non-existent path...') + mut result := fs_tools.find('/nonexistent', FindOptions{}) or { + println('✓ Find correctly failed with non-existent path') + return + } + + // If we get here, the error handling didn't work as expected + panic('Find should have failed with non-existent path') + + println('FsTools find edge cases test completed successfully!') +}