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");
     }