# 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