mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-27 14:09:24 +00:00
Compare commits
8 Commits
v0.9.14
...
9f911fe5d7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f911fe5d7 | ||
|
|
6361907152 | ||
|
|
0c0be859f9 | ||
|
|
d2c786eba6 | ||
|
|
dabddc6282 | ||
|
|
76b4194b94 | ||
|
|
db144ebcae | ||
|
|
8ed4db5824 |
32
.sqlx/query-3a29efff3d3f6e1e05595d1a2d095af5fc963572c90bd10a6616af78757f8c39.json
generated
Normal file
32
.sqlx/query-3a29efff3d3f6e1e05595d1a2d095af5fc963572c90bd10a6616af78757f8c39.json
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, uid, 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": "uid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ics",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 6
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3a29efff3d3f6e1e05595d1a2d095af5fc963572c90bd10a6616af78757f8c39"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "REPLACE INTO calendarobjects (principal, cal_id, id, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, date(?), date(?), ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 8
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3e1cca532372e891ab3e604ecb79311d8cd64108d4f238db4c79e9467a3b6d2e"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
|
||||
"query": "SELECT id, uid, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -9,18 +9,24 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ics",
|
||||
"name": "uid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ics",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "543838c030550cb09d1af08adfeade8b7ce3575d92fddbc6e9582d141bc9e49d"
|
||||
"hash": "505ebe8e64ac709b230dce7150240965e45442aca6c5f3b3115738ef508939ed"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO calendarobjects (principal, cal_id, id, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, date(?), date(?), ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 8
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "6327bee90e5df01536a0ddb15adcc37af3027f6902aa3786365c5ab2fbf06bda"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
|
||||
"query": "SELECT id, uid, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -9,18 +9,24 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ics",
|
||||
"name": "uid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ics",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "54c9c0e36a52e6963f11c6aa27f13aafb4204b8aa34b664fd825bd447db80e86"
|
||||
"hash": "804ed2a4a7032e9605d1871297498f5a96de0fc816ce660c705fb28318be0d42"
|
||||
}
|
||||
12
.sqlx/query-a68a1b96189b854a7ba2a3cd866ba583af5ad84bc1cd8b20cb805e9ce3bad820.json
generated
Normal file
12
.sqlx/query-a68a1b96189b854a7ba2a3cd866ba583af5ad84bc1cd8b20cb805e9ce3bad820.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "REPLACE INTO calendarobjects (principal, cal_id, id, uid, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, ?, date(?), date(?), ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 9
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a68a1b96189b854a7ba2a3cd866ba583af5ad84bc1cd8b20cb805e9ce3bad820"
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
12
.sqlx/query-d498a758ed707408b00b7d2675250ea739a681ce1f009f05e97f2e101bd7e556.json
generated
Normal file
12
.sqlx/query-d498a758ed707408b00b7d2675250ea739a681ce1f009f05e97f2e101bd7e556.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "INSERT INTO calendarobjects (principal, cal_id, id, uid, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, ?, date(?), date(?), ?, ?)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 9
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d498a758ed707408b00b7d2675250ea739a681ce1f009f05e97f2e101bd7e556"
|
||||
}
|
||||
22
Cargo.lock
generated
22
Cargo.lock
generated
@@ -2974,7 +2974,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -3017,7 +3017,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_caldav"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
@@ -3057,7 +3057,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_carddav"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3089,7 +3089,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3114,7 +3114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav_push"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3139,7 +3139,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_frontend"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"askama_web",
|
||||
@@ -3172,7 +3172,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_ical"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -3189,7 +3189,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_oidc"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3205,7 +3205,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3238,7 +3238,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store_sqlite"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -3259,7 +3259,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_xml"
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"thiserror 2.0.17",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.9.14"
|
||||
version = "0.10.0"
|
||||
edition = "2024"
|
||||
description = "A CalDAV server"
|
||||
documentation = "https://lennart-k.github.io/rustical/"
|
||||
|
||||
@@ -82,7 +82,7 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
|
||||
let objects = expanded_cals
|
||||
.into_iter()
|
||||
.map(|cal| cal.generate())
|
||||
.map(CalendarObject::from_ics)
|
||||
.map(|ics| CalendarObject::from_ics(ics, None))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let new_cal = Calendar {
|
||||
principal,
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
use crate::calendar::methods::report::calendar_query::{
|
||||
TimeRangeElement,
|
||||
prop_filter::{PropFilterElement, PropFilterable},
|
||||
};
|
||||
use rustical_ical::{CalendarObject, CalendarObjectComponent, CalendarObjectType};
|
||||
use rustical_xml::XmlDeserialize;
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1
|
||||
pub struct CompFilterElement {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) is_not_defined: Option<()>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) time_range: Option<TimeRangeElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||
pub(crate) prop_filter: Vec<PropFilterElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||
pub(crate) comp_filter: Vec<CompFilterElement>,
|
||||
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
pub trait CompFilterable: PropFilterable + Sized {
|
||||
fn get_comp_name(&self) -> &'static str;
|
||||
|
||||
fn match_time_range(&self, time_range: &TimeRangeElement) -> bool;
|
||||
|
||||
fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool;
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1
|
||||
// The scope of the
|
||||
// CALDAV:comp-filter XML element is the calendar object when used as
|
||||
// a child of the CALDAV:filter XML element. The scope of the
|
||||
// CALDAV:comp-filter XML element is the enclosing calendar component
|
||||
// when used as a child of another CALDAV:comp-filter XML element
|
||||
fn matches(&self, comp_filter: &CompFilterElement) -> bool {
|
||||
let name_matches = self.get_comp_name() == comp_filter.name;
|
||||
match (comp_filter.is_not_defined.is_some(), name_matches) {
|
||||
// We are the component that's not supposed to be defined
|
||||
(true, true)
|
||||
// We don't match
|
||||
| (false, false) => return false,
|
||||
// We shall not be and indeed we aren't
|
||||
(true, false) => return true,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(time_range) = comp_filter.time_range.as_ref()
|
||||
&& !self.match_time_range(time_range)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for prop_filter in &comp_filter.prop_filter {
|
||||
if !prop_filter.match_component(self) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
comp_filter
|
||||
.comp_filter
|
||||
.iter()
|
||||
.all(|filter| self.match_subcomponents(filter))
|
||||
}
|
||||
}
|
||||
|
||||
impl CompFilterable for CalendarObject {
|
||||
fn get_comp_name(&self) -> &'static str {
|
||||
"VCALENDAR"
|
||||
}
|
||||
|
||||
fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool {
|
||||
// VCALENDAR has no concept of time range
|
||||
false
|
||||
}
|
||||
|
||||
fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool {
|
||||
self.get_data().matches(comp_filter)
|
||||
}
|
||||
}
|
||||
|
||||
impl CompFilterable for CalendarObjectComponent {
|
||||
fn get_comp_name(&self) -> &'static str {
|
||||
CalendarObjectType::from(self).as_str()
|
||||
}
|
||||
|
||||
fn match_time_range(&self, time_range: &TimeRangeElement) -> bool {
|
||||
if let Some(start) = &time_range.start
|
||||
&& let Some(last_occurence) = self.get_last_occurence().unwrap_or(None)
|
||||
&& **start > last_occurence.utc()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(end) = &time_range.end
|
||||
&& let Some(first_occurence) = self.get_first_occurence().unwrap_or(None)
|
||||
&& **end < first_occurence.utc()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn match_subcomponents(&self, _comp_filter: &CompFilterElement) -> bool {
|
||||
// TODO: Properly check subcomponents
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{TimeZone, Utc};
|
||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||
|
||||
use crate::calendar::methods::report::calendar_query::{
|
||||
CompFilterable, TextMatchElement, TimeRangeElement, comp_filter::CompFilterElement,
|
||||
prop_filter::PropFilterElement,
|
||||
};
|
||||
|
||||
const ICS: &str = r"BEGIN:VCALENDAR
|
||||
CALSCALE:GREGORIAN
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Berlin
|
||||
X-LIC-LOCATION:Europe/Berlin
|
||||
END:VTIMEZONE
|
||||
|
||||
BEGIN:VEVENT
|
||||
UID:318ec6503573d9576818daf93dac07317058d95c
|
||||
DTSTAMP:20250502T132758Z
|
||||
DTSTART;TZID=Europe/Berlin:20250506T090000
|
||||
DTEND;TZID=Europe/Berlin:20250506T092500
|
||||
SEQUENCE:2
|
||||
SUMMARY:weekly stuff
|
||||
TRANSP:OPAQUE
|
||||
RRULE:FREQ=WEEKLY;COUNT=4;INTERVAL=2;BYDAY=TU,TH,SU
|
||||
END:VEVENT
|
||||
END:VCALENDAR";
|
||||
|
||||
#[test]
|
||||
fn test_comp_filter_matching() {
|
||||
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
|
||||
|
||||
let comp_filter = CompFilterElement {
|
||||
is_not_defined: Some(()),
|
||||
name: "VCALENDAR".to_string(),
|
||||
time_range: None,
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![],
|
||||
};
|
||||
assert!(!object.matches(&comp_filter), "filter: wants no VCALENDAR");
|
||||
|
||||
let comp_filter = CompFilterElement {
|
||||
is_not_defined: None,
|
||||
name: "VCALENDAR".to_string(),
|
||||
time_range: None,
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![CompFilterElement {
|
||||
name: "VTODO".to_string(),
|
||||
is_not_defined: None,
|
||||
time_range: None,
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![],
|
||||
}],
|
||||
};
|
||||
assert!(!object.matches(&comp_filter), "filter matches VTODO");
|
||||
|
||||
let comp_filter = CompFilterElement {
|
||||
is_not_defined: None,
|
||||
name: "VCALENDAR".to_string(),
|
||||
time_range: None,
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![CompFilterElement {
|
||||
name: "VEVENT".to_string(),
|
||||
is_not_defined: None,
|
||||
time_range: None,
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![],
|
||||
}],
|
||||
};
|
||||
assert!(object.matches(&comp_filter), "filter matches VEVENT");
|
||||
|
||||
let comp_filter = CompFilterElement {
|
||||
is_not_defined: None,
|
||||
name: "VCALENDAR".to_string(),
|
||||
time_range: None,
|
||||
prop_filter: vec![
|
||||
PropFilterElement {
|
||||
is_not_defined: None,
|
||||
name: "VERSION".to_string(),
|
||||
time_range: None,
|
||||
text_match: Some(TextMatchElement {
|
||||
needle: "2.0".to_string(),
|
||||
collation: None,
|
||||
negate_condition: None,
|
||||
}),
|
||||
param_filter: vec![],
|
||||
},
|
||||
PropFilterElement {
|
||||
is_not_defined: Some(()),
|
||||
name: "STUFF".to_string(),
|
||||
time_range: None,
|
||||
text_match: None,
|
||||
param_filter: vec![],
|
||||
},
|
||||
],
|
||||
comp_filter: vec![CompFilterElement {
|
||||
name: "VEVENT".to_string(),
|
||||
is_not_defined: None,
|
||||
time_range: None,
|
||||
prop_filter: vec![PropFilterElement {
|
||||
is_not_defined: None,
|
||||
name: "SUMMARY".to_string(),
|
||||
time_range: None,
|
||||
text_match: Some(TextMatchElement {
|
||||
collation: None,
|
||||
negate_condition: None,
|
||||
needle: "weekly".to_string(),
|
||||
}),
|
||||
param_filter: vec![],
|
||||
}],
|
||||
comp_filter: vec![],
|
||||
}],
|
||||
};
|
||||
assert!(
|
||||
object.matches(&comp_filter),
|
||||
"Some prop filters on VCALENDAR and VEVENT"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_comp_filter_time_range() {
|
||||
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
|
||||
|
||||
let comp_filter = CompFilterElement {
|
||||
is_not_defined: None,
|
||||
name: "VCALENDAR".to_string(),
|
||||
time_range: None,
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![CompFilterElement {
|
||||
name: "VEVENT".to_string(),
|
||||
is_not_defined: None,
|
||||
time_range: Some(TimeRangeElement {
|
||||
start: Some(UtcDateTime(
|
||||
Utc.with_ymd_and_hms(2025, 4, 1, 0, 0, 0).unwrap(),
|
||||
)),
|
||||
end: Some(UtcDateTime(
|
||||
Utc.with_ymd_and_hms(2025, 8, 1, 0, 0, 0).unwrap(),
|
||||
)),
|
||||
}),
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![],
|
||||
}],
|
||||
};
|
||||
assert!(
|
||||
object.matches(&comp_filter),
|
||||
"event should lie in time range"
|
||||
);
|
||||
|
||||
let comp_filter = CompFilterElement {
|
||||
is_not_defined: None,
|
||||
name: "VCALENDAR".to_string(),
|
||||
time_range: None,
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![CompFilterElement {
|
||||
name: "VEVENT".to_string(),
|
||||
is_not_defined: None,
|
||||
time_range: Some(TimeRangeElement {
|
||||
start: Some(UtcDateTime(
|
||||
Utc.with_ymd_and_hms(2024, 4, 1, 0, 0, 0).unwrap(),
|
||||
)),
|
||||
end: Some(UtcDateTime(
|
||||
Utc.with_ymd_and_hms(2024, 8, 1, 0, 0, 0).unwrap(),
|
||||
)),
|
||||
}),
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![],
|
||||
}],
|
||||
};
|
||||
assert!(
|
||||
!object.matches(&comp_filter),
|
||||
"event should not lie in time range"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::calendar_object::CalendarObjectPropWrapperName;
|
||||
use crate::{
|
||||
calendar::methods::report::calendar_query::comp_filter::{CompFilterElement, CompFilterable},
|
||||
calendar_object::CalendarObjectPropWrapperName,
|
||||
};
|
||||
use rustical_dav::xml::PropfindType;
|
||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||
use rustical_store::calendar_store::CalendarQuery;
|
||||
@@ -30,106 +33,12 @@ pub struct ParamFilterElement {
|
||||
#[allow(dead_code)]
|
||||
pub struct TextMatchElement {
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) collation: String,
|
||||
pub(crate) collation: Option<String>,
|
||||
#[xml(ty = "attr")]
|
||||
// "yes" or "no", default: "no"
|
||||
pub(crate) negate_condition: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2
|
||||
pub struct PropFilterElement {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) is_not_defined: Option<()>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) time_range: Option<TimeRangeElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) text_match: Option<TextMatchElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||
pub(crate) param_filter: Vec<ParamFilterElement>,
|
||||
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1
|
||||
pub struct CompFilterElement {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) is_not_defined: Option<()>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) time_range: Option<TimeRangeElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||
pub(crate) prop_filter: Vec<PropFilterElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||
pub(crate) comp_filter: Vec<CompFilterElement>,
|
||||
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
impl CompFilterElement {
|
||||
// match the VCALENDAR part
|
||||
pub fn matches_root(&self, cal_object: &CalendarObject) -> bool {
|
||||
let comp_vcal = self.name == "VCALENDAR";
|
||||
match (self.is_not_defined, comp_vcal) {
|
||||
// Client wants VCALENDAR to not exist but we are a VCALENDAR
|
||||
(Some(()), true) |
|
||||
// Client is asking for something different than a vcalendar
|
||||
(None, false) => return false,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if self.time_range.is_some() {
|
||||
// <time-range> should be applied on VEVENT/VTODO but not on VCALENDAR
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Implement prop-filter at some point
|
||||
|
||||
// Apply sub-comp-filters on VEVENT/VTODO/VJOURNAL component
|
||||
if self
|
||||
.comp_filter
|
||||
.iter()
|
||||
.all(|filter| filter.matches(cal_object))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
// match the VEVENT/VTODO/VJOURNAL part
|
||||
pub fn matches(&self, cal_object: &CalendarObject) -> bool {
|
||||
let comp_name_matches = self.name == cal_object.get_component_name();
|
||||
match (self.is_not_defined, comp_name_matches) {
|
||||
// Client wants VCALENDAR to not exist but we are a VCALENDAR
|
||||
(Some(()), true) |
|
||||
// Client is asking for something different than a vcalendar
|
||||
(None, false) => return false,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// TODO: Implement prop-filter (and comp-filter?) at some point
|
||||
|
||||
if let Some(time_range) = &self.time_range {
|
||||
if let Some(start) = &time_range.start
|
||||
&& let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None)
|
||||
&& **start > last_occurence.utc()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(end) = &time_range.end
|
||||
&& let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None)
|
||||
&& **end < first_occurence.utc()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
#[xml(ty = "text")]
|
||||
pub(crate) needle: String,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
@@ -143,7 +52,7 @@ pub struct FilterElement {
|
||||
|
||||
impl FilterElement {
|
||||
pub fn matches(&self, cal_object: &CalendarObject) -> bool {
|
||||
self.comp_filter.matches_root(cal_object)
|
||||
cal_object.matches(&self.comp_filter)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,14 @@ use crate::Error;
|
||||
use rustical_ical::CalendarObject;
|
||||
use rustical_store::CalendarStore;
|
||||
|
||||
mod comp_filter;
|
||||
mod elements;
|
||||
mod prop_filter;
|
||||
#[allow(unused_imports)]
|
||||
pub use comp_filter::{CompFilterElement, CompFilterable};
|
||||
pub use elements::*;
|
||||
#[allow(unused_imports)]
|
||||
pub use prop_filter::{PropFilterElement, PropFilterable};
|
||||
|
||||
pub async fn get_objects_calendar_query<C: CalendarStore>(
|
||||
cal_query: &CalendarQueryRequest,
|
||||
@@ -29,8 +35,8 @@ mod tests {
|
||||
calendar::methods::report::{
|
||||
ReportRequest,
|
||||
calendar_query::{
|
||||
CalendarQueryRequest, CompFilterElement, FilterElement, ParamFilterElement,
|
||||
PropFilterElement, TextMatchElement,
|
||||
CalendarQueryRequest, FilterElement, ParamFilterElement, TextMatchElement,
|
||||
comp_filter::CompFilterElement, prop_filter::PropFilterElement,
|
||||
},
|
||||
},
|
||||
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
||||
@@ -90,16 +96,18 @@ mod tests {
|
||||
prop_filter: vec![PropFilterElement {
|
||||
name: "ATTENDEE".to_owned(),
|
||||
text_match: Some(TextMatchElement {
|
||||
collation: "i;ascii-casemap".to_owned(),
|
||||
negate_condition: None
|
||||
collation: Some("i;ascii-casemap".to_owned()),
|
||||
negate_condition: None,
|
||||
needle: "mailto:lisa@example.com".to_string()
|
||||
}),
|
||||
is_not_defined: None,
|
||||
param_filter: vec![ParamFilterElement {
|
||||
is_not_defined: None,
|
||||
name: "PARTSTAT".to_owned(),
|
||||
text_match: Some(TextMatchElement {
|
||||
collation: "i;ascii-casemap".to_owned(),
|
||||
negate_condition: None
|
||||
collation: Some("i;ascii-casemap".to_owned()),
|
||||
negate_condition: None,
|
||||
needle: "NEEDS-ACTION".to_string()
|
||||
}),
|
||||
}],
|
||||
time_range: None
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ical::{
|
||||
generator::{IcalCalendar, IcalEvent},
|
||||
parser::{
|
||||
Component,
|
||||
ical::component::{IcalJournal, IcalTodo},
|
||||
},
|
||||
property::Property,
|
||||
};
|
||||
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectComponent, UtcDateTime};
|
||||
use rustical_xml::XmlDeserialize;
|
||||
|
||||
use crate::calendar::methods::report::calendar_query::{
|
||||
ParamFilterElement, TextMatchElement, TimeRangeElement,
|
||||
};
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2
|
||||
pub struct PropFilterElement {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) is_not_defined: Option<()>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) time_range: Option<TimeRangeElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) text_match: Option<TextMatchElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||
pub(crate) param_filter: Vec<ParamFilterElement>,
|
||||
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
impl PropFilterElement {
|
||||
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
|
||||
let property = comp.get_property(&self.name);
|
||||
let property = match (self.is_not_defined.is_some(), property) {
|
||||
// We are the component that's not supposed to be defined
|
||||
(true, Some(_))
|
||||
// We don't match
|
||||
| (false, None) => return false,
|
||||
// We shall not be and indeed we aren't
|
||||
(true, None) => return true,
|
||||
(false, Some(property)) => property
|
||||
};
|
||||
|
||||
if let Some(TimeRangeElement { start, end }) = &self.time_range {
|
||||
// TODO: Respect timezones
|
||||
let Ok(timestamp) = CalDateTime::parse_prop(property, &HashMap::default()) else {
|
||||
return false;
|
||||
};
|
||||
let timestamp = timestamp.utc();
|
||||
if let Some(UtcDateTime(start)) = start
|
||||
&& start > ×tamp
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(UtcDateTime(end)) = end
|
||||
&& end < ×tamp
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(TextMatchElement {
|
||||
collation: _collation,
|
||||
negate_condition,
|
||||
needle,
|
||||
}) = &self.text_match
|
||||
{
|
||||
let mut matches = property
|
||||
.value
|
||||
.as_ref()
|
||||
.is_some_and(|haystack| haystack.contains(needle));
|
||||
match negate_condition.as_deref() {
|
||||
None | Some("no") => {}
|
||||
Some("yes") => {
|
||||
matches = !matches;
|
||||
}
|
||||
// Invalid value
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
if !matches {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: param-filter
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PropFilterable {
|
||||
fn get_property(&self, name: &str) -> Option<&Property>;
|
||||
}
|
||||
|
||||
impl PropFilterable for CalendarObject {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
Self::get_property(self, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropFilterable for IcalEvent {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
Component::get_property(self, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropFilterable for IcalTodo {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
Component::get_property(self, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropFilterable for IcalJournal {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
Component::get_property(self, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropFilterable for IcalCalendar {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
Component::get_property(self, name)
|
||||
}
|
||||
}
|
||||
|
||||
impl PropFilterable for CalendarObjectComponent {
|
||||
fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
match self {
|
||||
Self::Event(event, _) => PropFilterable::get_property(&event.event, name),
|
||||
Self::Todo(todo, _) => PropFilterable::get_property(todo, name),
|
||||
Self::Journal(journal, _) => PropFilterable::get_property(journal, name),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use rustical_ical::CalendarObject;
|
||||
use rustical_store::CalendarStore;
|
||||
use rustical_store::auth::Principal;
|
||||
use std::str::FromStr;
|
||||
use tracing::{debug, error, instrument};
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
#[instrument(skip(cal_store))]
|
||||
pub async fn get_event<C: CalendarStore>(
|
||||
@@ -78,18 +78,10 @@ pub async fn put_event<C: CalendarStore>(
|
||||
true
|
||||
};
|
||||
|
||||
let Ok(object) = CalendarObject::from_ics(body.clone()) else {
|
||||
let Ok(object) = CalendarObject::from_ics(body.clone(), Some(object_id)) else {
|
||||
debug!("invalid calendar data:\n{body}");
|
||||
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
|
||||
};
|
||||
if object.get_id() != object_id {
|
||||
error!(
|
||||
"Calendar object UID and file name not matching: UID={}, filename={}",
|
||||
object.get_id(),
|
||||
object_id
|
||||
);
|
||||
return Err(Error::PreconditionFailed(Precondition::MatchingUid));
|
||||
}
|
||||
cal_store
|
||||
.put_object(principal, calendar_id, object, overwrite)
|
||||
.await?;
|
||||
|
||||
@@ -12,8 +12,6 @@ pub enum Precondition {
|
||||
#[error("valid-calendar-data")]
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
ValidCalendarData,
|
||||
#[error("matching-uid")]
|
||||
MatchingUid,
|
||||
}
|
||||
|
||||
impl IntoResponse for Precondition {
|
||||
|
||||
@@ -97,7 +97,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!(
|
||||
Some(CalendarObject::from_ics(
|
||||
format!(
|
||||
r"BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
@@ -116,7 +117,9 @@ DESCRIPTION:💍 {fullname}{year_suffix}
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR",
|
||||
))?)
|
||||
),
|
||||
None,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
@@ -136,7 +139,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!(
|
||||
Some(CalendarObject::from_ics(
|
||||
format!(
|
||||
r"BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
@@ -155,7 +159,9 @@ DESCRIPTION:🎂 {fullname}{year_suffix}
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR",
|
||||
))?)
|
||||
),
|
||||
None,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
||||
@@ -251,7 +251,7 @@ END:VEVENT\r\n",
|
||||
|
||||
#[test]
|
||||
fn test_expand_recurrence() {
|
||||
let event = CalendarObject::from_ics(ICS.to_string()).unwrap();
|
||||
let event = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
|
||||
let crate::CalendarObjectComponent::Event(event, overrides) = event.get_data() else {
|
||||
panic!()
|
||||
};
|
||||
|
||||
@@ -64,6 +64,19 @@ pub enum CalendarObjectComponent {
|
||||
Journal(IcalJournal, Vec<IcalJournal>),
|
||||
}
|
||||
|
||||
impl CalendarObjectComponent {
|
||||
#[must_use]
|
||||
pub fn get_uid(&self) -> &str {
|
||||
match &self {
|
||||
// We've made sure before that the first component exists and all components share the
|
||||
// same UID
|
||||
Self::Todo(todo, _) => todo.get_uid(),
|
||||
Self::Event(event, _) => event.event.get_uid(),
|
||||
Self::Journal(journal, _) => journal.get_uid(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CalendarObjectComponent> for CalendarObjectType {
|
||||
fn from(value: &CalendarObjectComponent) -> Self {
|
||||
match value {
|
||||
@@ -135,18 +148,47 @@ impl CalendarObjectComponent {
|
||||
}
|
||||
Ok(Self::Journal(main_journal, overrides))
|
||||
}
|
||||
|
||||
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
match &self {
|
||||
Self::Event(main_event, overrides) => Ok(overrides
|
||||
.iter()
|
||||
.chain(std::iter::once(main_event))
|
||||
.map(super::event::EventObject::get_dtstart)
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.min()),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
match &self {
|
||||
Self::Event(main_event, overrides) => Ok(overrides
|
||||
.iter()
|
||||
.chain(std::iter::once(main_event))
|
||||
.map(super::event::EventObject::get_last_occurence)
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.max()),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CalendarObject {
|
||||
data: CalendarObjectComponent,
|
||||
properties: Vec<Property>,
|
||||
id: String,
|
||||
ics: String,
|
||||
vtimezones: HashMap<String, IcalTimeZone>,
|
||||
}
|
||||
|
||||
impl CalendarObject {
|
||||
pub fn from_ics(ics: String) -> Result<Self, Error> {
|
||||
pub fn from_ics(ics: String, id: Option<String>) -> Result<Self, Error> {
|
||||
let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes()));
|
||||
let cal = parser.next().ok_or(Error::MissingCalendar)??;
|
||||
if parser.next().is_some() {
|
||||
@@ -202,6 +244,7 @@ impl CalendarObject {
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
id: id.unwrap_or_else(|| data.get_uid().to_owned()),
|
||||
data,
|
||||
properties: cal.properties,
|
||||
ics,
|
||||
@@ -220,20 +263,19 @@ impl CalendarObject {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_id(&self) -> &str {
|
||||
match &self.data {
|
||||
// We've made sure before that the first component exists and all components share the
|
||||
// same UID
|
||||
CalendarObjectComponent::Todo(todo, _) => todo.get_uid(),
|
||||
CalendarObjectComponent::Event(event, _) => event.event.get_uid(),
|
||||
CalendarObjectComponent::Journal(journal, _) => journal.get_uid(),
|
||||
pub fn get_uid(&self) -> &str {
|
||||
self.data.get_uid()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_etag(&self) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(self.get_id());
|
||||
hasher.update(self.get_uid());
|
||||
hasher.update(self.get_ics());
|
||||
format!("\"{:x}\"", hasher.finalize())
|
||||
}
|
||||
@@ -254,31 +296,11 @@ impl CalendarObject {
|
||||
}
|
||||
|
||||
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
match &self.data {
|
||||
CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides
|
||||
.iter()
|
||||
.chain(std::iter::once(main_event))
|
||||
.map(super::event::EventObject::get_dtstart)
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.min()),
|
||||
_ => Ok(None),
|
||||
}
|
||||
self.data.get_first_occurence()
|
||||
}
|
||||
|
||||
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
match &self.data {
|
||||
CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides
|
||||
.iter()
|
||||
.chain(std::iter::once(main_event))
|
||||
.map(super::event::EventObject::get_last_occurence)
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.max()),
|
||||
_ => Ok(None),
|
||||
}
|
||||
self.data.get_last_occurence()
|
||||
}
|
||||
|
||||
pub fn expand_recurrence(
|
||||
@@ -299,4 +321,11 @@ impl CalendarObject {
|
||||
_ => Ok(self.get_ics().to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
self.properties
|
||||
.iter()
|
||||
.find(|property| property.name == name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ END:VCALENDAR
|
||||
|
||||
#[test]
|
||||
fn parse_calendar_object() {
|
||||
let object = CalendarObject::from_ics(MULTI_VEVENT.to_string()).unwrap();
|
||||
let object = CalendarObject::from_ics(MULTI_VEVENT.to_string(), None).unwrap();
|
||||
object.expand_recurrence(None, None).unwrap();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::synctoken::format_synctoken;
|
||||
use chrono::NaiveDateTime;
|
||||
use rustical_ical::CalendarObjectType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct CalendarMetadata {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
DROP INDEX idx_calobjs_uid;
|
||||
ALTER TABLE calendarobjects RENAME TO calendarobjects_old;
|
||||
|
||||
CREATE TABLE calendarobjects (
|
||||
principal TEXT NOT NULL,
|
||||
cal_id TEXT NOT NULL,
|
||||
id TEXT NOT NULL, -- filename
|
||||
ics TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME,
|
||||
|
||||
-- For more efficient calendar-queries
|
||||
first_occurence DATE,
|
||||
last_occurence DATE,
|
||||
etag TEXT,
|
||||
object_type INTEGER NOT NULL, -- VEVENT(0)/VTODO(1)/VJOURNAL(2)
|
||||
|
||||
CONSTRAINT pk_calendarobject_id PRIMARY KEY (principal, cal_id, id),
|
||||
CONSTRAINT fk_calendarobject_calendar FOREIGN KEY (principal, cal_id)
|
||||
REFERENCES calendars (principal, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT INTO calendarobjects (
|
||||
principal,
|
||||
cal_id,
|
||||
id,
|
||||
ics,
|
||||
updated_at,
|
||||
deleted_at,
|
||||
first_occurence,
|
||||
last_occurence,
|
||||
etag,
|
||||
object_type
|
||||
) SELECT
|
||||
principal,
|
||||
cal_id,
|
||||
id,
|
||||
ics,
|
||||
updated_at,
|
||||
deleted_at,
|
||||
first_occurence,
|
||||
last_occurence,
|
||||
etag,
|
||||
object_type
|
||||
FROM calendarobjects_old;
|
||||
|
||||
DROP TABLE calendarobjects_old;
|
||||
@@ -0,0 +1,53 @@
|
||||
-- Adds the column "uid" and populates it with data from "id"
|
||||
ALTER TABLE calendarobjects RENAME TO calendarobjects_old;
|
||||
|
||||
CREATE TABLE calendarobjects (
|
||||
principal TEXT NOT NULL,
|
||||
cal_id TEXT NOT NULL,
|
||||
id TEXT NOT NULL, -- filename
|
||||
"uid" TEXT NOT NULL, -- global identifier
|
||||
ics TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME,
|
||||
|
||||
-- For more efficient calendar-queries
|
||||
first_occurence DATE,
|
||||
last_occurence DATE,
|
||||
etag TEXT,
|
||||
object_type INTEGER NOT NULL, -- VEVENT(0)/VTODO(1)/VJOURNAL(2)
|
||||
|
||||
CONSTRAINT pk_calendarobject_id PRIMARY KEY (principal, cal_id, id),
|
||||
CONSTRAINT uq_calendarobject_uid UNIQUE (principal, cal_id, "uid"),
|
||||
CONSTRAINT fk_calendarobject_calendar FOREIGN KEY (principal, cal_id)
|
||||
REFERENCES calendars (principal, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_calobjs_uid ON calendarobjects (principal, cal_id, "uid");
|
||||
|
||||
INSERT INTO calendarobjects (
|
||||
principal,
|
||||
cal_id,
|
||||
id,
|
||||
"uid",
|
||||
ics,
|
||||
updated_at,
|
||||
deleted_at,
|
||||
first_occurence,
|
||||
last_occurence,
|
||||
etag,
|
||||
object_type
|
||||
) SELECT
|
||||
principal,
|
||||
cal_id,
|
||||
id,
|
||||
id AS "uid",
|
||||
ics,
|
||||
updated_at,
|
||||
deleted_at,
|
||||
first_occurence,
|
||||
last_occurence,
|
||||
etag,
|
||||
object_type
|
||||
FROM calendarobjects_old;
|
||||
|
||||
DROP TABLE calendarobjects_old;
|
||||
@@ -17,19 +17,20 @@ use tracing::{error, instrument};
|
||||
struct CalendarObjectRow {
|
||||
id: String,
|
||||
ics: String,
|
||||
uid: String,
|
||||
}
|
||||
|
||||
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)?;
|
||||
if object.get_id() != value.id {
|
||||
let object = Self::from_ics(value.ics, Some(value.id))?;
|
||||
if object.get_uid() != value.uid {
|
||||
return Err(rustical_store::Error::IcalError(
|
||||
rustical_ical::Error::InvalidData(format!(
|
||||
"object_id={} and UID={} don't match",
|
||||
object.get_id(),
|
||||
value.id
|
||||
"uid={} and UID={} don't match",
|
||||
value.uid,
|
||||
object.get_uid()
|
||||
)),
|
||||
));
|
||||
}
|
||||
@@ -281,7 +282,7 @@ impl SqliteCalendarStore {
|
||||
) -> Result<Vec<CalendarObject>, Error> {
|
||||
sqlx::query_as!(
|
||||
CalendarObjectRow,
|
||||
"SELECT id, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
|
||||
"SELECT id, uid, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
|
||||
principal,
|
||||
cal_id
|
||||
)
|
||||
@@ -306,7 +307,7 @@ impl SqliteCalendarStore {
|
||||
|
||||
sqlx::query_as!(
|
||||
CalendarObjectRow,
|
||||
r"SELECT id, ics FROM calendarobjects
|
||||
r"SELECT id, uid, 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(?))
|
||||
@@ -335,7 +336,7 @@ impl SqliteCalendarStore {
|
||||
) -> Result<CalendarObject, Error> {
|
||||
sqlx::query_as!(
|
||||
CalendarObjectRow,
|
||||
"SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
|
||||
"SELECT id, uid, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
|
||||
principal,
|
||||
cal_id,
|
||||
object_id,
|
||||
@@ -355,7 +356,7 @@ impl SqliteCalendarStore {
|
||||
object: CalendarObject,
|
||||
overwrite: bool,
|
||||
) -> Result<(), Error> {
|
||||
let (object_id, ics) = (object.get_id(), object.get_ics());
|
||||
let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics());
|
||||
|
||||
let first_occurence = object
|
||||
.get_first_occurence()
|
||||
@@ -374,10 +375,11 @@ impl SqliteCalendarStore {
|
||||
|
||||
(if overwrite {
|
||||
sqlx::query!(
|
||||
"REPLACE INTO calendarobjects (principal, cal_id, id, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, date(?), date(?), ?, ?)",
|
||||
"REPLACE INTO calendarobjects (principal, cal_id, id, uid, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, ?, date(?), date(?), ?, ?)",
|
||||
principal,
|
||||
cal_id,
|
||||
object_id,
|
||||
uid,
|
||||
ics,
|
||||
first_occurence,
|
||||
last_occurence,
|
||||
@@ -387,10 +389,11 @@ impl SqliteCalendarStore {
|
||||
} else {
|
||||
// If the object already exists a database error is thrown and handled in error.rs
|
||||
sqlx::query!(
|
||||
"INSERT INTO calendarobjects (principal, cal_id, id, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, date(?), date(?), ?, ?)",
|
||||
"INSERT INTO calendarobjects (principal, cal_id, id, uid, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, ?, date(?), date(?), ?, ?)",
|
||||
principal,
|
||||
cal_id,
|
||||
object_id,
|
||||
uid,
|
||||
ics,
|
||||
first_occurence,
|
||||
last_occurence,
|
||||
@@ -410,7 +413,7 @@ impl SqliteCalendarStore {
|
||||
executor: E,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
id: &str,
|
||||
object_id: &str,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Error> {
|
||||
if use_trashbin {
|
||||
@@ -418,7 +421,7 @@ impl SqliteCalendarStore {
|
||||
"UPDATE calendarobjects SET deleted_at = datetime(), updated_at = datetime() WHERE (principal, cal_id, id) = (?, ?, ?)",
|
||||
principal,
|
||||
cal_id,
|
||||
id
|
||||
object_id
|
||||
)
|
||||
.execute(executor)
|
||||
.await.map_err(crate::Error::from)?;
|
||||
@@ -426,7 +429,7 @@ impl SqliteCalendarStore {
|
||||
sqlx::query!(
|
||||
"DELETE FROM calendarobjects WHERE cal_id = ? AND id = ?",
|
||||
cal_id,
|
||||
id
|
||||
object_id
|
||||
)
|
||||
.execute(executor)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user