migrate to new ical-rs version

This commit is contained in:
Lennart K
2026-01-07 11:32:53 +01:00
parent d84158e8ad
commit 69acde10ba
23 changed files with 227 additions and 1498 deletions

View File

@@ -5,11 +5,9 @@ use axum::extract::State;
use axum::{extract::Path, response::Response};
use headers::{ContentType, HeaderMapExt};
use http::{HeaderValue, Method, StatusCode, header};
use ical::builder::calendar::IcalCalendarBuilder;
use ical::generator::Emitter;
use ical::property::Property;
use ical::property::ContentLine;
use percent_encoding::{CONTROLS, utf8_percent_encode};
use rustical_ical::{CalendarObjectComponent, EventObject};
use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
use std::collections::HashMap;
use std::str::FromStr;
@@ -33,77 +31,79 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
return Err(crate::Error::Unauthorized);
}
let mut vtimezones = HashMap::new();
let objects = cal_store.get_objects(&principal, &calendar_id).await?;
// let mut vtimezones = HashMap::new();
// let objects = cal_store.get_objects(&principal, &calendar_id).await?;
let mut ical_calendar_builder = IcalCalendarBuilder::version("2.0")
.gregorian()
.prodid("RustiCal");
if let Some(displayname) = calendar.meta.displayname {
ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-CALNAME".to_owned(),
value: Some(displayname),
params: vec![],
});
}
if let Some(description) = calendar.meta.description {
ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-CALDESC".to_owned(),
value: Some(description),
params: vec![],
});
}
if let Some(timezone_id) = calendar.timezone_id {
ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-TIMEZONE".to_owned(),
value: Some(timezone_id),
params: vec![],
});
}
todo!()
for object in &objects {
vtimezones.extend(object.get_vtimezones());
match object.get_data() {
CalendarObjectComponent::Event(EventObject { event, .. }, overrides) => {
ical_calendar_builder = ical_calendar_builder
.add_event(event.clone())
.add_events(overrides.iter().map(|ev| ev.event.clone()));
}
CalendarObjectComponent::Todo(todo, overrides) => {
ical_calendar_builder = ical_calendar_builder
.add_todo(todo.clone())
.add_todos(overrides.iter().cloned());
}
CalendarObjectComponent::Journal(journal, overrides) => {
ical_calendar_builder = ical_calendar_builder
.add_journal(journal.clone())
.add_journals(overrides.iter().cloned());
}
}
}
ical_calendar_builder = ical_calendar_builder.add_timezones(vtimezones.into_values().cloned());
let ical_calendar = ical_calendar_builder
.build()
.map_err(|parser_error| Error::IcalError(parser_error.into()))?;
let mut resp = Response::builder().status(StatusCode::OK);
let hdrs = resp.headers_mut().unwrap();
hdrs.typed_insert(ContentType::from_str("text/calendar; charset=utf-8").unwrap());
let filename = format!("{}_{}.ics", calendar.principal, calendar.id);
let filename = utf8_percent_encode(&filename, CONTROLS);
hdrs.insert(
header::CONTENT_DISPOSITION,
HeaderValue::from_str(&format!(
"attachement; filename*=UTF-8''{filename}; filename={filename}",
))
.unwrap(),
);
if matches!(method, Method::HEAD) {
Ok(resp.body(Body::empty()).unwrap())
} else {
Ok(resp.body(Body::new(ical_calendar.generate())).unwrap())
}
// let mut ical_calendar_builder = IcalCalendarBuilder::version("2.0")
// .gregorian()
// .prodid("RustiCal");
// if let Some(displayname) = calendar.meta.displayname {
// ical_calendar_builder = ical_calendar_builder.set(ContentLine {
// name: "X-WR-CALNAME".to_owned(),
// value: Some(displayname),
// params: vec![].into(),
// });
// }
// if let Some(description) = calendar.meta.description {
// ical_calendar_builder = ical_calendar_builder.set(ContentLine {
// name: "X-WR-CALDESC".to_owned(),
// value: Some(description),
// params: vec![].into(),
// });
// }
// if let Some(timezone_id) = calendar.timezone_id {
// ical_calendar_builder = ical_calendar_builder.set(ContentLine {
// name: "X-WR-TIMEZONE".to_owned(),
// value: Some(timezone_id),
// params: vec![].into(),
// });
// }
//
// for object in &objects {
// vtimezones.extend(object.get_vtimezones());
// match object.get_data() {
// CalendarObjectComponent::Event(EventObject { event, .. }, overrides) => {
// ical_calendar_builder = ical_calendar_builder
// .add_event(event.clone())
// .add_events(overrides.iter().map(|ev| ev.event.clone()));
// }
// CalendarObjectComponent::Todo(todo, overrides) => {
// ical_calendar_builder = ical_calendar_builder
// .add_todo(todo.clone())
// .add_todos(overrides.iter().cloned());
// }
// CalendarObjectComponent::Journal(journal, overrides) => {
// ical_calendar_builder = ical_calendar_builder
// .add_journal(journal.clone())
// .add_journals(overrides.iter().cloned());
// }
// }
// }
//
// ical_calendar_builder = ical_calendar_builder.add_timezones(vtimezones.into_values().cloned());
//
// let ical_calendar = ical_calendar_builder
// .build()
// .map_err(|parser_error| Error::IcalError(parser_error.into()))?;
//
// let mut resp = Response::builder().status(StatusCode::OK);
// let hdrs = resp.headers_mut().unwrap();
// hdrs.typed_insert(ContentType::from_str("text/calendar; charset=utf-8").unwrap());
//
// let filename = format!("{}_{}.ics", calendar.principal, calendar.id);
// let filename = utf8_percent_encode(&filename, CONTROLS);
// hdrs.insert(
// header::CONTENT_DISPOSITION,
// HeaderValue::from_str(&format!(
// "attachement; filename*=UTF-8''{filename}; filename={filename}",
// ))
// .unwrap(),
// );
// if matches!(method, Method::HEAD) {
// Ok(resp.body(Body::empty()).unwrap())
// } else {
// Ok(resp.body(Body::new(ical_calendar.generate())).unwrap())
// }
}

