Files
Oxicloud/doc/WEBDAV-INTEGRATION-GUIDE.md
T
2025-04-04 21:31:41 +02:00

19 KiB

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
  2. Authentication
  3. Common Operations
  4. XML Schemas
  5. Code Examples
  6. Extending WebDAV
  7. 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:

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

To download a file, use the standard HTTP GET method:

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/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:

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/1.1 201 Created

For updated files, the server responds with:

HTTP/1.1 204 No Content

Creating Folders

To create a folder, use the WebDAV MKCOL method:

MKCOL /webdav/projects/new-folder HTTP/1.1
Host: your-oxicloud-server
Authorization: Basic [credentials]

Successful response:

HTTP/1.1 201 Created

Moving and Copying

To move resources, use the WebDAV MOVE method:

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:

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/1.1 204 No Content

Deleting Resources

To delete a file or folder, use the HTTP DELETE method:

DELETE /webdav/projects/document.pdf HTTP/1.1
Host: your-oxicloud-server
Authorization: Basic [credentials]

Successful response:

HTTP/1.1 204 No Content

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 browser's 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:

  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:

// 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 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