use crate::Error; use crate::calendar_object::{CalendarObjectPathComponents, CalendarObjectResourceService}; use crate::error::Precondition; use axum::body::Body; use axum::extract::{Path, State}; use axum::response::{IntoResponse, Response}; use axum_extra::TypedHeader; use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch}; use http::{HeaderMap, HeaderValue, Method, StatusCode}; use rustical_ical::CalendarObject; use rustical_store::CalendarStore; use rustical_store::auth::Principal; use std::str::FromStr; use tracing::{instrument, warn}; #[instrument(skip(cal_store))] pub async fn get_event( Path(CalendarObjectPathComponents { principal, calendar_id, object_id, }): Path, State(CalendarObjectResourceService { cal_store }): State>, user: Principal, method: Method, ) -> Result { if !user.is_principal(&principal) { return Err(crate::Error::Unauthorized); } let calendar = cal_store .get_calendar(&principal, &calendar_id, false) .await?; if !user.is_principal(&calendar.principal) { return Err(crate::Error::Unauthorized); } let event = cal_store .get_object(&principal, &calendar_id, &object_id, false) .await?; let mut resp = Response::builder().status(StatusCode::OK); let hdrs = resp.headers_mut().unwrap(); hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap()); hdrs.typed_insert(ContentType::from_str("text/calendar; charset=utf-8").unwrap()); if matches!(method, Method::HEAD) { Ok(resp.body(Body::empty()).unwrap()) } else { Ok(resp.body(Body::new(event.get_ics().to_owned())).unwrap()) } } #[instrument(skip(cal_store))] pub async fn put_event( Path(CalendarObjectPathComponents { principal, calendar_id, object_id, }): Path, State(CalendarObjectResourceService { cal_store }): State>, user: Principal, mut if_none_match: Option>, header_map: HeaderMap, body: String, ) -> Result { if !user.is_principal(&principal) { return Err(crate::Error::Unauthorized); } // https://github.com/hyperium/headers/issues/204 if !header_map.contains_key("If-None-Match") { if_none_match = None; } let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match { // TODO: Put into transaction? let existing = match cal_store .get_object(&principal, &calendar_id, &object_id, false) .await { Ok(existing) => Some(existing), Err(rustical_store::Error::NotFound) => None, Err(err) => Err(err)?, }; existing.is_none_or(|existing| { if_none_match.precondition_passes( &existing .get_etag() .parse() .expect("We only generate valid ETags"), ) }) } else { true }; let object = match CalendarObject::from_ics(body.clone()) { Ok(object) => object, Err(err) => { warn!("invalid calendar data:\n{body}"); warn!("{err}"); return Err(Error::PreconditionFailed(Precondition::ValidCalendarData)); } }; let etag = object.get_etag(); cal_store .put_object(&principal, &calendar_id, &object_id, object, overwrite) .await?; let mut headers = HeaderMap::new(); headers.insert( "ETag", HeaderValue::from_str(&etag).expect("Contains no invalid characters"), ); Ok((StatusCode::CREATED, headers).into_response()) }