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