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, IfMatch, 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 mut if_match: Option<TypedHeader<IfMatch>>,
70 header_map: HeaderMap,
71 body: String,
72) -> Result<Response, Error> {
73 if !user.is_principal(&principal) {
74 return Err(crate::Error::Unauthorized);
75 }
76
77 if !header_map.contains_key("If-None-Match") {
79 if_none_match = None;
80 }
81 if !header_map.contains_key("If-Match") {
82 if_match = None;
83 }
84
85 if if_match.is_some() || if_none_match.is_some() {
86 let existing = match cal_store
88 .get_object(&principal, &calendar_id, &object_id, false)
89 .await
90 {
91 Ok(existing) => Some(existing),
92 Err(rustical_store::Error::NotFound) => None,
93 Err(err) => Err(err)?,
94 };
95
96 if let Some(existing) = existing {
98 let etag: Option<ETag> = existing.get_etag().parse().ok();
99
100 if let Some(if_match) = if_match.as_ref()
101 && etag
102 .as_ref()
103 .is_none_or(|etag| !if_match.precondition_passes(etag))
105 {
106 return Err(Error::DavError(rustical_dav::Error::PreconditionFailed));
107 }
108
109 if let Some(if_none_match) = if_none_match.as_ref()
110 && etag
111 .as_ref()
112 .is_some_and(|etag| !if_none_match.precondition_passes(etag))
114 {
115 return Err(Error::DavError(rustical_dav::Error::PreconditionFailed));
116 }
117 }
118 else if if_match.is_some() {
129 return Err(Error::DavError(rustical_dav::Error::PreconditionFailed));
130 }
131 }
132
133 let object = match CalendarObject::import(
134 &body,
135 Some(ParserOptions {
136 rfc7809: config.rfc7809,
137 }),
138 ) {
139 Ok(object) => object,
140 Err(err) => {
141 warn!("invalid calendar data:\n{body}");
142 warn!("{err}");
143 return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
144 }
145 };
146 let etag = object.get_etag();
147 cal_store
148 .put_object(&principal, &calendar_id, &object_id, object, true)
149 .await?;
150
151 let mut headers = HeaderMap::new();
152 headers.insert(
153 "ETag",
154 HeaderValue::from_str(&etag).expect("Contains no invalid characters"),
155 );
156 Ok((StatusCode::CREATED, headers).into_response())
157}