Files
Oxicloud/doc/dav-integration.md
T
2026-04-01 23:14:42 +02:00

23 KiB

23 - DAV Integration

WebDAV, CalDAV, and CardDAV extend the platform to support clients and devices that speak these standard protocols. The implementation follows the existing hexagonal architecture -- each protocol gets its own adapter, service, and domain layer.

Table of Contents

  1. Introduction
  2. Implementation Architecture
  3. WebDAV
  4. CalDAV
  5. CardDAV
  6. Security Considerations
  7. Testing and Compatibility

Introduction

WebDAV (Web Distributed Authoring and Versioning)

An HTTP extension that lets clients create, modify, move, and delete files and directories on a remote server.

CalDAV (Calendaring Extensions to WebDAV)

A WebDAV-based protocol for accessing and managing calendar data (events and tasks).

CardDAV (vCard Extensions to WebDAV)

Extends WebDAV to allow access and management of contact data in vCard format.

Implementation Architecture

DAV protocols plug into the existing hexagonal architecture:

┌────────────────────────────────────────────────────────────────────┐
│                          INTERFACES                                │
│                                                                    │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────────────┐  │
│  │               │  │               │  │                       │  │
│  │  REST API     │  │  WebDAV API   │  │  CalDAV/CardDAV API   │  │
│  │               │  │               │  │                       │  │
│  └───────┬───────┘  └───────┬───────┘  └───────────┬───────────┘  │
│          │                  │                      │              │
└──────────┼──────────────────┼──────────────────────┼──────────────┘
           │                  │                      │
           ▼                  ▼                      ▼
┌──────────────────────────────────────────────────────────────────┐
│                          APPLICATION                             │
│                                                                  │
│  ┌───────────┐  ┌────────────┐  ┌───────────┐  ┌──────────────┐ │
│  │           │  │            │  │           │  │              │ │
│  │FileService│  │FolderService│  │CalendarSvc│  │ContactService│ │
│  │           │  │            │  │           │  │              │ │
│  └─────┬─────┘  └──────┬─────┘  └─────┬─────┘  └──────┬───────┘ │
│        │               │              │               │         │
└────────┼───────────────┼──────────────┼───────────────┼─────────┘
         │               │              │               │
         ▼               ▼              ▼               ▼
┌────────────────────────────────────────────────────────────────┐
│                          DOMAIN                                │
│                                                                │
│  ┌─────────┐  ┌──────────┐  ┌────────────┐  ┌───────────────┐ │
│  │         │  │          │  │            │  │               │ │
│  │  File   │  │  Folder  │  │  Calendar  │  │    Contact    │ │
│  │         │  │          │  │            │  │               │ │
│  └─────────┘  └──────────┘  └────────────┘  └───────────────┘ │
│                                                                │
└────────────────────────────────────────────────────────────────┘

Main Components

  1. DAV Adapters -- convert between DAV specs and internal models
  2. Application Services -- extended to include DAV-specific functionality
  3. Domain Models -- new entities for Calendar and Contact
  4. Repositories -- storage implementations for calendars and contacts

WebDAV

Required Endpoints

HTTP Method Endpoint Description
OPTIONS /webdav/{path} Reports supported WebDAV capabilities
PROPFIND /webdav/{path} Retrieves resource properties
PROPPATCH /webdav/{path} Modifies resource properties
MKCOL /webdav/{path} Creates collections (directories)
GET /webdav/{path} Retrieves resource content
HEAD /webdav/{path} Retrieves resource metadata
PUT /webdav/{path} Creates or updates resources
DELETE /webdav/{path} Deletes resources
COPY /webdav/{path} Copies resources
MOVE /webdav/{path} Moves resources
LOCK /webdav/{path} Locks resources
UNLOCK /webdav/{path} Unlocks resources

Implementation

  1. WebDAV Handler:
// src/interfaces/api/handlers/webdav_handler.rs
use std::sync::Arc;
use axum::{
    Router,
    routing::get,
    extract::{Path, State, Request, Extension},
    http::StatusCode,
    response::Response,
};
use http::{Method, header};

use crate::common::di::AppState;
use crate::interfaces::middleware::auth::CurrentUser;
use crate::application::ports::file_ports::{FileRetrievalUseCase, FileUploadUseCase};
use crate::application::ports::folder_ports::FolderUseCase;
use crate::common::errors::AppError;

