Skip to main content

rustical_caldav/calendar/methods/
import.rs

1use crate::Error;
2use crate::calendar::CalendarResourceService;
3use axum::{
4    extract::{Path, State},
5    response::{IntoResponse, Response},
6};
7use axum_extra::TypedHeader;
8use caldata::component::{Component, ComponentMut};
9use caldata::{IcalParser, parser::ParserOptions};
10use http::StatusCode;
11use rustical_dav::header::Overwrite;
12use rustical_ical::CalendarObjectType;
13use rustical_store::{
14    Calendar, CalendarMetadata, CalendarStore, SubscriptionStore, auth::Principal,
15};
16use tracing::instrument;
17
18#[instrument(skip(resource_service))]
19pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
20    Path((principal, cal_id)): Path<(String, String)>,
21    user: Principal,
22    State(resource_service): State<CalendarResourceService<C, S>>,
23    overwrite: Option<TypedHeader<Overwrite>>,
24    body: String,
25) -> Result<Response, Error> {
26    if !user.is_principal(&principal) {
27        return Err(Error::Unauthorized);
28    }
29
30    let overwrite = overwrite
31        .map(|TypedHeader(overwrite)| overwrite)
32        .unwrap_or_default()
33        .into();
34
35    let parser = IcalParser::from_slice(body.as_bytes());
36    let mut cal = match parser.expect_one() {
37        Ok(cal) => cal.mutable(),
38        Err(err) => return Ok((StatusCode::BAD_REQUEST, err.to_string()).into_response()),
39    };
40
41    // Extract calendar metadata
42    let displayname = cal
43        .get_property("X-WR-CALNAME")
44        .map(|prop| prop.value.clone());
45    let description = cal
46        .get_property("X-WR-CALDESC")
47        .map(|prop| prop.value.clone());
48    let color = cal
49        .get_property("X-WR-CALCOLOR")
50        .map(|prop| prop.value.clone());
51    let timezone_id = cal
52        .get_property("X-WR-TIMEZONE")
53        .map(|prop| prop.value.clone());
54    // These properties should not appear in the expanded calendar objects
55    cal.remove_property("X-WR-CALNAME");
56    cal.remove_property("X-WR-CALDESC");
57    cal.remove_property("X-WR-CALCOLOR");
58    cal.remove_property("X-WR-TIMEZONE");
59    let cal = cal.build(&ParserOptions::default(), None).unwrap();
60
61    // Make sure timezone is valid
62    if let Some(timezone_id) = timezone_id.as_ref() {
63        assert!(
64            vtimezones_rs::VTIMEZONES.contains_key(timezone_id),
65            "Invalid calendar timezone id"
66        );
67    }
68    // // Extract necessary component types
69    let mut cal_components = vec![];
70    if !cal.events.is_empty() {
71        cal_components.push(CalendarObjectType::Event);
72    }
73    if !cal.journals.is_empty() {
74        cal_components.push(CalendarObjectType::Journal);
75    }
76    if !cal.todos.is_empty() {
77        cal_components.push(CalendarObjectType::Todo);
78    }
79
80    let objects = match cal.into_objects() {
81        Ok(objects) => objects.into_iter().map(Into::into).collect(),
82        Err(err) => return Ok((StatusCode::BAD_REQUEST, err.to_string()).into_response()),
83    };
84    let new_cal = Calendar {
85        principal,
86        id: cal_id,
87        meta: CalendarMetadata {
88            displayname,
89            order: 0,
90            description,
91            color,
92        },
93        timezone_id,
94        deleted_at: None,
95        synctoken: 0,
96        subscription_url: None,
97        push_topic: uuid::Uuid::new_v4().to_string(),
98        components: cal_components,
99    };
100
101    let cal_store = resource_service.cal_store;
102    cal_store
103        .import_calendar(new_cal, objects, overwrite)
104        .await?;
105
106    Ok(StatusCode::OK.into_response())
107}