...
This commit is contained in:
@@ -116,11 +116,11 @@ fn (p CustomProperty) xml_str() string {
|
||||
fn test_custom_property() {
|
||||
// Test custom property
|
||||
custom_prop := CustomProperty{
|
||||
name: 'author'
|
||||
value: 'Kristof'
|
||||
name: 'author'
|
||||
value: 'Kristof'
|
||||
namespace: 'C'
|
||||
}
|
||||
|
||||
|
||||
assert custom_prop.xml_str() == '<C:author>Kristof</C:author>'
|
||||
assert custom_prop.xml_name() == '<author/>'
|
||||
}
|
||||
@@ -131,16 +131,15 @@ fn test_propfind_response() {
|
||||
props << DisplayName('test-file.txt')
|
||||
props << GetLastModified('Mon, 01 Jan 2024 12:00:00 GMT')
|
||||
props << GetContentLength('1024')
|
||||
|
||||
|
||||
// Build a complete PROPFIND response with multistatus
|
||||
xml_output := '<D:multistatus xmlns:D="DAV:">
|
||||
<D:response>
|
||||
<D:href>/test-file.txt</D:href>
|
||||
${props.xml_str()}
|
||||
</D:response>
|
||||
</D:multistatus>'
|
||||
|
||||
// Verify the XML structure
|
||||
</D:multistatus>' // Verify the XML structure
|
||||
|
||||
assert xml_output.contains('<D:multistatus')
|
||||
assert xml_output.contains('<D:response>')
|
||||
assert xml_output.contains('<D:href>')
|
||||
@@ -157,7 +156,7 @@ fn test_propfind_with_missing_properties() {
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 404 Not Found</D:status>
|
||||
</D:propstat>'
|
||||
|
||||
|
||||
// Simple verification of structure
|
||||
assert missing_prop_response.contains('<D:propstat>')
|
||||
assert missing_prop_response.contains('<D:nonexistent-property/>')
|
||||
@@ -167,12 +166,12 @@ fn test_propfind_with_missing_properties() {
|
||||
fn test_supported_lock_detailed() {
|
||||
supported_lock := SupportedLock('')
|
||||
xml_output := supported_lock.xml_str()
|
||||
|
||||
|
||||
// Test SupportedLock provides a fully formed XML snippet for supportedlock
|
||||
// Note: This test assumes the actual implementation returns a simplified version
|
||||
// as indicated by the xml_str() method which returns '<D:supportedlock>...</D:supportedlock>'
|
||||
assert xml_output.contains('<D:supportedlock>')
|
||||
|
||||
|
||||
// Detailed testing would need proper parsing of the XML to verify elements
|
||||
// For real implementation, test should check for:
|
||||
// - lockentry elements
|
||||
@@ -183,11 +182,11 @@ fn test_supported_lock_detailed() {
|
||||
fn test_proppatch_request() {
|
||||
// Create property to set
|
||||
author_prop := CustomProperty{
|
||||
name: 'author'
|
||||
value: 'Kristof'
|
||||
name: 'author'
|
||||
value: 'Kristof'
|
||||
namespace: 'C'
|
||||
}
|
||||
|
||||
|
||||
// Create XML for PROPPATCH request (set)
|
||||
proppatch_set := '<D:propertyupdate xmlns:D="DAV:" xmlns:C="http://example.com/customns">
|
||||
<D:set>
|
||||
@@ -195,14 +194,13 @@ fn test_proppatch_request() {
|
||||
${author_prop.xml_str()}
|
||||
</D:prop>
|
||||
</D:set>
|
||||
</D:propertyupdate>'
|
||||
|
||||
// Check structure
|
||||
</D:propertyupdate>' // Check structure
|
||||
|
||||
assert proppatch_set.contains('<D:propertyupdate')
|
||||
assert proppatch_set.contains('<D:set>')
|
||||
assert proppatch_set.contains('<D:prop>')
|
||||
assert proppatch_set.contains('<C:author>Kristof</C:author>')
|
||||
|
||||
|
||||
// Create XML for PROPPATCH request (remove)
|
||||
proppatch_remove := '<D:propertyupdate xmlns:D="DAV:">
|
||||
<D:remove>
|
||||
@@ -211,7 +209,7 @@ fn test_proppatch_request() {
|
||||
</D:prop>
|
||||
</D:remove>
|
||||
</D:propertyupdate>'
|
||||
|
||||
|
||||
// Check structure
|
||||
assert proppatch_remove.contains('<D:propertyupdate')
|
||||
assert proppatch_remove.contains('<D:remove>')
|
||||
@@ -224,7 +222,7 @@ fn test_prop_name_listing() {
|
||||
mut props := []Property{}
|
||||
props << DisplayName('file.txt')
|
||||
props << GetContentType('text/plain')
|
||||
|
||||
|
||||
// Generate propname response
|
||||
// Note: In a complete implementation, there would be a function to generate this XML
|
||||
// For testing purposes, we're manually creating the expected structure
|
||||
@@ -240,7 +238,7 @@ fn test_prop_name_listing() {
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>'
|
||||
|
||||
|
||||
// Verify structure
|
||||
assert propname_response.contains('<D:multistatus')
|
||||
assert propname_response.contains('<D:prop>')
|
||||
@@ -262,7 +260,7 @@ fn test_namespace_declarations() {
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>'
|
||||
|
||||
|
||||
// Verify key namespace elements
|
||||
assert response_with_ns.contains('xmlns:D="DAV:"')
|
||||
assert response_with_ns.contains('xmlns:C="http://example.com/customns"')
|
||||
@@ -290,7 +288,7 @@ fn test_depth_header_responses() {
|
||||
</D:propstat>
|
||||
</D:response>
|
||||
</D:multistatus>'
|
||||
|
||||
|
||||
// Verify structure contains multiple responses
|
||||
assert multi_response.contains('<D:response>')
|
||||
assert multi_response.count('<D:response>') == 2
|
||||
|
||||
@@ -303,22 +303,22 @@ fn (mut server Server) create_or_update(mut ctx Context, path string) veb.Result
|
||||
// Check if this is a binary file upload based on content type
|
||||
content_type := ctx.req.header.get(.content_type) or { '' }
|
||||
is_binary := is_binary_content_type(content_type)
|
||||
|
||||
|
||||
// Handle binary uploads directly
|
||||
if is_binary {
|
||||
log.info('[WebDAV] Processing binary upload for ${path} (${content_type})')
|
||||
|
||||
|
||||
// Handle the binary upload directly
|
||||
ctx.takeover_conn()
|
||||
|
||||
|
||||
// Process the request using standard methods
|
||||
is_update := server.vfs.exists(path)
|
||||
|
||||
|
||||
// Return success response
|
||||
ctx.res.set_status(if is_update { .ok } else { .created })
|
||||
return veb.no_result()
|
||||
}
|
||||
|
||||
|
||||
// For non-binary uploads, use the standard approach
|
||||
// Handle parent directory
|
||||
parent_path := path.all_before_last('/')
|
||||
@@ -345,13 +345,13 @@ fn (mut server Server) create_or_update(mut ctx Context, path string) veb.Result
|
||||
ctx.res.set_status(.conflict)
|
||||
return ctx.text('HTTP 409: Conflict - Cannot replace directory with file')
|
||||
}
|
||||
|
||||
|
||||
// Create the file after deleting the directory
|
||||
server.vfs.file_create(path) or {
|
||||
log.error('[WebDAV] Failed to create file ${path} after deleting directory: ${err.msg()}')
|
||||
return ctx.server_error('Failed to create file: ${err.msg()}')
|
||||
}
|
||||
|
||||
|
||||
// Now it's not an update anymore
|
||||
is_update = false
|
||||
}
|
||||
@@ -602,22 +602,15 @@ fn (mut server Server) create_or_update(mut ctx Context, path string) veb.Result
|
||||
fn is_binary_content_type(content_type string) bool {
|
||||
// Normalize the content type by converting to lowercase
|
||||
normalized := content_type.to_lower()
|
||||
|
||||
|
||||
// Check for common binary file types
|
||||
return normalized.contains('application/octet-stream') ||
|
||||
(normalized.contains('application/') && (
|
||||
normalized.contains('msword') ||
|
||||
normalized.contains('excel') ||
|
||||
normalized.contains('powerpoint') ||
|
||||
normalized.contains('pdf') ||
|
||||
normalized.contains('zip') ||
|
||||
normalized.contains('gzip') ||
|
||||
normalized.contains('x-tar') ||
|
||||
normalized.contains('x-7z') ||
|
||||
normalized.contains('x-rar')
|
||||
)) ||
|
||||
(normalized.contains('image/') && !normalized.contains('svg')) ||
|
||||
normalized.contains('audio/') ||
|
||||
normalized.contains('video/') ||
|
||||
normalized.contains('vnd.openxmlformats') // Office documents
|
||||
return normalized.contains('application/octet-stream')
|
||||
|| (normalized.contains('application/') && (normalized.contains('msword')
|
||||
|| normalized.contains('excel') || normalized.contains('powerpoint')
|
||||
|| normalized.contains('pdf') || normalized.contains('zip')
|
||||
|| normalized.contains('gzip') || normalized.contains('x-tar')
|
||||
|| normalized.contains('x-7z') || normalized.contains('x-rar')))
|
||||
|| (normalized.contains('image/') && !normalized.contains('svg'))
|
||||
|| normalized.contains('audio/') || normalized.contains('video/')
|
||||
|| normalized.contains('vnd.openxmlformats') // Office documents
|
||||
}
|
||||
|
||||
@@ -66,19 +66,35 @@ fn (mut server Server) get_entry_property(entry &vfs.FSEntry, name string) !Prop
|
||||
property_name := if name.contains(':') { name.all_after(':') } else { name }
|
||||
|
||||
return match property_name {
|
||||
'creationdate' { Property(CreationDate(format_iso8601(entry.get_metadata().created_time()))) }
|
||||
'getetag' { Property(GetETag(entry.get_metadata().id.str())) }
|
||||
'resourcetype' { Property(ResourceType(entry.is_dir())) }
|
||||
'getlastmodified', 'lastmodified_server' {
|
||||
'creationdate' {
|
||||
Property(CreationDate(format_iso8601(entry.get_metadata().created_time())))
|
||||
}
|
||||
'getetag' {
|
||||
Property(GetETag(entry.get_metadata().id.str()))
|
||||
}
|
||||
'resourcetype' {
|
||||
Property(ResourceType(entry.is_dir()))
|
||||
}
|
||||
'getlastmodified', 'lastmodified_server' {
|
||||
// Both standard getlastmodified and custom lastmodified_server properties
|
||||
// return the same information
|
||||
Property(GetLastModified(texttools.format_rfc1123(entry.get_metadata().modified_time())))
|
||||
}
|
||||
'getcontentlength' { Property(GetContentLength(entry.get_metadata().size.str())) }
|
||||
'quota-available-bytes' { Property(QuotaAvailableBytes(16184098816)) }
|
||||
'quota-used-bytes' { Property(QuotaUsedBytes(16184098816)) }
|
||||
'quotaused' { Property(QuotaUsed(16184098816)) }
|
||||
'quota' { Property(Quota(16184098816)) }
|
||||
'getcontentlength' {
|
||||
Property(GetContentLength(entry.get_metadata().size.str()))
|
||||
}
|
||||
'quota-available-bytes' {
|
||||
Property(QuotaAvailableBytes(16184098816))
|
||||
}
|
||||
'quota-used-bytes' {
|
||||
Property(QuotaUsedBytes(16184098816))
|
||||
}
|
||||
'quotaused' {
|
||||
Property(QuotaUsed(16184098816))
|
||||
}
|
||||
'quota' {
|
||||
Property(Quota(16184098816))
|
||||
}
|
||||
'displayname' {
|
||||
// RFC 4918, Section 15.2: displayname is a human-readable name for UI display
|
||||
// For now, we use the filename as the displayname, but this could be enhanced
|
||||
@@ -102,7 +118,7 @@ fn (mut server Server) get_entry_property(entry &vfs.FSEntry, name string) !Prop
|
||||
// Always show as unlocked for now to ensure compatibility
|
||||
Property(LockDiscovery(''))
|
||||
}
|
||||
else {
|
||||
else {
|
||||
// For any unimplemented property, return an empty string instead of panicking
|
||||
// This improves compatibility with various WebDAV clients
|
||||
log.info('[WebDAV] Unimplemented property requested: ${name}')
|
||||
@@ -127,16 +143,24 @@ fn (mut server Server) get_responses(entry vfs.FSEntry, req PropfindRequest, pat
|
||||
}
|
||||
// main entry response
|
||||
responses << PropfindResponse{
|
||||
href: ensure_leading_slash(if entry.is_dir() { '${path.trim_string_right('/')}/' } else { path })
|
||||
href: ensure_leading_slash(if entry.is_dir() {
|
||||
'${path.trim_string_right('/')}/'
|
||||
} else {
|
||||
path
|
||||
})
|
||||
// not_found: entry.get_unfound_properties(req)
|
||||
found_props: properties
|
||||
}
|
||||
} else {
|
||||
responses << PropfindResponse{
|
||||
href: ensure_leading_slash(if entry.is_dir() { '${path.trim_string_right('/')}/' } else { path })
|
||||
// not_found: entry.get_unfound_properties(req)
|
||||
found_props: server.get_properties(entry)
|
||||
}
|
||||
responses << PropfindResponse{
|
||||
href: ensure_leading_slash(if entry.is_dir() {
|
||||
'${path.trim_string_right('/')}/'
|
||||
} else {
|
||||
path
|
||||
})
|
||||
// not_found: entry.get_unfound_properties(req)
|
||||
found_props: server.get_properties(entry)
|
||||
}
|
||||
}
|
||||
|
||||
if !entry.is_dir() || req.depth == .zero {
|
||||
@@ -148,10 +172,10 @@ fn (mut server Server) get_responses(entry vfs.FSEntry, req PropfindRequest, pat
|
||||
return responses
|
||||
}
|
||||
for e in entries {
|
||||
child_path := if path.ends_with('/') {
|
||||
path + e.get_metadata().name
|
||||
} else {
|
||||
path + '/' + e.get_metadata().name
|
||||
child_path := if path.ends_with('/') {
|
||||
path + e.get_metadata().name
|
||||
} else {
|
||||
path + '/' + e.get_metadata().name
|
||||
}
|
||||
responses << server.get_responses(e, PropfindRequest{
|
||||
...req
|
||||
|
||||
@@ -487,11 +487,12 @@ fn test_server_propfind() ! {
|
||||
assert ctx.res.header.get(.content_type)! == 'application/xml'
|
||||
assert ctx.res.body.contains('<D:multistatus')
|
||||
assert ctx.res.body.contains('<D:response>')
|
||||
|
||||
|
||||
// Now that we know the correct format, check for it - directories have both leading and trailing slashes
|
||||
assert ctx.res.body.contains('<D:href>/${root_dir}/</D:href>')
|
||||
// Should only include the requested resource
|
||||
assert !ctx.res.body.contains('<D:href>/${file_in_root}</D:href>') && !ctx.res.body.contains('<D:href>/${file_in_root}')
|
||||
assert !ctx.res.body.contains('<D:href>/${file_in_root}</D:href>')
|
||||
&& !ctx.res.body.contains('<D:href>/${file_in_root}')
|
||||
|
||||
// Test PROPFIND with depth=1 (resource and immediate children)
|
||||
mut ctx2 := Context{
|
||||
|
||||
Reference in New Issue
Block a user