From 1b69148d6ffd6c940b2cae051c0d7eef5889fde9 Mon Sep 17 00:00:00 2001 From: Lennart K <18233294+lennart-k@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:36:02 +0100 Subject: [PATCH] Re-implement calendar export --- Cargo.lock | 2 +- crates/caldav/src/calendar/methods/get.rs | 131 ++++++++---------- crates/caldav/src/calendar/methods/import.rs | 3 + .../caldav/src/calendar/methods/mkcalendar.rs | 2 +- crates/caldav/src/calendar/resource.rs | 2 +- crates/caldav/src/calendar_object/resource.rs | 22 +-- crates/ical/src/calendar_object.rs | 6 + 7 files changed, 82 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ff12d52..f692c80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1771,7 +1771,7 @@ dependencies = [ [[package]] name = "ical" version = "0.11.0" -source = "git+https://github.com/lennart-k/ical-rs?branch=dev#0a50f3998b8ae104642cceb9d974e99b78838b14" +source = "git+https://github.com/lennart-k/ical-rs?branch=dev#8732224dd1514f6dcbccdcf63268a3f223d360e9" dependencies = [ "chrono", "chrono-tz", diff --git a/crates/caldav/src/calendar/methods/get.rs b/crates/caldav/src/calendar/methods/get.rs index 368870b..c2c1b76 100644 --- a/crates/caldav/src/calendar/methods/get.rs +++ b/crates/caldav/src/calendar/methods/get.rs @@ -5,11 +5,11 @@ use axum::extract::State; use axum::{extract::Path, response::Response}; use headers::{ContentType, HeaderMapExt}; use http::{HeaderValue, Method, StatusCode, header}; +use ical::component::IcalCalendar; use ical::generator::Emitter; use ical::property::ContentLine; use percent_encoding::{CONTROLS, utf8_percent_encode}; use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal}; -use std::collections::HashMap; use std::str::FromStr; use tracing::instrument; @@ -31,79 +31,62 @@ pub async fn route_get( return Err(crate::Error::Unauthorized); } - // let mut vtimezones = HashMap::new(); - // let objects = cal_store.get_objects(&principal, &calendar_id).await?; + let objects = cal_store + .get_objects(&principal, &calendar_id) + .await? + .into_iter() + .map(|(_, object)| object.into()) + .collect(); - todo!() + let mut props = vec![]; - // 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()) - // } + if let Some(displayname) = calendar.meta.displayname { + props.push(ContentLine { + name: "X-WR-CALNAME".to_owned(), + value: Some(displayname), + params: vec![].into(), + }); + } + if let Some(description) = calendar.meta.description { + props.push(ContentLine { + name: "X-WR-CALDESC".to_owned(), + value: Some(description), + params: vec![].into(), + }); + } + if let Some(color) = calendar.meta.color { + props.push(ContentLine { + name: "X-WR-CALCOLOR".to_owned(), + value: Some(color), + params: vec![].into(), + }); + } + if let Some(timezone_id) = calendar.timezone_id { + props.push(ContentLine { + name: "X-WR-TIMEZONE".to_owned(), + value: Some(timezone_id), + params: vec![].into(), + }); + } + + let export_calendar = IcalCalendar::from_objects(objects, props); + + 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(export_calendar.generate())).unwrap()) + } } diff --git a/crates/caldav/src/calendar/methods/import.rs b/crates/caldav/src/calendar/methods/import.rs index fe4f153..03746fc 100644 --- a/crates/caldav/src/calendar/methods/import.rs +++ b/crates/caldav/src/calendar/methods/import.rs @@ -46,6 +46,9 @@ pub async fn route_import( let description = cal .get_property("X-WR-CALDESC") .and_then(|prop| prop.value.clone()); + let color = cal + .get_property("X-WR-CALCOLOR") + .and_then(|prop| prop.value.clone()); let timezone_id = cal .get_property("X-WR-TIMEZONE") .and_then(|prop| prop.value.clone()); diff --git a/crates/caldav/src/calendar/methods/mkcalendar.rs b/crates/caldav/src/calendar/methods/mkcalendar.rs index 71a8a09..f85ca76 100644 --- a/crates/caldav/src/calendar/methods/mkcalendar.rs +++ b/crates/caldav/src/calendar/methods/mkcalendar.rs @@ -92,7 +92,7 @@ pub async fn route_mkcalendar( .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.vtimezones.first().ok_or_else(|| { + let timezone = calendar.vtimezones.values().next().ok_or_else(|| { rustical_dav::Error::BadRequest("No timezone data provided".to_owned()) })?; let timezone: Option = timezone.into(); diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index cb37d71..f80a4d4 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -215,7 +215,7 @@ impl Resource for CalendarResource { ) })?; - let timezone = calendar.vtimezones.first().ok_or_else(|| { + let timezone = calendar.vtimezones.values().next().ok_or_else(|| { rustical_dav::Error::BadRequest("No timezone data provided".to_owned()) })?; let timezone: Option = timezone.into(); diff --git a/crates/caldav/src/calendar_object/resource.rs b/crates/caldav/src/calendar_object/resource.rs index 61c1571..0bb74d7 100644 --- a/crates/caldav/src/calendar_object/resource.rs +++ b/crates/caldav/src/calendar_object/resource.rs @@ -4,6 +4,7 @@ use super::prop::{ }; use crate::Error; use derive_more::derive::{From, Into}; +use ical::generator::Emitter; use rustical_dav::{ extensions::CommonPropertiesExtension, privileges::UserPrivilegeSet, @@ -53,15 +54,18 @@ impl Resource for CalendarObjectResource { CalendarObjectProp::Getetag(self.object.get_etag()) } CalendarObjectPropName::CalendarData(CalendarData { expand, .. }) => { - CalendarObjectProp::CalendarData(if let Some(expand) = expand.as_ref() { - todo!() - // self.object.get_inner().expand_recurrence( - // Some(expand.start.to_utc()), - // Some(expand.end.to_utc()), - // ) - } else { - self.object.get_ics().to_owned() - }) + CalendarObjectProp::CalendarData(expand.as_ref().map_or_else( + || self.object.get_ics().to_owned(), + |expand| { + self.object + .get_inner() + .expand_recurrence( + Some(expand.start.to_utc()), + Some(expand.end.to_utc()), + ) + .generate() + }, + )) } CalendarObjectPropName::Getcontenttype => { CalendarObjectProp::Getcontenttype("text/calendar;charset=utf-8") diff --git a/crates/ical/src/calendar_object.rs b/crates/ical/src/calendar_object.rs index ead3cf0..c7518a8 100644 --- a/crates/ical/src/calendar_object.rs +++ b/crates/ical/src/calendar_object.rs @@ -102,3 +102,9 @@ impl CalendarObject { (&self.inner).into() } } + +impl From for IcalCalendarObject { + fn from(value: CalendarObject) -> Self { + value.inner + } +}