isolate vfs's and improve documentation
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')!
|
||||
|
||||
@@ -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/
|
||||
|
||||
28
lib/vfs/vfs_local/factory.v
Normal file
28
lib/vfs/vfs_local/factory.v
Normal 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}')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()!
|
||||
}
|
||||
32
lib/vfs/vfs_local/vfs_local.v
Normal file
32
lib/vfs/vfs_local/vfs_local.v
Normal 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)
|
||||
}
|
||||
48
lib/vfs/vfs_nested/README.md
Normal file
48
lib/vfs/vfs_nested/README.md
Normal 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
|
||||
@@ -1,4 +0,0 @@
|
||||
# VFS Overlay
|
||||
|
||||
This virtual filesystem combines multiple other VFS'es
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
// }
|
||||
Reference in New Issue
Block a user