use std::path::Path;
use async_trait::async_trait;
use http::{Request, Response, StatusCode, Uri};
use hyper::body::Incoming;
use libdav::{
CardDavClient,
carddav::{
CreateAddressBook, FindAddressBookHomeSet, FindAddressBooks, GetAddressBookResources,
},
dav::{
CheckSupport, Delete, GetProperties, GetProperty, ListResources, PutResource, SetProperty,
WebDavError, mime_types,
},
};
use tower::Service;
use crate::{
CollectionId, Error, ErrorKind, Etag, Href, ItemKind, Result,
base::{
Collection, CreateItemOptions, FetchedItem, FetchedProperty, Item, ItemVersion, Storage,
},
dav::{
self, collection_href_for_item, collection_id_for_href, name_for_creation,
parse_list_items, path_for_collection_in_home_set,
},
disco::{DiscoveredCollection, Discovery},
property::Property,
};
pub use crate::dav::CollectionIdSegment;
pub struct CardDavStorageBuilder<C>
where
C: Service<Request<String>, Response = Response<Incoming>> + Send + Sync + 'static,
C::Error: std::error::Error + Send + Sync,
C::Future: Send + Sync,
{
client: CardDavClient<C>,
collection_id_segment: CollectionIdSegment,
}
impl<C> CardDavStorageBuilder<C>
where
C: Service<Request<String>, Response = Response<Incoming>> + Send + Sync + 'static,
C::Error: std::error::Error + Send + Sync,
C::Future: Send + Sync,
{
#[must_use]
pub fn with_collection_id_segment(mut self, segment: CollectionIdSegment) -> Self {
self.collection_id_segment = segment;
self
}
pub async fn build(self) -> Result<CardDavStorage<C>> {
let principal = self
.client
.find_current_user_principal()
.await
.map_err(|e| ErrorKind::Io.error(e))?
.ok_or_else(|| ErrorKind::Unavailable.error("no user principal found"))?;
let address_book_home_set = self
.client
.request(FindAddressBookHomeSet::new(&principal))
.await
.map_err(|e| ErrorKind::Io.error(e))?
.home_sets;
Ok(CardDavStorage {
client: self.client,
address_book_home_set,
address_book_id_segment: self.collection_id_segment,
})
}
}
impl<C> CardDavStorage<C>
where
C: Service<Request<String>, Response = Response<Incoming>> + Send + Sync + 'static,
C::Error: std::error::Error + Send + Sync,
C::Future: Send + Sync,
{
#[must_use]
pub fn builder(client: CardDavClient<C>) -> CardDavStorageBuilder<C> {
CardDavStorageBuilder {
client,
collection_id_segment: CollectionIdSegment::default(),
}
}
}
pub struct CardDavStorage<C>
where
C: Service<Request<String>, Response = Response<Incoming>> + Send + Sync + 'static,
C::Error: std::error::Error + Send + Sync,
C::Future: Send + Sync,
{
client: CardDavClient<C>,
address_book_home_set: Vec<Uri>,
address_book_id_segment: CollectionIdSegment,
}
#[async_trait]
impl<C> Storage for CardDavStorage<C>
where
C: Service<Request<String>, Response = Response<Incoming>> + Send + Sync + 'static,
C::Error: std::error::Error + Send + Sync,
C::Future: Send + Sync,
{
fn item_kind(&self) -> ItemKind {
ItemKind::AddressBook
}
async fn check(&self) -> Result<()> {
self.client
.request(CheckSupport::carddav(&self.client.base_url))
.await
.map_err(|e| ErrorKind::Uncategorised.error(e))
}
async fn discover_collections(&self) -> Result<Discovery> {
let mut collections = Vec::new();
for home in &self.address_book_home_set {
let mut response = self
.client
.request(FindAddressBooks::new(home))
.await
.map_err(|e| ErrorKind::Io.error(e))?;
collections.append(&mut response.addressbooks);
}
collections
.into_iter()
.map(|collection| {
collection_id_for_href(&collection.href, self.address_book_id_segment)
.map_err(|e| ErrorKind::InvalidData.error(e))
.map(|id| DiscoveredCollection::new(collection.href, id))
})
.collect::<Result<Vec<_>>>()
.map(Discovery::try_from)?
.map_err(|e| ErrorKind::InvalidData.error(e))
}
async fn create_collection(&self, href: &str) -> Result<Collection> {
self.client
.request(CreateAddressBook::new(href))
.await
.map_err(|e| ErrorKind::Uncategorised.error(e))?;
Ok(Collection::new(href.to_string()))
}
async fn delete_collection(&self, href: &str) -> Result<()> {
let mut results = self
.client
.request(GetAddressBookResources::new(href).with_hrefs([href]))
.await
.map_err(|e| ErrorKind::Uncategorised.error(e))?
.resources;
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(ErrorKind::InvalidData
.error(format!("Requested href: {}, got: {}", href, item.href,)));
}
let etag = item
.content
.map_err(|e| ErrorKind::Uncategorised.error(format!("Got status code: {e}")))?
.etag;
let items = self.list_items(href).await?;
if !items.is_empty() {
return Err(ErrorKind::CollectionNotEmpty.into());
}
self.client
.request(Delete::new(href).with_etag(&etag))
.await
.map_err(|e| ErrorKind::Uncategorised.error(e))?;
Ok(())
}
async fn list_items(&self, collection_href: &str) -> Result<Vec<ItemVersion>> {
let response = self
.client
.request(ListResources::new(collection_href))
.await?
.resources;
parse_list_items(response)
}
async fn get_item(&self, href: &str) -> Result<(Item, Etag)> {
let collection_href = collection_href_for_item(href)?;
let mut results = self
.client
.request(GetAddressBookResources::new(collection_href).with_hrefs([href]))
.await
.map_err(|e| ErrorKind::Uncategorised.error(e))?
.resources;
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(ErrorKind::Uncategorised
.error(format!("Requested href: {}, got: {}", href, item.href,)));
}
let content = item
.content
.map_err(|e| ErrorKind::Uncategorised.error(format!("Got status code: {e}")))?;
Ok((Item::from(content.data), content.etag.into()))
}
async fn get_many_items(&self, hrefs: &[&str]) -> Result<Vec<FetchedItem>> {
let Some(first_href) = hrefs.first() else {
return Ok(Vec::new());
};
let collection_href = collection_href_for_item(first_href)?;
self.client
.request(
GetAddressBookResources::new(collection_href).with_hrefs(hrefs.iter().copied()),
)
.await
.map_err(|e| ErrorKind::Uncategorised.error(e))?
.resources
.into_iter()
.filter_map(|resource| match resource.content {
Ok(content) => Some(Ok(FetchedItem {
href: resource.href,
item: Item::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_href: &str) -> Result<Vec<FetchedItem>> {
let list = self.list_items(collection_href).await?;
let hrefs = list.iter().map(|i| i.href.as_str()).collect::<Vec<_>>();
self.get_many_items(&hrefs).await
}
async fn create_item(
&self,
collection_href: &str,
item: &Item,
opts: CreateItemOptions,
) -> Result<ItemVersion> {
let mut href = name_for_creation(collection_href, item, opts);
if !Path::new(&href)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("vcf"))
{
href.push_str(".vcf");
}
let response = self
.client
.request(PutResource::new(&href).create(item.as_str(), mime_types::ADDRESSBOOK))
.await
.map_err(|e| match e {
WebDavError::PreconditionFailed(p) => ErrorKind::Exists.error(p),
other => Error::from(other),
})?;
let etag = match response.etag {
Some(e) => e,
None => self.get_item(&href).await?.1.to_string(),
};
Ok(ItemVersion::new(href, Etag::from(etag)))
}
async fn update_item(&self, href: &str, etag: &Etag, item: &Item) -> Result<Etag> {
let response = self
.client
.request(PutResource::new(href).update(
item.as_str(),
mime_types::ADDRESSBOOK,
etag.as_str(),
))
.await?;
if let Some(etag) = response.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: Property, value: &str) -> Result<()> {
let propname = dav::dav_propname(prop, self.item_kind())?;
self.client
.request(SetProperty::new(href, propname, Some(value)))
.await
.map(|_| ())
.map_err(Error::from)
}
async fn unset_property(&self, href: &str, prop: Property) -> Result<()> {
let propname = dav::dav_propname(prop, self.item_kind())?;
self.client
.request(SetProperty::new(href, propname, None))
.await
.map(|_| ())
.map_err(Error::from)
}
async fn get_property(&self, href: &str, prop: Property) -> Result<Option<String>> {
let propname = dav::dav_propname(prop, self.item_kind())?;
self.client
.request(GetProperty::new(href, propname))
.await
.map(|r| r.value)
.map_err(Error::from)
}
async fn delete_item(&self, href: &str, etag: &Etag) -> Result<()> {
self.client
.request(Delete::new(href).with_etag(etag.as_str()))
.await?;
Ok(())
}
fn href_for_collection_id(&self, id: &CollectionId) -> Result<Href> {
if let Some(home_set) = &self.address_book_home_set.first() {
Ok(path_for_collection_in_home_set(home_set, id.as_ref()))
} else {
Err(ErrorKind::PreconditionFailed
.error("address book home set not found in carddav server"))
}
}
async fn list_properties(&self, collection_href: &str) -> Result<Vec<FetchedProperty>> {
let properties = Property::known_properties(self.item_kind());
let prop_names = properties
.iter()
.map(|p| dav::dav_propname(*p, self.item_kind()))
.collect::<Result<Vec<_>>>()?;
let result = self
.client
.request(GetProperties::new(collection_href, &prop_names))
.await?
.values
.into_iter()
.zip(properties)
.filter_map(|((_, v), p): ((_, _), &Property)| {
v.map(|value| FetchedProperty {
property: *p,
value,
})
})
.collect::<Vec<_>>();
Ok(result)
}
}