Crate fast_dav_rs

Crate fast_dav_rs 

Source
Expand description

Fast CalDAV client library for Rust.

This library provides a high-performance, asynchronous CalDAV client built on modern Rust ecosystem components including hyper 1.x, rustls, and tokio.

§Features

  • HTTP/2 multiplexing and connection pooling
  • Automatic response decompression (br/zstd/gzip)
  • Streaming-friendly APIs for large WebDAV responses
  • Batch operations with bounded concurrency
  • ETag helpers for safe conditional writes/deletes
  • Streaming XML parsing with minimal memory footprint

§Examples

§Basic Setup and Calendar Discovery

use fast_dav_rs::{CalDavClient, Depth};
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
    let client = CalDavClient::new(
        "https://caldav.example.com/user/",
        Some("username"),
        Some("password"),
    )?;

    // Discover the current user's principal
    let principal = client.discover_current_user_principal().await?
        .ok_or_else(|| anyhow::anyhow!("No principal found"))?;
     
    // Find calendar home sets
    let homes = client.discover_calendar_home_set(&principal).await?;
    let home = homes.first().ok_or_else(|| anyhow::anyhow!("No calendar home found"))?;
     
    // List all calendars
    let calendars = client.list_calendars(home).await?;
    for calendar in &calendars {
        println!("Found calendar: {:?}", calendar.displayname);
    }
     
    Ok(())
}

§Calendar Operations

use fast_dav_rs::{CalDavClient, Depth};
use bytes::Bytes;
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
    let client = CalDavClient::new(
        "https://caldav.example.com/user/",
        Some("username"),
        Some("password"),
    )?;
     
    // Create a new calendar
    let calendar_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
    <C:mkcalendar xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
      <D:set>
        <D:prop>
          <D:displayname>My New Calendar</D:displayname>
          <C:calendar-description>Calendar created with fast-dav-rs</C:calendar-description>
        </D:prop>
      </D:set>
    </C:mkcalendar>"#;
     
    let response = client.mkcalendar("my-new-calendar/", calendar_xml).await?;
    println!("Created calendar with status: {}", response.status());
     
    // Delete a calendar
    let delete_response = client.delete("my-new-calendar/").await?;
    println!("Deleted calendar with status: {}", delete_response.status());
     
    Ok(())
}

§Event Operations

