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/07/13 14:42:21 UTC
[incubator-opendal] 01/02: Save work
This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch add-nextcloud-test-for-webdav
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git
commit 29c07d796b6ebc90223601956675e2a03add5d65
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Thu Jul 13 21:58:50 2023 +0800
Save work
Signed-off-by: Xuanwo <gi...@xuanwo.io>
---
core/src/raw/path.rs | 1 -
core/src/services/webdav/backend.rs | 79 ++---
core/src/services/webdav/list_response.rs | 500 ------------------------------
core/src/services/webdav/mod.rs | 1 -
core/src/services/webdav/pager.rs | 488 ++++++++++++++++++++++++++++-
5 files changed, 508 insertions(+), 561 deletions(-)
diff --git a/core/src/raw/path.rs b/core/src/raw/path.rs
index 652938806..cdc88d032 100644
--- a/core/src/raw/path.rs
+++ b/core/src/raw/path.rs
@@ -176,7 +176,6 @@ pub fn get_basename(path: &str) -> &str {
}
/// Get parent from path.
-#[allow(dead_code)]
pub fn get_parent(path: &str) -> &str {
if path == "/" {
return "/";
diff --git a/core/src/services/webdav/backend.rs b/core/src/services/webdav/backend.rs
index 9288b25c8..3a1ff789b 100644
--- a/core/src/services/webdav/backend.rs
+++ b/core/src/services/webdav/backend.rs
@@ -30,7 +30,7 @@ use http::StatusCode;
use log::debug;
use super::error::parse_error;
-use super::list_response::Multistatus;
+use super::pager::Multistatus;
use super::pager::WebdavPager;
use super::writer::WebdavWriter;
use crate::raw::*;
@@ -298,29 +298,9 @@ 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?;
- let abs_path = build_abs_path(&self.root, path);
-
- let resp = self
- .webdav_mkcol(&abs_path, None, None, AsyncBody::Empty)
- .await?;
-
- let status = resp.status();
-
- match status {
- StatusCode::CREATED
- | StatusCode::OK
- // `File exists` will return `Method Not Allowed`
- | StatusCode::METHOD_NOT_ALLOWED
- // create existing dir will return conflict
- | StatusCode::CONFLICT
- // create existing file will return no_content
- | StatusCode::NO_CONTENT => {
- resp.into_body().consume().await?;
- Ok(RpCreateDir::default())
- }
- _ => Err(parse_error(resp).await?),
- }
+ Ok(RpCreateDir::default())
}
async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead, Self::Reader)> {
@@ -529,29 +509,19 @@ impl WebdavBackend {
self.client.send(req).await
}
- async fn webdav_mkcol(
- &self,
- abs_path: &str,
- content_type: Option<&str>,
- content_disposition: Option<&str>,
- body: AsyncBody,
- ) -> Result<Response<IncomingAsyncBody>> {
- let url = format!("{}/{}", self.endpoint, percent_encode_path(abs_path));
+ async fn webdav_mkcol(&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::builder().method("MKCOL").uri(&url);
if let Some(auth) = &self.authorization {
req = req.header(header::AUTHORIZATION, auth);
}
- if let Some(mime) = content_type {
- req = req.header(header::CONTENT_TYPE, mime)
- }
-
- if let Some(cd) = content_disposition {
- req = req.header(header::CONTENT_DISPOSITION, cd)
- }
-
- let req = req.body(body).map_err(new_request_build_error)?;
+ let req = req
+ .body(AsyncBody::Empty)
+ .map_err(new_request_build_error)?;
self.client.send(req).await
}
@@ -664,11 +634,7 @@ impl WebdavBackend {
}
async fn create_dir_internal(&self, path: &str) -> Result<()> {
- let abs_path = build_abs_path(&self.root, path);
-
- let resp = self
- .webdav_mkcol(&abs_path, None, None, AsyncBody::Empty)
- .await?;
+ let resp = self.webdav_mkcol(path).await?;
let status = resp.status();
@@ -680,7 +646,9 @@ impl WebdavBackend {
// create existing dir will return conflict
| StatusCode::CONFLICT
// create existing file will return no_content
- | StatusCode::NO_CONTENT => {
+ | StatusCode::NO_CONTENT
+ // ALlow mutiple status
+ | StatusCode::MULTI_STATUS=> {
resp.into_body().consume().await?;
Ok(())
}
@@ -694,13 +662,24 @@ impl WebdavBackend {
while path != "/" {
// check path first.
let parent = get_parent(path);
- match self.stat(path, OpStat::default()).await {
- Ok(_) => break,
- Err(err) if err.kind() != ErrorKind::NotFound => return Err(err),
- _ => {
+ log::info!("parent: {}", parent);
+ if parent == "/" {
+ break;
+ }
+
+ 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?;
+ match resp.status() {
+ StatusCode::OK | StatusCode::MULTI_STATUS => break,
+ StatusCode::NOT_FOUND => {
dirs.push_front(path);
path = parent
}
+ _ => return Err(parse_error(resp).await?),
}
}
diff --git a/core/src/services/webdav/list_response.rs b/core/src/services/webdav/list_response.rs
deleted file mode 100644
index e8b5c7000..000000000
--- a/core/src/services/webdav/list_response.rs
+++ /dev/null
@@ -1,500 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use serde::Deserialize;
-
-use crate::raw::parse_datetime_from_rfc2822;
-use crate::EntryMode;
-use crate::Error;
-use crate::ErrorKind;
-use crate::Metadata;
-use crate::Result;
-
-#[derive(Deserialize, Debug, PartialEq, Eq)]
-pub struct Multistatus {
- pub response: Vec<ListOpResponse>,
-}
-
-#[derive(Deserialize, Debug, PartialEq, Eq)]
-pub struct ListOpResponse {
- pub href: String,
- 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(parse_datetime_from_rfc2822(getlastmodified)?);
- Ok(m)
- }
-}
-
-#[derive(Deserialize, Debug, PartialEq, Eq)]
-pub struct Propstat {
- pub prop: Prop,
- pub status: String,
-}
-
-#[derive(Deserialize, Debug, PartialEq, Eq)]
-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)]
-pub struct ResourceTypeContainer {
- #[serde(rename = "$value")]
- pub value: Option<ResourceType>,
-}
-
-#[derive(Deserialize, Debug, PartialEq, Eq)]
-#[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();
- assert_eq!(multistatus.response.len(), 2);
- assert_eq!(multistatus.response[0].href, "/");
- assert_eq!(
- multistatus.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();
-
- assert_eq!(multistatus.response.len(), 3);
- let first_response = &multistatus.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 = &multistatus.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 = &multistatus.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();
-
- assert_eq!(multistatus.response.len(), 9);
-
- let first_response = &multistatus.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 63d00511b..2b46724bb 100644
--- a/core/src/services/webdav/mod.rs
+++ b/core/src/services/webdav/mod.rs
@@ -19,6 +19,5 @@ mod backend;
pub use backend::WebdavBuilder as Webdav;
mod error;
-mod list_response;
mod pager;
mod writer;
diff --git a/core/src/services/webdav/pager.rs b/core/src/services/webdav/pager.rs
index 3cd8ebb81..4bbf1e135 100644
--- a/core/src/services/webdav/pager.rs
+++ b/core/src/services/webdav/pager.rs
@@ -18,14 +18,10 @@
use std::mem;
use async_trait::async_trait;
+use serde::Deserialize;
-use super::list_response::Multistatus;
-use crate::raw::build_rel_path;
-use crate::raw::oio;
-use crate::EntryMode;
-use crate::Metadata;
-use crate::Result;
-
+use crate::raw::*;
+use crate::*;
pub struct WebdavPager {
root: String,
path: String,
@@ -66,8 +62,7 @@ impl oio::Page for WebdavPager {
return None;
}
- let entry = if de.propstat.prop.resourcetype.value
- == Some(super::list_response::ResourceType::Collection)
+ let entry = if de.propstat.prop.resourcetype.value == Some(ResourceType::Collection)
{
oio::Entry::new(&normalized_path, Metadata::new(EntryMode::DIR))
} else {
@@ -81,3 +76,478 @@ impl oio::Page for WebdavPager {
Ok(Some(oes))
}
}
+
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+pub struct Multistatus {
+ pub response: Vec<ListOpResponse>,
+}
+
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+pub struct ListOpResponse {
+ pub href: String,
+ 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(parse_datetime_from_rfc2822(getlastmodified)?);
+ Ok(m)
+ }
+}
+
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+pub struct Propstat {
+ pub prop: Prop,
+ pub status: String,
+}
+
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+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)]
+pub struct ResourceTypeContainer {
+ #[serde(rename = "$value")]
+ pub value: Option<ResourceType>,
+}
+
+#[derive(Deserialize, Debug, PartialEq, Eq)]
+#[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();
+ assert_eq!(multistatus.response.len(), 2);
+ assert_eq!(multistatus.response[0].href, "/");
+ assert_eq!(
+ multistatus.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();
+
+ assert_eq!(multistatus.response.len(), 3);
+ let first_response = &multistatus.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 = &multistatus.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 = &multistatus.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();
+
+ assert_eq!(multistatus.response.len(), 9);
+
+ let first_response = &multistatus.response[0];
+ assert_eq!(first_response.href, "/");
+ assert_eq!(
+ first_response.propstat.prop.getlastmodified,
+ "Fri, 17 Feb 2023 03:37:22 GMT"
+ );
+ }
+}