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