diff --git a/lib/vfs/ourdb_fs/common.v b/lib/vfs/ourdb_fs/common.v new file mode 100644 index 00000000..dac024ea --- /dev/null +++ b/lib/vfs/ourdb_fs/common.v @@ -0,0 +1,38 @@ +module ourdb_fs + +import time + +// FileType represents the type of a filesystem entry +pub enum FileType { + file + directory + symlink +} + +// Metadata represents the common metadata for both files and directories +pub struct Metadata { +pub mut: + id u32 // unique identifier used as key in DB + name string // name of file or directory + file_type FileType + size u64 + created_at i64 // unix epoch timestamp + modified_at i64 // unix epoch timestamp + accessed_at i64 // unix epoch timestamp + mode u32 // file permissions + owner string + group string +} + +// Get time.Time objects from epochs +pub fn (m Metadata) created_time() time.Time { + return time.unix(m.created_at) +} + +pub fn (m Metadata) modified_time() time.Time { + return time.unix(m.modified_at) +} + +pub fn (m Metadata) accessed_time() time.Time { + return time.unix(m.accessed_at) +} diff --git a/lib/vfs/ourdb_fs/data.v b/lib/vfs/ourdb_fs/data.v new file mode 100644 index 00000000..2de227ea --- /dev/null +++ b/lib/vfs/ourdb_fs/data.v @@ -0,0 +1,10 @@ +module ourdb_fs + +// DataBlock represents a block of file data +pub struct DataBlock { +pub mut: + id u32 // Block ID + data []u8 // Actual data content + size u32 // Size of data in bytes + next u32 // ID of next block (0 if last block) +} diff --git a/lib/vfs/ourdb_fs/directory.v b/lib/vfs/ourdb_fs/directory.v new file mode 100644 index 00000000..df28d392 --- /dev/null +++ b/lib/vfs/ourdb_fs/directory.v @@ -0,0 +1,290 @@ +module ourdb_fs + +import time + +// FSEntry represents any type of filesystem entry +pub type FSEntry = Directory | File | Symlink + +// Directory represents a directory in the virtual filesystem +pub struct Directory { +pub mut: + metadata Metadata // Metadata from models_common.v + children []u32 // List of child entry IDs (instead of actual entries) + parent_id u32 // ID of parent directory (0 for root) + myvfs &OurDBFS @[skip] +} + +pub fn (mut self Directory) save() ! { + self.myvfs.save_entry(self)! +} + +// write creates a new file or writes to an existing file +pub fn (mut dir Directory) write(name string, content string) !&File { + mut file := &File{ + myvfs: dir.myvfs + } + mut is_new := true + + // Check if file exists + for child_id in dir.children { + mut entry := dir.myvfs.load_entry(child_id)! + if entry.metadata.name == name { + if mut entry is File { + mut d := entry + file = &d + is_new = false + break + } else { + return error('${name} exists but is not a file') + } + } + } + + if is_new { + // Create new file + current_time := time.now().unix() + file = &File{ + metadata: Metadata{ + id: u32(time.now().unix()) + name: name + file_type: .file + size: u64(content.len) + created_at: current_time + modified_at: current_time + accessed_at: current_time + mode: 0o644 + owner: 'user' + group: 'user' + } + data: content + parent_id: dir.metadata.id + myvfs: dir.myvfs + } + + // Save new file to DB + dir.myvfs.save_entry(file)! + + // Update children list + dir.children << file.metadata.id + dir.myvfs.save_entry(dir)! + } else { + // Update existing file + file.write(content)! + } + + return file +} + +// read reads content from a file +pub fn (mut dir Directory) read(name string) !string { + // Find file + for child_id in dir.children { + if mut entry := dir.myvfs.load_entry(child_id) { + if entry.metadata.name == name { + if mut entry is File { + return entry.read() + } else { + return error('${name} is not a file') + } + } + } + } + return error('File ${name} not found') +} + +// str returns a formatted string of directory contents (non-recursive) +pub fn (mut dir Directory) str() string { + mut result := '${dir.metadata.name}/\n' + + for child_id in dir.children { + if entry := dir.myvfs.load_entry(child_id) { + if entry is Directory { + result += ' 📁 ${entry.metadata.name}/\n' + } else if entry is File { + result += ' 📄 ${entry.metadata.name}\n' + } else if entry is Symlink { + result += ' 🔗 ${entry.metadata.name} -> ${entry.target}\n' + } + } + } + return result +} + +// printall prints the directory structure recursively +pub fn (mut dir Directory) printall(indent string) !string { + mut result := '${indent}📁 ${dir.metadata.name}/\n' + + for child_id in dir.children { + mut entry := dir.myvfs.load_entry(child_id)! + if mut entry is Directory { + result += entry.printall(indent + ' ')! + } else if entry is File { + result += '${indent} 📄 ${entry.metadata.name}\n' + } else if mut entry is Symlink { + result += '${indent} 🔗 ${entry.metadata.name} -> ${entry.target}\n' + } + } + return result +} + +// mkdir creates a new directory with default permissions +pub fn (mut dir Directory) mkdir(name string) !&Directory { + // Check if directory already exists + for child_id in dir.children { + if entry := dir.myvfs.load_entry(child_id) { + if entry.metadata.name == name { + return error('Directory ${name} already exists') + } + } + } + + current_time := time.now().unix() + mut new_dir := Directory{ + metadata: Metadata{ + id: u32(time.now().unix()) // Use timestamp as ID + name: name + file_type: .directory + created_at: current_time + modified_at: current_time + accessed_at: current_time + mode: 0o755 // default directory permissions + owner: 'user' // TODO: get from system + group: 'user' // TODO: get from system + } + children: []u32{} + parent_id: dir.metadata.id + myvfs: dir.myvfs + } + + // Save new directory to DB + dir.myvfs.save_entry(new_dir)! + + // Update children list + dir.children << new_dir.metadata.id + dir.myvfs.save_entry(dir)! + + return &new_dir +} + +// touch creates a new empty file with default permissions +pub fn (mut dir Directory) touch(name string) !&File { + // Check if file already exists + for child_id in dir.children { + if entry := dir.myvfs.load_entry(child_id) { + if entry.metadata.name == name { + return error('File ${name} already exists') + } + } + } + + current_time := time.now().unix() + mut new_file := File{ + metadata: Metadata{ + name: name + file_type: .file + size: 0 + created_at: current_time + modified_at: current_time + accessed_at: current_time + mode: 0o644 // default file permissions + owner: 'user' // TODO: get from system + group: 'user' // TODO: get from system + } + data: '' // Initialize with empty content + parent_id: dir.metadata.id + myvfs: dir.myvfs + } + + // Save new file to DB + new_file.metadata.id = dir.myvfs.save_entry(new_file)! + + // Update children list + dir.children << new_file.metadata.id + dir.myvfs.save_entry(dir)! + + return &new_file +} + +// rm removes a file or directory by name +pub fn (mut dir Directory) rm(name string) ! { + mut found := false + mut found_id := u32(0) + mut found_idx := 0 + + for i, child_id in dir.children { + if entry := dir.myvfs.load_entry(child_id) { + if entry.metadata.name == name { + found = true + found_id = child_id + found_idx = i + if entry is Directory { + if entry.children.len > 0 { + return error('Directory not empty') + } + } + break + } + } + } + + if !found { + return error('${name} not found') + } + + // Delete entry from DB + dir.myvfs.delete_entry(found_id)! + + // Update children list + dir.children.delete(found_idx) + dir.myvfs.save_entry(dir)! +} + +// get_children returns all immediate children as FSEntry objects +pub fn (mut dir Directory) children(recursive bool) ![]FSEntry { + mut entries := []FSEntry{} + + for child_id in dir.children { + entry := dir.myvfs.load_entry(child_id)! + entries << entry + if recursive { + if entry is Directory { + mut d := entry + entries << d.children(true)! + } + } + } + + return entries +} + +pub fn (mut dir Directory) delete() { + // Delete all children first + for child_id in dir.children { + dir.myvfs.delete_entry(child_id) or {} + } + + // Clear children list + dir.children.clear() + + // Save the updated directory + dir.myvfs.save_entry(dir) or {} +} + +// add_symlink adds an existing symlink to this directory +pub fn (mut dir Directory) add_symlink(symlink Symlink) ! { + // Check if name already exists + for child_id in dir.children { + if entry := dir.myvfs.load_entry(child_id) { + if entry.metadata.name == symlink.metadata.name { + return error('Entry with name ${symlink.metadata.name} already exists') + } + } + } + + // Save symlink to DB + dir.myvfs.save_entry(symlink)! + + // Add to children + dir.children << symlink.metadata.id + dir.myvfs.save_entry(dir)! +} diff --git a/lib/vfs/ourdb_fs/encoder.v b/lib/vfs/ourdb_fs/encoder.v new file mode 100644 index 00000000..e21cf94c --- /dev/null +++ b/lib/vfs/ourdb_fs/encoder.v @@ -0,0 +1,202 @@ +module ourdb_fs + +import freeflowuniverse.crystallib.data.encoder + +// encode_metadata encodes the common metadata structure +fn encode_metadata(mut e encoder.Encoder, m Metadata) { + e.add_u32(m.id) + e.add_string(m.name) + e.add_u8(u8(m.file_type)) // FileType enum as u8 + e.add_u64(m.size) + e.add_i64(m.created_at) + e.add_i64(m.modified_at) + e.add_i64(m.accessed_at) + e.add_u32(m.mode) + e.add_string(m.owner) + e.add_string(m.group) +} + +// decode_metadata decodes the common metadata structure +fn decode_metadata(mut d encoder.Decoder) Metadata { + id := d.get_u32() + name := d.get_string() + file_type_byte := d.get_u8() + size := d.get_u64() + created_at := d.get_i64() + modified_at := d.get_i64() + accessed_at := d.get_i64() + mode := d.get_u32() + owner := d.get_string() + group := d.get_string() + + return Metadata{ + id: id + name: name + file_type: unsafe { FileType(file_type_byte) } + size: size + created_at: created_at + modified_at: modified_at + accessed_at: accessed_at + mode: mode + owner: owner + group: group + } +} + +// Directory encoding/decoding + +// encode encodes a Directory to binary format +pub fn (dir Directory) encode() []u8 { + mut e := encoder.new() + e.add_u8(1) // version byte + e.add_u8(u8(FileType.directory)) // type byte + + // Encode metadata + encode_metadata(mut e, dir.metadata) + + // Encode parent_id + e.add_u32(dir.parent_id) + + // Encode children IDs + e.add_u16(u16(dir.children.len)) + for child_id in dir.children { + e.add_u32(child_id) + } + + return e.data +} + +// decode_directory decodes a binary format back to Directory +pub fn decode_directory(data []u8) !Directory { + mut d := encoder.decoder_new(data) + version := d.get_u8() + if version != 1 { + return error('Unsupported version ${version}') + } + + type_byte := d.get_u8() + if type_byte != u8(FileType.directory) { + return error('Invalid type byte for directory') + } + + // Decode metadata + metadata := decode_metadata(mut d) + + // Decode parent_id + parent_id := d.get_u32() + + // Decode children IDs + children_count := int(d.get_u16()) + mut children := []u32{cap: children_count} + + for _ in 0 .. children_count { + children << d.get_u32() + } + + return Directory{ + metadata: metadata + parent_id: parent_id + children: children + myvfs: unsafe { nil } // Will be set by caller + } +} + +// File encoding/decoding + +// encode encodes a File to binary format +pub fn (f File) encode() []u8 { + mut e := encoder.new() + e.add_u8(1) // version byte + e.add_u8(u8(FileType.file)) // type byte + + // Encode metadata + encode_metadata(mut e, f.metadata) + + // Encode parent_id + e.add_u32(f.parent_id) + + // Encode file data + e.add_string(f.data) + + return e.data +} + +// decode_file decodes a binary format back to File +pub fn decode_file(data []u8) !File { + mut d := encoder.decoder_new(data) + version := d.get_u8() + if version != 1 { + return error('Unsupported version ${version}') + } + + type_byte := d.get_u8() + if type_byte != u8(FileType.file) { + return error('Invalid type byte for file') + } + + // Decode metadata + metadata := decode_metadata(mut d) + + // Decode parent_id + parent_id := d.get_u32() + + // Decode file data + data_content := d.get_string() + + return File{ + metadata: metadata + parent_id: parent_id + data: data_content + myvfs: unsafe { nil } // Will be set by caller + } +} + +// Symlink encoding/decoding + +// encode encodes a Symlink to binary format +pub fn (sl Symlink) encode() []u8 { + mut e := encoder.new() + e.add_u8(1) // version byte + e.add_u8(u8(FileType.symlink)) // type byte + + // Encode metadata + encode_metadata(mut e, sl.metadata) + + // Encode parent_id + e.add_u32(sl.parent_id) + + // Encode target path + e.add_string(sl.target) + + return e.data +} + +// decode_symlink decodes a binary format back to Symlink +pub fn decode_symlink(data []u8) !Symlink { + mut d := encoder.decoder_new(data) + version := d.get_u8() + if version != 1 { + return error('Unsupported version ${version}') + } + + type_byte := d.get_u8() + if type_byte != u8(FileType.symlink) { + return error('Invalid type byte for symlink') + } + + // Decode metadata + metadata := decode_metadata(mut d) + + // Decode parent_id + parent_id := d.get_u32() + + // Decode target path + target := d.get_string() + + return Symlink{ + metadata: metadata + parent_id: parent_id + target: target + myvfs: unsafe { nil } // Will be set by caller + } +} diff --git a/lib/vfs/ourdb_fs/factory.v b/lib/vfs/ourdb_fs/factory.v new file mode 100644 index 00000000..8c367672 --- /dev/null +++ b/lib/vfs/ourdb_fs/factory.v @@ -0,0 +1,38 @@ +module ourdb_fs + +import os +import freeflowuniverse.crystallib.data.ourdb + +// Factory method for creating a new OurDBFS instance +@[params] +pub struct VFSParams { +pub: + data_dir string // Directory to store OurDBFS data + metadata_dir string // Directory to store OurDBFS metadata +} + +// Factory method for creating a new OurDBFS instance +pub fn new(params VFSParams) !&OurDBFS { + if !os.exists(params.data_dir) { + os.mkdir(params.data_dir) or { return error('Failed to create data directory: ${err}') } + } + if !os.exists(params.metadata_dir) { + os.mkdir(params.metadata_dir) or { + return error('Failed to create metadata directory: ${err}') + } + } + + mut db_meta := ourdb.new(path: '${params.metadata_dir}/ourdb_fs.db_meta')! // TODO: doesn't seem to be good names + mut db_data := ourdb.new(path: '${params.data_dir}/vfs_metadata.db_meta')! + + mut fs := &OurDBFS{ + root_id: 1 + block_size: 1024 * 4 + data_dir: params.data_dir + metadata_dir: params.metadata_dir + db_meta: &db_meta + db_data: &db_data + } + + return fs +} diff --git a/lib/vfs/ourdb_fs/file.v b/lib/vfs/ourdb_fs/file.v new file mode 100644 index 00000000..d3bc278d --- /dev/null +++ b/lib/vfs/ourdb_fs/file.v @@ -0,0 +1,31 @@ +module ourdb_fs + +import time + +// File represents a file in the virtual filesystem +pub struct File { +pub mut: + metadata Metadata // Metadata from models_common.v + data string // File content stored in DB + parent_id u32 // ID of parent directory + myvfs &OurDBFS @[skip] +} + +pub fn (mut f File) save() ! { + f.myvfs.save_entry(f)! +} + +// write updates the file's content +pub fn (mut f File) write(content string) ! { + f.data = content + f.metadata.size = u64(content.len) + f.metadata.modified_at = time.now().unix() + + // Save updated file to DB + f.save()! +} + +// read returns the file's content +pub fn (mut f File) read() !string { + return f.data +} diff --git a/lib/vfs/ourdb_fs/readme.md b/lib/vfs/ourdb_fs/readme.md new file mode 100644 index 00000000..9b17e681 --- /dev/null +++ b/lib/vfs/ourdb_fs/readme.md @@ -0,0 +1,159 @@ +# a OurDBFS: filesystem interface on top of ourbd + +The OurDBFS manages files and directories using unique identifiers (u32) as keys and binary data ([]u8) as values. + + +## Architecture + +### Storage Backend (the ourdb) + +- Uses a key-value store where keys are u32 and values are []u8 (bytes) +- Stores both metadata and file data in the same database +- Example usage of underlying database: + +```v +import crystallib.data.ourdb + +mut db_meta := ourdb.new(path:"/tmp/mydb")! + +// Store data +db_meta.set(1, 'Hello World'.bytes())! + +// Retrieve data +data := db_meta.get(1)! // Returns []u8 + +// Delete data +db_meta.delete(1)! +``` + +### Core Components + +#### 1. Common Metadata (common.v) + +All filesystem entries (files and directories) share common metadata: +```v +pub struct Metadata { + id u32 // unique identifier used as key in DB + name string // name of file or directory + file_type FileType + size u64 + created_at i64 // unix epoch timestamp + modified_at i64 // unix epoch timestamp + accessed_at i64 // unix epoch timestamp + mode u32 // file permissions + owner string + group string +} +``` + +#### 2. Files (file.v) +Files are represented as: +```v +pub struct File { + metadata Metadata // Common metadata + parent_id u32 // ID of parent directory + data_blocks []u32 // List of block IDs containing file data +} +``` + +#### 3. Directories (directory.v) +Directories are represented as: +```v +pub struct Directory { + metadata Metadata // Common metadata + parent_id u32 // ID of parent directory + children []u32 // List of child IDs (files and directories) +} +``` + +#### 4. Data Storage (data.v) +File data is stored in blocks: +```v +pub struct DataBlock { + id u32 // Block ID + data []u8 // Actual data content + size u32 // Size of data in bytes + next u32 // ID of next block (0 if last block) +} +``` + +### Features + +1. **Hierarchical Structure** + - Files and directories are organized in a tree structure + - Each entry maintains a reference to its parent directory + - Directories maintain a list of child entries + +2. **Metadata Management** + - Comprehensive metadata tracking including: + - Creation, modification, and access timestamps + - File permissions + - Owner and group information + - File size and type + +3. **File Operations** + - File creation and deletion + - Data block management for file content + - Future support for read/write operations + +4. **Directory Operations** + - Directory creation and deletion + - Listing directory contents (recursive and non-recursive) + - Child management + +### Implementation Details + +1. **File Types** +```v +pub enum FileType { + file + directory + symlink +} +``` + +2. **Data Block Management** + - File data is split into blocks + - Blocks are linked using the 'next' pointer + - Each block has a unique ID for retrieval + +3. **Directory Traversal** + - Supports both recursive and non-recursive listing + - Uses child IDs for efficient navigation + +### TODO Items + + +> TODO: what is implemented and what not? + +1. Directory Implementation + - Implement recursive listing functionality + - Proper cleanup of children during deletion + - ID generation system + +2. File Implementation + - Proper cleanup of data blocks + - Data block management system + - Read/Write operations + +3. General Improvements + - Transaction support + - Error handling + - Performance optimizations + - Concurrency support + + + + + + +use @encoder dir to see how to encode/decode + +make an efficient encoder for Directory +add a id u32 to directory this will be the key of the keyvalue stor used + +try to use as few as possible bytes when doing the encoding + +the first byte is a version nr, so we know if we change the encoding format we can still decode + +we will only store directories \ No newline at end of file diff --git a/lib/vfs/ourdb_fs/symlink.v b/lib/vfs/ourdb_fs/symlink.v new file mode 100644 index 00000000..117bbd11 --- /dev/null +++ b/lib/vfs/ourdb_fs/symlink.v @@ -0,0 +1,35 @@ +module ourdb_fs + +import time + +// Symlink represents a symbolic link in the virtual filesystem +pub struct Symlink { +pub mut: + metadata Metadata // Metadata from models_common.v + target string // Path that this symlink points to + parent_id u32 // ID of parent directory + myvfs &OurDBFS @[skip] +} + +pub fn (mut sl Symlink) save() ! { + sl.myvfs.save_entry(sl)! +} + +// update_target changes the symlink's target path +pub fn (mut sl Symlink) update_target(new_target string) ! { + sl.target = new_target + sl.metadata.modified_at = time.now().unix() + + // Save updated symlink to DB + sl.save() or { return error('Failed to update symlink target: ${err}') } +} + +// get_target returns the current target path +pub fn (mut sl Symlink) get_target() !string { + sl.metadata.accessed_at = time.now().unix() + + // Update access time in DB + sl.save() or { return error('Failed to update symlink access time: ${err}') } + + return sl.target +} diff --git a/lib/vfs/ourdb_fs/vfs.v b/lib/vfs/ourdb_fs/vfs.v new file mode 100644 index 00000000..91f4029b --- /dev/null +++ b/lib/vfs/ourdb_fs/vfs.v @@ -0,0 +1,97 @@ +module ourdb_fs + +import freeflowuniverse.crystallib.data.ourdb + +// OurDBFS represents the virtual filesystem +@[heap] +pub struct OurDBFS { +pub mut: + root_id u32 // ID of root directory + block_size u32 // Size of data blocks in bytes + data_dir string // Directory to store OurDBFS data + metadata_dir string // Directory where we store the metadata + db_data &ourdb.OurDB // Database instance for persistent storage + db_meta &ourdb.OurDB // Database instance for metadata storage +} + +// get_root returns the root directory +pub fn (mut fs OurDBFS) get_root() !&Directory { + // Try to load root directory from DB if it exists + if data := fs.db_meta.get(fs.root_id) { + mut loaded_root := decode_directory(data) or { + return error('Failed to decode root directory: ${err}') + } + loaded_root.myvfs = &fs + return &loaded_root + } + // Save new root to DB + mut myroot := Directory{ + metadata: Metadata{} + parent_id: 0 + myvfs: &fs + } + myroot.save()! + + return &myroot +} + +// load_entry loads an entry from the database by ID and sets up parent references +fn (mut fs OurDBFS) load_entry(id u32) !FSEntry { + if data := fs.db_meta.get(id) { + // First byte is version, second byte indicates the type + // TODO: check we dont overflow filetype (u8 in boundaries of filetype) + entry_type := unsafe { FileType(data[1]) } + + match entry_type { + .directory { + mut dir := decode_directory(data) or { + return error('Failed to decode directory: ${err}') + } + dir.myvfs = unsafe { &fs } + return dir + } + .file { + mut file := decode_file(data) or { return error('Failed to decode file: ${err}') } + file.myvfs = unsafe { &fs } + return file + } + .symlink { + mut symlink := decode_symlink(data) or { + return error('Failed to decode symlink: ${err}') + } + symlink.myvfs = unsafe { &fs } + return symlink + } + } + } + return error('Entry not found') +} + +// save_entry saves an entry to the database +pub fn (mut fs OurDBFS) save_entry(entry FSEntry) !u32 { + match entry { + Directory { + encoded := entry.encode() + return fs.db_meta.set(entry.metadata.id, encoded) or { + return error('Failed to save directory on id:${entry.metadata.id}: ${err}') + } + } + File { + encoded := entry.encode() + return fs.db_meta.set(entry.metadata.id, encoded) or { + return error('Failed to save file on id:${entry.metadata.id}: ${err}') + } + } + Symlink { + encoded := entry.encode() + return fs.db_meta.set(entry.metadata.id, encoded) or { + return error('Failed to save symlink on id:${entry.metadata.id}: ${err}') + } + } + } +} + +// delete_entry deletes an entry from the database +pub fn (mut fs OurDBFS) delete_entry(id u32) ! { + fs.db_meta.delete(id) or { return error('Failed to delete entry: ${err}') } +} diff --git a/lib/vfs/vfscore/README.md b/lib/vfs/vfscore/README.md new file mode 100644 index 00000000..61311d6a --- /dev/null +++ b/lib/vfs/vfscore/README.md @@ -0,0 +1,127 @@ +# Virtual File System (vfscore) Module + +> is the interface, should not have an implementation + +This module provides a pluggable virtual filesystem interface with one default implementation done for local. + +1. Local filesystem implementation (direct passthrough to OS filesystem) +2. OurDB-based implementation (stores files and metadata in OurDB) + +## Interface + +The vfscore interface defines common operations for filesystem manipulation using a consistent naming pattern of `$subject_$method`: + +### File Operations +- `file_create(path string) !FSEntry` +- `file_read(path string) ![]u8` +- `file_write(path string, data []u8) !` +- `file_delete(path string) !` + +### Directory Operations +- `dir_create(path string) !FSEntry` +- `dir_list(path string) ![]FSEntry` +- `dir_delete(path string) !` + +### Entry Operations (Common) +- `entry_exists(path string) bool` +- `entry_get(path string) !FSEntry` +- `entry_rename(old_path string, new_path string) !` +- `entry_copy(src_path string, dst_path string) !` + +### Symlink Operations +- `link_create(target_path string, link_path string) !FSEntry` +- `link_read(path string) !string` + +## Usage + +```v +import vfscore + +fn main() ! { + // Create a local filesystem implementation + mut local_vfs := vfscore.new_vfs('local', 'my_local_fs')! + + // Create and write to a file + local_vfs.file_create('test.txt')! + local_vfs.file_write('test.txt', 'Hello, World!'.bytes())! + + // Read file contents + content := local_vfs.file_read('test.txt')! + println(content.bytestr()) + + // Create and list directory + local_vfs.dir_create('subdir')! + entries := local_vfs.dir_list('subdir')! + + // Create symlink + local_vfs.link_create('test.txt', 'test_link.txt')! + + // Clean up + local_vfs.file_delete('test.txt')! + local_vfs.dir_delete('subdir')! +} +``` + +## Implementations + +### Local Filesystem (LocalVFS) + +The LocalVFS implementation provides a direct passthrough to the operating system's filesystem. It implements all vfscore operations by delegating to the corresponding OS filesystem operations. + +Features: +- Direct access to local filesystem +- Full support for all vfscore operations +- Preserves file permissions and metadata +- Efficient for local file operations + +### OurDB Filesystem (ourdb_fs) + +The ourdb_fs implementation stores files and metadata in OurDB, providing a database-backed virtual filesystem. + +Features: +- Persistent storage in OurDB +- Transactional operations +- Structured metadata storage +- Suitable for embedded systems or custom storage requirements + +## Adding New Implementations + +To create a new vfscore implementation: + +1. Implement the `VFSImplementation` interface +2. Add your implementation to the `new_vfs` factory function +3. Ensure all required operations are implemented following the `$subject_$method` naming pattern +4. Add appropriate error handling and validation + +## Error Handling + +All operations that can fail return a `!` result type. Handle potential errors appropriately: + +```v +// Example error handling +if file := vfscore.file_create('test.txt') { + // Success case + println('File created successfully') +} else { + // Error case + println('Failed to create file: ${err}') +} +``` + +## Testing + +The module includes comprehensive tests for both implementations. Run tests using: + +```bash +v test vfscore/ +``` + +## Contributing + +To add a new vfscore implementation: + +1. Create a new file in the `vfscore` directory (e.g., `my_impl.v`) +2. Implement the `VFSImplementation` interface following the `$subject_$method` naming pattern +3. Add your implementation to `new_vfs()` in `interface.v` +4. Add tests to verify your implementation +5. Update documentation to include your implementation diff --git a/lib/vfs/vfscore/interface.v b/lib/vfs/vfscore/interface.v new file mode 100644 index 00000000..cfba493d --- /dev/null +++ b/lib/vfs/vfscore/interface.v @@ -0,0 +1,58 @@ +module vfscore + +// FileType represents the type of a filesystem entry +pub enum FileType { + file + directory + symlink +} + +// Metadata represents the common metadata for both files and directories +pub struct Metadata { +pub mut: + name string // name of file or directory + file_type FileType + size u64 + created_at i64 // unix epoch timestamp + modified_at i64 // unix epoch timestamp + accessed_at i64 // unix epoch timestamp +} + +// FSEntry represents a filesystem entry (file, directory, or symlink) +pub interface FSEntry { + get_metadata() Metadata + get_path() string +} + +// VFSImplementation defines the interface that all vfscore implementations must follow +pub interface VFSImplementation { +mut: + // Basic operations + root_get() !FSEntry + + // File operations + file_create(path string) !FSEntry + file_read(path string) ![]u8 + file_write(path string, data []u8) ! + file_delete(path string) ! + + // Directory operations + dir_create(path string) !FSEntry + dir_list(path string) ![]FSEntry + dir_delete(path string) ! + + // Common operations + exists(path string) bool + get(path string) !FSEntry + rename(old_path string, new_path string) ! + copy(src_path string, dst_path string) ! + delete(path string) ! + + // Symlink operations + link_create(target_path string, link_path string) !FSEntry + link_read(path string) !string + link_delete(path string) ! + + // Cleanup operation + destroy() ! +} diff --git a/lib/vfs/vfscore/local.v b/lib/vfs/vfscore/local.v new file mode 100644 index 00000000..c0e13c45 --- /dev/null +++ b/lib/vfs/vfscore/local.v @@ -0,0 +1,308 @@ +module vfscore + +import os + +// LocalFSEntry implements FSEntry for local filesystem +struct LocalFSEntry { +mut: + path string + metadata Metadata +} + +fn (e LocalFSEntry) get_metadata() Metadata { + return e.metadata +} + +fn (e LocalFSEntry) get_path() string { + return e.path +} + +// LocalVFS implements VFSImplementation for local filesystem +pub struct LocalVFS { +mut: + root_path string +} + +// Create a new LocalVFS instance +pub fn new_local_vfs(root_path string) !VFSImplementation { + mut myvfs := LocalVFS{ + root_path: root_path + } + myvfs.init()! + return myvfs +} + +// Initialize the local vfscore with a root path +fn (mut myvfs LocalVFS) init() ! { + if !os.exists(myvfs.root_path) { + os.mkdir_all(myvfs.root_path) or { + return error('Failed to create root directory ${myvfs.root_path}: ${err}') + } + } +} + +// Destroy the vfscore by removing all its contents +pub fn (mut myvfs LocalVFS) destroy() ! { + if !os.exists(myvfs.root_path) { + return error('vfscore root path does not exist: ${myvfs.root_path}') + } + os.rmdir_all(myvfs.root_path) or { + return error('Failed to destroy vfscore at ${myvfs.root_path}: ${err}') + } + myvfs.init()! +} + +// Convert path to Metadata with improved security and information gathering +fn (myvfs LocalVFS) os_attr_to_metadata(path string) !Metadata { + // Get file info atomically to prevent TOCTOU issues + attr := os.stat(path) or { return error('Failed to get file attributes: ${err}') } + + mut file_type := FileType.file + if os.is_dir(path) { + file_type = .directory + } else if os.is_link(path) { + file_type = .symlink + } + + return Metadata{ + name: os.base(path) + file_type: file_type + size: u64(attr.size) + created_at: i64(attr.ctime) // Creation time from stat + modified_at: i64(attr.mtime) // Modification time from stat + accessed_at: i64(attr.atime) // Access time from stat + } +} + +// Get absolute path from relative path +fn (myvfs LocalVFS) abs_path(path string) string { + return os.join_path(myvfs.root_path, path) +} + +// Basic operations +pub fn (myvfs LocalVFS) root_get() !FSEntry { + if !os.exists(myvfs.root_path) { + return error('Root path does not exist: ${myvfs.root_path}') + } + metadata := myvfs.os_attr_to_metadata(myvfs.root_path) or { + return error('Failed to get root metadata: ${err}') + } + return LocalFSEntry{ + path: '' + metadata: metadata + } +} + +// File operations with improved error handling and TOCTOU protection +pub fn (myvfs LocalVFS) file_create(path string) !FSEntry { + abs_path := myvfs.abs_path(path) + if os.exists(abs_path) { + return error('File already exists: ${path}') + } + os.write_file(abs_path, '') or { return error('Failed to create file ${path}: ${err}') } + metadata := myvfs.os_attr_to_metadata(abs_path) or { + return error('Failed to get metadata: ${err}') + } + return LocalFSEntry{ + path: path + metadata: metadata + } +} + +pub fn (myvfs LocalVFS) file_read(path string) ![]u8 { + abs_path := myvfs.abs_path(path) + if !os.exists(abs_path) { + return error('File does not exist: ${path}') + } + if os.is_dir(abs_path) { + return error('Path is a directory: ${path}') + } + return os.read_bytes(abs_path) or { return error('Failed to read file ${path}: ${err}') } +} + +pub fn (myvfs LocalVFS) file_write(path string, data []u8) ! { + abs_path := myvfs.abs_path(path) + if os.is_dir(abs_path) { + return error('Cannot write to directory: ${path}') + } + os.write_file(abs_path, data.bytestr()) or { + return error('Failed to write file ${path}: ${err}') + } +} + +pub fn (myvfs LocalVFS) file_delete(path string) ! { + abs_path := myvfs.abs_path(path) + if !os.exists(abs_path) { + return error('File does not exist: ${path}') + } + if os.is_dir(abs_path) { + return error('Cannot delete directory using file_delete: ${path}') + } + os.rm(abs_path) or { return error('Failed to delete file ${path}: ${err}') } +} + +// Directory operations with improved error handling +pub fn (myvfs LocalVFS) dir_create(path string) !FSEntry { + abs_path := myvfs.abs_path(path) + if os.exists(abs_path) { + return error('Path already exists: ${path}') + } + os.mkdir_all(abs_path) or { return error('Failed to create directory ${path}: ${err}') } + metadata := myvfs.os_attr_to_metadata(abs_path) or { + return error('Failed to get metadata: ${err}') + } + return LocalFSEntry{ + path: path + metadata: metadata + } +} + +pub fn (myvfs LocalVFS) dir_list(path string) ![]FSEntry { + abs_path := myvfs.abs_path(path) + if !os.exists(abs_path) { + return error('Directory does not exist: ${path}') + } + if !os.is_dir(abs_path) { + return error('Path is not a directory: ${path}') + } + + entries := os.ls(abs_path) or { return error('Failed to list directory ${path}: ${err}') } + mut result := []FSEntry{cap: entries.len} + + for entry in entries { + rel_path := os.join_path(path, entry) + abs_entry_path := os.join_path(abs_path, entry) + metadata := myvfs.os_attr_to_metadata(abs_entry_path) or { continue } // Skip entries we can't stat + result << LocalFSEntry{ + path: rel_path + metadata: metadata + } + } + return result +} + +pub fn (myvfs LocalVFS) dir_delete(path string) ! { + abs_path := myvfs.abs_path(path) + if !os.exists(abs_path) { + return error('Directory does not exist: ${path}') + } + if !os.is_dir(abs_path) { + return error('Path is not a directory: ${path}') + } + os.rmdir_all(abs_path) or { return error('Failed to delete directory ${path}: ${err}') } +} + +// Common operations with improved error handling +pub fn (myvfs LocalVFS) exists(path string) bool { + // TODO: check is link if link the link can be broken but it stil exists + return os.exists(myvfs.abs_path(path)) +} + +pub fn (myvfs LocalVFS) get(path string) !FSEntry { + abs_path := myvfs.abs_path(path) + if !os.exists(abs_path) { + return error('Entry does not exist: ${path}') + } + metadata := myvfs.os_attr_to_metadata(abs_path) or { + return error('Failed to get metadata: ${err}') + } + return LocalFSEntry{ + path: path + metadata: metadata + } +} + +pub fn (myvfs LocalVFS) rename(old_path string, new_path string) ! { + abs_old := myvfs.abs_path(old_path) + abs_new := myvfs.abs_path(new_path) + + if !os.exists(abs_old) { + return error('Source path does not exist: ${old_path}') + } + if os.exists(abs_new) { + return error('Destination path already exists: ${new_path}') + } + + os.mv(abs_old, abs_new) or { + return error('Failed to rename ${old_path} to ${new_path}: ${err}') + } +} + +pub fn (myvfs LocalVFS) copy(src_path string, dst_path string) ! { + abs_src := myvfs.abs_path(src_path) + abs_dst := myvfs.abs_path(dst_path) + + if !os.exists(abs_src) { + return error('Source path does not exist: ${src_path}') + } + if os.exists(abs_dst) { + return error('Destination path already exists: ${dst_path}') + } + + os.cp(abs_src, abs_dst) or { return error('Failed to copy ${src_path} to ${dst_path}: ${err}') } +} + +// Generic delete operation that handles all types +pub fn (myvfs LocalVFS) delete(path string) ! { + abs_path := myvfs.abs_path(path) + if !os.exists(abs_path) { + return error('Path does not exist: ${path}') + } + + if os.is_link(abs_path) { + myvfs.link_delete(path)! + } else if os.is_dir(abs_path) { + myvfs.dir_delete(path)! + } else { + myvfs.file_delete(path)! + } +} + +// Symlink operations with improved handling +pub fn (myvfs LocalVFS) link_create(target_path string, link_path string) !FSEntry { + abs_target := myvfs.abs_path(target_path) + abs_link := myvfs.abs_path(link_path) + + if !os.exists(abs_target) { + return error('Target path does not exist: ${target_path}') + } + if os.exists(abs_link) { + return error('Link path already exists: ${link_path}') + } + + os.symlink(target_path, abs_link) or { + return error('Failed to create symlink from ${target_path} to ${link_path}: ${err}') + } + + metadata := myvfs.os_attr_to_metadata(abs_link) or { + return error('Failed to get metadata: ${err}') + } + return LocalFSEntry{ + path: link_path + metadata: metadata + } +} + +pub fn (myvfs LocalVFS) link_read(path string) !string { + abs_path := myvfs.abs_path(path) + if !os.exists(abs_path) { + return error('Symlink does not exist: ${path}') + } + if !os.is_link(abs_path) { + return error('Path is not a symlink: ${path}') + } + + real_path := os.real_path(abs_path) + return os.base(real_path) +} + +pub fn (myvfs LocalVFS) link_delete(path string) ! { + abs_path := myvfs.abs_path(path) + if !os.exists(abs_path) { + return error('Symlink does not exist: ${path}') + } + if !os.is_link(abs_path) { + return error('Path is not a symlink: ${path}') + } + os.rm(abs_path) or { return error('Failed to delete symlink ${path}: ${err}') } +} diff --git a/lib/vfs/vfscore/local_test.v b/lib/vfs/vfscore/local_test.v new file mode 100644 index 00000000..7c7eb10a --- /dev/null +++ b/lib/vfs/vfscore/local_test.v @@ -0,0 +1,65 @@ +module vfscore + +import os + +fn test_vfs_implementations() ! { + // Test local vfscore + mut local_vfs := new_local_vfs('/tmp/test_local_vfs')! + local_vfs.destroy()! + + // Create and write to a file + local_vfs.file_create('test.txt')! + local_vfs.file_write('test.txt', 'Hello, World!'.bytes())! + + // Read the file + content := local_vfs.file_read('test.txt')! + assert content.bytestr() == 'Hello, World!' + + // Create a directory and list its contents + local_vfs.dir_create('subdir')! + local_vfs.file_create('subdir/file1.txt')! + local_vfs.file_write('subdir/file1.txt', 'File 1'.bytes())! + local_vfs.file_create('subdir/file2.txt')! + local_vfs.file_write('subdir/file2.txt', 'File 2'.bytes())! + + entries := local_vfs.dir_list('subdir')! + assert entries.len == 2 + + // Test entry operations + assert local_vfs.exists('test.txt') + entry := local_vfs.get('test.txt')! + assert entry.get_metadata().name == 'test.txt' + + // Test rename and copy + local_vfs.rename('test.txt', 'test2.txt')! + local_vfs.copy('test2.txt', 'test3.txt')! + + // Verify test2.txt exists before creating symlink + if !local_vfs.exists('test2.txt') { + panic('test2.txt does not exist before symlink creation') + } + + // Create and read symlink using relative paths + local_vfs.link_create('test2.txt', 'test_link.txt')! + + // Verify symlink was created + if !local_vfs.exists('test_link.txt') { + panic('test_link.txt was not created') + } + + // Read the symlink + link_target := local_vfs.link_read('test_link.txt')! + target_base := os.base(link_target) + if target_base != 'test2.txt' { + eprintln('Expected link target: test2.txt') + eprintln('Actual link target: ${target_base}') + panic('Symlink points to wrong target') + } + + // Cleanup + local_vfs.delete('test2.txt')! + local_vfs.delete('subdir')! + local_vfs.delete('test_link.txt')! + + os.rmdir('/tmp/test_local_vfs') or {} +} diff --git a/lib/vfs/vfsnested/nested_test.v b/lib/vfs/vfsnested/nested_test.v new file mode 100644 index 00000000..4d8bda61 --- /dev/null +++ b/lib/vfs/vfsnested/nested_test.v @@ -0,0 +1,84 @@ +module vfsnested + +import os + +fn test_nested() ! { + println('Testing Nested VFS...') + + // Create root directories for test VFS instances + os.mkdir_all('/tmp/test_nested_vfs/vfs1') or { panic(err) } + os.mkdir_all('/tmp/test_nested_vfs/vfs2') or { panic(err) } + os.mkdir_all('/tmp/test_nested_vfs/vfs3') or { panic(err) } + + // Create VFS instances + mut vfs1 := vfscore.new_local_vfs('/tmp/test_nested_vfs/vfs1') or { panic(err) } + mut vfs2 := vfscore.new_local_vfs('/tmp/test_nested_vfs/vfs2') or { panic(err) } + mut vfs3 := vfscore.new_local_vfs('/tmp/test_nested_vfs/vfs3') or { panic(err) } + + // Create nested VFS + mut nested_vfs := new() + + // Add VFS instances at different paths + nested_vfs.add_vfs('/data', vfs1) or { panic(err) } + nested_vfs.add_vfs('/config', vfs2) or { panic(err) } + nested_vfs.add_vfs('/data/backup', vfs3) or { panic(err) } // Nested under /data + + println('\nTesting file operations...') + + // Create and write to files in different VFS instances + nested_vfs.file_create('/data/test.txt') or { panic(err) } + nested_vfs.file_write('/data/test.txt', 'Hello from VFS1'.bytes()) or { panic(err) } + + nested_vfs.file_create('/config/settings.txt') or { panic(err) } + nested_vfs.file_write('/config/settings.txt', 'Hello from VFS2'.bytes()) or { panic(err) } + + nested_vfs.file_create('/data/backup/archive.txt') or { panic(err) } + nested_vfs.file_write('/data/backup/archive.txt', 'Hello from VFS3'.bytes()) or { panic(err) } + + // Read and verify file contents + data1 := nested_vfs.file_read('/data/test.txt') or { panic(err) } + println('Content from /data/test.txt: ${data1.bytestr()}') + + data2 := nested_vfs.file_read('/config/settings.txt') or { panic(err) } + println('Content from /config/settings.txt: ${data2.bytestr()}') + + data3 := nested_vfs.file_read('/data/backup/archive.txt') or { panic(err) } + println('Content from /data/backup/archive.txt: ${data3.bytestr()}') + + println('\nTesting directory operations...') + + // List root directory + println('Root directory contents:') + root_entries := nested_vfs.dir_list('/') or { panic(err) } + for entry in root_entries { + meta := entry.get_metadata() + println('- ${meta.name} (${meta.file_type})') + } + + // Create and list directories + nested_vfs.dir_create('/data/subdir') or { panic(err) } + nested_vfs.file_create('/data/subdir/file.txt') or { panic(err) } + nested_vfs.file_write('/data/subdir/file.txt', 'Nested file content'.bytes()) or { panic(err) } + + println('\nListing /data directory:') + data_entries := nested_vfs.dir_list('/data') or { panic(err) } + for entry in data_entries { + meta := entry.get_metadata() + println('- ${meta.name} (${meta.file_type})') + } + + println('\nTesting cross-VFS operations...') + + // Copy file between different VFS instances + nested_vfs.copy('/data/test.txt', '/config/test_copy.txt') or { panic(err) } + copy_data := nested_vfs.file_read('/config/test_copy.txt') or { panic(err) } + println('Copied file content: ${copy_data.bytestr()}') + + println('\nCleanup...') + + // Cleanup + nested_vfs.destroy() or { panic(err) } + os.rmdir_all('/tmp/test_nested_vfs') or { panic(err) } + + println('Test completed successfully!') +} diff --git a/lib/vfs/vfsnested/readme.md b/lib/vfs/vfsnested/readme.md new file mode 100644 index 00000000..cc8be24d --- /dev/null +++ b/lib/vfs/vfsnested/readme.md @@ -0,0 +1,4 @@ +# VFS Overlay + +This virtual filesystem combines multiple other VFS'es + diff --git a/lib/vfs/vfsnested/vfsnested.v b/lib/vfs/vfsnested/vfsnested.v new file mode 100644 index 00000000..ecdb54e8 --- /dev/null +++ b/lib/vfs/vfsnested/vfsnested.v @@ -0,0 +1,190 @@ +module vfsnested + +import freeflowuniverse.crystallib.vfs.vfscore + +// NestedVFS represents a VFS that can contain multiple nested VFS instances +pub struct NestedVFS { +mut: + vfs_map map[string]vfscore.VFSImplementation // Map of path prefixes to VFS implementations +} + +// new creates a new NestedVFS instance +pub fn new() &NestedVFS { + return &NestedVFS{ + vfs_map: map[string]vfscore.VFSImplementation{} + } +} + +// add_vfs adds a new VFS implementation at the specified path prefix +pub fn (mut self NestedVFS) add_vfs(prefix string, impl vfscore.VFSImplementation) ! { + if prefix in self.vfs_map { + return error('VFS already exists at prefix: ${prefix}') + } + self.vfs_map[prefix] = impl +} + +// find_vfs finds the appropriate VFS implementation for a given path +fn (self &NestedVFS) find_vfs(path string) !(vfscore.VFSImplementation, string) { + // Sort prefixes by length (longest first) to match most specific path + mut prefixes := self.vfs_map.keys() + prefixes.sort(a.len > b.len) + + for prefix in prefixes { + if path.starts_with(prefix) { + relative_path := path[prefix.len..] + if relative_path.starts_with('/') { + return self.vfs_map[prefix], relative_path[1..] + } + return self.vfs_map[prefix], relative_path + } + } + return error('No VFS found for path: ${path}') +} + +// Implementation of VFSImplementation interface +pub fn (mut self NestedVFS) root_get() !vfscore.FSEntry { + // Return a special root entry that represents the nested VFS + return &RootEntry{ + metadata: vfscore.Metadata{ + name: '' + file_type: .directory + size: 0 + created_at: 0 + modified_at: 0 + accessed_at: 0 + } + } +} + +pub fn (mut self NestedVFS) file_create(path string) !vfscore.FSEntry { + mut impl, rel_path := self.find_vfs(path)! + return impl.file_create(rel_path) +} + +pub fn (mut self NestedVFS) file_read(path string) ![]u8 { + mut impl, rel_path := self.find_vfs(path)! + return impl.file_read(rel_path) +} + +pub fn (mut self NestedVFS) file_write(path string, data []u8) ! { + mut impl, rel_path := self.find_vfs(path)! + return impl.file_write(rel_path, data) +} + +pub fn (mut self NestedVFS) file_delete(path string) ! { + mut impl, rel_path := self.find_vfs(path)! + return impl.file_delete(rel_path) +} + +pub fn (mut self NestedVFS) dir_create(path string) !vfscore.FSEntry { + mut impl, rel_path := self.find_vfs(path)! + return impl.dir_create(rel_path) +} + +pub fn (mut self NestedVFS) dir_list(path string) ![]vfscore.FSEntry { + // Special case for root directory + if path == '' || path == '/' { + mut entries := []vfscore.FSEntry{} + for prefix, mut impl in self.vfs_map { + root := impl.root_get() or { continue } + entries << &MountEntry{ + metadata: vfscore.Metadata{ + name: prefix + file_type: .directory + size: 0 + created_at: 0 + modified_at: 0 + accessed_at: 0 + } + impl: impl + } + } + return entries + } + + mut impl, rel_path := self.find_vfs(path)! + return impl.dir_list(rel_path) +} + +pub fn (mut self NestedVFS) dir_delete(path string) ! { + mut impl, rel_path := self.find_vfs(path)! + return impl.dir_delete(rel_path) +} + +pub fn (mut self NestedVFS) exists(path string) !bool { + mut impl, rel_path := self.find_vfs(path)! + return impl.exists(rel_path) +} + +pub fn (mut self NestedVFS) get(path string) !vfscore.FSEntry { + mut impl, rel_path := self.find_vfs(path)! + return impl.get(rel_path) +} + +pub fn (mut self NestedVFS) rename(old_path string, new_path string) ! { + mut old_impl, old_rel_path := self.find_vfs(old_path)! + mut new_impl, new_rel_path := self.find_vfs(new_path)! + + if old_impl != new_impl { + return error('Cannot rename across different VFS implementations') + } + + return old_impl.rename(old_rel_path, new_rel_path) +} + +pub fn (mut self NestedVFS) copy(src_path string, dst_path string) ! { + mut src_impl, src_rel_path := self.find_vfs(src_path)! + mut dst_impl, dst_rel_path := self.find_vfs(dst_path)! + + if src_impl == dst_impl { + return src_impl.copy(src_rel_path, dst_rel_path) + } + + // Copy across different VFS implementations + data := src_impl.file_read(src_rel_path)! + dst_impl.file_create(dst_rel_path)! + return dst_impl.file_write(dst_rel_path, data) +} + +pub fn (mut self NestedVFS) link_create(target_path string, link_path string) !vfscore.FSEntry { + mut impl, rel_path := self.find_vfs(link_path)! + return impl.link_create(target_path, rel_path) +} + +pub fn (mut self NestedVFS) link_read(path string) !string { + mut impl, rel_path := self.find_vfs(path)! + return impl.link_read(rel_path) +} + +pub fn (mut self NestedVFS) destroy() ! { + for _, mut impl in self.vfs_map { + impl.destroy()! + } +} + +// Special entry types for the nested VFS +struct RootEntry { + metadata vfscore.Metadata +} + +fn (e &RootEntry) get_metadata() vfscore.Metadata { + return e.metadata +} + +fn (e &RootEntry) get_path() string { + return '/' +} + +pub struct MountEntry { +pub mut: + metadata vfscore.Metadata + impl vfscore.VFSImplementation +} + +fn (e &MountEntry) get_metadata() vfscore.Metadata { + return e.metadata +} + +fn (e &MountEntry) get_path() string { + return '/${e.metadata.name}' +} diff --git a/lib/vfs/vfsourdb/readme.md b/lib/vfs/vfsourdb/readme.md new file mode 100644 index 00000000..7fa2f484 --- /dev/null +++ b/lib/vfs/vfsourdb/readme.md @@ -0,0 +1,8 @@ +# VFS Overlay of OURDb + +use the ourdb_fs implementation underneith which speaks with the ourdb + +this is basically a filesystem interface for storing files into an ourdb. + + + diff --git a/lib/vfs/vfsourdb/vfsourdb.v b/lib/vfs/vfsourdb/vfsourdb.v new file mode 100644 index 00000000..47cb3ffa --- /dev/null +++ b/lib/vfs/vfsourdb/vfsourdb.v @@ -0,0 +1,284 @@ +module vfsourdb + +import freeflowuniverse.crystallib.vfs.vfscore +import freeflowuniverse.crystallib.vfs.ourdb_fs +import os +import time + +// OurDBVFS represents a VFS that uses OurDB as the underlying storage +pub struct OurDBVFS { +mut: + core &ourdb_fs.VFS +} + +// new creates a new OurDBVFS instance +pub fn new(data_dir string, metadata_dir string) !&OurDBVFS { + mut core := ourdb_fs.new( + data_dir: data_dir + metadata_dir: metadata_dir + )! + + return &OurDBVFS{ + core: core + } +} + +// Implementation of VFSImplementation interface +pub fn (mut self OurDBVFS) root_get() !vfscore.FSEntry { + mut root := self.core.get_root()! + return convert_to_vfscore_entry(root) +} + +pub fn (mut self OurDBVFS) file_create(path string) !vfscore.FSEntry { + // Get parent directory + parent_path := os.dir(path) + file_name := os.base(path) + + mut parent_dir := self.get_directory(parent_path)! + mut file := parent_dir.touch(file_name)! + return convert_to_vfscore_entry(file) +} + +pub fn (mut self OurDBVFS) file_read(path string) ![]u8 { + mut entry := self.get_entry(path)! + if mut entry is ourdb_fs.File { + content := entry.read()! + return content.bytes() + } + return error('Not a file: ${path}') +} + +pub fn (mut self OurDBVFS) file_write(path string, data []u8) ! { + mut entry := self.get_entry(path)! + if mut entry is ourdb_fs.File { + entry.write(data.bytestr())! + } else { + return error('Not a file: ${path}') + } +} + +pub fn (mut self OurDBVFS) file_delete(path string) ! { + parent_path := os.dir(path) + file_name := os.base(path) + + mut parent_dir := self.get_directory(parent_path)! + parent_dir.rm(file_name)! +} + +pub fn (mut self OurDBVFS) dir_create(path string) !vfscore.FSEntry { + parent_path := os.dir(path) + dir_name := os.base(path) + + mut parent_dir := self.get_directory(parent_path)! + mut new_dir := parent_dir.mkdir(dir_name)! + return convert_to_vfscore_entry(new_dir) +} + +pub fn (mut self OurDBVFS) dir_list(path string) ![]vfscore.FSEntry { + mut dir := self.get_directory(path)! + mut entries := dir.children(false)! + mut result := []vfscore.FSEntry{} + + for entry in entries { + result << convert_to_vfscore_entry(entry) + } + + return result +} + +pub fn (mut self OurDBVFS) dir_delete(path string) ! { + parent_path := os.dir(path) + dir_name := os.base(path) + + mut parent_dir := self.get_directory(parent_path)! + parent_dir.rm(dir_name)! +} + +pub fn (mut self OurDBVFS) exists(path string) !bool { + if path == '/' { + return true + } + self.get_entry(path) or { return false } + return true +} + +pub fn (mut self OurDBVFS) get(path string) !vfscore.FSEntry { + mut entry := self.get_entry(path)! + return convert_to_vfscore_entry(entry) +} + +pub fn (mut self OurDBVFS) rename(old_path string, new_path string) ! { + return error('Not implemented') +} + +pub fn (mut self OurDBVFS) copy(src_path string, dst_path string) ! { + return error('Not implemented') +} + +pub fn (mut self OurDBVFS) link_create(target_path string, link_path string) !vfscore.FSEntry { + parent_path := os.dir(link_path) + link_name := os.base(link_path) + + mut parent_dir := self.get_directory(parent_path)! + + mut symlink := ourdb_fs.Symlink{ + metadata: ourdb_fs.Metadata{ + id: u32(time.now().unix()) + name: link_name + file_type: .symlink + created_at: time.now().unix() + modified_at: time.now().unix() + accessed_at: time.now().unix() + mode: 0o777 + owner: 'user' + group: 'user' + } + target: target_path + parent_id: parent_dir.metadata.id + myvfs: self.core + } + + parent_dir.add_symlink(symlink)! + return convert_to_vfscore_entry(symlink) +} + +pub fn (mut self OurDBVFS) link_read(path string) !string { + mut entry := self.get_entry(path)! + if mut entry is ourdb_fs.Symlink { + return entry.get_target()! + } + return error('Not a symlink: ${path}') +} + +pub fn (mut self OurDBVFS) destroy() ! { + // Nothing to do as the core VFS handles cleanup +} + +// Helper functions + +fn (mut self OurDBVFS) get_entry(path string) !ourdb_fs.FSEntry { + if path == '/' { + return self.core.get_root()! + } + + mut current := self.core.get_root()! + parts := path.trim_left('/').split('/') + + for i := 0; i < parts.len; i++ { + found := false + children := current.children(false)! + + for child in children { + if child.metadata.name == parts[i] { + match child { + ourdb_fs.Directory { + current = child + found = true + break + } + else { + if i == parts.len - 1 { + return child + } else { + return error('Not a directory: ${parts[i]}') + } + } + } + } + } + + if !found { + return error('Path not found: ${path}') + } + } + + return current +} + +fn (mut self OurDBVFS) get_directory(path string) !&ourdb_fs.Directory { + mut entry := self.get_entry(path)! + if mut entry is ourdb_fs.Directory { + return &entry + } + return error('Not a directory: ${path}') +} + +fn convert_to_vfscore_entry(entry ourdb_fs.FSEntry) vfscore.FSEntry { + match entry { + ourdb_fs.Directory { + return &DirectoryEntry{ + metadata: convert_metadata(entry.metadata) + path: entry.metadata.name + } + } + ourdb_fs.File { + return &FileEntry{ + metadata: convert_metadata(entry.metadata) + path: entry.metadata.name + } + } + ourdb_fs.Symlink { + return &SymlinkEntry{ + metadata: convert_metadata(entry.metadata) + path: entry.metadata.name + target: entry.target + } + } + } +} + +fn convert_metadata(meta ourdb_fs.Metadata) vfscore.Metadata { + return vfscore.Metadata{ + name: meta.name + file_type: match meta.file_type { + .file { vfscore.FileType.file } + .directory { vfscore.FileType.directory } + .symlink { vfscore.FileType.symlink } + } + size: meta.size + created_at: meta.created_at + modified_at: meta.modified_at + accessed_at: meta.accessed_at + } +} + +// Entry type implementations +struct DirectoryEntry { + metadata vfscore.Metadata + path string +} + +fn (e &DirectoryEntry) get_metadata() vfscore.Metadata { + return e.metadata +} + +fn (e &DirectoryEntry) get_path() string { + return e.path +} + +struct FileEntry { + metadata vfscore.Metadata + path string +} + +fn (e &FileEntry) get_metadata() vfscore.Metadata { + return e.metadata +} + +fn (e &FileEntry) get_path() string { + return e.path +} + +struct SymlinkEntry { + metadata vfscore.Metadata + path string + target string +} + +fn (e &SymlinkEntry) get_metadata() vfscore.Metadata { + return e.metadata +} + +fn (e &SymlinkEntry) get_path() string { + return e.path +} diff --git a/lib/vfs/vfsourdb/vfsourdb_test.v b/lib/vfs/vfsourdb/vfsourdb_test.v new file mode 100644 index 00000000..6e8eca0f --- /dev/null +++ b/lib/vfs/vfsourdb/vfsourdb_test.v @@ -0,0 +1,69 @@ +module vfsourdb + +import os + +fn test_vfsourdb() ! { + println('Testing OurDB VFS...') + + // Create test directories + test_data_dir := os.join_path(os.temp_dir(), 'vfsourdb_test_data') + test_meta_dir := os.join_path(os.temp_dir(), 'vfsourdb_test_meta') + + os.mkdir_all(test_data_dir)! + os.mkdir_all(test_meta_dir)! + + defer { + os.rmdir_all(test_data_dir) or {} + os.rmdir_all(test_meta_dir) or {} + } + + // Create VFS instance + mut vfs := new(test_data_dir, test_meta_dir)! + + // Test root directory + mut root := vfs.root_get()! + assert root.get_metadata().file_type == .directory + assert root.get_metadata().name == '' + + // Test directory creation + mut test_dir := vfs.dir_create('/test_dir')! + assert test_dir.get_metadata().name == 'test_dir' + assert test_dir.get_metadata().file_type == .directory + + // Test file creation and writing + mut test_file := vfs.file_create('/test_dir/test.txt')! + assert test_file.get_metadata().name == 'test.txt' + assert test_file.get_metadata().file_type == .file + + test_content := 'Hello, World!'.bytes() + vfs.file_write('/test_dir/test.txt', test_content)! + + // Test file reading + read_content := vfs.file_read('/test_dir/test.txt')! + assert read_content == test_content + + // Test directory listing + entries := vfs.dir_list('/test_dir')! + assert entries.len == 1 + assert entries[0].get_metadata().name == 'test.txt' + + // Test exists + assert vfs.exists('/test_dir')! == true + assert vfs.exists('/test_dir/test.txt')! == true + assert vfs.exists('/nonexistent')! == false + + // Test symlink creation and reading + vfs.link_create('/test_dir/test.txt', '/test_dir/test_link')! + link_target := vfs.link_read('/test_dir/test_link')! + assert link_target == '/test_dir/test.txt' + + // Test file deletion + vfs.file_delete('/test_dir/test.txt')! + assert vfs.exists('/test_dir/test.txt')! == false + + // Test directory deletion + vfs.dir_delete('/test_dir')! + assert vfs.exists('/test_dir')! == false + + println('Test completed successfully!') +}