fix webdav server implementation and logic
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
module vfscore
|
||||
|
||||
import time
|
||||
|
||||
// FileType represents the type of a filesystem entry
|
||||
pub enum FileType {
|
||||
file
|
||||
@@ -19,6 +21,19 @@ pub mut:
|
||||
accessed_at i64 // unix epoch timestamp
|
||||
}
|
||||
|
||||
// Get time.Time objects from epochs
|
||||
pub fn (m Metadata) created_time() time.Time {
|
||||
return time.unix(m.created_at)
|
||||
}
|
||||
|
||||
pub fn (m Metadata) modified_time() time.Time {
|
||||
return time.unix(m.modified_at)
|
||||
}
|
||||
|
||||
pub fn (m Metadata) accessed_time() time.Time {
|
||||
return time.unix(m.accessed_at)
|
||||
}
|
||||
|
||||
// FSEntry represents a filesystem entry (file, directory, or symlink)
|
||||
pub interface FSEntry {
|
||||
get_metadata() Metadata
|
||||
|
||||
@@ -25,6 +25,10 @@ pub fn (mut self NestedVFS) add_vfs(prefix string, impl vfscore.VFSImplementatio
|
||||
|
||||
// find_vfs finds the appropriate VFS implementation for a given path
|
||||
fn (self &NestedVFS) find_vfs(path string) !(vfscore.VFSImplementation, string) {
|
||||
if path == '' || path == '/' {
|
||||
return self, '/'
|
||||
}
|
||||
|
||||
// Sort prefixes by length (longest first) to match most specific path
|
||||
mut prefixes := self.vfs_map.keys()
|
||||
prefixes.sort(a.len > b.len)
|
||||
@@ -122,11 +126,18 @@ pub fn (mut self NestedVFS) dir_delete(path string) ! {
|
||||
}
|
||||
|
||||
pub fn (mut self NestedVFS) exists(path string) bool {
|
||||
// QUESTION: should root be nestervfs's own?
|
||||
if path == '' || path == '/' {
|
||||
return true
|
||||
}
|
||||
mut impl, rel_path := self.find_vfs(path) or { return false }
|
||||
return impl.exists(rel_path)
|
||||
}
|
||||
|
||||
pub fn (mut self NestedVFS) get(path string) !vfscore.FSEntry {
|
||||
if path == '' || path == '/' {
|
||||
return self.root_get()
|
||||
}
|
||||
mut impl, rel_path := self.find_vfs(path)!
|
||||
return impl.get(rel_path)
|
||||
}
|
||||
@@ -227,7 +238,7 @@ fn (e &MountEntry) get_metadata() vfscore.Metadata {
|
||||
}
|
||||
|
||||
fn (e &MountEntry) get_path() string {
|
||||
return '/${e.metadata.name}'
|
||||
return "/${e.metadata.name.trim_left('/')}"
|
||||
}
|
||||
|
||||
// is_dir returns true if the entry is a directory
|
||||
|
||||
@@ -90,6 +90,7 @@ pub fn (mut self OurDBVFS) dir_create(path string) !vfscore.FSEntry {
|
||||
}
|
||||
|
||||
pub fn (mut self OurDBVFS) dir_list(path string) ![]vfscore.FSEntry {
|
||||
println('listing ${path}')
|
||||
mut dir := self.get_directory(path)!
|
||||
mut entries := dir.children(false)!
|
||||
mut result := []vfscore.FSEntry{}
|
||||
@@ -109,7 +110,10 @@ pub fn (mut self OurDBVFS) dir_delete(path string) ! {
|
||||
parent_dir.rm(dir_name)!
|
||||
}
|
||||
|
||||
pub fn (mut self OurDBVFS) exists(path string) bool {
|
||||
pub fn (mut self OurDBVFS) exists(path_ string) bool {
|
||||
path := if !path_.starts_with('/') {
|
||||
'/${path_}'
|
||||
} else {path_}
|
||||
if path == '/' {
|
||||
return true
|
||||
}
|
||||
@@ -174,7 +178,7 @@ pub fn (mut self OurDBVFS) destroy() ! {
|
||||
}
|
||||
|
||||
fn (mut self OurDBVFS) get_entry(path string) !ourdb_fs.FSEntry {
|
||||
if path == '/' {
|
||||
if path == '/' || path == '' {
|
||||
return ourdb_fs.FSEntry(self.core.get_root()!)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ import veb
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.vfs.vfscore
|
||||
|
||||
@[heap]
|
||||
pub struct App {
|
||||
veb.Middleware[Context]
|
||||
server_port int
|
||||
pub mut:
|
||||
lock_manager LockManager
|
||||
user_db map[string]string @[required]
|
||||
vfs vfscore.VFSImplementation @[veb_global]
|
||||
vfs vfscore.VFSImplementation
|
||||
}
|
||||
|
||||
pub struct Context {
|
||||
@@ -31,8 +31,8 @@ pub fn new_app(args AppArgs) !&App {
|
||||
}
|
||||
|
||||
// register middlewares for all routes
|
||||
app.use(handler: app.auth_middleware)
|
||||
app.use(handler: logging_middleware)
|
||||
app.use(handler: unsafe{app.auth_middleware})
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -46,7 +46,7 @@ pub mut:
|
||||
}
|
||||
|
||||
pub fn (mut app App) run(params RunParams) {
|
||||
console.print_green('Running the server on port: ${app.server_port}')
|
||||
console.print_green('Running the server on port: ${params.port}')
|
||||
if params.background {
|
||||
spawn veb.run[App, Context](mut app, params.port)
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module webdav
|
||||
|
||||
import time
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import encoding.xml
|
||||
import net.urllib
|
||||
@@ -14,6 +15,7 @@ pub fn (app &App) options(mut ctx Context, path string) veb.Result {
|
||||
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('')
|
||||
}
|
||||
|
||||
@@ -78,6 +80,49 @@ pub fn (mut app App) get_file(mut ctx Context, path string) veb.Result {
|
||||
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) {
|
||||
@@ -172,14 +217,12 @@ 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'
|
||||
@@ -189,11 +232,30 @@ fn (mut app App) propfind(mut ctx Context, path string) veb.Result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res := '<?xml version="1.0" encoding="UTF-8"?>${doc.pretty_str('').split('\n')[1..].join('')}'
|
||||
// println('res: ${res}')
|
||||
|
||||
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}')
|
||||
}
|
||||
|
||||
@@ -2,24 +2,26 @@ module webdav
|
||||
|
||||
import encoding.base64
|
||||
|
||||
fn (mut app App) auth_middleware(mut ctx Context) bool {
|
||||
fn (app &App) auth_middleware(mut ctx Context) bool {
|
||||
// return true
|
||||
auth_header := ctx.get_header(.authorization) or {
|
||||
ctx.res.set_status(.unauthorized)
|
||||
ctx.send_response_to_client('', 'Authorization header not found in request.')
|
||||
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('', '')
|
||||
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('', '')
|
||||
ctx.send_response_to_client('text', 'unauthorized')
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -31,16 +33,18 @@ fn (mut app App) auth_middleware(mut ctx Context) bool {
|
||||
ctx.send_response_to_client('', '')
|
||||
return false
|
||||
}
|
||||
|
||||
username := split_credentials[0]
|
||||
hashed_pass := split_credentials[1]
|
||||
|
||||
if app.user_db[username] != hashed_pass {
|
||||
ctx.res.set_status(.unauthorized)
|
||||
ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"')
|
||||
ctx.send_response_to_client('', '')
|
||||
return false
|
||||
if user := app.user_db[username] {
|
||||
if user != hashed_pass {
|
||||
ctx.res.set_status(.unauthorized)
|
||||
ctx.send_response_to_client('text', 'unauthorized')
|
||||
return false
|
||||
}
|
||||
println('Successfully authenticated user. ${ctx.req}')
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
ctx.res.set_status(.unauthorized)
|
||||
ctx.send_response_to_client('text', 'unauthorized')
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
module webdav
|
||||
|
||||
import vweb
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
fn logging_middleware(mut ctx Context) bool {
|
||||
|
||||
@@ -1,95 +1,84 @@
|
||||
module webdav
|
||||
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.vfs.vfscore
|
||||
import encoding.xml
|
||||
import os
|
||||
import time
|
||||
import vweb
|
||||
import veb
|
||||
|
||||
fn (mut app App) generate_response_element(path string, depth int) xml.XMLNode {
|
||||
mut path_ := path
|
||||
if !path_.starts_with('/') {
|
||||
path_ = '/${path_}'
|
||||
}
|
||||
|
||||
if os.is_dir(path) && path_ != '/' {
|
||||
path_ = '${path_}/'
|
||||
}
|
||||
|
||||
href := xml.XMLNode{
|
||||
name: 'D:href'
|
||||
children: [path_]
|
||||
}
|
||||
|
||||
propstat := app.generate_propstat_element(path, depth)
|
||||
fn generate_response_element(entry vfscore.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: [href, propstat]
|
||||
children: [
|
||||
xml.XMLNode{
|
||||
name: 'D:href'
|
||||
children: [path]
|
||||
},
|
||||
generate_propstat_element(entry)!
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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']
|
||||
}
|
||||
const xml_ok_status = xml.XMLNode{
|
||||
name: 'D:status'
|
||||
children: ['HTTP/1.1 200 OK']
|
||||
}
|
||||
|
||||
prop := app.generate_prop_element(path, depth) or {
|
||||
const xml_500_status = xml.XMLNode{
|
||||
name: 'D:status'
|
||||
children: ['HTTP/1.1 500 Internal Server Error']
|
||||
}
|
||||
|
||||
fn generate_propstat_element(entry vfscore.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.XMLNode{
|
||||
name: 'D:status'
|
||||
children: ['HTTP/1.1 500 Internal Server Error']
|
||||
},
|
||||
]
|
||||
children: [xml_500_status]
|
||||
}
|
||||
}
|
||||
|
||||
return xml.XMLNode{
|
||||
name: 'D:propstat'
|
||||
children: [prop, status]
|
||||
children: [prop, xml_ok_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)!
|
||||
fn generate_prop_element(entry vfscore.FSEntry) !xml.XMLNode {
|
||||
metadata := entry.get_metadata()
|
||||
|
||||
display_name := xml.XMLNode{
|
||||
name: 'D:displayname'
|
||||
children: ['${os.file_name(path)}']
|
||||
children: ['${metadata.name}']
|
||||
}
|
||||
|
||||
content_length := if os.is_dir(path) { 0 } else { stat.size }
|
||||
content_length := if entry.is_dir() { 0 } else { metadata.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}']
|
||||
children: ['${format_iso8601(metadata.created_time())}']
|
||||
}
|
||||
|
||||
mtime := format_iso8601(time.unix(stat.mtime))
|
||||
get_last_mod := xml.XMLNode{
|
||||
name: 'D:getlastmodified'
|
||||
children: ['${mtime}']
|
||||
children: ['${format_iso8601(metadata.modified_time())}']
|
||||
}
|
||||
|
||||
content_type := match os.is_dir(path) {
|
||||
content_type := match entry.is_dir() {
|
||||
true {
|
||||
'httpd/unix-directory'
|
||||
}
|
||||
false {
|
||||
app.get_file_content_type(path)
|
||||
get_file_content_type(entry.get_path())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +89,7 @@ fn (mut app App) generate_prop_element(path string, depth int) !xml.XMLNode {
|
||||
|
||||
mut get_resource_type_children := []xml.XMLNodeContents{}
|
||||
|
||||
if os.is_dir(path) {
|
||||
if entry.is_dir() {
|
||||
get_resource_type_children << xml.XMLNode{
|
||||
name: 'D:collection xmlns:D="DAV:"'
|
||||
}
|
||||
@@ -116,7 +105,7 @@ fn (mut app App) generate_prop_element(path string, depth int) !xml.XMLNode {
|
||||
nodes << get_last_mod
|
||||
nodes << get_content_type
|
||||
nodes << get_resource_type
|
||||
if !os.is_dir(path) {
|
||||
if !entry.is_dir() {
|
||||
nodes << get_content_length
|
||||
}
|
||||
nodes << creation_date
|
||||
@@ -129,9 +118,9 @@ fn (mut app App) generate_prop_element(path string, depth int) !xml.XMLNode {
|
||||
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] {
|
||||
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'
|
||||
@@ -147,24 +136,15 @@ fn format_iso8601(t time.Time) string {
|
||||
fn (mut app App) get_responses(path string, depth int) ![]xml.XMLNodeContents {
|
||||
mut responses := []xml.XMLNodeContents{}
|
||||
|
||||
responses << app.generate_response_element(path, depth)
|
||||
entry := app.vfs.get(path)!
|
||||
responses << generate_response_element(entry)!
|
||||
if depth == 0 {
|
||||
return responses
|
||||
}
|
||||
|
||||
if os.is_dir(path) {
|
||||
mut dir := pathlib.get_dir(path: path) or {
|
||||
return error('failed to get directory ${path}: ${err}')
|
||||
}
|
||||
|
||||
entries := dir.list(recursive: false) or {
|
||||
return error('failed to list directory ${path}: ${err}')
|
||||
}
|
||||
|
||||
for entry in entries.paths {
|
||||
responses << app.generate_response_element(entry.path, depth)
|
||||
}
|
||||
entries := app.vfs.dir_list(path) or {return responses}
|
||||
for e in entries {
|
||||
responses << generate_response_element(e)!
|
||||
}
|
||||
|
||||
return responses
|
||||
}
|
||||
Reference in New Issue
Block a user