rustical_caldav/calendar_object/
methods.rs

1use crate::Error;
2use crate::calendar_object::{CalendarObjectPathComponents, CalendarObjectResourceService};
3use crate::error::Precondition;
4use axum::body::Body;
5use axum::extract::{Path, State};
6use axum::response::{IntoResponse, Response};
7use axum_extra::TypedHeader;
8use caldata::parser::ParserOptions;
9use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
10use http::{HeaderMap, HeaderValue, Method, StatusCode};
11use rustical_ical::CalendarObject;
12use rustical_store::CalendarStore;
13use rustical_store::auth::Principal;
14use std::str::FromStr;
15use tracing::{instrument, warn};
16
17#[instrument(skip(cal_store))]
18pub async fn get_event<C: CalendarStore>(
19    Path(CalendarObjectPathComponents {
20        principal,
21        calendar_id,
22        object_id,
23    }): Path<CalendarObjectPathComponents>,
24    State(CalendarObjectResourceService {
25        cal_store,
26        config: _,
27    }): State<CalendarObjectResourceService<C>>,
28    user: Principal,
29    method: Method,
30) -> Result<Response, Error> {
31    if !user.is_principal(&principal) {
32        return Err(crate::Error::Unauthorized);
33    }
34
35    let calendar = cal_store
36        .get_calendar(&principal, &calendar_id, false)
37        .await?;
38    if !user.is_principal(&calendar.principal) {
39        return Err(crate::Error::Unauthorized);
40    }
41
42    let event = cal_store
43        .get_object(&principal, &calendar_id, &object_id, false)
44        .await?;
45
46    let mut resp = Response::builder().status(StatusCode::OK);
47    let hdrs = resp.headers_mut().unwrap();
48    hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap());
49    hdrs.typed_insert(ContentType::from_str("text/calendar; charset=utf-8").unwrap());
50    if matches!(method, Method::HEAD) {
51        Ok(resp.body(Body::empty()).unwrap())
52    } else {
53        Ok(resp.body(Body::new(event.get_ics().to_owned())).unwrap())
54    }
55}
56
57#[instrument(skip(cal_store))]
58pub async fn put_event<C: CalendarStore>(
59    Path(CalendarObjectPathComponents {
60        principal,
61        calendar_id,
62        object_id,
63    }): Path<CalendarObjectPathComponents>,
64    State(CalendarObjectResourceService { cal_store, config }): State<
65        CalendarObjectResourceService<C>,
66    >,
67    user: Principal,
68    mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
69    header_map: HeaderMap,
70    body: String,
71) -> Result<Response, Error> {
72    if !user.is_principal(&principal) {
73        return Err(crate::Error::Unauthorized);
74    }
75
76    // https://github.com/hyperium/headers/issues/204
77    if !header_map.contains_key("If-None-Match") {
78        if_none_match = None;
79    }
80
81    let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
82        // TODO: Put into transaction?
83        let existing = match cal_store
84            .get_object(&principal, &calendar_id, &object_id, false)
85            .await
86        {
87            Ok(existing) => Some(existing),
88            Err(rustical_store::Error::NotFound) => None,
89            Err(err) => Err(err)?,
90        };
91        existing.is_none_or(|existing| {
92            if_none_match.precondition_passes(
93                &existing
94                    .get_etag()
95                    .parse()
96                    .expect("We only generate valid ETags"),
97            )
98        })
99    } else {
100        true
101    };
102
103    let object = match CalendarObject::import(
104        &body,
105        Some(ParserOptions {
106            rfc7809: config.rfc7809,
107        }),
108    ) {
109        Ok(object) => object,
110        Err(err) => {
111            warn!("invalid calendar data:\n{body}");
112            warn!("{err}");
113            return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
114        }
115    };
116    let etag = object.get_etag();
117    cal_store
118        .put_object(&principal, &calendar_id, &object_id, object, overwrite)
119        .await?;
120
121    let mut headers = HeaderMap::new();
122    headers.insert(
123        "ETag",
124        HeaderValue::from_str(&etag).expect("Contains no invalid characters"),
125    );
126    Ok((StatusCode::CREATED, headers).into_response())
127}