CalDAV query, freebusy and expand-property reports

This commit is contained in:
mdecimus
2025-04-27 18:13:28 +02:00
parent e10e94325f
commit f74ec98c0d
61 changed files with 2424 additions and 693 deletions

View File

@@ -0,0 +1,262 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::str::FromStr;
use super::{ArchivedCalendarEventData, ArchivedTimezone, CalendarEventData, Timezone};
use crate::calendar::ComponentTimeRange;
use calcard::{
common::timezone::Tz,
icalendar::{
ICalendar,
dates::{CalendarEvent, TimeOrDelta},
},
};
use chrono::{DateTime, TimeZone};
use dav_proto::schema::property::TimeRange;
use store::{
ahash::AHashMap,
write::{bitpack::BitpackIterator, key::KeySerializer},
};
use utils::codec::leb128::Leb128Reader;
impl CalendarEventData {
pub fn new(event: ICalendar, max_expansions: usize) -> Self {
let mut ranges = TimeRanges::default();
let expanded = event.expand_dates(Tz::Floating, max_expansions);
let mut groups: AHashMap<(u16, u16, u16, i32), Vec<i64>> = AHashMap::with_capacity(16);
for event in expanded.events {
let start_naive = event.start.naive_local();
let start_tz = event.start.timezone().as_id();
let start_timestamp_utc = event.start.timestamp();
let start_timestamp_naive = start_naive.and_utc().timestamp();
let (end_timestamp_utc, end_timestamp_naive, end_tz) = match event.end {
TimeOrDelta::Time(time) => {
let end_naive = time.naive_local();
let end_timestamp_utc = time.timestamp();
let end_timestamp_naive = end_naive.and_utc().timestamp();
(
end_timestamp_utc,
end_timestamp_naive,
time.timezone().as_id(),
)
}
TimeOrDelta::Delta(delta) => {
let delta = delta.num_seconds();
(
start_timestamp_utc + delta,
start_timestamp_naive + delta,
start_tz,
)
}
};
ranges.update(start_timestamp_utc, start_timestamp_naive);
ranges.update(end_timestamp_utc, end_timestamp_naive);
groups
.entry((
start_tz,
end_tz,
event.comp_id,
(end_timestamp_naive - start_timestamp_naive) as i32,
))
.or_default()
.push(start_timestamp_naive);
}
let mut events = Vec::with_capacity(groups.len());
for ((start_tz, end_tz, id, duration), mut instances) in groups {
let instances = if instances.len() > 1 {
instances.sort_unstable();
// Bitpack instances
let mut instance_offsets = Vec::with_capacity(instances.len());
for instance in instances {
instance_offsets.push((ranges.base_offset - instance) as u32);
}
KeySerializer::new(instance_offsets.len() * std::mem::size_of::<u32>())
.bitpack_sorted(&instance_offsets)
.finalize()
} else {
KeySerializer::new(std::mem::size_of::<u32>())
.write_leb128((ranges.base_offset - instances.first().unwrap()) as u32)
.finalize()
};
events.push(ComponentTimeRange {
id,
start_tz,
end_tz,
duration,
instances: instances.into_boxed_slice(),
});
}
for error in expanded.errors {
let todo = "log me";
}
CalendarEventData {
event,
time_ranges: events.into_boxed_slice(),
base_offset: ranges.base_offset,
base_time_utc: (ranges.min_time_utc - ranges.base_offset) as u32,
duration: (ranges.max_time_utc - ranges.min_time_utc) as u32,
}
}
pub fn event_range(&self) -> Option<(i64, u32)> {
if self.base_offset != 0 {
Some((self.base_offset + self.base_time_utc as i64, self.duration))
} else {
None
}
}
}
impl ArchivedCalendarEventData {
pub fn expand(&self, default_tz: Tz, limit: TimeRange) -> Option<Vec<CalendarEvent<i64, i64>>> {
let mut expansion = Vec::with_capacity(self.time_ranges.len());
let base_offset = self.base_offset.to_native();
let expansion_limit = limit.start..=limit.end;
for range in self.time_ranges.iter() {
let instances = range.instances.as_ref();
let (offset_or_count, bytes_read) = instances.read_leb128::<u32>()?;
let comp_id = range.id.to_native();
let duration = range.duration.to_native() as i64;
let mut start_tz = Tz::from_id(range.start_tz.to_native())?;
let mut end_tz = Tz::from_id(range.end_tz.to_native())?;
if start_tz.is_floating() && !default_tz.is_floating() {
start_tz = default_tz;
}
if end_tz.is_floating() && !default_tz.is_floating() {
end_tz = default_tz;
}
if instances.len() > bytes_read {
// Recurring event
let unpacker =
BitpackIterator::from_bytes_and_offset(instances, bytes_read, offset_or_count);
for start_offset in unpacker {
let start_date_naive = start_offset as i64 + base_offset;
let end_date_naive = start_date_naive + duration;
let start = start_tz
.from_local_datetime(
&DateTime::from_timestamp(start_date_naive, 0)?.naive_local(),
)
.single()?
.timestamp();
let end = end_tz
.from_local_datetime(
&DateTime::from_timestamp(end_date_naive, 0)?.naive_local(),
)
.single()?
.timestamp();
if expansion_limit.contains(&start) || expansion_limit.contains(&end) {
expansion.push(CalendarEvent {
comp_id,
start,
end,
});
} else if end > limit.end {
break;
}
}
} else {
// Single event
let start_date_naive = offset_or_count as i64 + base_offset;
let end_date_naive = start_date_naive + duration;
let start = start_tz
.from_local_datetime(
&DateTime::from_timestamp(start_date_naive, 0)?.naive_local(),
)
.single()?
.timestamp();
let end = end_tz
.from_local_datetime(
&DateTime::from_timestamp(end_date_naive, 0)?.naive_local(),
)
.single()?
.timestamp();
if expansion_limit.contains(&start) || expansion_limit.contains(&end) {
expansion.push(CalendarEvent {
comp_id,
start,
end,
});
}
}
}
Some(expansion)
}
}
#[derive(Default)]
struct TimeRanges {
max_time_utc: i64,
min_time_utc: i64,
base_offset: i64,
}
impl TimeRanges {
pub fn update(&mut self, utc_timestamp: i64, naive_timestamp: i64) {
if utc_timestamp > self.max_time_utc {
self.max_time_utc = utc_timestamp;
}
if utc_timestamp < self.min_time_utc || self.max_time_utc == 0 {
self.min_time_utc = utc_timestamp;
}
let offset = std::cmp::min(utc_timestamp, naive_timestamp);
if offset < self.base_offset || self.base_offset == 0 {
self.base_offset = offset;
}
}
}
impl ArchivedCalendarEventData {
pub fn event_range(&self) -> Option<(i64, u32)> {
if self.base_offset != 0 {
Some((
self.base_offset.to_native() + self.base_time_utc.to_native() as i64,
self.duration.to_native(),
))
} else {
None
}
}
}
impl Timezone {
pub fn tz(&self) -> Option<Tz> {
match self {
Timezone::IANA(iana) => Tz::from_str(iana).ok(),
Timezone::Custom(icalendar) => icalendar
.timezones()
.filter_map(|t| t.timezone().map(|x| x.1))
.next(),
Timezone::Default => None,
}
}
}
impl ArchivedTimezone {
pub fn tz(&self) -> Option<Tz> {
match self {
ArchivedTimezone::IANA(iana) => Tz::from_str(iana).ok(),
ArchivedTimezone::Custom(icalendar) => icalendar
.timezones()
.filter_map(|t| t.timezone().map(|x| x.1))
.next(),
ArchivedTimezone::Default => None,
}
}
}

