diff --git a/.sqlx/query-c550dbf3d5ce7069f28d767ea9045e477ef8d29d6186851760757a06dec42339.json b/.sqlx/query-c550dbf3d5ce7069f28d767ea9045e477ef8d29d6186851760757a06dec42339.json new file mode 100644 index 0000000..2c5ddda --- /dev/null +++ b/.sqlx/query-c550dbf3d5ce7069f28d767ea9045e477ef8d29d6186851760757a06dec42339.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT id, ics FROM calendarobjects\n WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL\n AND (last_occurence IS NULL OR ? IS NULL OR last_occurence >= date(?))\n AND (first_occurence IS NULL OR ? IS NULL OR first_occurence <= date(?))\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "ics", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 6 + }, + "nullable": [ + false, + false + ] + }, + "hash": "c550dbf3d5ce7069f28d767ea9045e477ef8d29d6186851760757a06dec42339" +} diff --git a/Cargo.lock b/Cargo.lock index af66503..4092641 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2883,6 +2883,7 @@ name = "rustical_store_sqlite" version = "0.1.0" dependencies = [ "async-trait", + "chrono", "derive_more 1.0.0", "rustical_store", "serde", diff --git a/crates/caldav/src/calendar/methods/report/calendar_query.rs b/crates/caldav/src/calendar/methods/report/calendar_query.rs index 2b29666..673dafb 100644 --- a/crates/caldav/src/calendar/methods/report/calendar_query.rs +++ b/crates/caldav/src/calendar/methods/report/calendar_query.rs @@ -1,12 +1,13 @@ -use std::ops::Deref; - use actix_web::HttpRequest; use rustical_dav::{ resource::Resource, xml::{MultistatusElement, PropElement, PropfindType}, }; -use rustical_store::{auth::User, calendar::UtcDateTime, CalendarObject, CalendarStore}; +use rustical_store::{ + auth::User, calendar::UtcDateTime, calendar_store::CalendarQuery, CalendarObject, CalendarStore, +}; use rustical_xml::XmlDeserialize; +use std::ops::Deref; use crate::{ calendar_object::resource::{CalendarObjectPropWrapper, CalendarObjectResource}, @@ -141,6 +142,7 @@ impl CompFilterElement { #[allow(dead_code)] // https://datatracker.ietf.org/doc/html/rfc4791#section-9.7 pub(crate) struct FilterElement { + // This comp-filter matches on VCALENDAR #[xml(ns = "rustical_dav::namespace::NS_CALDAV")] pub(crate) comp_filter: CompFilterElement, } @@ -151,6 +153,27 @@ impl FilterElement { } } +impl From<&FilterElement> for CalendarQuery { + fn from(value: &FilterElement) -> Self { + let comp_filter_vcalendar = &value.comp_filter; + for comp_filter in comp_filter_vcalendar.comp_filter.iter() { + // A calendar object cannot contain both VEVENT and VTODO, so we only have to handle + // whatever we get first + if matches!(comp_filter.name.as_str(), "VEVENT" | "VTODO") { + if let Some(time_range) = &comp_filter.time_range { + let start = time_range.start.as_ref().map(|start| start.date_naive()); + let end = time_range.end.as_ref().map(|end| end.date_naive()); + return CalendarQuery { + time_start: start, + time_end: end, + }; + } + } + } + Default::default() + } +} + #[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[allow(dead_code)] // @@ -165,13 +188,25 @@ pub struct CalendarQueryRequest { pub(crate) timezone_id: Option, } +impl From<&CalendarQueryRequest> for CalendarQuery { + fn from(value: &CalendarQueryRequest) -> Self { + value + .filter + .as_ref() + .map(CalendarQuery::from) + .unwrap_or_default() + } +} + pub async fn get_objects_calendar_query( cal_query: &CalendarQueryRequest, principal: &str, cal_id: &str, store: &C, ) -> Result, Error> { - let mut objects = store.get_objects(principal, cal_id).await?; + let mut objects = store + .calendar_query(principal, cal_id, cal_query.into()) + .await?; if let Some(filter) = &cal_query.filter { objects.retain(|object| filter.matches(object)); } diff --git a/crates/store/src/calendar_store.rs b/crates/store/src/calendar_store.rs index 8028df6..8200dd1 100644 --- a/crates/store/src/calendar_store.rs +++ b/crates/store/src/calendar_store.rs @@ -1,6 +1,13 @@ use crate::calendar::{Calendar, CalendarObject}; use crate::error::Error; use async_trait::async_trait; +use chrono::NaiveDate; + +#[derive(Default, Debug, Clone)] +pub struct CalendarQuery { + pub time_start: Option, + pub time_end: Option, +} #[async_trait] pub trait CalendarStore: Send + Sync + 'static { @@ -30,6 +37,17 @@ pub trait CalendarStore: Send + Sync + 'static { synctoken: i64, ) -> Result<(Vec, Vec, i64), Error>; + /// Since the rules are rather complex this function + /// is only meant to do some prefiltering + async fn calendar_query( + &self, + principal: &str, + cal_id: &str, + _query: CalendarQuery, + ) -> Result, Error> { + self.get_objects(principal, cal_id).await + } + async fn get_objects( &self, principal: &str, diff --git a/crates/store_sqlite/Cargo.toml b/crates/store_sqlite/Cargo.toml index c34c6c9..a2eb52a 100644 --- a/crates/store_sqlite/Cargo.toml +++ b/crates/store_sqlite/Cargo.toml @@ -15,3 +15,4 @@ sqlx = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } derive_more.workspace = true +chrono.workspace = true diff --git a/crates/store_sqlite/src/calendar_store.rs b/crates/store_sqlite/src/calendar_store.rs index f9b6a39..277a6ec 100644 --- a/crates/store_sqlite/src/calendar_store.rs +++ b/crates/store_sqlite/src/calendar_store.rs @@ -1,7 +1,9 @@ use super::ChangeOperation; use async_trait::async_trait; +use chrono::TimeDelta; use derive_more::derive::Constructor; use rustical_store::calendar::{CalDateTime, CalendarObjectType}; +use rustical_store::calendar_store::CalendarQuery; use rustical_store::synctoken::format_synctoken; use rustical_store::{Calendar, CalendarObject, CalendarStore, Error}; use rustical_store::{CollectionOperation, CollectionOperationType}; @@ -254,6 +256,40 @@ impl SqliteCalendarStore { .collect() } + async fn _calendar_query<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + principal: &str, + cal_id: &str, + query: CalendarQuery, + ) -> Result, Error> { + // We extend our query interval by one day in each direction since we really don't want to + // miss any objects because of timezone differences + // I've previously tried NaiveDate::MIN,MAX, but it seems like sqlite cannot handle these + let start = query.time_start.map(|start| start - TimeDelta::days(1)); + let end = query.time_end.map(|end| end + TimeDelta::days(1)); + + sqlx::query_as!( + CalendarObjectRow, + r"SELECT id, ics FROM calendarobjects + WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL + AND (last_occurence IS NULL OR ? IS NULL OR last_occurence >= date(?)) + AND (first_occurence IS NULL OR ? IS NULL OR first_occurence <= date(?)) + ", + principal, + cal_id, + start, + start, + end, + end, + ) + .fetch_all(executor) + .await + .map_err(crate::Error::from)? + .into_iter() + .map(|row| row.try_into().map_err(rustical_store::Error::from)) + .collect() + } + async fn _get_object<'e, E: Executor<'e, Database = Sqlite>>( executor: E, principal: &str, @@ -497,6 +533,16 @@ impl CalendarStore for SqliteCalendarStore { Self::_restore_calendar(&self.db, principal, id).await } + #[instrument] + async fn calendar_query( + &self, + principal: &str, + cal_id: &str, + query: CalendarQuery, + ) -> Result, Error> { + Self::_calendar_query(&self.db, principal, cal_id, query).await + } + #[instrument] async fn get_objects( &self,