CardDAV working with Thunderbird and Apple Contacts
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
use common::storage::index::{
|
||||
IndexItem, IndexValue, IndexableAndSerializableObject, IndexableObject,
|
||||
};
|
||||
use jmap_proto::types::value::AclGrant;
|
||||
use jmap_proto::types::{collection::Collection, value::AclGrant};
|
||||
use store::SerializeInfallible;
|
||||
|
||||
use crate::{IDX_CARD_UID, IDX_NAME};
|
||||
@@ -89,6 +89,10 @@ impl IndexableObject for ContactCard {
|
||||
+ self.size,
|
||||
},
|
||||
IndexValue::LogChild { prefix: None },
|
||||
IndexValue::LogParent {
|
||||
collection: Collection::AddressBook.into(),
|
||||
ids: self.names.iter().map(|n| n.parent_id).collect(),
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
@@ -116,6 +120,10 @@ impl IndexableObject for &ArchivedContactCard {
|
||||
+ self.size,
|
||||
},
|
||||
IndexValue::LogChild { prefix: None },
|
||||
IndexValue::LogParent {
|
||||
collection: Collection::AddressBook.into(),
|
||||
ids: self.names.iter().map(|n| n.parent_id.to_native()).collect(),
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
pub mod index;
|
||||
pub mod storage;
|
||||
|
||||
use calcard::vcard::VCard;
|
||||
|
||||
|
||||
231
crates/groupware/src/contact/storage.rs
Normal file
231
crates/groupware/src/contact/storage.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
|
||||
use jmap_proto::types::collection::Collection;
|
||||
use store::write::{Archive, BatchBuilder, now};
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::DestroyArchive;
|
||||
|
||||
use super::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard};
|
||||
|
||||
impl ContactCard {
|
||||
pub fn update<'x>(
|
||||
self,
|
||||
access_token: &AccessToken,
|
||||
card: Archive<&ArchivedContactCard>,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
batch: &'x mut BatchBuilder,
|
||||
) -> trc::Result<&'x mut BatchBuilder> {
|
||||
let mut new_card = self;
|
||||
|
||||
// Build card
|
||||
new_card.modified = now() as i64;
|
||||
|
||||
// Prepare write batch
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::ContactCard)
|
||||
.update_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(card)
|
||||
.with_changes(new_card)
|
||||
.with_tenant_id(access_token),
|
||||
)
|
||||
.map(|b| b.commit_point())
|
||||
}
|
||||
|
||||
pub fn insert<'x>(
|
||||
self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
batch: &'x mut BatchBuilder,
|
||||
) -> trc::Result<&'x mut BatchBuilder> {
|
||||
// Build card
|
||||
let mut card = self;
|
||||
let now = now() as i64;
|
||||
card.modified = now;
|
||||
card.created = now;
|
||||
|
||||
// Prepare write batch
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::ContactCard)
|
||||
.create_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::<(), _>::new()
|
||||
.with_changes(card)
|
||||
.with_tenant_id(access_token),
|
||||
)
|
||||
.map(|b| b.commit_point())
|
||||
}
|
||||
}
|
||||
|
||||
impl AddressBook {
|
||||
pub fn insert<'x>(
|
||||
self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
batch: &'x mut BatchBuilder,
|
||||
) -> trc::Result<&'x mut BatchBuilder> {
|
||||
// Build address book
|
||||
let mut book = self;
|
||||
let now = now() as i64;
|
||||
book.modified = now;
|
||||
book.created = now;
|
||||
|
||||
// Prepare write batch
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::AddressBook)
|
||||
.create_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::<(), _>::new()
|
||||
.with_changes(book)
|
||||
.with_tenant_id(access_token),
|
||||
)
|
||||
.map(|b| b.commit_point())
|
||||
}
|
||||
|
||||
pub fn update<'x>(
|
||||
self,
|
||||
access_token: &AccessToken,
|
||||
book: Archive<&ArchivedAddressBook>,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
batch: &'x mut BatchBuilder,
|
||||
) -> trc::Result<&'x mut BatchBuilder> {
|
||||
// Build address book
|
||||
let mut new_book = self;
|
||||
new_book.modified = now() as i64;
|
||||
|
||||
// Prepare write batch
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::AddressBook)
|
||||
.update_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(book)
|
||||
.with_changes(new_book)
|
||||
.with_tenant_id(access_token),
|
||||
)
|
||||
.map(|b| b.commit_point())
|
||||
}
|
||||
}
|
||||
|
||||
impl DestroyArchive<Archive<&ArchivedAddressBook>> {
|
||||
pub async fn delete_with_cards(
|
||||
self,
|
||||
server: &Server,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
children_ids: Vec<u32>,
|
||||
batch: &mut BatchBuilder,
|
||||
) -> trc::Result<()> {
|
||||
// Process deletions
|
||||
let addressbook_id = document_id;
|
||||
for document_id in children_ids {
|
||||
if let Some(card_) = server
|
||||
.get_archive(account_id, Collection::ContactCard, document_id)
|
||||
.await?
|
||||
{
|
||||
DestroyArchive(
|
||||
card_
|
||||
.to_unarchived::<ContactCard>()
|
||||
.caused_by(trc::location!())?,
|
||||
)
|
||||
.delete(
|
||||
access_token,
|
||||
account_id,
|
||||
document_id,
|
||||
addressbook_id,
|
||||
batch,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.delete(access_token, account_id, document_id, batch)
|
||||
}
|
||||
|
||||
pub fn delete(
|
||||
self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
batch: &mut BatchBuilder,
|
||||
) -> trc::Result<()> {
|
||||
let book = self.0;
|
||||
// Delete addressbook
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::AddressBook)
|
||||
.delete_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::<_, ()>::new()
|
||||
.with_tenant_id(access_token)
|
||||
.with_current(book),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl DestroyArchive<Archive<&ArchivedContactCard>> {
|
||||
pub fn delete(
|
||||
self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
addressbook_id: u32,
|
||||
batch: &mut BatchBuilder,
|
||||
) -> trc::Result<()> {
|
||||
let card = self.0;
|
||||
if let Some(delete_idx) = card
|
||||
.inner
|
||||
.names
|
||||
.iter()
|
||||
.position(|name| name.parent_id == addressbook_id)
|
||||
{
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::ContactCard);
|
||||
|
||||
if card.inner.names.len() > 1 {
|
||||
// Unlink addressbook id from card
|
||||
let mut new_card = card
|
||||
.deserialize::<ContactCard>()
|
||||
.caused_by(trc::location!())?;
|
||||
new_card.names.swap_remove(delete_idx);
|
||||
batch
|
||||
.update_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_tenant_id(access_token)
|
||||
.with_current(card)
|
||||
.with_changes(new_card),
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
} else {
|
||||
// Delete card
|
||||
batch
|
||||
.delete_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::<_, ()>::new()
|
||||
.with_tenant_id(access_token)
|
||||
.with_current(card),
|
||||
)
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
batch.commit_point();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
pub mod index;
|
||||
|
||||
pub mod storage;
|
||||
|
||||
use dav_proto::schema::request::DeadProperty;
|
||||
use jmap_proto::types::value::AclGrant;
|
||||
|
||||
133
crates/groupware/src/file/storage.rs
Normal file
133
crates/groupware/src/file/storage.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
|
||||
use jmap_proto::types::collection::Collection;
|
||||
use store::write::{Archive, BatchBuilder, now};
|
||||
use trc::AddContext;
|
||||
|
||||
use crate::DestroyArchive;
|
||||
|
||||
use super::{ArchivedFileNode, FileNode};
|
||||
|
||||
impl FileNode {
|
||||
pub fn insert<'x>(
|
||||
self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
batch: &'x mut BatchBuilder,
|
||||
) -> trc::Result<&'x mut BatchBuilder> {
|
||||
// Build node
|
||||
let mut node = self;
|
||||
let now = now() as i64;
|
||||
node.modified = now;
|
||||
node.created = now;
|
||||
|
||||
// Prepare write batch
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::FileNode)
|
||||
.create_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::<(), _>::new()
|
||||
.with_changes(node)
|
||||
.with_tenant_id(access_token),
|
||||
)
|
||||
.map(|b| b.commit_point())
|
||||
}
|
||||
pub fn update<'x>(
|
||||
self,
|
||||
access_token: &AccessToken,
|
||||
node: Archive<&ArchivedFileNode>,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
batch: &'x mut BatchBuilder,
|
||||
) -> trc::Result<&'x mut BatchBuilder> {
|
||||
// Build node
|
||||
let mut new_node = self;
|
||||
new_node.modified = now() as i64;
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::FileNode)
|
||||
.update_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(node)
|
||||
.with_changes(new_node)
|
||||
.with_tenant_id(access_token),
|
||||
)
|
||||
.map(|b| b.commit_point())
|
||||
}
|
||||
}
|
||||
|
||||
impl DestroyArchive<Archive<&ArchivedFileNode>> {
|
||||
pub fn delete(
|
||||
self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
document_id: u32,
|
||||
batch: &mut BatchBuilder,
|
||||
) -> trc::Result<()> {
|
||||
// Prepare write batch
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::FileNode)
|
||||
.delete_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::<_, ()>::new()
|
||||
.with_current(self.0)
|
||||
.with_tenant_id(access_token),
|
||||
)?
|
||||
.commit_point();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl DestroyArchive<Vec<u32>> {
|
||||
pub async fn delete(
|
||||
self,
|
||||
server: &Server,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
) -> trc::Result<()> {
|
||||
// Process deletions
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::FileNode);
|
||||
for document_id in self.0 {
|
||||
if let Some(node) = server
|
||||
.get_archive(account_id, Collection::FileNode, document_id)
|
||||
.await?
|
||||
{
|
||||
// Delete record
|
||||
batch
|
||||
.delete_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::<_, ()>::new()
|
||||
.with_tenant_id(access_token)
|
||||
.with_current(
|
||||
node.to_unarchived::<FileNode>()
|
||||
.caused_by(trc::location!())?,
|
||||
),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
}
|
||||
}
|
||||
|
||||
// Write changes
|
||||
if !batch.is_empty() {
|
||||
server
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -6,28 +6,45 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::{DavResource, DavResourceId, DavResources, Server};
|
||||
use common::{DavResource, DavResourceId, DavResources, Server, auth::AccessToken};
|
||||
use directory::backend::internal::manage::ManageDirectory;
|
||||
use jmap_proto::types::collection::Collection;
|
||||
use percent_encoding::NON_ALPHANUMERIC;
|
||||
use store::{
|
||||
Deserialize, IndexKey, IterateParams, SerializeInfallible, U32_LEN, ahash::AHashMap,
|
||||
write::key::DeserializeBigEndian,
|
||||
Deserialize, IndexKey, IndexKeyPrefix, IterateParams, SerializeInfallible, U32_LEN,
|
||||
ahash::AHashMap,
|
||||
write::{BatchBuilder, key::DeserializeBigEndian},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use utils::bimap::IdBimap;
|
||||
|
||||
use crate::{DavName, IDX_NAME, file::FileNode};
|
||||
use crate::{DavName, DavResourceName, IDX_NAME, contact::AddressBook, file::FileNode};
|
||||
|
||||
pub trait DavHierarchy: Sync + Send {
|
||||
fn fetch_dav_resources(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
collection: Collection,
|
||||
) -> impl Future<Output = trc::Result<Arc<DavResources>>> + Send;
|
||||
|
||||
fn create_default_addressbook(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
) -> impl Future<Output = trc::Result<()>> + Send;
|
||||
|
||||
fn create_default_calendar(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
) -> impl Future<Output = trc::Result<()>> + Send;
|
||||
}
|
||||
|
||||
impl DavHierarchy for Server {
|
||||
async fn fetch_dav_resources(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
collection: Collection,
|
||||
) -> trc::Result<Arc<DavResources>> {
|
||||
@@ -51,7 +68,23 @@ impl DavHierarchy for Server {
|
||||
} else {
|
||||
let mut files = match collection {
|
||||
Collection::Calendar | Collection::AddressBook => {
|
||||
build_hierarchy(self, account_id, collection).await?
|
||||
let files = build_hierarchy(self, account_id, collection).await?;
|
||||
if files.paths.is_empty() {
|
||||
match collection {
|
||||
Collection::Calendar => {
|
||||
self.create_default_calendar(access_token, account_id)
|
||||
.await?
|
||||
}
|
||||
Collection::AddressBook => {
|
||||
self.create_default_addressbook(access_token, account_id)
|
||||
.await?
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
build_hierarchy(self, account_id, collection).await?
|
||||
} else {
|
||||
files
|
||||
}
|
||||
}
|
||||
Collection::FileNode => build_file_hierarchy(self, account_id).await?,
|
||||
_ => unreachable!(),
|
||||
@@ -64,6 +97,38 @@ impl DavHierarchy for Server {
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_default_addressbook(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
) -> trc::Result<()> {
|
||||
if let Some(name) = &self.core.dav.default_addressbook_name {
|
||||
let mut batch = BatchBuilder::new();
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::AddressBook, 1)
|
||||
.await?;
|
||||
AddressBook {
|
||||
name: name.clone(),
|
||||
display_name: self.core.dav.default_addressbook_display_name.clone(),
|
||||
is_default: true,
|
||||
..Default::default()
|
||||
}
|
||||
.insert(access_token, account_id, document_id, &mut batch)?;
|
||||
self.commit_batch(batch).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_default_calendar(
|
||||
&self,
|
||||
access_token: &AccessToken,
|
||||
account_id: u32,
|
||||
) -> trc::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_hierarchy(
|
||||
@@ -71,6 +136,7 @@ async fn build_hierarchy(
|
||||
account_id: u32,
|
||||
collection: Collection,
|
||||
) -> trc::Result<DavResources> {
|
||||
let base_path = DavResourceName::from(collection).base_path();
|
||||
let collection = u8::from(collection);
|
||||
let mut containers: AHashMap<u32, String> = AHashMap::with_capacity(16);
|
||||
let mut resources: AHashMap<u32, Vec<DavName>> = AHashMap::with_capacity(16);
|
||||
@@ -99,7 +165,7 @@ async fn build_hierarchy(
|
||||
|key, _| {
|
||||
let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?;
|
||||
let value = key
|
||||
.get(key.len() - (U32_LEN * 2)..key.len() - U32_LEN)
|
||||
.get(IndexKeyPrefix::len()..key.len() - U32_LEN)
|
||||
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
|
||||
let key_collection = key
|
||||
.get(U32_LEN)
|
||||
@@ -126,10 +192,22 @@ async fn build_hierarchy(
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let name = server
|
||||
.store()
|
||||
.get_principal_name(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap_or_else(|| format!("_{account_id}"));
|
||||
|
||||
let mut files = DavResources {
|
||||
paths: IdBimap::with_capacity(containers.len() + resources.len()),
|
||||
size: std::mem::size_of::<DavResources>() as u64,
|
||||
modseq: None,
|
||||
base_path: format!(
|
||||
"{}/{}/",
|
||||
base_path,
|
||||
percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC),
|
||||
),
|
||||
};
|
||||
|
||||
for (document_id, dav_names) in resources {
|
||||
@@ -172,7 +250,18 @@ async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result<D
|
||||
.fetch_folders::<FileNode>(account_id, Collection::FileNode)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let name = server
|
||||
.store()
|
||||
.get_principal_name(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.unwrap_or_else(|| format!("_{account_id}"));
|
||||
let mut files = DavResources {
|
||||
base_path: format!(
|
||||
"{}/{}/",
|
||||
DavResourceName::Card.base_path(),
|
||||
percent_encoding::utf8_percent_encode(&name, NON_ALPHANUMERIC),
|
||||
),
|
||||
paths: IdBimap::with_capacity(list.len()),
|
||||
size: std::mem::size_of::<DavResources>() as u64,
|
||||
modseq: None,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use jmap_proto::types::collection::Collection;
|
||||
use store::{Deserialize, SerializeInfallible, write::key::KeySerializer};
|
||||
use utils::codec::leb128::Leb128Reader;
|
||||
|
||||
@@ -15,6 +16,16 @@ pub mod hierarchy;
|
||||
pub const IDX_NAME: u8 = 0;
|
||||
pub const IDX_CARD_UID: u8 = 1;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DavResourceName {
|
||||
Card,
|
||||
Cal,
|
||||
File,
|
||||
Principal,
|
||||
}
|
||||
|
||||
pub struct DestroyArchive<T>(pub T);
|
||||
|
||||
#[derive(
|
||||
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
|
||||
)]
|
||||
@@ -69,3 +80,55 @@ impl Deserialize for DavName {
|
||||
Ok(DavName { name, parent_id })
|
||||
}
|
||||
}
|
||||
|
||||
impl DavResourceName {
|
||||
pub fn parse(service: &str) -> Option<Self> {
|
||||
hashify::tiny_map!(service.as_bytes(),
|
||||
"card" => DavResourceName::Card,
|
||||
"cal" => DavResourceName::Cal,
|
||||
"file" => DavResourceName::File,
|
||||
"pal" => DavResourceName::Principal,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn base_path(&self) -> &'static str {
|
||||
match self {
|
||||
DavResourceName::Card => "/dav/card",
|
||||
DavResourceName::Cal => "/dav/cal",
|
||||
DavResourceName::File => "/dav/file",
|
||||
DavResourceName::Principal => "/dav/pal",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collection_path(&self) -> &'static str {
|
||||
match self {
|
||||
DavResourceName::Card => "/dav/card/",
|
||||
DavResourceName::Cal => "/dav/cal/",
|
||||
DavResourceName::File => "/dav/file/",
|
||||
DavResourceName::Principal => "/dav/pal/",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DavResourceName> for Collection {
|
||||
fn from(value: DavResourceName) -> Self {
|
||||
match value {
|
||||
DavResourceName::Card => Collection::AddressBook,
|
||||
DavResourceName::Cal => Collection::Calendar,
|
||||
DavResourceName::File => Collection::FileNode,
|
||||
DavResourceName::Principal => Collection::Principal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Collection> for DavResourceName {
|
||||
fn from(value: Collection) -> Self {
|
||||
match value {
|
||||
Collection::AddressBook => DavResourceName::Card,
|
||||
Collection::Calendar => DavResourceName::Cal,
|
||||
Collection::FileNode => DavResourceName::File,
|
||||
Collection::Principal => DavResourceName::Principal,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user