rustical_caldav/calendar/methods/
get.rs

1use crate::Error;
2use crate::calendar::CalendarResourceService;
3use axum::body::Body;
4use axum::extract::State;
5use axum::{extract::Path, response::Response};
6use caldata::component::IcalCalendar;
7use caldata::generator::Emitter;
8use caldata::parser::ContentLine;
9use headers::{ContentType, HeaderMapExt};
10use http::{HeaderValue, Method, StatusCode, header};
11use percent_encoding::{CONTROLS, utf8_percent_encode};
12use rustical_store::{CalendarStore, SubscriptionStore, auth::Principal};
13use std::str::FromStr;
14use tracing::instrument;
15
16#[instrument(skip(cal_store))]
17pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
18    Path((principal, calendar_id)): Path<(String, String)>,
19    State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
20    user: Principal,
21    method: Method,
22) -> Result<Response, Error> {
23    if !user.is_principal(&principal) {
24        return Err(crate::Error::Unauthorized);
25    }
26
27    let calendar = cal_store
28        .get_calendar(&principal, &calendar_id, true)
29        .await?;
30    if !user.is_principal(&calendar.principal) {
31        return Err(crate::Error::Unauthorized);
32    }
33
34    let objects = cal_store
35        .get_objects(&principal, &calendar_id)
36        .await?
37        .into_iter()
38        .map(|(_, object)| object.into())
39        .collect();
40
41    let mut props = vec![];
42
43    if let Some(displayname) = calendar.meta.displayname {
44        props.push(ContentLine {
45            name: "X-WR-CALNAME".to_owned(),
46            value: Some(displayname),
47            params: vec![].into(),
48        });
49    }
50    if let Some(description) = calendar.meta.description {
51        props.push(ContentLine {
52            name: "X-WR-CALDESC".to_owned(),
53            value: Some(description),
54            params: vec![].into(),
55        });
56    }
57    if let Some(color) = calendar.meta.color {
58        props.push(ContentLine {
59            name: "X-WR-CALCOLOR".to_owned(),
60            value: Some(color),
61            params: vec![].into(),
62        });
63    }
64    if let Some(timezone_id) = calendar.timezone_id {
65        props.push(ContentLine {
66            name: "X-WR-TIMEZONE".to_owned(),
67            value: Some(timezone_id),
68            params: vec![].into(),
69        });
70    }
71
72    let export_calendar = IcalCalendar::from_objects("RustiCal Export".to_owned(), objects, props);
73
74    let mut resp = Response::builder().status(StatusCode::OK);
75    let hdrs = resp.headers_mut().unwrap();
76    hdrs.typed_insert(ContentType::from_str("text/calendar; charset=utf-8").unwrap());
77
78    let filename = format!("{}_{}.ics", calendar.principal, calendar.id);
79    let filename = utf8_percent_encode(&filename, CONTROLS);
80    hdrs.insert(
81        header::CONTENT_DISPOSITION,
82        HeaderValue::from_str(&format!(
83            "attachement; filename*=UTF-8''{filename}; filename={filename}",
84        ))
85        .unwrap(),
86    );
87    if matches!(method, Method::HEAD) {
88        Ok(resp.body(Body::empty()).unwrap())
89    } else {
90        Ok(resp.body(Body::new(export_calendar.generate())).unwrap())
91    }
92}