CardDAV skeleton + Message ingestion performance improvements
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "groupware"
|
||||
version = "0.11.5"
|
||||
version = "0.11.7"
|
||||
edition = "2024"
|
||||
resolver = "2"
|
||||
|
||||
@@ -12,7 +12,7 @@ jmap_proto = { path = "../jmap-proto" }
|
||||
trc = { path = "../trc" }
|
||||
directory = { path = "../directory" }
|
||||
dav-proto = { path = "../dav-proto" }
|
||||
calcard = { path = "/Users/me/code/calcard" }
|
||||
calcard = { path = "/Users/me/code/calcard", features = ["rkyv"] }
|
||||
hashify = "0.2"
|
||||
rkyv = { version = "0.8.10", features = ["little_endian"] }
|
||||
percent-encoding = "2.3.1"
|
||||
|
||||
@@ -28,12 +28,15 @@ pub struct CalendarPreferences {
|
||||
pub is_subscribed: bool,
|
||||
pub is_default: bool,
|
||||
pub is_visible: bool,
|
||||
/*pub include_in_availability: IncludeInAvailability,
|
||||
pub include_in_availability: IncludeInAvailability,
|
||||
pub default_alerts_with_time: VecMap<String, ICalendar>,
|
||||
pub default_alerts_without_time: VecMap<String, ICalendar>,
|
||||
pub time_zone: Timezone,*/
|
||||
pub time_zone: Timezone,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
|
||||
)]
|
||||
pub struct CalendarEvent {
|
||||
pub name: Option<String>,
|
||||
pub event: ICalendar,
|
||||
@@ -47,15 +50,24 @@ pub struct CalendarEvent {
|
||||
pub is_draft: bool,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
|
||||
)]
|
||||
pub enum Timezone {
|
||||
IANA(String),
|
||||
Custom(ICalendar),
|
||||
#[default]
|
||||
Default,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
|
||||
)]
|
||||
#[rkyv(derive(Debug))]
|
||||
pub enum IncludeInAvailability {
|
||||
All,
|
||||
Attending,
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
|
||||
81
crates/groupware/src/contact/index.rs
Normal file
81
crates/groupware/src/contact/index.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::storage::index::{IndexValue, IndexableAndSerializableObject, IndexableObject};
|
||||
use jmap_proto::types::{property::Property, value::AclGrant};
|
||||
|
||||
use super::{AddressBook, ArchivedAddressBook, ContactCard};
|
||||
|
||||
impl IndexableObject for AddressBook {
|
||||
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
|
||||
[
|
||||
IndexValue::Text {
|
||||
field: Property::Name.into(),
|
||||
value: self.name.as_str().into(),
|
||||
},
|
||||
IndexValue::Acl {
|
||||
value: (&self.acls).into(),
|
||||
},
|
||||
IndexValue::Quota {
|
||||
used: self.dead_properties.size() as u32
|
||||
+ self.display_name.as_ref().map_or(0, |n| n.len() as u32)
|
||||
+ self.description.as_ref().map_or(0, |n| n.len() as u32)
|
||||
+ self.name.len() as u32,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexableObject for &ArchivedAddressBook {
|
||||
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
|
||||
[
|
||||
IndexValue::Text {
|
||||
field: Property::Name.into(),
|
||||
value: self.name.as_str().into(),
|
||||
},
|
||||
IndexValue::Acl {
|
||||
value: self
|
||||
.acls
|
||||
.iter()
|
||||
.map(AclGrant::from)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
},
|
||||
IndexValue::Quota {
|
||||
used: self.dead_properties.size() as u32
|
||||
+ self.display_name.as_ref().map_or(0, |n| n.len() as u32)
|
||||
+ self.description.as_ref().map_or(0, |n| n.len() as u32)
|
||||
+ self.name.len() as u32,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexableAndSerializableObject for AddressBook {}
|
||||
|
||||
impl IndexableObject for ContactCard {
|
||||
fn index_values(&self) -> impl Iterator<Item = IndexValue<'_>> {
|
||||
[
|
||||
IndexValue::Text {
|
||||
field: Property::Name.into(),
|
||||
value: self.name.as_str().into(),
|
||||
},
|
||||
IndexValue::U32List {
|
||||
field: Property::ParentId.into(),
|
||||
value: self.addressbook_ids.as_slice().into(),
|
||||
},
|
||||
IndexValue::Quota {
|
||||
used: self.dead_properties.size() as u32
|
||||
+ self.display_name.as_ref().map_or(0, |n| n.len() as u32)
|
||||
+ self.name.len() as u32
|
||||
+ self.size,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,10 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod index;
|
||||
|
||||
use calcard::vcard::VCard;
|
||||
use dav_proto::schema::request::DeadProperty;
|
||||
use jmap_proto::types::{acl::Acl, value::AclGrant};
|
||||
use store::{SERIALIZE_OBJ_15_V1, SerializedVersion};
|
||||
|
||||
@@ -19,6 +22,7 @@ pub struct AddressBook {
|
||||
pub sort_order: u32,
|
||||
pub is_default: bool,
|
||||
pub subscribers: Vec<u32>,
|
||||
pub dead_properties: DeadProperty,
|
||||
pub acls: Vec<AclGrant>,
|
||||
}
|
||||
|
||||
@@ -29,13 +33,19 @@ pub enum AddressBookRight {
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
|
||||
)]
|
||||
#[rkyv(derive(Debug))]
|
||||
pub struct ContactCard {
|
||||
pub name: Option<String>,
|
||||
pub name: String,
|
||||
pub display_name: Option<String>,
|
||||
pub addressbook_ids: Vec<u32>,
|
||||
pub card: VCard,
|
||||
pub dead_properties: DeadProperty,
|
||||
pub created: u64,
|
||||
pub updated: u64,
|
||||
pub size: u32,
|
||||
}
|
||||
|
||||
impl TryFrom<Acl> for AddressBookRight {
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::{FileItem, Files, Server};
|
||||
use jmap_proto::types::collection::Collection;
|
||||
use trc::AddContext;
|
||||
use utils::bimap::IdBimap;
|
||||
|
||||
use crate::file::FileNode;
|
||||
|
||||
pub trait FileHierarchy: Sync + Send {
|
||||
fn fetch_file_hierarchy(
|
||||
&self,
|
||||
account_id: u32,
|
||||
) -> impl Future<Output = trc::Result<Arc<Files>>> + Send;
|
||||
}
|
||||
|
||||
impl FileHierarchy for Server {
|
||||
async fn fetch_file_hierarchy(&self, account_id: u32) -> trc::Result<Arc<Files>> {
|
||||
let change_id = self
|
||||
.store()
|
||||
.get_last_change_id(account_id, Collection::FileNode)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
if let Some(files) = self
|
||||
.inner
|
||||
.cache
|
||||
.files
|
||||
.get(&account_id)
|
||||
.filter(|x| x.modseq == change_id)
|
||||
{
|
||||
Ok(files)
|
||||
} else {
|
||||
let mut files = build_file_hierarchy(self, account_id).await?;
|
||||
files.modseq = change_id;
|
||||
let files = Arc::new(files);
|
||||
self.inner.cache.files.insert(account_id, files.clone());
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result<Files> {
|
||||
let list = server
|
||||
.fetch_folders::<FileNode>(account_id, Collection::FileNode)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let mut files = Files {
|
||||
files: IdBimap::with_capacity(list.len()),
|
||||
size: std::mem::size_of::<Files>() as u64,
|
||||
modseq: None,
|
||||
};
|
||||
|
||||
for expanded in list.into_iterator() {
|
||||
files.size += (std::mem::size_of::<u32>()
|
||||
+ std::mem::size_of::<String>()
|
||||
+ expanded.name.len()) as u64;
|
||||
files.files.insert(FileItem {
|
||||
document_id: expanded.document_id,
|
||||
parent_id: expanded.parent_id,
|
||||
name: expanded.name,
|
||||
size: expanded.size,
|
||||
is_container: expanded.is_container,
|
||||
hierarchy_sequence: expanded.hierarchy_sequence,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod hierarchy;
|
||||
pub mod index;
|
||||
|
||||
use dav_proto::schema::request::DeadProperty;
|
||||
|
||||
209
crates/groupware/src/hierarchy.rs
Normal file
209
crates/groupware/src/hierarchy.rs
Normal file
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use common::{DavResource, DavResourceId, DavResources, Server};
|
||||
use jmap_proto::types::{collection::Collection, property::Property};
|
||||
use store::{
|
||||
Deserialize, IndexKey, IterateParams, SerializeInfallible, U32_LEN, ahash::AHashMap,
|
||||
write::key::DeserializeBigEndian,
|
||||
};
|
||||
use trc::AddContext;
|
||||
use utils::bimap::IdBimap;
|
||||
|
||||
use crate::file::FileNode;
|
||||
|
||||
pub trait DavHierarchy: Sync + Send {
|
||||
fn fetch_dav_hierarchy(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: Collection,
|
||||
) -> impl Future<Output = trc::Result<Arc<DavResources>>> + Send;
|
||||
}
|
||||
|
||||
impl DavHierarchy for Server {
|
||||
async fn fetch_dav_hierarchy(
|
||||
&self,
|
||||
account_id: u32,
|
||||
collection: Collection,
|
||||
) -> trc::Result<Arc<DavResources>> {
|
||||
let change_id = self
|
||||
.store()
|
||||
.get_last_change_id(account_id, collection)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let resource_id = DavResourceId {
|
||||
account_id,
|
||||
collection: collection.into(),
|
||||
};
|
||||
if let Some(files) = self
|
||||
.inner
|
||||
.cache
|
||||
.dav
|
||||
.get(&resource_id)
|
||||
.filter(|x| x.modseq == change_id)
|
||||
{
|
||||
Ok(files)
|
||||
} else {
|
||||
let mut files = match collection {
|
||||
Collection::Calendar | Collection::AddressBook => {
|
||||
build_hierarchy(self, account_id, collection).await?
|
||||
}
|
||||
Collection::FileNode => build_file_hierarchy(self, account_id).await?,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
files.modseq = change_id;
|
||||
let files = Arc::new(files);
|
||||
self.inner.cache.dav.insert(resource_id, files.clone());
|
||||
Ok(files)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DavTempResource {
|
||||
name: String,
|
||||
parent_id: Vec<u32>,
|
||||
}
|
||||
|
||||
async fn build_hierarchy(
|
||||
server: &Server,
|
||||
account_id: u32,
|
||||
collection: Collection,
|
||||
) -> trc::Result<DavResources> {
|
||||
let collection = u8::from(collection);
|
||||
let mut containers: AHashMap<u32, DavTempResource> = AHashMap::with_capacity(16);
|
||||
let mut resources: AHashMap<u32, DavTempResource> = AHashMap::with_capacity(16);
|
||||
|
||||
server
|
||||
.store()
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
IndexKey {
|
||||
account_id,
|
||||
collection,
|
||||
document_id: 0,
|
||||
field: 0,
|
||||
key: 0u32.serialize(),
|
||||
},
|
||||
IndexKey {
|
||||
account_id,
|
||||
collection: collection + 1,
|
||||
document_id: u32::MAX,
|
||||
field: u8::MAX,
|
||||
key: u32::MAX.serialize(),
|
||||
},
|
||||
)
|
||||
.no_values()
|
||||
.ascending(),
|
||||
|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)
|
||||
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
|
||||
let key_collection = key
|
||||
.get(U32_LEN)
|
||||
.copied()
|
||||
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
|
||||
let key_property = key
|
||||
.get(U32_LEN + 1)
|
||||
.copied()
|
||||
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
|
||||
|
||||
let resource = if key_collection == collection {
|
||||
containers.entry(document_id).or_default()
|
||||
} else {
|
||||
resources.entry(document_id).or_default()
|
||||
};
|
||||
|
||||
if key_property == u8::from(Property::Value) {
|
||||
resource.name = std::str::from_utf8(value)
|
||||
.map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))?
|
||||
.to_string();
|
||||
} else if key_property == u8::from(Property::ParentId) {
|
||||
resource.parent_id.push(
|
||||
u32::deserialize(value)
|
||||
.map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
let mut files = DavResources {
|
||||
files: IdBimap::with_capacity(containers.len() + resources.len()),
|
||||
size: std::mem::size_of::<DavResources>() as u64,
|
||||
modseq: None,
|
||||
};
|
||||
|
||||
for (document_id, resource) in resources {
|
||||
for parent_id in resource.parent_id {
|
||||
if let Some(container) = containers.get(&parent_id) {
|
||||
let name = format!("{}/{}", container.name, resource.name);
|
||||
files.size += (std::mem::size_of::<u32>()
|
||||
+ std::mem::size_of::<String>()
|
||||
+ name.len()) as u64;
|
||||
files.files.insert(DavResource {
|
||||
document_id,
|
||||
parent_id: parent_id.into(),
|
||||
name,
|
||||
size: 0,
|
||||
is_container: false,
|
||||
hierarchy_sequence: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (document_id, container) in containers {
|
||||
files.size += (std::mem::size_of::<u32>()
|
||||
+ std::mem::size_of::<String>()
|
||||
+ container.name.len()) as u64;
|
||||
files.files.insert(DavResource {
|
||||
document_id,
|
||||
parent_id: None,
|
||||
name: container.name,
|
||||
size: 0,
|
||||
is_container: true,
|
||||
hierarchy_sequence: 0,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result<DavResources> {
|
||||
let list = server
|
||||
.fetch_folders::<FileNode>(account_id, Collection::FileNode)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
let mut files = DavResources {
|
||||
files: IdBimap::with_capacity(list.len()),
|
||||
size: std::mem::size_of::<DavResources>() as u64,
|
||||
modseq: None,
|
||||
};
|
||||
|
||||
for expanded in list.into_iterator() {
|
||||
files.size += (std::mem::size_of::<u32>()
|
||||
+ std::mem::size_of::<String>()
|
||||
+ expanded.name.len()) as u64;
|
||||
files.files.insert(DavResource {
|
||||
document_id: expanded.document_id,
|
||||
parent_id: expanded.parent_id,
|
||||
name: expanded.name,
|
||||
size: expanded.size,
|
||||
is_container: expanded.is_container,
|
||||
hierarchy_sequence: expanded.hierarchy_sequence,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
@@ -7,3 +7,4 @@
|
||||
pub mod calendar;
|
||||
pub mod contact;
|
||||
pub mod file;
|
||||
pub mod hierarchy;
|
||||
|
||||
Reference in New Issue
Block a user