...
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>')
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
231
lib/dav/webdav/specs/properties.md
Normal file
231
lib/dav/webdav/specs/properties.md
Normal 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>
|
||||||
|
```
|
||||||
|
|
||||||
Reference in New Issue
Block a user