2026-02-12 22:29:35 +01:00
# 22 - WebDAV Integration Guide
2025-04-04 21:31:41 +02:00
2026-02-12 22:29:35 +01:00
The WebDAV interface exposes file operations over HTTP at a single base path. All standard WebDAV methods are supported: **PROPFIND** , **GET** , **PUT** , **MKCOL** , **MOVE** , **COPY** , **DELETE** . Authentication is HTTP Basic over TLS.
2025-04-04 21:31:41 +02:00
## Table of Contents
1. [Base URL and Endpoints ](#base-url-and-endpoints )
2. [Authentication ](#authentication )
3. [Common Operations ](#common-operations )
- [Listing Directories ](#listing-directories )
- [Downloading Files ](#downloading-files )
- [Uploading Files ](#uploading-files )
- [Creating Folders ](#creating-folders )
- [Moving and Copying ](#moving-and-copying )
- [Deleting Resources ](#deleting-resources )
4. [XML Schemas ](#xml-schemas )
5. [Code Examples ](#code-examples )
6. [Extending WebDAV ](#extending-webdav )
7. [Troubleshooting ](#troubleshooting )
## Base URL and Endpoints
2026-02-12 22:29:35 +01:00
The WebDAV interface lives at:
2025-04-04 21:31:41 +02:00
```
https://[your-oxicloud-server]/webdav/
```
2026-02-12 22:29:35 +01:00
All file and folder operations hang off this base path. Append the resource path to the URL.
2025-04-04 21:31:41 +02:00
Examples:
- Root folder: `https://[your-oxicloud-server]/webdav/`
- File "document.pdf" in root: `https://[your-oxicloud-server]/webdav/document.pdf`
- Folder "projects": `https://[your-oxicloud-server]/webdav/projects/`
- File in subfolder: `https://[your-oxicloud-server]/webdav/projects/proposal.docx`
## Authentication
2026-02-12 22:29:35 +01:00
WebDAV uses HTTP Basic Authentication. Include the `Authorization` header with base64-encoded credentials:
2025-04-04 21:31:41 +02:00
```
Authorization: Basic base64(username:password)
```
2026-02-12 22:29:35 +01:00
Always use HTTPS.
2025-04-04 21:31:41 +02:00
## Common Operations
### Listing Directories
2026-02-12 22:29:35 +01:00
Use the **PROPFIND** method with a **Depth** header:
2025-04-04 21:31:41 +02:00
2026-02-12 22:29:35 +01:00
- `Depth: 0` -- info about the resource itself
- `Depth: 1` -- the resource and its immediate children (recommended)
- `Depth: infinity` -- the resource and all descendants (careful with large trees)
2025-04-04 21:31:41 +02:00
Request:
```http
PROPFIND /webdav/projects/ HTTP/1.1
Host: your-oxicloud-server
Depth: 1
Content-Type: application/xml
Authorization: Basic [credentials]
<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:">
<D:allprop/>
</D:propfind>
```
Response:
```http
HTTP / 1.1 207 Multi-Status
Content-Type : application/xml; charset=utf-8
```
### Downloading Files
Standard HTTP **GET** :
```http
GET /webdav/projects/document.pdf HTTP / 1.1
Host : your-oxicloud-server
Authorization : Basic [credentials]
```
Response:
```http
HTTP / 1.1 200 OK
Content-Type : application/pdf
Content-Length : 12345
Last-Modified : Wed, 15 Nov 2023 12:34:56 GMT
2026-02-12 22:29:35 +01:00
ETag : "abc123"
2025-04-04 21:31:41 +02:00
[File content]
```
### Uploading Files
Use HTTP **PUT** to upload or update a file:
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00
```http
PUT /webdav/projects/document.pdf HTTP / 1.1
Host : your-oxicloud-server
Content-Type : application/pdf
Content-Length : 12345
Authorization : Basic [credentials]
[File content]
```
New files return `201 Created` . Updates return `204 No Content` .
### Creating Folders
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00
Use the **MKCOL** method:
```http
MKCOL /webdav/projects/new-folder HTTP/1.1
Host: your-oxicloud-server
Authorization: Basic [credentials]
```
Returns `201 Created` on success.
### Moving and Copying
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00
**MOVE** a resource:
```http
2026-02-12 22:29:35 +01:00
MOVE /webdav/old-location.pdf HTTP/1.1
2025-04-04 21:31:41 +02:00
Host: your-oxicloud-server
Destination: https://your-oxicloud-server/webdav/new-location.pdf
Authorization: Basic [credentials]
```
**COPY** a resource:
2026-02-12 22:29:35 +01:00
```http
2025-04-04 21:31:41 +02:00
COPY /webdav/original.pdf HTTP/1.1
Host: your-oxicloud-server
Destination: https://your-oxicloud-server/webdav/copy.pdf
2026-02-12 22:29:35 +01:00
Authorization: Basic [credentials]
2025-04-04 21:31:41 +02:00
```
Both return `204 No Content` on success.
### Deleting Resources
Use HTTP **DELETE** :
2026-02-12 22:29:35 +01:00
```http
2025-04-04 21:31:41 +02:00
DELETE /webdav/projects/document.pdf HTTP / 1.1
Host : your-oxicloud-server
Authorization : Basic [credentials]
```
Returns `204 No Content` on success.
## XML Schemas
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00
### PROPFIND Request
Request all properties:
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00
```xml
<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D= "DAV:" >
<D:allprop/>
</D:propfind>
```
2026-02-12 22:29:35 +01:00
Request specific properties:
2025-04-04 21:31:41 +02:00
```xml
<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D= "DAV:" >
<D:prop>
<D:displayname/>
<D:getcontentlength/>
<D:getlastmodified/>
</D:prop>
</D:propfind>
```
### PROPPATCH Request
Set and remove properties:
```xml
<?xml version="1.0" encoding="utf-8" ?>
<D:propertyupdate xmlns:D= "DAV:" xmlns:Z= "http://example.org/custom/" >
<D:set>
<D:prop>
<Z:custom-property> Custom Value</Z:custom-property>
</D:prop>
</D:set>
<D:remove>
<D:prop>
<Z:old-property/>
</D:prop>
</D:remove>
</D:propertyupdate>
```
### LOCK Request
```xml
<?xml version="1.0" encoding="utf-8" ?>
<D:lockinfo xmlns:D= "DAV:" >
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
<D:owner>
<D:href> mailto:user@example.com</D:href>
</D:owner>
</D:lockinfo>
```
## Code Examples
### Python Example
Using the `requests` library:
```python
import requests
from requests.auth import HTTPBasicAuth
import xml.etree.ElementTree as ET
# Set up authentication
auth = HTTPBasicAuth ( 'username' , 'password' )
base_url = 'https://your-oxicloud-server/webdav'
# 1. List directory contents
headers = { 'Depth' : '1' }
body = '''<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:">
<D:allprop/>
</D:propfind>'''
response = requests . request (
'PROPFIND' ,
f ' { base_url } /projects/' ,
headers = headers ,
data = body ,
auth = auth
)
if response . status_code == 207 : # Multi-Status
# Parse XML response
root = ET . fromstring ( response . content )
for response_elem in root . findall ( '.// {DAV:} response' ):
href = response_elem . find ( '.// {DAV:} href' ) . text
print ( f "Resource: { href } " )
# Get displayname if available
2026-02-12 22:29:35 +01:00
displayname = response_elem . find ( '.// {DAV:} displayname' )
if displayname is not None and displayname . text :
print ( f " Name: { displayname . text } " )
2025-04-04 21:31:41 +02:00
# Check if it's a collection (folder)
resourcetype = response_elem . find ( '.// {DAV:} resourcetype' )
is_collection = resourcetype is not None and resourcetype . find ( '.// {DAV:} collection' ) is not None
print ( f " Type: { 'Folder' if is_collection else 'File' } " )
# Get size if it's a file
if not is_collection :
contentlength = response_elem . find ( '.// {DAV:} getcontentlength' )
if contentlength is not None and contentlength . text :
2026-02-12 22:29:35 +01:00
print ( f " Size: { contentlength . text } bytes" )
2025-04-04 21:31:41 +02:00
# 2. Upload a file
with open ( 'local-file.pdf' , 'rb' ) as f :
file_content = f . read ()
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00
response = requests . put (
f ' { base_url } /projects/document.pdf' ,
data = file_content ,
auth = auth
2026-02-12 22:29:35 +01:00
)
2025-04-04 21:31:41 +02:00
if response . status_code in ( 201 , 204 ):
print ( "File uploaded successfully" )
# 3. Download a file
response = requests . get (
f ' { base_url } /projects/document.pdf' ,
auth = auth
)
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00
if response . status_code == 200 :
with open ( 'downloaded-file.pdf' , 'wb' ) as f :
f . write ( response . content )
print ( "File downloaded successfully" )
# 4. Create a folder
response = requests . request (
'MKCOL' ,
f ' { base_url } /projects/new-folder' ,
auth = auth
)
if response . status_code == 201 :
print ( "Folder created successfully" )
# 5. Move a file
headers = {
'Destination' : f ' { base_url } /projects/new-location.pdf'
}
response = requests . request (
'MOVE' ,
f ' { base_url } /projects/old-location.pdf' ,
headers = headers ,
auth = auth
)
if response . status_code == 204 :
print ( "File moved successfully" )
# 6. Delete a file
response = requests . delete (
f ' { base_url } /projects/document.pdf' ,
auth = auth
)
if response . status_code == 204 :
print ( "File deleted successfully" )
```
### JavaScript Example
Using the browser `fetch` API:
```javascript
// Base configuration
const baseUrl = 'https://your-oxicloud-server/webdav' ;
const credentials = btoa ( 'username:password' );
const headers = {
'Authorization' : `Basic ${ credentials } `
};
// 1. List directory contents
async function listDirectory ( path ) {
const response = await fetch ( ` ${ baseUrl }${ path } ` , {
method : 'PROPFIND' ,
headers : {
2026-02-12 22:29:35 +01:00
... headers ,
2025-04-04 21:31:41 +02:00
'Depth' : '1' ,
'Content-Type' : 'application/xml'
},
body : `<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:">
<D:allprop/>
</D:propfind>`
});
if ( response . status === 207 ) {
const text = await response . text ();
const parser = new DOMParser ();
const xmlDoc = parser . parseFromString ( text , 'text/xml' );
const responses = xmlDoc . getElementsByTagNameNS ( 'DAV:' , 'response' );
const resources = [];
for ( let i = 0 ; i < responses . length ; i ++ ) {
const response = responses [ i ];
const href = response . getElementsByTagNameNS ( 'DAV:' , 'href' )[ 0 ]. textContent ;
let displayName = '' ;
const displayNameElems = response . getElementsByTagNameNS ( 'DAV:' , 'displayname' );
2026-02-12 22:29:35 +01:00
if ( displayNameElems . length > 0 ) {
2025-04-04 21:31:41 +02:00
displayName = displayNameElems [ 0 ]. textContent ;
}
// Check if resource is a collection (folder)
2026-02-12 22:29:35 +01:00
const resourceTypeElem = response . getElementsByTagNameNS ( 'DAV:' , 'resourcetype' )[ 0 ];
2025-04-04 21:31:41 +02:00
const isCollection = resourceTypeElem . getElementsByTagNameNS ( 'DAV:' , 'collection' ). length > 0 ;
2026-02-12 22:29:35 +01:00
// Get file size if it's a file
2025-04-04 21:31:41 +02:00
let size = null ;
if ( ! isCollection ) {
const contentLengthElems = response . getElementsByTagNameNS ( 'DAV:' , 'getcontentlength' );
2026-02-12 22:29:35 +01:00
if ( contentLengthElems . length > 0 ) {
2025-04-04 21:31:41 +02:00
size = parseInt ( contentLengthElems [ 0 ]. textContent , 10 );
}
}
resources . push ({
2026-02-12 22:29:35 +01:00
href ,
2025-04-04 21:31:41 +02:00
displayName ,
isCollection ,
size
2026-02-12 22:29:35 +01:00
});
2025-04-04 21:31:41 +02:00
}
return resources ;
} else {
throw new Error ( `Failed to list directory: ${ response . status } ` );
}
}
2026-02-12 22:29:35 +01:00
// 2. Upload a file
2025-04-04 21:31:41 +02:00
async function uploadFile ( path , fileContent ) {
const response = await fetch ( ` ${ baseUrl }${ path } ` , {
method : 'PUT' ,
headers : {
... headers ,
'Content-Type' : 'application/octet-stream'
},
2026-02-12 22:29:35 +01:00
body : fileContent
2025-04-04 21:31:41 +02:00
});
return response . status === 201 || response . status === 204 ;
}
// Example usage with a File object from an input
const fileInput = document . getElementById ( 'fileInput' );
fileInput . addEventListener ( 'change' , async ( event ) => {
const file = event . target . files [ 0 ];
if ( file ) {
const result = await uploadFile ( `/projects/ ${ file . name } ` , file );
console . log ( `Upload ${ result ? 'successful' : 'failed' } ` );
}
});
// 3. Download a file
2026-02-12 22:29:35 +01:00
async function downloadFile ( path ) {
2025-04-04 21:31:41 +02:00
const response = await fetch ( ` ${ baseUrl }${ path } ` , {
method : 'GET' ,
headers
});
if ( response . status === 200 ) {
return await response . blob ();
} else {
throw new Error ( `Failed to download: ${ response . status } ` );
}
}
// Example usage with download attribute
async function downloadAndSave ( path , filename ) {
try {
const blob = await downloadFile ( path );
const url = URL . createObjectURL ( blob );
const a = document . createElement ( 'a' );
2026-02-12 22:29:35 +01:00
a . href = url ;
2025-04-04 21:31:41 +02:00
a . download = filename ;
document . body . appendChild ( a );
a . click ();
// Clean up
document . body . removeChild ( a );
URL . revokeObjectURL ( url );
} catch ( error ) {
console . error ( 'Download failed:' , error );
}
}
2026-02-12 22:29:35 +01:00
// 4. Create a folder
2025-04-04 21:31:41 +02:00
async function createFolder ( path ) {
const response = await fetch ( ` ${ baseUrl }${ path } ` , {
method : 'MKCOL' ,
headers
});
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00
return response . status === 201 ;
}
// 5. Move a file
async function moveResource ( fromPath , toPath ) {
const response = await fetch ( ` ${ baseUrl }${ fromPath } ` , {
method : 'MOVE' ,
headers : {
... headers ,
'Destination' : ` ${ baseUrl }${ toPath } `
}
});
return response . status === 204 ;
2026-02-12 22:29:35 +01:00
}
2025-04-04 21:31:41 +02:00
// 6. Delete a resource
async function deleteResource ( path ) {
const response = await fetch ( ` ${ baseUrl }${ path } ` , {
method : 'DELETE' ,
headers
});
return response . status === 204 ;
}
```
2026-02-12 22:29:35 +01:00
### C# Example
2025-04-04 21:31:41 +02:00
```csharp
using System ;
using System.Net.Http ;
using System.Net.Http.Headers ;
using System.Text ;
using System.Threading.Tasks ;
using System.Xml.Linq ;
2026-02-12 22:29:35 +01:00
class WebDavClient
2025-04-04 21:31:41 +02:00
{
private readonly HttpClient _httpClient ;
private readonly string _baseUrl ;
public WebDavClient ( string baseUrl , string username , string password )
{
_baseUrl = baseUrl . TrimEnd ( '/' ) + "/webdav" ;
_httpClient = new HttpClient ();
// Set Basic Authentication
var credentials = Convert . ToBase64String ( Encoding . UTF8 . GetBytes ( $"{username}:{password}" ));
_httpClient . DefaultRequestHeaders . Authorization =
new AuthenticationHeaderValue ( "Basic" , credentials );
}
public async Task < XDocument > ListDirectoryAsync ( string path )
{
var request = new HttpRequestMessage ( new HttpMethod ( "PROPFIND" ), $"{_baseUrl}/{path.TrimStart('/')}" );
2026-02-12 22:29:35 +01:00
request . Headers . Add ( "Depth" , "1" );
2025-04-04 21:31:41 +02:00
request . Content = new StringContent (
@"<?xml version=""1.0"" encoding=""utf-8"" ?>
<D:propfind xmlns:D=""DAV:"">
<D:allprop/>
2026-02-12 22:29:35 +01:00
</D:propfind>" ,
2025-04-04 21:31:41 +02:00
Encoding . UTF8 ,
"application/xml"
2026-02-12 22:29:35 +01:00
);
2025-04-04 21:31:41 +02:00
var response = await _httpClient . SendAsync ( request );
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00
if ( response . StatusCode == System . Net . HttpStatusCode . MultiStatus )
{
var content = await response . Content . ReadAsStringAsync ();
return XDocument . Parse ( content );
}
throw new Exception ( $"Failed to list directory: {response.StatusCode}" );
}
public async Task < bool > UploadFileAsync ( string path , byte [] content )
{
var request = new HttpRequestMessage ( HttpMethod . Put , $"{_baseUrl}/{path.TrimStart('/')}" );
2026-02-12 22:29:35 +01:00
request . Content = new ByteArrayContent ( content );
2025-04-04 21:31:41 +02:00
2026-02-12 22:29:35 +01:00
var response = await _httpClient . SendAsync ( request );
2025-04-04 21:31:41 +02:00
return response . StatusCode == System . Net . HttpStatusCode . Created ||
response . StatusCode == System . Net . HttpStatusCode . NoContent ;
}
2026-02-12 22:29:35 +01:00
public async Task < byte []> DownloadFileAsync ( string path )
2025-04-04 21:31:41 +02:00
{
var response = await _httpClient . GetAsync ( $"{_baseUrl}/{path.TrimStart('/')}" );
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00
if ( response . IsSuccessStatusCode )
{
return await response . Content . ReadAsByteArrayAsync ();
}
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00
throw new Exception ( $"Failed to download file: {response.StatusCode}" );
2026-02-12 22:29:35 +01:00
}
2025-04-04 21:31:41 +02:00
public async Task < bool > CreateFolderAsync ( string path )
{
2026-02-12 22:29:35 +01:00
var request = new HttpRequestMessage ( new HttpMethod ( "MKCOL" ), $"{_baseUrl}/{path.TrimStart('/')}" );
2025-04-04 21:31:41 +02:00
var response = await _httpClient . SendAsync ( request );
return response . StatusCode == System . Net . HttpStatusCode . Created ;
2026-02-12 22:29:35 +01:00
}
2025-04-04 21:31:41 +02:00
public async Task < bool > MoveResourceAsync ( string fromPath , string toPath )
{
var request = new HttpRequestMessage ( new HttpMethod ( "MOVE" ), $"{_baseUrl}/{fromPath.TrimStart('/')}" );
2026-02-12 22:29:35 +01:00
request . Headers . Add ( "Destination" , $"{_baseUrl}/{toPath.TrimStart('/')}" );
2025-04-04 21:31:41 +02:00
var response = await _httpClient . SendAsync ( request );
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00
return response . StatusCode == System . Net . HttpStatusCode . NoContent ;
}
public async Task < bool > DeleteResourceAsync ( string path )
2026-02-12 22:29:35 +01:00
{
2025-04-04 21:31:41 +02:00
var response = await _httpClient . DeleteAsync ( $"{_baseUrl}/{path.TrimStart('/')}" );
2026-02-12 22:29:35 +01:00
return response . StatusCode == System . Net . HttpStatusCode . NoContent ;
2025-04-04 21:31:41 +02:00
}
}
// Example usage
2026-02-12 22:29:35 +01:00
async Task RunExampleAsync ()
2025-04-04 21:31:41 +02:00
{
2026-02-12 22:29:35 +01:00
var client = new WebDavClient ( "https://your-oxicloud-server" , "username" , "password" );
2025-04-04 21:31:41 +02:00
// List directory
2026-02-12 22:29:35 +01:00
try
2025-04-04 21:31:41 +02:00
{
var directoryListing = await client . ListDirectoryAsync ( "/projects" );
// Process XML results...
2026-02-12 22:29:35 +01:00
Console . WriteLine ( "Directory listing successful" );
2025-04-04 21:31:41 +02:00
}
catch ( Exception ex )
{
Console . WriteLine ( $"Error listing directory: {ex.Message}" );
}
// Upload a file
try
2026-02-12 22:29:35 +01:00
{
2025-04-04 21:31:41 +02:00
var fileContent = await File . ReadAllBytesAsync ( "local-file.pdf" );
var result = await client . UploadFileAsync ( "/projects/document.pdf" , fileContent );
Console . WriteLine ( $"Upload {(result ? " successful " : " failed ")}" );
}
catch ( Exception ex )
{
Console . WriteLine ( $"Error uploading file: {ex.Message}" );
}
// Download a file
try
2026-02-12 22:29:35 +01:00
{
2025-04-04 21:31:41 +02:00
var fileContent = await client . DownloadFileAsync ( "/projects/document.pdf" );
await File . WriteAllBytesAsync ( "downloaded-file.pdf" , fileContent );
Console . WriteLine ( "Download successful" );
}
catch ( Exception ex )
{
Console . WriteLine ( $"Error downloading file: {ex.Message}" );
}
}
```
2026-02-12 22:29:35 +01:00
## Extending WebDAV
2025-04-04 21:31:41 +02:00
### Adding Custom Properties
To support custom WebDAV properties:
1. Define your XML namespace for custom properties.
2. Implement storage for them (database table recommended).
3. Update the WebDAV adapter to handle these properties.
Example adapter code for custom properties:
```rust
// Add to WebDavAdapter implementation
fn handle_custom_property ( name : & QualifiedName , value : Option <& str > ) -> Result < bool > {
if name . namespace == "http://example.org/custom/" {
// Store the custom property in your database
// ...
return Ok ( true );
}
2026-02-12 22:29:35 +01:00
// Property not handled
Ok ( false )
}
2025-04-04 21:31:41 +02:00
```
### Supporting CalDAV/CardDAV
To extend with CalDAV/CardDAV support:
1. Create additional adapters for calendar and contact data.
2. Implement the additional XML namespaces required.
3. Create handlers for the specialized methods.
4. Integrate with calendar and contacts storage.
2026-02-12 22:29:35 +01:00
See `caldav-technical-spec.md` and `carddav-technical-spec.md` for details.
2025-04-04 21:31:41 +02:00
## Troubleshooting
### Common Issues
1. **Authentication Failures**
- Check credentials are correctly Base64-encoded
2026-02-12 22:29:35 +01:00
- Ensure the Authorization header is formatted correctly
- Verify the user has the necessary permissions
2025-04-04 21:31:41 +02:00
2026-02-12 22:29:35 +01:00
2025-04-04 21:31:41 +02:00