View File

@@ -8,9 +8,9 @@ use common::storage::index::{
IndexItem, IndexValue, IndexableAndSerializableObject, IndexableObject,
};
use jmap_proto::types::{collection::Collection, value::AclGrant};
use store::SerializeInfallible;
use store::{SerializeInfallible, write::key::KeySerializer};
use crate::{IDX_NAME, IDX_UID};
use crate::{IDX_NAME, IDX_TIME, IDX_UID};
use super::{
ArchivedCalendar, ArchivedCalendarEvent, ArchivedCalendarPreferences, ArchivedDefaultAlert,
@@ -26,6 +26,15 @@ impl IndexableObject for Calendar {
field: IDX_NAME,
value: self.name.as_str().into(),
},
IndexValue::Index {
field: IDX_TIME,
value: self
.preferences
.first()
.and_then(|p| p.time_zone.tz())
.map(|tz| tz.as_id().serialize())
.into(),
},
IndexValue::Acl {
value: (&self.acls).into(),
},
@@ -48,6 +57,15 @@ impl IndexableObject for &ArchivedCalendar {
field: IDX_NAME,
value: self.name.as_str().into(),
},
IndexValue::Index {
field: IDX_TIME,
value: self
.preferences
.first()
.and_then(|p| p.time_zone.tz())
.map(|tz| tz.as_id().serialize())
.into(),
},
IndexValue::Acl {
value: self
.acls
@@ -83,7 +101,20 @@ impl IndexableObject for CalendarEvent {
},
IndexValue::Index {
field: IDX_UID,
value: self.event.uids().next().into(),
value: self.data.event.uids().next().into(),
},
IndexValue::Index {
field: IDX_TIME,
value: self
.data
.event_range()
.map(|(start, duration)| {
KeySerializer::new(std::mem::size_of::<i64>() + std::mem::size_of::<u32>())
.write(start as u64)
.write(duration)
.finalize()
})
.into(),
},
IndexValue::Quota {
used: self.dead_properties.size() as u32
@@ -114,7 +145,20 @@ impl IndexableObject for &ArchivedCalendarEvent {
},
IndexValue::Index {
field: IDX_UID,
value: self.event.uids().next().into(),
value: self.data.event.uids().next().into(),
},
IndexValue::Index {
field: IDX_TIME,
value: self
.data
.event_range()
.map(|(start, duration)| {
KeySerializer::new(std::mem::size_of::<i64>() + std::mem::size_of::<u32>())
.write(start as u64)
.write(duration)
.finalize()
})
.into(),
},
IndexValue::Quota {
used: self.dead_properties.size() as u32

View File

@@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod dates;
pub mod index;
pub mod storage;
@@ -67,7 +68,7 @@ pub const EVENT_ORIGIN: u16 = 1 << 4;
pub struct CalendarEvent {
pub names: Vec<DavName>,
pub display_name: Option<String>,
pub event: ICalendar,
pub data: CalendarEventData,
pub user_properties: Vec<UserProperties>,
pub flags: u16,
pub dead_properties: DeadProperty,
@@ -76,6 +77,28 @@ pub struct CalendarEvent {
pub modified: i64,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct CalendarEventData {
pub event: ICalendar,
pub time_ranges: Box<[ComponentTimeRange]>,
pub base_offset: i64,
pub base_time_utc: u32,
pub duration: u32,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]
pub struct ComponentTimeRange {
pub id: u16,
pub start_tz: u16,
pub end_tz: u16,
pub duration: i32,
pub instances: Box<[u8]>,
}
#[derive(
rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq,
)]

View File

@@ -4,27 +4,28 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::Arc;
use common::{DavResource, DavResourceId, DavResources, Server, auth::AccessToken};
use crate::{
DavName, DavResourceName, IDX_NAME, IDX_TIME,
calendar::{Calendar, CalendarPreferences},
contact::AddressBook,
file::FileNode,
};
use calcard::common::timezone::Tz;
use common::{
DavResource, DavResourceId, DavResourceMetadata, DavResources, Server, auth::AccessToken,
};
use directory::backend::internal::manage::ManageDirectory;
use jmap_proto::types::collection::Collection;
use percent_encoding::NON_ALPHANUMERIC;
use std::sync::Arc;
use store::{
Deserialize, IndexKey, IndexKeyPrefix, IterateParams, SerializeInfallible, U32_LEN,
Deserialize, IndexKey, IndexKeyPrefix, IterateParams, SerializeInfallible, U32_LEN, U64_LEN,
ahash::AHashMap,
write::{BatchBuilder, key::DeserializeBigEndian},
};
use trc::AddContext;
use utils::bimap::IdBimap;
use crate::{
DavName, DavResourceName, IDX_NAME,
calendar::{Calendar, CalendarPreferences},
contact::AddressBook,
file::FileNode,
};
pub trait DavHierarchy: Sync + Send {
fn fetch_dav_resources(
&self,
@@ -44,6 +45,12 @@ pub trait DavHierarchy: Sync + Send {
access_token: &AccessToken,
account_id: u32,
) -> impl Future<Output = trc::Result<()>> + Send;
fn cached_dav_resources(
&self,
account_id: u32,
collection: Collection,
) -> Option<Arc<DavResources>>;
}
impl DavHierarchy for Server {
@@ -154,18 +161,36 @@ impl DavHierarchy for Server {
Ok(())
}
fn cached_dav_resources(
&self,
account_id: u32,
collection: Collection,
) -> Option<Arc<DavResources>> {
self.inner
.cache
.dav
.get(&DavResourceId {
account_id,
collection: collection.into(),
})
.clone()
}
}
async fn build_hierarchy(
server: &Server,
account_id: u32,
collection: Collection,
collection_: Collection,
) -> trc::Result<DavResources> {
let base_path = DavResourceName::from(collection).base_path();
let collection = u8::from(collection);
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);
let mut time_ranges: AHashMap<u32, (i64, u32)> = AHashMap::new();
let mut time_zones: AHashMap<u32, Tz> = AHashMap::new();
server
.store()
.iterate(
@@ -181,7 +206,7 @@ async fn build_hierarchy(
account_id,
collection: collection + 1,
document_id: u32::MAX,
field: IDX_NAME,
field: IDX_TIME,
key: u32::MAX.serialize(),
},
)
@@ -196,19 +221,39 @@ async fn build_hierarchy(
.get(U32_LEN)
.copied()
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
let field = key
.get(U32_LEN + 1)
.copied()
.ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
if key_collection == collection {
containers.insert(
document_id,
std::str::from_utf8(value)
.map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))?
.to_string(),
);
} else {
if field == IDX_NAME {
containers.insert(
document_id,
std::str::from_utf8(value)
.map_err(|_| {
trc::Error::corrupted_key(key, None, trc::location!())
})?
.to_string(),
);
} else if field == IDX_TIME {
let tz = Tz::from_id(key.deserialize_be_u16(IndexKeyPrefix::len())?)
.ok_or_else(|| {
trc::Error::corrupted_key(key, None, trc::location!())
})?;
time_zones.insert(document_id, tz);
}
} else if field == IDX_NAME {
resources.entry(document_id).or_default().push(
DavName::deserialize(value)
.map_err(|_| trc::Error::corrupted_key(key, None, trc::location!()))?,
);
} else if field == IDX_TIME {
let start_time = key.deserialize_be_u64(IndexKeyPrefix::len())?;
let duration = key.deserialize_be_u32(IndexKeyPrefix::len() + U64_LEN)?;
time_ranges.insert(document_id, (start_time as i64, duration));
}
Ok(true)
@@ -246,9 +291,13 @@ async fn build_hierarchy(
document_id,
parent_id: dav_name.parent_id.into(),
name,
size: 0,
is_container: false,
hierarchy_sequence: 1,
data: time_ranges
.get(&document_id)
.map(|(start, duration)| DavResourceMetadata::CalendarEvent {
start: *start,
duration: *duration,
})
.unwrap_or(DavResourceMetadata::None),
});
}
}
@@ -261,9 +310,10 @@ async fn build_hierarchy(
document_id,
parent_id: None,
name,
size: 0,
is_container: true,
hierarchy_sequence: 0,
data: time_zones
.get(&document_id)
.map(|tz| DavResourceMetadata::Calendar { tz: *tz })
.unwrap_or(DavResourceMetadata::None),
});
}
@@ -300,9 +350,11 @@ async fn build_file_hierarchy(server: &Server, account_id: u32) -> trc::Result<D
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,
data: DavResourceMetadata::File {
size: expanded.size,
hierarchy_sequence: expanded.hierarchy_sequence,
is_container: expanded.is_container,
},
});
}

