This commit is contained in:
2025-05-04 08:19:47 +03:00
parent d8a59d0726
commit 46e1c6706c
177 changed files with 5708 additions and 5512 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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{