WIP: Implement WebDAV server
- Add a WebDAV server implementation using the `vweb` framework. - The server supports basic authentication, request logging, and essential WebDAV methods. - Implements file operations, authentication, and request logging. Co-authored-by: mahmmoud.hassanein <mahmmoud.hassanein@gmail.com>
This commit is contained in:
153
lib/vfs/webdav/README.md
Normal file
153
lib/vfs/webdav/README.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# **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.
|
||||
69
lib/vfs/webdav/app.v
Normal file
69
lib/vfs/webdav/app.v
Normal file
@@ -0,0 +1,69 @@
|
||||
module webdav
|
||||
|
||||
import vweb
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
@[heap]
|
||||
struct App {
|
||||
vweb.Context
|
||||
user_db map[string]string @[required]
|
||||
root_dir pathlib.Path @[vweb_global]
|
||||
pub mut:
|
||||
lock_manager LockManager
|
||||
server_port int
|
||||
middlewares map[string][]vweb.Middleware
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct AppArgs {
|
||||
pub mut:
|
||||
server_port int = 8080
|
||||
root_dir string @[required]
|
||||
user_db map[string]string @[required]
|
||||
}
|
||||
|
||||
pub fn new_app(args AppArgs) !&App {
|
||||
root_dir := pathlib.get_dir(path: args.root_dir, create: true)!
|
||||
mut app := &App{
|
||||
user_db: args.user_db.clone()
|
||||
root_dir: root_dir
|
||||
server_port: args.server_port
|
||||
}
|
||||
|
||||
app.middlewares['/'] << logging_middleware
|
||||
app.middlewares['/'] << app.auth_middleware
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct RunArgs {
|
||||
pub mut:
|
||||
background bool
|
||||
}
|
||||
|
||||
pub fn (mut app App) run(args RunArgs) {
|
||||
console.print_green('Running the server on port: ${app.server_port}')
|
||||
|
||||
if args.background {
|
||||
spawn vweb.run(app, app.server_port)
|
||||
} else {
|
||||
vweb.run(app, app.server_port)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut app App) not_found() vweb.Result {
|
||||
app.set_status(404, 'Not Found')
|
||||
return app.text('Not Found')
|
||||
}
|
||||
|
||||
pub fn (mut app App) server_error() vweb.Result {
|
||||
app.set_status(500, 'Inernal Server Error')
|
||||
return app.text('Internal Server Error')
|
||||
}
|
||||
|
||||
pub fn (mut app App) bad_request(message string) vweb.Result {
|
||||
app.set_status(400, 'Bad Request')
|
||||
return app.text(message)
|
||||
}
|
||||
43
lib/vfs/webdav/auth.v
Normal file
43
lib/vfs/webdav/auth.v
Normal file
@@ -0,0 +1,43 @@
|
||||
module webdav
|
||||
|
||||
import vweb
|
||||
import encoding.base64
|
||||
|
||||
fn (mut app App) auth_middleware(mut ctx vweb.Context) bool {
|
||||
auth_header := ctx.get_header('Authorization')
|
||||
|
||||
if auth_header == '' {
|
||||
ctx.set_status(401, 'Unauthorized')
|
||||
ctx.add_header('WWW-Authenticate', 'Basic realm="WebDAV Server"')
|
||||
ctx.send_response_to_client('', '')
|
||||
return false
|
||||
}
|
||||
|
||||
if !auth_header.starts_with('Basic ') {
|
||||
ctx.set_status(401, 'Unauthorized')
|
||||
ctx.add_header('WWW-Authenticate', 'Basic realm="WebDAV Server"')
|
||||
ctx.send_response_to_client('', '')
|
||||
return false
|
||||
}
|
||||
|
||||
auth_decoded := base64.decode_str(auth_header[6..])
|
||||
split_credentials := auth_decoded.split(':')
|
||||
if split_credentials.len != 2 {
|
||||
ctx.set_status(401, 'Unauthorized')
|
||||
ctx.add_header('WWW-Authenticate', 'Basic realm="WebDAV Server"')
|
||||
ctx.send_response_to_client('', '')
|
||||
return false
|
||||
}
|
||||
|
||||
username := split_credentials[0]
|
||||
hashed_pass := split_credentials[1]
|
||||
|
||||
if app.user_db[username] != hashed_pass {
|
||||
ctx.set_status(401, 'Unauthorized')
|
||||
ctx.add_header('WWW-Authenticate', 'Basic realm="WebDAV Server"')
|
||||
ctx.send_response_to_client('', '')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
67
lib/vfs/webdav/bin/main.v
Normal file
67
lib/vfs/webdav/bin/main.v
Normal file
@@ -0,0 +1,67 @@
|
||||
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)
|
||||
}
|
||||
87
lib/vfs/webdav/lock.v
Normal file
87
lib/vfs/webdav/lock.v
Normal file
@@ -0,0 +1,87 @@
|
||||
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)
|
||||
}
|
||||
13
lib/vfs/webdav/logging.v
Normal file
13
lib/vfs/webdav/logging.v
Normal file
@@ -0,0 +1,13 @@
|
||||
module webdav
|
||||
|
||||
import vweb
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
fn logging_middleware(mut ctx vweb.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
|
||||
}
|
||||
270
lib/vfs/webdav/methods.v
Normal file
270
lib/vfs/webdav/methods.v
Normal file
@@ -0,0 +1,270 @@
|
||||
module webdav
|
||||
|
||||
import vweb
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import encoding.xml
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import net.urllib
|
||||
|
||||
@['/:path...'; LOCK]
|
||||
fn (mut app App) lock_handler(path string) vweb.Result {
|
||||
// Not yet working
|
||||
// TODO: Test with multiple clients
|
||||
resource := app.req.url
|
||||
owner := app.get_header('Owner')
|
||||
if owner.len == 0 {
|
||||
return app.bad_request('Owner header is required.')
|
||||
}
|
||||
|
||||
depth := if app.get_header('Depth').len > 0 { app.get_header('Depth').int() } else { 0 }
|
||||
timeout := if app.get_header('Timeout').len > 0 { app.get_header('Timeout').int() } else { 3600 }
|
||||
|
||||
token := app.lock_manager.lock(resource, owner, depth, timeout) or {
|
||||
app.set_status(423, 'Locked')
|
||||
return app.text('Resource is already locked.')
|
||||
}
|
||||
|
||||
app.set_status(200, 'OK')
|
||||
app.add_header('Lock-Token', token)
|
||||
return app.text('Lock granted with token: ${token}')
|
||||
}
|
||||
|
||||
@['/:path...'; UNLOCK]
|
||||
fn (mut app App) unlock_handler(path string) vweb.Result {
|
||||
// Not yet working
|
||||
// TODO: Test with multiple clients
|
||||
resource := app.req.url
|
||||
token := app.get_header('Lock-Token')
|
||||
if token.len == 0 {
|
||||
console.print_stderr('Unlock failed: `Lock-Token` header required.')
|
||||
return app.bad_request('Unlock failed: `Lock-Token` header required.')
|
||||
}
|
||||
|
||||
if app.lock_manager.unlock_with_token(resource, token) {
|
||||
app.set_status(204, 'No Content')
|
||||
return app.text('Lock successfully released')
|
||||
}
|
||||
|
||||
console.print_stderr('Resource is not locked or token mismatch.')
|
||||
app.set_status(409, 'Conflict')
|
||||
return app.text('Resource is not locked or token mismatch')
|
||||
}
|
||||
|
||||
@['/:path...'; get]
|
||||
fn (mut app App) get_file(path string) vweb.Result {
|
||||
mut file_path := pathlib.get_file(path: app.root_dir.path + path) or { return app.not_found() }
|
||||
if !file_path.exists() {
|
||||
return app.not_found()
|
||||
}
|
||||
|
||||
file_data := file_path.read() or {
|
||||
console.print_stderr('failed to read file ${file_path.path}: ${err}')
|
||||
return app.server_error()
|
||||
}
|
||||
|
||||
ext := os.file_ext(file_path.path)
|
||||
content_type := if v := vweb.mime_types[ext] {
|
||||
v
|
||||
} else {
|
||||
'text/plain'
|
||||
}
|
||||
|
||||
app.set_status(200, 'Ok')
|
||||
app.send_response_to_client(content_type, file_data)
|
||||
|
||||
return vweb.not_found() // this is for returning a dummy result
|
||||
}
|
||||
|
||||
@['/:path...'; delete]
|
||||
fn (mut app App) delete(path string) vweb.Result {
|
||||
mut p := pathlib.get(app.root_dir.path + path)
|
||||
if !p.exists() {
|
||||
return app.not_found()
|
||||
}
|
||||
|
||||
if p.is_dir() {
|
||||
console.print_debug('deleting directory: ${p.path}')
|
||||
os.rmdir_all(p.path) or { return app.server_error() }
|
||||
}
|
||||
|
||||
if p.is_file() {
|
||||
console.print_debug('deleting file: ${p.path}')
|
||||
os.rm(p.path) or { return app.server_error() }
|
||||
}
|
||||
|
||||
console.print_debug('entry: ${p.path} is deleted')
|
||||
app.set_status(204, 'No Content')
|
||||
|
||||
return app.text('entry ${p.path} is deleted')
|
||||
}
|
||||
|
||||
@['/:path...'; put]
|
||||
fn (mut app App) create_or_update(path string) vweb.Result {
|
||||
mut p := pathlib.get(app.root_dir.path + path)
|
||||
|
||||
if p.is_dir() {
|
||||
console.print_stderr('Cannot PUT to a directory: ${p.path}')
|
||||
app.set_status(405, 'Method Not Allowed')
|
||||
return app.text('HTTP 405: Method Not Allowed')
|
||||
}
|
||||
|
||||
file_data := app.req.data
|
||||
p = pathlib.get_file(path: p.path, create: true) or {
|
||||
console.print_stderr('failed to get file ${p.path}: ${err}')
|
||||
return app.server_error()
|
||||
}
|
||||
|
||||
p.write(file_data) or {
|
||||
console.print_stderr('failed to write file data ${p.path}: ${err}')
|
||||
return app.server_error()
|
||||
}
|
||||
|
||||
app.set_status(200, 'Successfully saved file: ${p.path}')
|
||||
return app.text('HTTP 200: Successfully saved file: ${p.path}')
|
||||
}
|
||||
|
||||
@['/:path...'; copy]
|
||||
fn (mut app App) copy(path string) vweb.Result {
|
||||
mut p := pathlib.get(app.root_dir.path + path)
|
||||
if !p.exists() {
|
||||
return app.not_found()
|
||||
}
|
||||
|
||||
destination := app.get_header('Destination')
|
||||
destination_url := urllib.parse(destination) or {
|
||||
return app.bad_request('Invalid Destination ${destination}: ${err}')
|
||||
}
|
||||
destination_path_str := destination_url.path
|
||||
|
||||
mut destination_path := pathlib.get(app.root_dir.path + destination_path_str)
|
||||
if destination_path.exists() {
|
||||
return app.bad_request('Destination ${destination_path.path} already exists')
|
||||
}
|
||||
|
||||
os.cp_all(p.path, destination_path.path, false) or {
|
||||
console.print_stderr('failed to copy: ${err}')
|
||||
return app.server_error()
|
||||
}
|
||||
|
||||
app.set_status(200, 'Successfully copied entry: ${p.path}')
|
||||
return app.text('HTTP 200: Successfully copied entry: ${p.path}')
|
||||
}
|
||||
|
||||
@['/:path...'; move]
|
||||
fn (mut app App) move(path string) vweb.Result {
|
||||
mut p := pathlib.get(app.root_dir.path + path)
|
||||
if !p.exists() {
|
||||
return app.not_found()
|
||||
}
|
||||
|
||||
destination := app.get_header('Destination')
|
||||
destination_url := urllib.parse(destination) or {
|
||||
return app.bad_request('Invalid Destination ${destination}: ${err}')
|
||||
}
|
||||
destination_path_str := destination_url.path
|
||||
|
||||
mut destination_path := pathlib.get(app.root_dir.path + destination_path_str)
|
||||
if destination_path.exists() {
|
||||
return app.bad_request('Destination ${destination_path.path} already exists')
|
||||
}
|
||||
|
||||
os.mv(p.path, destination_path.path) or {
|
||||
console.print_stderr('failed to copy: ${err}')
|
||||
return app.server_error()
|
||||
}
|
||||
|
||||
app.set_status(200, 'Successfully moved entry: ${p.path}')
|
||||
return app.text('HTTP 200: Successfully moved entry: ${p.path}')
|
||||
}
|
||||
|
||||
@['/:path...'; mkcol]
|
||||
fn (mut app App) mkcol(path string) vweb.Result {
|
||||
mut p := pathlib.get(app.root_dir.path + path)
|
||||
if p.exists() {
|
||||
return app.bad_request('Another collection exists at ${p.path}')
|
||||
}
|
||||
|
||||
p = pathlib.get_dir(path: p.path, create: true) or {
|
||||
console.print_stderr('failed to create directory ${p.path}: ${err}')
|
||||
return app.server_error()
|
||||
}
|
||||
|
||||
app.set_status(201, 'Created')
|
||||
return app.text('HTTP 201: Created')
|
||||
}
|
||||
|
||||
@['/:path...'; options]
|
||||
fn (mut app App) options(path string) vweb.Result {
|
||||
app.set_status(200, 'OK')
|
||||
app.add_header('DAV', '1,2')
|
||||
app.add_header('Allow', 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
|
||||
app.add_header('MS-Author-Via', 'DAV')
|
||||
app.add_header('Access-Control-Allow-Origin', '*')
|
||||
app.add_header('Access-Control-Allow-Methods', 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
|
||||
app.add_header('Access-Control-Allow-Headers', 'Authorization, Content-Type')
|
||||
app.send_response_to_client('text/plain', '')
|
||||
return vweb.not_found()
|
||||
}
|
||||
|
||||
@['/:path...'; propfind]
|
||||
fn (mut app App) propfind(path string) vweb.Result {
|
||||
mut p := pathlib.get(app.root_dir.path + path)
|
||||
if !p.exists() {
|
||||
return app.not_found()
|
||||
}
|
||||
|
||||
depth := app.get_header('Depth').int()
|
||||
println('depth: ${depth}')
|
||||
|
||||
responses := app.get_responses(p.path, depth) or {
|
||||
console.print_stderr('failed to get responses: ${err}')
|
||||
return app.server_error()
|
||||
}
|
||||
|
||||
doc := xml.XMLDocument{
|
||||
root: xml.XMLNode{
|
||||
name: 'D:multistatus'
|
||||
children: responses
|
||||
attributes: {
|
||||
'xmlns:D': 'DAV:'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res := doc.pretty_str('').split('\n')[1..].join('')
|
||||
println('res: ${res}')
|
||||
|
||||
app.set_status(207, 'Multi-Status')
|
||||
app.send_response_to_client('application/xml', res)
|
||||
return vweb.not_found()
|
||||
}
|
||||
|
||||
fn (mut app App) generate_resource_response(path string) string {
|
||||
mut response := ''
|
||||
response += app.generate_element('response', 2)
|
||||
response += app.generate_element('href', 4)
|
||||
response += app.generate_element('/href', 4)
|
||||
response += app.generate_element('/response', 2)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
fn (mut app App) generate_element(element string, space_cnt int) string {
|
||||
mut spaces := ''
|
||||
for i := 0; i < space_cnt; i++ {
|
||||
spaces += ' '
|
||||
}
|
||||
|
||||
return '${spaces}<${element}>\n'
|
||||
}
|
||||
|
||||
// TODO: implement
|
||||
// @['/'; proppatch]
|
||||
// fn (mut app App) prop_patch() vweb.Result {
|
||||
// }
|
||||
|
||||
// TODO: implement, now it's used with PUT
|
||||
// @['/'; post]
|
||||
// fn (mut app App) post() vweb.Result {
|
||||
// }
|
||||
181
lib/vfs/webdav/prop.v
Normal file
181
lib/vfs/webdav/prop.v
Normal file
@@ -0,0 +1,181 @@
|
||||
module webdav
|
||||
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import encoding.xml
|
||||
import os
|
||||
import time
|
||||
import vweb
|
||||
import net.urllib
|
||||
|
||||
fn (mut app App) generate_response_element(path string, depth int) xml.XMLNode {
|
||||
name := os.file_name(path)
|
||||
href_link := urllib.path_escape(name)
|
||||
href := xml.XMLNode{
|
||||
name: 'D:href'
|
||||
children: ['${href_link}']
|
||||
}
|
||||
|
||||
propstat := app.generate_propstat_element(path, depth)
|
||||
|
||||
return xml.XMLNode{
|
||||
name: 'D:response'
|
||||
children: [href, propstat]
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut app App) generate_propstat_element(path string, depth int) xml.XMLNode {
|
||||
mut status := xml.XMLNode{
|
||||
name: 'D:status'
|
||||
children: ['HTTP/1.1 200 OK']
|
||||
}
|
||||
|
||||
prop := app.generate_prop_element(path, depth) or {
|
||||
// TODO: status should be according to returned error
|
||||
return xml.XMLNode{
|
||||
name: 'D:propstat'
|
||||
children: [
|
||||
xml.XMLNode{
|
||||
name: 'D:status'
|
||||
children: ['HTTP/1.1 500 Internal Server Error']
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return xml.XMLNode{
|
||||
name: 'D:propstat'
|
||||
children: [prop, status]
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut app App) generate_prop_element(path string, depth int) !xml.XMLNode {
|
||||
if !os.exists(path) {
|
||||
return error('not found')
|
||||
}
|
||||
|
||||
stat := os.stat(path)!
|
||||
|
||||
// name := match os.is_dir(path) {
|
||||
// true {
|
||||
// os.base(path)
|
||||
// }
|
||||
// false {
|
||||
// os.file_name(path)
|
||||
// }
|
||||
// }
|
||||
// display_name := xml.XMLNode{
|
||||
// name: 'D:displayname'
|
||||
// children: ['${name}']
|
||||
// }
|
||||
|
||||
content_length := if os.is_dir(path) { 0 } else { stat.size }
|
||||
get_content_length := xml.XMLNode{
|
||||
name: 'D:getcontentlength'
|
||||
children: ['${content_length}']
|
||||
}
|
||||
|
||||
ctime := format_iso8601(time.unix(stat.ctime))
|
||||
creation_date := xml.XMLNode{
|
||||
name: 'D:creationdate'
|
||||
children: ['${ctime}']
|
||||
}
|
||||
|
||||
mtime := format_iso8601(time.unix(stat.mtime))
|
||||
get_last_mod := xml.XMLNode{
|
||||
name: 'D:getlastmodified'
|
||||
children: ['${mtime}']
|
||||
}
|
||||
|
||||
content_type := match os.is_dir(path) {
|
||||
true {
|
||||
'httpd/unix-directory'
|
||||
}
|
||||
false {
|
||||
app.get_file_content_type(path)
|
||||
}
|
||||
}
|
||||
|
||||
get_content_type := xml.XMLNode{
|
||||
name: 'D:getcontenttype'
|
||||
children: ['${content_type}']
|
||||
}
|
||||
|
||||
mut get_resource_type_children := []xml.XMLNodeContents{}
|
||||
if os.is_dir(path) {
|
||||
get_resource_type_children << xml.XMLNode{
|
||||
name: 'D:collection '
|
||||
}
|
||||
}
|
||||
|
||||
get_resource_type := xml.XMLNode{
|
||||
name: 'D:resourcetype'
|
||||
children: get_resource_type_children
|
||||
}
|
||||
|
||||
mut nodes := []xml.XMLNodeContents{}
|
||||
nodes << get_content_length
|
||||
nodes << creation_date
|
||||
nodes << get_last_mod
|
||||
nodes << get_resource_type
|
||||
|
||||
if depth > 0 {
|
||||
nodes << get_content_type
|
||||
}
|
||||
|
||||
mut res := xml.XMLNode{
|
||||
name: 'D:prop'
|
||||
children: nodes.clone()
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
fn (mut app App) get_file_content_type(path string) string {
|
||||
ext := os.file_ext(path)
|
||||
content_type := if v := vweb.mime_types[ext] {
|
||||
v
|
||||
} else {
|
||||
'application/octet-stream'
|
||||
}
|
||||
|
||||
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{}
|
||||
|
||||
if depth == 0 {
|
||||
responses << app.generate_response_element(path, depth)
|
||||
return responses
|
||||
}
|
||||
|
||||
if os.is_dir(path) {
|
||||
mut dir := pathlib.get_dir(path: path) or {
|
||||
app.set_status(500, 'failed to get directory ${path}: ${err}')
|
||||
return error('failed to get directory ${path}: ${err}')
|
||||
}
|
||||
|
||||
entries := dir.list(recursive: false) or {
|
||||
app.set_status(500, 'failed to list directory ${path}: ${err}')
|
||||
return error('failed to list directory ${path}: ${err}')
|
||||
}
|
||||
|
||||
// if entries.paths.len == 0 {
|
||||
// // An empty directory
|
||||
// responses << app.generate_response_element(path)
|
||||
// return responses
|
||||
// }
|
||||
|
||||
for entry in entries.paths {
|
||||
responses << app.generate_response_element(entry.path, depth)
|
||||
}
|
||||
} else {
|
||||
responses << app.generate_response_element(path, depth)
|
||||
}
|
||||
|
||||
return responses
|
||||
}
|
||||
216
lib/vfs/webdav/server_test.v
Normal file
216
lib/vfs/webdav/server_test.v
Normal file
@@ -0,0 +1,216 @@
|
||||
module webdav
|
||||
|
||||
import net.http
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import time
|
||||
import encoding.base64
|
||||
import rand
|
||||
|
||||
fn test_run() {
|
||||
root_dir := '/tmp/webdav'
|
||||
mut app := new_app(
|
||||
root_dir: root_dir
|
||||
user_db: {
|
||||
'mario': '123'
|
||||
}
|
||||
)!
|
||||
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