View File

@@ -4,6 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use calcard::common::timezone::Tz;
use common::DavResources;
use jmap_proto::types::collection::Collection;
use store::{Deserialize, SerializeInfallible, write::key::KeySerializer};
use utils::codec::leb128::Leb128Reader;
@@ -14,7 +16,8 @@ pub mod file;
pub mod hierarchy;
pub const IDX_NAME: u8 = 0;
pub const IDX_UID: u8 = 1;
pub const IDX_TIME: u8 = 1;
pub const IDX_UID: u8 = 2;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DavResourceName {
@@ -132,3 +135,25 @@ impl From<Collection> for DavResourceName {
}
}
}
pub trait DavCalendarResource {
fn calendar_default_tz(&self, calendar_id: u32) -> Option<Tz>;
fn event_default_tz(&self, event_id: u32) -> Option<Tz>;
}
impl DavCalendarResource for DavResources {
fn calendar_default_tz(&self, calendar_id: u32) -> Option<Tz> {
self.paths
.iter()
.find(|c| c.is_container() && c.document_id == calendar_id)
.and_then(|c| c.timezone())
}
fn event_default_tz(&self, event_id: u32) -> Option<Tz> {
self.paths
.iter()
.find(|c| !c.is_container() && c.document_id == event_id)
.and_then(|c| c.parent_id)
.and_then(|parent_id| self.calendar_default_tz(parent_id))
}
}