pub fn webdav_routes() -> Router<Arc<AppState>> {
    Router::new()
        .route("/webdav/*path", get(handle_get))
        .route_with_tsr("/webdav/*path", axum::routing::on(
            Method::OPTIONS, handle_options,
            Method::PROPFIND, handle_propfind,
            Method::PROPPATCH, handle_proppatch,
            Method::MKCOL, handle_mkcol,
            Method::PUT, handle_put,
            Method::DELETE, handle_delete,
            Method::COPY, handle_copy,
            Method::MOVE, handle_move,
            Method::LOCK, handle_lock,
            Method::UNLOCK, handle_unlock,
        ))
}

// Implement functions for each WebDAV method...
  1. WebDAV Adapter:
// src/application/adapters/webdav_adapter.rs
use xml::reader::{EventReader, XmlEvent};
use xml::writer::{EventWriter, EmitterConfig, XmlEvent as WriteEvent};
use std::io::{Read, Write};
use crate::application::dtos::file_dto::FileDto;
use crate::application::dtos::folder_dto::FolderDto;

/// Converts between internal objects and WebDAV representations
pub struct WebDavAdapter;

impl WebDavAdapter {
    /// Parses a PROPFIND XML property into a request object
    pub fn parse_propfind<R: Read>(reader: R) -> Result<PropFindRequest, Error> {
        // Implementation...
    }

    /// Generates PROPFIND XML response from files and folders
    pub fn generate_propfind_response<W: Write>(
        writer: W,
        files: &[FileDto],
        folders: &[FolderDto],
        base_url: &str,
    ) -> Result<(), Error> {
        // Implementation...
    }

    // Other methods for different WebDAV operations...
}

CalDAV

Required Endpoints

HTTP Method Endpoint Description
PROPFIND /caldav/{calendar} Retrieves calendar properties
REPORT /caldav/{calendar} Queries calendar events
MKCALENDAR /caldav/{calendar} Creates a new calendar
PUT /caldav/{calendar}/{event}.ics Creates or updates an event
GET /caldav/{calendar}/{event}.ics Retrieves an event
DELETE /caldav/{calendar}/{event}.ics Deletes an event

Implementation

  1. Domain Entities:
// src/domain/entities/calendar.rs
use uuid::Uuid;
use chrono::{DateTime, Utc};

#[derive(Debug, Clone)]
pub struct Calendar {
    id: Uuid,
    name: String,
    owner_id: String,
    description: Option<String>,
    color: Option<String>,
    created_at: DateTime<Utc>,
    updated_at: DateTime<Utc>,
}

// src/domain/entities/calendar_event.rs
use uuid::Uuid;
use chrono::{DateTime, Utc};

#[derive(Debug, Clone)]
pub struct CalendarEvent {
    id: Uuid,
    calendar_id: Uuid,
    summary: String,
    description: Option<String>,
    location: Option<String>,
    start_time: DateTime<Utc>,
    end_time: DateTime<Utc>,
    all_day: bool,
    rrule: Option<String>,  // Recurrence rule
    ical_data: String,      // Full iCalendar data
    created_at: DateTime<Utc>,
    updated_at: DateTime<Utc>,
}
  1. Repositories:
// src/domain/repositories/calendar_repository.rs
use async_trait::async_trait;
use uuid::Uuid;
use crate::domain::entities::calendar::Calendar;
use crate::common::errors::Result;

#[async_trait]
pub trait CalendarRepository: Send + Sync {
    async fn create_calendar(&self, calendar: Calendar) -> Result<Calendar>;
    async fn get_calendar_by_id(&self, id: &Uuid) -> Result<Calendar>;
    async fn get_calendars_by_owner(&self, owner_id: &str) -> Result<Vec<Calendar>>;
    async fn update_calendar(&self, calendar: Calendar) -> Result<Calendar>;
    async fn delete_calendar(&self, id: &Uuid) -> Result<()>;
}

// src/domain/repositories/calendar_event_repository.rs
use async_trait::async_trait;
use uuid::Uuid;
use chrono::{DateTime, Utc};
use crate::domain::entities::calendar_event::CalendarEvent;
use crate::common::errors::Result;