use fast_dav_rs::{CalDavClient, Depth};
use bytes::Bytes;
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
    let client = CalDavClient::new(
        "https://caldav.example.com/user/",
        Some("username"),
        Some("password"),
    )?;
     
    // Create a new event
    let event_ics = Bytes::from(r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//fast-dav-rs//EN
BEGIN:VEVENT
UID:event-123@example.com
DTSTAMP:20230101T000000Z
DTSTART:20231225T100000Z
DTEND:20231225T110000Z
SUMMARY:Christmas Day Event
DESCRIPTION:Celebrate Christmas with family
END:VEVENT
END:VCALENDAR
"#);
     
    // Safe creation with If-None-Match to prevent overwriting
    let response = client.put_if_none_match("my-calendar/christmas-event.ics", event_ics).await?;
    println!("Created event with status: {}", response.status());
     
    // Query events in a date range
    let events = client.calendar_query_timerange(
        "my-calendar/",
        "VEVENT",
        Some("20231201T000000Z"),  // Start date
        Some("20231231T235959Z"),  // End date
        true  // Include event data
    ).await?;
     
    for event in &events {
        println!("Event: {:?}, ETag: {:?}", event.href, event.etag);
    }
     
    // Update an existing event (if we found one)
    if let Some(first_event) = events.first() {
        if let Some(etag) = &first_event.etag {
            let updated_ics = Bytes::from(format!(r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//fast-dav-rs//EN
BEGIN:VEVENT
UID:event-123@example.com
DTSTAMP:20230102T000000Z
DTSTART:20231225T100000Z
DTEND:20231225T120000Z  // Extended end time
SUMMARY:Christmas Day Event (Extended)
DESCRIPTION:Celebrate Christmas with extended family
END:VEVENT
END:VCALENDAR
"#));
             
            // Safe update with If-Match
            let update_response = client.put_if_match(
                &first_event.href,
                updated_ics,
                etag
            ).await?;
            println!("Updated event with status: {}", update_response.status());
        }
    }
     
    // Delete an event (using conditional delete for safety)
    if let Some(first_event) = events.first() {
        if let Some(etag) = &first_event.etag {
            let delete_response = client.delete_if_match(&first_event.href, etag).await?;
            println!("Deleted event with status: {}", delete_response.status());
        }
    }
     
    Ok(())
}

§Working with ETags for Safe Operations

use fast_dav_rs::CalDavClient;
use bytes::Bytes;
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
    let client = CalDavClient::new(
        "https://caldav.example.com/user/",
        Some("username"),
        Some("password"),
    )?;
     
    // Get current ETag before modifying a resource
    let head_response = client.head("my-calendar/some-event.ics").await?;
    if let Some(etag) = CalDavClient::etag_from_headers(head_response.headers()) {
        // Now we can safely update with If-Match
        let updated_ics = Bytes::from(r#"BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//fast-dav-rs//EN
BEGIN:VEVENT
UID:some-event@example.com
DTSTAMP:20230101T000000Z
DTSTART:20231225T100000Z
DTEND:20231225T110000Z
SUMMARY:Updated Event Title
END:VEVENT
END:VCALENDAR
"#);
         
        let response = client.put_if_match("my-calendar/some-event.ics", updated_ics, &etag).await?;
        if response.status().is_success() {
            println!("Successfully updated event");
        } else {
            println!("Failed to update event: {}", response.status());
        }
    }
     
    Ok(())
}

§Streaming Large Responses

For processing large collections without loading everything into memory:

use fast_dav_rs::{CalDavClient, Depth, parse_multistatus_stream, detect_encoding};
use bytes::Bytes;
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
    let client = CalDavClient::new(
        "https://caldav.example.com/user/",
        Some("username"),
        Some("password"),
    )?;
     
    // Stream a large PROPFIND response
    let propfind_xml = r#"
    <D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
      <D:prop>
        <D:displayname/>
        <D:getetag/>
        <C:calendar-data/>
      </D:prop>
    </D:propfind>"#;
     
    let response = client.propfind_stream("large-calendar/", Depth::One, propfind_xml).await?;
    let encodings = detect_encodings(response.headers());
    let items = parse_multistatus_stream(response.into_body(), &encodings).await?;
     
    // Process items one by one without loading everything into memory
    for item in items {
        println!("Found item: {} with etag: {:?}",
                 item.displayname.unwrap_or_default(),
                 item.etag);
         
        // Process calendar data if present
        if let Some(data) = item.calendar_data {
            // Handle large iCalendar data efficiently
            println!("Processing calendar data of length: {}", data.len());
        }
    }
     
    Ok(())
}

§Concurrent Batch Operations

Execute multiple operations concurrently with controlled parallelism:

use fast_dav_rs::{CalDavClient, Depth};
use bytes::Bytes;
use std::sync::Arc;
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
    let client = CalDavClient::new(
        "https://caldav.example.com/user/",
        Some("username"),
        Some("password"),
    )?;
     
    // Prepare a common PROPFIND request for multiple calendars
    let propfind_body = Arc::new(Bytes::from(r#"
    <D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
      <D:prop>
        <D:displayname/>
        <D:getetag/>
        <C:supported-calendar-component-set/>
      </D:prop>
    </D:propfind>"#));
     
    // Execute PROPFIND on multiple calendars concurrently (max 5 in parallel)
    let results = client.propfind_many(
        calendar_paths,  // Vector of calendar paths
        Depth::Zero,
        propfind_body,
        5  // Maximum concurrency
    ).await;
     
    // Process results in the same order as input
    for result in results {
        match result.result {
            Ok(response) => {
                if response.status().is_success() {
                    println!("Successfully queried: {}", result.pub_path);
                    // Parse response body as needed
                } else {
                    println!("Query failed for {}: {}", result.pub_path, response.status());
                }
            }
            Err(e) => {
                println!("Error querying {}: {}", result.pub_path, e);
            }
        }
    }
     
    // Batch event updates
    let event_updates = vec![
        ("event1.ics", "BEGIN:VCALENDAR...END:VCALENDAR"),
        ("event2.ics", "BEGIN:VCALENDAR...END:VCALENDAR"),
    ];
     
    // Upload multiple events concurrently
    let mut upload_tasks = Vec::new();
    for (filename, ical_data) in event_updates {
        let client_clone = client.clone();
        let path = format!("{}/{}", calendar_path, filename);
        let data = Bytes::from(ical_data);
         
        upload_tasks.push(tokio::spawn(async move {
            client_clone.put(&path, data).await
        }));
    }
     
    // Wait for all uploads to complete
    let upload_results = futures::future::join_all(upload_tasks).await;
    for (i, result) in upload_results.into_iter().enumerate() {
        match result {
            Ok(Ok(response)) => {
                println!("Upload {} completed with status: {}", i, response.status());
            }
            Ok(Err(e)) => {
                println!("Upload {} failed with error: {}", i, e);
            }
            Err(e) => {
                println!("Upload {} panicked: {}", i, e);
            }
        }
    }
     
    Ok(())
}

§Bootstrap and Capability Detection

Discover server capabilities and choose appropriate synchronization methods:

use fast_dav_rs::{CalDavClient, Depth};
use anyhow::Result;

#[tokio::main]
async fn main() -> Result<()> {
    let client = CalDavClient::new(
        "https://caldav.example.com/user/",
        Some("username"),
        Some("password"),
    )?;
     
    // Bootstrap: Discover server capabilities
    println!("Detecting server capabilities...");
     
    // Check if server supports WebDAV-Sync (RFC 6578)
    let has_sync_support = client.supports_webdav_sync().await?;
    println!("WebDAV-Sync support: {}", has_sync_support);
     
    // Discover user principal
    let principal = client.discover_current_user_principal().await?
        .ok_or_else(|| anyhow::anyhow!("No principal found"))?;
    println!("User principal: {}", principal);
     
    // Discover calendar homes
    let homes = client.discover_calendar_home_set(&principal).await?;
    let home = homes.first().ok_or_else(|| anyhow::anyhow!("No calendar home found"))?;
    println!("Calendar home: {}", home);
     
    // List calendars with detailed info
    let calendars = client.list_calendars(home).await?;
    for calendar in &calendars {
        println!("Calendar: {} (sync token: {:?})",
                 calendar.displayname.as_deref().unwrap_or("unnamed"),
                 calendar.sync_token.as_ref().map(|s| &s[..20]));
    }
     
    // Choose synchronization strategy based on capabilities
    if has_sync_support && !calendars.is_empty() {
        println!("Using efficient WebDAV-Sync for synchronization");
        sync_with_webdav_sync(&client, &calendars[0]).await?;
    } else {
        println!("Using traditional polling for synchronization");
        sync_with_polling(&client, home).await?;
    }
     
    Ok(())
}

/// Efficient synchronization using WebDAV-Sync
async fn sync_with_webdav_sync(client: &CalDavClient, calendar: &fast_dav_rs::CalendarInfo) -> Result<()> {
    let mut sync_token = calendar.sync_token.clone();
     
    loop {
        println!("Syncing with token: {:?}", sync_token.as_ref().map(|s| &s[..20]));
         
        // Perform incremental sync
        let sync_response = client.sync_collection(
            &calendar.href,
            sync_token.as_deref(),
            Some(100), // Limit results
            true // Include data
        ).await?;
         
        println!("Received {} updates", sync_response.items.len());
         
        // Process changes
        for item in &sync_response.items {
            if item.is_deleted {
                println!("Deleted: {}", item.href);
            } else if let Some(data) = &item.calendar_data {
                println!("Updated: {} ({} chars)", item.href, data.len());
            } else {
                println!("Changed: {} (no data)", item.href);
            }
        }
         
        // Update sync token for next iteration
        sync_token = sync_response.sync_token;
         
        // Break if no more changes or implement your own exit condition
        if sync_response.items.is_empty() {
            break;
        }
         
        // In a real application, you'd probably want to sleep between syncs
        // tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
    }
     
    Ok(())
}

/// Traditional synchronization using polling
async fn sync_with_polling(client: &CalDavClient, calendar_home: &str) -> Result<()> {
    // Get all calendars
    let calendars = client.list_calendars(calendar_home).await?;
     
    for calendar in calendars {
        println!("Polling calendar: {:?}", calendar.displayname);
         
        // Query recent events (example with fixed dates)
        let start = "20240101T000000Z";
        let end = "20240201T000000Z";
         
        let events = client.calendar_query_timerange(
            &calendar.href,
            "VEVENT",
            Some(&start),
            Some(&end),
            true // Include data
        ).await?;
         
        println!("Found {} events in {}", events.len(), calendar.displayname.unwrap_or_default());
         
        // Process events (in a real app, you'd compare with local cache)
        for event in events {
            if let Some(data) = event.calendar_data {
                println!("Event: {} ({})", event.href, data.lines().next().unwrap_or(""));
            }
        }
    }
     
    Ok(())
}

Re-exports§

pub use caldav::streaming::parse_multistatus_bytes;
pub use caldav::streaming::parse_multistatus_bytes_visit;
pub use caldav::streaming::parse_multistatus_stream;
pub use caldav::streaming::parse_multistatus_stream_visit;
pub use caldav::BatchItem;
pub use caldav::CalDavClient;
pub use caldav::CalendarInfo;
pub use caldav::CalendarObject;
pub use caldav::DavItem;
pub use caldav::Depth;
pub use caldav::SyncItem;
pub use caldav::SyncResponse;
pub use caldav::build_calendar_multiget_body;
pub use caldav::build_calendar_query_body;
pub use caldav::build_sync_collection_body;
pub use caldav::map_calendar_list;
pub use caldav::map_calendar_objects;
pub use caldav::map_sync_response;
pub use common::compression::ContentEncoding;
pub use common::compression::add_accept_encoding;
pub use common::compression::add_content_encoding;
pub use common::compression::compress_payload;
pub use common::compression::detect_encoding;
pub use common::compression::detect_encodings;
pub use common::compression::detect_request_compression_preference;

Modules§

caldav
client
common
compression
streaming
types