diff options
author | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-05-16 17:38:34 +0200 |
---|---|---|
committer | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-05-16 17:38:34 +0200 |
commit | 32dfd25f570b7a55bf43752684d286be0f6b2dc2 (patch) | |
tree | dd77871cda851bb5795743a3f04be61cf4c3ad61 /aero-proto/src/dav/controller.rs | |
parent | 6b9542088cd1b66af46e95b787493b601accb495 (diff) | |
download | aerogramme-32dfd25f570b7a55bf43752684d286be0f6b2dc2.tar.gz aerogramme-32dfd25f570b7a55bf43752684d286be0f6b2dc2.zip |
format + WIP calendar-query
Diffstat (limited to 'aero-proto/src/dav/controller.rs')
-rw-r--r-- | aero-proto/src/dav/controller.rs | 200 |
1 files changed, 139 insertions, 61 deletions
diff --git a/aero-proto/src/dav/controller.rs b/aero-proto/src/dav/controller.rs index 885828f..0bf7a7d 100644 --- a/aero-proto/src/dav/controller.rs +++ b/aero-proto/src/dav/controller.rs @@ -1,21 +1,21 @@ use anyhow::Result; -use http_body_util::combinators::{UnsyncBoxBody, BoxBody}; -use hyper::body::Incoming; -use hyper::{Request, Response, body::Bytes}; +use futures::stream::{StreamExt, TryStreamExt}; +use http_body_util::combinators::{BoxBody, UnsyncBoxBody}; use http_body_util::BodyStream; use http_body_util::StreamBody; use hyper::body::Frame; -use futures::stream::{StreamExt, TryStreamExt}; +use hyper::body::Incoming; +use hyper::{body::Bytes, Request, Response}; use aero_collections::user::User; -use aero_dav::types as dav; -use aero_dav::realization::All; use aero_dav::caltypes as cal; +use aero_dav::realization::All; +use aero_dav::types as dav; -use crate::dav::codec::{serialize, deserialize, depth, text_body}; +use crate::dav::codec; +use crate::dav::codec::{depth, deserialize, serialize, text_body}; use crate::dav::node::{DavNode, PutPolicy}; use crate::dav::resource::RootNode; -use crate::dav::codec; pub(super) type ArcUser = std::sync::Arc<User>; pub(super) type HttpResponse = Response<UnsyncBoxBody<Bytes, std::io::Error>>; @@ -39,19 +39,22 @@ pub(crate) struct Controller { req: Request<Incoming>, } impl Controller { - pub(crate) async fn route(user: std::sync::Arc<User>, req: Request<Incoming>) -> Result<HttpResponse> { + pub(crate) async fn route( + user: std::sync::Arc<User>, + req: Request<Incoming>, + ) -> Result<HttpResponse> { let path = req.uri().path().to_string(); let path_segments: Vec<_> = path.split("/").filter(|s| *s != "").collect(); let method = req.method().as_str().to_uppercase(); let can_create = matches!(method.as_str(), "PUT" | "MKCOL" | "MKCALENDAR"); - let node = match (RootNode {}).fetch(&user, &path_segments, can_create).await{ + let node = match (RootNode {}).fetch(&user, &path_segments, can_create).await { Ok(v) => v, Err(e) => { tracing::warn!(err=?e, "dav node fetch failed"); return Ok(Response::builder() .status(404) - .body(codec::text_body("Resource not found"))?) + .body(codec::text_body("Resource not found"))?); } }; @@ -80,7 +83,6 @@ impl Controller { } } - // --- Per-method functions --- /// REPORT has been first described in the "Versioning Extension" of WebDAV @@ -89,7 +91,7 @@ impl Controller { /// Note: current implementation is not generic at all, it is heavily tied to CalDAV. /// A rewrite would be required to make it more generic (with the extension system that has /// been introduced in aero-dav) - async fn report(self) -> Result<HttpResponse> { + async fn report(self) -> Result<HttpResponse> { let status = hyper::StatusCode::from_u16(207)?; let report = match deserialize::<cal::Report<All>>(self.req).await { @@ -97,54 +99,75 @@ impl Controller { Err(e) => { tracing::error!(err=?e, "unable to decode REPORT body"); return Ok(Response::builder() - .status(400) - .body(text_body("Bad request"))?) + .status(400) + .body(text_body("Bad request"))?); } }; - // Multiget is really like a propfind where Depth: 0|1|Infinity is replaced by an arbitrary - // list of URLs - // @FIXME - let multiget = match report { - cal::Report::Multiget(m) => m, - cal::Report::Query(q) => todo!(), - cal::Report::FreeBusy(_) => return Ok(Response::builder() - .status(501) - .body(text_body("Not implemented"))?), - }; - - // Getting the list of nodes + // Internal representation that will handle processed request let (mut ok_node, mut not_found) = (Vec::new(), Vec::new()); - for h in multiget.href.into_iter() { - let maybe_collected_node = match Path::new(h.0.as_str()) { - Ok(Path::Abs(p)) => RootNode{}.fetch(&self.user, p.as_slice(), false).await.or(Err(h)), - Ok(Path::Rel(p)) => self.node.fetch(&self.user, p.as_slice(), false).await.or(Err(h)), - Err(_) => Err(h), - }; - - match maybe_collected_node { - Ok(v) => ok_node.push(v), - Err(h) => not_found.push(h), - }; - } + let calprop: Option<cal::CalendarSelector<All>>; + + // Extracting request information + match report { + cal::Report::Multiget(m) => { + // Multiget is really like a propfind where Depth: 0|1|Infinity is replaced by an arbitrary + // list of URLs + // Getting the list of nodes + for h in m.href.into_iter() { + let maybe_collected_node = match Path::new(h.0.as_str()) { + Ok(Path::Abs(p)) => RootNode {} + .fetch(&self.user, p.as_slice(), false) + .await + .or(Err(h)), + Ok(Path::Rel(p)) => self + .node + .fetch(&self.user, p.as_slice(), false) + .await + .or(Err(h)), + Err(_) => Err(h), + }; + + match maybe_collected_node { + Ok(v) => ok_node.push(v), + Err(h) => not_found.push(h), + }; + } + calprop = m.selector; + } + cal::Report::Query(q) => { + calprop = q.selector; + ok_node = apply_filter(&self.user, self.node.children(&self.user).await, q.filter) + .try_collect() + .await?; + } + cal::Report::FreeBusy(_) => { + return Ok(Response::builder() + .status(501) + .body(text_body("Not implemented"))?) + } + }; // Getting props - let props = match multiget.selector { + let props = match calprop { None | Some(cal::CalendarSelector::AllProp) => Some(dav::PropName(ALLPROP.to_vec())), Some(cal::CalendarSelector::PropName) => None, Some(cal::CalendarSelector::Prop(inner)) => Some(inner), }; - serialize(status, Self::multistatus(&self.user, ok_node, not_found, props).await) + serialize( + status, + Self::multistatus(&self.user, ok_node, not_found, props).await, + ) } /// PROPFIND is the standard way to fetch WebDAV properties - async fn propfind(self) -> Result<HttpResponse> { + async fn propfind(self) -> Result<HttpResponse> { let depth = depth(&self.req); if matches!(depth, dav::Depth::Infinity) { return Ok(Response::builder() .status(501) - .body(text_body("Depth: Infinity not implemented"))?) + .body(text_body("Depth: Infinity not implemented"))?); } let status = hyper::StatusCode::from_u16(207)?; @@ -153,7 +176,9 @@ impl Controller { // request body MUST be treated as if it were an 'allprop' request. // @FIXME here we handle any invalid data as an allprop, an empty request is thus correctly // handled, but corrupted requests are also silently handled as allprop. - let propfind = deserialize::<dav::PropFind<All>>(self.req).await.unwrap_or_else(|_| dav::PropFind::<All>::AllProp(None)); + let propfind = deserialize::<dav::PropFind<All>>(self.req) + .await + .unwrap_or_else(|_| dav::PropFind::<All>::AllProp(None)); tracing::debug!(recv=?propfind, "inferred propfind request"); // Collect nodes as PROPFIND is not limited to the targeted node @@ -170,29 +195,36 @@ impl Controller { dav::PropFind::AllProp(Some(dav::Include(mut include))) => { include.extend_from_slice(&ALLPROP); Some(dav::PropName(include)) - }, + } dav::PropFind::Prop(inner) => Some(inner), }; // Not Found is currently impossible considering the way we designed this function let not_found = vec![]; - serialize(status, Self::multistatus(&self.user, nodes, not_found, propname).await) + serialize( + status, + Self::multistatus(&self.user, nodes, not_found, propname).await, + ) } - async fn put(self) -> Result<HttpResponse> { + async fn put(self) -> Result<HttpResponse> { let put_policy = codec::put_policy(&self.req)?; let stream_of_frames = BodyStream::new(self.req.into_body()); let stream_of_bytes = stream_of_frames - .map_ok(|frame| frame.into_data()) - .map(|obj| match obj { - Ok(Ok(v)) => Ok(v), - Ok(Err(_)) => Err(std::io::Error::new(std::io::ErrorKind::Other, "conversion error")), - Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err)), - }).boxed(); + .map_ok(|frame| frame.into_data()) + .map(|obj| match obj { + Ok(Ok(v)) => Ok(v), + Ok(Err(_)) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "conversion error", + )), + Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err)), + }) + .boxed(); let etag = self.node.put(put_policy, stream_of_bytes).await?; - + let response = Response::builder() .status(201) .header("ETag", etag) @@ -202,7 +234,7 @@ impl Controller { Ok(response) } - async fn get(self) -> Result<HttpResponse> { + async fn get(self) -> Result<HttpResponse> { let stream_body = StreamBody::new(self.node.content().map_ok(|v| Frame::data(v))); let boxed_body = UnsyncBoxBody::new(stream_body); @@ -227,17 +259,33 @@ impl Controller { // --- Common utility functions --- /// Build a multistatus response from a list of DavNodes - async fn multistatus(user: &ArcUser, nodes: Vec<Box<dyn DavNode>>, not_found: Vec<dav::Href>, props: Option<dav::PropName<All>>) -> dav::Multistatus<All> { + async fn multistatus( + user: &ArcUser, + nodes: Vec<Box<dyn DavNode>>, + not_found: Vec<dav::Href>, + props: Option<dav::PropName<All>>, + ) -> dav::Multistatus<All> { // Collect properties on existing objects let mut responses: Vec<dav::Response<All>> = match props { - Some(props) => futures::stream::iter(nodes).then(|n| n.response_props(user, props.clone())).collect().await, - None => nodes.into_iter().map(|n| n.response_propname(user)).collect(), + Some(props) => { + futures::stream::iter(nodes) + .then(|n| n.response_props(user, props.clone())) + .collect() + .await + } + None => nodes + .into_iter() + .map(|n| n.response_propname(user)) + .collect(), }; // Register not found objects only if relevant if !not_found.is_empty() { responses.push(dav::Response { - status_or_propstat: dav::StatusOrPropstat::Status(not_found, dav::Status(hyper::StatusCode::NOT_FOUND)), + status_or_propstat: dav::StatusOrPropstat::Status( + not_found, + dav::Status(hyper::StatusCode::NOT_FOUND), + ), error: None, location: None, responsedescription: None, @@ -252,7 +300,6 @@ impl Controller { } } - /// Path is a voluntarily feature limited /// compared to the expressiveness of a UNIX path /// For example getting parent with ../ is not supported, scheme is not supported, etc. @@ -271,8 +318,39 @@ impl<'a> Path<'a> { let path_segments: Vec<_> = path.split("/").filter(|s| *s != "" && *s != ".").collect(); if path.starts_with("/") { - return Ok(Path::Abs(path_segments)) + return Ok(Path::Abs(path_segments)); } Ok(Path::Rel(path_segments)) } } + +//@FIXME move somewhere else +//@FIXME naive implementation, must be refactored later +use futures::stream::Stream; +use icalendar; +fn apply_filter( + user: &ArcUser, + nodes: Vec<Box<dyn DavNode>>, + filter: cal::Filter, +) -> impl Stream<Item = std::result::Result<Box<dyn DavNode>, std::io::Error>> { + futures::stream::iter(nodes).filter_map(|single_node| async move { + // Get ICS + let chunks: Vec<_> = match single_node.content().try_collect().await { + Ok(v) => v, + Err(e) => return Some(Err(e)), + }; + let raw_ics = chunks.iter().fold(String::new(), |mut acc, single_chunk| { + let str_fragment = std::str::from_utf8(single_chunk.as_ref()); + acc.extend(str_fragment); + acc + }); + + // Parse ICS + let ics = icalendar::parser::read_calendar(&raw_ics).unwrap(); + + // Do checks + + // Object has been kept + Some(Ok(single_node)) + }) +} |