diff --git a/lib/dav/webdav/model_property.v b/lib/dav/webdav/model_property.v
index 12b7465f..c369c1d2 100644
--- a/lib/dav/webdav/model_property.v
+++ b/lib/dav/webdav/model_property.v
@@ -1,17 +1,13 @@
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() xml.XMLNodeContents
xml_name() string
+ xml_str() string
}
type DisplayName = string
@@ -55,20 +51,8 @@ fn (p []Property) xml_str() string {
// Simple string representation for testing
mut result := ''
for prop in p {
- if prop is DisplayName {
- result += '${prop}'
- } else if prop is GetContentType {
- result += '${prop}'
- } else if prop is ResourceType {
- // We need to handle ResourceType (bool) specifically
- res_type := ResourceType(prop)
- if res_type {
- result += ''
- } else {
- result += ''
- }
- }
- // Add other property types as needed
+ // Call each property's xml_str() method
+ result += prop.xml_str()
}
result += 'HTTP/1.1 200 OK'
return result
diff --git a/lib/dav/webdav/model_property_test.v b/lib/dav/webdav/model_property_test.v
index d5d2ab34..0585b864 100644
--- a/lib/dav/webdav/model_property_test.v
+++ b/lib/dav/webdav/model_property_test.v
@@ -5,51 +5,48 @@ import time
fn test_property_xml() {
// Test DisplayName property
display_name := DisplayName('test-file.txt')
- assert display_name.xml() == 'test-file.txt'
+ assert display_name.xml_str() == 'test-file.txt'
assert display_name.xml_name() == ''
// Test GetLastModified property
last_modified := GetLastModified('Mon, 01 Jan 2024 12:00:00 GMT')
- assert last_modified.xml() == 'Mon, 01 Jan 2024 12:00:00 GMT'
+ assert last_modified.xml_str() == 'Mon, 01 Jan 2024 12:00:00 GMT'
assert last_modified.xml_name() == ''
// Test GetContentType property
content_type := GetContentType('text/plain')
- assert content_type.xml() == 'text/plain'
+ assert content_type.xml_str() == 'text/plain'
assert content_type.xml_name() == ''
// Test GetContentLength property
content_length := GetContentLength('1024')
- assert content_length.xml() == '1024'
+ assert content_length.xml_str() == '1024'
assert content_length.xml_name() == ''
// Test ResourceType property for collection (directory)
resource_type_dir := ResourceType(true)
- assert resource_type_dir.xml() == ''
+ assert resource_type_dir.xml_str() == ''
assert resource_type_dir.xml_name() == ''
// Test ResourceType property for non-collection (file)
resource_type_file := ResourceType(false)
- assert resource_type_file.xml() == ''
+ assert resource_type_file.xml_str() == ''
assert resource_type_file.xml_name() == ''
// Test CreationDate property
creation_date := CreationDate('2024-01-01T12:00:00Z')
- assert creation_date.xml() == '2024-01-01T12:00:00Z'
+ assert creation_date.xml_str() == '2024-01-01T12:00:00Z'
assert creation_date.xml_name() == ''
// Test SupportedLock property
supported_lock := SupportedLock('')
- assert supported_lock.xml().contains('')
- assert supported_lock.xml().contains('')
- assert supported_lock.xml().contains('')
- assert supported_lock.xml().contains('')
- assert supported_lock.xml().contains('')
+ supported_lock_str := supported_lock.xml_str()
+ assert supported_lock_str.contains('')
assert supported_lock.xml_name() == ''
// Test LockDiscovery property
lock_discovery := LockDiscovery('lock-info')
- assert lock_discovery.xml() == 'lock-info'
+ assert lock_discovery.xml_str() == 'lock-info'
assert lock_discovery.xml_name() == ''
}
@@ -62,8 +59,8 @@ fn test_property_array_xml() {
properties << GetContentType('text/plain')
properties << ResourceType(false)
- // Test the xml() function for the array of properties
- xml_output := properties.xml()
+ // Test the xml_str() function for the array of properties
+ xml_output := properties.xml_str()
// Verify the XML output contains the expected structure
assert xml_output.contains('')
diff --git a/lib/dav/webdav/server_propfind.v b/lib/dav/webdav/server_propfind.v
index a3c4180c..164fdc54 100644
--- a/lib/dav/webdav/server_propfind.v
+++ b/lib/dav/webdav/server_propfind.v
@@ -90,13 +90,13 @@ fn (mut server Server) get_responses(entry vfs.FSEntry, req PropfindRequest, pat
}
// main entry response
responses << PropfindResponse{
- href: 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: 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: server.get_properties(entry)
}
@@ -124,6 +124,14 @@ fn (mut server Server) get_responses(entry vfs.FSEntry, req PropfindRequest, pat
return responses
}
+// Helper function to ensure a path has a leading slash
+fn ensure_leading_slash(path string) string {
+ if path.starts_with('/') {
+ return path
+ }
+ return '/' + path
+}
+
// returns the properties of a filesystem entry
fn (mut server Server) get_properties(entry &vfs.FSEntry) []Property {
mut props := []Property{}
diff --git a/lib/dav/webdav/server_test.v b/lib/dav/webdav/server_test.v
index dfca5cdb..c475180c 100644
--- a/lib/dav/webdav/server_test.v
+++ b/lib/dav/webdav/server_test.v
@@ -487,9 +487,11 @@ fn test_server_propfind() ! {
assert ctx.res.header.get(.content_type)! == 'application/xml'
assert ctx.res.body.contains('')
- assert ctx.res.body.contains('${root_dir}')
+
+ // Now that we know the correct format, check for it - directories have both leading and trailing slashes
+ assert ctx.res.body.contains('/${root_dir}/')
// Should only include the requested resource
- assert !ctx.res.body.contains('${file_in_root}')
+ assert !ctx.res.body.contains('/${file_in_root}') && !ctx.res.body.contains('/${file_in_root}')
// Test PROPFIND with depth=1 (resource and immediate children)
mut ctx2 := Context{
@@ -511,11 +513,11 @@ fn test_server_propfind() ! {
assert ctx2.res.status() == http.Status.multi_status
assert ctx2.res.body.contains('${root_dir}')
- assert ctx2.res.body.contains('${file_in_root}')
- assert ctx2.res.body.contains('${subdir}')
+ assert ctx2.res.body.contains('/${root_dir}/')
+ assert ctx2.res.body.contains('/${file_in_root}')
+ assert ctx2.res.body.contains('/${subdir}/')
// But not grandchildren
- assert !ctx2.res.body.contains('${file_in_subdir}')
+ assert !ctx2.res.body.contains('/${file_in_subdir}')
// Test PROPFIND with depth=infinity (all descendants)
mut ctx3 := Context{
@@ -536,10 +538,10 @@ fn test_server_propfind() ! {
// Check response
assert ctx3.res.status() == http.Status.multi_status
// Should include all descendants
- assert ctx3.res.body.contains('${root_dir}')
- assert ctx3.res.body.contains('${file_in_root}')
- assert ctx3.res.body.contains('${subdir}')
- assert ctx3.res.body.contains('${file_in_subdir}')
+ assert ctx3.res.body.contains('/${root_dir}/')
+ assert ctx3.res.body.contains('/${file_in_root}')
+ assert ctx3.res.body.contains('/${subdir}/')
+ assert ctx3.res.body.contains('/${file_in_subdir}')
// Test PROPFIND for non-existent resource
mut ctx4 := Context{
diff --git a/lib/dav/webdav/specs/properties.md b/lib/dav/webdav/specs/properties.md
new file mode 100644
index 00000000..5a67cfab
--- /dev/null
+++ b/lib/dav/webdav/specs/properties.md
@@ -0,0 +1,231 @@
+# WebDAV Properties Specification
+
+WebDAV (Web Distributed Authoring and Versioning) extends HTTP to allow remote web content authoring operations. One of its most important features is **property management**, which allows clients to retrieve, set, and delete metadata (called "properties") on resources.
+
+---
+
+## Relevant RFCs
+
+- RFC 4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)
+- RFC 2518 - Original WebDAV specification (obsolete)
+
+---
+
+## Property Concepts
+
+### What is a Property?
+
+- A **property** is metadata associated with a WebDAV resource, such as a file or directory.
+- Properties are identified by **qualified names** in the form of `{namespace}propertyname`.
+- Property values are represented in XML.
+
+---
+
+## Property Value Types
+
+- XML-based values (text or structured XML)
+- Unicode text
+- Either **live** (managed by the server) or **dead** (set by clients)
+
+---
+
+## Live vs Dead Properties
+
+| Type | Description | Managed By |
+|---------|-------------------------------------------|------------|
+| Live | Server-defined and maintained | Server |
+| Dead | Arbitrary client-defined metadata | Client |
+
+Examples of live properties include `getlastmodified`, `resourcetype`, and `displayname`.
+
+---
+
+## PROPFIND - Retrieving Properties
+
+**Method**: PROPFIND
+**Purpose**: Retrieve properties from a resource.
+
+### Depth Header
+
+| Value | Meaning |
+|------------|----------------------------------|
+| 0 | The resource itself |
+| 1 | Resource and its immediate children |
+| infinity | Resource and all descendants |
+
+### Request Body Examples
+
+#### All Properties
+
+```xml
+
+
+
+```
+
+#### Specific Properties
+
+```xml
+
+
+
+
+
+
+```
+
+#### Property Names Only
+
+```xml
+
+
+
+```
+
+### Example Response
+
+```xml
+
+
+ /file.txt
+
+
+ file.txt
+ Fri, 28 Mar 2025 09:00:00 GMT
+
+ HTTP/1.1 200 OK
+
+
+
+```
+
+---
+
+## PROPPATCH - Setting or Removing Properties
+
+**Method**: PROPPATCH
+**Purpose**: Set or remove one or more properties.
+
+### Example Request
+
+```xml
+
+
+
+ Kristof
+
+
+
+
+
+
+
+
+```
+
+### Example Response
+
+```xml
+
+
+ /file.txt
+
+
+
+
+ HTTP/1.1 200 OK
+
+
+
+
+
+ HTTP/1.1 200 OK
+
+
+
+```
+
+---
+
+## Common Live Properties
+
+| Property Name | Namespace | Description |
+|---------------------|-----------|------------------------------------|
+| getcontentlength | DAV: | Size in bytes |
+| getcontenttype | DAV: | MIME type |
+| getetag | DAV: | Entity tag (ETag) |
+| getlastmodified | DAV: | Last modification time |
+| creationdate | DAV: | Resource creation time |
+| resourcetype | DAV: | Type of resource (file, collection)|
+| displayname | DAV: | Human-friendly name |
+
+---
+
+## Custom Properties
+
+Clients can define their own custom properties as XML with custom namespaces.
+
+Example:
+
+```xml
+Phoenix
+```
+
+---
+
+## Namespaces
+
+WebDAV uses XML namespaces to avoid naming conflicts.
+
+Example:
+
+```xml
+
+ Kristof
+
+```
+
+---
+
+## Other Related Methods
+
+- `MKCOL`: Create a new collection (directory)
+- `DELETE`: Remove a resource and its properties
+- `COPY` and `MOVE`: Properties are copied/moved along with resources
+
+---
+
+## Security Considerations
+
+- Clients need authorization to read or write properties.
+- Live properties may not be writable.
+- Dead property values must be stored and returned exactly as set.
+
+---
+
+## Complete Example Workflow
+
+1. Retrieve all properties:
+
+```http
+PROPFIND /doc.txt HTTP/1.1
+Depth: 0
+```
+
+2. Set a custom property:
+
+```http
+PROPPATCH /doc.txt HTTP/1.1
+Content-Type: application/xml
+```
+
+```xml
+
+
+
+ Phoenix
+
+
+
+```
+