#[async_trait]
pub trait CalendarEventRepository: Send + Sync {
    async fn create_event(&self, event: CalendarEvent) -> Result<CalendarEvent>;
    async fn get_event_by_id(&self, id: &Uuid) -> Result<CalendarEvent>;
    async fn get_events_by_calendar(&self, calendar_id: &Uuid) -> Result<Vec<CalendarEvent>>;
    async fn get_events_in_timerange(
        &self,
        calendar_id: &Uuid,
        start: &DateTime<Utc>,
        end: &DateTime<Utc>
    ) -> Result<Vec<CalendarEvent>>;
    async fn update_event(&self, event: CalendarEvent) -> Result<CalendarEvent>;
    async fn delete_event(&self, id: &Uuid) -> Result<()>;
}
  1. CalDAV Service:
// src/application/services/calendar_service.rs
use std::sync::Arc;
use crate::application::ports::calendar_ports::CalendarStoragePort;
use crate::application::dtos::calendar_dto::*;

pub struct CalendarService {
    storage: Arc<dyn CalendarStoragePort>,
}

impl CalendarService {
    pub fn new(storage: Arc<dyn CalendarStoragePort>) -> Self {
        Self { storage }
    }

    // Implements CalendarUseCase for calendar and event operations...
}
  1. CalDAV Handler:
// src/interfaces/api/handlers/caldav_handler.rs
use std::sync::Arc;
use axum::{
    Router,
    routing::{get, put, delete},
    extract::{Path, State, Request, Extension},
    http::StatusCode,
    response::Response,
};
use http::{Method, header};

use crate::common::di::AppState;
use crate::interfaces::middleware::auth::CurrentUser;
use crate::application::services::caldav_service::CalDavService;
use crate::common::errors::AppError;

pub fn caldav_routes() -> Router<Arc<AppState>> {
    Router::new()
        .route("/caldav/", get(get_calendars))
        .route("/caldav/:calendar", get(get_calendar))
        .route_with_tsr("/caldav/:calendar", axum::routing::on(
            Method::PROPFIND, handle_calendar_propfind,
            Method::REPORT, handle_calendar_report,
            Method::MKCALENDAR, handle_mkcalendar,
        ))
        .route("/caldav/:calendar/:event", get(get_event))
        .route("/caldav/:calendar/:event", put(put_event))
        .route("/caldav/:calendar/:event", delete(delete_event))
}

// Implement functions for each CalDAV method...

CardDAV

Required Endpoints

HTTP Method Endpoint Description
PROPFIND /carddav/addressbooks/{addressbook} Retrieves address book properties
REPORT /carddav/addressbooks/{addressbook} Queries contacts
MKCOL /carddav/addressbooks/{addressbook} Creates a new address book
PUT /carddav/addressbooks/{addressbook}/{contact}.vcf Creates or updates a contact
GET /carddav/addressbooks/{addressbook}/{contact}.vcf Retrieves a contact
DELETE /carddav/addressbooks/{addressbook}/{contact}.vcf Deletes a contact

Implementation

  1. Domain Entities:
// Note: AddressBook and Contact are both defined in src/domain/entities/contact.rs

#[derive(Debug, Clone)]
pub struct AddressBook {
    id: Uuid,
    name: String,
    owner_id: String,
    description: Option<String>,
    color: Option<String>,
    is_public: bool,
    created_at: DateTime<Utc>,
    updated_at: DateTime<Utc>,
}

#[derive(Debug, Clone)]
pub struct Contact {
    id: Uuid,
    address_book_id: Uuid,
    uid: String,
    full_name: Option<String>,
    first_name: Option<String>,
    last_name: Option<String>,
    nickname: Option<String>,
    email: Vec<Email>,        // Struct with email, type, is_primary
    phone: Vec<Phone>,        // Struct with number, type, is_primary
    address: Vec<Address>,    // Struct with street, city, state, postal_code, country, type, is_primary
    organization: Option<String>,
    title: Option<String>,
    notes: Option<String>,
    photo_url: Option<String>,
    birthday: Option<NaiveDate>,
    anniversary: Option<NaiveDate>,
    vcard: String,            // Full vCard data
    etag: String,
    created_at: DateTime<Utc>,
    updated_at: DateTime<Utc>,
}
  1. Repositories:
// src/domain/repositories/address_book_repository.rs
use async_trait::async_trait;
use uuid::Uuid;
use crate::domain::entities::address_book::AddressBook;
use crate::common::errors::Result;

