fast_dav_rs/lib.rs
1//! Fast CalDAV client library for Rust.
2//!
3//! This library provides a high-performance, asynchronous CalDAV client built on modern
4//! Rust ecosystem components including hyper 1.x, rustls, and tokio.
5//!
6//! # Features
7//!
8//! - HTTP/2 multiplexing and connection pooling
9//! - Automatic response decompression (br/zstd/gzip)
10//! - Streaming-friendly APIs for large WebDAV responses
11//! - Batch operations with bounded concurrency
12//! - ETag helpers for safe conditional writes/deletes
13//! - Streaming XML parsing with minimal memory footprint
14//!
15//! # Examples
16//!
17//! ## Basic Setup and Calendar Discovery
18//!
19//! ```no_run
20//! use fast_dav_rs::{CalDavClient, Depth};
21//! use anyhow::Result;
22//!
23//! #[tokio::main]
24//! async fn main() -> Result<()> {
25//! let client = CalDavClient::new(
26//! "https://caldav.example.com/user/",
27//! Some("username"),
28//! Some("password"),
29//! )?;
30//!
31//! // Discover the current user's principal
32//! let principal = client.discover_current_user_principal().await?
33//! .ok_or_else(|| anyhow::anyhow!("No principal found"))?;
34//!
35//! // Find calendar home sets
36//! let homes = client.discover_calendar_home_set(&principal).await?;
37//! let home = homes.first().ok_or_else(|| anyhow::anyhow!("No calendar home found"))?;
38//!
39//! // List all calendars
40//! let calendars = client.list_calendars(home).await?;
41//! for calendar in &calendars {
42//! println!("Found calendar: {:?}", calendar.displayname);
43//! }
44//!
45//! Ok(())
46//! }
47//! ```
48//!
49//! ## Calendar Operations
50//!
51//! ```no_run
52//! use fast_dav_rs::{CalDavClient, Depth};
53//! use bytes::Bytes;
54//! use anyhow::Result;
55//!
56//! #[tokio::main]
57//! async fn main() -> Result<()> {
58//! let client = CalDavClient::new(
59//! "https://caldav.example.com/user/",
60//! Some("username"),
61//! Some("password"),
62//! )?;
63//!
64//! # let home = ""; // Placeholder for calendar home path
65//! // Create a new calendar
66//! let calendar_xml = r#"<?xml version="1.0" encoding="UTF-8"?>
67//! <C:mkcalendar xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
68//! <D:set>
69//! <D:prop>
70//! <D:displayname>My New Calendar</D:displayname>
71//! <C:calendar-description>Calendar created with fast-dav-rs</C:calendar-description>
72//! </D:prop>
73//! </D:set>
74//! </C:mkcalendar>"#;
75//!
76//! let response = client.mkcalendar("my-new-calendar/", calendar_xml).await?;
77//! println!("Created calendar with status: {}", response.status());
78//!
79//! // Delete a calendar
80//! let delete_response = client.delete("my-new-calendar/").await?;
81//! println!("Deleted calendar with status: {}", delete_response.status());
82//!
83//! Ok(())
84//! }
85//! ```
86//!
87//! ## Event Operations
88//!
89//! ```no_run
90//! use fast_dav_rs::{CalDavClient, Depth};
91//! use bytes::Bytes;
92//! use anyhow::Result;
93//!
94//! #[tokio::main]
95//! async fn main() -> Result<()> {
96//! let client = CalDavClient::new(
97//! "https://caldav.example.com/user/",
98//! Some("username"),
99//! Some("password"),
100//! )?;
101//!
102//! # let calendar_path = ""; // Placeholder for calendar path
103//! // Create a new event
104//! let event_ics = Bytes::from(r#"BEGIN:VCALENDAR
105//! VERSION:2.0
106//! PRODID:-//fast-dav-rs//EN
107//! BEGIN:VEVENT
108//! UID:event-123@example.com
109//! DTSTAMP:20230101T000000Z
110//! DTSTART:20231225T100000Z
111//! DTEND:20231225T110000Z
112//! SUMMARY:Christmas Day Event
113//! DESCRIPTION:Celebrate Christmas with family
114//! END:VEVENT
115//! END:VCALENDAR
116//! "#);
117//!
118//! // Safe creation with If-None-Match to prevent overwriting
119//! let response = client.put_if_none_match("my-calendar/christmas-event.ics", event_ics).await?;
120//! println!("Created event with status: {}", response.status());
121//!
122//! // Query events in a date range
123//! let events = client.calendar_query_timerange(
124//! "my-calendar/",
125//! "VEVENT",
126//! Some("20231201T000000Z"), // Start date
127//! Some("20231231T235959Z"), // End date
128//! true // Include event data
129//! ).await?;
130//!
131//! for event in &events {
132//! println!("Event: {:?}, ETag: {:?}", event.href, event.etag);
133//! }
134//!
135//! // Update an existing event (if we found one)
136//! if let Some(first_event) = events.first() {
137//! if let Some(etag) = &first_event.etag {
138//! let updated_ics = Bytes::from(format!(r#"BEGIN:VCALENDAR
139//! VERSION:2.0
140//! PRODID:-//fast-dav-rs//EN
141//! BEGIN:VEVENT
142//! UID:event-123@example.com
143//! DTSTAMP:20230102T000000Z
144//! DTSTART:20231225T100000Z
145//! DTEND:20231225T120000Z // Extended end time
146//! SUMMARY:Christmas Day Event (Extended)
147//! DESCRIPTION:Celebrate Christmas with extended family
148//! END:VEVENT
149//! END:VCALENDAR
150//! "#));
151//!
152//! // Safe update with If-Match
153//! let update_response = client.put_if_match(
154//! &first_event.href,
155//! updated_ics,
156//! etag
157//! ).await?;
158//! println!("Updated event with status: {}", update_response.status());
159//! }
160//! }
161//!
162//! // Delete an event (using conditional delete for safety)
163//! if let Some(first_event) = events.first() {
164//! if let Some(etag) = &first_event.etag {
165//! let delete_response = client.delete_if_match(&first_event.href, etag).await?;
166//! println!("Deleted event with status: {}", delete_response.status());
167//! }
168//! }
169//!
170//! Ok(())
171//! }
172//! ```
173//!
174//! ## Working with ETags for Safe Operations
175//!
176//! ```no_run
177//! use fast_dav_rs::CalDavClient;
178//! use bytes::Bytes;
179//! use anyhow::Result;
180//!
181//! #[tokio::main]
182//! async fn main() -> Result<()> {
183//! let client = CalDavClient::new(
184//! "https://caldav.example.com/user/",
185//! Some("username"),
186//! Some("password"),
187//! )?;
188//!
189//! # let event_path = ""; // Placeholder
190//! // Get current ETag before modifying a resource
191//! let head_response = client.head("my-calendar/some-event.ics").await?;
192//! if let Some(etag) = CalDavClient::etag_from_headers(head_response.headers()) {
193//! // Now we can safely update with If-Match
194//! let updated_ics = Bytes::from(r#"BEGIN:VCALENDAR
195//! VERSION:2.0
196//! PRODID:-//fast-dav-rs//EN
197//! BEGIN:VEVENT
198//! UID:some-event@example.com
199//! DTSTAMP:20230101T000000Z
200//! DTSTART:20231225T100000Z
201//! DTEND:20231225T110000Z
202//! SUMMARY:Updated Event Title
203//! END:VEVENT
204//! END:VCALENDAR
205//! "#);
206//!
207//! let response = client.put_if_match("my-calendar/some-event.ics", updated_ics, &etag).await?;
208//! if response.status().is_success() {
209//! println!("Successfully updated event");
210//! } else {
211//! println!("Failed to update event: {}", response.status());
212//! }
213//! }
214//!
215//! Ok(())
216//! }
217//! ```
218//!
219//! ## Streaming Large Responses
220//!
221//! For processing large collections without loading everything into memory:
222//!
223//! ```no_run
224//! use fast_dav_rs::{CalDavClient, Depth, parse_multistatus_stream, detect_encoding};
225//! use bytes::Bytes;
226//! use anyhow::Result;
227//!
228//! #[tokio::main]
229//! async fn main() -> Result<()> {
230//! let client = CalDavClient::new(
231//! "https://caldav.example.com/user/",
232//! Some("username"),
233//! Some("password"),
234//! )?;
235//!
236//! # let calendar_path = ""; // Placeholder
237//! // Stream a large PROPFIND response
238//! let propfind_xml = r#"
239//! <D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
240//! <D:prop>
241//! <D:displayname/>
242//! <D:getetag/>
243//! <C:calendar-data/>
244//! </D:prop>
245//! </D:propfind>"#;
246//!
247//! let response = client.propfind_stream("large-calendar/", Depth::One, propfind_xml).await?;
248//! let encodings = detect_encodings(response.headers());
249//! let items = parse_multistatus_stream(response.into_body(), &encodings).await?;
250//!
251//! // Process items one by one without loading everything into memory
252//! for item in items {
253//! println!("Found item: {} with etag: {:?}",
254//! item.displayname.unwrap_or_default(),
255//! item.etag);
256//!
257//! // Process calendar data if present
258//! if let Some(data) = item.calendar_data {
259//! // Handle large iCalendar data efficiently
260//! println!("Processing calendar data of length: {}", data.len());
261//! }
262//! }
263//!
264//! Ok(())
265//! }
266//! ```
267//!
268//! ## Concurrent Batch Operations
269//!
270//! Execute multiple operations concurrently with controlled parallelism:
271//!
272//! ```no_run
273//! use fast_dav_rs::{CalDavClient, Depth};
274//! use bytes::Bytes;
275//! use std::sync::Arc;
276//! use anyhow::Result;
277//!
278//! #[tokio::main]
279//! async fn main() -> Result<()> {
280//! let client = CalDavClient::new(
281//! "https://caldav.example.com/user/",
282//! Some("username"),
283//! Some("password"),
284//! )?;
285//!
286//! # let calendar_paths = vec!["".to_string()]; // Placeholder
287//! // Prepare a common PROPFIND request for multiple calendars
288//! let propfind_body = Arc::new(Bytes::from(r#"
289//! <D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
290//! <D:prop>
291//! <D:displayname/>
292//! <D:getetag/>
293//! <C:supported-calendar-component-set/>
294//! </D:prop>
295//! </D:propfind>"#));
296//!
297//! // Execute PROPFIND on multiple calendars concurrently (max 5 in parallel)
298//! let results = client.propfind_many(
299//! calendar_paths, // Vector of calendar paths
300//! Depth::Zero,
301//! propfind_body,
302//! 5 // Maximum concurrency
303//! ).await;
304//!
305//! // Process results in the same order as input
306//! for result in results {
307//! match result.result {
308//! Ok(response) => {
309//! if response.status().is_success() {
310//! println!("Successfully queried: {}", result.pub_path);
311//! // Parse response body as needed
312//! } else {
313//! println!("Query failed for {}: {}", result.pub_path, response.status());
314//! }
315//! }
316//! Err(e) => {
317//! println!("Error querying {}: {}", result.pub_path, e);
318//! }
319//! }
320//! }
321//!
322//! // Batch event updates
323//! let event_updates = vec![
324//! ("event1.ics", "BEGIN:VCALENDAR...END:VCALENDAR"),
325//! ("event2.ics", "BEGIN:VCALENDAR...END:VCALENDAR"),
326//! ];
327//!
328//! # let calendar_path = ""; // Placeholder
329//! // Upload multiple events concurrently
330//! let mut upload_tasks = Vec::new();
331//! for (filename, ical_data) in event_updates {
332//! let client_clone = client.clone();
333//! let path = format!("{}/{}", calendar_path, filename);
334//! let data = Bytes::from(ical_data);
335//!
336//! upload_tasks.push(tokio::spawn(async move {
337//! client_clone.put(&path, data).await
338//! }));
339//! }
340//!
341//! // Wait for all uploads to complete
342//! let upload_results = futures::future::join_all(upload_tasks).await;
343//! for (i, result) in upload_results.into_iter().enumerate() {
344//! match result {
345//! Ok(Ok(response)) => {
346//! println!("Upload {} completed with status: {}", i, response.status());
347//! }
348//! Ok(Err(e)) => {
349//! println!("Upload {} failed with error: {}", i, e);
350//! }
351//! Err(e) => {
352//! println!("Upload {} panicked: {}", i, e);
353//! }
354//! }
355//! }
356//!
357//! Ok(())
358//! }
359//! ```
360//!
361//! ## Bootstrap and Capability Detection
362//!
363//! Discover server capabilities and choose appropriate synchronization methods:
364//!
365//! ```no_run
366//! use fast_dav_rs::{CalDavClient, Depth};
367//! use anyhow::Result;
368//!
369//! #[tokio::main]
370//! async fn main() -> Result<()> {
371//! let client = CalDavClient::new(
372//! "https://caldav.example.com/user/",
373//! Some("username"),
374//! Some("password"),
375//! )?;
376//!
377//! // Bootstrap: Discover server capabilities
378//! println!("Detecting server capabilities...");
379//!
380//! // Check if server supports WebDAV-Sync (RFC 6578)
381//! let has_sync_support = client.supports_webdav_sync().await?;
382//! println!("WebDAV-Sync support: {}", has_sync_support);
383//!
384//! // Discover user principal
385//! let principal = client.discover_current_user_principal().await?
386//! .ok_or_else(|| anyhow::anyhow!("No principal found"))?;
387//! println!("User principal: {}", principal);
388//!
389//! // Discover calendar homes
390//! let homes = client.discover_calendar_home_set(&principal).await?;
391//! let home = homes.first().ok_or_else(|| anyhow::anyhow!("No calendar home found"))?;
392//! println!("Calendar home: {}", home);
393//!
394//! // List calendars with detailed info
395//! let calendars = client.list_calendars(home).await?;
396//! for calendar in &calendars {
397//! println!("Calendar: {} (sync token: {:?})",
398//! calendar.displayname.as_deref().unwrap_or("unnamed"),
399//! calendar.sync_token.as_ref().map(|s| &s[..20]));
400//! }
401//!
402//! // Choose synchronization strategy based on capabilities
403//! if has_sync_support && !calendars.is_empty() {
404//! println!("Using efficient WebDAV-Sync for synchronization");
405//! sync_with_webdav_sync(&client, &calendars[0]).await?;
406//! } else {
407//! println!("Using traditional polling for synchronization");
408//! sync_with_polling(&client, home).await?;
409//! }
410//!
411//! Ok(())
412//! }
413//!
414//! /// Efficient synchronization using WebDAV-Sync
415//! async fn sync_with_webdav_sync(client: &CalDavClient, calendar: &fast_dav_rs::CalendarInfo) -> Result<()> {
416//! let mut sync_token = calendar.sync_token.clone();
417//!
418//! loop {
419//! println!("Syncing with token: {:?}", sync_token.as_ref().map(|s| &s[..20]));
420//!
421//! // Perform incremental sync
422//! let sync_response = client.sync_collection(
423//! &calendar.href,
424//! sync_token.as_deref(),
425//! Some(100), // Limit results
426//! true // Include data
427//! ).await?;
428//!
429//! println!("Received {} updates", sync_response.items.len());
430//!
431//! // Process changes
432//! for item in &sync_response.items {
433//! if item.is_deleted {
434//! println!("Deleted: {}", item.href);
435//! } else if let Some(data) = &item.calendar_data {
436//! println!("Updated: {} ({} chars)", item.href, data.len());
437//! } else {
438//! println!("Changed: {} (no data)", item.href);
439//! }
440//! }
441//!
442//! // Update sync token for next iteration
443//! sync_token = sync_response.sync_token;
444//!
445//! // Break if no more changes or implement your own exit condition
446//! if sync_response.items.is_empty() {
447//! break;
448//! }
449//!
450//! // In a real application, you'd probably want to sleep between syncs
451//! // tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
452//! }
453//!
454//! Ok(())
455//! }
456//!
457//! /// Traditional synchronization using polling
458//! async fn sync_with_polling(client: &CalDavClient, calendar_home: &str) -> Result<()> {
459//! // Get all calendars
460//! let calendars = client.list_calendars(calendar_home).await?;
461//!
462//! for calendar in calendars {
463//! println!("Polling calendar: {:?}", calendar.displayname);
464//!
465//! // Query recent events (example with fixed dates)
466//! let start = "20240101T000000Z";
467//! let end = "20240201T000000Z";
468//!
469//! let events = client.calendar_query_timerange(
470//! &calendar.href,
471//! "VEVENT",
472//! Some(&start),
473//! Some(&end),
474//! true // Include data
475//! ).await?;
476//!
477//! println!("Found {} events in {}", events.len(), calendar.displayname.unwrap_or_default());
478//!
479//! // Process events (in a real app, you'd compare with local cache)
480//! for event in events {
481//! if let Some(data) = event.calendar_data {
482//! println!("Event: {} ({})", event.href, data.lines().next().unwrap_or(""));
483//! }
484//! }
485//! }
486//!
487//! Ok(())
488//! }
489//! ```
490pub mod caldav;
491pub mod common;
492
493// Backwards-compatible re-exports
494pub use caldav::streaming::{
495 parse_multistatus_bytes, parse_multistatus_bytes_visit, parse_multistatus_stream,
496 parse_multistatus_stream_visit,
497};
498pub use caldav::{
499 BatchItem, CalDavClient, CalendarInfo, CalendarObject, DavItem, Depth, SyncItem, SyncResponse,
500 build_calendar_multiget_body, build_calendar_query_body, build_sync_collection_body,
501 map_calendar_list, map_calendar_objects, map_sync_response,
502};
503pub use common::compression::{
504 ContentEncoding, add_accept_encoding, add_content_encoding, compress_payload, detect_encoding,
505 detect_encodings, detect_request_compression_preference,
506};
507
508// Legacy module paths kept for compatibility with existing imports.
509pub mod client {
510 pub use crate::caldav::client::*;
511}
512
513pub mod streaming {
514 pub use crate::caldav::streaming::*;
515}
516
517pub mod types {
518 pub use crate::caldav::types::*;
519}
520
521pub mod compression {
522 pub use crate::common::compression::*;
523}