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/opendal.git


The following commit(s) were added to refs/heads/main by this push:
     new b84903485c refactor: Rewrite webdav to improve code quality (#4280)
b84903485c is described below

commit b84903485c8b207dfbf8d973cc4565345e8ed6cb
Author: Xuanwo <[email protected]>
AuthorDate: Wed Feb 28 10:07:25 2024 +0800

    refactor: Rewrite webdav to improve code quality (#4280)
---
 .github/services/webdav/jfrog/action.yml        |   5 +-
 core/src/services/webdav/backend.rs             | 452 ++-----------------
 core/src/services/webdav/{lister.rs => core.rs} | 517 ++++++++++++++++------
 core/src/services/webdav/lister.rs              | 557 ++----------------------
 core/src/services/webdav/mod.rs                 |   1 +
 core/src/services/webdav/writer.rs              |  11 +-
 6 files changed, 492 insertions(+), 1051 deletions(-)

diff --git a/.github/services/webdav/jfrog/action.yml 
b/.github/services/webdav/jfrog/action.yml
index aaa264ea61..b575fd532a 100644
--- a/.github/services/webdav/jfrog/action.yml
+++ b/.github/services/webdav/jfrog/action.yml
@@ -24,10 +24,7 @@ runs:
     - name: Setup webdav in jfrog
       shell: bash
       working-directory: fixtures/webdav
-      run: |
-        # Can we remove this?
-        # touch artifactory/etc/system.yaml
-        docker compose -f docker-compose-webdav-jfrog.yml up -d --wait
+      run: docker compose -f docker-compose-webdav-jfrog.yml up -d --wait
 
     - name: Setup
       shell: bash
diff --git a/core/src/services/webdav/backend.rs 
b/core/src/services/webdav/backend.rs
index c2e14436fc..ed358b943f 100644
--- a/core/src/services/webdav/backend.rs
+++ b/core/src/services/webdav/backend.rs
@@ -16,22 +16,18 @@
 // under the License.
 
 use std::collections::HashMap;
-use std::collections::VecDeque;
 use std::fmt::Debug;
 use std::fmt::Formatter;
+use std::str::FromStr;
+use std::sync::Arc;
 
 use async_trait::async_trait;
-use bytes::Buf;
-use http::header;
-use http::HeaderMap;
-use http::Request;
-use http::Response;
 use http::StatusCode;
 use log::debug;
 use serde::Deserialize;
 
+use super::core::*;
 use super::error::parse_error;
-use super::lister::Multistatus;
 use super::lister::WebdavLister;
 use super::writer::WebdavWriter;
 use crate::raw::*;
@@ -177,6 +173,16 @@ impl Builder for WebdavBuilder {
                     .with_context("service", Scheme::Webdav));
             }
         };
+        // Some services might return the path with suffix 
`/remote.php/webdav/`, we need to trim them.
+        let server_path = http::Uri::from_str(endpoint)
+            .map_err(|err| {
+                Error::new(ErrorKind::ConfigInvalid, "endpoint is invalid")
+                    .with_context("service", Scheme::Webdav)
+                    .set_source(err)
+            })?
+            .path()
+            .trim_end_matches('/')
+            .to_string();
 
         let root = 
normalize_root(&self.config.root.clone().unwrap_or_default());
         debug!("backend use root {}", root);
@@ -190,45 +196,41 @@ impl Builder for WebdavBuilder {
             })?
         };
 
-        let mut auth = None;
+        let mut authorization = None;
         if let Some(username) = &self.config.username {
-            auth = Some(format_authorization_by_basic(
+            authorization = Some(format_authorization_by_basic(
                 username,
                 self.config.password.as_deref().unwrap_or_default(),
             )?);
         }
         if let Some(token) = &self.config.token {
-            auth = Some(format_authorization_by_bearer(token)?)
+            authorization = Some(format_authorization_by_bearer(token)?)
         }
 
         debug!("backend build finished: {:?}", &self);
-        Ok(WebdavBackend {
+
+        let core = Arc::new(WebdavCore {
             endpoint: endpoint.to_string(),
-            authorization: auth,
+            server_path,
+            authorization,
             disable_copy: self.config.disable_copy,
             root,
             client,
-        })
+        });
+        Ok(WebdavBackend { core })
     }
 }
 
 /// Backend is used to serve `Accessor` support for http.
 #[derive(Clone)]
 pub struct WebdavBackend {
-    endpoint: String,
-    root: String,
-    client: HttpClient,
-    disable_copy: bool,
-
-    authorization: Option<String>,
+    core: Arc<WebdavCore>,
 }
 
 impl Debug for WebdavBackend {
     fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-        f.debug_struct("Backend")
-            .field("endpoint", &self.endpoint)
-            .field("root", &self.root)
-            .field("client", &self.client)
+        f.debug_struct("WebdavBackend")
+            .field("core", &self.core)
             .finish()
     }
 }
