From d06a8061840d59589f921e367e31eabe451b0cf9 Mon Sep 17 00:00:00 2001 From: timurgordon Date: Thu, 27 Feb 2025 11:42:46 +0300 Subject: [PATCH] isolate vfs's and improve documentation --- lib/vfs/README.md | 203 +++++++------ lib/vfs/vfs_db/factory.v | 19 +- lib/vfs/vfs_db/readme.md | 267 +++++++++--------- lib/vfs/vfs_db/vfs_implementation_test.v | 45 +-- lib/vfs/vfs_local/README.md | 114 +++++++- lib/vfs/vfs_local/factory.v | 28 ++ .../{local.v => vfs_implementation.v} | 164 ++++------- ...local_test.v => vfs_implementation_test.v} | 0 lib/vfs/vfs_local/vfs_local.v | 32 +++ lib/vfs/vfs_nested/README.md | 48 ++++ .../{vfsnested => vfs_nested}/nested_test.v | 0 lib/vfs/{vfsnested => vfs_nested}/vfsnested.v | 0 lib/vfs/vfsnested/readme.md | 4 - lib/vfs/webdav/README.md | 153 ---------- lib/vfs/webdav/app.v | 54 ---- lib/vfs/webdav/bin/main.v | 67 ----- lib/vfs/webdav/lock.v | 87 ------ lib/vfs/webdav/logic_test.v | 39 --- lib/vfs/webdav/methods.v | 259 ----------------- lib/vfs/webdav/middleware_auth.v | 49 ---- lib/vfs/webdav/middleware_log.v | 12 - lib/vfs/webdav/prop.v | 152 ---------- lib/vfs/webdav/server_test.v | 214 -------------- 23 files changed, 551 insertions(+), 1459 deletions(-) create mode 100644 lib/vfs/vfs_local/factory.v rename lib/vfs/vfs_local/{local.v => vfs_implementation.v} (84%) rename lib/vfs/vfs_local/{local_test.v => vfs_implementation_test.v} (100%) create mode 100644 lib/vfs/vfs_local/vfs_local.v create mode 100644 lib/vfs/vfs_nested/README.md rename lib/vfs/{vfsnested => vfs_nested}/nested_test.v (100%) rename lib/vfs/{vfsnested => vfs_nested}/vfsnested.v (100%) delete mode 100644 lib/vfs/vfsnested/readme.md delete mode 100644 lib/vfs/webdav/README.md delete mode 100644 lib/vfs/webdav/app.v delete mode 100644 lib/vfs/webdav/bin/main.v delete mode 100644 lib/vfs/webdav/lock.v delete mode 100644 lib/vfs/webdav/logic_test.v delete mode 100644 lib/vfs/webdav/methods.v delete mode 100644 lib/vfs/webdav/middleware_auth.v delete mode 100644 lib/vfs/webdav/middleware_log.v delete mode 100644 lib/vfs/webdav/prop.v delete mode 100644 lib/vfs/webdav/server_test.v diff --git a/lib/vfs/README.md b/lib/vfs/README.md index 61311d6a..fb022a77 100644 --- a/lib/vfs/README.md +++ b/lib/vfs/README.md @@ -1,127 +1,156 @@ -# Virtual File System (vfscore) Module +# Virtual File System (VFS) 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) +This module provides a pluggable virtual filesystem interface that allows different storage backends to implement a common set of filesystem operations. ## Interface -The vfscore interface defines common operations for filesystem manipulation using a consistent naming pattern of `$subject_$method`: +The VFS interface (`VFSImplementation`) defines the following operations: + +### Basic Operations +- `root_get() !FSEntry` - Get the root directory entry ### File Operations -- `file_create(path string) !FSEntry` -- `file_read(path string) ![]u8` -- `file_write(path string, data []u8) !` -- `file_delete(path string) !` +- `file_create(path string) !FSEntry` - Create a new file +- `file_read(path string) ![]u8` - Read file contents as bytes +- `file_write(path string, data []u8) !` - Write bytes to a file +- `file_delete(path string) !` - Delete a file ### 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) !` +- `dir_create(path string) !FSEntry` - Create a new directory +- `dir_list(path string) ![]FSEntry` - List directory contents +- `dir_delete(path string) !` - Delete a directory ### Symlink Operations -- `link_create(target_path string, link_path string) !FSEntry` -- `link_read(path string) !string` +- `link_create(target_path string, link_path string) !FSEntry` - Create a symbolic link +- `link_read(path string) !string` - Read symlink target +- `link_delete(path string) !` - Delete a symlink -## Usage +### Common Operations +- `exists(path string) bool` - Check if path exists +- `get(path string) !FSEntry` - Get entry at path +- `rename(old_path string, new_path string) !FSEntry` - Rename/move an entry +- `copy(src_path string, dst_path string) !FSEntry` - Copy an entry +- `move(src_path string, dst_path string) !FSEntry` - Move an entry +- `delete(path string) !` - Delete any type of entry +- `destroy() !` - Clean up VFS resources + +## FSEntry Interface + +All filesystem entries implement the FSEntry interface: ```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')! +interface FSEntry { + get_metadata() Metadata + get_path() string + is_dir() bool + is_file() bool + is_symlink() bool } ``` ## 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. +### Local Filesystem (vfs_local) +Direct passthrough to the operating system's filesystem. Features: -- Direct access to local filesystem -- Full support for all vfscore operations +- Native filesystem access +- Full POSIX compliance - 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. +### Database Filesystem (vfs_db) +Stores files and metadata in a database backend. Features: -- Persistent storage in OurDB +- Persistent storage in database - Transactional operations - Structured metadata storage -- Suitable for embedded systems or custom storage requirements -## Adding New Implementations +### Nested Filesystem (vfs_nested) +Allows mounting other VFS implementations at specific paths. -To create a new vfscore implementation: +Features: +- Composite filesystem views +- Mix different implementations +- Flexible organization -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 +## Implementation Standards -## Error Handling +When creating a new VFS implementation: -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}') -} +1. Directory Structure: +``` +vfs_/ +├── factory.v # Implementation factory/constructor +├── vfs_implementation.v # Core interface implementation +├── model_*.v # Data structure definitions +├── README.md # Implementation documentation +└── *_test.v # Tests ``` -## Testing +2. Naming Conventions: +- Implementation module: `vfs_` +- Main struct: `VFS` (e.g., LocalVFS, DatabaseVFS) +- Factory function: `new__vfs()` -The module includes comprehensive tests for both implementations. Run tests using: +3. Error Handling: +- Use descriptive error messages +- Include path information in errors +- Handle edge cases (e.g., missing files, type mismatches) -```bash -v test vfscore/ +4. Documentation: +- Document implementation-specific behavior +- Note any limitations or special features +- Include usage examples + +## Usage Example + +```v +import vfs + +fn main() ! { + // Create a local filesystem implementation + mut fs := vfs.new_vfs('local', '/tmp/test')! + + // Create and write to a file + fs.file_create('test.txt')! + fs.file_write('test.txt', 'Hello, World!'.bytes())! + + // Read file contents + content := fs.file_read('test.txt')! + println(content.bytestr()) + + // Create and list directory + fs.dir_create('subdir')! + entries := fs.dir_list('subdir')! + + // Create symlink + fs.link_create('test.txt', 'test_link.txt')! + + // Clean up + fs.destroy()! +} ``` ## Contributing -To add a new vfscore implementation: +To add a new VFS 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 +1. Create a new directory `vfs_` following the structure above +2. Implement the `VFSImplementation` interface +3. Add factory function to create your implementation +4. Include comprehensive tests +5. Document implementation details and usage +6. Update the main VFS documentation + +## Testing + +Each implementation must include tests that verify: +- All interface methods +- Error conditions +- Edge cases +- Implementation-specific features + +Run tests with: +```bash +v test vfs/ diff --git a/lib/vfs/vfs_db/factory.v b/lib/vfs/vfs_db/factory.v index c54c397a..16744b9a 100644 --- a/lib/vfs/vfs_db/factory.v +++ b/lib/vfs/vfs_db/factory.v @@ -8,35 +8,20 @@ import freeflowuniverse.herolib.core.pathlib pub struct VFSParams { pub: data_dir string // Directory to store DatabaseVFS data - metadata_dir string // Directory to store DatabaseVFS metadata incremental_mode bool // Whether to enable incremental mode } -// new creates a new DatabaseVFS instance -pub fn new(data_dir string, metadata_dir string) !&DatabaseVFS { - return vfs_new( - data_dir: data_dir - metadata_dir: metadata_dir - incremental_mode: false - )! -} - // Factory method for creating a new DatabaseVFS instance -pub fn vfs_new(params VFSParams) !&DatabaseVFS { +pub fn new(mut database Database, params VFSParams) !&DatabaseVFS { pathlib.get_dir(path: params.data_dir, create: true) or { return error('Failed to create data directory: ${err}') } - mut db_data := ourdb.new( - path: '${params.data_dir}/ourdb_fs.db_data' - incremental_mode: params.incremental_mode - )! - mut fs := &DatabaseVFS{ root_id: 1 block_size: 1024 * 4 data_dir: params.data_dir - db_data: &db_data + db_data: database } return fs diff --git a/lib/vfs/vfs_db/readme.md b/lib/vfs/vfs_db/readme.md index 0753fccf..56b85993 100644 --- a/lib/vfs/vfs_db/readme.md +++ b/lib/vfs/vfs_db/readme.md @@ -1,90 +1,38 @@ -# VFS DB: A Virtual File System with Database Backend +# Database Filesystem Implementation (vfs_db) -A virtual file system implementation that provides a filesystem interface on top of a database backend (OURDb). This module enables hierarchical file system operations while storing all data in a key-value database. +A virtual filesystem implementation that uses OurDB as its storage backend, providing a complete filesystem interface with database-backed storage. -## Overview +## Features -VFS DB implements a complete virtual file system that: -- Uses OURDb as the storage backend -- Supports files, directories, and symbolic links -- Provides standard file system operations -- Maintains hierarchical structure -- Handles metadata and file data efficiently +- Persistent storage in OurDB database +- Full support for files, directories, and symlinks +- Transactional operations +- Structured metadata storage +- Hierarchical filesystem structure +- Thread-safe operations -## Architecture +## Implementation Details -### Core Components - -#### 1. Database Backend (OURDb) -- Uses key-value store with u32 keys and []u8 values -- Stores both metadata and file content -- Provides atomic operations for data consistency - -#### 2. File System Entries -All entries (files, directories, symlinks) share common metadata: -```v -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 -} +### Structure +``` +vfs_db/ +├── factory.v # VFS factory implementation +├── vfs_implementation.v # Core VFS interface implementation +├── vfs.v # DatabaseVFS type definition +├── model_file.v # File type implementation +├── model_directory.v # Directory type implementation +├── model_symlink.v # Symlink type implementation +├── model_fsentry.v # Common FSEntry interface +├── metadata.v # Metadata structure +├── encoder.v # Data encoding utilities +├── vfs_directory.v # Directory operations +├── vfs_getters.v # Common getter methods +└── *_test.v # Implementation tests ``` -The system supports three types of entries: -- Files: Store actual file data -- Directories: Maintain parent-child relationships -- Symlinks: Store symbolic link targets +### Key Components -### Key Features - -1. **File Operations** - - Create/delete files - - Read/write file content - - Copy and move files - - Rename files - - Check file existence - -2. **Directory Operations** - - Create/delete directories - - List directory contents - - Traverse directory tree - - Manage parent-child relationships - -3. **Symbolic Link Support** - - Create symbolic links - - Read link targets - - Delete links - -4. **Metadata Management** - - Track creation, modification, and access times - - Handle file permissions - - Store owner and group information - -### Implementation Details - -1. **Entry Types** -```v -pub type FSEntry = Directory | File | Symlink -``` - -2. **Database Interface** -```v -pub interface Database { -mut: - get(id u32) ![]u8 - set(ourdb.OurDBSetArgs) !u32 - delete(id u32)! -} -``` - -3. **VFS Structure** +- `DatabaseVFS`: Main implementation struct ```v pub struct DatabaseVFS { pub mut: @@ -97,71 +45,130 @@ pub mut: } ``` -### Usage Example - +- `FSEntry` implementations: ```v -// Create a new VFS instance -mut fs := vfs_db.new(data_dir: "/path/to/data", metadata_dir: "/path/to/metadata")! - -// Create a directory -fs.dir_create("/mydir")! - -// Create and write to a file -fs.file_create("/mydir/test.txt")! -fs.file_write("/mydir/test.txt", "Hello World".bytes())! - -// Read file content -content := fs.file_read("/mydir/test.txt")! - -// Create a symbolic link -fs.link_create("/mydir/test.txt", "/mydir/link.txt")! - -// List directory contents -entries := fs.dir_list("/mydir")! - -// Delete files/directories -fs.file_delete("/mydir/test.txt")! -fs.dir_delete("/mydir")! +pub type FSEntry = Directory | File | Symlink ``` -### Data Encoding +### Data Storage -The system uses an efficient binary encoding format for storing entries: -- First byte: Version number for format compatibility -- Second byte: Entry type indicator -- Remaining bytes: Entry-specific data +#### Metadata Structure +```v +struct Metadata { + id u32 // Unique identifier + name string // Entry name + file_type FileType + size u64 + created_at i64 // Unix timestamp + modified_at i64 + accessed_at i64 + mode u32 // Permissions + owner string + group string +} +``` -This ensures minimal storage overhead while maintaining data integrity. +#### Database Interface +```v +pub interface Database { +mut: + get(id u32) ![]u8 + set(ourdb.OurDBSetArgs) !u32 + delete(id u32)! +} +``` -## Error Handling +## Usage -The implementation uses V's error handling system with descriptive error messages for: -- File/directory not found -- Permission issues -- Invalid operations -- Database errors +```v +import vfs -## Thread Safety +fn main() ! { + // Create a new database-backed VFS + mut fs := vfs.new_vfs('db', { + data_dir: '/path/to/data' + metadata_dir: '/path/to/metadata' + })! + + // Create directory structure + fs.dir_create('documents')! + fs.dir_create('documents/reports')! + + // Create and write files + fs.file_create('documents/reports/q1.txt')! + fs.file_write('documents/reports/q1.txt', 'Q1 Report Content'.bytes())! + + // Create symbolic links + fs.link_create('documents/reports/q1.txt', 'documents/latest.txt')! + + // List directory contents + entries := fs.dir_list('documents')! + for entry in entries { + println('${entry.get_path()} (${entry.get_metadata().size} bytes)') + } + + // Clean up + fs.destroy()! +} +``` -The implementation is designed to be thread-safe through: -- Proper mutex usage -- Atomic operations -- Clear ownership semantics +## Implementation Notes + +1. Data Encoding: + - Version byte for format compatibility + - Entry type indicator + - Entry-specific binary data + - Efficient storage format + +2. Thread Safety: + - Mutex protection for concurrent access + - Atomic operations + - Clear ownership semantics + +3. Error Handling: + - Descriptive error messages + - Proper error propagation + - Recovery mechanisms + - Consistency checks + +## Limitations + +- Performance overhead compared to direct filesystem access +- Database size grows with filesystem usage +- Requires proper database maintenance +- Limited by database backend capabilities + +## Testing + +The implementation includes tests for: +- Basic operations (create, read, write, delete) +- Directory operations and traversal +- Symlink handling +- Concurrent access +- Error conditions +- Edge cases +- Data consistency + +Run tests with: +```bash +v test vfs/vfs_db/ +``` ## Future Improvements -1. **Performance Optimizations** - - Caching frequently accessed entries - - Batch operations support - - Improved directory traversal +1. Performance Optimizations: + - Entry caching + - Batch operations + - Improved traversal algorithms -2. **Feature Additions** - - Extended attribute support +2. Feature Additions: + - Extended attributes - Access control lists - Quota management - Transaction support -3. **Robustness** - - Recovery mechanisms - - Consistency checks - - Better error recovery +3. Robustness: + - Automated recovery + - Consistency verification + - Better error handling + - Backup/restore capabilities diff --git a/lib/vfs/vfs_db/vfs_implementation_test.v b/lib/vfs/vfs_db/vfs_implementation_test.v index c1f13359..764bb72e 100644 --- a/lib/vfs/vfs_db/vfs_implementation_test.v +++ b/lib/vfs/vfs_db/vfs_implementation_test.v @@ -1,28 +1,31 @@ module vfs_db import os +import freeflowuniverse.herolib.data.ourdb import rand -fn setup_vfs() !(&DatabaseVFS, string, string) { +fn setup_vfs() !(&DatabaseVFS, string) { test_data_dir := os.join_path(os.temp_dir(), 'vfsourdb_test_data_${rand.string(3)}') - test_meta_dir := os.join_path(os.temp_dir(), 'vfsourdb_test_meta_${rand.string(3)}') os.mkdir_all(test_data_dir)! - os.mkdir_all(test_meta_dir)! - mut vfs := new(test_data_dir, test_meta_dir)! - return vfs, test_data_dir, test_meta_dir + mut db_data := ourdb.new( + path: test_data_dir + incremental_mode: false + )! + + mut vfs := new(mut db_data, data_dir: test_data_dir)! + return vfs, test_data_dir } -fn teardown_vfs(data_dir string, meta_dir string) { +fn teardown_vfs(data_dir string) { os.rmdir_all(data_dir) or {} - os.rmdir_all(meta_dir) or {} } fn test_root_directory() ! { - mut vfs, data_dir, meta_dir := setup_vfs()! + mut vfs, data_dir := setup_vfs()! defer { - teardown_vfs(data_dir, meta_dir) + teardown_vfs(data_dir) } mut root := vfs.root_get()! @@ -31,9 +34,9 @@ fn test_root_directory() ! { } fn test_directory_operations() ! { - mut vfs, data_dir, meta_dir := setup_vfs()! + mut vfs, data_dir := setup_vfs()! defer { - teardown_vfs(data_dir, meta_dir) + teardown_vfs(data_dir) } // Test creation @@ -51,9 +54,9 @@ fn test_directory_operations() ! { } fn test_file_operations() ! { - mut vfs, data_dir, meta_dir := setup_vfs()! + mut vfs, data_dir := setup_vfs()! defer { - teardown_vfs(data_dir, meta_dir) + teardown_vfs(data_dir) } vfs.dir_create('/test_dir')! @@ -74,9 +77,9 @@ fn test_file_operations() ! { } fn test_directory_move() ! { - mut vfs, data_dir, meta_dir := setup_vfs()! + mut vfs, data_dir := setup_vfs()! defer { - teardown_vfs(data_dir, meta_dir) + teardown_vfs(data_dir) } vfs.dir_create('/test_dir')! @@ -98,9 +101,9 @@ fn test_directory_move() ! { } fn test_directory_copy() ! { - mut vfs, data_dir, meta_dir := setup_vfs()! + mut vfs, data_dir := setup_vfs()! defer { - teardown_vfs(data_dir, meta_dir) + teardown_vfs(data_dir) } vfs.dir_create('/test_dir')! @@ -115,9 +118,9 @@ fn test_directory_copy() ! { } fn test_nested_directory_move() ! { - mut vfs, data_dir, meta_dir := setup_vfs()! + mut vfs, data_dir := setup_vfs()! defer { - teardown_vfs(data_dir, meta_dir) + teardown_vfs(data_dir) } vfs.dir_create('/test_dir2')! @@ -132,9 +135,9 @@ fn test_nested_directory_move() ! { } fn test_deletion_operations() ! { - mut vfs, data_dir, meta_dir := setup_vfs()! + mut vfs, data_dir := setup_vfs()! defer { - teardown_vfs(data_dir, meta_dir) + teardown_vfs(data_dir) } vfs.dir_create('/test_dir')! diff --git a/lib/vfs/vfs_local/README.md b/lib/vfs/vfs_local/README.md index 7254efad..f63a3580 100644 --- a/lib/vfs/vfs_local/README.md +++ b/lib/vfs/vfs_local/README.md @@ -1,9 +1,111 @@ -### Local Filesystem (LocalVFS) +# Local Filesystem Implementation (vfs_local) -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. +The LocalVFS implementation provides a direct passthrough to the operating system's filesystem. It implements all VFS operations by delegating to the corresponding OS filesystem operations. -Features: -- Direct access to local filesystem -- Full support for all vfscore operations +## Features + +- Native filesystem access with full POSIX compliance - Preserves file permissions and metadata -- Efficient for local file operations \ No newline at end of file +- Efficient direct access to local files +- Support for all VFS operations including symlinks +- Path-based access relative to root directory + +## Implementation Details + +### Structure +``` +vfs_local/ +├── factory.v # VFS factory implementation +├── vfs_implementation.v # Core VFS interface implementation +├── vfs_local.v # LocalVFS type definition +├── model_fsentry.v # FSEntry implementation +└── vfs_implementation_test.v # Implementation tests +``` + +### Key Components + +- `LocalVFS`: Main implementation struct that handles filesystem operations +- `LocalFSEntry`: Implementation of FSEntry interface for local filesystem entries +- `factory.v`: Provides `new_local_vfs()` for creating instances + +### Error Handling + +The implementation provides detailed error messages including: +- Path validation +- Permission checks +- File existence verification +- Type checking (file/directory/symlink) + +## Usage + +```v +import vfs + +fn main() ! { + // Create a new local VFS instance rooted at /tmp/test + mut fs := vfs.new_vfs('local', '/tmp/test')! + + // Basic file operations + fs.file_create('example.txt')! + fs.file_write('example.txt', 'Hello from LocalVFS'.bytes())! + + // Read file contents + content := fs.file_read('example.txt')! + println(content.bytestr()) + + // Directory operations + fs.dir_create('subdir')! + fs.file_create('subdir/nested.txt')! + + // List directory contents + entries := fs.dir_list('subdir')! + for entry in entries { + println('Found: ${entry.get_path()}') + } + + // Symlink operations + fs.link_create('example.txt', 'link.txt')! + target := fs.link_read('link.txt')! + println('Link target: ${target}') + + // Clean up + fs.destroy()! +} +``` + +## Limitations + +- Operations are restricted to the root directory specified during creation +- Symlink support depends on OS capabilities +- File permissions follow OS user context + +## Implementation Notes + +1. Path Handling: + - All paths are made relative to the VFS root + - Absolute paths are converted to relative + - Parent directory (..) references are resolved + +2. Error Cases: + - Non-existent files/directories + - Permission denied + - Invalid operations (e.g., reading directory as file) + - Path traversal attempts + +3. Metadata: + - Preserves OS file metadata + - Maps OS attributes to VFS metadata structure + - Maintains creation/modification times + +## Testing + +The implementation includes comprehensive tests covering: +- Basic file operations +- Directory manipulation +- Symlink handling +- Error conditions +- Edge cases + +Run tests with: +```bash +v test vfs/vfs_local/ diff --git a/lib/vfs/vfs_local/factory.v b/lib/vfs/vfs_local/factory.v new file mode 100644 index 00000000..76231147 --- /dev/null +++ b/lib/vfs/vfs_local/factory.v @@ -0,0 +1,28 @@ +module vfs_local + +import os +import freeflowuniverse.herolib.vfs + +// LocalVFS implements vfs.VFSImplementation for local filesystem +pub struct LocalVFS { +mut: + root_path string +} + +// Create a new LocalVFS instance +pub fn new_local_vfs(root_path string) !vfs.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}') + } + } +} diff --git a/lib/vfs/vfs_local/local.v b/lib/vfs/vfs_local/vfs_implementation.v similarity index 84% rename from lib/vfs/vfs_local/local.v rename to lib/vfs/vfs_local/vfs_implementation.v index c8133b4f..200842e2 100644 --- a/lib/vfs/vfs_local/local.v +++ b/lib/vfs/vfs_local/vfs_implementation.v @@ -3,69 +3,6 @@ module vfs_local import os import freeflowuniverse.herolib.vfs -// LocalVFS implements vfs.VFSImplementation for local filesystem -pub struct LocalVFS { -mut: - root_path string -} - -// Create a new LocalVFS instance -pub fn new_local_vfs(root_path string) !vfs.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 vfs.Metadata with improved security and information gathering -fn (myvfs LocalVFS) os_attr_to_metadata(path string) !vfs.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 := vfs.FileType.file - if os.is_dir(path) { - file_type = .directory - } else if os.is_link(path) { - file_type = .symlink - } - - return vfs.Metadata{ - id: u32(attr.inode) // QUESTION: what should id be here - 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() !vfs.FSEntry { if !os.exists(myvfs.root_path) { @@ -179,6 +116,55 @@ pub fn (myvfs LocalVFS) dir_delete(path string) ! { os.rmdir_all(abs_path) or { return error('Failed to delete directory ${path}: ${err}') } } +// Symlink operations with improved handling +pub fn (myvfs LocalVFS) link_create(target_path string, link_path string) !vfs.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}') } +} + // 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 @@ -280,51 +266,13 @@ pub fn (myvfs LocalVFS) delete(path string) ! { } } -// Symlink operations with improved handling -pub fn (myvfs LocalVFS) link_create(target_path string, link_path string) !vfs.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}') +// 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}') } - 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 + os.rmdir_all(myvfs.root_path) or { + return error('Failed to destroy vfscore at ${myvfs.root_path}: ${err}') } -} - -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}') } + myvfs.init()! } diff --git a/lib/vfs/vfs_local/local_test.v b/lib/vfs/vfs_local/vfs_implementation_test.v similarity index 100% rename from lib/vfs/vfs_local/local_test.v rename to lib/vfs/vfs_local/vfs_implementation_test.v diff --git a/lib/vfs/vfs_local/vfs_local.v b/lib/vfs/vfs_local/vfs_local.v new file mode 100644 index 00000000..0d50f466 --- /dev/null +++ b/lib/vfs/vfs_local/vfs_local.v @@ -0,0 +1,32 @@ +module vfs_local + +import os +import freeflowuniverse.herolib.vfs + +// Convert path to vfs.Metadata with improved security and information gathering +fn (myvfs LocalVFS) os_attr_to_metadata(path string) !vfs.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 := vfs.FileType.file + if os.is_dir(path) { + file_type = .directory + } else if os.is_link(path) { + file_type = .symlink + } + + return vfs.Metadata{ + id: u32(attr.inode) // QUESTION: what should id be here + 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) +} diff --git a/lib/vfs/vfs_nested/README.md b/lib/vfs/vfs_nested/README.md new file mode 100644 index 00000000..042d44fe --- /dev/null +++ b/lib/vfs/vfs_nested/README.md @@ -0,0 +1,48 @@ +# Nested Filesystem Implementation (vfs_nested) + +A virtual filesystem implementation that allows mounting multiple VFS implementations at different path prefixes, creating a unified filesystem view. + +## Features + +- Mount multiple VFS implementations +- Path-based routing to appropriate implementations +- Transparent operation across mounted filesystems +- Hierarchical organization +- Cross-implementation file operations +- Virtual root directory showing mount points + +## Implementation Details + +### Structure +``` +vfs_nested/ +├── vfsnested.v # Core implementation +└── nested_test.v # Implementation tests +``` + +### Key Components + +- `NestedVFS`: Main implementation struct that manages mounted filesystems +- `RootEntry`: Special entry type representing the root directory +- `MountEntry`: Special entry type representing mounted filesystem points + +## Usage + +```v +import vfs +import vfs_nested + +fn main() ! { + mut nested := vfs_nested.new() + mut local_fs := vfs.new_vfs('local', '/tmp/local')! + nested.add_vfs('/local', local_fs)! + nested.file_create('/local/test.txt')! +} +``` + +## Limitations + +- Cannot rename/move files across different implementations +- Symlinks must be contained within a single implementation +- No atomic operations across implementations +- Mount points are fixed after creation diff --git a/lib/vfs/vfsnested/nested_test.v b/lib/vfs/vfs_nested/nested_test.v similarity index 100% rename from lib/vfs/vfsnested/nested_test.v rename to lib/vfs/vfs_nested/nested_test.v diff --git a/lib/vfs/vfsnested/vfsnested.v b/lib/vfs/vfs_nested/vfsnested.v similarity index 100% rename from lib/vfs/vfsnested/vfsnested.v rename to lib/vfs/vfs_nested/vfsnested.v diff --git a/lib/vfs/vfsnested/readme.md b/lib/vfs/vfsnested/readme.md deleted file mode 100644 index cc8be24d..00000000 --- a/lib/vfs/vfsnested/readme.md +++ /dev/null @@ -1,4 +0,0 @@ -# VFS Overlay - -This virtual filesystem combines multiple other VFS'es - diff --git a/lib/vfs/webdav/README.md b/lib/vfs/webdav/README.md deleted file mode 100644 index 85eee645..00000000 --- a/lib/vfs/webdav/README.md +++ /dev/null @@ -1,153 +0,0 @@ -# **WebDAV Server in V** - -This project implements a WebDAV server using the `vweb` framework and modules from `crystallib`. The server supports essential WebDAV file operations such as reading, writing, copying, moving, and deleting files and directories. It also includes **authentication** and **request logging** for better control and debugging. - ---- - -## **Features** - -- **File Operations**: - Supports standard WebDAV methods: `GET`, `PUT`, `DELETE`, `COPY`, `MOVE`, and `MKCOL` (create directory) for files and directories. -- **Authentication**: - Basic HTTP authentication using an in-memory user database (`username:password`). -- **Request Logging**: - Logs incoming requests for debugging and monitoring purposes. -- **WebDAV Compliance**: - Implements WebDAV HTTP methods with proper responses to ensure compatibility with WebDAV clients. -- **Customizable Middleware**: - Extend or modify middleware for custom logging, authentication, or request handling. - ---- - -## **Usage** - -### Running the Server - -```v -module main - -import freeflowuniverse.herolib.vfs.webdav - -fn main() { - mut app := webdav.new_app( - root_dir: '/tmp/rootdir' // Directory to serve via WebDAV - user_db: { - 'admin': 'admin' // Username and password for authentication - } - )! - - app.run() -} -``` - -### **Mounting the Server** - -Once the server is running, you can mount it as a WebDAV volume: - -```bash -sudo mount -t davfs -``` - -For example: -```bash -sudo mount -t davfs http://localhost:8080 /mnt/webdav -``` - -**Important Note**: -Ensure the `root_dir` is **not the same as the mount point** to avoid performance issues during operations like `ls`. - ---- - -## **Supported Routes** - -| **Method** | **Route** | **Description** | -|------------|--------------|----------------------------------------------------------| -| `GET` | `/:path...` | Retrieves the contents of a file. | -| `PUT` | `/:path...` | Creates a new file or updates an existing one. | -| `DELETE` | `/:path...` | Deletes a file or directory. | -| `COPY` | `/:path...` | Copies a file or directory to a new location. | -| `MOVE` | `/:path...` | Moves a file or directory to a new location. | -| `MKCOL` | `/:path...` | Creates a new directory. | -| `OPTIONS` | `/:path...` | Lists supported WebDAV methods. | -| `PROPFIND` | `/:path...` | Retrieves properties (e.g., size, date) of a file or directory. | - ---- - -## **Authentication** - -This WebDAV server uses **Basic Authentication**. -Set the `Authorization` header in your client to include your credentials in base64 format: - -```http -Authorization: Basic -``` - -**Example**: -For the credentials `admin:admin`, the header would look like this: -```http -Authorization: Basic YWRtaW46YWRtaW4= -``` - ---- - -## **Configuration** - -You can configure the WebDAV server using the following parameters when calling `new_app`: - -| **Parameter** | **Type** | **Description** | -|-----------------|-------------------|---------------------------------------------------------------| -| `root_dir` | `string` | Root directory to serve files from. | -| `user_db` | `map[string]string` | A map containing usernames as keys and passwords as values. | -| `port` (optional) | `int` | The port on which the server will run. Defaults to `8080`. | - ---- - -## **Example Workflow** - -1. Start the server: - ```bash - v run webdav_server.v - ``` - -2. Mount the server using `davfs`: - ```bash - sudo mount -t davfs http://localhost:8080 /mnt/webdav - ``` - -3. Perform operations: - - Create a new file: - ```bash - echo "Hello WebDAV!" > /mnt/webdav/hello.txt - ``` - - List files: - ```bash - ls /mnt/webdav - ``` - - Delete a file: - ```bash - rm /mnt/webdav/hello.txt - ``` - -4. Check server logs for incoming requests and responses. - ---- - -## **Performance Notes** - -- Avoid mounting the WebDAV server directly into its own root directory (`root_dir`), as this can cause significant slowdowns for file operations like `ls`. -- Use tools like `cadaver`, `curl`, or `davfs` for interacting with the WebDAV server. - ---- - -## **Dependencies** - -- V Programming Language -- Crystallib VFS Module (for WebDAV support) - ---- - -## **Future Enhancements** - -- Support for advanced WebDAV methods like `LOCK` and `UNLOCK`. -- Integration with persistent databases for user credentials. -- TLS/SSL support for secure connections. diff --git a/lib/vfs/webdav/app.v b/lib/vfs/webdav/app.v deleted file mode 100644 index 527073c6..00000000 --- a/lib/vfs/webdav/app.v +++ /dev/null @@ -1,54 +0,0 @@ -module webdav - -import veb -import freeflowuniverse.herolib.ui.console -import freeflowuniverse.herolib.vfs - -@[heap] -pub struct App { - veb.Middleware[Context] -pub mut: - lock_manager LockManager - user_db map[string]string @[required] - vfs vfs.VFSImplementation -} - -pub struct Context { - veb.Context -} - -@[params] -pub struct AppArgs { -pub mut: - user_db map[string]string @[required] - vfs vfs.VFSImplementation -} - -pub fn new_app(args AppArgs) !&App { - mut app := &App{ - user_db: args.user_db.clone() - vfs: args.vfs - } - - // register middlewares for all routes - app.use(handler: app.auth_middleware) - app.use(handler: logging_middleware) - - return app -} - -@[params] -pub struct RunParams { -pub mut: - port int = 8088 - background bool -} - -pub fn (mut app App) run(params RunParams) { - console.print_green('Running the server on port: ${params.port}') - if params.background { - spawn veb.run[App, Context](mut app, params.port) - } else { - veb.run[App, Context](mut app, params.port) - } -} diff --git a/lib/vfs/webdav/bin/main.v b/lib/vfs/webdav/bin/main.v deleted file mode 100644 index b978e8cf..00000000 --- a/lib/vfs/webdav/bin/main.v +++ /dev/null @@ -1,67 +0,0 @@ -import freeflowuniverse.herolib.vfs.webdav -import cli { Command, Flag } -import os - -fn main() { - mut cmd := Command{ - name: 'webdav' - description: 'Vlang Webdav Server' - } - - mut app := Command{ - name: 'webdav' - description: 'Vlang Webdav Server' - execute: fn (cmd Command) ! { - port := cmd.flags.get_int('port')! - directory := cmd.flags.get_string('directory')! - user := cmd.flags.get_string('user')! - password := cmd.flags.get_string('password')! - - mut server := webdav.new_app( - root_dir: directory - server_port: port - user_db: { - user: password - } - )! - - server.run() - return - } - } - - app.add_flag(Flag{ - flag: .int - name: 'port' - abbrev: 'p' - description: 'server port' - default_value: ['8000'] - }) - - app.add_flag(Flag{ - flag: .string - required: true - name: 'directory' - abbrev: 'd' - description: 'server directory' - }) - - app.add_flag(Flag{ - flag: .string - required: true - name: 'user' - abbrev: 'u' - description: 'username' - }) - - app.add_flag(Flag{ - flag: .string - required: true - name: 'password' - abbrev: 'pw' - description: 'user password' - }) - - app.setup() - app.parse(os.args) -} diff --git a/lib/vfs/webdav/lock.v b/lib/vfs/webdav/lock.v deleted file mode 100644 index c8ce1b44..00000000 --- a/lib/vfs/webdav/lock.v +++ /dev/null @@ -1,87 +0,0 @@ -module webdav - -import time -import rand - -struct Lock { - resource string - owner string - token string - depth int // 0 for a single resource, 1 for recursive - timeout int // in seconds - created_at time.Time -} - -struct LockManager { -mut: - locks map[string]Lock -} - -pub fn (mut lm LockManager) lock(resource string, owner string, depth int, timeout int) !string { - if resource in lm.locks { - // Check if the lock is still valid - existing_lock := lm.locks[resource] - if time.now().unix() - existing_lock.created_at.unix() < existing_lock.timeout { - return existing_lock.token // Resource is already locked - } - // Expired lock, remove it - lm.unlock(resource) - } - - // Generate a new lock token - token := rand.uuid_v4() - lm.locks[resource] = Lock{ - resource: resource - owner: owner - token: token - depth: depth - timeout: timeout - created_at: time.now() - } - return token -} - -pub fn (mut lm LockManager) unlock(resource string) bool { - if resource in lm.locks { - lm.locks.delete(resource) - return true - } - return false -} - -pub fn (lm LockManager) is_locked(resource string) bool { - if resource in lm.locks { - lock_ := lm.locks[resource] - // Check if lock is expired - if time.now().unix() - lock_.created_at.unix() >= lock_.timeout { - return false - } - return true - } - return false -} - -pub fn (mut lm LockManager) unlock_with_token(resource string, token string) bool { - if resource in lm.locks { - lock_ := lm.locks[resource] - if lock_.token == token { - lm.locks.delete(resource) - return true - } - } - return false -} - -fn (mut lm LockManager) lock_recursive(resource string, owner string, depth int, timeout int) !string { - if depth == 0 { - return lm.lock(resource, owner, depth, timeout) - } - // Implement logic to lock child resources if depth == 1 - return '' -} - -pub fn (mut lm LockManager) cleanup_expired_locks() { - // now := time.now().unix() - // lm.locks - // lm.locks = lm.locks.filter(it.value.created_at.unix() + it.value.timeout > now) -} diff --git a/lib/vfs/webdav/logic_test.v b/lib/vfs/webdav/logic_test.v deleted file mode 100644 index 852ad38f..00000000 --- a/lib/vfs/webdav/logic_test.v +++ /dev/null @@ -1,39 +0,0 @@ -import freeflowuniverse.herolib.vfs.webdav -import freeflowuniverse.herolib.vfs.vfsnested -import freeflowuniverse.herolib.vfs -import freeflowuniverse.herolib.vfs.vfs_db -import os - -fn test_logic() ! { - println('Testing OurDB VFS Logic to WebDAV Server...') - - // Create test directories - test_data_dir := os.join_path(os.temp_dir(), 'vfs_db_test_data') - test_meta_dir := os.join_path(os.temp_dir(), 'vfs_db_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; lower level VFS Implementations that use OurDB - mut vfs1 := vfs_db.new(test_data_dir, test_meta_dir)! - - mut high_level_vfs := vfsnested.new() - - // Nest OurDB VFS instances at different paths - high_level_vfs.add_vfs('/', vfs1) or { panic(err) } - - // Test directory listing - entries := high_level_vfs.dir_list('/')! - assert entries.len == 1 // Data directory - - // // Check if dir is existing - // assert high_level_vfs.exists('/') == true - - // // Check if dir is not existing - // assert high_level_vfs.exists('/data') == true -} diff --git a/lib/vfs/webdav/methods.v b/lib/vfs/webdav/methods.v deleted file mode 100644 index 5aff9ca2..00000000 --- a/lib/vfs/webdav/methods.v +++ /dev/null @@ -1,259 +0,0 @@ -module webdav - -import time -import freeflowuniverse.herolib.ui.console -import encoding.xml -import net.urllib -import veb - -@['/:path...'; options] -pub fn (app &App) options(mut ctx Context, path string) veb.Result { - ctx.res.set_status(.ok) - ctx.res.header.add_custom('dav', '1,2') or { return ctx.server_error(err.msg()) } - ctx.res.header.add(.allow, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE') - ctx.res.header.add_custom('MS-Author-Via', 'DAV') or { return ctx.server_error(err.msg()) } - ctx.res.header.add(.access_control_allow_origin, '*') - ctx.res.header.add(.access_control_allow_methods, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE') - ctx.res.header.add(.access_control_allow_headers, 'Authorization, Content-Type') - ctx.res.header.add(.content_length, '0') - return ctx.text('') -} - -@['/:path...'; lock] -pub fn (mut app App) lock_handler(mut ctx Context, path string) veb.Result { - resource := ctx.req.url - owner := ctx.get_custom_header('owner') or { return ctx.server_error(err.msg()) } - if owner.len == 0 { - ctx.res.set_status(.bad_request) - return ctx.text('Owner header is required.') - } - - depth := ctx.get_custom_header('Depth') or { '0' }.int() - timeout := ctx.get_custom_header('Timeout') or { '3600' }.int() - token := app.lock_manager.lock(resource, owner, depth, timeout) or { - ctx.res.set_status(.locked) - return ctx.text('Resource is already locked.') - } - - ctx.res.set_status(.ok) - ctx.res.header.add_custom('Lock-Token', token) or { return ctx.server_error(err.msg()) } - return ctx.text('Lock granted with token: ${token}') -} - -@['/:path...'; unlock] -pub fn (mut app App) unlock_handler(mut ctx Context, path string) veb.Result { - resource := ctx.req.url - token := ctx.get_custom_header('Lock-Token') or { return ctx.server_error(err.msg()) } - if token.len == 0 { - console.print_stderr('Unlock failed: `Lock-Token` header required.') - ctx.res.set_status(.bad_request) - return ctx.text('Lock failed: `Owner` header missing.') - } - - if app.lock_manager.unlock_with_token(resource, token) { - ctx.res.set_status(.no_content) - return ctx.text('Lock successfully released') - } - - console.print_stderr('Resource is not locked or token mismatch.') - ctx.res.set_status(.conflict) - return ctx.text('Resource is not locked or token mismatch') -} - -@['/:path...'; get] -pub fn (mut app App) get_file(mut ctx Context, path string) veb.Result { - if !app.vfs.exists(path) { - return ctx.not_found() - } - - fs_entry := app.vfs.get(path) or { - console.print_stderr('failed to get FS Entry ${path}: ${err}') - return ctx.server_error(err.msg()) - } - - file_data := app.vfs.file_read(fs_entry.get_path()) or { return ctx.server_error(err.msg()) } - - ext := fs_entry.get_metadata().name.all_after_last('.') - content_type := veb.mime_types[ext] or { 'text/plain' } - - ctx.res.set_status(.ok) - return ctx.text(file_data.str()) -} - -@[head] -pub fn (app &App) index(mut ctx Context) veb.Result { - ctx.res.header.add(.content_length, '0') - return ctx.ok('') -} - -@['/:path...'; head] -pub fn (mut app App) exists(mut ctx Context, path string) veb.Result { - // Check if the requested path exists in the virtual filesystem - if !app.vfs.exists(path) { - return ctx.not_found() - } - - // Add necessary WebDAV headers - ctx.res.header.add(.authorization, 'Basic') // Indicates Basic auth usage - ctx.res.header.add_custom('DAV', '1, 2') or { - return ctx.server_error('Failed to set DAV header: ${err}') - } - ctx.res.header.add_custom('Etag', 'abc123xyz') or { - return ctx.server_error('Failed to set ETag header: ${err}') - } - ctx.res.header.add(.content_length, '0') // HEAD request, so no body - ctx.res.header.add(.date, time.now().as_utc().format()) // Correct UTC date format - // ctx.res.header.add(.content_type, 'application/xml') // XML is common for WebDAV metadata - ctx.res.header.add_custom('Allow', 'OPTIONS, GET, HEAD, PROPFIND, PROPPATCH, MKCOL, PUT, DELETE, COPY, MOVE, LOCK, UNLOCK') or { - return ctx.server_error('Failed to set Allow header: ${err}') - } - ctx.res.header.add(.accept_ranges, 'bytes') // Allows range-based file downloads - ctx.res.header.add_custom('Cache-Control', 'no-cache, no-store, must-revalidate') or { - return ctx.server_error('Failed to set Cache-Control header: ${err}') - } - ctx.res.header.add_custom('Last-Modified', time.now().as_utc().format()) or { - return ctx.server_error('Failed to set Last-Modified header: ${err}') - } - ctx.res.set_status(.ok) - ctx.res.set_version(.v1_1) - - // Debugging output (can be removed in production) - println('HEAD response: ${ctx.res}') - - return ctx.ok('') -} - -@['/:path...'; delete] -pub fn (mut app App) delete(mut ctx Context, path string) veb.Result { - if !app.vfs.exists(path) { - return ctx.not_found() - } - - fs_entry := app.vfs.get(path) or { - console.print_stderr('failed to get FS Entry ${path}: ${err}') - return ctx.server_error(err.msg()) - } - - if fs_entry.is_dir() { - console.print_debug('deleting directory: ${path}') - app.vfs.dir_delete(path) or { return ctx.server_error(err.msg()) } - } - - if fs_entry.is_file() { - console.print_debug('deleting file: ${path}') - app.vfs.file_delete(path) or { return ctx.server_error(err.msg()) } - } - - ctx.res.set_status(.no_content) - return ctx.text('entry ${path} is deleted') -} - -@['/:path...'; copy] -pub fn (mut app App) copy(mut ctx Context, path string) veb.Result { - if !app.vfs.exists(path) { - return ctx.not_found() - } - - destination := ctx.req.header.get_custom('Destination') or { - return ctx.server_error(err.msg()) - } - destination_url := urllib.parse(destination) or { - ctx.res.set_status(.bad_request) - return ctx.text('Invalid Destination ${destination}: ${err}') - } - destination_path_str := destination_url.path - - app.vfs.copy(path, destination_path_str) or { - console.print_stderr('failed to copy: ${err}') - return ctx.server_error(err.msg()) - } - - ctx.res.set_status(.ok) - return ctx.text('HTTP 200: Successfully copied entry: ${path}') -} - -@['/:path...'; move] -pub fn (mut app App) move(mut ctx Context, path string) veb.Result { - if !app.vfs.exists(path) { - return ctx.not_found() - } - - destination := ctx.req.header.get_custom('Destination') or { - return ctx.server_error(err.msg()) - } - destination_url := urllib.parse(destination) or { - ctx.res.set_status(.bad_request) - return ctx.text('Invalid Destination ${destination}: ${err}') - } - destination_path_str := destination_url.path - - app.vfs.move(path, destination_path_str) or { - console.print_stderr('failed to move: ${err}') - return ctx.server_error(err.msg()) - } - - ctx.res.set_status(.ok) - return ctx.text('HTTP 200: Successfully copied entry: ${path}') -} - -@['/:path...'; mkcol] -pub fn (mut app App) mkcol(mut ctx Context, path string) veb.Result { - if app.vfs.exists(path) { - ctx.res.set_status(.bad_request) - return ctx.text('Another collection exists at ${path}') - } - - app.vfs.dir_create(path) or { - console.print_stderr('failed to create directory ${path}: ${err}') - return ctx.server_error(err.msg()) - } - - ctx.res.set_status(.created) - return ctx.text('HTTP 201: Created') -} - -@['/:path...'; propfind] -fn (mut app App) propfind(mut ctx Context, path string) veb.Result { - if !app.vfs.exists(path) { - return ctx.not_found() - } - depth := ctx.req.header.get_custom('Depth') or { '0' }.int() - - responses := app.get_responses(path, depth) or { - console.print_stderr('failed to get responses: ${err}') - return ctx.server_error(err.msg()) - } - doc := xml.XMLDocument{ - root: xml.XMLNode{ - name: 'D:multistatus' - children: responses - attributes: { - 'xmlns:D': 'DAV:' - } - } - } - res := '${doc.pretty_str('').split('\n')[1..].join('')}' - ctx.res.set_status(.multi_status) - return ctx.send_response_to_client('application/xml', res) - // return veb.not_found() -} - -@['/:path...'; put] -fn (mut app App) create_or_update(mut ctx Context, path string) veb.Result { - if app.vfs.exists(path) { - if fs_entry := app.vfs.get(path) { - if fs_entry.is_dir() { - console.print_stderr('Cannot PUT to a directory: ${path}') - ctx.res.set_status(.method_not_allowed) - return ctx.text('HTTP 405: Method Not Allowed') - } - } else { - return ctx.server_error('failed to get FS Entry ${path}: ${err.msg()}') - } - } - - data := ctx.req.data.bytes() - app.vfs.file_write(path, data) or { return ctx.server_error(err.msg()) } - - return ctx.ok('HTTP 200: Successfully saved file: ${path}') -} diff --git a/lib/vfs/webdav/middleware_auth.v b/lib/vfs/webdav/middleware_auth.v deleted file mode 100644 index 6318dbb1..00000000 --- a/lib/vfs/webdav/middleware_auth.v +++ /dev/null @@ -1,49 +0,0 @@ -module webdav - -import encoding.base64 - -fn (app &App) auth_middleware(mut ctx Context) bool { - // return true - auth_header := ctx.get_header(.authorization) or { - ctx.res.set_status(.unauthorized) - ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"') - ctx.send_response_to_client('text', 'unauthorized') - return false - } - - if auth_header == '' { - ctx.res.set_status(.unauthorized) - ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"') - ctx.send_response_to_client('text', 'unauthorized') - return false - } - - if !auth_header.starts_with('Basic ') { - ctx.res.set_status(.unauthorized) - ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"') - ctx.send_response_to_client('text', 'unauthorized') - return false - } - - auth_decoded := base64.decode_str(auth_header[6..]) - split_credentials := auth_decoded.split(':') - if split_credentials.len != 2 { - ctx.res.set_status(.unauthorized) - ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"') - ctx.send_response_to_client('', '') - return false - } - username := split_credentials[0] - hashed_pass := split_credentials[1] - if user := app.user_db[username] { - if user != hashed_pass { - ctx.res.set_status(.unauthorized) - ctx.send_response_to_client('text', 'unauthorized') - return false - } - return true - } - ctx.res.set_status(.unauthorized) - ctx.send_response_to_client('text', 'unauthorized') - return false -} diff --git a/lib/vfs/webdav/middleware_log.v b/lib/vfs/webdav/middleware_log.v deleted file mode 100644 index a78a56ab..00000000 --- a/lib/vfs/webdav/middleware_log.v +++ /dev/null @@ -1,12 +0,0 @@ -module webdav - -import freeflowuniverse.herolib.ui.console - -fn logging_middleware(mut ctx Context) bool { - console.print_green('=== New Request ===') - console.print_green('Method: ${ctx.req.method.str()}') - console.print_green('Path: ${ctx.req.url}') - console.print_green('Headers: ${ctx.req.header}') - console.print_green('') - return true -} diff --git a/lib/vfs/webdav/prop.v b/lib/vfs/webdav/prop.v deleted file mode 100644 index 88becba4..00000000 --- a/lib/vfs/webdav/prop.v +++ /dev/null @@ -1,152 +0,0 @@ -module webdav - -import freeflowuniverse.herolib.core.pathlib -import freeflowuniverse.herolib.vfs -import encoding.xml -import os -import time -import veb - -fn generate_response_element(entry vfs.FSEntry) !xml.XMLNode { - path := if entry.is_dir() && entry.get_path() != '/' { - '${entry.get_path()}/' - } else { - entry.get_path() - } - - return xml.XMLNode{ - name: 'D:response' - children: [ - xml.XMLNode{ - name: 'D:href' - children: [path] - }, - generate_propstat_element(entry)!, - ] - } -} - -const xml_ok_status = xml.XMLNode{ - name: 'D:status' - children: ['HTTP/1.1 200 OK'] -} - -const xml_500_status = xml.XMLNode{ - name: 'D:status' - children: ['HTTP/1.1 500 Internal Server Error'] -} - -fn generate_propstat_element(entry vfs.FSEntry) !xml.XMLNode { - prop := generate_prop_element(entry) or { - // TODO: status should be according to returned error - return xml.XMLNode{ - name: 'D:propstat' - children: [xml_500_status] - } - } - - return xml.XMLNode{ - name: 'D:propstat' - children: [prop, xml_ok_status] - } -} - -fn generate_prop_element(entry vfs.FSEntry) !xml.XMLNode { - metadata := entry.get_metadata() - - display_name := xml.XMLNode{ - name: 'D:displayname' - children: ['${metadata.name}'] - } - - content_length := if entry.is_dir() { 0 } else { metadata.size } - get_content_length := xml.XMLNode{ - name: 'D:getcontentlength' - children: ['${content_length}'] - } - - creation_date := xml.XMLNode{ - name: 'D:creationdate' - children: ['${format_iso8601(metadata.created_time())}'] - } - - get_last_mod := xml.XMLNode{ - name: 'D:getlastmodified' - children: ['${format_iso8601(metadata.modified_time())}'] - } - - content_type := match entry.is_dir() { - true { - 'httpd/unix-directory' - } - false { - get_file_content_type(entry.get_path()) - } - } - - get_content_type := xml.XMLNode{ - name: 'D:getcontenttype' - children: ['${content_type}'] - } - - mut get_resource_type_children := []xml.XMLNodeContents{} - - if entry.is_dir() { - get_resource_type_children << xml.XMLNode{ - name: 'D:collection xmlns:D="DAV:"' - } - } - - get_resource_type := xml.XMLNode{ - name: 'D:resourcetype' - children: get_resource_type_children - } - - mut nodes := []xml.XMLNodeContents{} - nodes << display_name - nodes << get_last_mod - nodes << get_content_type - nodes << get_resource_type - if !entry.is_dir() { - nodes << get_content_length - } - nodes << creation_date - - mut res := xml.XMLNode{ - name: 'D:prop' - children: nodes.clone() - } - - return res -} - -fn get_file_content_type(path string) string { - ext := path.all_after_last('.') - content_type := if v := veb.mime_types[ext] { - v - } else { - 'text/plain; charset=utf-8' - } - - return content_type -} - -fn format_iso8601(t time.Time) string { - return '${t.year:04d}-${t.month:02d}-${t.day:02d}T${t.hour:02d}:${t.minute:02d}:${t.second:02d}Z' -} - -fn (mut app App) get_responses(path string, depth int) ![]xml.XMLNodeContents { - mut responses := []xml.XMLNodeContents{} - - entry := app.vfs.get(path)! - responses << generate_response_element(entry)! - if depth == 0 { - return responses - } - - entries := app.vfs.dir_list(path) or { return responses } - for e in entries { - responses << generate_response_element(e)! - } - return responses -} diff --git a/lib/vfs/webdav/server_test.v b/lib/vfs/webdav/server_test.v deleted file mode 100644 index 813c0ee2..00000000 --- a/lib/vfs/webdav/server_test.v +++ /dev/null @@ -1,214 +0,0 @@ -module webdav - -import net.http -import freeflowuniverse.herolib.core.pathlib -import time -import encoding.base64 -import rand - -fn test_run() { - mut app := new_app( - user_db: { - 'mario': '123' - } - )! - spawn app.run() -} - -// fn test_get() { -// root_dir := '/tmp/webdav' -// mut app := new_app( -// server_port: rand.int_in_range(8000, 9000)! -// root_dir: root_dir -// user_db: { -// 'mario': '123' -// } -// )! -// app.run(background: true) -// time.sleep(1 * time.second) -// file_name := 'newfile.txt' -// mut p := pathlib.get_file(path: '${root_dir}/${file_name}', create: true)! -// p.write('my new file')! - -// mut req := http.new_request(.get, 'http://localhost:${app.server_port}/${file_name}', -// '') -// signature := base64.encode_str('mario:123') -// req.add_custom_header('Authorization', 'Basic ${signature}')! - -// response := req.do()! -// assert response.body == 'my new file' -// } - -// fn test_put() { -// root_dir := '/tmp/webdav' -// mut app := new_app( -// server_port: rand.int_in_range(8000, 9000)! -// root_dir: root_dir -// user_db: { -// 'mario': '123' -// } -// )! -// app.run(background: true) -// time.sleep(1 * time.second) -// file_name := 'newfile_put.txt' - -// mut data := 'my new put file' -// mut req := http.new_request(.put, 'http://localhost:${app.server_port}/${file_name}', -// data) -// signature := base64.encode_str('mario:123') -// req.add_custom_header('Authorization', 'Basic ${signature}')! -// mut response := req.do()! - -// mut p := pathlib.get_file(path: '${root_dir}/${file_name}')! - -// assert p.exists() -// assert p.read()! == data - -// data = 'updated data' -// req = http.new_request(.put, 'http://localhost:${app.server_port}/${file_name}', data) -// req.add_custom_header('Authorization', 'Basic ${signature}')! -// response = req.do()! - -// p = pathlib.get_file(path: '${root_dir}/${file_name}')! - -// assert p.exists() -// assert p.read()! == data -// } - -// fn test_copy() { -// root_dir := '/tmp/webdav' -// mut app := new_app( -// server_port: rand.int_in_range(8000, 9000)! -// root_dir: root_dir -// user_db: { -// 'mario': '123' -// } -// )! -// app.run(background: true) - -// time.sleep(1 * time.second) -// file_name1, file_name2 := 'newfile_copy1.txt', 'newfile_copy2.txt' -// mut p1 := pathlib.get_file(path: '${root_dir}/${file_name1}', create: true)! -// data := 'file copy data' -// p1.write(data)! - -// mut req := http.new_request(.copy, 'http://localhost:${app.server_port}/${file_name1}', -// '') -// signature := base64.encode_str('mario:123') -// req.add_custom_header('Authorization', 'Basic ${signature}')! -// req.add_custom_header('Destination', 'http://localhost:${app.server_port}/${file_name2}')! -// mut response := req.do()! - -// assert p1.exists() -// mut p2 := pathlib.get_file(path: '${root_dir}/${file_name2}')! -// assert p2.exists() -// assert p2.read()! == data -// } - -// fn test_move() { -// root_dir := '/tmp/webdav' -// mut app := new_app( -// server_port: rand.int_in_range(8000, 9000)! -// root_dir: root_dir -// user_db: { -// 'mario': '123' -// } -// )! -// app.run(background: true) - -// time.sleep(1 * time.second) -// file_name1, file_name2 := 'newfile_move1.txt', 'newfile_move2.txt' -// mut p := pathlib.get_file(path: '${root_dir}/${file_name1}', create: true)! -// data := 'file move data' -// p.write(data)! - -// mut req := http.new_request(.move, 'http://localhost:${app.server_port}/${file_name1}', -// '') -// signature := base64.encode_str('mario:123') -// req.add_custom_header('Authorization', 'Basic ${signature}')! -// req.add_custom_header('Destination', 'http://localhost:${app.server_port}/${file_name2}')! -// mut response := req.do()! - -// p = pathlib.get_file(path: '${root_dir}/${file_name2}')! -// assert p.exists() -// assert p.read()! == data -// } - -// fn test_delete() { -// root_dir := '/tmp/webdav' -// mut app := new_app( -// server_port: rand.int_in_range(8000, 9000)! -// root_dir: root_dir -// user_db: { -// 'mario': '123' -// } -// )! -// app.run(background: true) - -// time.sleep(1 * time.second) -// file_name := 'newfile_delete.txt' -// mut p := pathlib.get_file(path: '${root_dir}/${file_name}', create: true)! - -// mut req := http.new_request(.delete, 'http://localhost:${app.server_port}/${file_name}', -// '') -// signature := base64.encode_str('mario:123') -// req.add_custom_header('Authorization', 'Basic ${signature}')! -// mut response := req.do()! - -// assert !p.exists() -// } - -// fn test_mkcol() { -// root_dir := '/tmp/webdav' -// mut app := new_app( -// server_port: rand.int_in_range(8000, 9000)! -// root_dir: root_dir -// user_db: { -// 'mario': '123' -// } -// )! -// app.run(background: true) - -// time.sleep(1 * time.second) -// dir_name := 'newdir' - -// mut req := http.new_request(.mkcol, 'http://localhost:${app.server_port}/${dir_name}', -// '') -// signature := base64.encode_str('mario:123') -// req.add_custom_header('Authorization', 'Basic ${signature}')! -// mut response := req.do()! - -// mut p := pathlib.get_dir(path: '${root_dir}/${dir_name}')! -// assert p.exists() -// } - -// fn test_propfind() { -// root_dir := '/tmp/webdav' -// mut app := new_app( -// server_port: rand.int_in_range(8000, 9000)! -// root_dir: root_dir -// user_db: { -// 'mario': '123' -// } -// )! -// app.run(background: true) - -// time.sleep(1 * time.second) -// dir_name := 'newdir' -// file1 := 'file1.txt' -// file2 := 'file2.html' -// dir1 := 'dir1' - -// mut p := pathlib.get_dir(path: '${root_dir}/${dir_name}', create: true)! -// mut file1_p := pathlib.get_file(path: '${p.path}/${file1}', create: true)! -// mut file2_p := pathlib.get_file(path: '${p.path}/${file2}', create: true)! -// mut dir1_p := pathlib.get_dir(path: '${p.path}/${dir1}', create: true)! - -// mut req := http.new_request(.propfind, 'http://localhost:${app.server_port}/${dir_name}', -// '') -// signature := base64.encode_str('mario:123') -// req.add_custom_header('Authorization', 'Basic ${signature}')! -// mut response := req.do()! - -// assert response.status_code == 207 -// }