rustical_caldav/calendar_object/
methods.rs1use 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 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 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}