isolate vfs's and improve documentation

This commit is contained in:
timurgordon
2025-02-27 11:42:46 +03:00
parent fe1becabaf
commit d06a806184
23 changed files with 551 additions and 1459 deletions

View File

@@ -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_<name>/
├── 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_<name>`
- Main struct: `<Name>VFS` (e.g., LocalVFS, DatabaseVFS)
- Factory function: `new_<name>_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_<name>` 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/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
# VFS Overlay
This virtual filesystem combines multiple other VFS'es

View File

@@ -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 <server_url> <mount_point>
```
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 <base64-encoded-credentials>
```
**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.

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 := '<?xml version="1.0" encoding="UTF-8"?>${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}')
}

View File

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

View File

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

View File

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

View File

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