#[async_trait]
pub trait AddressBookRepository: Send + Sync {
    async fn create_address_book(&self, address_book: AddressBook) -> Result<AddressBook>;
    async fn get_address_book_by_id(&self, id: &Uuid) -> Result<AddressBook>;
    async fn get_address_books_by_owner(&self, owner_id: &str) -> Result<Vec<AddressBook>>;
    async fn update_address_book(&self, address_book: AddressBook) -> Result<AddressBook>;
    async fn delete_address_book(&self, id: &Uuid) -> Result<()>;
}

// src/domain/repositories/contact_repository.rs
use async_trait::async_trait;
use uuid::Uuid;
use crate::domain::entities::contact::Contact;
use crate::common::errors::Result;

#[async_trait]
pub trait ContactRepository: Send + Sync {
    async fn create_contact(&self, contact: Contact) -> Result<Contact>;
    async fn get_contact_by_id(&self, id: &Uuid) -> Result<Contact>;
    async fn get_contacts_by_address_book(&self, address_book_id: &Uuid) -> Result<Vec<Contact>>;
    async fn search_contacts(&self, address_book_id: &Uuid, query: &str) -> Result<Vec<Contact>>;
    async fn update_contact(&self, contact: Contact) -> Result<Contact>;
    async fn delete_contact(&self, id: &Uuid) -> Result<()>;
}
  1. CardDAV Service:
// src/application/services/contact_service.rs
use std::sync::Arc;
use crate::application::dtos::contact_dto::*;
use crate::application::dtos::address_book_dto::*;

pub struct ContactService {
    // Implements AddressBookUseCase and ContactUseCase
    // Uses ContactStorageAdapter as infrastructure
}

impl ContactService {
    // Implements methods for CardDAV operations...
}
  1. CardDAV Handler:
// src/interfaces/api/handlers/carddav_handler.rs
use std::sync::Arc;
use axum::{
    Router,
    routing::{get, put, delete},
    extract::{Path, State, Request, Extension},
    http::StatusCode,
    response::Response,
};
use http::{Method, header};

use crate::common::di::AppState;
use crate::interfaces::middleware::auth::CurrentUser;
use crate::application::services::carddav_service::CardDavService;
use crate::common::errors::AppError;

pub fn carddav_routes() -> Router<Arc<AppState>> {
    Router::new()
        .route("/carddav/addressbooks/", get(get_address_books))
        .route("/carddav/addressbooks/:addressbook", get(get_address_book))
        .route_with_tsr("/carddav/addressbooks/:addressbook", axum::routing::on(
            Method::PROPFIND, handle_addressbook_propfind,
            Method::REPORT, handle_addressbook_report,
            Method::MKCOL, handle_mkaddressbook,
        ))
        .route("/carddav/addressbooks/:addressbook/:contact", get(get_contact))
        .route("/carddav/addressbooks/:addressbook/:contact", put(put_contact))
        .route("/carddav/addressbooks/:addressbook/:contact", delete(delete_contact))
}

// Implement functions for each CardDAV method...

Database Schema

-- CalDAV schema
CREATE SCHEMA IF NOT EXISTS caldav;

