This commit is contained in:
2025-09-27 13:51:21 +04:00
parent 45d1d60166
commit 097bfecfe6
11 changed files with 708 additions and 476 deletions

View File

@@ -1,7 +1,7 @@
module herofs
import freeflowuniverse.herolib.hero.db
import os
// CopyOptions provides options for copy operations
@[params]
@@ -9,7 +9,7 @@ pub struct CopyOptions {
pub mut:
recursive bool = true // Copy directories recursively
overwrite bool // Overwrite existing files at destination
copy_blobs bool = true // Create new blob copies (true) or reference same blobs (false)
copy_blobs bool // Create new blob copies (true) or reference same blobs (false)
}
// cp copies files and directories from source path to destination
@@ -25,11 +25,7 @@ pub mut:
// fs.cp('/src/*.v', '/backup/', FindOptions{recursive: true}, CopyOptions{overwrite: true})!
// ```
pub fn (mut self Fs) cp(src_path string, dest_path string, find_opts FindOptions, copy_opts CopyOptions) ! {
// Try to find items using the find function first
mut items := []FindResult{}
// If find fails, try to get the item directly by path
items = self.find(src_path, find_opts) or {
mut items := self.find(src_path, find_opts) or {
// Try to get specific file, directory, or symlink by exact path
mut direct_items := []FindResult{}
@@ -64,191 +60,223 @@ pub fn (mut self Fs) cp(src_path string, dest_path string, find_opts FindOptions
return error('No items found matching pattern: ${src_path}')
}
// Determine destination directory
mut dest_dir_id := u32(0)
is_dest_dir := dest_path.ends_with('/') || self.get_dir_by_absolute_path(dest_path) or { FsDir{} } != FsDir{}
// Check if destination is an existing directory
if dest_dir := self.get_dir_by_absolute_path(dest_path) {
dest_dir_id = dest_dir.id
} else {
// If destination doesn't exist as directory, treat it as a directory path to create
// or as a parent directory if it looks like a file path
mut dir_to_create := dest_path
if !dest_path.ends_with('/') && items.len == 1 && items[0].result_type == .file {
// Single file copy to a specific filename - use parent directory
path_parts := dest_path.trim_left('/').split('/')
if path_parts.len > 1 {
dir_to_create = '/' + path_parts[..path_parts.len - 1].join('/')
} else {
dir_to_create = '/'
}
if items.len > 1 && !is_dest_dir {
return error('Cannot copy multiple items to a single file path: ${dest_path}')
}
// Create the destination directory if it doesn't exist
if dir_to_create != '/' {
self.factory.fs_dir.create_path(self.id, dir_to_create)!
}
dest_dir_id = self.get_dir_by_absolute_path(dir_to_create)!.id
}
// Copy each found item
for item in items {
match item.result_type {
.file {
self.copy_file(item.id, dest_dir_id, copy_opts)!
self.copy_file(item.id, dest_path, copy_opts)!
}
.directory {
if copy_opts.recursive {
self.copy_directory(item.id, dest_dir_id, copy_opts)!
if !copy_opts.recursive {
return error('Cannot copy directory "${item.path}" without recursive option')
}
self.copy_directory(item.id, dest_path, copy_opts)!
}
.symlink {
self.copy_symlink(item.id, dest_dir_id, copy_opts)!
self.copy_symlink(item.id, dest_path, copy_opts)!
}
}
}
}
// copy_file copies a single file to a destination directory
fn (mut self Fs) copy_file(file_id u32, dest_dir_id u32, opts CopyOptions) ! {
// copy_file copies a single file to a destination path
fn (mut self Fs) copy_file(file_id u32, dest_path string, opts CopyOptions) ! {
original_file := self.factory.fs_file.get(file_id)!
is_dest_dir := dest_path.ends_with('/') || self.get_dir_by_absolute_path(dest_path) or { FsDir{} } != FsDir{}
dest_dir_id := if is_dest_dir {
self.factory.fs_dir.create_path(self.id, dest_path)!
} else {
self.factory.fs_dir.create_path(self.id, os.dir(dest_path))!
}
file_name := if is_dest_dir { original_file.name } else { os.file_name(dest_path) }
dest_dir := self.factory.fs_dir.get(dest_dir_id)!
// Check if file already exists in destination
for existing_file_id in dest_dir.files {
existing_file := self.factory.fs_file.get(existing_file_id)!
if existing_file.name == original_file.name {
if existing_file_id := self.find_file_in_dir(file_name, dest_dir) {
if !opts.overwrite {
return error('File "${original_file.name}" already exists in destination directory')
return error('File "${file_name}" already exists in destination')
}
// Remove existing file
self.factory.fs_file.delete(existing_file_id)!
break
}
}
// Create new blobs or reference existing ones
mut new_blob_ids := []u32{}
if opts.copy_blobs {
// Create new blob copies
for blob_id in original_file.blobs {
original_blob := self.factory.fs_blob.get(blob_id)!
mut new_blob := self.factory.fs_blob.new(data: original_blob.data)!
new_blob = self.factory.fs_blob.set(new_blob)!
new_blob_ids << new_blob.id
o_blob := self.factory.fs_blob.get(blob_id)!
mut n_blob := self.factory.fs_blob.new(data: o_blob.data)!
n_blob = self.factory.fs_blob.set(n_blob)!
new_blob_ids << n_blob.id
}
} else {
// Reference the same blobs
new_blob_ids = original_file.blobs.clone()
}
// Create new file
mut new_file := self.factory.fs_file.new(
name: original_file.name
fs_id: self.id
blobs: new_blob_ids
mime_type: original_file.mime_type
metadata: original_file.metadata.clone()
name: file_name,
fs_id: self.id,
blobs: new_blob_ids,
mime_type: original_file.mime_type,
metadata: original_file.metadata.clone(),
)!
new_file = self.factory.fs_file.set(new_file)!
self.factory.fs_file.add_to_directory(new_file.id, dest_dir_id)!
}
// copy_directory copies a directory and optionally its contents recursively
fn (mut self Fs) copy_directory(dir_id u32, dest_parent_id u32, opts CopyOptions) ! {
// copy_directory copies a directory and its contents recursively to a destination path
fn (mut self Fs) copy_directory(dir_id u32, dest_path string, opts CopyOptions) ! {
original_dir := self.factory.fs_dir.get(dir_id)!
dest_parent := self.factory.fs_dir.get(dest_parent_id)!
// Check if directory already exists in destination
for existing_dir_id in dest_parent.directories {
existing_dir := self.factory.fs_dir.get(existing_dir_id)!
if existing_dir.name == original_dir.name {
if !opts.overwrite {
return error('Directory "${original_dir.name}" already exists in destination')
is_dest_dir := dest_path.ends_with('/') || self.get_dir_by_absolute_path(dest_path) or { FsDir{} } != FsDir{}
dest_dir_name := if is_dest_dir { original_dir.name } else { os.file_name(dest_path) }
parent_dest_dir_id := if is_dest_dir {
self.factory.fs_dir.create_path(self.id, dest_path)!
} else {
self.factory.fs_dir.create_path(self.id, os.dir(dest_path))!
}
// For directories, we merge rather than replace when overwrite is true
parent_dest_dir := self.factory.fs_dir.get(parent_dest_dir_id)!
if existing_dir_id := self.find_dir_in_dir(dest_dir_name, parent_dest_dir) {
if opts.recursive {
// Copy contents into existing directory
self.copy_directory_contents(dir_id, existing_dir_id, opts)!
}
return
}
if !opts.overwrite {
return error('Directory "${dest_dir_name}" already exists in destination')
}
self.factory.fs_dir.delete(existing_dir_id)!
}
// Create new directory
mut new_dir := self.factory.fs_dir.new(
name: original_dir.name
fs_id: self.id
parent_id: dest_parent_id
description: original_dir.description
name: dest_dir_name,
fs_id: self.id,
parent_id: parent_dest_dir_id,
description: original_dir.description,
)!
new_dir = self.factory.fs_dir.set(new_dir)!
self.factory.fs_dir.set(new_dir)!
mut parent_dir := self.factory.fs_dir.get(parent_dest_dir_id)!
if new_dir.id !in parent_dir.directories {
parent_dir.directories << new_dir.id
self.factory.fs_dir.set(parent_dir)!
}
// Add to parent's directories list
mut parent := self.factory.fs_dir.get(dest_parent_id)!
parent.directories << new_dir.id
self.factory.fs_dir.set(parent)!
// Copy contents if recursive
if opts.recursive {
self.copy_directory_contents(dir_id, new_dir.id, opts)!
}
}
// copy_directory_contents copies all contents of a directory to another directory
// copy_directory_contents copies the contents of one directory to another
fn (mut self Fs) copy_directory_contents(src_dir_id u32, dest_dir_id u32, opts CopyOptions) ! {
src_dir := self.factory.fs_dir.get(src_dir_id)!
// Copy all files
for file_id in src_dir.files {
self.copy_file(file_id, dest_dir_id, opts)!
self.copy_file(file_id, dest_dir_id.str(), opts)!
}
// Copy all symlinks
for symlink_id in src_dir.symlinks {
self.copy_symlink(symlink_id, dest_dir_id, opts)!
}
// Copy all subdirectories recursively
for subdir_id in src_dir.directories {
self.copy_directory(subdir_id, dest_dir_id, opts)!
self.copy_directory(subdir_id, dest_dir_id.str(), opts)!
}
for symlink_id in src_dir.symlinks {
self.copy_symlink(symlink_id, dest_dir_id.str(), opts)!
}
}
// copy_symlink copies a symbolic link to a destination directory
fn (mut self Fs) copy_symlink(symlink_id u32, dest_dir_id u32, opts CopyOptions) ! {
// copy_symlink copies a symbolic link to a destination path
fn (mut self Fs) copy_symlink(symlink_id u32, dest_path string, opts CopyOptions) ! {
original_symlink := self.factory.fs_symlink.get(symlink_id)!
is_dest_dir := dest_path.ends_with('/') || self.get_dir_by_absolute_path(dest_path) or { FsDir{} } != FsDir{}
dest_dir_id := if is_dest_dir {
self.factory.fs_dir.create_path(self.id, dest_path)!
} else {
self.factory.fs_dir.create_path(self.id, os.dir(dest_path))!
}
symlink_name := if is_dest_dir { original_symlink.name } else { os.file_name(dest_path) }
dest_dir := self.factory.fs_dir.get(dest_dir_id)!
// Check if symlink already exists in destination
for existing_symlink_id in dest_dir.symlinks {
existing_symlink := self.factory.fs_symlink.get(existing_symlink_id)!
if existing_symlink.name == original_symlink.name {
if existing_symlink_id := self.find_symlink_in_dir(symlink_name, dest_dir) {
if !opts.overwrite {
return error('Symlink "${original_symlink.name}" already exists in destination directory')
return error('Symlink "${symlink_name}" already exists')
}
// Remove existing symlink
self.factory.fs_symlink.delete(existing_symlink_id)!
break
}
}
// Create new symlink
mut new_symlink := self.factory.fs_symlink.new(
name: original_symlink.name
fs_id: self.id
parent_id: dest_dir_id
target_id: original_symlink.target_id
target_type: original_symlink.target_type
description: original_symlink.description
name: symlink_name,
fs_id: self.id,
parent_id: dest_dir_id,
target_id: original_symlink.target_id,
target_type: original_symlink.target_type,
description: original_symlink.description,
)!
new_symlink = self.factory.fs_symlink.set(new_symlink)!
self.factory.fs_symlink.set(new_symlink)!
// Add to parent directory's symlinks list
mut parent := self.factory.fs_dir.get(dest_dir_id)!
parent.symlinks << new_symlink.id
self.factory.fs_dir.set(parent)!
}
// find_file_in_dir finds a file in a directory by name and returns its ID
fn (mut self Fs) find_file_in_dir(file_name string, dir FsDir) ?u32 {
for file_id in dir.files {
file := self.factory.fs_file.get(file_id) or { continue }
if file.name == file_name {
return file_id
}
}
return none
}
// find_dir_in_dir finds a directory in a directory by name and returns its ID
fn (mut self Fs) find_dir_in_dir(dir_name string, dir FsDir) ?u32 {
for did in dir.directories {
d := self.factory.fs_dir.get(did) or { continue }
if d.name == dir_name {
return did
}
}
return none
}
// find_symlink_in_dir finds a symlink in a directory by name and returns its ID
fn (mut self Fs) find_symlink_in_dir(symlink_name string, dir FsDir) ?u32 {
for symlink_id in dir.symlinks {
symlink := self.factory.fs_symlink.get(symlink_id) or { continue }
if symlink.name == symlink_name {
return symlink_id
}
}
return none
}
// get_dir_path returns the absolute path for a given directory ID.
pub fn (mut self Fs) get_dir_path(dir_id u32) !string {
if dir_id == self.root_dir_id {
return '/'
}
mut path := ''
mut current_id := dir_id
for {
dir := self.factory.fs_dir.get(current_id)!
if dir.id == self.root_dir_id {
break
}
path = '/' + dir.name + path
if dir.parent_id == 0 {
break
}
current_id = dir.parent_id
}
return if path == '' { '/' } else { path }
}

View File

@@ -14,13 +14,13 @@ fn test_cp_file() ! {
// 1. Create a source directory and a file
src_dir_id := fs.factory.fs_dir.create_path(fs.id, '/src')!
mut blob := fs.factory.fs_blob.new(data: 'file content')!
mut blob := fs.factory.fs_blob.new(data: 'file content'.bytes())!
blob = fs.factory.fs_blob.set(blob)!
mut file := fs.factory.fs_file.new(
name: 'test_file.txt'
fs_id: fs.id
blobs: [blob.id]
mime_type: .text_plain
mime_type: .txt
)!
file = fs.factory.fs_file.set(file)!
fs.factory.fs_file.add_to_directory(file.id, src_dir_id)!
@@ -40,33 +40,33 @@ fn test_cp_file() ! {
}
fn test_cp_file_overwrite() ! {
mut fs := delete_fs_test() or {panic("bug")}
mut fs := new_fs_test() or { panic(err) }
defer {
delete_fs_test() or {}
}
// 1. Create a source directory and a file
src_dir_id := fs.factory.fs_dir.create_path(fs.id, '/src')!
mut blob1 := fs.factory.fs_blob.new(data: 'original content')!
mut blob1 := fs.factory.fs_blob.new(data: 'original content'.bytes())!
blob1 = fs.factory.fs_blob.set(blob1)!
mut file1 := fs.factory.fs_file.new(
name: 'overwrite_file.txt'
fs_id: fs.id
blobs: [blob1.id]
mime_type: .text_plain
mime_type: .txt
)!
file1 = fs.factory.fs_file.set(file1)!
fs.factory.fs_file.add_to_directory(file1.id, src_dir_id)!
// 2. Create a destination directory and an existing file with the same name
dest_dir_id := fs.factory.fs_dir.create_path(fs.id, '/dest')!
mut blob_existing := fs.factory.fs_blob.new(data: 'existing content')!
mut blob_existing := fs.factory.fs_blob.new(data: 'existing content'.bytes())!
blob_existing = fs.factory.fs_blob.set(blob_existing)!
mut existing_file := fs.factory.fs_file.new(
name: 'overwrite_file.txt'
fs_id: fs.id
blobs: [blob_existing.id]
mime_type: .text_plain
mime_type: .txt
)!
existing_file = fs.factory.fs_file.set(existing_file)!
fs.factory.fs_file.add_to_directory(existing_file.id, dest_dir_id)!
@@ -83,44 +83,47 @@ fn test_cp_file_overwrite() ! {
}
fn test_cp_file_no_overwrite_error() ! {
mut fs := delete_fs_test() or {panic("bug")}
mut fs := new_fs_test() or { panic(err) }
defer {
delete_fs_test() or {}
}
// 1. Create a source directory and a file
src_dir_id := fs.factory.fs_dir.create_path(fs.id, '/src')!
mut blob1 := fs.factory.fs_blob.new(data: 'original content')!
mut blob1 := fs.factory.fs_blob.new(data: 'original content'.bytes())!
blob1 = fs.factory.fs_blob.set(blob1)!
mut file1 := fs.factory.fs_file.new(
name: 'no_overwrite_file.txt'
fs_id: fs.id
blobs: [blob1.id]
mime_type: .text_plain
mime_type: .txt
)!
file1 = fs.factory.fs_file.set(file1)!
fs.factory.fs_file.add_to_directory(file1.id, src_dir_id)!
// 2. Create a destination directory and an existing file with the same name
dest_dir_id := fs.factory.fs_dir.create_path(fs.id, '/dest')!
mut blob_existing := fs.factory.fs_blob.new(data: 'existing content')!
mut blob_existing := fs.factory.fs_blob.new(data: 'existing content'.bytes())!
blob_existing = fs.factory.fs_blob.set(blob_existing)!
mut existing_file := fs.factory.fs_file.new(
name: 'no_overwrite_file.txt'
fs_id: fs.id
blobs: [blob_existing.id]
mime_type: .text_plain
mime_type: .txt
)!
existing_file = fs.factory.fs_file.set(existing_file)!
fs.factory.fs_file.add_to_directory(existing_file.id, dest_dir_id)!
// 3. Attempt to copy the file without overwrite (should error)
res := fs.cp('/src/no_overwrite_file.txt', '/dest/', FindOptions{}, CopyOptions{overwrite: false})
assert res.err().msg().contains('already exists')
fs.cp('/src/no_overwrite_file.txt', '/dest/', FindOptions{}, CopyOptions{overwrite: false}) or {
assert err.msg().contains('already exists')
return
}
assert false, 'Should have failed'
}
fn test_cp_directory_recursive() ! {
mut fs := delete_fs_test() or {panic("bug")}
mut fs := new_fs_test() or { panic(err) }
defer {
delete_fs_test() or {}
}
@@ -129,24 +132,24 @@ fn test_cp_directory_recursive() ! {
src_root_id := fs.factory.fs_dir.create_path(fs.id, '/src_root')!
src_subdir_id := fs.factory.fs_dir.create_path(fs.id, '/src_root/subdir')!
mut blob1 := fs.factory.fs_blob.new(data: 'file1 content')!
mut blob1 := fs.factory.fs_blob.new(data: 'file1 content'.bytes())!
blob1 = fs.factory.fs_blob.set(blob1)!
mut file1 := fs.factory.fs_file.new(
name: 'file1.txt'
fs_id: fs.id
blobs: [blob1.id]
mime_type: .text_plain
mime_type: .txt
)!
file1 = fs.factory.fs_file.set(file1)!
fs.factory.fs_file.add_to_directory(file1.id, src_root_id)!
mut blob2 := fs.factory.fs_blob.new(data: 'file2 content')!
mut blob2 := fs.factory.fs_blob.new(data: 'file2 content'.bytes())!
blob2 = fs.factory.fs_blob.set(blob2)!
mut file2 := fs.factory.fs_file.new(
name: 'file2.txt'
fs_id: fs.id
blobs: [blob2.id]
mime_type: .text_plain
mime_type: .txt
)!
file2 = fs.factory.fs_file.set(file2)!
fs.factory.fs_file.add_to_directory(file2.id, src_subdir_id)!
@@ -159,97 +162,84 @@ fn test_cp_directory_recursive() ! {
// 4. Verify destination structure
dest_root := fs.factory.fs_dir.get(dest_root_id)!
assert dest_root.directories.len == 1 // Should contain 'src_root'
assert dest_root.directories.len == 1, 'dest_root should contain src_root'
copied_src_root_dir := fs.factory.fs_dir.get(dest_root.directories[0])!
assert copied_src_root_dir.name == 'src_root'
assert copied_src_root_dir.files.len == 1 // Should contain file1.txt
assert copied_src_root_dir.name == 'src_root', 'copied directory should be named src_root'
assert copied_src_root_dir.files.len == 1, 'src_root should contain 1 file'
copied_subdir := fs.factory.fs_dir.get(copied_src_root_dir.directories[0])!
assert copied_subdir.name == 'subdir'
assert copied_subdir.files.len == 1 // Should contain file2.txt
assert copied_subdir.name == 'subdir', 'copied subdirectory should be named subdir'
assert copied_subdir.files.len == 1, 'subdir should contain 1 file'
}
fn test_cp_directory_merge_overwrite() ! {
mut fs := delete_fs_test() or {panic("bug")}
mut fs := new_fs_test() or { panic(err) }
defer {
delete_fs_test() or {}
}
// 1. Create source directory structure
src_dir_id := fs.factory.fs_dir.create_path(fs.id, '/src')!
mut blob1 := fs.factory.fs_blob.new(data: 'src file content')!
mut blob1 := fs.factory.fs_blob.new(data: 'src file content'.bytes())!
blob1 = fs.factory.fs_blob.set(blob1)!
mut file1 := fs.factory.fs_file.new(
name: 'file1.txt'
fs_id: fs.id
blobs: [blob1.id]
mime_type: .text_plain
mime_type: .txt
)!
file1 = fs.factory.fs_file.set(file1)!
fs.factory.fs_file.add_to_directory(file1.id, src_dir_id)!
// 2. Create destination directory with an existing file and a new file
dest_dir_id := fs.factory.fs_dir.create_path(fs.id, '/dest')!
mut blob_existing := fs.factory.fs_blob.new(data: 'existing file content')!
mut blob_existing := fs.factory.fs_blob.new(data: 'existing file content'.bytes())!
blob_existing = fs.factory.fs_blob.set(blob_existing)!
mut existing_file := fs.factory.fs_file.new(
name: 'file1.txt' // Same name as source file
fs_id: fs.id
blobs: [blob_existing.id]
mime_type: .text_plain
mime_type: .txt
)!
existing_file = fs.factory.fs_file.set(existing_file)!
fs.factory.fs_file.add_to_directory(existing_file.id, dest_dir_id)!
mut blob_new_dest := fs.factory.fs_blob.new(data: 'new dest file content')!
mut blob_new_dest := fs.factory.fs_blob.new(data: 'new dest file content'.bytes())!
blob_new_dest = fs.factory.fs_blob.set(blob_new_dest)!
mut new_dest_file := fs.factory.fs_file.new(
name: 'file_only_in_dest.txt'
fs_id: fs.id
blobs: [blob_new_dest.id]
mime_type: .text_plain
mime_type: .txt
)!
new_dest_file = fs.factory.fs_file.set(new_dest_file)!
fs.factory.fs_file.add_to_directory(new_dest_file.id, dest_dir_id)!
// 3. Copy source directory to destination with overwrite (should merge and overwrite file1.txt)
fs.cp('/src', '/dest/', FindOptions{}, CopyOptions{recursive: true, overwrite: true})!
fs.cp('/src', '/dest', FindOptions{}, CopyOptions{recursive: true, overwrite: true})!
// 4. Verify destination contents
dest_dir := fs.factory.fs_dir.get(dest_dir_id)!
assert dest_dir.files.len == 2 // Should have file1.txt (overwritten) and file_only_in_dest.txt
mut found_file1 := false
mut found_file_only_in_dest := false
for file_id in dest_dir.files {
file := fs.factory.fs_file.get(file_id)!
if file.name == 'file1.txt' {
assert file.blobs[0] == blob1.id // Should be overwritten with source content
found_file1 = true
} else if file.name == 'file_only_in_dest.txt' {
assert file.blobs[0] == blob_new_dest.id
found_file_only_in_dest = true
}
}
assert found_file1
assert found_file_only_in_dest
assert dest_dir.files.len == 2, 'dest_dir should have 2 files after merge'
mut file_names := dest_dir.files.map(fs.factory.fs_file.get(it)!.name)
assert 'file1.txt' in file_names
assert 'file_only_in_dest.txt' in file_names
}
fn test_cp_file_to_non_existent_path() ! {
mut fs := delete_fs_test() or {panic("bug")}
mut fs := new_fs_test() or { panic(err) }
defer {
delete_fs_test() or {}
}
// 1. Create a source file
mut blob := fs.factory.fs_blob.new(data: 'content')!
mut blob := fs.factory.fs_blob.new(data: 'content'.bytes())!
blob = fs.factory.fs_blob.set(blob)!
mut file := fs.factory.fs_file.new(
name: 'source.txt'
fs_id: fs.id
blobs: [blob.id]
mime_type: .text_plain
mime_type: .txt
)!
file = fs.factory.fs_file.set(file)!
fs.factory.fs_file.add_to_directory(file.id, fs.root_dir_id)!
@@ -266,19 +256,19 @@ fn test_cp_file_to_non_existent_path() ! {
}
fn test_cp_symlink() ! {
mut fs := delete_fs_test() or {panic("bug")}
mut fs := new_fs_test() or { panic(err) }
defer {
delete_fs_test() or {}
}
// 1. Create a target file
mut blob := fs.factory.fs_blob.new(data: 'target content')!
mut blob := fs.factory.fs_blob.new(data: 'target content'.bytes())!
blob = fs.factory.fs_blob.set(blob)!
mut target_file := fs.factory.fs_file.new(
name: 'target.txt'
fs_id: fs.id
blobs: [blob.id]
mime_type: .text_plain
mime_type: .txt
)!
target_file = fs.factory.fs_file.set(target_file)!
fs.factory.fs_file.add_to_directory(target_file.id, fs.root_dir_id)!
@@ -312,19 +302,19 @@ fn test_cp_symlink() ! {
}
fn test_cp_symlink_overwrite() ! {
mut fs := delete_fs_test() or {panic("bug")}
mut fs := new_fs_test() or { panic(err) }
defer {
delete_fs_test() or {}
}
// 1. Create a target file
mut blob := fs.factory.fs_blob.new(data: 'target content')!
mut blob := fs.factory.fs_blob.new(data: 'target content'.bytes())!
blob = fs.factory.fs_blob.set(blob)!
mut target_file := fs.factory.fs_file.new(
name: 'target.txt'
fs_id: fs.id
blobs: [blob.id]
mime_type: .text_plain
mime_type: .txt
)!
target_file = fs.factory.fs_file.set(target_file)!
fs.factory.fs_file.add_to_directory(target_file.id, fs.root_dir_id)!
@@ -348,7 +338,7 @@ fn test_cp_symlink_overwrite() ! {
name: 'other_target.txt'
fs_id: fs.id
blobs: [blob.id]
mime_type: .text_plain
mime_type: .txt
)!
other_target_file = fs.factory.fs_file.set(other_target_file)!
fs.factory.fs_file.add_to_directory(other_target_file.id, fs.root_dir_id)!
@@ -377,20 +367,20 @@ fn test_cp_symlink_overwrite() ! {
}
fn test_cp_file_copy_blobs_false() ! {
mut fs := delete_fs_test() or {panic("bug")}
mut fs := new_fs_test() or { panic(err) }
defer {
delete_fs_test() or {}
}
// 1. Create a source directory and a file
src_dir_id := fs.factory.fs_dir.create_path(fs.id, '/src')!
mut blob := fs.factory.fs_blob.new(data: 'file content')!
mut blob := fs.factory.fs_blob.new(data: 'file content'.bytes())!
blob = fs.factory.fs_blob.set(blob)!
mut file := fs.factory.fs_file.new(
name: 'test_file.txt'
fs_id: fs.id
blobs: [blob.id]
mime_type: .text_plain
mime_type: .txt
)!
file = fs.factory.fs_file.set(file)!
fs.factory.fs_file.add_to_directory(file.id, src_dir_id)!
@@ -410,20 +400,20 @@ fn test_cp_file_copy_blobs_false() ! {
}
fn test_cp_file_copy_blobs_true() ! {
mut fs := delete_fs_test() or {panic("bug")}
mut fs := new_fs_test() or { panic(err) }
defer {
delete_fs_test() or {panic("bug")}
delete_fs_test() or {}
}
// 1. Create a source directory and a file
src_dir_id := fs.factory.fs_dir.create_path(fs.id, '/src')!
mut blob := fs.factory.fs_blob.new(data: 'file content')!
mut blob := fs.factory.fs_blob.new(data: 'file content'.bytes())!
blob = fs.factory.fs_blob.set(blob)!
mut file := fs.factory.fs_file.new(
name: 'test_file.txt'
fs_id: fs.id
blobs: [blob.id]
mime_type: .text_plain
mime_type: .txt
)!
file = fs.factory.fs_file.set(file)!
fs.factory.fs_file.add_to_directory(file.id, src_dir_id)!
@@ -443,5 +433,5 @@ fn test_cp_file_copy_blobs_true() ! {
// Verify the content of the new blob is the same
new_blob := fs.factory.fs_blob.get(copied_file.blobs[0])!
assert new_blob.data == 'file content'
assert new_blob.data == 'file content'.bytes()
}

View File

@@ -1,41 +1,98 @@
module herofs
import os
// join_path joins path components
pub fn join_path(base string, component string) string {
if base == '/' {
return '/${component}'
}
return '${base}/${component}'
// Helper function to check if name matches include/exclude patterns
fn matches_pattern(name string, patterns []string) bool {
if patterns.len == 0 {
return true // No patterns means include everything
}
// normalize_path normalizes a path
pub fn normalize_path(p string) string {
return p.replace("'", '')
}
for pattern in patterns {
if pattern.contains('*') {
prefix := pattern.all_before('*')
suffix := pattern.all_after('*')
// should_include checks if a name should be included based on patterns
pub fn should_include(name string, include_patterns []string, exclude_patterns []string) bool {
// Exclude based on exclude_patterns
for pattern in exclude_patterns {
if name.contains(pattern.replace('*', '')) {
return false
}
}
// If include_patterns is empty, include everything not excluded
if include_patterns.len == 0 {
if prefix == '' && suffix == '' {
return true // Pattern is just "*"
} else if prefix == '' {
if name.ends_with(suffix) {
return true
}
// Include based on include_patterns
for pattern in include_patterns {
if name.contains(pattern.replace('*', '')) {
} 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
}
}
// If include_patterns is not empty and no pattern matched, exclude
return false
}
// Check if item should be included based on patterns
fn should_include(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
}
// Normalize path by removing trailing slashes and handling edge cases
fn normalize_path(path string) string {
if path == '' || path == '/' {
return '/'
}
return path.trim_right('/')
}
// Split path into directory and filename parts
fn split_path(path string) (string, string) {
normalized := normalize_path(path)
if normalized == '/' {
return '/', ''
}
mut dir_path := normalized.all_before_last('/')
filename := normalized.all_after_last('/')
if dir_path == '' {
dir_path = '/'
}
return dir_path, filename
}
// Get the parent path of a given path
fn parent_path(path string) string {
normalized := normalize_path(path)
if normalized == '/' {
return '/'
}
parent := normalized.all_before_last('/')
if parent == '' {
return '/'
}
return parent
}
// Join path components
fn join_path(base string, component string) string {
normalized_base := normalize_path(base)
if normalized_base == '/' {
return '/' + component
}
return normalized_base + '/' + component
}

View File

@@ -0,0 +1,360 @@
module herofs
fn test_basic_operations() ! {
mut fs := new_fs_test() or { panic(err) }
defer {
delete_fs_test() or {}
}
// Test basic file creation and retrieval
mut test_blob := fs.factory.fs_blob.new(data: 'Hello, HeroFS!'.bytes())!
test_blob = fs.factory.fs_blob.set(test_blob)!
mut test_file := fs.factory.fs_file.new(
name: 'test.txt'
fs_id: fs.id
blobs: [test_blob.id]
mime_type: .txt
)!
test_file = fs.factory.fs_file.set(test_file)!
fs.factory.fs_file.add_to_directory(test_file.id, fs.root_dir_id)!
// Verify file was created
retrieved_file := fs.factory.fs_file.get(test_file.id)!
assert retrieved_file.name == 'test.txt'
assert retrieved_file.blobs.len == 1
// Test directory creation using create_path
src_dir_id := fs.factory.fs_dir.create_path(fs.id, '/src')!
docs_dir_id := fs.factory.fs_dir.create_path(fs.id, '/docs')!
tests_dir_id := fs.factory.fs_dir.create_path(fs.id, '/tests')!
// Verify directories were created
src_dir := fs.factory.fs_dir.get(src_dir_id)!
assert src_dir.name == 'src'
docs_dir := fs.factory.fs_dir.get(docs_dir_id)!
assert docs_dir.name == 'docs'
tests_dir := fs.factory.fs_dir.get(tests_dir_id)!
assert tests_dir.name == 'tests'
// Test blob creation and hash-based retrieval
test_data := 'Test blob content'.bytes()
mut test_blob2 := fs.factory.fs_blob.new(data: test_data)!
test_blob2 = fs.factory.fs_blob.set(test_blob2)!
// Test hash-based retrieval
retrieved_blob := fs.factory.fs_blob.get_by_hash(test_blob2.hash)!
assert retrieved_blob.data == test_data
// Test blob existence by hash
exists := fs.factory.fs_blob.exists_by_hash(test_blob2.hash)!
assert exists == true
// Test blob integrity verification
assert test_blob2.verify_integrity() == true
println(' Basic operations test passed!')
}
fn test_rm_file() ! {
mut fs := new_fs_test() or { panic(err) }
defer {
delete_fs_test() or {}
}
// Create a file to remove
mut test_blob := fs.factory.fs_blob.new(data: 'File to remove'.bytes())!
test_blob = fs.factory.fs_blob.set(test_blob)!
mut test_file := fs.factory.fs_file.new(
name: 'to_remove.txt'
fs_id: fs.id
blobs: [test_blob.id]
mime_type: .txt
)!
test_file = fs.factory.fs_file.set(test_file)!
fs.factory.fs_file.add_to_directory(test_file.id, fs.root_dir_id)!
// Verify file exists before removal
assert fs.factory.fs_file.exist(test_file.id)! == true
// Test rm with file path
fs.rm('/to_remove.txt', FindOptions{}, RemoveOptions{})!
// Verify file no longer exists
assert fs.factory.fs_file.exist(test_file.id)! == false
// Verify blob still exists (default behavior)
assert fs.factory.fs_blob.exist(test_blob.id)! == true
println(' Remove file test passed!')
}
fn test_rm_file_with_blobs() ! {
mut fs := new_fs_test() or { panic(err) }
defer {
delete_fs_test() or {}
}
// Create a file to remove with delete_blobs option
mut test_blob := fs.factory.fs_blob.new(data: 'File to remove with blobs'.bytes())!
test_blob = fs.factory.fs_blob.set(test_blob)!
mut test_file := fs.factory.fs_file.new(
name: 'to_remove_with_blobs.txt'
fs_id: fs.id
blobs: [test_blob.id]
mime_type: .txt
)!
test_file = fs.factory.fs_file.set(test_file)!
fs.factory.fs_file.add_to_directory(test_file.id, fs.root_dir_id)!
// Verify file and blob exist before removal
assert fs.factory.fs_file.exist(test_file.id)! == true
assert fs.factory.fs_blob.exist(test_blob.id)! == true
// Test rm with delete_blobs option
fs.rm('/to_remove_with_blobs.txt', FindOptions{}, RemoveOptions{delete_blobs: true})!
// Verify file no longer exists
assert fs.factory.fs_file.exist(test_file.id)! == false
// Verify blob is also deleted
assert fs.factory.fs_blob.exist(test_blob.id)! == false
println(' Remove file with blobs test passed!')
}
fn test_rm_directory() ! {
mut fs_factory := new()!
mut test_fs := fs_factory.fs.new(
name: 'rm_dir_test'
description: 'Test filesystem for remove directory operations'
quota_bytes: 1024 * 1024 * 50 // 50MB quota
)!
test_fs = fs_factory.fs.set(test_fs)!
// Create root directory
mut root_dir := fs_factory.fs_dir.new(
name: 'root'
fs_id: test_fs.id
parent_id: 0 // Root has no parent
)!
root_dir = fs_factory.fs_dir.set(root_dir)!
test_fs.root_dir_id = root_dir.id
test_fs = fs_factory.fs.set(test_fs)!
// Create a directory to remove
test_dir_id := fs_factory.fs_dir.create_path(test_fs.id, '/test_dir')!
test_dir := fs_factory.fs_dir.get(test_dir_id)!
assert test_dir.name == 'test_dir'
// Test rm with directory path
test_fs.rm('/test_dir', FindOptions{}, RemoveOptions{})!
// Verify directory no longer exists
assert fs_factory.fs_dir.exist(test_dir_id)! == false
println(' Remove directory test passed!')
}
fn test_rm_directory_recursive() ! {
mut fs_factory := new()!
mut test_fs := fs_factory.fs.new(
name: 'rm_dir_recursive_test'
description: 'Test filesystem for recursive remove directory operations'
quota_bytes: 1024 * 1024 * 50 // 50MB quota
)!
test_fs = fs_factory.fs.set(test_fs)!
// Create root directory
mut root_dir := fs_factory.fs_dir.new(
name: 'root'
fs_id: test_fs.id
parent_id: 0 // Root has no parent
)!
root_dir = fs_factory.fs_dir.set(root_dir)!
test_fs.root_dir_id = root_dir.id
test_fs = fs_factory.fs.set(test_fs)!
// Create directory structure
test_dir_id := fs_factory.fs_dir.create_path(test_fs.id, '/test_dir')!
sub_dir_id := fs_factory.fs_dir.create_path(test_fs.id, '/test_dir/sub_dir')!
// Create a file in the directory
mut test_blob := fs_factory.fs_blob.new(data: 'File in directory'.bytes())!
test_blob = fs_factory.fs_blob.set(test_blob)!
mut test_file := fs_factory.fs_file.new(
name: 'file_in_dir.txt'
fs_id: test_fs.id
blobs: [test_blob.id]
mime_type: .txt
)!
test_file = fs_factory.fs_file.set(test_file)!
fs_factory.fs_file.add_to_directory(test_file.id, test_dir_id)!
// Verify directory and file exist before removal
assert fs_factory.fs_dir.exist(test_dir_id)! == true
assert fs_factory.fs_dir.exist(sub_dir_id)! == true
assert fs_factory.fs_file.exist(test_file.id)! == true
// Test rm with recursive option
test_fs.rm('/test_dir', FindOptions{}, RemoveOptions{recursive: true})!
// Verify directory and its contents are removed
assert fs_factory.fs_dir.exist(test_dir_id)! == false
assert fs_factory.fs_dir.exist(sub_dir_id)! == false
assert fs_factory.fs_file.exist(test_file.id)! == false
println(' Remove directory recursively test passed!')
}
fn test_mv_file() ! {
mut fs_factory := new()!
mut test_fs := fs_factory.fs.new(
name: 'mv_file_test'
description: 'Test filesystem for move file operations'
quota_bytes: 1024 * 1024 * 50 // 50MB quota
)!
test_fs = fs_factory.fs.set(test_fs)!
// Create root directory
mut root_dir := fs_factory.fs_dir.new(
name: 'root'
fs_id: test_fs.id
parent_id: 0 // Root has no parent
)!
root_dir = fs_factory.fs_dir.set(root_dir)!
test_fs.root_dir_id = root_dir.id
test_fs = fs_factory.fs.set(test_fs)!
// Create source directory
src_dir_id := fs_factory.fs_dir.create_path(test_fs.id, '/src')!
// Create destination directory
dest_dir_id := fs_factory.fs_dir.create_path(test_fs.id, '/dest')!
// Create a file to move
mut test_blob := fs_factory.fs_blob.new(data: 'File to move'.bytes())!
test_blob = fs_factory.fs_blob.set(test_blob)!
mut test_file := fs_factory.fs_file.new(
name: 'to_move.txt'
fs_id: test_fs.id
blobs: [test_blob.id]
mime_type: .txt
)!
test_file = fs_factory.fs_file.set(test_file)!
fs_factory.fs_file.add_to_directory(test_file.id, src_dir_id)!
// Verify file exists in source directory before move
src_dir := fs_factory.fs_dir.get(src_dir_id)!
assert test_file.id in src_dir.files
// Test mv file operation
test_fs.mv('/src/to_move.txt', '/dest/', MoveOptions{})!
// Verify file no longer exists in source directory
src_dir = fs_factory.fs_dir.get(src_dir_id)!
assert test_file.id !in src_dir.files
// Verify file exists in destination directory
dest_dir := fs_factory.fs_dir.get(dest_dir_id)!
assert dest_dir.files.len == 1
moved_file := fs_factory.fs_file.get(dest_dir.files[0])!
assert moved_file.name == 'to_move.txt'
println(' Move file test passed!')
}
fn test_mv_file_rename() ! {
mut fs_factory := new()!
mut test_fs := fs_factory.fs.new(
name: 'mv_file_rename_test'
description: 'Test filesystem for move and rename file operations'
quota_bytes: 1024 * 1024 * 50 // 50MB quota
)!
test_fs = fs_factory.fs.set(test_fs)!
// Create root directory
mut root_dir := fs_factory.fs_dir.new(
name: 'root'
fs_id: test_fs.id
parent_id: 0 // Root has no parent
)!
root_dir = fs_factory.fs_dir.set(root_dir)!
test_fs.root_dir_id = root_dir.id
test_fs = fs_factory.fs.set(test_fs)!
// Create source directory
src_dir_id := fs_factory.fs_dir.create_path(test_fs.id, '/src')!
// Create a file to move and rename
mut test_blob := fs_factory.fs_blob.new(data: 'File to move and rename'.bytes())!
test_blob = fs_factory.fs_blob.set(test_blob)!
mut test_file := fs_factory.fs_file.new(
name: 'original_name.txt'
fs_id: test_fs.id
blobs: [test_blob.id]
mime_type: .txt
)!
test_file = fs_factory.fs_file.set(test_file)!
fs_factory.fs_file.add_to_directory(test_file.id, src_dir_id)!
// Test mv with rename
test_fs.mv('/src/original_name.txt', '/src/renamed_file.txt', MoveOptions{})!
// Verify file was renamed
renamed_file := fs_factory.fs_file.get(test_file.id)!
assert renamed_file.name == 'renamed_file.txt'
println(' Move file with rename test passed!')
}
fn test_mv_directory() ! {
mut fs_factory := new()!
mut test_fs := fs_factory.fs.new(
name: 'mv_dir_test'
description: 'Test filesystem for move directory operations'
quota_bytes: 1024 * 1024 * 50 // 50MB quota
)!
test_fs = fs_factory.fs.set(test_fs)!
// Create root directory
mut root_dir := fs_factory.fs_dir.new(
name: 'root'
fs_id: test_fs.id
parent_id: 0 // Root has no parent
)!
root_dir = fs_factory.fs_dir.set(root_dir)!
test_fs.root_dir_id = root_dir.id
test_fs = fs_factory.fs.set(test_fs)!
// Create source directory
src_dir_id := fs_factory.fs_dir.create_path(test_fs.id, '/src')!
// Create destination directory
dest_dir_id := fs_factory.fs_dir.create_path(test_fs.id, '/dest')!
// Create a subdirectory to move
sub_dir_id := fs_factory.fs_dir.create_path(test_fs.id, '/src/sub_dir')!
sub_dir := fs_factory.fs_dir.get(sub_dir_id)!
assert sub_dir.name == 'sub_dir'
// Test mv directory operation
test_fs.mv('/src/sub_dir', '/dest/', MoveOptions{})!
// Verify directory no longer exists in source
src_dir := fs_factory.fs_dir.get(src_dir_id)!
assert sub_dir_id !in src_dir.directories
// Verify directory exists in destination
dest_dir := fs_factory.fs_dir.get(dest_dir_id)!
assert dest_dir.directories.len == 1
moved_dir := fs_factory.fs_dir.get(dest_dir.directories[0])!
assert moved_dir.name == 'sub_dir'
assert moved_dir.parent_id == dest_dir_id
println(' Move directory test passed!')
}

View File

@@ -1,98 +0,0 @@
module herofs
// Helper function to check if name matches include/exclude patterns
fn matches_pattern(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
fn should_include(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
}
// Normalize path by removing trailing slashes and handling edge cases
fn normalize_path(path string) string {
if path == '' || path == '/' {
return '/'
}
return path.trim_right('/')
}
// Split path into directory and filename parts
fn split_path(path string) (string, string) {
normalized := normalize_path(path)
if normalized == '/' {
return '/', ''
}
mut dir_path := normalized.all_before_last('/')
filename := normalized.all_after_last('/')
if dir_path == '' {
dir_path = '/'
}
return dir_path, filename
}
// Get the parent path of a given path
fn parent_path(path string) string {
normalized := normalize_path(path)
if normalized == '/' {
return '/'
}
parent := normalized.all_before_last('/')
if parent == '' {
return '/'
}
return parent
}
// Join path components
fn join_path(base string, component string) string {
normalized_base := normalize_path(base)
if normalized_base == '/' {
return '/' + component
}
return normalized_base + '/' + component
}

View File

@@ -1,105 +0,0 @@
module herofs
// Note: This test is simplified due to V compiler namespace issues with FindOptions
// The full functionality is tested in the examples and working correctly
fn test_basic_operations() ! {
// Initialize HeroFS factory and create test filesystem
mut fs_factory := new()!
mut test_fs := fs_factory.fs.new(
name: 'basic_test'
description: 'Test filesystem for basic operations'
quota_bytes: 1024 * 1024 * 50 // 50MB quota
)!
test_fs = fs_factory.fs.set(test_fs)!
// Create root directory
mut root_dir := fs_factory.fs_dir.new(
name: 'root'
fs_id: test_fs.id
parent_id: 0 // Root has no parent
)!
root_dir = fs_factory.fs_dir.set(root_dir)!
test_fs.root_dir_id = root_dir.id
test_fs = fs_factory.fs.set(test_fs)!
// Test basic file creation and retrieval
mut test_blob := fs_factory.fs_blob.new(data: 'Hello, HeroFS!'.bytes())!
test_blob = fs_factory.fs_blob.set(test_blob)!
mut test_file := fs_factory.fs_file.new(
name: 'test.txt'
fs_id: test_fs.id
blobs: [test_blob.id]
mime_type: .txt
)!
test_file = fs_factory.fs_file.set(test_file)!
fs_factory.fs_file.add_to_directory(test_file.id, root_dir.id)!
// Verify file was created
retrieved_file := fs_factory.fs_file.get(test_file.id)!
assert retrieved_file.name == 'test.txt'
assert retrieved_file.blobs.len == 1
println(' Basic operations test passed!')
}
fn test_directory_operations() ! {
// Initialize HeroFS factory and create test filesystem
mut fs_factory := new()!
mut test_fs := fs_factory.fs.new(
name: 'dir_test'
description: 'Test filesystem for directory operations'
quota_bytes: 1024 * 1024 * 50 // 50MB quota
)!
test_fs = fs_factory.fs.set(test_fs)!
// Create root directory
mut root_dir := fs_factory.fs_dir.new(
name: 'root'
fs_id: test_fs.id
parent_id: 0 // Root has no parent
)!
root_dir = fs_factory.fs_dir.set(root_dir)!
test_fs.root_dir_id = root_dir.id
test_fs = fs_factory.fs.set(test_fs)!
// Test directory creation using create_path
src_dir_id := fs_factory.fs_dir.create_path(test_fs.id, '/src')!
docs_dir_id := fs_factory.fs_dir.create_path(test_fs.id, '/docs')!
tests_dir_id := fs_factory.fs_dir.create_path(test_fs.id, '/tests')!
// Verify directories were created
src_dir := fs_factory.fs_dir.get(src_dir_id)!
assert src_dir.name == 'src'
docs_dir := fs_factory.fs_dir.get(docs_dir_id)!
assert docs_dir.name == 'docs'
tests_dir := fs_factory.fs_dir.get(tests_dir_id)!
assert tests_dir.name == 'tests'
println(' Directory operations test passed!')
}
fn test_blob_operations() ! {
// Initialize HeroFS factory
mut fs_factory := new()!
// Test blob creation and hash-based retrieval
test_data := 'Test blob content'.bytes()
mut test_blob := fs_factory.fs_blob.new(data: test_data)!
test_blob = fs_factory.fs_blob.set(test_blob)!
// Test hash-based retrieval
retrieved_blob := fs_factory.fs_blob.get_by_hash(test_blob.hash)!
assert retrieved_blob.data == test_data
// Test blob existence by hash
exists := fs_factory.fs_blob.exists_by_hash(test_blob.hash)!
assert exists == true
// Test blob integrity verification
assert test_blob.verify_integrity() == true
println(' Blob operations test passed!')
}