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}