From 276fdcacf51f4da080999213d2b60a13d89893f9 Mon Sep 17 00:00:00 2001 From: Lennart K <18233294+lennart-k@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:17:39 +0100 Subject: [PATCH] Re-implement calendar imports --- Cargo.lock | 2 +- crates/caldav/src/calendar/methods/import.rs | 126 +++++++++---------- crates/ical/src/calendar_object.rs | 10 ++ 3 files changed, 70 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d59a39..d0194e6 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#dd310bbb9866dd2b51d3e83e2b3572e4a3c7f7c9" +source = "git+https://github.com/lennart-k/ical-rs?branch=dev#a64b2f6b2920c238f0aee6dd02bafcda5ca76040" dependencies = [ "chrono", "chrono-tz", diff --git a/crates/caldav/src/calendar/methods/import.rs b/crates/caldav/src/calendar/methods/import.rs index 03746fc..054b7ef 100644 --- a/crates/caldav/src/calendar/methods/import.rs +++ b/crates/caldav/src/calendar/methods/import.rs @@ -5,13 +5,13 @@ use axum::{ response::{IntoResponse, Response}, }; use http::StatusCode; -use ical::{generator::Emitter, parser::Component}; +use ical::parser::{Component, ComponentMut}; use rustical_dav::header::Overwrite; -use rustical_ical::{CalendarObject, CalendarObjectType}; +use rustical_ical::CalendarObjectType; use rustical_store::{ Calendar, CalendarMetadata, CalendarStore, SubscriptionStore, auth::Principal, }; -use std::io::BufReader; +use std::{collections::HashMap, io::BufReader}; use tracing::instrument; #[instrument(skip(resource_service))] @@ -26,18 +26,11 @@ pub async fn route_import( return Err(Error::Unauthorized); } - let mut parser = ical::IcalParser::new(BufReader::new(body.as_bytes())); + let parser = ical::IcalParser::new(BufReader::new(body.as_bytes())); let mut cal = parser - .next() - .expect("input must contain calendar") - .unwrap() + .expect_one() + .map_err(rustical_ical::Error::ParserError)? .mutable(); - if parser.next().is_some() { - return Err(rustical_ical::Error::InvalidData( - "multiple calendars, only one allowed".to_owned(), - ) - .into()); - } // Extract calendar metadata let displayname = cal @@ -53,59 +46,58 @@ pub async fn route_import( .get_property("X-WR-TIMEZONE") .and_then(|prop| prop.value.clone()); // These properties should not appear in the expanded calendar objects - 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" - // ); - // } - // + cal.remove_property("X-WR-CALNAME"); + cal.remove_property("X-WR-CALDESC"); + cal.remove_property("X-WR-CALCOLOR"); + cal.remove_property("X-WR-TIMEZONE"); + let cal = cal.build(&HashMap::new()).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::, _>>()?; - // 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()) + 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 objects = cal + .into_objects() + .map_err(rustical_ical::Error::ParserError)? + .into_iter() + .map(Into::into) + .collect(); + let new_cal = Calendar { + principal, + id: cal_id, + meta: CalendarMetadata { + displayname, + order: 0, + description, + color, + }, + 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()) } diff --git a/crates/ical/src/calendar_object.rs b/crates/ical/src/calendar_object.rs index c7518a8..dcecf56 100644 --- a/crates/ical/src/calendar_object.rs +++ b/crates/ical/src/calendar_object.rs @@ -2,6 +2,7 @@ use crate::Error; use derive_more::Display; use ical::component::CalendarInnerData; use ical::component::IcalCalendarObject; +use ical::generator::Emitter; use ical::parser::ComponentParser; use serde::Deserialize; use serde::Serialize; @@ -108,3 +109,12 @@ impl From for IcalCalendarObject { value.inner } } + +impl From for CalendarObject { + fn from(value: IcalCalendarObject) -> Self { + Self { + ics: value.generate(), + inner: value, + } + } +}