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}