Skip to main content

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