# 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 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 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 itself - `Depth: 1` -- the resource and its immediate children (recommended) - `Depth: infinity` -- the resource and all descendants (careful with large trees) 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 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 ETag: "abc123" [File content] ``` ### Uploading Files Use HTTP **PUT** to upload or update a file: ```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 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 **MOVE** a resource: ```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] ``` **COPY** a resource: ```http 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**: ```http 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 ``` 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 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: { ...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 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 { 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: 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. See `caldav-technical-spec.md` and `carddav-technical-spec.md` for details. ## 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