aboutsummaryrefslogtreecommitdiff
path: root/aero-proto/src/dav/controller.rs
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2024-05-16 17:38:34 +0200
committerQuentin Dufour <quentin@deuxfleurs.fr>2024-05-16 17:38:34 +0200
commit32dfd25f570b7a55bf43752684d286be0f6b2dc2 (patch)
treedd77871cda851bb5795743a3f04be61cf4c3ad61 /aero-proto/src/dav/controller.rs
parent6b9542088cd1b66af46e95b787493b601accb495 (diff)
downloadaerogramme-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.rs200
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))
+ })
+}