Make CalendarObject id an extrinsic property

This commit is contained in:
Lennart K
2025-12-28 14:14:04 +01:00
parent 2e89b63cd2
commit 968a5e931c
20 changed files with 317 additions and 109 deletions

View File

@@ -61,7 +61,7 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
});
}
for object in &objects {
for (_object_id, object) in &objects {
vtimezones.extend(object.get_inner().get_vtimezones());
match object.get_inner().get_inner() {
CalendarInnerData::Event(main, overrides) => {

View File

@@ -82,7 +82,10 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
let objects = expanded_cals
.into_iter()
.map(|cal| cal.generate())
.map(|ics| CalendarObject::from_ics(ics, None))
.map(|ics| {
CalendarObject::from_ics(ics)
.map(|object| (object.get_inner().get_uid().to_owned(), object))
})
.collect::<Result<Vec<_>, _>>()?;
let new_cal = Calendar {
principal,

View File

@@ -21,7 +21,7 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
principal: &str,
cal_id: &str,
store: &C,
) -> Result<(Vec<CalendarObject>, Vec<String>), Error> {
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>), Error> {
let mut result = vec![];
let mut not_found = vec![];
@@ -32,7 +32,7 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
let filename = filename.trim_start_matches('/');
if let Some(object_id) = filename.strip_suffix(".ics") {
match store.get_object(principal, cal_id, object_id, false).await {
Ok(object) => result.push(object),
Ok(object) => result.push((object_id.to_owned(), object)),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()),
Err(err) => return Err(err.into()),
}

View File

@@ -167,7 +167,7 @@ END:VCALENDAR";
#[test]
fn test_comp_filter_matching() {
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
let object = CalendarObject::from_ics(ICS.to_string()).unwrap();
let comp_filter = CompFilterElement {
is_not_defined: Some(()),
@@ -257,7 +257,7 @@ END:VCALENDAR";
}
#[test]
fn test_comp_filter_time_range() {
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
let object = CalendarObject::from_ics(ICS.to_string()).unwrap();
let comp_filter = CompFilterElement {
is_not_defined: None,
@@ -312,7 +312,7 @@ END:VCALENDAR";
#[test]
fn test_match_timezone() {
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
let object = CalendarObject::from_ics(ICS.to_string()).unwrap();
let comp_filter = CompFilterElement {
is_not_defined: None,

View File

@@ -16,12 +16,12 @@ pub async fn get_objects_calendar_query<C: CalendarStore>(
principal: &str,
cal_id: &str,
store: &C,
) -> Result<Vec<CalendarObject>, Error> {
) -> Result<Vec<(String, CalendarObject)>, Error> {
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));
objects.retain(|(_, object)| filter.matches(object));
}
Ok(objects)
}

View File

@@ -51,7 +51,7 @@ impl ReportRequest {
}
fn objects_response(
objects: Vec<CalendarObject>,
objects: Vec<(String, CalendarObject)>,
not_found: Vec<String>,
path: &str,
principal: &str,
@@ -60,11 +60,12 @@ fn objects_response(
prop: &PropfindType<CalendarObjectPropWrapperName>,
) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
let mut responses = Vec::new();
for object in objects {
let path = format!("{}/{}.ics", path, object.get_id());
for (object_id, object) in objects {
let path = format!("{}/{}.ics", path, &object_id);
responses.push(
CalendarObjectResource {
object,
object_id,
principal: principal.to_owned(),
}
.propfind(&path, prop, None, puri, user)?,

View File

@@ -32,11 +32,12 @@ pub async fn handle_sync_collection<C: CalendarStore>(
.await?;
let mut responses = Vec::new();
for object in new_objects {
let path = format!("{}/{}.ics", path, object.get_id());
for (object_id, object) in new_objects {
let path = format!("{}/{}.ics", path, &object_id);
responses.push(
CalendarObjectResource {
object,
object_id,
principal: principal.to_owned(),
}
.propfind(&path, &sync_collection.prop, None, puri, user)?,

View File

@@ -78,9 +78,10 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
.get_objects(principal, cal_id)
.await?
.into_iter()
.map(|object| CalendarObjectResource {
.map(|(object_id, object)| CalendarObjectResource {
object,
principal: principal.to_owned(),
object_id,
})
.collect())
}

View File

@@ -78,12 +78,12 @@ pub async fn put_event<C: CalendarStore>(
true
};
let Ok(object) = CalendarObject::from_ics(body.clone(), Some(object_id)) else {
let Ok(object) = CalendarObject::from_ics(body.clone()) else {
debug!("invalid calendar data:\n{body}");
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
};
cal_store
.put_object(principal, calendar_id, object, overwrite)
.put_object(principal, calendar_id, (object_id, object), overwrite)
.await?;
Ok(StatusCode::CREATED.into_response())

View File

@@ -17,12 +17,13 @@ use rustical_store::auth::Principal;
#[derive(Clone, From, Into)]
pub struct CalendarObjectResource {
pub object: CalendarObject,
pub object_id: String,
pub principal: String,
}
impl ResourceName for CalendarObjectResource {
fn get_name(&self) -> String {
format!("{}.ics", self.object.get_id())
format!("{}.ics", self.object_id)
}
}

View File

@@ -67,6 +67,7 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
Ok(CalendarObjectResource {
object,
principal: principal.to_owned(),
object_id: object_id.to_owned(),
})
}

View File

@@ -7,7 +7,7 @@ use ical::parser::{
};
use ical::types::CalDate;
use sha2::{Digest, Sha256};
use std::{collections::HashMap, io::BufReader};
use std::io::BufReader;
#[derive(Debug, Clone)]
pub struct AddressObject {
@@ -93,9 +93,8 @@ impl AddressObject {
let uid = format!("{}-anniversary", self.get_id());
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics(
format!(
r"BEGIN:VCALENDAR
Some(CalendarObject::from_ics(format!(
r"BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
@@ -113,9 +112,7 @@ DESCRIPTION:💍 {fullname}{year_suffix}
END:VALARM
END:VEVENT
END:VCALENDAR",
),
None,
)?)
))?)
} else {
None
},
@@ -134,9 +131,8 @@ END:VCALENDAR",
let uid = format!("{}-birthday", self.get_id());
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics(
format!(
r"BEGIN:VCALENDAR
Some(CalendarObject::from_ics(format!(
r"BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
@@ -154,9 +150,7 @@ DESCRIPTION:🎂 {fullname}{year_suffix}
END:VALARM
END:VEVENT
END:VCALENDAR",
),
None,
)?)
))?)
} else {
None
},
@@ -164,13 +158,13 @@ END:VCALENDAR",
}
/// Get significant dates associated with this address object
pub fn get_significant_dates(&self) -> Result<HashMap<&'static str, CalendarObject>, Error> {
let mut out = HashMap::new();
pub fn get_significant_dates(&self) -> Result<Vec<(String, CalendarObject)>, Error> {
let mut out = vec![];
if let Some(birthday) = self.get_birthday_object()? {
out.insert("birthday", birthday);
out.push((birthday.get_inner().get_uid().to_owned(), birthday));
}
if let Some(anniversary) = self.get_anniversary_object()? {
out.insert("anniversary", anniversary);
out.push((anniversary.get_inner().get_uid().to_owned(), anniversary));
}
Ok(out)
}

View File

@@ -8,13 +8,12 @@ use std::io::BufReader;
#[derive(Debug, Clone)]
pub struct CalendarObject {
id: String,
ics: String,
inner: IcalCalendarObject,
}
impl CalendarObject {
pub fn from_ics(ics: String, id: Option<String>) -> Result<Self, Error> {
pub fn from_ics(ics: String) -> Result<Self, Error> {
let mut parser = ical::IcalObjectParser::new(BufReader::new(ics.as_bytes()));
let cal = parser.next().ok_or(Error::MissingCalendar)??;
if parser.next().is_some() {
@@ -23,11 +22,7 @@ impl CalendarObject {
));
}
Ok(Self {
id: id.unwrap_or_else(|| cal.get_uid().to_owned()),
ics,
inner: cal,
})
Ok(Self { ics, inner: cal })
}
#[must_use]
@@ -35,11 +30,6 @@ impl CalendarObject {
&self.inner
}
#[must_use]
pub fn get_id(&self) -> &str {
&self.id
}
#[must_use]
pub fn get_etag(&self) -> String {
let mut hasher = Sha256::new();

View File

@@ -25,6 +25,6 @@ END:VCALENDAR
#[test]
fn parse_calendar_object() {
let object = CalendarObject::from_ics(MULTI_VEVENT.to_string(), None).unwrap();
let object = CalendarObject::from_ics(MULTI_VEVENT.to_string()).unwrap();
object.get_inner().expand_recurrence(None, None).unwrap();
}

View File

@@ -37,7 +37,7 @@ pub trait CalendarStore: Send + Sync + 'static {
async fn import_calendar(
&self,
calendar: Calendar,
objects: Vec<CalendarObject>,
objects: Vec<(String, CalendarObject)>,
merge_existing: bool,
) -> Result<(), Error>;
@@ -46,7 +46,7 @@ pub trait CalendarStore: Send + Sync + 'static {
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error>;
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), Error>;
/// Since the <calendar-query> rules are rather complex this function
/// is only meant to do some prefiltering
@@ -55,7 +55,7 @@ pub trait CalendarStore: Send + Sync + 'static {
principal: &str,
cal_id: &str,
_query: CalendarQuery,
) -> Result<Vec<CalendarObject>, Error> {
) -> Result<Vec<(String, CalendarObject)>, Error> {
self.get_objects(principal, cal_id).await
}
@@ -69,7 +69,7 @@ pub trait CalendarStore: Send + Sync + 'static {
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error>;
) -> Result<Vec<(String, CalendarObject)>, Error>;
async fn get_object(
&self,
principal: &str,
@@ -81,14 +81,14 @@ pub trait CalendarStore: Send + Sync + 'static {
&self,
principal: String,
cal_id: String,
objects: Vec<CalendarObject>,
objects: Vec<(String, CalendarObject)>,
overwrite: bool,
) -> Result<(), Error>;
async fn put_object(
&self,
principal: String,
cal_id: String,
object: CalendarObject,
object: (String, CalendarObject),
overwrite: bool,
) -> Result<(), Error> {
self.put_objects(principal, cal_id, vec![object], overwrite)

View File

@@ -1,5 +1,6 @@
use crate::CalendarStore;
use async_trait::async_trait;
use rustical_ical::CalendarObject;
use std::{collections::HashMap, sync::Arc};
pub trait PrefixedCalendarStore: CalendarStore {
@@ -88,7 +89,7 @@ impl CalendarStore for CombinedCalendarStore {
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<rustical_ical::CalendarObject>, Vec<String>, i64), crate::Error> {
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), crate::Error> {
self.store_for_id(cal_id)
.sync_changes(principal, cal_id, synctoken)
.await
@@ -97,7 +98,7 @@ impl CalendarStore for CombinedCalendarStore {
async fn import_calendar(
&self,
calendar: crate::Calendar,
objects: Vec<rustical_ical::CalendarObject>,
objects: Vec<(String, CalendarObject)>,
merge_existing: bool,
) -> Result<(), crate::Error> {
self.store_for_id(&calendar.id)
@@ -110,7 +111,7 @@ impl CalendarStore for CombinedCalendarStore {
principal: &str,
cal_id: &str,
query: crate::calendar_store::CalendarQuery,
) -> Result<Vec<rustical_ical::CalendarObject>, crate::Error> {
) -> Result<Vec<(String, CalendarObject)>, crate::Error> {
self.store_for_id(cal_id)
.calendar_query(principal, cal_id, query)
.await
@@ -141,7 +142,7 @@ impl CalendarStore for CombinedCalendarStore {
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<rustical_ical::CalendarObject>, crate::Error> {
) -> Result<Vec<(String, CalendarObject)>, crate::Error> {
self.store_for_id(cal_id)
.get_objects(principal, cal_id)
.await
@@ -151,7 +152,7 @@ impl CalendarStore for CombinedCalendarStore {
&self,
principal: String,
cal_id: String,
objects: Vec<rustical_ical::CalendarObject>,
objects: Vec<(String, CalendarObject)>,
overwrite: bool,
) -> Result<(), crate::Error> {
self.store_for_id(&cal_id)

View File

@@ -34,10 +34,14 @@ fn benchmark(c: &mut Criterion) {
cal_store
});
let object = CalendarObject::from_ics(include_str!("ical_event.ics").to_owned(), None).unwrap();
let object = CalendarObject::from_ics(include_str!("ical_event.ics").to_owned()).unwrap();
let batch_size = 1000;
let objects: Vec<_> = std::iter::repeat_n(object.clone(), batch_size).collect();
let objects: Vec<_> = std::iter::repeat_n(
(object.get_inner().get_uid().to_owned(), object.clone()),
batch_size,
)
.collect();
c.bench_function("put_batch", |b| {
b.to_async(&runtime).iter(async || {
@@ -54,7 +58,12 @@ fn benchmark(c: &mut Criterion) {
// yeet
for _ in 0..1000 {
cal_store
.put_object("user".to_owned(), "okwow".to_owned(), object.clone(), true)
.put_object(
"user".to_owned(),
"okwow".to_owned(),
(object.get_inner().get_uid().to_owned(), object.clone()),
true,
)
.await
.unwrap();
}

View File

@@ -8,7 +8,6 @@ use rustical_store::{
};
use sha2::{Digest, Sha256};
use sqlx::{Executor, Sqlite};
use std::collections::HashMap;
use tracing::instrument;
pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
@@ -312,7 +311,7 @@ impl CalendarStore for SqliteAddressbookStore {
async fn import_calendar(
&self,
_calendar: Calendar,
_objects: Vec<CalendarObject>,
_objects: Vec<(String, CalendarObject)>,
_merge_existing: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)
@@ -324,17 +323,19 @@ impl CalendarStore for SqliteAddressbookStore {
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let (objects, deleted_objects, new_synctoken) =
AddressbookStore::sync_changes(self, principal, cal_id, synctoken).await?;
let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
let objects = objects
.iter()
.map(AddressObject::get_birthday_object)
.map(AddressObject::get_significant_dates)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.collect();
let objects = objects?.into_iter().flatten().collect();
Ok((objects, deleted_objects, new_synctoken))
}
@@ -356,22 +357,18 @@ impl CalendarStore for SqliteAddressbookStore {
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error> {
) -> Result<Vec<(String, CalendarObject)>, Error> {
let cal_id = cal_id
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
AddressbookStore::get_objects(self, principal, cal_id)
.await?
.iter()
.map(AddressObject::get_significant_dates)
.collect();
let objects = objects?
Ok(AddressbookStore::get_objects(self, principal, cal_id)
.await?
.iter()
.map(AddressObject::get_significant_dates)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flat_map(HashMap::into_values)
.collect();
Ok(objects)
.flatten()
.collect())
}
#[instrument]
@@ -386,11 +383,14 @@ impl CalendarStore for SqliteAddressbookStore {
.strip_prefix(BIRTHDAYS_PREFIX)
.ok_or(Error::NotFound)?;
let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted)
.await?
.get_significant_dates()?
.remove(date_type)
.ok_or(Error::NotFound)
let addr_object =
AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted)
.await?;
match date_type {
"birthday" => addr_object.get_birthday_object()?.ok_or(Error::NotFound),
"anniversary" => addr_object.get_anniversary_object()?.ok_or(Error::NotFound),
_ => Err(Error::NotFound),
}
}
#[instrument]
@@ -398,7 +398,7 @@ impl CalendarStore for SqliteAddressbookStore {
&self,
_principal: String,
_cal_id: String,
_objects: Vec<CalendarObject>,
_objects: Vec<(String, CalendarObject)>,
_overwrite: bool,
) -> Result<(), Error> {
Err(Error::ReadOnly)

View File

@@ -21,11 +21,21 @@ struct CalendarObjectRow {
uid: String,
}
impl TryFrom<CalendarObjectRow> for (String, CalendarObject) {
type Error = rustical_store::Error;
fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> {
let object_id = value.id.clone();
Ok((object_id, value.try_into()?))
}
}
impl TryFrom<CalendarObjectRow> for CalendarObject {
type Error = rustical_store::Error;
fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> {
let object = Self::from_ics(value.ics, Some(value.id))?;
let object = Self::from_ics(value.ics)?;
if object.get_inner().get_uid() != value.uid {
return Err(rustical_store::Error::IcalError(
rustical_ical::Error::InvalidData(format!(
@@ -379,7 +389,7 @@ impl SqliteCalendarStore {
executor: E,
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error> {
) -> Result<Vec<(String, CalendarObject)>, Error> {
sqlx::query_as!(
CalendarObjectRow,
"SELECT id, uid, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
@@ -389,7 +399,7 @@ impl SqliteCalendarStore {
.fetch_all(executor)
.await.map_err(crate::Error::from)?
.into_iter()
.map(std::convert::TryInto::try_into)
.map(TryInto::try_into)
.collect()
}
@@ -398,7 +408,7 @@ impl SqliteCalendarStore {
principal: &str,
cal_id: &str,
query: CalendarQuery,
) -> Result<Vec<CalendarObject>, Error> {
) -> Result<Vec<(String, CalendarObject)>, 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
@@ -423,7 +433,7 @@ impl SqliteCalendarStore {
.await
.map_err(crate::Error::from)?
.into_iter()
.map(std::convert::TryInto::try_into)
.map(TryInto::try_into)
.collect()
}
@@ -453,14 +463,11 @@ impl SqliteCalendarStore {
executor: E,
principal: &str,
cal_id: &str,
object_id: &str,
object: &CalendarObject,
overwrite: bool,
) -> Result<(), Error> {
let (object_id, uid, ics) = (
object.get_id(),
object.get_inner().get_uid(),
object.get_ics(),
);
let (uid, ics) = (object.get_inner().get_uid(), object.get_ics());
let first_occurence = object
.get_inner()
@@ -567,7 +574,7 @@ impl SqliteCalendarStore {
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), Error> {
struct Row {
object_id: String,
synctoken: i64,
@@ -594,7 +601,7 @@ impl SqliteCalendarStore {
for Row { object_id, .. } in changes {
match Self::_get_object(&mut *conn, principal, cal_id, &object_id, false).await {
Ok(object) => objects.push(object),
Ok(object) => objects.push((object_id, object)),
Err(rustical_store::Error::NotFound) => deleted_objects.push(object_id),
Err(err) => return Err(err),
}
@@ -679,7 +686,7 @@ impl CalendarStore for SqliteCalendarStore {
async fn import_calendar(
&self,
calendar: Calendar,
objects: Vec<CalendarObject>,
objects: Vec<(String, CalendarObject)>,
merge_existing: bool,
) -> Result<(), Error> {
let mut tx = self
@@ -702,15 +709,23 @@ impl CalendarStore for SqliteCalendarStore {
}
let mut sync_token = None;
for object in objects {
Self::_put_object(&mut *tx, &calendar.principal, &calendar.id, &object, false).await?;
for (object_id, object) in objects {
Self::_put_object(
&mut *tx,
&calendar.principal,
&calendar.id,
&object_id,
&object,
false,
)
.await?;
sync_token = Some(
Self::log_object_operation(
&mut tx,
&calendar.principal,
&calendar.id,
object.get_id(),
&object_id,
ChangeOperation::Add,
)
.await?,
@@ -736,7 +751,7 @@ impl CalendarStore for SqliteCalendarStore {
principal: &str,
cal_id: &str,
query: CalendarQuery,
) -> Result<Vec<CalendarObject>, Error> {
) -> Result<Vec<(String, CalendarObject)>, Error> {
Self::_calendar_query(&self.db, principal, cal_id, query).await
}
@@ -767,7 +782,7 @@ impl CalendarStore for SqliteCalendarStore {
&self,
principal: &str,
cal_id: &str,
) -> Result<Vec<CalendarObject>, Error> {
) -> Result<Vec<(String, CalendarObject)>, Error> {
Self::_get_objects(&self.db, principal, cal_id).await
}
@@ -787,7 +802,7 @@ impl CalendarStore for SqliteCalendarStore {
&self,
principal: String,
cal_id: String,
objects: Vec<CalendarObject>,
objects: Vec<(String, CalendarObject)>,
overwrite: bool,
) -> Result<(), Error> {
let mut tx = self
@@ -803,18 +818,21 @@ impl CalendarStore for SqliteCalendarStore {
}
let mut sync_token = None;
for object in objects {
for (object_id, object) in objects {
sync_token = Some(
Self::log_object_operation(
&mut tx,
&principal,
&cal_id,
object.get_id(),
&object_id,
ChangeOperation::Add,
)
.await?,
);
Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).await?;
Self::_put_object(
&mut *tx, &principal, &cal_id, &object_id, &object, overwrite,
)
.await?;
}
tx.commit().await.map_err(crate::Error::from)?;
@@ -892,7 +910,7 @@ impl CalendarStore for SqliteCalendarStore {
principal: &str,
cal_id: &str,
synctoken: i64,
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
) -> Result<(Vec<(String, CalendarObject)>, Vec<String>, i64), Error> {
Self::_sync_changes(&self.db, principal, cal_id, synctoken).await
}

View File

@@ -50,4 +50,192 @@ expression: body
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
<href>/caldav/principal/user/calendar/</href>
<propstat>
<prop>
<calendar-color xmlns="http://apple.com/ns/ical/">#00FF00</calendar-color>
<CAL:calendar-description>Description</CAL:calendar-description>
<CAL:calendar-timezone>BEGIN:VCALENDAR
PRODID:-//github.com/lennart-k/vzic-rs//RustiCal Calendar server//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
LAST-MODIFIED:20250723T190331Z
X-LIC-LOCATION:Europe/Berlin
X-PROLEPTIC-TZNAME:LMT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+005328
TZOFFSETTO:+0100
DTSTART:18930401T000000
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19160430T230000
RDATE:19400401T020000
RDATE:19430329T020000
RDATE:19460414T020000
RDATE:19470406T030000
RDATE:19480418T020000
RDATE:19490410T020000
RDATE:19800406T020000
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19161001T010000
RDATE:19421102T030000
RDATE:19431004T030000
RDATE:19441002T030000
RDATE:19451118T030000
RDATE:19461007T030000
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19170416T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=3MO;UNTIL=19180415T010000Z
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19170917T030000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=3MO;UNTIL=19180916T010000Z
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19440403T020000
RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1MO;UNTIL=19450402T010000Z
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEMT
TZOFFSETFROM:+0200
TZOFFSETTO:+0300
DTSTART:19450524T020000
RDATE:19470511T030000
END:DAYLIGHT
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0300
TZOFFSETTO:+0200
DTSTART:19450924T030000
RDATE:19470629T030000
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0100
TZOFFSETTO:+0100
DTSTART:19460101T000000
RDATE:19800101T000000
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19471005T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU;UNTIL=19491002T010000Z
END:STANDARD
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19800928T030000
RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU;UNTIL=19950924T010000Z
END:STANDARD
BEGIN:DAYLIGHT
TZNAME:CEST
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
DTSTART:19810329T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
END:DAYLIGHT
BEGIN:STANDARD
TZNAME:CET
TZOFFSETFROM:+0200
TZOFFSETTO:+0100
DTSTART:19961027T030000
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
END:STANDARD
END:VTIMEZONE
END:VCALENDAR
</CAL:calendar-timezone>
<CAL:timezone-service-set>
<href>https://www.iana.org/time-zones</href>
</CAL:timezone-service-set>
<CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
<calendar-order xmlns="http://apple.com/ns/ical/">0</calendar-order>
<CAL:supported-calendar-component-set>
<CAL:comp name="VEVENT"/>
<CAL:comp name="VTODO"/>
<CAL:comp name="VJOURNAL"/>
</CAL:supported-calendar-component-set>
<CAL:supported-calendar-data>
<CAL:calendar-data content-type="text/calendar" version="2.0"/>
</CAL:supported-calendar-data>
<CAL:supported-collation-set>
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
<CAL:supported-collation>i;octet</CAL:supported-collation>
</CAL:supported-collation-set>
<max-resource-size>10000000</max-resource-size>
<supported-report-set>
<supported-report>
<report>
<CAL:calendar-query/>
</report>
</supported-report>
<supported-report>
<report>
<CAL:calendar-multiget/>
</report>
</supported-report>
<supported-report>
<report>
<sync-collection/>
</report>
</supported-report>
</supported-report-set>
<CAL:min-date-time>-2621430101T000000Z</CAL:min-date-time>
<CAL:max-date-time>+2621421231T235959Z</CAL:max-date-time>
<sync-token>github.com/lennart-k/rustical/ns/0</sync-token>
<CS:getctag>github.com/lennart-k/rustical/ns/0</CS:getctag>
<PUSH:transports>
<PUSH:web-push/>
</PUSH:transports>
<PUSH:topic>[PUSH_TOPIC]</PUSH:topic>
<PUSH:supported-triggers>
<PUSH:content-update>
<depth>1</depth>
</PUSH:content-update>
<PUSH:property-update>
<depth>1</depth>
</PUSH:property-update>
</PUSH:supported-triggers>
<resourcetype>
<collection/>
<CAL:calendar/>
</resourcetype>
<displayname>Calendar</displayname>
<current-user-principal>
<href>/caldav/principal/user/</href>
</current-user-principal>
<current-user-privilege-set>
<privilege>
<all/>
</privilege>
</current-user-privilege-set>
<owner>
<href>/caldav/principal/user/</href>
</owner>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>