# WebDAV Integration Guide for OxiCloud
This guide provides developers with information on how to interact with OxiCloud's WebDAV interface programmatically and how to extend the WebDAV functionality.
## 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
The WebDAV interface is available at:
```
https://[your-oxicloud-server]/webdav/
```
All file and folder operations are performed under this base path. Resource paths are appended to this 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
OxiCloud's WebDAV interface supports HTTP Basic Authentication. When making requests, include the `Authorization` header with base64-encoded credentials:
```
Authorization: Basic base64(username:password)
```
For security reasons, always use HTTPS when connecting to WebDAV.
## Common Operations
### Listing Directories
To list the contents of a directory, use the `PROPFIND` method with an appropriate `Depth` header:
- `Depth: 0` - Returns information about the resource itself
- `Depth: 1` - Returns information about the resource and its immediate children (recommended)
- `Depth: infinity` - Returns information about the resource and all descendants (use carefully with large directories)
Request:
```http
PROPFIND /webdav/projects/ HTTP/1.1
Host: your-oxicloud-server
Depth: 1
Content-Type: application/xml
Authorization: Basic [credentials]
```
Response:
```http
HTTP/1.1 207 Multi-Status
Content-Type: application/xml; charset=utf-8
/webdav/projects/
projects
HTTP/1.1 200 OK
```
### Downloading Files
To download a file, use the standard HTTP `GET` method:
```http
GET /webdav/projects/document.pdf HTTP/1.1
Host: your-oxicloud-server
Authorization: Basic [credentials]
```
The server will respond with the file content and appropriate headers:
```http
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
To upload or update a file, use the HTTP `PUT` method:
```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]
```
For new files, the server responds with:
```http
HTTP/1.1 201 Created
```
For updated files, the server responds with:
```http
HTTP/1.1 204 No Content
```
### Creating Folders
To create a folder, use the WebDAV `MKCOL` method:
```http
MKCOL /webdav/projects/new-folder HTTP/1.1
Host: your-oxicloud-server
Authorization: Basic [credentials]
```
Successful response:
```http
HTTP/1.1 201 Created
```
### Moving and Copying
To move resources, use the WebDAV `MOVE` method:
```http
MOVE /webdav/old-location.pdf HTTP/1.1
Host: your-oxicloud-server
Destination: https://your-oxicloud-server/webdav/new-location.pdf
Authorization: Basic [credentials]
```
To copy resources, use the WebDAV `COPY` method:
```http
COPY /webdav/original.pdf HTTP/1.1
Host: your-oxicloud-server
Destination: https://your-oxicloud-server/webdav/copy.pdf
Authorization: Basic [credentials]
```
For both operations, a successful response is:
```http
HTTP/1.1 204 No Content
```
### Deleting Resources
To delete a file or folder, use the HTTP `DELETE` method:
```http
DELETE /webdav/projects/document.pdf HTTP/1.1
Host: your-oxicloud-server
Authorization: Basic [credentials]
```
Successful response:
```http
HTTP/1.1 204 No Content
```
## XML Schemas
### PROPFIND Request
Request all properties:
```xml
```
Request specific properties:
```xml
```
### PROPPATCH Request
Set and remove properties:
```xml
Custom Value
```
### LOCK Request
```xml
mailto:user@example.com
```
## 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 = '''
'''
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 browser's `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: {
...headers,
'Depth': '1',
'Content-Type': 'application/xml'
},
body: `
`
});
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
```csharp
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 ListDirectoryAsync(string path)
{
var request = new HttpRequestMessage(new HttpMethod("PROPFIND"), $"{_baseUrl}/{path.TrimStart('/')}");
request.Headers.Add("Depth", "1");
request.Content = new StringContent(
@"
",
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 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 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 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 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 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:
1. Define your XML namespace for custom properties
2. Implement storage for these properties (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 {
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 OxiCloud 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
## Troubleshooting
### Common Issues
1. **Authentication Failures**
- Check credentials are correctly Base64-encoded
- Ensure the Authorization header is formatted correctly
- Verify the user has the necessary permissions
2. **Path Resolution Problems**
- Ensure paths are properly URL-encoded
- Check for leading/trailing slashes as appropriate
- Verify resource exists at the specified path
3. **XML Parsing Errors**
- Validate XML structure against WebDAV specifications
- Ensure proper namespace declarations
- Check for special characters that might need encoding
### Debugging
For debugging WebDAV operations:
1. **Enable debug logging** in OxiCloud configuration
2. **Use WebDAV-specific tools** like:
- cadaver (command-line WebDAV client)
- DAVExplorer (Java-based GUI client)
- Wireshark with HTTP filtering
3. **Check server logs** for detailed error information
### Performance Optimization
To optimize WebDAV performance:
1. **Limit Depth usage** - Avoid "Depth: infinity" for large directories
2. **Use efficient property requests** - Request only needed properties
3. **Consider caching** - Implement client-side caching using ETags
4. **Compress responses** - Enable HTTP compression for WebDAV responses