...
This commit is contained in:
@@ -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 := '<D:propstat><D:prop>'
|
||||
for prop in p {
|
||||
if prop is DisplayName {
|
||||
result += '<D:displayname>${prop}</D:displayname>'
|
||||
} 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
|
||||
// Call each property's xml_str() method
|
||||
result += prop.xml_str()
|
||||
}
|
||||
result += '</D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat>'
|
||||
return result
|
||||
|
||||
@@ -5,51 +5,48 @@ import time
|
||||
fn test_property_xml() {
|
||||
// Test DisplayName property
|
||||
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/>'
|
||||
|
||||
// Test GetLastModified property
|
||||
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/>'
|
||||
|
||||
// Test GetContentType property
|
||||
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/>'
|
||||
|
||||
// Test GetContentLength property
|
||||
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/>'
|
||||
|
||||
// Test ResourceType property for collection (directory)
|
||||
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/>'
|
||||
|
||||
// Test ResourceType property for non-collection (file)
|
||||
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/>'
|
||||
|
||||
// Test CreationDate property
|
||||
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/>'
|
||||
|
||||
// Test SupportedLock property
|
||||
supported_lock := SupportedLock('')
|
||||
assert supported_lock.xml().contains('<D:supportedlock>')
|
||||
assert supported_lock.xml().contains('<D:lockentry>')
|
||||
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>')
|
||||
supported_lock_str := supported_lock.xml_str()
|
||||
assert supported_lock_str.contains('<D:supportedlock>')
|
||||
assert supported_lock.xml_name() == '<supportedlock/>'
|
||||
|
||||
// Test LockDiscovery property
|
||||
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/>'
|
||||
}
|
||||
|
||||
@@ -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('<D:propstat>')
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -487,9 +487,11 @@ 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>')
|
||||
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
|
||||
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)
|
||||
mut ctx2 := Context{
|
||||
@@ -511,11 +513,11 @@ fn test_server_propfind() ! {
|
||||
assert ctx2.res.status() == http.Status.multi_status
|
||||
assert ctx2.res.body.contains('<D:multistatus')
|
||||
// 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>${file_in_root}</D:href>')
|
||||
assert ctx2.res.body.contains('<D:href>${subdir}</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>/${subdir}/</D:href>')
|
||||
// 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)
|
||||
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('<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>${subdir}</D:href>')
|
||||
assert ctx3.res.body.contains('<D:href>${file_in_subdir}</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>/${subdir}/</D:href>')
|
||||
assert ctx3.res.body.contains('<D:href>/${file_in_subdir}</D:href>')
|
||||
|
||||
// Test PROPFIND for non-existent resource
|
||||
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