18 KiB
22 - WebDAV Integration Guide
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.
Table of Contents
- Base URL and Endpoints
- Authentication
- Common Operations
- XML Schemas
- Code Examples
- Extending WebDAV
- Troubleshooting
Base URL and Endpoints
The WebDAV interface lives at:
https://[your-oxicloud-server]/webdav/
All file and folder operations hang off this base path. Append the resource path to the URL.
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
WebDAV uses HTTP Basic Authentication. Include the Authorization header with base64-encoded credentials:
Authorization: Basic base64(username:password)
Always use HTTPS.
Common Operations
Listing Directories
Use the PROPFIND method with a Depth header:
Depth: 0-- info about the resource itselfDepth: 1-- the resource and its immediate children (recommended)Depth: infinity-- the resource and all descendants (careful with large trees)
Request:
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/1.1 207 Multi-Status
Content-Type: application/xml; charset=utf-8
<?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>/webdav/projects/</D:href>
<D:propstat>
<D:prop>
<D:resourcetype><D:collection/></D:resourcetype>
<D:displayname>projects</D:displayname>
<!-- Other properties... -->
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
<!-- Child resources... -->
</D:multistatus>
Downloading Files
Standard HTTP GET:
GET /webdav/projects/document.pdf HTTP/1.1
Host: your-oxicloud-server
Authorization: Basic [credentials]
Response:
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 12345
Last-Modified: Wed, 15 Nov 2023 12:34:56 GMT
ETag: "abc123"
[File content]
Uploading Files
Use HTTP PUT to upload or update a file:
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
Use the MKCOL method:
MKCOL /webdav/projects/new-folder HTTP/1.1
Host: your-oxicloud-server
Authorization: Basic [credentials]
Returns 201 Created on success.
Moving and Copying
MOVE a resource:
MOVE /webdav/old-location.pdf HTTP/1.1
Host: your-oxicloud-server
Destination: https://your-oxicloud-server/webdav/new-location.pdf
Authorization: Basic [credentials]
COPY a resource:
COPY /webdav/original.pdf HTTP/1.1
Host: your-oxicloud-server
Destination: https://your-oxicloud-server/webdav/copy.pdf
Authorization: Basic [credentials]
Both return 204 No Content on success.
Deleting Resources
Use HTTP DELETE:
DELETE /webdav/projects/document.pdf HTTP/1.1
Host: your-oxicloud-server
Authorization: Basic [credentials]
Returns 204 No Content on success.
XML Schemas
PROPFIND Request
Request all properties:
<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:">
<D:allprop/>
</D:propfind>
Request specific properties:
<?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 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 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:
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
displayname = response_elem.find('.//{DAV:}displayname')
if displayname is not None and displayname.text:
print(f" Name: {displayname.text}")
# 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:
print(f" Size: {contentlength.text} bytes")
# 2. Upload a file
with open('local-file.pdf', 'rb') as f:
file_content = f.read()
response = requests.put(
f'{base_url}/projects/document.pdf',
data=file_content,
auth=auth
)
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
)
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:
// 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: {
...headers,
'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');
if (displayNameElems.length > 0) {
displayName = displayNameElems[0].textContent;
}
// Check if resource is a collection (folder)
const resourceTypeElem = response.getElementsByTagNameNS('DAV:', 'resourcetype')[0];
const isCollection = resourceTypeElem.getElementsByTagNameNS('DAV:', 'collection').length > 0;
// Get file size if it's a file
let size = null;
if (!isCollection) {
const contentLengthElems = response.getElementsByTagNameNS('DAV:', 'getcontentlength');
if (contentLengthElems.length > 0) {
size = parseInt(contentLengthElems[0].textContent, 10);
}
}
resources.push({
href,
displayName,
isCollection,
size
});
}
return resources;
} else {
throw new Error(`Failed to list directory: ${response.status}`);
}
}
// 2. Upload a file
async function uploadFile(path, fileContent) {
const response = await fetch(`${baseUrl}${path}`, {
method: 'PUT',
headers: {
...headers,
'Content-Type': 'application/octet-stream'
},
body: fileContent
});
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
async function downloadFile(path) {
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');
a.href = url;
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);
}
}
// 4. Create a folder
async function createFolder(path) {
const response = await fetch(`${baseUrl}${path}`, {
method: 'MKCOL',
headers
});
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;
}
// 6. Delete a resource
async function deleteResource(path) {
const response = await fetch(`${baseUrl}${path}`, {
method: 'DELETE',
headers
});
return response.status === 204;
}
C# Example
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
class WebDavClient
{
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('/')}");
request.Headers.Add("Depth", "1");
request.Content = new StringContent(
@"<?xml version=""1.0"" encoding=""utf-8"" ?>
<D:propfind xmlns:D=""DAV:"">
<D:allprop/>
</D:propfind>",
Encoding.UTF8,
"application/xml"
);
var response = await _httpClient.SendAsync(request);
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('/')}");
request.Content = new ByteArrayContent(content);
var response = await _httpClient.SendAsync(request);
return response.StatusCode == System.Net.HttpStatusCode.Created ||
response.StatusCode == System.Net.HttpStatusCode.NoContent;
}
public async Task<byte[]> DownloadFileAsync(string path)
{
var response = await _httpClient.GetAsync($"{_baseUrl}/{path.TrimStart('/')}");
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsByteArrayAsync();
}
throw new Exception($"Failed to download file: {response.StatusCode}");
}
public async Task<bool> CreateFolderAsync(string path)
{
var request = new HttpRequestMessage(new HttpMethod("MKCOL"), $"{_baseUrl}/{path.TrimStart('/')}");
var response = await _httpClient.SendAsync(request);
return response.StatusCode == System.Net.HttpStatusCode.Created;
}
public async Task<bool> MoveResourceAsync(string fromPath, string toPath)
{
var request = new HttpRequestMessage(new HttpMethod("MOVE"), $"{_baseUrl}/{fromPath.TrimStart('/')}");
request.Headers.Add("Destination", $"{_baseUrl}/{toPath.TrimStart('/')}");
var response = await _httpClient.SendAsync(request);
return response.StatusCode == System.Net.HttpStatusCode.NoContent;
}
public async Task<bool> DeleteResourceAsync(string path)
{
var response = await _httpClient.DeleteAsync($"{_baseUrl}/{path.TrimStart('/')}");
return response.StatusCode == System.Net.HttpStatusCode.NoContent;
}
}
// Example usage
async Task RunExampleAsync()
{
var client = new WebDavClient("https://your-oxicloud-server", "username", "password");
// List directory
try
{
var directoryListing = await client.ListDirectoryAsync("/projects");
// Process XML results...
Console.WriteLine("Directory listing successful");
}
catch (Exception ex)
{
Console.WriteLine($"Error listing directory: {ex.Message}");
}
// Upload a file
try
{
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
{
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}");
}
}
Extending WebDAV
Adding Custom Properties
To support custom WebDAV properties:
- Define your XML namespace for custom properties.
- Implement storage for them (database table recommended).
- Update the WebDAV adapter to handle these properties.
Example adapter code for custom properties:
// 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);
}
// Property not handled
Ok(false)
}
Supporting CalDAV/CardDAV
To extend with CalDAV/CardDAV support:
- Create additional adapters for calendar and contact data.
- Implement the additional XML namespaces required.
- Create handlers for the specialized methods.
- Integrate with calendar and contacts storage.
See caldav-technical-spec.md and carddav-technical-spec.md for details.
Troubleshooting
Common Issues
- Authentication Failures
- Check credentials are correctly Base64-encoded
- Ensure the Authorization header is formatted correctly
- Verify the user has the necessary permissions