diff --git a/lib/vfs/vfscore/interface.v b/lib/vfs/vfscore/interface.v index 54716890..a859c71d 100644 --- a/lib/vfs/vfscore/interface.v +++ b/lib/vfs/vfscore/interface.v @@ -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 diff --git a/lib/vfs/vfsnested/vfsnested.v b/lib/vfs/vfsnested/vfsnested.v index e4ab06ce..3b2462c6 100644 --- a/lib/vfs/vfsnested/vfsnested.v +++ b/lib/vfs/vfsnested/vfsnested.v @@ -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 diff --git a/lib/vfs/vfsourdb/vfsourdb.v b/lib/vfs/vfsourdb/vfsourdb.v index 87ec3df6..3cb4ca5e 100644 --- a/lib/vfs/vfsourdb/vfsourdb.v +++ b/lib/vfs/vfsourdb/vfsourdb.v @@ -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()!) } diff --git a/lib/vfs/webdav/app.v b/lib/vfs/webdav/app.v index 9e3cc58a..d0425057 100644 --- a/lib/vfs/webdav/app.v +++ b/lib/vfs/webdav/app.v @@ -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 { diff --git a/lib/vfs/webdav/methods.v b/lib/vfs/webdav/methods.v index 3b064415..1ed44cf9 100644 --- a/lib/vfs/webdav/methods.v +++ b/lib/vfs/webdav/methods.v @@ -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 := '${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() -} \ No newline at end of file +} + +@['/: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}') +} diff --git a/lib/vfs/webdav/middleware_auth.v b/lib/vfs/webdav/middleware_auth.v index 0f289db8..09b98f9e 100644 --- a/lib/vfs/webdav/middleware_auth.v +++ b/lib/vfs/webdav/middleware_auth.v @@ -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 } diff --git a/lib/vfs/webdav/middleware_log.v b/lib/vfs/webdav/middleware_log.v index 78660a49..a78a56ab 100644 --- a/lib/vfs/webdav/middleware_log.v +++ b/lib/vfs/webdav/middleware_log.v @@ -1,6 +1,5 @@ module webdav -import vweb import freeflowuniverse.herolib.ui.console fn logging_middleware(mut ctx Context) bool { diff --git a/lib/vfs/webdav/prop.v b/lib/vfs/webdav/prop.v index cf58bf59..ee98f541 100644 --- a/lib/vfs/webdav/prop.v +++ b/lib/vfs/webdav/prop.v @@ -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' @@ -146,25 +135,16 @@ 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 -} +} \ No newline at end of file