webdav fixes
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 {
|
||||
if !app.vfs.exists(path) {
|
||||
return ctx.not_found()
|
||||
}
|
||||
depth := ctx.req.header.get_custom('Depth') or { '0' }.int()
|
||||
log.info('[WebDAV] ${@FN} ${path}')
|
||||
|
||||
responses := app.get_responses(path, depth) or {
|
||||
console.print_stderr('failed to get responses: ${err}')
|
||||
return ctx.server_error(err.msg())
|
||||
// Check if resource exists
|
||||
if !app.vfs.exists(path) {
|
||||
return ctx.error(
|
||||
status: .not_found
|
||||
message: 'Path ${path} does not exist'
|
||||
tag: 'resource-must-be-null'
|
||||
)
|
||||
}
|
||||
doc := xml.XMLDocument{
|
||||
root: xml.XMLNode{
|
||||
name: 'D:multistatus'
|
||||
children: responses
|
||||
attributes: {
|
||||
'xmlns:D': 'DAV:'
|
||||
|
||||
// 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'
|
||||
})
|
||||
}
|
||||
|
||||
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}')
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user