Skip to main content

io_webdav/rfc4791/item/
list.rs

1//! `list-items` coroutine: REPORT `calendar-query` against a calendar
2//! collection.
3//!
4//! Stays byte-oriented: the iCalendar payload is returned as raw bytes
5//! and parsed by io-calendar.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use std::{
11//!     io::{Read, Write},
12//!     net::TcpStream,
13//! };
14//!
15//! use io_webdav::{
16//!     coroutine::{WebdavCoroutine, WebdavCoroutineState, WebdavYield},
17//!     rfc4791::item::list::ListItems,
18//!     rfc4918::WebdavAuth,
19//! };
20//! use url::Url;
21//!
22//! // Ready stream needed (TCP-connected, TLS-negociated)
23//! let mut stream = TcpStream::connect("dav.example.org:443").unwrap();
24//! let mut buf = [0u8; 4096];
25//!
26//! let base_url: Url = "https://dav.example.org/".parse().unwrap();
27//! let auth = WebdavAuth::None;
28//! let mut coroutine = ListItems::new(
29//!     &base_url,
30//!     &auth,
31//!     "io-webdav",
32//!     "/dav/calendars/personal/",
33//!     "<C:comp-filter name=\"VEVENT\" />",
34//! );
35//! let mut arg = None;
36//!
37//! let items = loop {
38//!     match coroutine.resume(arg.take()) {
39//!         WebdavCoroutineState::Yielded(WebdavYield::WantsWrite(bytes)) => {
40//!             stream.write_all(&bytes).unwrap();
41//!         }
42//!         WebdavCoroutineState::Yielded(WebdavYield::WantsRead) => {
43//!             let n = stream.read(&mut buf).unwrap();
44//!             arg = Some(&buf[..n]);
45//!         }
46//!         WebdavCoroutineState::Complete(Ok(items)) => break items,
47//!         WebdavCoroutineState::Complete(Err(err)) => panic!("{err}"),
48//!     }
49//! };
50//!
51//! println!("{} items", items.len());
52//! ```
53
54use alloc::{
55    collections::BTreeSet,
56    string::{String, ToString},
57    vec::Vec,
58};
59
60use log::trace;
61use url::Url;
62
63use crate::{
64    coroutine::*,
65    rfc4791::calendar::{CALENDAR_DATA, calendar_query_body},
66    rfc4918::{
67        GETETAG, WebdavAuth,
68        report::Report,
69        send::SendError,
70        trace_unrecognized, {Property, ResponseEntry},
71    },
72    webdav_try,
73};
74
75const ITEM_PROPS: &[Property] = &[GETETAG, CALENDAR_DATA];
76
77/// Coroutine that lists items inside a calendar via REPORT
78/// `calendar-query`.
79#[derive(Debug)]
80pub struct ListItems {
81    state: State,
82}
83
84impl ListItems {
85    /// Builds a new `list-items` coroutine.
86    ///
87    /// `calendar_path` is the calendar collection path. `comp_filter`
88    /// is the optional VCALENDAR child filter (e.g.
89    /// `<C:comp-filter name="VEVENT" />`); pass an empty string to
90    /// list every component type.
91    pub fn new(
92        base_url: &Url,
93        auth: &WebdavAuth,
94        user_agent: &str,
95        calendar_path: &str,
96        comp_filter: &str,
97    ) -> Self {
98        let body = calendar_query_body(ITEM_PROPS, comp_filter);
99        let report = Report::new(base_url, auth, user_agent, calendar_path, 1, body);
100        Self {
101            state: State::Report(report),
102        }
103    }
104}
105
106impl WebdavCoroutine for ListItems {
107    type Yield = WebdavYield;
108    type Return = Result<BTreeSet<ItemEntry>, SendError>;
109
110    fn resume(&mut self, arg: Option<&[u8]>) -> WebdavCoroutineState<Self::Yield, Self::Return> {
111        trace!("sending request");
112        match &mut self.state {
113            State::Report(report) => {
114                let multistatus = webdav_try!(report, arg);
115                let items = multistatus
116                    .responses
117                    .iter()
118                    .filter_map(from_entry)
119                    .collect();
120                WebdavCoroutineState::Complete(Ok(items))
121            }
122        }
123    }
124}
125
126fn from_entry(entry: &ResponseEntry) -> Option<ItemEntry> {
127    let id = entry.id().trim_end_matches(".ics");
128    if id.is_empty() {
129        return None;
130    }
131
132    let data = entry.text(CALENDAR_DATA)?;
133    trace_unrecognized(entry, ITEM_PROPS);
134
135    let etag = entry
136        .text(GETETAG)
137        .map(|raw| raw.trim_matches('"').to_string());
138
139    Some(ItemEntry {
140        id: id.to_string(),
141        etag,
142        data: data.as_bytes().to_vec(),
143    })
144}
145
146#[derive(Debug)]
147enum State {
148    Report(Report),
149}
150
151/// Raw calendar item entry returned by
152/// [`ListItems`].
153#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
154pub struct ItemEntry {
155    /// Item identifier (last path segment of the href, with `.ics`
156    /// stripped).
157    pub id: String,
158
159    /// Entity tag (RFC 9110 ยง8.8.3), without surrounding quotes.
160    pub etag: Option<String>,
161
162    /// Raw iCalendar bytes (`calendar-data`).
163    pub data: Vec<u8>,
164}