diff options
author | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-04-23 18:07:00 +0200 |
---|---|---|
committer | Quentin Dufour <quentin@deuxfleurs.fr> | 2024-04-23 18:07:00 +0200 |
commit | 6de63055a239be05053424460d019cea8b8495a2 (patch) | |
tree | 3ee2972693237e61d63a91989aa31d3ce7305e7f /aero-proto | |
parent | adbccd88348f472751373a2e1d536e818be8fa67 (diff) | |
download | aerogramme-6de63055a239be05053424460d019cea8b8495a2.tar.gz aerogramme-6de63055a239be05053424460d019cea8b8495a2.zip |
successfully return ICS in REPORT queries
Diffstat (limited to 'aero-proto')
-rw-r--r-- | aero-proto/src/dav/controller.rs | 10 | ||||
-rw-r--r-- | aero-proto/src/dav/node.rs | 82 | ||||
-rw-r--r-- | aero-proto/src/dav/resource.rs | 245 |
3 files changed, 171 insertions, 166 deletions
diff --git a/aero-proto/src/dav/controller.rs b/aero-proto/src/dav/controller.rs index 5762581..2dcc7bc 100644 --- a/aero-proto/src/dav/controller.rs +++ b/aero-proto/src/dav/controller.rs @@ -135,7 +135,7 @@ impl Controller { Some(cal::CalendarSelector::Prop(inner)) => Some(inner), }; - serialize(status, Self::multistatus(&self.user, ok_node, not_found, props)) + serialize(status, Self::multistatus(&self.user, ok_node, not_found, props).await) } /// PROPFIND is the standard way to fetch WebDAV properties @@ -176,7 +176,7 @@ impl Controller { // 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)) + serialize(status, Self::multistatus(&self.user, nodes, not_found, propname).await) } async fn put(self) -> Result<HttpResponse> { @@ -204,7 +204,7 @@ impl Controller { } async fn get(self) -> Result<HttpResponse> { - let stream_body = StreamBody::new(self.node.content().await.map_ok(|v| Frame::data(v))); + let stream_body = StreamBody::new(self.node.content().map_ok(|v| Frame::data(v))); let boxed_body = UnsyncBoxBody::new(stream_body); let response = Response::builder() @@ -217,10 +217,10 @@ impl Controller { // --- Common utility functions --- /// Build a multistatus response from a list of DavNodes - 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) => nodes.into_iter().map(|n| n.response_props(user, props.clone())).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(), }; diff --git a/aero-proto/src/dav/node.rs b/aero-proto/src/dav/node.rs index 0b63900..00dabce 100644 --- a/aero-proto/src/dav/node.rs +++ b/aero-proto/src/dav/node.rs @@ -1,6 +1,6 @@ use anyhow::Result; -use futures::stream::{BoxStream, Stream}; -use futures::future::BoxFuture; +use futures::stream::{BoxStream, Stream, StreamExt}; +use futures::future::{BoxFuture, FutureExt}; use hyper::body::Bytes; use aero_dav::types as dav; @@ -10,6 +10,7 @@ use aero_collections::davdag::Etag; use super::controller::ArcUser; pub(crate) type Content<'a> = BoxStream<'a, std::result::Result<Bytes, std::io::Error>>; +pub(crate) type PropertyStream<'a> = BoxStream<'a, std::result::Result<dav::Property<All>, dav::PropertyRequest<All>>>; pub(crate) enum PutPolicy { CreateOnly, @@ -31,13 +32,14 @@ pub(crate) trait DavNode: Send { /// Get the supported WebDAV properties fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All>; /// Get the values for the given properties - fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> Vec<dav::AnyProperty<All>>; + fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static>; + //fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> Vec<dav::AnyProperty<All>>; /// Put an element (create or update) fn put<'a>(&'a self, policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, Result<Etag>>; /// Content type of the element fn content_type(&self) -> &str; /// Get content - fn content<'a>(&'a self) -> BoxFuture<'a, Content<'static>>; + fn content(&self) -> Content<'static>; //@FIXME maybe add etag, maybe add a way to set content @@ -62,39 +64,51 @@ pub(crate) trait DavNode: Send { } /// Utility function to get a prop response from a node & a list of propname - fn response_props(&self, user: &ArcUser, props: dav::PropName<All>) -> dav::Response<All> { - let mut prop_desc = vec![]; - let (found, not_found): (Vec<_>, Vec<_>) = self.properties(user, props).into_iter().partition(|v| matches!(v, dav::AnyProperty::Value(_))); + fn response_props(&self, user: &ArcUser, props: dav::PropName<All>) -> BoxFuture<'static, dav::Response<All>> { + //@FIXME we should make the DAV parsed object a stream... + let mut result_stream = self.properties(user, props); + let path = self.path(user); - // If at least one property has been found on this object, adding a HTTP 200 propstat to - // the response - if !found.is_empty() { - prop_desc.push(dav::PropStat { - status: dav::Status(hyper::StatusCode::OK), - prop: dav::AnyProp(found), - error: None, - responsedescription: None, - }); - } + async move { + let mut prop_desc = vec![]; + let (mut found, mut not_found) = (vec![], vec![]); + while let Some(maybe_prop) = result_stream.next().await { + match maybe_prop { + Ok(v) => found.push(dav::AnyProperty::Value(v)), + Err(v) => not_found.push(dav::AnyProperty::Request(v)), + } + } - // If at least one property can't be found on this object, adding a HTTP 404 propstat to - // the response - if !not_found.is_empty() { - prop_desc.push(dav::PropStat { - status: dav::Status(hyper::StatusCode::NOT_FOUND), - prop: dav::AnyProp(not_found), - error: None, - responsedescription: None, - }) - } + // If at least one property has been found on this object, adding a HTTP 200 propstat to + // the response + if !found.is_empty() { + prop_desc.push(dav::PropStat { + status: dav::Status(hyper::StatusCode::OK), + prop: dav::AnyProp(found), + error: None, + responsedescription: None, + }); + } - // Build the finale response - dav::Response { - status_or_propstat: dav::StatusOrPropstat::PropStat(dav::Href(self.path(user)), prop_desc), - error: None, - location: None, - responsedescription: None - } + // If at least one property can't be found on this object, adding a HTTP 404 propstat to + // the response + if !not_found.is_empty() { + prop_desc.push(dav::PropStat { + status: dav::Status(hyper::StatusCode::NOT_FOUND), + prop: dav::AnyProp(not_found), + error: None, + responsedescription: None, + }) + } + + // Build the finale response + dav::Response { + status_or_propstat: dav::StatusOrPropstat::PropStat(dav::Href(path), prop_desc), + error: None, + location: None, + responsedescription: None + } + }.boxed() } } diff --git a/aero-proto/src/dav/resource.rs b/aero-proto/src/dav/resource.rs index f13fb0c..7477ba9 100644 --- a/aero-proto/src/dav/resource.rs +++ b/aero-proto/src/dav/resource.rs @@ -13,6 +13,7 @@ use aero_dav::acltypes as acl; use aero_dav::realization::{All, self as all}; use crate::dav::node::{DavNode, PutPolicy, Content}; +use super::node::PropertyStream; #[derive(Clone)] pub(crate) struct RootNode {} @@ -48,27 +49,30 @@ impl DavNode for RootNode { dav::PropertyRequest::Extension(all::PropertyRequest::Acl(acl::PropertyRequest::CurrentUserPrincipal)), ]) } - fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> Vec<dav::AnyProperty<All>> { - prop.0.into_iter().map(|n| match n { - dav::PropertyRequest::DisplayName => dav::AnyProperty::Value(dav::Property::DisplayName("DAV Root".to_string())), - dav::PropertyRequest::ResourceType => dav::AnyProperty::Value(dav::Property::ResourceType(vec![ - dav::ResourceType::Collection, - ])), - dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())), - dav::PropertyRequest::Extension(all::PropertyRequest::Acl(acl::PropertyRequest::CurrentUserPrincipal)) => - dav::AnyProperty::Value(dav::Property::Extension(all::Property::Acl(acl::Property::CurrentUserPrincipal(acl::User::Authenticated(dav::Href(HomeNode{}.path(user))))))), - v => dav::AnyProperty::Request(v), - }).collect() + + fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> { + let user = user.clone(); + futures::stream::iter(prop.0).map(move |n| { + let prop = match n { + dav::PropertyRequest::DisplayName => dav::Property::DisplayName("DAV Root".to_string()), + dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![ + dav::ResourceType::Collection, + ]), + dav::PropertyRequest::GetContentType => dav::Property::GetContentType("httpd/unix-directory".into()), + dav::PropertyRequest::Extension(all::PropertyRequest::Acl(acl::PropertyRequest::CurrentUserPrincipal)) => + dav::Property::Extension(all::Property::Acl(acl::Property::CurrentUserPrincipal(acl::User::Authenticated(dav::Href(HomeNode{}.path(&user)))))), + v => return Err(v), + }; + Ok(prop) + }).boxed() } fn put<'a>(&'a self, _policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, Result<Etag>> { todo!() } - fn content<'a>(&'a self) -> BoxFuture<'a, Content<'static>> { - async { - futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed() - }.boxed() + fn content(&self) -> Content<'static> { + futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed() } fn content_type(&self) -> &str { @@ -116,32 +120,35 @@ impl DavNode for HomeNode { dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::CalendarHomeSet)), ]) } - fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> Vec<dav::AnyProperty<All>> { - prop.0.into_iter().map(|n| match n { - dav::PropertyRequest::DisplayName => dav::AnyProperty::Value(dav::Property::DisplayName(format!("{} home", user.username))), - dav::PropertyRequest::ResourceType => dav::AnyProperty::Value(dav::Property::ResourceType(vec![ - dav::ResourceType::Collection, - dav::ResourceType::Extension(all::ResourceType::Acl(acl::ResourceType::Principal)), - ])), - dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())), - dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::CalendarHomeSet)) => - dav::AnyProperty::Value(dav::Property::Extension(all::Property::Cal(cal::Property::CalendarHomeSet(dav::Href( - //@FIXME we are hardcoding the calendar path, instead we would want to use - //objects - format!("/{}/calendar/", user.username) - ))))), - v => dav::AnyProperty::Request(v), - }).collect() + fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> { + let user = user.clone(); + + futures::stream::iter(prop.0).map(move |n| { + let prop = match n { + dav::PropertyRequest::DisplayName => dav::Property::DisplayName(format!("{} home", user.username)), + dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![ + dav::ResourceType::Collection, + dav::ResourceType::Extension(all::ResourceType::Acl(acl::ResourceType::Principal)), + ]), + dav::PropertyRequest::GetContentType => dav::Property::GetContentType("httpd/unix-directory".into()), + dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::CalendarHomeSet)) => + dav::Property::Extension(all::Property::Cal(cal::Property::CalendarHomeSet(dav::Href( + //@FIXME we are hardcoding the calendar path, instead we would want to use + //objects + format!("/{}/calendar/", user.username) + )))), + v => return Err(v), + }; + Ok(prop) + }).boxed() } fn put<'a>(&'a self, _policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, Result<Etag>> { todo!() } - fn content<'a>(&'a self) -> BoxFuture<'a, Content<'static>> { - async { - futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed() - }.boxed() + fn content(&self) -> Content<'static> { + futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed() } @@ -209,23 +216,26 @@ impl DavNode for CalendarListNode { dav::PropertyRequest::GetContentType, ]) } - fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> Vec<dav::AnyProperty<All>> { - prop.0.into_iter().map(|n| match n { - dav::PropertyRequest::DisplayName => dav::AnyProperty::Value(dav::Property::DisplayName(format!("{} calendars", user.username))), - dav::PropertyRequest::ResourceType => dav::AnyProperty::Value(dav::Property::ResourceType(vec![dav::ResourceType::Collection])), - dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())), - v => dav::AnyProperty::Request(v), - }).collect() + fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> { + let user = user.clone(); + + futures::stream::iter(prop.0).map(move |n| { + let prop = match n { + dav::PropertyRequest::DisplayName => dav::Property::DisplayName(format!("{} calendars", user.username)), + dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![dav::ResourceType::Collection]), + dav::PropertyRequest::GetContentType => dav::Property::GetContentType("httpd/unix-directory".into()), + v => return Err(v), + }; + Ok(prop) + }).boxed() } fn put<'a>(&'a self, _policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, Result<Etag>> { todo!() } - fn content<'a>(&'a self) -> BoxFuture<'a, Content<'static>> { - async { - futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed() - }.boxed() + fn content(&self) -> Content<'static> { + futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed() } fn content_type(&self) -> &str { @@ -300,32 +310,35 @@ impl DavNode for CalendarNode { dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::SupportedCalendarComponentSet)), ]) } - fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> Vec<dav::AnyProperty<All>> { - prop.0.into_iter().map(|n| match n { - dav::PropertyRequest::DisplayName => dav::AnyProperty::Value(dav::Property::DisplayName(format!("{} calendar", self.calname))), - dav::PropertyRequest::ResourceType => dav::AnyProperty::Value(dav::Property::ResourceType(vec![ - dav::ResourceType::Collection, - dav::ResourceType::Extension(all::ResourceType::Cal(cal::ResourceType::Calendar)), - ])), - //dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())), - //@FIXME seems wrong but seems to be what Thunderbird expects... - dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("text/calendar".into())), - dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::SupportedCalendarComponentSet)) - => dav::AnyProperty::Value(dav::Property::Extension(all::Property::Cal(cal::Property::SupportedCalendarComponentSet(vec![ - cal::CompSupport(cal::Component::VEvent), - ])))), - v => dav::AnyProperty::Request(v), - }).collect() + fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> { + let calname = self.calname.to_string(); + + futures::stream::iter(prop.0).map(move |n| { + let prop = match n { + dav::PropertyRequest::DisplayName => dav::Property::DisplayName(format!("{} calendar", calname)), + dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![ + dav::ResourceType::Collection, + dav::ResourceType::Extension(all::ResourceType::Cal(cal::ResourceType::Calendar)), + ]), + //dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())), + //@FIXME seems wrong but seems to be what Thunderbird expects... + dav::PropertyRequest::GetContentType => dav::Property::GetContentType("text/calendar".into()), + dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::SupportedCalendarComponentSet)) + => dav::Property::Extension(all::Property::Cal(cal::Property::SupportedCalendarComponentSet(vec![ + cal::CompSupport(cal::Component::VEvent), + ]))), + v => return Err(v), + }; + Ok(prop) + }).boxed() } fn put<'a>(&'a self, _policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, Result<Etag>> { todo!() } - fn content<'a>(&'a self) -> BoxFuture<'a, Content<'static>> { - async { - futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed() - }.boxed() + fn content<'a>(&'a self) -> Content<'static> { + futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed() } fn content_type(&self) -> &str { @@ -333,37 +346,6 @@ impl DavNode for CalendarNode { } } -const FAKE_ICS: &str = r#"BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Example Corp.//CalDAV Client//EN -BEGIN:VTIMEZONE -LAST-MODIFIED:20040110T032845Z -TZID:US/Eastern -BEGIN:DAYLIGHT -DTSTART:20000404T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 -TZNAME:EDT -TZOFFSETFROM:-0500 -TZOFFSETTO:-0400 -END:DAYLIGHT -BEGIN:STANDARD -DTSTART:20001026T020000 -RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 -TZNAME:EST -TZOFFSETFROM:-0400 -TZOFFSETTO:-0500 -END:STANDARD -END:VTIMEZONE -BEGIN:VEVENT -DTSTAMP:20240406T001102Z -DTSTART;TZID=US/Eastern:20240406T100000 -DURATION:PT1H -SUMMARY:Event #1 -Description:Go Steelers! -UID:74855313FA803DA593CD579A@example.com -END:VEVENT -END:VCALENDAR"#; - #[derive(Clone)] pub(crate) struct EventNode { col: Arc<Calendar>, @@ -403,19 +385,31 @@ impl DavNode for EventNode { dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::CalendarData(cal::CalendarDataRequest::default()))), ]) } - fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> Vec<dav::AnyProperty<All>> { - prop.0.into_iter().map(|n| match n { - dav::PropertyRequest::DisplayName => dav::AnyProperty::Value(dav::Property::DisplayName(format!("{} event", self.filename))), - dav::PropertyRequest::ResourceType => dav::AnyProperty::Value(dav::Property::ResourceType(vec![])), - dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("text/calendar".into())), - dav::PropertyRequest::GetEtag => dav::AnyProperty::Value(dav::Property::GetEtag("\"abcdefg\"".into())), - dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::CalendarData(_req))) => - dav::AnyProperty::Value(dav::Property::Extension(all::Property::Cal(cal::Property::CalendarData(cal::CalendarDataPayload { - mime: None, - payload: FAKE_ICS.into() - })))), - v => dav::AnyProperty::Request(v), - }).collect() + fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> { + let this = self.clone(); + + futures::stream::iter(prop.0).then(move |n| { + let this = this.clone(); + + async move { + let prop = match &n { + dav::PropertyRequest::DisplayName => dav::Property::DisplayName(format!("{} event", this.filename)), + dav::PropertyRequest::ResourceType => dav::Property::ResourceType(vec![]), + dav::PropertyRequest::GetContentType => dav::Property::GetContentType("text/calendar".into()), + dav::PropertyRequest::GetEtag => dav::Property::GetEtag("\"abcdefg\"".into()), + dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::CalendarData(_req))) => { + let ics = String::from_utf8(this.col.get(this.blob_id).await.or(Err(n.clone()))?).or(Err(n.clone()))?; + + dav::Property::Extension(all::Property::Cal(cal::Property::CalendarData(cal::CalendarDataPayload { + mime: None, + payload: ics, + }))) + }, + _ => return Err(n), + }; + Ok(prop) + } + }).boxed() } fn put<'a>(&'a self, policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, Result<Etag>> { @@ -437,17 +431,16 @@ impl DavNode for EventNode { }.boxed() } - fn content<'a>(&'a self) -> BoxFuture<'a, Content<'static>> { - async { - //@FIXME for now, our storage interface does not allow streaming, - // so we load everything in memory - let content = self.col.get(self.blob_id).await.or(Err(std::io::Error::from(std::io::ErrorKind::Interrupted))); - let r = async { - Ok(hyper::body::Bytes::from(content?)) - }; - //tokio::pin!(r); - futures::stream::once(Box::pin(r)).boxed() - }.boxed() + fn content<'a>(&'a self) -> Content<'static> { + //@FIXME for now, our storage interface does not allow streaming, + // so we load everything in memory + let calendar = self.col.clone(); + let blob_id = self.blob_id.clone(); + let r = async move { + let content = calendar.get(blob_id).await.or(Err(std::io::Error::from(std::io::ErrorKind::Interrupted))); + Ok(hyper::body::Bytes::from(content?)) + }; + futures::stream::once(Box::pin(r)).boxed() } fn content_type(&self) -> &str { @@ -483,8 +476,8 @@ impl DavNode for CreateEventNode { dav::PropName(vec![]) } - fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> Vec<dav::AnyProperty<All>> { - vec![] + fn properties(&self, _user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static> { + futures::stream::iter(vec![]).boxed() } fn put<'a>(&'a self, _policy: PutPolicy, stream: Content<'a>) -> BoxFuture<'a, Result<Etag>> { @@ -500,10 +493,8 @@ impl DavNode for CreateEventNode { }.boxed() } - fn content<'a>(&'a self) -> BoxFuture<'a, Content<'static>> { - async { - futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed() - }.boxed() + fn content(&self) -> Content<'static> { + futures::stream::once(futures::future::err(std::io::Error::from(std::io::ErrorKind::Unsupported))).boxed() } fn content_type(&self) -> &str { |