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:
2025-02-03 18:02:16 +02:00
parent ee205c4b07
commit 1c0535a8b4
9 changed files with 1099 additions and 0 deletions

153
lib/vfs/webdav/README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

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