View File

@@ -5,10 +5,7 @@ use axum::{
response::{IntoResponse, Response},
};
use http::StatusCode;
use ical::{
generator::Emitter,
parser::{Component, ComponentMut},
};
use ical::{generator::Emitter, parser::Component};
use rustical_dav::header::Overwrite;
use rustical_ical::{CalendarObject, CalendarObjectType};
use rustical_store::{
@@ -53,58 +50,59 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
.get_property("X-WR-TIMEZONE")
.and_then(|prop| prop.value.clone());
// These properties should not appear in the expanded calendar objects
cal.remove_property("X-WR-CALNAME");
cal.remove_property("X-WR-CALDESC");
cal.remove_property("X-WR-TIMEZONE");
let cal = cal.verify().unwrap();
// Make sure timezone is valid
if let Some(timezone_id) = timezone_id.as_ref() {
assert!(
vtimezones_rs::VTIMEZONES.contains_key(timezone_id),
"Invalid calendar timezone id"
);
}
// Extract necessary component types
let mut cal_components = vec![];
if !cal.events.is_empty() {
cal_components.push(CalendarObjectType::Event);
}
if !cal.journals.is_empty() {
cal_components.push(CalendarObjectType::Journal);
}
if !cal.todos.is_empty() {
cal_components.push(CalendarObjectType::Todo);
}
let expanded_cals = cal.expand_calendar();
// Janky way to convert between IcalCalendar and CalendarObject
let objects = expanded_cals
.into_iter()
.map(|cal| cal.generate())
.map(|ics| CalendarObject::from_ics(ics, None))
.collect::<Result<Vec<_>, _>>()?;
let new_cal = Calendar {
principal,
id: cal_id,
meta: CalendarMetadata {
displayname,
order: 0,
description,
color: None,
},
timezone_id,
deleted_at: None,
synctoken: 0,
subscription_url: None,
push_topic: uuid::Uuid::new_v4().to_string(),
components: cal_components,
};
let cal_store = resource_service.cal_store;
cal_store
.import_calendar(new_cal, objects, overwrite)
.await?;
Ok(StatusCode::OK.into_response())
todo!();
// cal.remove_property("X-WR-CALNAME");
// cal.remove_property("X-WR-CALDESC");
// cal.remove_property("X-WR-TIMEZONE");
// let cal = cal.verify().unwrap();
// // Make sure timezone is valid
// if let Some(timezone_id) = timezone_id.as_ref() {
// assert!(
// vtimezones_rs::VTIMEZONES.contains_key(timezone_id),
// "Invalid calendar timezone id"
// );
// }
//
// // Extract necessary component types
// let mut cal_components = vec![];
// if !cal.events.is_empty() {
// cal_components.push(CalendarObjectType::Event);
// }
// if !cal.journals.is_empty() {
// cal_components.push(CalendarObjectType::Journal);
// }
// if !cal.todos.is_empty() {
// cal_components.push(CalendarObjectType::Todo);
// }
//
// let expanded_cals = cal.expand_calendar();
// // Janky way to convert between IcalCalendar and CalendarObject
// let objects = expanded_cals
// .into_iter()
// .map(|cal| cal.generate())
// .map(|ics| CalendarObject::from_ics(ics, None))
// .collect::<Result<Vec<_>, _>>()?;
// let new_cal = Calendar {
// principal,
// id: cal_id,
// meta: CalendarMetadata {
// displayname,
// order: 0,
// description,
// color: None,
// },
// timezone_id,
// deleted_at: None,
// synctoken: 0,
// subscription_url: None,
// push_topic: uuid::Uuid::new_v4().to_string(),
// components: cal_components,
// };
//
// let cal_store = resource_service.cal_store;
// cal_store
// .import_calendar(new_cal, objects, overwrite)
// .await?;
//
// Ok(StatusCode::OK.into_response())
}