@@ -237,7 +239,7 @@ impl Debug for WebdavBackend {
 impl Accessor for WebdavBackend {
     type Reader = IncomingAsyncBody;
     type Writer = oio::OneShotWriter<WebdavWriter>;
-    type Lister = Option<oio::PageLister<WebdavLister>>;
+    type Lister = oio::PageLister<WebdavLister>;
     type BlockingReader = ();
     type BlockingWriter = ();
     type BlockingLister = ();
@@ -245,7 +247,7 @@ impl Accessor for WebdavBackend {
     fn info(&self) -> AccessorInfo {
         let mut ma = AccessorInfo::default();
         ma.set_scheme(Scheme::Webdav)
-            .set_root(&self.root)
+            .set_root(&self.core.root)
             .set_native_capability(Capability {
                 stat: true,
 
@@ -259,12 +261,13 @@ impl Accessor for WebdavBackend {
                 create_dir: true,
                 delete: true,
 
-                copy: !self.disable_copy,
+                copy: !self.core.disable_copy,
 
                 rename: true,
 
                 list: true,
-
+                // We already support recursive list but some details still 
need to polish.
+                // list_with_recursive: true,
                 ..Default::default()
             });
 
@@ -272,60 +275,17 @@ impl Accessor for WebdavBackend {
     }
 
     async fn create_dir(&self, path: &str, _: OpCreateDir) -> 
Result<RpCreateDir> {
-        self.ensure_parent_path(path).await?;
-        self.create_dir_internal(path).await?;
-
+        self.core.webdav_mkcol(path).await?;
         Ok(RpCreateDir::default())
     }
 
     async fn stat(&self, path: &str, _: OpStat) -> Result<RpStat> {
-        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();
-
-        if !status.is_success() {
-            return Err(parse_error(resp).await?);
-        }
-
-        let bs = resp.into_body().bytes().await?;
-        let s = String::from_utf8_lossy(&bs);
-
-        // Make sure the string is escaped.
-        // Related to <https://github.com/tafia/quick-xml/issues/719>
-        //
-        // This is a temporary solution, we should find a better way to handle 
this.
-        let s = s.replace("&()_+-=;", "%26%28%29_%2B-%3D%3B");
-        let result: Multistatus = 
quick_xml::de::from_str(&s).map_err(new_xml_deserialize_error)?;
-
-        let response = match result.response {
-            Some(v) => v,
-            None => {
-                return Err(Error::new(
-                    ErrorKind::NotFound,
-                    "Failed getting item stat: response field was not found",
-                ));
-            }
-        };
-
-        let item = response
-            .first()
-            .ok_or_else(|| {
-                Error::new(
-                    ErrorKind::Unexpected,
-                    "Failed getting item stat: bad response",
-                )
-            })?
-            .parse_into_metadata()?;
-        Ok(RpStat::new(item))
+        let metadata = self.core.webdav_stat(path).await?;
+        Ok(RpStat::new(metadata))
     }
 
     async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, 
Self::Reader)> {
-        let resp = self.webdav_get(path, args).await?;
+        let resp = self.core.webdav_get(path, args).await?;
         let status = resp.status();
         match status {
             StatusCode::OK | StatusCode::PARTIAL_CONTENT => {
@@ -345,21 +305,19 @@ impl Accessor for WebdavBackend {
     }
 
     async fn write(&self, path: &str, args: OpWrite) -> Result<(RpWrite, 
Self::Writer)> {
-        self.ensure_parent_path(path).await?;
-
-        let p = build_abs_path(&self.root, path);
+        // Ensure parent path exists
+        self.core.webdav_mkcol(get_parent(path)).await?;
 
         Ok((
             RpWrite::default(),
-            oio::OneShotWriter::new(WebdavWriter::new(self.clone(), args, p)),
+            oio::OneShotWriter::new(WebdavWriter::new(self.core.clone(), args, 
path.to_string())),
         ))
     }
 
     async fn delete(&self, path: &str, _: OpDelete) -> Result<RpDelete> {
-        let resp = self.webdav_delete(path).await?;
+        let resp = self.core.webdav_delete(path).await?;
 
         let status = resp.status();
-
         match status {
             StatusCode::NO_CONTENT | StatusCode::NOT_FOUND => 
Ok(RpDelete::default()),
             _ => Err(parse_error(resp).await?),
@@ -367,57 +325,14 @@ impl Accessor for WebdavBackend {
     }
 
     async fn list(&self, path: &str, args: OpList) -> Result<(RpList, 
Self::Lister)> {
-        if args.recursive() {
-            return Err(Error::new(
-                ErrorKind::Unsupported,
-                "webdav doesn't support list with recursive",
-            ));
-        }
-
-        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 {
-            StatusCode::OK | StatusCode::MULTI_STATUS => {
-                let bs = resp.into_body().bytes().await?;
-                let result: Multistatus =
-                    
quick_xml::de::from_reader(bs.reader()).map_err(new_xml_deserialize_error)?;
-
-                let result = match result.response {
-                    Some(v) => v,
-                    None => {
-                        return Ok((RpList::default(), None));
-                    }
-                };
-
-                let l = WebdavLister::new(&self.endpoint, &self.root, path, 
result);
-
-                Ok((RpList::default(), Some(oio::PageLister::new(l))))
-            }
-            StatusCode::NOT_FOUND if path.ends_with('/') => 
Ok((RpList::default(), None)),
-            _ => Err(parse_error(resp).await?),
-        }
+        Ok((
+            RpList::default(),
+            oio::PageLister::new(WebdavLister::new(self.core.clone(), path, 
args)),
+        ))
     }
 
-    /// # Notes
-    ///
-    /// There is a strange dead lock issues when copying a non-exist file, so 
we will check
-    /// if the source exists first.
-    ///
-    /// For example: <https://github.com/apache/opendal/pull/2809>
     async fn copy(&self, from: &str, to: &str, _args: OpCopy) -> 
Result<RpCopy> {
-        if let Err(err) = self.stat(from, OpStat::default()).await {
-            if err.kind() == ErrorKind::NotFound {
-                return Err(err);
-            }
-        }
-
-        self.ensure_parent_path(to).await?;
-
-        let resp = self.webdav_copy(from, to).await?;
+        let resp = self.core.webdav_copy(from, to).await?;
 
         let status = resp.status();
 
@@ -428,12 +343,9 @@ impl Accessor for WebdavBackend {
     }
 
     async fn rename(&self, from: &str, to: &str, _args: OpRename) -> 
Result<RpRename> {
-        self.ensure_parent_path(to).await?;
-
-        let resp = self.webdav_move(from, to).await?;
+        let resp = self.core.webdav_move(from, to).await?;
 
         let status = resp.status();
-
         match status {
             StatusCode::CREATED | StatusCode::NO_CONTENT | StatusCode::OK => {
                 Ok(RpRename::default())
@@ -442,279 +354,3 @@ impl Accessor for WebdavBackend {
         }
     }
 }
-
-impl WebdavBackend {
-    async fn webdav_get(&self, path: &str, args: OpRead) -> 
Result<Response<IncomingAsyncBody>> {
-        let p = build_rooted_abs_path(&self.root, path);
-        let url: String = format!("{}{}", self.endpoint, 
percent_encode_path(&p));
-
-        let mut req = Request::get(&url);
-
-        if let Some(auth) = &self.authorization {
-            req = req.header(header::AUTHORIZATION, auth.clone())
-        }
-
-        let range = args.range();
-        if !range.is_full() {
-            req = req.header(header::RANGE, range.to_header());
-        }
-
-        let req = req
-            .body(AsyncBody::Empty)
-            .map_err(new_request_build_error)?;
-
-        self.client.send(req).await
-    }
-
-    pub async fn webdav_put(
-        &self,
-        abs_path: &str,
-        size: Option<u64>,
-        args: &OpWrite,
-        body: AsyncBody,
-    ) -> Result<Response<IncomingAsyncBody>> {
-        let url = format!("{}/{}", self.endpoint, 
percent_encode_path(abs_path));
-
-        let mut req = Request::put(&url);
-
-        if let Some(auth) = &self.authorization {
-            req = req.header(header::AUTHORIZATION, auth.clone())
-        }
-
-        if let Some(size) = size {
-            req = req.header(header::CONTENT_LENGTH, size)
-        }
-
-        if let Some(mime) = args.content_type() {
-            req = req.header(header::CONTENT_TYPE, mime)
-        }
-
-        if let Some(cd) = args.content_disposition() {
-            req = req.header(header::CONTENT_DISPOSITION, cd)
-        }
-
-        // Set body
-        let req = req.body(body).map_err(new_request_build_error)?;
-
-        self.client.send(req).await
-    }
-
-    async fn webdav_mkcol_absolute_path(&self, path: &str) -> 
Result<Response<IncomingAsyncBody>> {
-        debug_assert!(path.starts_with('/'), "path must be absolute path");
-        let url = format!("{}{}", self.endpoint, percent_encode_path(path));
-
-        let mut req = Request::builder().method("MKCOL").uri(&url);
-        if let Some(auth) = &self.authorization {
-            req = req.header(header::AUTHORIZATION, auth);
-        }
-
-        let req = req
-            .body(AsyncBody::Empty)
-            .map_err(new_request_build_error)?;
-
-        self.client.send(req).await
-    }
-
-    async fn webdav_propfind(
-        &self,
-        path: &str,
-        headers: Option<HeaderMap>,
-    ) -> Result<Response<IncomingAsyncBody>> {
-        let p = build_rooted_abs_path(&self.root, path);
-
-        self.webdav_propfind_absolute_path(&p, headers).await
-    }
-
-    async fn webdav_propfind_absolute_path(
-        &self,
-        path: &str,
-        headers: Option<HeaderMap>,
-    ) -> Result<Response<IncomingAsyncBody>> {
-        debug_assert!(path.starts_with('/'), "path must be absolute path");
-
-        let url = format!("{}{}", self.endpoint, percent_encode_path(path));
-        let mut req = Request::builder().method("PROPFIND").uri(&url);
-
-        if let Some(auth) = &self.authorization {
-            req = req.header(header::AUTHORIZATION, auth);
-        }
-
-        if let Some(headers) = headers {
-            for (name, value) in headers {
-                // all key should be not None, otherwise panic
-                req = req.header(name.unwrap(), value);
-            }
-        }
-
-        // 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)?;
-
-        self.client.send(req).await
-    }
-
-    async fn webdav_delete(&self, path: &str) -> 
Result<Response<IncomingAsyncBody>> {
-        let p = build_abs_path(&self.root, path);
-
-        let url = format!("{}/{}", self.endpoint, percent_encode_path(&p));
-
-        let mut req = Request::delete(&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(req).await
-    }
-
-    async fn webdav_copy(&self, from: &str, to: &str) -> 
Result<Response<IncomingAsyncBody>> {
-        let source = build_abs_path(&self.root, from);
-        let target = build_abs_path(&self.root, to);
-        // Make sure target's dir is exist.
-        self.ensure_parent_path(&target).await?;
-
-        let source = format!("{}/{}", self.endpoint, 
percent_encode_path(&source));
-        let target = format!("{}/{}", self.endpoint, 
percent_encode_path(&target));
-
-        let mut req = Request::builder().method("COPY").uri(&source);
-
-        if let Some(auth) = &self.authorization {
-            req = req.header(header::AUTHORIZATION, auth);
-        }
-
-        req = req.header("Destination", target);
-
-        // We always specific "T" for keeping to overwrite the destination.
-        req = req.header("Overwrite", "T");
-
-        let req = req
-            .body(AsyncBody::Empty)
-            .map_err(new_request_build_error)?;
-
-        self.client.send(req).await
-    }
-
-    async fn webdav_move(&self, from: &str, to: &str) -> 
Result<Response<IncomingAsyncBody>> {
-        // Check if the source exists first.
-        self.stat(from, OpStat::new()).await?;
-
-        let source = build_abs_path(&self.root, from);
-        let target = build_abs_path(&self.root, to);
-        // Make sure target's dir is exist.
-        self.ensure_parent_path(&target).await?;
-
-        let source = format!("{}/{}", self.endpoint, 
percent_encode_path(&source));
-        let target = format!("{}/{}", self.endpoint, 
percent_encode_path(&target));
-
-        let mut req = Request::builder().method("MOVE").uri(&source);
-
-        if let Some(auth) = &self.authorization {
-            req = req.header(header::AUTHORIZATION, auth);
-        }
-
-        req = req.header("Destination", target);
-
-        // We always specific "T" for keeping to overwrite the destination.
-        req = req.header("Overwrite", "T");
-
-        let req = req
-            .body(AsyncBody::Empty)
-            .map_err(new_request_build_error)?;
-
-        self.client.send(req).await
-    }
-
-    async fn create_dir_internal(&self, path: &str) -> Result<()> {
-        let p = build_rooted_abs_path(&self.root, path);
-        self.create_dir_internal_absolute_path(&p).await
-    }
-
-    async fn create_dir_internal_absolute_path(&self, path: &str) -> 
Result<()> {
-        debug_assert!(path.starts_with('/'), "path must be absolute path");
-
-        let resp = self.webdav_mkcol_absolute_path(path).await?;
-
-        let status = resp.status();
-
-        match status {
-            StatusCode::CREATED
-            // Allow multiple status
-            | StatusCode::MULTI_STATUS
-            // The MKCOL method can only be performed on a deleted or 
non-existent resource.
-            // This error means the directory already exists which is allowed 
by create_dir.
-            | StatusCode::METHOD_NOT_ALLOWED => {
-                resp.into_body().consume().await?;
-                Ok(())
-            }
-            _ => Err(parse_error(resp).await?),
-        }
-    }
-
-    async fn ensure_parent_path(&self, path: &str) -> Result<()> {
-        let path = build_rooted_abs_path(&self.root, path);
-        let mut path = path.as_str();
-
-        let mut dirs = VecDeque::default();
-
-        loop {
-            // check path first.
-            let parent = get_parent(path);
-
-            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_absolute_path(parent, Some(header_map))
-                .await?;
-            match resp.status() {
-                StatusCode::OK => {
-                    break;
-                }
-                StatusCode::MULTI_STATUS => {
-                    let bs = resp.into_body().bytes().await?;
-                    let s = String::from_utf8_lossy(&bs);
-                    let result: Multistatus =
-                        
quick_xml::de::from_str(&s).map_err(new_xml_deserialize_error)?;
-
-                    if result.response.is_some() {
-                        break;
-                    }
-
-                    dirs.push_front(parent);
-                    path = parent
-                }
-                StatusCode::NOT_FOUND => {
-                    dirs.push_front(parent);
-                    path = parent
-                }
-                _ => return Err(parse_error(resp).await?),
-            }
-
-            if path == "/" {
-                break;
-            }
-        }
-
-        for dir in dirs {
-            self.create_dir_internal_absolute_path(dir).await?;
-        }
-        Ok(())
-    }
-}
diff --git a/core/src/services/webdav/lister.rs 
b/core/src/services/webdav/core.rs
similarity index 52%
copy from core/src/services/webdav/lister.rs
copy to core/src/services/webdav/core.rs
index 84f52f65ee..126f1e3f9d 100644
--- a/core/src/services/webdav/lister.rs
+++ b/core/src/services/webdav/core.rs
@@ -15,161 +15,434 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use async_trait::async_trait;
-use serde::Deserialize;
-use std::str::FromStr;
-
+use super::error::parse_error;
 use crate::raw::*;
 use crate::*;
+use bytes::Bytes;
+use http::{header, Request, Response, StatusCode};
+use serde::Deserialize;
+use std::collections::VecDeque;
+use std::fmt;
+use std::fmt::{Debug, Formatter};
+
+/// The request to query all properties of a file or directory.
+///
+/// rfc4918 9.1: retrieve all properties define in specification
+static PROPFIND_REQUEST: &str = r#"<?xml version="1.0" encoding="utf-8" 
?><D:propfind xmlns:D="DAV:"><D:allprop/></D:propfind>"#;
+
+/// The header to specify the depth of the query.
+///
+/// Valid values are `0`, `1`, `infinity`.
+///
+/// - `0`: only to the resource itself.
+/// - `1`: to the resource and its internal members only.
+/// - `infinity`: to the resource and all its members.
+///
+/// reference: [RFC4918: 10.2. Depth 
Header](https://datatracker.ietf.org/doc/html/rfc4918#section-10.2)
+static HEADER_DEPTH: &str = "Depth";
+/// The header to specify the destination of the query.
+///
+/// The Destination request header specifies the URI that identifies a
+/// destination resource for methods such as COPY and MOVE, which take
+/// two URIs as parameters.
+///
+/// reference: [RFC4918: 10.3.  Destination 
Header](https://datatracker.ietf.org/doc/html/rfc4918#section-10.3)
+static HEADER_DESTINATION: &str = "Destination";
+/// The header to specify the overwrite behavior of the query
+///
+/// The Overwrite request header specifies whether the server should
+/// overwrite a resource mapped to the destination URL during a COPY or
+/// MOVE.
+///
+/// Valid values are `T` and `F`.
+///
+/// A value of "F" states that the server must not perform the COPY or MOVE 
operation
+/// if the destination URL does map to a resource.
+///
+/// reference: [RFC4918: 10.6.  Overwrite 
Header](https://datatracker.ietf.org/doc/html/rfc4918#section-10.6)
+static HEADER_OVERWRITE: &str = "Overwrite";
+
+pub struct WebdavCore {
+    pub endpoint: String,
+    pub server_path: String,
+    pub root: String,
+    pub disable_copy: bool,
+    pub authorization: Option<String>,
+
+    pub client: HttpClient,
+}
 
-pub struct WebdavLister {
-    server_path: String,
-    root: String,
-    path: String,
-    response: Vec<ListOpResponse>,
+impl Debug for WebdavCore {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        f.debug_struct("WebdavCore")
+            .field("endpoint", &self.endpoint)
+            .field("root", &self.root)
+            .finish_non_exhaustive()
+    }
 }
 
-impl WebdavLister {
-    /// TODO: sending request in `next_page` instead of in `new`.
-    pub fn new(endpoint: &str, root: &str, path: &str, response: 
Vec<ListOpResponse>) -> Self {
-        // Some services might return the path with suffix 
`/remote.php/webdav/`, we need to trim them.
-        let server_path = http::Uri::from_str(endpoint)
-            .expect("must be valid http uri")
-            .path()
-            .trim_end_matches('/')
-            .to_string();
-        Self {
-            server_path,
-            root: root.into(),
-            path: path.into(),
-            response,
+impl WebdavCore {
+    pub async fn webdav_stat(&self, path: &str) -> Result<Metadata> {
+        let path = build_rooted_abs_path(&self.root, path);
+        self.webdav_stat_rooted_abs_path(&path).await
+    }
+
+    /// Input path must be `rooted_abs_path`.
+    async fn webdav_stat_rooted_abs_path(&self, rooted_abs_path: &str) -> 
Result<Metadata> {
+        let url = format!("{}{}", self.endpoint, 
percent_encode_path(rooted_abs_path));
+        let mut req = Request::builder().method("PROPFIND").uri(url);
+
+        req = req.header(header::CONTENT_TYPE, "application/xml");
+        req = req.header(header::CONTENT_LENGTH, PROPFIND_REQUEST.len());
+        if let Some(auth) = &self.authorization {
+            req = req.header(header::AUTHORIZATION, auth);
         }
+
+        // Only stat the resource itself.
+        req = req.header(HEADER_DEPTH, "0");
+
+        let req = req
+            .body(AsyncBody::Bytes(Bytes::from(PROPFIND_REQUEST)))
+            .map_err(new_request_build_error)?;
+
+        let resp = self.client.send(req).await?;
+        if !resp.status().is_success() {
+            return Err(parse_error(resp).await?);
+        }
+
+        let bs = resp.into_body().bytes().await?;
+
+        let result: Multistatus = deserialize_multistatus(&bs)?;
+        let propfind_resp = result.response.first().ok_or_else(|| {
+            Error::new(
+                ErrorKind::NotFound,
+                "propfind response is empty, the resource is not exist",
+            )
+        })?;
+
+        let metadata = parse_propstat(&propfind_resp.propstat)?;
+        Ok(metadata)
     }
-}
 
-#[async_trait]
-impl oio::PageList for WebdavLister {
-    async fn next_page(&self, ctx: &mut oio::PageContext) -> Result<()> {
-        for res in &self.response {
-            let mut path = res
-                .href
-                .strip_prefix(&self.server_path)
-                .unwrap_or(&res.href)
-                .to_string();
-
-            let meta = res.parse_into_metadata()?;
-
-            // Append `/` to path if it's a dir
-            if !path.ends_with('/') && meta.is_dir() {
-                path += "/"
-            }
+    pub async fn webdav_get(
+        &self,
+        path: &str,
+        args: OpRead,
+    ) -> Result<Response<IncomingAsyncBody>> {
+        let path = build_rooted_abs_path(&self.root, path);
+        let url: String = format!("{}{}", self.endpoint, 
percent_encode_path(&path));
 
-            // Ignore the root path itself.
-            if self.root == path {
-                continue;
-            }
+        let mut req = Request::get(&url);
 
-            let normalized_path = build_rel_path(&self.root, &path);
-            let decoded_path = percent_decode_path(normalized_path.as_str());
+        if let Some(auth) = &self.authorization {
+            req = req.header(header::AUTHORIZATION, auth.clone())
+        }
 
-            if normalized_path == self.path || decoded_path == self.path {
-                // WebDav server may return the current path as an entry.
-                continue;
-            }
+        let range = args.range();
+        if !range.is_full() {
+            req = req.header(header::RANGE, range.to_header());
+        }
 
-            // Mark files complete if it's an `application/x-checksum` file.
-            //
-            // AFAIK, this content type is only used by jfrog artifactory. And 
this file is
-            // a shadow file that can't be stat, so we mark it as complete.
-            if meta.contains_metakey(Metakey::ContentType)
-                && meta.content_type() == Some("application/x-checksum")
-            {
-                continue;
-            }
+        let req = req
+            .body(AsyncBody::Empty)
+            .map_err(new_request_build_error)?;
+
+        self.client.send(req).await
+    }
+
+    pub async fn webdav_put(
+        &self,
+        path: &str,
+        size: Option<u64>,
+        args: &OpWrite,
+        body: AsyncBody,
+    ) -> Result<Response<IncomingAsyncBody>> {
+        let path = build_rooted_abs_path(&self.root, path);
+        let url = format!("{}{}", self.endpoint, percent_encode_path(&path));
 
-            ctx.entries.push_back(oio::Entry::new(&decoded_path, meta))
+        let mut req = Request::put(&url);
+
+        if let Some(v) = &self.authorization {
+            req = req.header(header::AUTHORIZATION, v)
         }
-        ctx.done = true;
 
-        Ok(())
+        if let Some(v) = size {
+            req = req.header(header::CONTENT_LENGTH, v)
+        }
+
+        if let Some(v) = args.content_type() {
+            req = req.header(header::CONTENT_TYPE, v)
+        }
+
+        if let Some(v) = args.content_disposition() {
+            req = req.header(header::CONTENT_DISPOSITION, v)
+        }
+
+        let req = req.body(body).map_err(new_request_build_error)?;
+
+        self.client.send(req).await
     }
-}
 
-#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
-pub struct Multistatus {
-    pub response: Option<Vec<ListOpResponse>>,
-}
+    pub async fn webdav_delete(&self, path: &str) -> 
Result<Response<IncomingAsyncBody>> {
+        let path = build_rooted_abs_path(&self.root, path);
+        let url = format!("{}{}", self.endpoint, percent_encode_path(&path));
 
-#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
-pub struct ListOpResponse {
-    pub href: String,
-    pub propstat: Propstat,
-}
+        let mut req = Request::delete(&url);
 
-impl ListOpResponse {
-    pub fn parse_into_metadata(&self) -> Result<Metadata> {
-        let ListOpResponse {
-            propstat:
-                Propstat {
-                    prop:
-                        Prop {
-                            getlastmodified,
-                            getcontentlength,
-                            getcontenttype,
-                            getetag,
-                            resourcetype,
-                            ..
-                        },
-                    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),
-                ));
-            }
+        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(req).await
+    }
+
+    pub async fn webdav_copy(&self, from: &str, to: &str) -> 
Result<Response<IncomingAsyncBody>> {
+        // Check if source file exists.
+        let _ = self.webdav_stat(from).await?;
+        // Make sure target's dir is exist.
+        self.webdav_mkcol(get_parent(to)).await?;
+
+        let source = build_rooted_abs_path(&self.root, from);
+        let source_uri = format!("{}{}", self.endpoint, 
percent_encode_path(&source));
+
+        let target = build_rooted_abs_path(&self.root, to);
+        let target_uri = format!("{}{}", self.endpoint, 
percent_encode_path(&target));
+
+        let mut req = Request::builder().method("COPY").uri(&source_uri);
+
+        if let Some(auth) = &self.authorization {
+            req = req.header(header::AUTHORIZATION, auth);
         }
 
-        let mode: EntryMode = if resourcetype.value == 
Some(ResourceType::Collection) {
-            EntryMode::DIR
+        req = req.header(HEADER_DESTINATION, target_uri);
+        req = req.header(HEADER_OVERWRITE, "T");
+
+        let req = req
+            .body(AsyncBody::Empty)
+            .map_err(new_request_build_error)?;
+
+        self.client.send(req).await
+    }
+
+    pub async fn webdav_move(&self, from: &str, to: &str) -> 
Result<Response<IncomingAsyncBody>> {
+        // Check if source file exists.
+        let _ = self.webdav_stat(from).await?;
+        // Make sure target's dir is exist.
+        self.webdav_mkcol(get_parent(to)).await?;
+
+        let source = build_rooted_abs_path(&self.root, from);
+        let source_uri = format!("{}{}", self.endpoint, 
percent_encode_path(&source));
+
+        let target = build_rooted_abs_path(&self.root, to);
+        let target_uri = format!("{}{}", self.endpoint, 
percent_encode_path(&target));
+
+        let mut req = Request::builder().method("MOVE").uri(&source_uri);
+
+        if let Some(auth) = &self.authorization {
+            req = req.header(header::AUTHORIZATION, auth);
+        }
+
+        req = req.header(HEADER_DESTINATION, target_uri);
+        req = req.header(HEADER_OVERWRITE, "T");
+
+        let req = req
+            .body(AsyncBody::Empty)
+            .map_err(new_request_build_error)?;
+
+        self.client.send(req).await
+    }
+
+    pub async fn webdav_list(
+        &self,
+        path: &str,
+        args: &OpList,
+    ) -> Result<Response<IncomingAsyncBody>> {
+        let path = build_rooted_abs_path(&self.root, path);
+        let url = format!("{}{}", self.endpoint, percent_encode_path(&path));
+
+        let mut req = Request::builder().method("PROPFIND").uri(&url);
+
+        req = req.header(header::CONTENT_TYPE, "application/xml");
+        req = req.header(header::CONTENT_LENGTH, PROPFIND_REQUEST.len());
+        if let Some(auth) = &self.authorization {
+            req = req.header(header::AUTHORIZATION, auth);
+        }
+
+        if args.recursive() {
+            req = req.header(HEADER_DEPTH, "infinity");
         } else {
-            EntryMode::FILE
-        };
-        let mut m = Metadata::new(mode);
+            req = req.header(HEADER_DEPTH, "1");
+        }
+
+        let req = req
+            .body(AsyncBody::Bytes(Bytes::from(PROPFIND_REQUEST)))
+            .map_err(new_request_build_error)?;
+
+        self.client.send(req).await
+    }
+
+    /// Create dir recursively for given path.
+    ///
+    /// # Notes
+    ///
+    /// We only expose this method to the backend since there are dependencies 
on input path.
+    pub async fn webdav_mkcol(&self, path: &str) -> Result<()> {
+        let path = build_rooted_abs_path(&self.root, path);
+        let mut path = path.as_str();
+
+        let mut dirs = VecDeque::default();
+
+        loop {
+            match self.webdav_stat_rooted_abs_path(path).await {
+                // Dir is exist, break the loop.
+                Ok(_) => {
+                    break;
+                }
+                // Dir not found, keep going.
+                Err(err) if err.kind() == ErrorKind::NotFound => {
+                    dirs.push_front(path);
+                    path = get_parent(path);
+                }
+                // Unexpected error found, return it.
+                Err(err) => return Err(err),
+            }
+
+            if path == "/" {
+                break;
+            }
+        }
+
+        for dir in dirs {
+            self.webdav_mkcol_rooted_abs_path(dir).await?;
+        }
+        Ok(())
+    }
+
+    /// Create a dir
+    ///
+    /// Input path must be `rooted_abs_path`
+    ///
+    /// Reference: [RFC4918: 9.3.1.  MKCOL Status 
Codes](https://datatracker.ietf.org/doc/html/rfc4918#section-9.3.1)
+    async fn webdav_mkcol_rooted_abs_path(&self, rooted_abs_path: &str) -> 
Result<()> {
+        let url = format!("{}{}", self.endpoint, 
percent_encode_path(rooted_abs_path));
 
-        if let Some(v) = getcontentlength {
-            m.set_content_length(v.parse::<u64>().unwrap());
+        let mut req = Request::builder().method("MKCOL").uri(&url);
+
+        if let Some(auth) = &self.authorization {
+            req = req.header(header::AUTHORIZATION, auth.clone())
         }
 
-        if let Some(v) = getcontenttype {
-            m.set_content_type(v);
+        let req = req
+            .body(AsyncBody::Empty)
+            .map_err(new_request_build_error)?;
+
+        let resp = self.client.send(req).await?;
+        let status = resp.status();
+
+        match status {
+            // 201 (Created) - The collection was created.
+            StatusCode::CREATED
+            // 405 (Method Not Allowed) - MKCOL can only be executed on an 
unmapped URL.
+            //
+            // The MKCOL method can only be performed on a deleted or 
non-existent resource.
+            // This error means the directory already exists which is allowed 
by create_dir.
+            | StatusCode::METHOD_NOT_ALLOWED => {
+                resp.into_body().consume().await?;
+                Ok(())
+            }
+            _ => Err(parse_error(resp).await?),
         }
+    }
+}
 
-        if let Some(v) = getetag {
-            m.set_etag(v);
+pub fn deserialize_multistatus(bs: &[u8]) -> Result<Multistatus> {
+    let s = String::from_utf8_lossy(bs);
+    // HACKS! HACKS! HACKS!
+    //
+    // Make sure the string is escaped.
+    // Related to <https://github.com/tafia/quick-xml/issues/719>
+    //
+    // This is a temporary solution, we should find a better way to handle 
this.
+    let s = s.replace("&()_+-=;", "%26%28%29_%2B-%3D%3B");
+
+    quick_xml::de::from_str(&s).map_err(new_xml_deserialize_error)
+}
+
+pub fn parse_propstat(propstat: &Propstat) -> Result<Metadata> {
+    let Propstat {
+        prop:
+            Prop {
+                getlastmodified,
+                getcontentlength,
+                getcontenttype,
+                getetag,
+                resourcetype,
+                ..
+            },
+        status,
+    } = propstat;
+
+    if let [_, code, text] = status.splitn(3, ' ').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!("propfind response is unexpected: {} {}", code, text),
+            ));
         }
+    }
+
+    let mode: EntryMode = if resourcetype.value == 
Some(ResourceType::Collection) {
+        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);
+    }
 
-        // https://www.rfc-editor.org/rfc/rfc4918#section-14.18
-        m.set_last_modified(parse_datetime_from_rfc2822(getlastmodified)?);
-        Ok(m)
+    if let Some(v) = getetag {
+        m.set_etag(v);
     }
+
+    // https://www.rfc-editor.org/rfc/rfc4918#section-14.18
+    m.set_last_modified(parse_datetime_from_rfc2822(getlastmodified)?);
+
+    // the storage services have returned all the properties
+    Ok(m.with_metakey(Metakey::Complete))
+}
+
+#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
+#[serde(default)]
+pub struct Multistatus {
+    pub response: Vec<PropfindResponse>,
+}
+
+#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
+pub struct PropfindResponse {
+    pub href: String,
+    pub propstat: Propstat,
 }
 
 #[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
 pub struct Propstat {
-    pub prop: Prop,
     pub status: String,
+    pub prop: Prop,
 }
 
 #[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
 pub struct Prop {
-    #[serde(default)]
-    pub displayname: String,
     pub getlastmodified: String,
     pub getetag: Option<String>,
     pub getcontentlength: Option<String>,
@@ -247,11 +520,9 @@ mod tests {
             </D:propstat>
         </D:response>"#;
 
-        let response = from_str::<ListOpResponse>(xml).unwrap();
+        let response = from_str::<PropfindResponse>(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"
@@ -289,7 +560,7 @@ mod tests {
         </D:propstat>
       </D:response>"#;
 
-        let response = from_str::<ListOpResponse>(xml).unwrap();
+        let response = from_str::<PropfindResponse>(xml).unwrap();
         assert_eq!(response.href, "/test_file");
         assert_eq!(
             response.propstat.prop.getlastmodified,
@@ -343,7 +614,7 @@ mod tests {
 
         let multistatus = from_str::<Multistatus>(xml).unwrap();
 
-        let response = multistatus.response.unwrap();
+        let response = multistatus.response;
         assert_eq!(response.len(), 2);
         assert_eq!(response[0].href, "/");
         assert_eq!(
@@ -431,7 +702,7 @@ mod tests {
 
         let multistatus = from_str::<Multistatus>(xml).unwrap();
 
-        let response = multistatus.response.unwrap();
+        let response = multistatus.response;
         assert_eq!(response.len(), 3);
         let first_response = &response[0];
         assert_eq!(first_response.href, "/");
@@ -564,7 +835,7 @@ mod tests {
 
         let multistatus: Multistatus = from_str(xml).unwrap();
 
-        let response = multistatus.response.unwrap();
+        let response = multistatus.response;
         assert_eq!(response.len(), 9);
 
         let first_response = &response[0];
diff --git a/core/src/services/webdav/lister.rs 
b/core/src/services/webdav/lister.rs
index 84f52f65ee..b69efb61b5 100644
--- a/core/src/services/webdav/lister.rs
+++ b/core/src/services/webdav/lister.rs
@@ -16,33 +16,27 @@
 // under the License.
 
 use async_trait::async_trait;
-use serde::Deserialize;
-use std::str::FromStr;
+use http::StatusCode;
+use std::sync::Arc;
 
+use super::core::*;
+use super::error::*;
 use crate::raw::*;
 use crate::*;
 
 pub struct WebdavLister {
-    server_path: String,
-    root: String,
+    core: Arc<WebdavCore>,
+
     path: String,
-    response: Vec<ListOpResponse>,
+    args: OpList,
 }
 
 impl WebdavLister {
-    /// TODO: sending request in `next_page` instead of in `new`.
-    pub fn new(endpoint: &str, root: &str, path: &str, response: 
Vec<ListOpResponse>) -> Self {
-        // Some services might return the path with suffix 
`/remote.php/webdav/`, we need to trim them.
-        let server_path = http::Uri::from_str(endpoint)
-            .expect("must be valid http uri")
-            .path()
-            .trim_end_matches('/')
-            .to_string();
+    pub fn new(core: Arc<WebdavCore>, path: &str, args: OpList) -> Self {
         Self {
-            server_path,
-            root: root.into(),
-            path: path.into(),
-            response,
+            core,
+            path: path.to_string(),
+            args,
         }
     }
 }
@@ -50,14 +44,37 @@ impl WebdavLister {
 #[async_trait]
 impl oio::PageList for WebdavLister {
     async fn next_page(&self, ctx: &mut oio::PageContext) -> Result<()> {
-        for res in &self.response {
+        let resp = self.core.webdav_list(&self.path, &self.args).await?;
+
+        // jfrog artifactory's webdav services have some strange behavior.
+        // We add this flag to check if the server is jfrog artifactory.
+        //
+        // Example: `"x-jfrog-version": "Artifactory/7.77.5 77705900"`
+        let is_jfrog_artifactory = if let Some(v) = 
resp.headers().get("x-jfrog-version") {
+            v.to_str().unwrap_or_default().starts_with("Artifactory")
+        } else {
+            false
+        };
+
+        let bs = if resp.status().is_success() {
+            resp.into_body().bytes().await?
+        } else if resp.status() == StatusCode::NOT_FOUND && 
self.path.ends_with('/') {
+            ctx.done = true;
+            return Ok(());
+        } else {
+            return Err(parse_error(resp).await?);
+        };
+
+        let result: Multistatus = deserialize_multistatus(&bs)?;
+
+        for res in result.response {
             let mut path = res
                 .href
-                .strip_prefix(&self.server_path)
+                .strip_prefix(&self.core.server_path)
                 .unwrap_or(&res.href)
                 .to_string();
 
-            let meta = res.parse_into_metadata()?;
+            let meta = parse_propstat(&res.propstat)?;
 
             // Append `/` to path if it's a dir
             if !path.ends_with('/') && meta.is_dir() {
@@ -65,25 +82,24 @@ impl oio::PageList for WebdavLister {
             }
 
             // Ignore the root path itself.
-            if self.root == path {
+            if self.core.root == path {
                 continue;
             }
 
-            let normalized_path = build_rel_path(&self.root, &path);
-            let decoded_path = percent_decode_path(normalized_path.as_str());
+            let normalized_path = build_rel_path(&self.core.root, &path);
+            let decoded_path = percent_decode_path(&normalized_path);
 
             if normalized_path == self.path || decoded_path == self.path {
-                // WebDav server may return the current path as an entry.
+                // WebDAV server may return the current path as an entry.
                 continue;
             }
 
-            // Mark files complete if it's an `application/x-checksum` file.
+            // HACKS! HACKS! HACKS!
             //
-            // AFAIK, this content type is only used by jfrog artifactory. And 
this file is
-            // a shadow file that can't be stat, so we mark it as complete.
-            if meta.contains_metakey(Metakey::ContentType)
-                && meta.content_type() == Some("application/x-checksum")
-            {
+            // jfrog artifactory will generate a virtual checksum file for 
each file.
+            // The checksum file can't be stated, but can be listed and read.
+            // We ignore the checksum files to avoid listing unexpected files.
+            if is_jfrog_artifactory && meta.content_type() == 
Some("application/x-checksum") {
                 continue;
             }
 
@@ -94,484 +110,3 @@ impl oio::PageList for WebdavLister {
         Ok(())
     }
 }
-
-#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
-pub struct Multistatus {
-    pub response: Option<Vec<ListOpResponse>>,
-}
-
-#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
-pub struct ListOpResponse {
-    pub href: String,
-    pub propstat: Propstat,
-}
-
-impl ListOpResponse {
-    pub fn parse_into_metadata(&self) -> Result<Metadata> {
-        let ListOpResponse {
-            propstat:
-                Propstat {
-                    prop:
-                        Prop {
-                            getlastmodified,
-                            getcontentlength,
-                            getcontenttype,
-                            getetag,
-                            resourcetype,
-                            ..
-                        },
-                    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: EntryMode = if resourcetype.value == 
Some(ResourceType::Collection) {
-            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(parse_datetime_from_rfc2822(getlastmodified)?);
-        Ok(m)
-    }
-}
-
-#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
-pub struct Propstat {
-    pub prop: Prop,
-    pub status: String,
-}
-
-#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
-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,
-}
-
-#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
-pub struct ResourceTypeContainer {
-    #[serde(rename = "$value")]
-    pub value: Option<ResourceType>,
-}
-
-#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
-#[serde(rename_all = "lowercase")]
-pub enum ResourceType {
-    Collection,
-}
-
-#[cfg(test)]
-mod tests {
-    use quick_xml::de::from_str;
-
-    use super::*;
-
-    #[test]
-    fn test_propstat() {
-        let xml = r#"<D:propstat>
-            <D:prop>
-                <D:displayname>/</D:displayname>
-                <D:getlastmodified>Tue, 01 May 2022 06:39:47 
GMT</D:getlastmodified>
-                <D:resourcetype><D:collection/></D:resourcetype>
-                <D:lockdiscovery/>
-                <D:supportedlock>
-                    <D:lockentry>
-                        <D:lockscope><D:exclusive/></D:lockscope>
-                        <D:locktype><D:write/></D:locktype>
-                    </D:lockentry>
-                </D:supportedlock>
-            </D:prop>
-            <D:status>HTTP/1.1 200 OK</D:status>
-        </D:propstat>"#;
-
-        let propstat = from_str::<Propstat>(xml).unwrap();
-        assert_eq!(
-            propstat.prop.getlastmodified,
-            "Tue, 01 May 2022 06:39:47 GMT"
-        );
-        assert_eq!(
-            propstat.prop.resourcetype.value.unwrap(),
-            ResourceType::Collection
-        );
-
-        assert_eq!(propstat.status, "HTTP/1.1 200 OK");
-    }
-
-    #[test]
-    fn test_response_simple() {
-        let xml = r#"<D:response>
-            <D:href>/</D:href>
-            <D:propstat>
-                <D:prop>
-                    <D:displayname>/</D:displayname>
-                    <D:getlastmodified>Tue, 01 May 2022 06:39:47 
GMT</D:getlastmodified>
-                    <D:resourcetype><D:collection/></D:resourcetype>
-                    <D:lockdiscovery/>
-                    <D:supportedlock>
-                        <D:lockentry>
-                            <D:lockscope><D:exclusive/></D:lockscope>
-                            <D:locktype><D:write/></D:locktype>
-                        </D:lockentry>
-                    </D:supportedlock>
-                </D:prop>
-                <D:status>HTTP/1.1 200 OK</D:status>
-            </D:propstat>
-        </D:response>"#;
-
-        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"
-        );
-        assert_eq!(
-            response.propstat.prop.resourcetype.value.unwrap(),
-            ResourceType::Collection
-        );
-        assert_eq!(response.propstat.status, "HTTP/1.1 200 OK");
-    }
-
-    #[test]
-    fn test_response_file() {
-        let xml = r#"<D:response>
-        <D:href>/test_file</D:href>
-        <D:propstat>
-          <D:prop>
-            <D:displayname>test_file</D:displayname>
-            <D:getcontentlength>1</D:getcontentlength>
-            <D:getlastmodified>Tue, 07 May 2022 05:52:22 
GMT</D:getlastmodified>
-            <D:resourcetype></D:resourcetype>
-            <D:lockdiscovery />
-            <D:supportedlock>
-              <D:lockentry>
-                <D:lockscope>
-                  <D:exclusive />
-                </D:lockscope>
-                <D:locktype>
-                  <D:write />
-                </D:locktype>
-              </D:lockentry>
-            </D:supportedlock>
-          </D:prop>
-          <D:status>HTTP/1.1 200 OK</D:status>
-        </D:propstat>
-      </D:response>"#;
-
-        let response = from_str::<ListOpResponse>(xml).unwrap();
-        assert_eq!(response.href, "/test_file");
-        assert_eq!(
-            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");
-    }
-
-    #[test]
-    fn test_with_multiple_items_simple() {
-        let xml = r#"<D:multistatus xmlns:D="DAV:">
-        <D:response>
-        <D:href>/</D:href>
-        <D:propstat>
-            <D:prop>
-                <D:displayname>/</D:displayname>
-                <D:getlastmodified>Tue, 01 May 2022 06:39:47 
GMT</D:getlastmodified>
-                <D:resourcetype><D:collection/></D:resourcetype>
-                <D:lockdiscovery/>
-                <D:supportedlock>
-                    <D:lockentry>
-                        <D:lockscope><D:exclusive/></D:lockscope>
-                        <D:locktype><D:write/></D:locktype>
-                    </D:lockentry>
-                </D:supportedlock>
-            </D:prop>
-            <D:status>HTTP/1.1 200 OK</D:status>
-        </D:propstat>
-    </D:response>
-    <D:response>
-            <D:href>/</D:href>
-            <D:propstat>
-                <D:prop>
-                    <D:displayname>/</D:displayname>
-                    <D:getlastmodified>Tue, 01 May 2022 06:39:47 
GMT</D:getlastmodified>
-                    <D:resourcetype><D:collection/></D:resourcetype>
-                    <D:lockdiscovery/>
-                    <D:supportedlock>
-                        <D:lockentry>
-                            <D:lockscope><D:exclusive/></D:lockscope>
-                            <D:locktype><D:write/></D:locktype>
-                        </D:lockentry>
-                    </D:supportedlock>
-                </D:prop>
-                <D:status>HTTP/1.1 200 OK</D:status>
-            </D:propstat>
-        </D:response>
-        </D:multistatus>"#;
-
-        let multistatus = from_str::<Multistatus>(xml).unwrap();
-
-        let response = multistatus.response.unwrap();
-        assert_eq!(response.len(), 2);
-        assert_eq!(response[0].href, "/");
-        assert_eq!(
-            response[0].propstat.prop.getlastmodified,
-            "Tue, 01 May 2022 06:39:47 GMT"
-        );
-    }
-
-    #[test]
-    fn test_with_multiple_items_mixed() {
-        let xml = r#"<?xml version="1.0" encoding="utf-8"?>
-        <D:multistatus xmlns:D="DAV:">
-          <D:response>
-            <D:href>/</D:href>
-            <D:propstat>
-              <D:prop>
-                <D:displayname>/</D:displayname>
-                <D:getlastmodified>Tue, 07 May 2022 06:39:47 
GMT</D:getlastmodified>
-                <D:resourcetype>
-                  <D:collection />
-                </D:resourcetype>
-                <D:lockdiscovery />
-                <D:supportedlock>
-                  <D:lockentry>
-                    <D:lockscope>
-                      <D:exclusive />
-                    </D:lockscope>
-                    <D:locktype>
-                      <D:write />
-                    </D:locktype>
-                  </D:lockentry>
-                </D:supportedlock>
-              </D:prop>
-              <D:status>HTTP/1.1 200 OK</D:status>
-            </D:propstat>
-          </D:response>
-          <D:response>
-            <D:href>/testdir/</D:href>
-            <D:propstat>
-              <D:prop>
-                <D:displayname>testdir</D:displayname>
-                <D:getlastmodified>Tue, 07 May 2022 06:40:10 
GMT</D:getlastmodified>
-                <D:resourcetype>
-                  <D:collection />
-                </D:resourcetype>
-                <D:lockdiscovery />
-                <D:supportedlock>
-                  <D:lockentry>
-                    <D:lockscope>
-                      <D:exclusive />
-                    </D:lockscope>
-                    <D:locktype>
-                      <D:write />
-                    </D:locktype>
-                  </D:lockentry>
-                </D:supportedlock>
-              </D:prop>
-              <D:status>HTTP/1.1 200 OK</D:status>
-            </D:propstat>
-          </D:response>
-          <D:response>
-            <D:href>/test_file</D:href>
-            <D:propstat>
-              <D:prop>
-                <D:displayname>test_file</D:displayname>
-                <D:getcontentlength>1</D:getcontentlength>
-                <D:getlastmodified>Tue, 07 May 2022 05:52:22 
GMT</D:getlastmodified>
-                <D:resourcetype></D:resourcetype>
-                <D:lockdiscovery />
-                <D:supportedlock>
-                  <D:lockentry>
-                    <D:lockscope>
-                      <D:exclusive />
-                    </D:lockscope>
-                    <D:locktype>
-                      <D:write />
-                    </D:locktype>
-                  </D:lockentry>
-                </D:supportedlock>
-              </D:prop>
-              <D:status>HTTP/1.1 200 OK</D:status>
-            </D:propstat>
-          </D:response>
-        </D:multistatus>"#;
-
-        let multistatus = from_str::<Multistatus>(xml).unwrap();
-
-        let response = multistatus.response.unwrap();
-        assert_eq!(response.len(), 3);
-        let first_response = &response[0];
-        assert_eq!(first_response.href, "/");
-        assert_eq!(
-            first_response.propstat.prop.getlastmodified,
-            "Tue, 07 May 2022 06:39:47 GMT"
-        );
-
-        let second_response = &response[1];
-        assert_eq!(second_response.href, "/testdir/");
-        assert_eq!(
-            second_response.propstat.prop.getlastmodified,
-            "Tue, 07 May 2022 06:40:10 GMT"
-        );
-
-        let third_response = &response[2];
-        assert_eq!(third_response.href, "/test_file");
-        assert_eq!(
-            third_response.propstat.prop.getlastmodified,
-            "Tue, 07 May 2022 05:52:22 GMT"
-        );
-    }
-
-    #[test]
-    fn test_with_multiple_items_mixed_nginx() {
-        let xml = r#"<?xml version="1.0" encoding="utf-8"?>
-      <D:multistatus xmlns:D="DAV:">
-        <D:response>
-          <D:href>/</D:href>
-          <D:propstat>
-            <D:prop>
-              <D:getlastmodified>Fri, 17 Feb 2023 03:37:22 
GMT</D:getlastmodified>
-              <D:resourcetype>
-                <D:collection />
-              </D:resourcetype>
-            </D:prop>
-            <D:status>HTTP/1.1 200 OK</D:status>
-          </D:propstat>
-        </D:response>
-        <D:response>
-          <D:href>/test_file_75</D:href>
-          <D:propstat>
-            <D:prop>
-              <D:getcontentlength>1</D:getcontentlength>
-              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 
GMT</D:getlastmodified>
-              <D:resourcetype></D:resourcetype>
-            </D:prop>
-            <D:status>HTTP/1.1 200 OK</D:status>
-          </D:propstat>
-        </D:response>
-        <D:response>
-          <D:href>/test_file_36</D:href>
-          <D:propstat>
-            <D:prop>
-              <D:getcontentlength>1</D:getcontentlength>
-              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 
GMT</D:getlastmodified>
-              <D:resourcetype></D:resourcetype>
-            </D:prop>
-            <D:status>HTTP/1.1 200 OK</D:status>
-          </D:propstat>
-        </D:response>
-        <D:response>
-          <D:href>/test_file_38</D:href>
-          <D:propstat>
-            <D:prop>
-              <D:getcontentlength>1</D:getcontentlength>
-              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 
GMT</D:getlastmodified>
-              <D:resourcetype></D:resourcetype>
-            </D:prop>
-            <D:status>HTTP/1.1 200 OK</D:status>
-          </D:propstat>
-        </D:response>
-        <D:response>
-          <D:href>/test_file_59</D:href>
-          <D:propstat>
-            <D:prop>
-              <D:getcontentlength>1</D:getcontentlength>
-              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 
GMT</D:getlastmodified>
-              <D:resourcetype></D:resourcetype>
-            </D:prop>
-            <D:status>HTTP/1.1 200 OK</D:status>
-          </D:propstat>
-        </D:response>
-        <D:response>
-          <D:href>/test_file_9</D:href>
-          <D:propstat>
-            <D:prop>
-              <D:getcontentlength>1</D:getcontentlength>
-              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 
GMT</D:getlastmodified>
-              <D:resourcetype></D:resourcetype>
-            </D:prop>
-            <D:status>HTTP/1.1 200 OK</D:status>
-          </D:propstat>
-        </D:response>
-        <D:response>
-          <D:href>/test_file_93</D:href>
-          <D:propstat>
-            <D:prop>
-              <D:getcontentlength>1</D:getcontentlength>
-              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 
GMT</D:getlastmodified>
-              <D:resourcetype></D:resourcetype>
-            </D:prop>
-            <D:status>HTTP/1.1 200 OK</D:status>
-          </D:propstat>
-        </D:response>
-        <D:response>
-          <D:href>/test_file_43</D:href>
-          <D:propstat>
-            <D:prop>
-              <D:getcontentlength>1</D:getcontentlength>
-              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 
GMT</D:getlastmodified>
-              <D:resourcetype></D:resourcetype>
-            </D:prop>
-            <D:status>HTTP/1.1 200 OK</D:status>
-          </D:propstat>
-        </D:response>
-        <D:response>
-          <D:href>/test_file_95</D:href>
-          <D:propstat>
-            <D:prop>
-              <D:getcontentlength>1</D:getcontentlength>
-              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 
GMT</D:getlastmodified>
-              <D:resourcetype></D:resourcetype>
-            </D:prop>
-            <D:status>HTTP/1.1 200 OK</D:status>
-          </D:propstat>
-        </D:response>
-      </D:multistatus>
-      "#;
-
-        let multistatus: Multistatus = from_str(xml).unwrap();
-
-        let response = multistatus.response.unwrap();
-        assert_eq!(response.len(), 9);
-
-        let first_response = &response[0];
-        assert_eq!(first_response.href, "/");
-        assert_eq!(
-            first_response.propstat.prop.getlastmodified,
-            "Fri, 17 Feb 2023 03:37:22 GMT"
-        );
-    }
-}
diff --git a/core/src/services/webdav/mod.rs b/core/src/services/webdav/mod.rs
index f300241371..1e18871a98 100644
--- a/core/src/services/webdav/mod.rs
+++ b/core/src/services/webdav/mod.rs
@@ -19,6 +19,7 @@ mod backend;
 pub use backend::WebdavBuilder as Webdav;
 pub use backend::WebdavConfig;
 
+mod core;
 mod error;
 mod lister;
 mod writer;
diff --git a/core/src/services/webdav/writer.rs 
b/core/src/services/webdav/writer.rs
index 7bf365b00f..72ec6c098e 100644
--- a/core/src/services/webdav/writer.rs
+++ b/core/src/services/webdav/writer.rs
@@ -17,23 +17,24 @@
 
 use async_trait::async_trait;
 use http::StatusCode;
+use std::sync::Arc;
 
-use super::backend::WebdavBackend;
+use super::core::*;
 use super::error::parse_error;
 use crate::raw::oio::WriteBuf;
 use crate::raw::*;
 use crate::*;
 
 pub struct WebdavWriter {
-    backend: WebdavBackend,
+    core: Arc<WebdavCore>,
 
     op: OpWrite,
     path: String,
 }
 
 impl WebdavWriter {
-    pub fn new(backend: WebdavBackend, op: OpWrite, path: String) -> Self {
-        WebdavWriter { backend, op, path }
+    pub fn new(core: Arc<WebdavCore>, op: OpWrite, path: String) -> Self {
+        WebdavWriter { core, op, path }
     }
 }
 
@@ -43,7 +44,7 @@ impl oio::OneShotWrite for WebdavWriter {
         let bs = 
oio::ChunkedBytes::from_vec(bs.vectored_bytes(bs.remaining()));
 
         let resp = self
-            .backend
+            .core
             .webdav_put(
                 &self.path,
                 Some(bs.len() as u64),

Reply via email to