You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2023/03/28 05:27:55 UTC
[incubator-opendal] branch main updated: fix: align WebDAV stat with RFC specification (#1783)
This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git
The following commit(s) were added to refs/heads/main by this push:
new 59d709d6 fix: align WebDAV stat with RFC specification (#1783)
59d709d6 is described below
commit 59d709d65b47322a904c5043c715058538012e3b
Author: Lemon <bx...@outlook.com>
AuthorDate: Tue Mar 28 13:27:51 2023 +0800
fix: align WebDAV stat with RFC specification (#1783)
* fix: align WebDAV stat with RFC specification
* fix: CI
* fix: CI
* fix: CI
* chore
---
core/src/services/webdav/backend.rs | 109 ++++++++++++++++--------------
core/src/services/webdav/list_response.rs | 65 ++++++++++++++++++
2 files changed, 122 insertions(+), 52 deletions(-)
diff --git a/core/src/services/webdav/backend.rs b/core/src/services/webdav/backend.rs
index ac6822f3..0f539ed9 100644
--- a/core/src/services/webdav/backend.rs
+++ b/core/src/services/webdav/backend.rs
@@ -21,19 +21,20 @@ use std::fmt::Formatter;
use async_trait::async_trait;
use bytes::Buf;
-use http::header;
use http::Request;
use http::Response;
use http::StatusCode;
+use http::{header, HeaderMap};
use log::debug;
+use crate::ops::*;
+use crate::raw::*;
+use crate::*;
+
use super::error::parse_error;
use super::list_response::Multistatus;
use super::pager::WebdavPager;
use super::writer::WebdavWriter;
-use crate::ops::*;
-use crate::raw::*;
-use crate::*;
/// [WebDAV](https://datatracker.ietf.org/doc/html/rfc4918) backend support.
///
@@ -229,6 +230,7 @@ impl Builder for WebdavBuilder {
})
}
}
+
/// Backend is used to serve `Accessor` support for http.
#[derive(Clone)]
pub struct WebdavBackend {
@@ -322,18 +324,39 @@ impl Accessor for WebdavBackend {
return Ok(RpStat::new(Metadata::new(EntryMode::DIR)));
}
- let resp = self.webdav_head(path).await?;
+ let mut header_map = HeaderMap::new();
+ // not include children
+ header_map.insert("Depth", "0".parse().unwrap());
+ header_map.insert(header::ACCEPT, "application/xml".parse().unwrap());
+
+ let resp = self.webdav_propfind(path, Some(header_map)).await?;
let status = resp.status();
- match status {
- StatusCode::OK => parse_into_metadata(path, resp.headers()).map(RpStat::new),
- // HTTP Server like nginx could return FORBIDDEN if auto-index
- // is not enabled, we should ignore them.
- StatusCode::NOT_FOUND | StatusCode::FORBIDDEN if path.ends_with('/') => {
- Ok(RpStat::new(Metadata::new(EntryMode::DIR)))
+ if !status.is_success() {
+ match status {
+ // HTTP Server like nginx could return FORBIDDEN if auto-index
+ // is not enabled, we should ignore them.
+ StatusCode::NOT_FOUND | StatusCode::FORBIDDEN if path.ends_with('/') => {
+ Ok(RpStat::new(Metadata::new(EntryMode::DIR)))
+ }
+ _ => Err(parse_error(resp).await?),
}
- _ => Err(parse_error(resp).await?),
+ } else {
+ let bs = resp.into_body().bytes().await?;
+ let result: Multistatus =
+ quick_xml::de::from_reader(bs.reader()).map_err(new_xml_deserialize_error)?;
+ let item = result
+ .response
+ .get(0)
+ .ok_or_else(|| {
+ Error::new(
+ ErrorKind::Unexpected,
+ "Failed getting item stat: bad response",
+ )
+ })?
+ .parse_into_metadata()?;
+ Ok(RpStat::new(item))
}
}
@@ -349,17 +372,10 @@ impl Accessor for WebdavBackend {
}
async fn list(&self, path: &str, _: OpList) -> Result<(RpList, Self::Pager)> {
- // XML body must start without a new line. Otherwise, the server will panic: `xmlParseChunk() failed`
- let all_prop_xml_body = r#"<?xml version="1.0" encoding="utf-8" ?>
- <D:propfind xmlns:D="DAV:">
- <D:allprop/>
- </D:propfind>
- "#;
-
- let async_body = AsyncBody::Bytes(bytes::Bytes::from(all_prop_xml_body));
- let resp = self
- .webdav_propfind(path, None, "application/xml".into(), async_body)
- .await?;
+ let mut header_map = HeaderMap::new();
+ header_map.insert("Depth", "1".parse().unwrap());
+ header_map.insert(header::CONTENT_TYPE, "application/xml".parse().unwrap());
+ let resp = self.webdav_propfind(path, Some(header_map)).await?;
let status = resp.status();
match status {
@@ -479,28 +495,35 @@ impl WebdavBackend {
async fn webdav_propfind(
&self,
path: &str,
- size: Option<u64>,
- content_type: Option<&str>,
- body: AsyncBody,
+ headers: Option<HeaderMap>,
) -> Result<Response<IncomingAsyncBody>> {
let p = build_abs_path(&self.root, path);
let url = format!("{}/{}", self.endpoint, percent_encode_path(&p));
- let mut req = Request::builder()
- .method("PROPFIND")
- .uri(&url)
- .header("Depth", "1");
+ let mut req = Request::builder().method("PROPFIND").uri(&url);
if let Some(auth) = &self.authorization {
req = req.header(header::AUTHORIZATION, auth);
}
- if let Some(size) = size {
- req = req.header(header::CONTENT_LENGTH, size)
+ if let Some(headers) = headers {
+ for (name, value) in headers {
+ // all key should be not None, otherwise panic
+ req = req.header(name.unwrap(), value);
+ }
}
- if let Some(mime) = content_type {
- req = req.header(header::CONTENT_TYPE, mime)
+ // rfc4918 9.1: retrieve all properties define in specification
+ let body;
+ {
+ req = req.header(header::CONTENT_TYPE, "application/xml");
+ // XML body must start without a new line. Otherwise, the server will panic: `xmlParseChunk() failed`
+ let all_prop_xml_body = r#"<?xml version="1.0" encoding="utf-8" ?>
+ <D:propfind xmlns:D="DAV:">
+ <D:allprop/>
+ </D:propfind>
+ "#;
+ body = AsyncBody::Bytes(bytes::Bytes::from(all_prop_xml_body));
}
let req = req.body(body).map_err(new_request_build_error)?;
@@ -508,24 +531,6 @@ impl WebdavBackend {
self.client.send_async(req).await
}
- async fn webdav_head(&self, path: &str) -> Result<Response<IncomingAsyncBody>> {
- let p = build_rooted_abs_path(&self.root, path);
-
- let url = format!("{}{}", self.endpoint, percent_encode_path(&p));
-
- let mut req = Request::head(&url);
-
- if let Some(auth) = &self.authorization {
- req = req.header(header::AUTHORIZATION, auth.clone())
- }
-
- let req = req
- .body(AsyncBody::Empty)
- .map_err(new_request_build_error)?;
-
- self.client.send_async(req).await
- }
-
async fn webdav_delete(&self, path: &str) -> Result<Response<IncomingAsyncBody>> {
let p = build_abs_path(&self.root, path);
diff --git a/core/src/services/webdav/list_response.rs b/core/src/services/webdav/list_response.rs
index 747fc360..b12b5c05 100644
--- a/core/src/services/webdav/list_response.rs
+++ b/core/src/services/webdav/list_response.rs
@@ -16,6 +16,10 @@
// under the License.
use serde::Deserialize;
+use time::format_description::well_known::Rfc2822;
+use time::OffsetDateTime;
+
+use crate::{EntryMode, Error, ErrorKind, Metadata, Result};
#[derive(Deserialize, Debug, PartialEq)]
pub struct Multistatus {
@@ -28,6 +32,58 @@ pub struct ListOpResponse {
pub propstat: Propstat,
}
+impl ListOpResponse {
+ pub fn parse_into_metadata(&self) -> Result<Metadata> {
+ let ListOpResponse {
+ href,
+ propstat:
+ Propstat {
+ prop:
+ Prop {
+ getlastmodified,
+ getcontentlength,
+ getcontenttype,
+ getetag,
+ ..
+ },
+ status,
+ },
+ } = self;
+ if let [_, code, text] = status.split(' ').collect::<Vec<_>>()[..3] {
+ // As defined in https://tools.ietf.org/html/rfc2068#section-6.1
+ let code = code.parse::<u16>().unwrap();
+ if code >= 400 {
+ return Err(Error::new(
+ ErrorKind::Unexpected,
+ &format!("Invalid response: {} {}", code, text),
+ ));
+ }
+ }
+
+ let mode = if href.ends_with('/') {
+ EntryMode::DIR
+ } else {
+ EntryMode::FILE
+ };
+ let mut m = Metadata::new(mode);
+
+ if let Some(v) = getcontentlength {
+ m.set_content_length(v.parse::<u64>().unwrap());
+ }
+
+ if let Some(v) = getcontenttype {
+ m.set_content_type(v);
+ }
+
+ if let Some(v) = getetag {
+ m.set_etag(v);
+ }
+ // https://www.rfc-editor.org/rfc/rfc4918#section-14.18
+ m.set_last_modified(OffsetDateTime::parse(getlastmodified, &Rfc2822).unwrap());
+ Ok(m)
+ }
+}
+
#[derive(Deserialize, Debug, PartialEq)]
pub struct Propstat {
pub prop: Prop,
@@ -36,7 +92,12 @@ pub struct Propstat {
#[derive(Deserialize, Debug, PartialEq)]
pub struct Prop {
+ #[serde(default)]
+ pub displayname: String,
pub getlastmodified: String,
+ pub getetag: Option<String>,
+ pub getcontentlength: Option<String>,
+ pub getcontenttype: Option<String>,
pub resourcetype: ResourceTypeContainer,
}
@@ -112,6 +173,9 @@ mod tests {
let response = from_str::<ListOpResponse>(xml).unwrap();
assert_eq!(response.href, "/");
+
+ assert_eq!(response.propstat.prop.displayname, "/");
+
assert_eq!(
response.propstat.prop.getlastmodified,
"Tue, 01 May 2022 06:39:47 GMT"
@@ -155,6 +219,7 @@ mod tests {
response.propstat.prop.getlastmodified,
"Tue, 07 May 2022 05:52:22 GMT"
);
+ assert_eq!(response.propstat.prop.getcontentlength.unwrap(), "1");
assert_eq!(response.propstat.prop.resourcetype.value, None);
assert_eq!(response.propstat.status, "HTTP/1.1 200 OK");
}