View File

@@ -92,10 +92,11 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
.ok_or_else(|| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?
.map_err(|_| rustical_dav::Error::BadRequest("Error parsing timezone".to_owned()))?;
let timezone = calendar.timezones.first().ok_or_else(|| {
let timezone = calendar.vtimezones.first().ok_or_else(|| {
rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
})?;
let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| {
let timezone: Option<chrono_tz::Tz> = timezone.into();
let timezone = timezone.ok_or_else(|| {
rustical_dav::Error::BadRequest("Cannot translate VTIMEZONE into IANA TZID".to_owned())
})?;

View File

@@ -1,9 +1,10 @@
use crate::calendar::methods::report::calendar_query::{
TimeRangeElement,
prop_filter::{PropFilterElement, PropFilterable},
TimeRangeElement, prop_filter::PropFilterElement,
};
use ical::{
component::IcalCalendarObject,
parser::{Component, ical::component::IcalTimeZone},
};
use ical::parser::ical::component::IcalTimeZone;
use rustical_ical::{CalendarObject, CalendarObjectComponent, CalendarObjectType};
use rustical_xml::XmlDeserialize;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
@@ -24,9 +25,7 @@ pub struct CompFilterElement {
pub(crate) name: String,
}
pub trait CompFilterable: PropFilterable + Sized {
fn get_comp_name(&self) -> &'static str;
pub trait CompFilterable: Component + Sized {
fn match_time_range(&self, time_range: &TimeRangeElement) -> bool;
fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool;
@@ -68,11 +67,7 @@ pub trait CompFilterable: PropFilterable + Sized {
}
}
impl CompFilterable for CalendarObject {
fn get_comp_name(&self) -> &'static str {
"VCALENDAR"
}
impl CompFilterable for IcalCalendarObject {
fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool {
// VCALENDAR has no concept of time range
false
@@ -83,7 +78,7 @@ impl CompFilterable for CalendarObject {
.get_vtimezones()
.values()
.map(|tz| tz.matches(comp_filter))
.chain([self.get_data().matches(comp_filter)]);
.chain([self.matches(comp_filter)]);
if comp_filter.is_not_defined.is_some() {
matches.all(|x| x)
@@ -94,10 +89,6 @@ impl CompFilterable for CalendarObject {
}
impl CompFilterable for IcalTimeZone {
fn get_comp_name(&self) -> &'static str {
"VTIMEZONE"
}
fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool {
false
}
@@ -107,33 +98,6 @@ impl CompFilterable for IcalTimeZone {
}
}
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};

View File

@@ -1,8 +1,8 @@
use super::comp_filter::{CompFilterElement, CompFilterable};
use crate::calendar_object::CalendarObjectPropWrapperName;
use ical::property::Property;
use ical::{component::IcalCalendarObject, property::ContentLine};
use rustical_dav::xml::{PropfindType, TextMatchElement};
use rustical_ical::{CalendarObject, UtcDateTime};
use rustical_ical::UtcDateTime;
use rustical_store::calendar_store::CalendarQuery;
use rustical_xml::{XmlDeserialize, XmlRootTag};
@@ -30,8 +30,8 @@ pub struct ParamFilterElement {
impl ParamFilterElement {
#[must_use]
pub fn match_property(&self, prop: &Property) -> bool {
let Some(param) = prop.get_param(&self.name) else {
pub fn match_property(&self, prop: &ContentLine) -> bool {
let Some(param) = prop.params.get_param(&self.name) else {
return self.is_not_defined.is_some();
};
if self.is_not_defined.is_some() {
@@ -57,7 +57,7 @@ pub struct FilterElement {
impl FilterElement {
#[must_use]
pub fn matches(&self, cal_object: &CalendarObject) -> bool {
pub fn matches(&self, cal_object: &IcalCalendarObject) -> bool {
cal_object.matches(&self.comp_filter)
}
}

View File

@@ -11,7 +11,7 @@ mod tests;
pub use comp_filter::{CompFilterElement, CompFilterable};
pub use elements::*;
#[allow(unused_imports)]
pub use prop_filter::{PropFilterElement, PropFilterable};
pub use prop_filter::PropFilterElement;
pub async fn get_objects_calendar_query<C: CalendarStore>(
cal_query: &CalendarQueryRequest,
@@ -23,7 +23,7 @@ pub async fn get_objects_calendar_query<C: CalendarStore>(
.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.get_inner()));
}
Ok(objects)
}

View File

@@ -1,14 +1,7 @@
use super::{ParamFilterElement, TimeRangeElement};
use ical::{
generator::{IcalCalendar, IcalEvent},
parser::{
Component,
ical::component::{IcalJournal, IcalTimeZone, IcalTodo},
},
property::Property,
};
use ical::{parser::Component, property::ContentLine, types::CalDateTime};
use rustical_dav::xml::TextMatchElement;
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectComponent, UtcDateTime};
use rustical_ical::UtcDateTime;
use rustical_xml::XmlDeserialize;
use std::collections::HashMap;
@@ -31,7 +24,7 @@ pub struct PropFilterElement {
impl PropFilterElement {
#[must_use]
pub fn match_property(&self, property: &Property) -> bool {
pub fn match_property(&self, property: &ContentLine) -> bool {
if let Some(TimeRangeElement { start, end }) = &self.time_range {
// TODO: Respect timezones
let Ok(timestamp) = CalDateTime::parse_prop(property, &HashMap::default()) else {
@@ -68,7 +61,7 @@ impl PropFilterElement {
true
}
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
pub fn match_component(&self, comp: &impl Component) -> bool {
let properties = comp.get_named_properties(&self.name);
if self.is_not_defined.is_some() {
return properties.is_empty();
@@ -79,53 +72,3 @@ impl PropFilterElement {
properties.iter().any(|prop| self.match_property(prop))
}
}
pub trait PropFilterable {
fn get_named_properties(&self, name: &str) -> Vec<&Property>;
}
impl PropFilterable for CalendarObject {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
Self::get_named_properties(self, name)
}
}
impl PropFilterable for IcalEvent {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
Component::get_named_properties(self, name)
}
}
impl PropFilterable for IcalTodo {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
Component::get_named_properties(self, name)
}
}
impl PropFilterable for IcalJournal {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
Component::get_named_properties(self, name)
}
}
impl PropFilterable for IcalCalendar {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
Component::get_named_properties(self, name)
}
}
impl PropFilterable for IcalTimeZone {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
Component::get_named_properties(self, name)
}
}
impl PropFilterable for CalendarObjectComponent {
fn get_named_properties(&self, name: &str) -> Vec<&Property> {
match self {
Self::Event(event, _) => PropFilterable::get_named_properties(&event.event, name),
Self::Todo(todo, _) => PropFilterable::get_named_properties(todo, name),
Self::Journal(journal, _) => PropFilterable::get_named_properties(journal, name),
}
}
}

View File

@@ -4,6 +4,7 @@ use crate::calendar::prop::{ReportMethod, SupportedCollationSet};
use chrono::{DateTime, Utc};
use derive_more::derive::{From, Into};
use ical::IcalParser;
use ical::types::CalDateTime;
use rustical_dav::extensions::{
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
};
@@ -11,7 +12,6 @@ use rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedReportSet};
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp};
use rustical_ical::CalDateTime;
use rustical_store::Calendar;
use rustical_store::auth::Principal;
use rustical_xml::{EnumVariants, PropName};
@@ -215,13 +215,13 @@ impl Resource for CalendarResource {
)
})?;
let timezone = calendar.timezones.first().ok_or_else(|| {
let timezone = calendar.vtimezones.first().ok_or_else(|| {
rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
})?;
let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| {
let timezone: Option<chrono_tz::Tz> = timezone.into();
let timezone = timezone.ok_or_else(|| {
rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
})?;
self.cal.timezone_id = Some(timezone.name().to_owned());
}
Ok(())