Merge branch 'development_ourdb_new' of https://github.com/freeflowuniverse/herolib into development_ourdb_new
This commit is contained in:
@@ -34,7 +34,6 @@ pub fn new_app(args AppArgs) !&App {
|
||||
app.use(handler: app.auth_middleware)
|
||||
app.use(handler: middleware_log_request)
|
||||
app.use(handler: middleware_log_response, after: true)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
|
||||
196
lib/dav/webdav/app_propfind.v
Normal file
196
lib/dav/webdav/app_propfind.v
Normal file
@@ -0,0 +1,196 @@
|
||||
module webdav
|
||||
|
||||
import encoding.xml
|
||||
import log
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.vfs
|
||||
import freeflowuniverse.herolib.vfs.vfs_db
|
||||
import os
|
||||
import time
|
||||
import veb
|
||||
|
||||
// PropfindRequest represents a parsed PROPFIND request
|
||||
pub struct PropfindRequest {
|
||||
pub:
|
||||
typ PropfindType
|
||||
props []string // Property names if typ is prop
|
||||
depth int // Depth of the request (0, 1, or -1 for infinity)
|
||||
xml_content string // Original XML content
|
||||
}
|
||||
|
||||
// PropfindType represents the type of PROPFIND request
|
||||
pub enum PropfindType {
|
||||
allprop // Request all properties
|
||||
propname // Request property names only
|
||||
prop // Request specific properties
|
||||
invalid // Invalid request
|
||||
}
|
||||
|
||||
// parse_propfind_xml parses the XML body of a PROPFIND request
|
||||
pub fn parse_propfind_xml(data string) !PropfindRequest {
|
||||
if data.len == 0 {
|
||||
// If no body is provided, default to allprop
|
||||
return PropfindRequest{
|
||||
typ: .allprop
|
||||
depth: 0
|
||||
xml_content: ''
|
||||
}
|
||||
}
|
||||
|
||||
doc := xml.XMLDocument.from_string(data) or {
|
||||
return error('Failed to parse XML: ${err}')
|
||||
}
|
||||
|
||||
root := doc.root
|
||||
if root.name.to_lower() != 'propfind' && !root.name.ends_with(':propfind') {
|
||||
return error('Invalid PROPFIND request: root element must be propfind')
|
||||
}
|
||||
|
||||
mut typ := PropfindType.invalid
|
||||
mut props := []string{}
|
||||
|
||||
// Check for allprop, propname, or prop elements
|
||||
for child in root.children {
|
||||
if child is xml.XMLNode {
|
||||
node := child as xml.XMLNode
|
||||
|
||||
// Check for allprop
|
||||
if node.name == 'allprop' || node.name == 'D:allprop' {
|
||||
typ = .allprop
|
||||
break
|
||||
}
|
||||
|
||||
// Check for propname
|
||||
if node.name == 'propname' || node.name == 'D:propname' {
|
||||
typ = .propname
|
||||
break
|
||||
}
|
||||
|
||||
// Check for prop
|
||||
if node.name == 'prop' || node.name == 'D:prop' {
|
||||
typ = .prop
|
||||
|
||||
// Extract property names
|
||||
for prop_child in node.children {
|
||||
if prop_child is xml.XMLNode {
|
||||
prop_node := prop_child as xml.XMLNode
|
||||
props << prop_node.name
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if typ == .invalid {
|
||||
return error('Invalid PROPFIND request: missing prop, allprop, or propname element')
|
||||
}
|
||||
|
||||
return PropfindRequest{
|
||||
typ: typ
|
||||
props: props
|
||||
depth: 0
|
||||
xml_content: data
|
||||
}
|
||||
}
|
||||
|
||||
// parse_depth parses the Depth header value
|
||||
pub fn parse_depth(depth_str string) int {
|
||||
if depth_str == 'infinity' {
|
||||
return -1 // Use -1 to represent infinity
|
||||
}
|
||||
|
||||
depth := depth_str.int()
|
||||
// Only 0, 1, and infinity are valid values for Depth
|
||||
if depth != 0 && depth != 1 {
|
||||
// Invalid depth value, default to 0
|
||||
log.warn('[WebDAV] Invalid Depth header value: ${depth_str}, defaulting to 0')
|
||||
return 0
|
||||
}
|
||||
|
||||
return depth
|
||||
}
|
||||
|
||||
// returns the properties of a filesystem entry
|
||||
fn get_properties(entry &vfs.FSEntry) []Property {
|
||||
mut props := []Property{}
|
||||
|
||||
metadata := entry.get_metadata()
|
||||
|
||||
// Display name
|
||||
props << DisplayName(metadata.name)
|
||||
props << GetLastModified(format_iso8601(metadata.modified_time()))
|
||||
props << GetContentType(if entry.is_dir() {'httpd/unix-directory'} else {get_file_content_type(entry.get_path())})
|
||||
props << ResourceType(entry.is_dir())
|
||||
|
||||
// Content length (only for files)
|
||||
if !entry.is_dir() {
|
||||
props << GetContentLength(metadata.size.str())
|
||||
}
|
||||
|
||||
// Creation date
|
||||
props << CreationDate(format_iso8601(metadata.created_time()))
|
||||
return props
|
||||
}
|
||||
|
||||
// Response represents a WebDAV response for a resource
|
||||
pub struct Response {
|
||||
pub:
|
||||
href string
|
||||
found_props []Property
|
||||
not_found_props []Property
|
||||
}
|
||||
|
||||
fn (r Response) xml() string {
|
||||
return '<D:response>\n<D:href>${r.href}</D:href>
|
||||
<D:propstat>${r.found_props.xml()}<D:status>HTTP/1.1 200 OK</D:status></D:propstat>
|
||||
<D:propstat>${r.not_found_props.xml()}<D:status>HTTP/1.1 404 Not Found</D:status></D:propstat>
|
||||
</D:response>'
|
||||
}
|
||||
|
||||
// generate_propfind_response generates a PROPFIND response XML string from Response structs
|
||||
pub fn (r []Response) xml () string {
|
||||
return '<?xml version="1.0" encoding="UTF-8"?>\n<D:multistatus xmlns:D="DAV:">
|
||||
${r.map(it.xml()).join_lines()}\n</D:multistatus>'
|
||||
}
|
||||
|
||||
fn get_file_content_type(path string) string {
|
||||
ext := path.all_after_last('.')
|
||||
content_type := if v := veb.mime_types[ext] {
|
||||
v
|
||||
} else {
|
||||
'text/plain; charset=utf-8'
|
||||
}
|
||||
|
||||
return content_type
|
||||
}
|
||||
|
||||
// get_responses returns all properties for the given path and depth
|
||||
fn (mut app App) get_responses(entry vfs.FSEntry, req PropfindRequest) ![]Response {
|
||||
mut responses := []Response{}
|
||||
|
||||
path := if entry.is_dir() && entry.get_path() != '/' {
|
||||
'${entry.get_path()}/'
|
||||
} else {
|
||||
entry.get_path()
|
||||
}
|
||||
// main entry response
|
||||
responses << Response {
|
||||
href: path
|
||||
// not_found: entry.get_unfound_properties(req)
|
||||
found_props: get_properties(entry)
|
||||
}
|
||||
|
||||
if req.depth == 0 && entry.get_path() != '/' { return responses }
|
||||
|
||||
entries := app.vfs.dir_list(path) or {
|
||||
log.error('Failed to list directory for ${path} ${err}')
|
||||
return responses }
|
||||
for e in entries {
|
||||
responses << app.get_responses(e, PropfindRequest {
|
||||
...req,
|
||||
depth: if req.depth == 1 { 0 } else {-1}
|
||||
})!
|
||||
}
|
||||
return responses
|
||||
}
|
||||
19
lib/dav/webdav/errors.v
Normal file
19
lib/dav/webdav/errors.v
Normal file
@@ -0,0 +1,19 @@
|
||||
module webdav
|
||||
|
||||
import net.http
|
||||
import veb
|
||||
import log
|
||||
|
||||
struct WebDAVError {
|
||||
pub:
|
||||
status http.Status
|
||||
message string
|
||||
tag string
|
||||
}
|
||||
|
||||
pub fn (mut ctx Context) error(err WebDAVError) veb.Result {
|
||||
message := if err.message != '' {err.message } else {err.status.str().replace('_', ' ').title()}
|
||||
log.error('[WebDAV] ${message}')
|
||||
ctx.res.set_status(err.status)
|
||||
return ctx.send_response_to_client('application/xml', $tmpl('./templates/error_response.xml'))
|
||||
}
|
||||
@@ -3,15 +3,6 @@ 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
|
||||
@@ -27,7 +18,7 @@ pub:
|
||||
// lock attempts to lock a resource for a specific owner
|
||||
// Returns a LockResult with the lock token and whether it's a new lock
|
||||
// Returns an error if the resource is already locked by a different owner
|
||||
pub fn (mut lm LockManager) lock(resource string, owner string, depth int, timeout int) !LockResult {
|
||||
pub fn (mut lm LockManager) lock(resource string, owner string, depth int, timeout int) !Lock {
|
||||
if resource in lm.locks {
|
||||
// Check if the lock is still valid
|
||||
existing_lock := lm.locks[resource]
|
||||
@@ -35,18 +26,15 @@ pub fn (mut lm LockManager) lock(resource string, owner string, depth int, timeo
|
||||
// Resource is already locked
|
||||
if existing_lock.owner == owner {
|
||||
// Same owner, refresh the lock
|
||||
lm.locks[resource] = Lock{
|
||||
refreshed_lock := Lock {...existing_lock,
|
||||
resource: resource
|
||||
owner: owner
|
||||
token: existing_lock.token
|
||||
depth: depth
|
||||
timeout: timeout
|
||||
created_at: time.now()
|
||||
}
|
||||
return LockResult{
|
||||
token: existing_lock.token
|
||||
is_new_lock: false
|
||||
}
|
||||
lm.locks[resource] = refreshed_lock
|
||||
return refreshed_lock
|
||||
} else {
|
||||
// Different owner, return an error
|
||||
return error('Resource is already locked by a different owner')
|
||||
@@ -57,19 +45,16 @@ pub fn (mut lm LockManager) lock(resource string, owner string, depth int, timeo
|
||||
}
|
||||
|
||||
// Generate a new lock token
|
||||
token := rand.uuid_v4()
|
||||
lm.locks[resource] = Lock{
|
||||
new_lock := Lock{
|
||||
resource: resource
|
||||
owner: owner
|
||||
token: token
|
||||
token: rand.uuid_v4()
|
||||
depth: depth
|
||||
timeout: timeout
|
||||
created_at: time.now()
|
||||
}
|
||||
return LockResult{
|
||||
token: token
|
||||
is_new_lock: true
|
||||
}
|
||||
lm.locks[resource] = new_lock
|
||||
return new_lock
|
||||
}
|
||||
|
||||
pub fn (mut lm LockManager) unlock(resource string) bool {
|
||||
@@ -107,9 +92,7 @@ pub fn (lm LockManager) get_lock(resource string) ?Lock {
|
||||
}
|
||||
|
||||
pub fn (mut lm LockManager) unlock_with_token(resource string, token string) bool {
|
||||
println('debugzoA ${resource} ${lm.locks}')
|
||||
if resource in lm.locks {
|
||||
println('debugzoB ${token} ${lm.locks[resource]}')
|
||||
lock_ := lm.locks[resource]
|
||||
if lock_.token == token {
|
||||
lm.locks.delete(resource)
|
||||
@@ -119,7 +102,7 @@ pub fn (mut lm LockManager) unlock_with_token(resource string, token string) boo
|
||||
return false
|
||||
}
|
||||
|
||||
fn (mut lm LockManager) lock_recursive(resource string, owner string, depth int, timeout int) !LockResult {
|
||||
fn (mut lm LockManager) lock_recursive(resource string, owner string, depth int, timeout int) !Lock {
|
||||
if depth == 0 {
|
||||
return lm.lock(resource, owner, depth, timeout)
|
||||
}
|
||||
|
||||
@@ -10,128 +10,19 @@ import strings
|
||||
|
||||
@['/:path...'; options]
|
||||
pub fn (app &App) options(mut ctx Context, path string) veb.Result {
|
||||
ctx.res.set_status(.ok)
|
||||
ctx.res.header.add_custom('dav', '1,2') or { return ctx.server_error(err.msg()) }
|
||||
ctx.res.header.add(.allow, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
|
||||
ctx.res.header.add_custom('MS-Author-Via', 'DAV') or { return ctx.server_error(err.msg()) }
|
||||
ctx.res.header.add(.access_control_allow_origin, '*')
|
||||
ctx.res.header.add(.access_control_allow_methods, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
|
||||
ctx.res.header.add(.access_control_allow_headers, 'Authorization, Content-Type')
|
||||
ctx.res.header.add(.content_length, '0')
|
||||
return ctx.text('')
|
||||
}
|
||||
|
||||
// parse_lock_xml parses the XML data from a WebDAV LOCK request
|
||||
// and extracts the lock parameters (scope, type, owner)
|
||||
fn parse_lock_xml(xml_data string) !LockInfo {
|
||||
mut lock_info := LockInfo{
|
||||
scope: 'exclusive' // default values
|
||||
lock_type: 'write'
|
||||
owner: ''
|
||||
}
|
||||
|
||||
// Parse the XML document
|
||||
doc := xml.XMLDocument.from_string(xml_data) or {
|
||||
return error('Failed to parse XML: ${err}')
|
||||
}
|
||||
|
||||
// Get the root element (lockinfo)
|
||||
root := doc.root
|
||||
|
||||
// Handle namespace prefixes (D:) in element names
|
||||
// WebDAV uses namespaces, so we need to check for both prefixed and non-prefixed names
|
||||
|
||||
// Extract lockscope
|
||||
for child in root.children {
|
||||
if child is xml.XMLNode {
|
||||
node := child as xml.XMLNode
|
||||
|
||||
// Check for lockscope (with or without namespace prefix)
|
||||
if node.name == 'lockscope' || node.name == 'D:lockscope' {
|
||||
for scope_child in node.children {
|
||||
if scope_child is xml.XMLNode {
|
||||
scope_node := scope_child as xml.XMLNode
|
||||
if scope_node.name == 'exclusive' || scope_node.name == 'D:exclusive' {
|
||||
lock_info.scope = 'exclusive'
|
||||
} else if scope_node.name == 'shared' || scope_node.name == 'D:shared' {
|
||||
lock_info.scope = 'shared'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for locktype (with or without namespace prefix)
|
||||
if node.name == 'locktype' || node.name == 'D:locktype' {
|
||||
for type_child in node.children {
|
||||
if type_child is xml.XMLNode {
|
||||
type_node := type_child as xml.XMLNode
|
||||
if type_node.name == 'write' || type_node.name == 'D:write' {
|
||||
lock_info.lock_type = 'write'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for owner (with or without namespace prefix)
|
||||
if node.name == 'owner' || node.name == 'D:owner' {
|
||||
for owner_child in node.children {
|
||||
if owner_child is xml.XMLNode {
|
||||
owner_node := owner_child as xml.XMLNode
|
||||
if owner_node.name == 'href' || owner_node.name == 'D:href' {
|
||||
for href_content in owner_node.children {
|
||||
if href_content is string {
|
||||
lock_info.owner = (href_content as string).trim_space()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if owner_child is string {
|
||||
// Some clients might include owner text directly
|
||||
lock_info.owner = (owner_child as string).trim_space()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If owner is still empty, try to extract it from any text content in the owner node
|
||||
if lock_info.owner.len == 0 {
|
||||
for child in root.children {
|
||||
if child is xml.XMLNode {
|
||||
node := child as xml.XMLNode
|
||||
if node.name == 'owner' || node.name == 'D:owner' {
|
||||
for content in node.children {
|
||||
if content is string {
|
||||
lock_info.owner = (content as string).trim_space()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use a default owner if none was found
|
||||
if lock_info.owner.len == 0 {
|
||||
lock_info.owner = 'unknown-client'
|
||||
}
|
||||
|
||||
// Debug output
|
||||
println('Parsed lock info: scope=${lock_info.scope}, type=${lock_info.lock_type}, owner=${lock_info.owner}')
|
||||
|
||||
return lock_info
|
||||
}
|
||||
|
||||
// LockInfo holds the parsed information from a WebDAV LOCK request
|
||||
struct LockInfo {
|
||||
pub mut:
|
||||
scope string // 'exclusive' or 'shared'
|
||||
lock_type string // typically 'write'
|
||||
owner string // owner identifier
|
||||
ctx.set_custom_header('DAV', '1,2') or { return ctx.server_error(err.msg()) }
|
||||
ctx.set_header(.allow, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
|
||||
// ctx.set_header(.connection, 'close')
|
||||
ctx.set_custom_header('MS-Author-Via', 'DAV') or { return ctx.server_error(err.msg()) }
|
||||
ctx.set_header(.access_control_allow_origin, '*')
|
||||
ctx.set_header(.access_control_allow_methods, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
|
||||
ctx.set_header(.access_control_allow_headers, 'Authorization, Content-Type')
|
||||
ctx.set_header(.content_length, '0')
|
||||
return ctx.ok('')
|
||||
}
|
||||
|
||||
@['/:path...'; lock]
|
||||
pub fn (mut app App) lock_handler(mut ctx Context, path string) veb.Result {
|
||||
pub fn (mut app App) lock(mut ctx Context, path string) veb.Result {
|
||||
resource := ctx.req.url
|
||||
|
||||
// Parse lock information from XML body instead of headers
|
||||
@@ -161,12 +52,7 @@ pub fn (mut app App) lock_handler(mut ctx Context, path string) veb.Result {
|
||||
// Resource is locked by a different owner
|
||||
// Return a 423 Locked status with information about the existing lock
|
||||
ctx.res.set_status(.locked)
|
||||
|
||||
// Create a response with information about the existing lock
|
||||
lock_discovery_response := create_lock_discovery_response(existing_lock)
|
||||
ctx.res.header.add(.content_type, 'application/xml; charset="utf-8"')
|
||||
|
||||
return ctx.text(lock_discovery_response)
|
||||
return ctx.send_response_to_client('application/xml', existing_lock.xml())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,70 +63,14 @@ pub fn (mut app App) lock_handler(mut ctx Context, path string) veb.Result {
|
||||
return ctx.text('Resource is already locked by a different owner.')
|
||||
}
|
||||
ctx.res.set_status(.ok)
|
||||
ctx.res.header.add_custom('Lock-Token', '${lock_result.token}') or { return ctx.server_error(err.msg()) }
|
||||
ctx.set_custom_header('Lock-Token', '${lock_result.token}') or { return ctx.server_error(err.msg()) }
|
||||
|
||||
// Create a proper WebDAV lock response
|
||||
lock_response := create_lock_response(lock_result.token, lock_info, resource, timeout)
|
||||
println('debugzo4444 ${lock_response}')
|
||||
return ctx.send_response_to_client('application/xml', lock_response)
|
||||
}
|
||||
|
||||
// create_lock_discovery_response generates an XML response with information about an existing lock
|
||||
fn create_lock_discovery_response(lock_ Lock) string {
|
||||
mut sb := strings.new_builder(500)
|
||||
sb.write_string('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
// sb.write_string('<D:prop xmlns:D="DAV:">\n')
|
||||
// sb.write_string(' <D:lockdiscovery>\n')
|
||||
sb.write_string(' <D:activelock>\n')
|
||||
sb.write_string(' <D:locktype><D:write/></D:locktype>\n')
|
||||
sb.write_string(' <D:lockscope><D:exclusive/></D:lockscope>\n')
|
||||
sb.write_string(' <D:depth>${lock_.depth}</D:depth>\n')
|
||||
sb.write_string(' <D:owner>\n')
|
||||
sb.write_string(' <D:href>${lock_.owner}</D:href>\n')
|
||||
sb.write_string(' </D:owner>\n')
|
||||
sb.write_string(' <D:timeout>Second-${lock_.timeout}</D:timeout>\n')
|
||||
sb.write_string(' <D:locktoken>\n')
|
||||
sb.write_string(' <D:href>${lock_.token}</D:href>\n')
|
||||
sb.write_string(' </D:locktoken>\n')
|
||||
sb.write_string(' <D:lockroot>\n')
|
||||
sb.write_string(' <D:href>${lock_.resource}</D:href>\n')
|
||||
sb.write_string(' </D:lockroot>\n')
|
||||
sb.write_string(' </D:activelock>\n')
|
||||
// sb.write_string(' </D:lockdiscovery>\n')
|
||||
// sb.write_string('</D:prop>\n')
|
||||
|
||||
return sb.str()
|
||||
}
|
||||
|
||||
// create_lock_response generates the XML response for a successful lock request
|
||||
fn create_lock_response(token string, lock_info LockInfo, resource string, timeout int) string {
|
||||
mut sb := strings.new_builder(500)
|
||||
sb.write_string('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
sb.write_string('<D:prop xmlns:D="DAV:">\n')
|
||||
sb.write_string(' <D:lockdiscovery xmlns:D="DAV:">\n')
|
||||
sb.write_string(' <D:activelock>\n')
|
||||
sb.write_string(' <D:locktype><D:${lock_info.lock_type}/></D:locktype>\n')
|
||||
sb.write_string(' <D:lockscope><D:${lock_info.scope}/></D:lockscope>\n')
|
||||
sb.write_string(' <D:depth>infinity</D:depth>\n')
|
||||
sb.write_string(' <D:owner>\n')
|
||||
sb.write_string(' <D:href>${lock_info.owner}</D:href>\n')
|
||||
sb.write_string(' </D:owner>\n')
|
||||
sb.write_string(' <D:timeout>Second-${timeout}</D:timeout>\n')
|
||||
sb.write_string(' <D:locktoken>\n')
|
||||
sb.write_string(' <D:href>${token}</D:href>\n')
|
||||
sb.write_string(' </D:locktoken>\n')
|
||||
sb.write_string(' <D:lockroot>\n')
|
||||
sb.write_string(' <D:href>${resource}</D:href>\n')
|
||||
sb.write_string(' </D:lockroot>\n')
|
||||
sb.write_string(' </D:activelock>\n')
|
||||
sb.write_string(' </D:lockdiscovery>\n')
|
||||
sb.write_string('</D:prop>\n')
|
||||
|
||||
return sb.str()
|
||||
return ctx.send_response_to_client('application/xml', lock_result.xml())
|
||||
}
|
||||
|
||||
@['/:path...'; unlock]
|
||||
pub fn (mut app App) unlock_handler(mut ctx Context, path string) veb.Result {
|
||||
pub fn (mut app App) unlock(mut ctx Context, path string) veb.Result {
|
||||
resource := ctx.req.url
|
||||
token_ := ctx.get_custom_header('Lock-Token') or { return ctx.server_error(err.msg()) }
|
||||
token := token_.trim_string_left('<').trim_string_right('>')
|
||||
@@ -250,7 +80,6 @@ pub fn (mut app App) unlock_handler(mut ctx Context, path string) veb.Result {
|
||||
return ctx.text('Lock failed: `Owner` header missing.')
|
||||
}
|
||||
|
||||
println('debugzoZ ${token}')
|
||||
if app.lock_manager.unlock_with_token(resource, token) {
|
||||
ctx.res.set_status(.no_content)
|
||||
return ctx.text('Lock successfully released')
|
||||
@@ -263,16 +92,16 @@ pub fn (mut app App) unlock_handler(mut ctx Context, path string) veb.Result {
|
||||
|
||||
@['/:path...'; get]
|
||||
pub fn (mut app App) get_file(mut ctx Context, path string) veb.Result {
|
||||
log.info('[WebDAV] Getting file ${path}')
|
||||
if !app.vfs.exists(path) {
|
||||
return ctx.not_found()
|
||||
}
|
||||
|
||||
fs_entry := app.vfs.get(path) or {
|
||||
console.print_stderr('failed to get FS Entry ${path}: ${err}')
|
||||
log.error('[WebDAV] failed to get FS Entry ${path}: ${err}')
|
||||
return ctx.server_error(err.msg())
|
||||
}
|
||||
|
||||
println('debugzone-- ${fs_entry.get_path()} >> ${path}')
|
||||
file_data := app.vfs.file_read(path) or { return ctx.server_error(err.msg()) }
|
||||
|
||||
ext := fs_entry.get_metadata().name.all_after_last('.')
|
||||
@@ -284,7 +113,13 @@ pub fn (mut app App) get_file(mut ctx Context, path string) veb.Result {
|
||||
|
||||
@[head]
|
||||
pub fn (app &App) index(mut ctx Context) veb.Result {
|
||||
ctx.res.header.add(.content_length, '0')
|
||||
ctx.set_custom_header('DAV', '1,2') or { return ctx.server_error(err.msg()) }
|
||||
ctx.set_header(.allow, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
|
||||
ctx.set_custom_header('MS-Author-Via', 'DAV') or { return ctx.server_error(err.msg()) }
|
||||
ctx.set_header(.access_control_allow_origin, '*')
|
||||
ctx.set_header(.access_control_allow_methods, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
|
||||
ctx.set_header(.access_control_allow_headers, 'Authorization, Content-Type')
|
||||
ctx.set_header(.content_length, '0')
|
||||
return ctx.ok('')
|
||||
}
|
||||
|
||||
@@ -296,32 +131,26 @@ pub fn (mut app App) exists(mut ctx Context, path string) veb.Result {
|
||||
}
|
||||
|
||||
// Add necessary WebDAV headers
|
||||
ctx.res.header.add(.authorization, 'Basic') // Indicates Basic auth usage
|
||||
ctx.res.header.add_custom('DAV', '1, 2') or {
|
||||
// ctx.set_header(.authorization, 'Basic') // Indicates Basic auth usage
|
||||
ctx.set_custom_header('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 {
|
||||
ctx.set_header(.content_length, '0') // HEAD request, so no body
|
||||
// ctx.set_header(.date, time.now().as_utc().format_rfc1123()) // Correct UTC date format
|
||||
// ctx.set_header(.content_type, 'application/xml') // XML is common for WebDAV metadata
|
||||
ctx.set_custom_header('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 {
|
||||
ctx.set_header(.accept_ranges, 'bytes') // Allows range-based file downloads
|
||||
ctx.set_custom_header('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 {
|
||||
ctx.set_custom_header('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('')
|
||||
}
|
||||
|
||||
@@ -336,6 +165,14 @@ pub fn (mut app App) delete(mut ctx Context, path string) veb.Result {
|
||||
return ctx.server_error(err.msg())
|
||||
}
|
||||
|
||||
// Check if the resource is locked
|
||||
if app.lock_manager.is_locked(ctx.req.url) {
|
||||
// Resource is locked, return a 207 Multi-Status response with a 423 Locked status
|
||||
ctx.res.set_status(.multi_status)
|
||||
return ctx.send_response_to_client('application/xml', $tmpl('./templates/delete_response.xml'))
|
||||
}
|
||||
|
||||
// If not locked, proceed with deletion
|
||||
if fs_entry.is_dir() {
|
||||
console.print_debug('deleting directory: ${path}')
|
||||
app.vfs.dir_delete(path) or { return ctx.server_error(err.msg()) }
|
||||
@@ -346,8 +183,8 @@ pub fn (mut app App) delete(mut ctx Context, path string) veb.Result {
|
||||
app.vfs.file_delete(path) or { return ctx.server_error(err.msg()) }
|
||||
}
|
||||
|
||||
ctx.res.set_status(.no_content)
|
||||
return ctx.text('entry ${path} is deleted')
|
||||
// Return success response
|
||||
return ctx.no_content()
|
||||
}
|
||||
|
||||
@['/:path...'; copy]
|
||||
@@ -418,28 +255,52 @@ pub fn (mut app App) mkcol(mut ctx Context, path string) veb.Result {
|
||||
|
||||
@['/:path...'; propfind]
|
||||
fn (mut app App) propfind(mut ctx Context, path string) veb.Result {
|
||||
log.info('[WebDAV] ${@FN} ${path}')
|
||||
|
||||
// Check if resource exists
|
||||
if !app.vfs.exists(path) {
|
||||
return ctx.not_found()
|
||||
return ctx.error(
|
||||
status: .not_found
|
||||
message: 'Path ${path} does not exist'
|
||||
tag: 'resource-must-be-null'
|
||||
)
|
||||
}
|
||||
|
||||
// Parse Depth header
|
||||
depth_str := ctx.req.header.get_custom('Depth') or { '0' }
|
||||
depth := parse_depth(depth_str)
|
||||
|
||||
|
||||
// Parse PROPFIND request
|
||||
propfind_req := parse_propfind_xml(ctx.req.data) or {
|
||||
return ctx.error(WebDAVError{
|
||||
status: .bad_request
|
||||
message: 'Failed to parse PROPFIND XML: ${err}'
|
||||
tag: 'propfind-parse-error'
|
||||
})
|
||||
}
|
||||
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())
|
||||
log.debug('[WebDAV] ${@FN} Propfind Request: ${propfind_req.typ} ${propfind_req.depth}')
|
||||
|
||||
|
||||
// Check if resource is locked
|
||||
if app.lock_manager.is_locked(ctx.req.url) {
|
||||
// If the resource is locked, we should still return properties
|
||||
// but we might need to indicate the lock status in the response
|
||||
// This is handled in the property generation
|
||||
log.info('[WebDAV] Resource is locked: ${ctx.req.url}')
|
||||
}
|
||||
doc := xml.XMLDocument{
|
||||
root: xml.XMLNode{
|
||||
name: 'D:multistatus'
|
||||
children: responses
|
||||
attributes: {
|
||||
'xmlns:D': 'DAV:'
|
||||
}
|
||||
}
|
||||
|
||||
entry := app.vfs.get(path) or {return ctx.server_error('entry not found ${err}')}
|
||||
|
||||
responses := app.get_responses(entry, propfind_req) or {
|
||||
return ctx.server_error('Failed to get entry properties ${err}')
|
||||
}
|
||||
res := '<?xml version="1.0" encoding="UTF-8"?>${doc.pretty_str('').split('\n')[1..].join('')}'
|
||||
println('debugzo ${responses}')
|
||||
|
||||
// Create multistatus response using the responses
|
||||
ctx.res.set_status(.multi_status)
|
||||
return ctx.send_response_to_client('application/xml', res)
|
||||
// return veb.not_found()
|
||||
return ctx.send_response_to_client('application/xml', responses.xml())
|
||||
}
|
||||
|
||||
@['/:path...'; put]
|
||||
@@ -454,12 +315,15 @@ fn (mut app App) create_or_update(mut ctx Context, path string) veb.Result {
|
||||
} 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}')
|
||||
// return ctx.ok('HTTP 200: Successfully saved file: ${path}')
|
||||
} else {
|
||||
data := ctx.req.data.bytes()
|
||||
app.vfs.file_create(path) or { return ctx.server_error(err.msg()) }
|
||||
}
|
||||
if ctx.req.data.len > 0 {
|
||||
data := ctx.req.data.bytes()
|
||||
app.vfs.file_write(path, data) or { return ctx.server_error(err.msg()) }
|
||||
return ctx.ok('HTTP 200: Successfully wrote file: ${path}')
|
||||
}
|
||||
return ctx.ok('HTTP 200: Successfully created file: ${path}')
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ fn (app &App) auth_middleware(mut ctx Context) bool {
|
||||
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"')
|
||||
@@ -24,7 +23,6 @@ fn (app &App) auth_middleware(mut ctx Context) bool {
|
||||
ctx.send_response_to_client('text', 'unauthorized')
|
||||
return false
|
||||
}
|
||||
|
||||
auth_decoded := base64.decode_str(auth_header[6..])
|
||||
split_credentials := auth_decoded.split(':')
|
||||
if split_credentials.len != 2 {
|
||||
@@ -38,12 +36,14 @@ fn (app &App) auth_middleware(mut ctx Context) bool {
|
||||
if user := app.user_db[username] {
|
||||
if user != hashed_pass {
|
||||
ctx.res.set_status(.unauthorized)
|
||||
ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"')
|
||||
ctx.send_response_to_client('text', 'unauthorized')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
ctx.res.set_status(.unauthorized)
|
||||
ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"')
|
||||
ctx.send_response_to_client('text', 'unauthorized')
|
||||
return false
|
||||
}
|
||||
|
||||
11
lib/dav/webdav/middleware_headers.v
Normal file
11
lib/dav/webdav/middleware_headers.v
Normal file
@@ -0,0 +1,11 @@
|
||||
module webdav
|
||||
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import log
|
||||
|
||||
// fn add_dav_headers(mut ctx Context) bool {
|
||||
// ctx.set_custom_header('dav', '1,2') or { return ctx.server_error(err.msg()) }
|
||||
// ctx.set_header(.allow, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
|
||||
// ctx.set_custom_header('MS-Author-Via', 'DAV') or { return ctx.server_error(err.msg()) }
|
||||
// return true
|
||||
// }
|
||||
@@ -4,14 +4,12 @@ import freeflowuniverse.herolib.ui.console
|
||||
import log
|
||||
|
||||
fn middleware_log_request(mut ctx Context) bool {
|
||||
log.debug('[WebDAV] New Request: Method: ${ctx.req.method.str()} Path: ${ctx.req.url}')
|
||||
log.debug('[WebDAV] New Request: Method: ${ctx.req.method.str()} Path: ${ctx.req.url}')
|
||||
log.debug('[WebDAV] Request: ${ctx.req.method.str()} ${ctx.req.url}')
|
||||
return true
|
||||
}
|
||||
|
||||
fn middleware_log_response(mut ctx Context) bool {
|
||||
log.debug('[WebDAV] Response: Method: ${ctx.req.method.str()} Path: ${ctx.req.url}')
|
||||
log.debug('[WebDAV] Response: ${ctx.res}')
|
||||
log.debug('[WebDAV] Response: ${ctx.req.url} ${ctx.res.status()}')
|
||||
return true
|
||||
}
|
||||
|
||||
122
lib/dav/webdav/model_lock.v
Normal file
122
lib/dav/webdav/model_lock.v
Normal file
@@ -0,0 +1,122 @@
|
||||
module webdav
|
||||
|
||||
import encoding.xml
|
||||
import time
|
||||
|
||||
pub struct Lock {
|
||||
pub mut:
|
||||
resource string
|
||||
owner string
|
||||
token string
|
||||
depth int // 0 for a single resource, 1 for recursive
|
||||
timeout int // in seconds
|
||||
created_at time.Time
|
||||
lock_type string // typically 'write'
|
||||
scope string // 'exclusive' or 'shared'
|
||||
}
|
||||
|
||||
fn (l Lock) xml() string {
|
||||
return $tmpl('./templates/lock_response.xml')
|
||||
}
|
||||
|
||||
// parse_lock_xml parses the XML data from a WebDAV LOCK request
|
||||
// and extracts the lock parameters (scope, type, owner)
|
||||
fn parse_lock_xml(xml_data string) !Lock {
|
||||
mut lock_info := Lock{
|
||||
scope: 'exclusive' // default values
|
||||
lock_type: 'write'
|
||||
owner: ''
|
||||
}
|
||||
|
||||
// Parse the XML document
|
||||
doc := xml.XMLDocument.from_string(xml_data) or {
|
||||
return error('Failed to parse XML: ${err}')
|
||||
}
|
||||
|
||||
// Get the root element (lockinfo)
|
||||
root := doc.root
|
||||
|
||||
// Handle namespace prefixes (D:) in element names
|
||||
// WebDAV uses namespaces, so we need to check for both prefixed and non-prefixed names
|
||||
|
||||
// Extract lockscope
|
||||
for child in root.children {
|
||||
if child is xml.XMLNode {
|
||||
node := child as xml.XMLNode
|
||||
|
||||
// Check for lockscope (with or without namespace prefix)
|
||||
if node.name == 'lockscope' || node.name == 'D:lockscope' {
|
||||
for scope_child in node.children {
|
||||
if scope_child is xml.XMLNode {
|
||||
scope_node := scope_child as xml.XMLNode
|
||||
if scope_node.name == 'exclusive' || scope_node.name == 'D:exclusive' {
|
||||
lock_info.scope = 'exclusive'
|
||||
} else if scope_node.name == 'shared' || scope_node.name == 'D:shared' {
|
||||
lock_info.scope = 'shared'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for locktype (with or without namespace prefix)
|
||||
if node.name == 'locktype' || node.name == 'D:locktype' {
|
||||
for type_child in node.children {
|
||||
if type_child is xml.XMLNode {
|
||||
type_node := type_child as xml.XMLNode
|
||||
if type_node.name == 'write' || type_node.name == 'D:write' {
|
||||
lock_info.lock_type = 'write'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for owner (with or without namespace prefix)
|
||||
if node.name == 'owner' || node.name == 'D:owner' {
|
||||
for owner_child in node.children {
|
||||
if owner_child is xml.XMLNode {
|
||||
owner_node := owner_child as xml.XMLNode
|
||||
if owner_node.name == 'href' || owner_node.name == 'D:href' {
|
||||
for href_content in owner_node.children {
|
||||
if href_content is string {
|
||||
lock_info.owner = (href_content as string).trim_space()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if owner_child is string {
|
||||
// Some clients might include owner text directly
|
||||
lock_info.owner = (owner_child as string).trim_space()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If owner is still empty, try to extract it from any text content in the owner node
|
||||
if lock_info.owner.len == 0 {
|
||||
for child in root.children {
|
||||
if child is xml.XMLNode {
|
||||
node := child as xml.XMLNode
|
||||
if node.name == 'owner' || node.name == 'D:owner' {
|
||||
for content in node.children {
|
||||
if content is string {
|
||||
lock_info.owner = (content as string).trim_space()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use a default owner if none was found
|
||||
if lock_info.owner.len == 0 {
|
||||
lock_info.owner = 'unknown-client'
|
||||
}
|
||||
|
||||
// Debug output
|
||||
// println('Parsed lock info: scope=${lock_info.scope}, type=${lock_info.lock_type}, owner=${lock_info.owner}')
|
||||
|
||||
return lock_info
|
||||
}
|
||||
|
||||
112
lib/dav/webdav/model_propfind.v
Normal file
112
lib/dav/webdav/model_propfind.v
Normal file
@@ -0,0 +1,112 @@
|
||||
module webdav
|
||||
|
||||
import encoding.xml
|
||||
import log
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.vfs
|
||||
import os
|
||||
import time
|
||||
import veb
|
||||
|
||||
// Property represents a WebDAV property
|
||||
pub interface Property {
|
||||
xml() string
|
||||
xml_name() string
|
||||
}
|
||||
|
||||
type DisplayName = string
|
||||
type GetLastModified = string
|
||||
type GetContentType = string
|
||||
type GetContentLength = string
|
||||
type ResourceType = bool
|
||||
type CreationDate = string
|
||||
type SupportedLock = string
|
||||
type LockDiscovery = string
|
||||
|
||||
fn (p []Property) xml() string {
|
||||
return '<D:propstat>
|
||||
<D:prop>${p.map(it.xml()).join_lines()}</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>'
|
||||
}
|
||||
|
||||
fn (p DisplayName) xml() string {
|
||||
return '<D:displayname>${p}</D:displayname>'
|
||||
}
|
||||
|
||||
fn (p DisplayName) xml_name() string {
|
||||
return '<displayname/>'
|
||||
}
|
||||
|
||||
fn (p GetLastModified) xml() string {
|
||||
return '<D:getlastmodified>${p}</D:getlastmodified>'
|
||||
}
|
||||
|
||||
fn (p GetLastModified) xml_name() string {
|
||||
return '<getlastmodified/>'
|
||||
}
|
||||
|
||||
fn (p GetContentType) xml() string {
|
||||
return '<D:getcontenttype>${p}</D:getcontenttype>'
|
||||
}
|
||||
|
||||
fn (p GetContentType) xml_name() string {
|
||||
return '<getcontenttype/>'
|
||||
}
|
||||
|
||||
fn (p GetContentLength) xml() string {
|
||||
return '<D:getcontentlength>${p}</D:getcontentlength>'
|
||||
}
|
||||
|
||||
fn (p GetContentLength) xml_name() string {
|
||||
return '<getcontentlength/>'
|
||||
}
|
||||
|
||||
fn (p ResourceType) xml() string {
|
||||
return if p {
|
||||
'<D:resourcetype><D:collection/></D:resourcetype>'
|
||||
} else {
|
||||
'<D:resourcetype/>'
|
||||
}
|
||||
}
|
||||
|
||||
fn (p ResourceType) xml_name() string {
|
||||
return '<resourcetype/>'
|
||||
}
|
||||
|
||||
fn (p CreationDate) xml() string {
|
||||
return '<D:creationdate>${p}</D:creationdate>'
|
||||
}
|
||||
|
||||
fn (p CreationDate) xml_name() string {
|
||||
return '<creationdate/>'
|
||||
}
|
||||
|
||||
fn (p SupportedLock) xml() string {
|
||||
return '<D:supportedlock>
|
||||
<D:lockentry>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockentry>
|
||||
<D:lockentry>
|
||||
<D:lockscope><D:shared/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockentry>
|
||||
</D:supportedlock>'
|
||||
}
|
||||
|
||||
fn (p SupportedLock) xml_name() string {
|
||||
return '<supportedlock/>'
|
||||
}
|
||||
|
||||
fn (p LockDiscovery) xml() string {
|
||||
return '<D:lockdiscovery>${p}</D:lockdiscovery>'
|
||||
}
|
||||
|
||||
fn (p LockDiscovery) xml_name() string {
|
||||
return '<lockdiscovery/>'
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
module webdav
|
||||
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.vfs
|
||||
import encoding.xml
|
||||
import os
|
||||
import time
|
||||
import veb
|
||||
|
||||
fn generate_response_element(entry vfs.FSEntry) !xml.XMLNode {
|
||||
path := if entry.is_dir() && entry.get_path() != '/' {
|
||||
'${entry.get_path()}/'
|
||||
} else {
|
||||
entry.get_path()
|
||||
}
|
||||
|
||||
return xml.XMLNode{
|
||||
name: 'D:response'
|
||||
children: [
|
||||
xml.XMLNode{
|
||||
name: 'D:href'
|
||||
children: [path]
|
||||
},
|
||||
generate_propstat_element(entry)!,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const xml_ok_status = xml.XMLNode{
|
||||
name: 'D:status'
|
||||
children: ['HTTP/1.1 200 OK']
|
||||
}
|
||||
|
||||
const xml_500_status = xml.XMLNode{
|
||||
name: 'D:status'
|
||||
children: ['HTTP/1.1 500 Internal Server Error']
|
||||
}
|
||||
|
||||
fn generate_propstat_element(entry vfs.FSEntry) !xml.XMLNode {
|
||||
prop := generate_prop_element(entry) or {
|
||||
// TODO: status should be according to returned error
|
||||
return xml.XMLNode{
|
||||
name: 'D:propstat'
|
||||
children: [xml_500_status]
|
||||
}
|
||||
}
|
||||
|
||||
return xml.XMLNode{
|
||||
name: 'D:propstat'
|
||||
children: [prop, xml_ok_status]
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_prop_element(entry vfs.FSEntry) !xml.XMLNode {
|
||||
metadata := entry.get_metadata()
|
||||
|
||||
display_name := xml.XMLNode{
|
||||
name: 'D:displayname'
|
||||
children: ['${metadata.name}']
|
||||
}
|
||||
|
||||
content_length := if entry.is_dir() { 0 } else { metadata.size }
|
||||
get_content_length := xml.XMLNode{
|
||||
name: 'D:getcontentlength'
|
||||
children: ['${content_length}']
|
||||
}
|
||||
|
||||
creation_date := xml.XMLNode{
|
||||
name: 'D:creationdate'
|
||||
children: ['${format_iso8601(metadata.created_time())}']
|
||||
}
|
||||
|
||||
get_last_mod := xml.XMLNode{
|
||||
name: 'D:getlastmodified'
|
||||
children: ['${format_iso8601(metadata.modified_time())}']
|
||||
}
|
||||
|
||||
content_type := match entry.is_dir() {
|
||||
true {
|
||||
'httpd/unix-directory'
|
||||
}
|
||||
false {
|
||||
get_file_content_type(entry.get_path())
|
||||
}
|
||||
}
|
||||
|
||||
get_content_type := xml.XMLNode{
|
||||
name: 'D:getcontenttype'
|
||||
children: ['${content_type}']
|
||||
}
|
||||
|
||||
mut get_resource_type_children := []xml.XMLNodeContents{}
|
||||
|
||||
if entry.is_dir() {
|
||||
get_resource_type_children << xml.XMLNode{
|
||||
name: 'D:collection xmlns:D="DAV:"'
|
||||
}
|
||||
}
|
||||
|
||||
get_resource_type := xml.XMLNode{
|
||||
name: 'D:resourcetype'
|
||||
children: get_resource_type_children
|
||||
}
|
||||
|
||||
mut nodes := []xml.XMLNodeContents{}
|
||||
nodes << display_name
|
||||
nodes << get_last_mod
|
||||
nodes << get_content_type
|
||||
nodes << get_resource_type
|
||||
if !entry.is_dir() {
|
||||
nodes << get_content_length
|
||||
}
|
||||
nodes << creation_date
|
||||
|
||||
mut res := xml.XMLNode{
|
||||
name: 'D:prop'
|
||||
children: nodes.clone()
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
fn get_file_content_type(path string) string {
|
||||
ext := path.all_after_last('.')
|
||||
content_type := if v := veb.mime_types[ext] {
|
||||
v
|
||||
} else {
|
||||
'text/plain; charset=utf-8'
|
||||
}
|
||||
|
||||
return content_type
|
||||
}
|
||||
|
||||
fn format_iso8601(t time.Time) string {
|
||||
return '${t.year:04d}-${t.month:02d}-${t.day:02d}T${t.hour:02d}:${t.minute:02d}:${t.second:02d}Z'
|
||||
}
|
||||
|
||||
fn (mut app App) get_responses(path string, depth int) ![]xml.XMLNodeContents {
|
||||
if depth == 0 {
|
||||
entry := app.vfs.get(path)!
|
||||
return [generate_response_element(entry)!]
|
||||
}
|
||||
|
||||
mut responses := []xml.XMLNodeContents{}
|
||||
entries := app.vfs.dir_list(path) or { return responses }
|
||||
for e in entries {
|
||||
responses << generate_response_element(e)!
|
||||
}
|
||||
return responses
|
||||
}
|
||||
8
lib/dav/webdav/templates/delete_response.xml
Normal file
8
lib/dav/webdav/templates/delete_response.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>@{ctx.req.url}</d:href>
|
||||
<d:status>HTTP/1.1 423 Locked</d:status>
|
||||
<d:error><d:lock-token-submitted/></d:error>
|
||||
</d:response>
|
||||
</d:multistatus>
|
||||
8
lib/dav/webdav/templates/error_response.xml
Normal file
8
lib/dav/webdav/templates/error_response.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<d:multistatus xmlns:d="DAV:">
|
||||
<d:response>
|
||||
<d:href>@{ctx.req.url}</d:href>
|
||||
<d:status>HTTP/1.1 @{err.status.int()} @{message}</d:status>
|
||||
<d:error><d:@{err.tag}/></d:error>
|
||||
</d:response>
|
||||
</d:multistatus>
|
||||
20
lib/dav/webdav/templates/lock_response.xml
Normal file
20
lib/dav/webdav/templates/lock_response.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:prop xmlns:D="DAV:">
|
||||
<D:lockdiscovery xmlns:D="DAV:">
|
||||
<D:activelock>
|
||||
<D:locktype><D:@{l.lock_type}/></D:locktype>
|
||||
<D:lockscope><D:@{l.scope}/></D:lockscope>
|
||||
<D:depth>@{l.depth}</D:depth>
|
||||
<D:owner>
|
||||
<D:href>@{l.owner}</D:href>
|
||||
</D:owner>
|
||||
<D:timeout>Second-@{l.timeout}</D:timeout>
|
||||
<D:locktoken>
|
||||
<D:href>@{l.token}</D:href>
|
||||
</D:locktoken>
|
||||
<D:lockroot>
|
||||
<D:href>@{l.resource}</D:href>
|
||||
</D:lockroot>
|
||||
</D:activelock>
|
||||
</D:lockdiscovery>
|
||||
</D:prop>
|
||||
@@ -7,6 +7,7 @@ pub struct Metadata {
|
||||
pub mut:
|
||||
id u32 @[required] // unique identifier used as key in DB
|
||||
name string @[required] // name of file or directory
|
||||
path string @[required] // path of file or directory
|
||||
file_type FileType
|
||||
size u64
|
||||
created_at i64 // unix epoch timestamp
|
||||
|
||||
@@ -7,6 +7,7 @@ import freeflowuniverse.herolib.vfs
|
||||
fn encode_metadata(mut e encoder.Encoder, m vfs.Metadata) {
|
||||
e.add_u32(m.id)
|
||||
e.add_string(m.name)
|
||||
e.add_string(m.path)
|
||||
e.add_u8(u8(m.file_type)) // FileType enum as u8
|
||||
e.add_u64(m.size)
|
||||
e.add_i64(m.created_at)
|
||||
@@ -21,6 +22,7 @@ fn encode_metadata(mut e encoder.Encoder, m vfs.Metadata) {
|
||||
fn decode_metadata(mut d encoder.Decoder) !vfs.Metadata {
|
||||
id := d.get_u32()!
|
||||
name := d.get_string()!
|
||||
path := d.get_string()!
|
||||
file_type_byte := d.get_u8()!
|
||||
size := d.get_u64()!
|
||||
created_at := d.get_i64()!
|
||||
@@ -33,6 +35,7 @@ fn decode_metadata(mut d encoder.Decoder) !vfs.Metadata {
|
||||
return vfs.Metadata{
|
||||
id: id
|
||||
name: name
|
||||
path: path
|
||||
file_type: unsafe { vfs.FileType(file_type_byte) }
|
||||
size: size
|
||||
created_at: created_at
|
||||
|
||||
@@ -7,6 +7,7 @@ import freeflowuniverse.herolib.vfs
|
||||
pub struct NewMetadata {
|
||||
pub mut:
|
||||
name string @[required] // name of file or directory
|
||||
path string @[required] // name of file or directory
|
||||
file_type vfs.FileType @[required]
|
||||
size u64 @[required]
|
||||
mode u32 = 0o644 // file permissions
|
||||
@@ -18,6 +19,7 @@ pub fn (mut fs DatabaseVFS) new_metadata(metadata NewMetadata) vfs.Metadata {
|
||||
return vfs.new_metadata(
|
||||
id: fs.get_next_id()
|
||||
name: metadata.name
|
||||
path: metadata.path
|
||||
file_type: metadata.file_type
|
||||
size: metadata.size
|
||||
mode: metadata.mode
|
||||
|
||||
@@ -15,7 +15,7 @@ fn (d &Directory) get_metadata() vfs.Metadata {
|
||||
}
|
||||
|
||||
fn (d &Directory) get_path() string {
|
||||
return d.metadata.name
|
||||
return '/${d.metadata.name.trim_string_left('/')}'
|
||||
}
|
||||
|
||||
// is_dir returns true if the entry is a directory
|
||||
|
||||
@@ -32,7 +32,7 @@ fn (f &File) get_metadata() vfs.Metadata {
|
||||
}
|
||||
|
||||
fn (f &File) get_path() string {
|
||||
return f.metadata.name
|
||||
return '/${f.metadata.name.trim_string_left('/')}'
|
||||
}
|
||||
|
||||
// is_dir returns true if the entry is a directory
|
||||
@@ -53,6 +53,7 @@ pub fn (f &File) is_symlink() bool {
|
||||
pub struct NewFile {
|
||||
pub:
|
||||
name string @[required] // name of file or directory
|
||||
path string @[required] // path of file or directory
|
||||
data string
|
||||
mode u32 = 0o644 // file permissions
|
||||
owner string = 'user'
|
||||
@@ -67,6 +68,7 @@ pub fn (mut fs DatabaseVFS) new_file(file NewFile) !&File {
|
||||
parent_id: file.parent_id
|
||||
metadata: fs.new_metadata(NewMetadata{
|
||||
name: file.name
|
||||
path: file.path
|
||||
mode: file.mode
|
||||
owner: file.owner
|
||||
group: file.group
|
||||
@@ -85,6 +87,7 @@ pub fn (mut fs DatabaseVFS) copy_file(file File) !&File {
|
||||
return fs.new_file(
|
||||
data: file.data
|
||||
name: file.metadata.name
|
||||
path: file.metadata.path
|
||||
mode: file.metadata.mode
|
||||
owner: file.metadata.owner
|
||||
group: file.metadata.group
|
||||
|
||||
@@ -10,7 +10,7 @@ fn (e &FSEntry) get_metadata() vfs.Metadata {
|
||||
}
|
||||
|
||||
fn (e &FSEntry) get_path() string {
|
||||
return e.metadata.name
|
||||
return e.metadata.path
|
||||
}
|
||||
|
||||
fn (e &FSEntry) is_dir() bool {
|
||||
|
||||
@@ -4,7 +4,8 @@ import freeflowuniverse.herolib.vfs { Metadata }
|
||||
import time
|
||||
|
||||
// mkdir creates a new directory with default permissions
|
||||
pub fn (mut fs DatabaseVFS) directory_mkdir(mut dir Directory, name string) !&Directory {
|
||||
pub fn (mut fs DatabaseVFS) directory_mkdir(mut dir Directory, name_ string) !&Directory {
|
||||
name := name_.trim('/')
|
||||
// Check if directory already exists
|
||||
for child_id in dir.children {
|
||||
if entry := fs.load_entry(child_id) {
|
||||
@@ -14,7 +15,18 @@ pub fn (mut fs DatabaseVFS) directory_mkdir(mut dir Directory, name string) !&Di
|
||||
}
|
||||
}
|
||||
|
||||
new_dir := fs.new_directory(name: name, parent_id: dir.metadata.id)!
|
||||
path := if dir.metadata.path == '/' {
|
||||
'/${name}'
|
||||
} else {
|
||||
"${dir.metadata.path.trim('/')}/${name}"
|
||||
}
|
||||
|
||||
new_dir := fs.new_directory(
|
||||
name: name,
|
||||
path: path
|
||||
parent_id: dir.metadata.id
|
||||
)!
|
||||
println('new_dit ${new_dir}')
|
||||
dir.children << new_dir.metadata.id
|
||||
fs.save_entry(dir)!
|
||||
return new_dir
|
||||
@@ -23,6 +35,7 @@ pub fn (mut fs DatabaseVFS) directory_mkdir(mut dir Directory, name string) !&Di
|
||||
pub struct NewDirectory {
|
||||
pub:
|
||||
name string @[required] // name of file or directory
|
||||
path string @[required] // name of file or directory
|
||||
mode u32 = 0o755 // file permissions
|
||||
owner string = 'user'
|
||||
group string = 'user'
|
||||
@@ -36,6 +49,7 @@ pub fn (mut fs DatabaseVFS) new_directory(dir NewDirectory) !&Directory {
|
||||
parent_id: dir.parent_id
|
||||
metadata: fs.new_metadata(NewMetadata{
|
||||
name: dir.name
|
||||
path: dir.path
|
||||
mode: dir.mode
|
||||
owner: dir.owner
|
||||
group: dir.group
|
||||
@@ -75,7 +89,8 @@ pub fn (mut fs DatabaseVFS) copy_directory(dir Directory) !&Directory {
|
||||
}
|
||||
|
||||
// touch creates a new empty file with default permissions
|
||||
pub fn (mut fs DatabaseVFS) directory_touch(dir_ Directory, name string) !&File {
|
||||
pub fn (mut fs DatabaseVFS) directory_touch(dir_ Directory, name_ string) !&File {
|
||||
name := name_.trim('/')
|
||||
mut dir := dir_
|
||||
|
||||
// First, make sure we're working with the latest version of the directory
|
||||
@@ -98,6 +113,7 @@ pub fn (mut fs DatabaseVFS) directory_touch(dir_ Directory, name string) !&File
|
||||
mut new_file := fs.new_file(
|
||||
parent_id: dir.metadata.id
|
||||
name: name
|
||||
path: "${dir.metadata.path.trim('/')}/${name}"
|
||||
)!
|
||||
|
||||
// Ensure parent_id is set correctly
|
||||
@@ -199,44 +215,60 @@ pub fn (mut fs DatabaseVFS) directory_move(dir_ Directory, args_ MoveDirArgs) !&
|
||||
for child_id in dir.children {
|
||||
if mut entry := fs.load_entry(child_id) {
|
||||
if entry.metadata.name == args.src_entry_name {
|
||||
if entry is File {
|
||||
return error('${args.src_entry_name} is a file')
|
||||
}
|
||||
|
||||
if entry is Symlink {
|
||||
return error('${args.src_entry_name} is a symlink')
|
||||
}
|
||||
|
||||
found = true
|
||||
child_id_to_remove = child_id
|
||||
mut entry_ := entry as Directory
|
||||
entry_.metadata.name = args.dst_entry_name
|
||||
entry_.metadata.modified_at = time.now().unix()
|
||||
entry_.parent_id = args.dst_parent_dir.metadata.id
|
||||
|
||||
// Recursively update all child paths in moved directory
|
||||
fs.move_children_recursive(mut entry_)!
|
||||
// Handle both files and directories
|
||||
if entry is File {
|
||||
mut file_entry := entry as File
|
||||
file_entry.metadata.name = args.dst_entry_name
|
||||
file_entry.metadata.path = "/${args.dst_parent_dir.metadata.path.trim('/')}/${args.dst_entry_name}"
|
||||
file_entry.metadata.modified_at = time.now().unix()
|
||||
file_entry.parent_id = args.dst_parent_dir.metadata.id
|
||||
|
||||
// Ensure no duplicate entries in dst_parent_dir
|
||||
if entry_.metadata.id !in args.dst_parent_dir.children {
|
||||
args.dst_parent_dir.children << entry_.metadata.id
|
||||
}
|
||||
|
||||
// Remove from old parent's children before saving the entry
|
||||
dir.children = dir.children.filter(it != child_id_to_remove)
|
||||
fs.save_entry(dir)!
|
||||
|
||||
fs.save_entry(entry_)!
|
||||
fs.save_entry(args.dst_parent_dir)!
|
||||
|
||||
// Reload the source directory to ensure we have the latest version
|
||||
if updated_src_dir := fs.load_entry(dir.metadata.id) {
|
||||
if updated_src_dir is Directory {
|
||||
dir = updated_src_dir
|
||||
// Ensure no duplicate entries in dst_parent_dir
|
||||
if file_entry.metadata.id !in args.dst_parent_dir.children {
|
||||
args.dst_parent_dir.children << file_entry.metadata.id
|
||||
}
|
||||
}
|
||||
|
||||
return &entry_
|
||||
// Remove from old parent's children before saving the entry
|
||||
dir.children = dir.children.filter(it != child_id_to_remove)
|
||||
fs.save_entry(dir)!
|
||||
|
||||
fs.save_entry(file_entry)!
|
||||
fs.save_entry(args.dst_parent_dir)!
|
||||
|
||||
// Return the destination directory
|
||||
return args.dst_parent_dir
|
||||
} else {
|
||||
// Handle directory
|
||||
mut dir_entry := entry as Directory
|
||||
dir_entry.metadata.name = args.dst_entry_name
|
||||
dir_entry.metadata.path = "${args.dst_parent_dir.metadata.path.trim_string_right('/')}/${args.dst_entry_name}"
|
||||
dir_entry.metadata.modified_at = time.now().unix()
|
||||
dir_entry.parent_id = args.dst_parent_dir.metadata.id
|
||||
|
||||
// Recursively update all child paths in moved directory
|
||||
fs.move_children_recursive(mut dir_entry)!
|
||||
|
||||
// Ensure no duplicate entries in dst_parent_dir
|
||||
if dir_entry.metadata.id !in args.dst_parent_dir.children {
|
||||
args.dst_parent_dir.children << dir_entry.metadata.id
|
||||
}
|
||||
|
||||
// Remove from old parent's children before saving the entry
|
||||
dir.children = dir.children.filter(it != child_id_to_remove)
|
||||
fs.save_entry(dir)!
|
||||
|
||||
fs.save_entry(dir_entry)!
|
||||
fs.save_entry(args.dst_parent_dir)!
|
||||
|
||||
return &dir_entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,6 +450,7 @@ pub fn (mut fs DatabaseVFS) directory_rename(dir Directory, src_name string, dst
|
||||
found = true
|
||||
mut dir_entry := entry as Directory
|
||||
dir_entry.metadata.name = dst_name
|
||||
dir_entry.metadata.path = "${dir_entry.metadata.path.all_before_last('/')}/dst_name"
|
||||
dir_entry.metadata.modified_at = time.now().unix()
|
||||
fs.save_entry(dir_entry)!
|
||||
return &dir_entry
|
||||
|
||||
@@ -22,6 +22,7 @@ pub fn (mut fs DatabaseVFS) root_get_as_dir() !&Directory {
|
||||
id: fs.get_next_id()
|
||||
file_type: .directory
|
||||
name: ''
|
||||
path: '/'
|
||||
created_at: time.now().unix()
|
||||
modified_at: time.now().unix()
|
||||
accessed_at: time.now().unix()
|
||||
@@ -35,42 +36,34 @@ pub fn (mut fs DatabaseVFS) root_get_as_dir() !&Directory {
|
||||
return &myroot
|
||||
}
|
||||
|
||||
fn (mut self DatabaseVFS) get_entry(path string) !FSEntry {
|
||||
fn (mut self DatabaseVFS) get_entry(path_ string) !FSEntry {
|
||||
path := '/${path_.trim_left('/').trim_right('/')}'
|
||||
if path == '/' || path == '' || path == '.' {
|
||||
return FSEntry(self.root_get_as_dir()!)
|
||||
}
|
||||
|
||||
mut current := *self.root_get_as_dir()!
|
||||
parts := path.trim_left('/').split('/')
|
||||
return self.directory_get_entry(mut current, path) or {
|
||||
return error('Path not found: ${path}')
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < parts.len; i++ {
|
||||
mut found := false
|
||||
children := self.directory_children(mut current, false)!
|
||||
for child in children {
|
||||
if child.metadata.name == parts[i] {
|
||||
match child {
|
||||
Directory {
|
||||
current = child
|
||||
found = true
|
||||
break
|
||||
}
|
||||
else {
|
||||
if i == parts.len - 1 {
|
||||
return child
|
||||
} else {
|
||||
return error('Not a directory: ${parts[i]}')
|
||||
}
|
||||
}
|
||||
}
|
||||
fn (mut self DatabaseVFS) directory_get_entry(mut dir Directory, path string) ?FSEntry {
|
||||
mut children := self.directory_children(mut dir, false) or {
|
||||
panic('this should never happen')
|
||||
}
|
||||
for mut child in children {
|
||||
println('debugzoni1 ${child.metadata.path} ${path}')
|
||||
if child.metadata.path == path {
|
||||
return child
|
||||
} else if child is Directory {
|
||||
mut child_dir := child as Directory
|
||||
return self.directory_get_entry(mut child_dir, path) or {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return error('Path not found: ${path}')
|
||||
}
|
||||
}
|
||||
|
||||
return FSEntry(current)
|
||||
return none
|
||||
}
|
||||
|
||||
fn (mut self DatabaseVFS) get_directory(path string) !&Directory {
|
||||
|
||||
@@ -11,7 +11,8 @@ pub fn (mut fs DatabaseVFS) root_get() !vfs.FSEntry {
|
||||
return fs.root_get_as_dir()!
|
||||
}
|
||||
|
||||
pub fn (mut self DatabaseVFS) file_create(path string) !vfs.FSEntry {
|
||||
pub fn (mut self DatabaseVFS) file_create(path_ string) !vfs.FSEntry {
|
||||
path := '/${path_.trim_left('/').trim_right('/')}'
|
||||
log.info('[DatabaseVFS] Creating file ${path}')
|
||||
// Get parent directory
|
||||
parent_path := os.dir(path)
|
||||
@@ -21,7 +22,8 @@ pub fn (mut self DatabaseVFS) file_create(path string) !vfs.FSEntry {
|
||||
return self.directory_touch(parent_dir, file_name)!
|
||||
}
|
||||
|
||||
pub fn (mut self DatabaseVFS) file_read(path string) ![]u8 {
|
||||
pub fn (mut self DatabaseVFS) file_read(path_ string) ![]u8 {
|
||||
path := '/${path_.trim_left('/').trim_right('/')}'
|
||||
log.info('[DatabaseVFS] Reading file ${path}')
|
||||
mut entry := self.get_entry(path)!
|
||||
if mut entry is File {
|
||||
@@ -31,16 +33,17 @@ pub fn (mut self DatabaseVFS) file_read(path string) ![]u8 {
|
||||
}
|
||||
|
||||
pub fn (mut self DatabaseVFS) file_write(path string, data []u8) ! {
|
||||
self.print()!
|
||||
if mut entry := self.get_entry(path) {
|
||||
println(entry)
|
||||
if mut entry is File {
|
||||
log.info('[DatabaseVFS] Writing file ${path}')
|
||||
log.info('[DatabaseVFS] Writing data ${data.bytestr()}')
|
||||
entry.write(data.bytestr())
|
||||
self.save_entry(entry)!
|
||||
} else {
|
||||
panic('handle error')
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
self.file_create(path)!
|
||||
self.file_write(path, data)!
|
||||
}
|
||||
@@ -54,13 +57,13 @@ pub fn (mut self DatabaseVFS) file_delete(path string) ! {
|
||||
self.directory_rm(mut parent_dir, file_name)!
|
||||
}
|
||||
|
||||
pub fn (mut self DatabaseVFS) dir_create(path string) !vfs.FSEntry {
|
||||
pub fn (mut self DatabaseVFS) dir_create(path_ string) !vfs.FSEntry {
|
||||
path := '/${path_.trim_left('/').trim_right('/')}'
|
||||
log.info('[DatabaseVFS] Creating Directory ${path}')
|
||||
parent_path := os.dir(path)
|
||||
dir_name := os.base(path)
|
||||
|
||||
file_name := os.base(path)
|
||||
mut parent_dir := self.get_directory(parent_path)!
|
||||
return self.directory_mkdir(mut parent_dir, dir_name)!
|
||||
return self.directory_mkdir(mut parent_dir, file_name)!
|
||||
}
|
||||
|
||||
pub fn (mut self DatabaseVFS) dir_list(path string) ![]vfs.FSEntry {
|
||||
@@ -97,6 +100,7 @@ pub fn (mut self DatabaseVFS) link_create(target_path string, link_path string)
|
||||
metadata: vfs.Metadata{
|
||||
id: self.get_next_id()
|
||||
name: link_name
|
||||
path: link_path
|
||||
file_type: .symlink
|
||||
created_at: time.now().unix()
|
||||
modified_at: time.now().unix()
|
||||
@@ -130,6 +134,8 @@ pub fn (mut self DatabaseVFS) link_delete(path string) ! {
|
||||
}
|
||||
|
||||
pub fn (mut self DatabaseVFS) exists(path_ string) bool {
|
||||
println('debugznoiki')
|
||||
self.print() or {panic(err.msg())}
|
||||
path := if !path_.starts_with('/') {
|
||||
'/${path_}'
|
||||
} else {
|
||||
@@ -143,6 +149,7 @@ pub fn (mut self DatabaseVFS) exists(path_ string) bool {
|
||||
}
|
||||
|
||||
pub fn (mut fs DatabaseVFS) get(path string) !vfs.FSEntry {
|
||||
log.info('[DatabaseVFS] Getting filesystem entry ${path}')
|
||||
return fs.get_entry(path)!
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
module vfs_nested
|
||||
|
||||
import freeflowuniverse.herolib.vfs
|
||||
import time
|
||||
|
||||
// NestedVFS represents a VFS that can contain multiple nested VFS instances
|
||||
pub struct NestedVFS {
|
||||
@@ -33,7 +34,6 @@ fn (self &NestedVFS) find_vfs(path string) !(vfs.VFSImplementation, string) {
|
||||
mut prefixes := self.vfs_map.keys()
|
||||
prefixes.sort(a.len > b.len)
|
||||
|
||||
println('debugzone ${path} ${prefixes}')
|
||||
for prefix in prefixes {
|
||||
if path.starts_with(prefix) {
|
||||
relative_path := path[prefix.len..]
|
||||
@@ -50,6 +50,7 @@ pub fn (mut self NestedVFS) root_get() !vfs.FSEntry {
|
||||
metadata: vfs.Metadata{
|
||||
id: 0
|
||||
name: ''
|
||||
path: '/'
|
||||
file_type: .directory
|
||||
size: 0
|
||||
created_at: 0
|
||||
@@ -87,6 +88,13 @@ pub fn (mut self NestedVFS) file_create(path string) !vfs.FSEntry {
|
||||
|
||||
pub fn (mut self NestedVFS) file_read(path string) ![]u8 {
|
||||
println('debuzone- File read ${path}')
|
||||
|
||||
// Special handling for macOS resource fork files (._*)
|
||||
if path.starts_with('/._') || path.contains('/._') {
|
||||
// Return empty data for resource fork files
|
||||
return []u8{}
|
||||
}
|
||||
|
||||
mut impl, rel_path := self.find_vfs(path)!
|
||||
return impl.file_read(rel_path)
|
||||
}
|
||||
@@ -127,6 +135,7 @@ pub fn (mut self NestedVFS) dir_list(path string) ![]vfs.FSEntry {
|
||||
metadata: vfs.Metadata{
|
||||
id: 0
|
||||
name: prefix
|
||||
path: prefix
|
||||
file_type: .directory
|
||||
size: 0
|
||||
created_at: 0
|
||||
@@ -170,6 +179,12 @@ pub fn (mut self NestedVFS) exists(path string) bool {
|
||||
if path == '' || path == '/' {
|
||||
return true
|
||||
}
|
||||
|
||||
// // Special handling for macOS resource fork files (._*)
|
||||
// if path.starts_with('/._') || path.contains('/._') {
|
||||
// return true // Pretend these files exist for WebDAV Class 2 compatibility
|
||||
// }
|
||||
|
||||
mut impl, rel_path := self.find_vfs(path) or { return false }
|
||||
return impl.exists(rel_path)
|
||||
}
|
||||
@@ -178,6 +193,27 @@ pub fn (mut self NestedVFS) get(path string) !vfs.FSEntry {
|
||||
if path == '' || path == '/' {
|
||||
return self.root_get()
|
||||
}
|
||||
|
||||
// // Special handling for macOS resource fork files (._*)
|
||||
// if path.starts_with('/._') || path.contains('/._') {
|
||||
// // Extract the filename from the path
|
||||
// filename := path.all_after_last('/')
|
||||
|
||||
// // Create a dummy resource fork entry
|
||||
// return &ResourceForkEntry{
|
||||
// metadata: vfs.Metadata{
|
||||
// id: 0
|
||||
// name: filename
|
||||
// file_type: .file
|
||||
// size: 0
|
||||
// created_at: time.now().unix()
|
||||
// modified_at: time.now().unix()
|
||||
// accessed_at: time.now().unix()
|
||||
// }
|
||||
// path: path
|
||||
// }
|
||||
// }
|
||||
|
||||
mut impl, rel_path := self.find_vfs(path)!
|
||||
|
||||
// now must convert entry of nested fvs to entry of nester
|
||||
@@ -380,7 +416,7 @@ fn (e &NestedEntry) get_path() string {
|
||||
if original_path == '/' {
|
||||
return e.prefix
|
||||
}
|
||||
return e.prefix + original_path
|
||||
return e.prefix + '/${original_path.trim_string_left("/")}'
|
||||
}
|
||||
|
||||
// is_dir returns true if the entry is a directory
|
||||
@@ -397,3 +433,33 @@ pub fn (self &NestedEntry) is_file() bool {
|
||||
pub fn (self &NestedEntry) is_symlink() bool {
|
||||
return self.original.is_symlink()
|
||||
}
|
||||
|
||||
// // ResourceForkEntry represents a macOS resource fork file (._*)
|
||||
// pub struct ResourceForkEntry {
|
||||
// pub mut:
|
||||
// metadata vfs.Metadata
|
||||
// path string
|
||||
// }
|
||||
|
||||
// fn (e &ResourceForkEntry) get_metadata() vfs.Metadata {
|
||||
// return e.metadata
|
||||
// }
|
||||
|
||||
// fn (e &ResourceForkEntry) get_path() string {
|
||||
// return e.path
|
||||
// }
|
||||
|
||||
// // is_dir returns true if the entry is a directory
|
||||
// pub fn (self &ResourceForkEntry) is_dir() bool {
|
||||
// return false
|
||||
// }
|
||||
|
||||
// // is_file returns true if the entry is a file
|
||||
// pub fn (self &ResourceForkEntry) is_file() bool {
|
||||
// return true
|
||||
// }
|
||||
|
||||
// // is_symlink returns true if the entry is a symlink
|
||||
// pub fn (self &ResourceForkEntry) is_symlink() bool {
|
||||
// return false
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user