This commit is contained in:
2025-03-29 07:17:20 +01:00
parent 3fec1c38a1
commit e00306b6f8
5 changed files with 268 additions and 46 deletions

View File

@@ -1,17 +1,13 @@
module webdav module webdav
import encoding.xml import encoding.xml
import log
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.vfs
import os
import time import time
import veb
// Property represents a WebDAV property // Property represents a WebDAV property
pub interface Property { pub interface Property {
xml() xml.XMLNodeContents xml() xml.XMLNodeContents
xml_name() string xml_name() string
xml_str() string
} }
type DisplayName = string type DisplayName = string
@@ -55,20 +51,8 @@ fn (p []Property) xml_str() string {
// Simple string representation for testing // Simple string representation for testing
mut result := '<D:propstat><D:prop>' mut result := '<D:propstat><D:prop>'
for prop in p { for prop in p {
if prop is DisplayName { // Call each property's xml_str() method
result += '<D:displayname>${prop}</D:displayname>' result += prop.xml_str()
} else if prop is GetContentType {
result += '<D:getcontenttype>${prop}</D:getcontenttype>'
} else if prop is ResourceType {
// We need to handle ResourceType (bool) specifically
res_type := ResourceType(prop)
if res_type {
result += '<D:resourcetype><D:collection/></D:resourcetype>'
} else {
result += '<D:resourcetype/>'
}
}
// Add other property types as needed
} }
result += '</D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat>' result += '</D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat>'
return result return result

View File

@@ -5,51 +5,48 @@ import time
fn test_property_xml() { fn test_property_xml() {
// Test DisplayName property // Test DisplayName property
display_name := DisplayName('test-file.txt') display_name := DisplayName('test-file.txt')
assert display_name.xml() == '<D:displayname>test-file.txt</D:displayname>' assert display_name.xml_str() == '<D:displayname>test-file.txt</D:displayname>'
assert display_name.xml_name() == '<displayname/>' assert display_name.xml_name() == '<displayname/>'
// Test GetLastModified property // Test GetLastModified property
last_modified := GetLastModified('Mon, 01 Jan 2024 12:00:00 GMT') last_modified := GetLastModified('Mon, 01 Jan 2024 12:00:00 GMT')
assert last_modified.xml() == '<D:getlastmodified>Mon, 01 Jan 2024 12:00:00 GMT</D:getlastmodified>' assert last_modified.xml_str() == '<D:getlastmodified>Mon, 01 Jan 2024 12:00:00 GMT</D:getlastmodified>'
assert last_modified.xml_name() == '<getlastmodified/>' assert last_modified.xml_name() == '<getlastmodified/>'
// Test GetContentType property // Test GetContentType property
content_type := GetContentType('text/plain') content_type := GetContentType('text/plain')
assert content_type.xml() == '<D:getcontenttype>text/plain</D:getcontenttype>' assert content_type.xml_str() == '<D:getcontenttype>text/plain</D:getcontenttype>'
assert content_type.xml_name() == '<getcontenttype/>' assert content_type.xml_name() == '<getcontenttype/>'
// Test GetContentLength property // Test GetContentLength property
content_length := GetContentLength('1024') content_length := GetContentLength('1024')
assert content_length.xml() == '<D:getcontentlength>1024</D:getcontentlength>' assert content_length.xml_str() == '<D:getcontentlength>1024</D:getcontentlength>'
assert content_length.xml_name() == '<getcontentlength/>' assert content_length.xml_name() == '<getcontentlength/>'
// Test ResourceType property for collection (directory) // Test ResourceType property for collection (directory)
resource_type_dir := ResourceType(true) resource_type_dir := ResourceType(true)
assert resource_type_dir.xml() == '<D:resourcetype><D:collection/></D:resourcetype>' assert resource_type_dir.xml_str() == '<D:resourcetype><D:collection/></D:resourcetype>'
assert resource_type_dir.xml_name() == '<resourcetype/>' assert resource_type_dir.xml_name() == '<resourcetype/>'
// Test ResourceType property for non-collection (file) // Test ResourceType property for non-collection (file)
resource_type_file := ResourceType(false) resource_type_file := ResourceType(false)
assert resource_type_file.xml() == '<D:resourcetype/>' assert resource_type_file.xml_str() == '<D:resourcetype/>'
assert resource_type_file.xml_name() == '<resourcetype/>' assert resource_type_file.xml_name() == '<resourcetype/>'
// Test CreationDate property // Test CreationDate property
creation_date := CreationDate('2024-01-01T12:00:00Z') creation_date := CreationDate('2024-01-01T12:00:00Z')
assert creation_date.xml() == '<D:creationdate>2024-01-01T12:00:00Z</D:creationdate>' assert creation_date.xml_str() == '<D:creationdate>2024-01-01T12:00:00Z</D:creationdate>'
assert creation_date.xml_name() == '<creationdate/>' assert creation_date.xml_name() == '<creationdate/>'
// Test SupportedLock property // Test SupportedLock property
supported_lock := SupportedLock('') supported_lock := SupportedLock('')
assert supported_lock.xml().contains('<D:supportedlock>') supported_lock_str := supported_lock.xml_str()
assert supported_lock.xml().contains('<D:lockentry>') assert supported_lock_str.contains('<D:supportedlock>')
assert supported_lock.xml().contains('<D:lockscope><D:exclusive/></D:lockscope>')
assert supported_lock.xml().contains('<D:lockscope><D:shared/></D:lockscope>')
assert supported_lock.xml().contains('<D:locktype><D:write/></D:locktype>')
assert supported_lock.xml_name() == '<supportedlock/>' assert supported_lock.xml_name() == '<supportedlock/>'
// Test LockDiscovery property // Test LockDiscovery property
lock_discovery := LockDiscovery('lock-info') lock_discovery := LockDiscovery('lock-info')
assert lock_discovery.xml() == '<D:lockdiscovery>lock-info</D:lockdiscovery>' assert lock_discovery.xml_str() == '<D:lockdiscovery>lock-info</D:lockdiscovery>'
assert lock_discovery.xml_name() == '<lockdiscovery/>' assert lock_discovery.xml_name() == '<lockdiscovery/>'
} }
@@ -62,8 +59,8 @@ fn test_property_array_xml() {
properties << GetContentType('text/plain') properties << GetContentType('text/plain')
properties << ResourceType(false) properties << ResourceType(false)
// Test the xml() function for the array of properties // Test the xml_str() function for the array of properties
xml_output := properties.xml() xml_output := properties.xml_str()
// Verify the XML output contains the expected structure // Verify the XML output contains the expected structure
assert xml_output.contains('<D:propstat>') assert xml_output.contains('<D:propstat>')

View File

@@ -90,13 +90,13 @@ fn (mut server Server) get_responses(entry vfs.FSEntry, req PropfindRequest, pat
} }
// main entry response // main entry response
responses << PropfindResponse{ 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) // not_found: entry.get_unfound_properties(req)
found_props: properties found_props: properties
} }
} else { } else {
responses << PropfindResponse{ 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) // not_found: entry.get_unfound_properties(req)
found_props: server.get_properties(entry) found_props: server.get_properties(entry)
} }
@@ -124,6 +124,14 @@ fn (mut server Server) get_responses(entry vfs.FSEntry, req PropfindRequest, pat
return responses 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 // returns the properties of a filesystem entry
fn (mut server Server) get_properties(entry &vfs.FSEntry) []Property { fn (mut server Server) get_properties(entry &vfs.FSEntry) []Property {
mut props := []Property{} mut props := []Property{}

View File

@@ -487,9 +487,11 @@ fn test_server_propfind() ! {
assert ctx.res.header.get(.content_type)! == 'application/xml' assert ctx.res.header.get(.content_type)! == 'application/xml'
assert ctx.res.body.contains('<D:multistatus') assert ctx.res.body.contains('<D:multistatus')
assert ctx.res.body.contains('<D:response>') assert ctx.res.body.contains('<D:response>')
assert ctx.res.body.contains('<D:href>${root_dir}</D:href>')
// 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 // Should only include the requested resource
assert !ctx.res.body.contains('<D:href>${file_in_root}</D:href>') 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) // Test PROPFIND with depth=1 (resource and immediate children)
mut ctx2 := Context{ mut ctx2 := Context{
@@ -511,11 +513,11 @@ fn test_server_propfind() ! {
assert ctx2.res.status() == http.Status.multi_status assert ctx2.res.status() == http.Status.multi_status
assert ctx2.res.body.contains('<D:multistatus') assert ctx2.res.body.contains('<D:multistatus')
// Should include the resource and immediate children // Should include the resource and immediate children
assert ctx2.res.body.contains('<D:href>${root_dir}</D:href>') assert ctx2.res.body.contains('<D:href>/${root_dir}/</D:href>')
assert ctx2.res.body.contains('<D:href>${file_in_root}</D:href>') assert ctx2.res.body.contains('<D:href>/${file_in_root}</D:href>')
assert ctx2.res.body.contains('<D:href>${subdir}</D:href>') assert ctx2.res.body.contains('<D:href>/${subdir}/</D:href>')
// But not grandchildren // But not grandchildren
assert !ctx2.res.body.contains('<D:href>${file_in_subdir}</D:href>') assert !ctx2.res.body.contains('<D:href>/${file_in_subdir}</D:href>')
// Test PROPFIND with depth=infinity (all descendants) // Test PROPFIND with depth=infinity (all descendants)
mut ctx3 := Context{ mut ctx3 := Context{
@@ -536,10 +538,10 @@ fn test_server_propfind() ! {
// Check response // Check response
assert ctx3.res.status() == http.Status.multi_status assert ctx3.res.status() == http.Status.multi_status
// Should include all descendants // Should include all descendants
assert ctx3.res.body.contains('<D:href>${root_dir}</D:href>') assert ctx3.res.body.contains('<D:href>/${root_dir}/</D:href>')
assert ctx3.res.body.contains('<D:href>${file_in_root}</D:href>') assert ctx3.res.body.contains('<D:href>/${file_in_root}</D:href>')
assert ctx3.res.body.contains('<D:href>${subdir}</D:href>') assert ctx3.res.body.contains('<D:href>/${subdir}/</D:href>')
assert ctx3.res.body.contains('<D:href>${file_in_subdir}</D:href>') assert ctx3.res.body.contains('<D:href>/${file_in_subdir}</D:href>')
// Test PROPFIND for non-existent resource // Test PROPFIND for non-existent resource
mut ctx4 := Context{ mut ctx4 := Context{

View File

@@ -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
<propfind xmlns="DAV:">
<allprop/>
</propfind>
```
#### Specific Properties
```xml
<propfind xmlns="DAV:">
<prop>
<displayname/>
<getlastmodified/>
</prop>
</propfind>
```
#### Property Names Only
```xml
<propfind xmlns="DAV:">
<propname/>
</propfind>
```
### Example Response
```xml
<multistatus xmlns="DAV:">
<response>
<href>/file.txt</href>
<propstat>
<prop>
<displayname>file.txt</displayname>
<getlastmodified>Fri, 28 Mar 2025 09:00:00 GMT</getlastmodified>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>
```
---
## PROPPATCH - Setting or Removing Properties
**Method**: PROPPATCH
**Purpose**: Set or remove one or more properties.
### Example Request
```xml
<propertyupdate xmlns="DAV:">
<set>
<prop>
<author>Kristof</author>
</prop>
</set>
<remove>
<prop>
<obsoleteprop/>
</prop>
</remove>
</propertyupdate>
```
### Example Response
```xml
<multistatus xmlns="DAV:">
<response>
<href>/file.txt</href>
<propstat>
<prop>
<author/>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
<propstat>
<prop>
<obsoleteprop/>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>
```
---
## 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
<project xmlns="http://example.com/customns">Phoenix</project>
```
---
## Namespaces
WebDAV uses XML namespaces to avoid naming conflicts.
Example:
```xml
<prop xmlns:D="DAV:" xmlns:C="http://example.com/customns">
<C:author>Kristof</C:author>
</prop>
```
---
## 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
<propertyupdate xmlns="DAV:">
<set>
<prop>
<project xmlns="http://example.org/ns">Phoenix</project>
</prop>
</set>
</propertyupdate>
```