use async_trait::async_trait;
use http::{StatusCode, Uri};
use hyper_util::client::legacy::connect::Connect;
use libdav::dav::mime_types;
use libdav::sd::BootstrapError;
use libdav::CalDavClient;
use crate::base::{
CalendarProperty, Collection, FetchedItem, IcsItem, Item, ItemRef, ListedProperty, Storage,
};
use crate::dav::{
collection_href_for_item, collection_id_for_href, parse_list_items,
path_for_collection_in_home_set,
};
use crate::disco::{DiscoveredCollection, Discovery};
use crate::vdir::PropertyWithFilename;
use crate::{CollectionId, Error, ErrorKind, Etag, Href, Result};
impl<C> CalDavStorage<C>
where
C: Connect + Send + Sync + Clone + std::fmt::Debug,
{
pub async fn new(client: CalDavClient<C>) -> Result<CalDavStorage<C>> {
let principal = client
.find_current_user_principal()
.await
.map_err(|e| Error::new(ErrorKind::Io, e))?
.ok_or_else(|| Error::new(ErrorKind::Unavailable, "no user principal found"))?;
let calendar_home_set = client
.find_calendar_home_set(&principal)
.await
.map_err(|e| Error::new(ErrorKind::Io, e))?;
Ok(CalDavStorage {
client,
calendar_home_set,
})
}
}
impl From<BootstrapError> for Error {
fn from(value: BootstrapError) -> Self {
Error::new(ErrorKind::Uncategorised, value)
}
}
impl From<libdav::dav::WebDavError> for Error {
fn from(value: libdav::dav::WebDavError) -> Self {
Error::new(ErrorKind::Uncategorised, value)
}
}
pub struct CalDavStorage<C: Connect + Clone + Sync + Send + 'static> {
client: CalDavClient<C>,
calendar_home_set: Vec<Uri>,
}
#[async_trait]
impl<C> Storage<IcsItem> for CalDavStorage<C>
where
C: Connect + Sync + Send + Clone,
{
async fn check(&self) -> Result<()> {
self.client
.check_support(&self.client.base_url)
.await
.map_err(|e| Error::new(ErrorKind::Uncategorised, e))
}
async fn discover_collections(&self) -> Result<Discovery> {
let mut collections = Vec::new();
for home in &self.calendar_home_set {
collections.append(&mut self.client.find_calendars(home).await?);
}
collections
.into_iter()
.map(|collection| {
collection_id_for_href(&collection.href)
.map_err(|e| Error::new(ErrorKind::InvalidData, e))
.map(|id| DiscoveredCollection::new(collection.href, id))
})
.collect::<Result<Vec<_>>>()
.map(Discovery::from)
}
async fn create_collection(&self, href: &str) -> Result<Collection> {
self.client
.create_calendar(href)
.await
.map_err(|e| Error::new(ErrorKind::Uncategorised, e))?;
Ok(Collection::new(href.to_string()))
}
async fn destroy_collection(&self, href: &str) -> Result<()> {
let mut results = self
.client
.get_calendar_resources(href, &[href])
.await
.map_err(|e| Error::new(ErrorKind::Uncategorised, e))?;
if results.len() > 1 {
return Err(ErrorKind::InvalidData.into());
}
let item = results
.pop()
.ok_or_else(|| Error::from(ErrorKind::InvalidData))?;
if item.href != href {
return Err(Error::new(
ErrorKind::InvalidData,
format!("Requested href: {}, got: {}", href, item.href,),
));
}
let etag = item
.content
.map_err(|e| Error::new(ErrorKind::Uncategorised, format!("Got status code: {e}")))?
.etag;
let items = self.list_items(href).await?;
if !items.is_empty() {
return Err(ErrorKind::CollectionNotEmpty.into());
}
self.client
.delete(href, etag)
.await
.map_err(|e| Error::new(ErrorKind::Uncategorised, e))?;
Ok(())
}
async fn list_items(&self, collection_href: &str) -> Result<Vec<ItemRef>> {
let response = self.client.list_resources(collection_href).await?;
parse_list_items(response)
}
async fn get_item(&self, href: &str) -> Result<(IcsItem, Etag)> {
let collection_href = collection_href_for_item(href)?;
let mut results = self
.client
.get_calendar_resources(collection_href, &[href])
.await
.map_err(|e| Error::new(ErrorKind::Uncategorised, e))?;
if results.len() != 1 {
return Err(ErrorKind::InvalidData.into());
}
let item = results.pop().expect("results has exactly one item");
if item.href != href {
return Err(Error::new(
ErrorKind::Uncategorised,
format!("Requested href: {}, got: {}", href, item.href,),
));
}
let content = item
.content
.map_err(|e| Error::new(ErrorKind::Uncategorised, format!("Got status code: {e}")))?;
Ok((IcsItem::from(content.data), content.etag.into()))
}
async fn get_many_items(&self, hrefs: &[&str]) -> Result<Vec<FetchedItem<IcsItem>>> {
if hrefs.is_empty() {
return Ok(Vec::new());
}
let collection_href = collection_href_for_item(hrefs[0])?;
self.client
.get_calendar_resources(collection_href, hrefs)
.await
.map_err(|e| Error::new(ErrorKind::Uncategorised, e))?
.into_iter()
.filter_map(|resource| match resource.content {
Ok(content) => Some(Ok(FetchedItem {
href: resource.href,
item: IcsItem::from(content.data),
etag: content.etag.into(),
})),
Err(StatusCode::NOT_FOUND) => None,
Err(e) => Some(Err(
ErrorKind::Io.error(format!("Got status code {} for {}", e, resource.href))
)),
})
.collect()
}
async fn get_all_items(&self, collection: &str) -> Result<Vec<FetchedItem<IcsItem>>> {
let list = self.list_items(collection).await?;
let hrefs = list.iter().map(|i| i.href.as_str()).collect::<Vec<_>>();
self.get_many_items(&hrefs).await
}
async fn add_item(&self, collection_href: &str, item: &IcsItem) -> Result<ItemRef> {
let href = join_hrefs(collection_href, &item.ident());
let response = self
.client
.create_resource(
&href,
item.as_str().as_bytes().to_vec(),
mime_types::CALENDAR,
)
.await?;
let etag = match response {
Some(e) => e,
None => self.get_item(&href).await?.1.to_string(),
};
Ok(ItemRef {
href,
etag: Etag::from(etag),
})
}
async fn update_item(&self, href: &str, etag: &Etag, item: &IcsItem) -> Result<Etag> {
let raw_etag = self
.client
.update_resource(
href,
item.as_str().as_bytes().to_vec(),
etag,
mime_types::CALENDAR,
)
.await?;
if let Some(etag) = raw_etag {
return Ok(Etag::from(etag));
}
let (new_item, etag) = self.get_item(href).await?;
if new_item.hash() == item.hash() {
return Ok(etag);
}
return Err(ErrorKind::Io.error("Item was overwritten replaced before reading Etag"));
}
async fn set_property(&self, href: &str, prop: CalendarProperty, value: &str) -> Result<()> {
self.client
.set_property(href, prop.dav_propname(), Some(value))
.await
.map(|_| ())
.map_err(Error::from)
}
async fn unset_property(&self, href: &str, prop: CalendarProperty) -> Result<()> {
self.client
.set_property(href, prop.dav_propname(), None)
.await
.map(|_| ())
.map_err(Error::from)
}
async fn get_property(&self, href: &str, prop: CalendarProperty) -> Result<Option<String>> {
self.client
.get_property(href, prop.dav_propname())
.await
.map_err(Error::from)
}
async fn delete_item(&self, href: &str, etag: &Etag) -> Result<()> {
self.client.delete(href, etag).await?;
Ok(())
}
fn collection_id(&self, collection_href: &str) -> Result<CollectionId> {
collection_id_for_href(collection_href).map_err(|e| Error::new(ErrorKind::InvalidInput, e))
}
fn href_for_collection_id(&self, id: &CollectionId) -> Result<Href> {
if let Some(home_set) = &self.calendar_home_set.first() {
Ok(path_for_collection_in_home_set(home_set, id.as_ref()))
} else {
Err(Error::new(
ErrorKind::PreconditionFailed,
"calendar home set not found in caldav server",
))
}
}
async fn list_properties(
&self,
collection_href: &str,
) -> Result<Vec<ListedProperty<CalendarProperty>>> {
let prop_names = CalendarProperty::known_properties()
.iter()
.map(|p| p.dav_propname())
.collect::<Vec<_>>();
let result = self
.client
.get_properties(collection_href, &prop_names)
.await?
.into_iter()
.zip(CalendarProperty::known_properties())
.filter_map(|((_, v), p)| {
v.map(|value| ListedProperty {
property: p.clone(),
value,
})
})
.collect::<Vec<_>>();
return Ok(result);
}
}
fn join_hrefs(collection_href: &str, item_href: &str) -> String {
if item_href.starts_with('/') {
return item_href.to_string();
}
let mut href = collection_href
.strip_suffix('/')
.unwrap_or(collection_href)
.to_string();
href.push('/');
href.push_str(item_href);
href
}
#[cfg(test)]
mod test {
use hyper_rustls::HttpsConnectorBuilder;
use libdav::{auth::Auth, dav::WebDavClient, CalDavClient};
use crate::{base::Storage, caldav::CalDavStorage};
#[test]
fn test_collection_id() {
let test_client = {
let https = HttpsConnectorBuilder::new()
.with_native_roots()
.unwrap()
.https_or_http()
.enable_http1()
.build();
let base_url = "https://example.com".parse().unwrap();
let webdav = WebDavClient::new(base_url, Auth::None, https);
let client = CalDavClient::new(webdav);
CalDavStorage {
client,
calendar_home_set: Vec::new(),
}
};
let samples = &[
("/path/to/collection/", "collection"),
("/path/to/collection", "collection"),
("/path/to//collection/", "collection"),
("/path/to/collection//", "collection"),
("path/to/collection", "collection"),
("/", ""),
];
for (input, output) in samples {
let collection_id = test_client.collection_id(input).unwrap();
assert_eq!(collection_id, output.parse().unwrap());
}
}
}