CREATE TABLE IF NOT EXISTS caldav.calendars (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    owner_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    description TEXT,
    color VARCHAR(50),
    is_public BOOLEAN NOT NULL DEFAULT FALSE,
    ctag VARCHAR(255),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS caldav.calendar_events (
    id UUID PRIMARY KEY,
    calendar_id UUID NOT NULL REFERENCES caldav.calendars(id) ON DELETE CASCADE,
    summary VARCHAR(255) NOT NULL,
    description TEXT,
    location TEXT,
    start_time TIMESTAMPTZ NOT NULL,
    end_time TIMESTAMPTZ NOT NULL,
    all_day BOOLEAN NOT NULL DEFAULT FALSE,
    rrule TEXT,
    ical_uid VARCHAR(255) NOT NULL,
    ical_data TEXT NOT NULL,
    etag VARCHAR(255),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS caldav.calendar_shares (
    id SERIAL PRIMARY KEY,
    calendar_id UUID NOT NULL REFERENCES caldav.calendars(id) ON DELETE CASCADE,
    user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    access_level VARCHAR(20) NOT NULL DEFAULT 'read',
    UNIQUE(calendar_id, user_id)
);

CREATE TABLE IF NOT EXISTS caldav.calendar_properties (
    id SERIAL PRIMARY KEY,
    calendar_id UUID NOT NULL REFERENCES caldav.calendars(id) ON DELETE CASCADE,
    property_name VARCHAR(255) NOT NULL,
    property_value TEXT,
    UNIQUE(calendar_id, property_name)
);

-- CardDAV schema
CREATE SCHEMA IF NOT EXISTS carddav;

CREATE TABLE IF NOT EXISTS carddav.address_books (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    owner_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    description TEXT,
    color VARCHAR(50),
    is_public BOOLEAN NOT NULL DEFAULT FALSE,
    ctag VARCHAR(255),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(owner_id, name)
);

CREATE TABLE IF NOT EXISTS carddav.contacts (
    id UUID PRIMARY KEY,
    address_book_id UUID NOT NULL REFERENCES carddav.address_books(id) ON DELETE CASCADE,
    uid VARCHAR(255) NOT NULL,
    full_name VARCHAR(255),
    first_name VARCHAR(255),
    last_name VARCHAR(255),
    nickname VARCHAR(255),
    organization VARCHAR(255),
    title VARCHAR(255),
    notes TEXT,
    photo_url TEXT,
    birthday DATE,
    anniversary DATE,
    email JSONB,
    phone JSONB,
    address JSONB,
    vcard TEXT NOT NULL,
    etag VARCHAR(255) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE(address_book_id, uid)
);

CREATE TABLE IF NOT EXISTS carddav.address_book_shares (
    id SERIAL PRIMARY KEY,
    address_book_id UUID NOT NULL REFERENCES carddav.address_books(id) ON DELETE CASCADE,
    user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    can_write BOOLEAN NOT NULL DEFAULT FALSE,
    UNIQUE(address_book_id, user_id)
);

CREATE TABLE IF NOT EXISTS carddav.contact_groups (
    id UUID PRIMARY KEY,
    address_book_id UUID NOT NULL REFERENCES carddav.address_books(id) ON DELETE CASCADE,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS carddav.group_memberships (
    id SERIAL PRIMARY KEY,
    group_id UUID NOT NULL REFERENCES carddav.contact_groups(id) ON DELETE CASCADE,
    contact_id UUID NOT NULL REFERENCES carddav.contacts(id) ON DELETE CASCADE,
    UNIQUE(group_id, contact_id)
);

-- Indexes for efficient lookup
CREATE INDEX IF NOT EXISTS idx_calendars_owner ON caldav.calendars(owner_id);
CREATE INDEX IF NOT EXISTS idx_calendar_events_calendar ON caldav.calendar_events(calendar_id);
CREATE INDEX IF NOT EXISTS idx_address_books_owner ON carddav.address_books(owner_id);
CREATE INDEX IF NOT EXISTS idx_contacts_address_book ON carddav.contacts(address_book_id);
CREATE INDEX IF NOT EXISTS idx_contacts_full_name ON carddav.contacts(full_name);

Security Considerations

  1. Authentication

    • Uses existing authentication
    • Supports HTTP Basic Authentication for DAV clients
    • Digest authentication can be added if needed
  2. Authorization

    • Verify user permissions before granting resource access
    • Owner-based and shared-permission access control
    • Users can only access their own calendars and address books
  3. Attack Prevention

    • Validate and sanitize all XML input
    • Limit maximum payload size
    • Rate limiting on DAV endpoints

Testing and Compatibility

Clients to Test

  1. WebDAV

    • Windows Explorer
    • macOS Finder
    • Cyberduck
    • FileZilla (with WebDAV extension)
  2. CalDAV

    • Apple Calendar
    • Mozilla Thunderbird (Lightning)
    • Microsoft Outlook (with CalDAV add-in)
    • Google Calendar (via sync)
  3. CardDAV

    • Apple Contacts
    • Mozilla Thunderbird
    • Microsoft Outlook (with CardDAV add-in)
    • Google Contacts (via sync)

Compliance Testing

  • Use the CalDAVTester suite to verify standards conformance
  • Validate RFC compliance for each protocol
  • Stress tests to evaluate performance under load

Debugging

  • Detailed logging for DAV operations
  • Diagnostic tools for debugging complex DAV requests
  • Clear error messages for troubleshooting