This is an automated email from the ASF dual-hosted git repository.
suyanhanx pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git
The following commit(s) were added to refs/heads/main by this push:
new bdd79f05a ci(services/webdav): Setup integration test for nextcloud
(#2631)
bdd79f05a is described below
commit bdd79f05abccea8d200ccba02f288cd04b5a44a0
Author: Xuanwo <[email protected]>
AuthorDate: Fri Jul 14 00:09:07 2023 +0800
ci(services/webdav): Setup integration test for nextcloud (#2631)
* ci(services/webdav): Setup integration test for nextcloud
Signed-off-by: Xuanwo <[email protected]>
* Use cargo test instead
Signed-off-by: Xuanwo <[email protected]>
* Fix write
Signed-off-by: Xuanwo <[email protected]>
* Allow retry for locked
Signed-off-by: Xuanwo <[email protected]>
* Remove not needed wait
Signed-off-by: Xuanwo <[email protected]>
* split job
Signed-off-by: Xuanwo <[email protected]>
* Only enable log for opendal
Signed-off-by: Xuanwo <[email protected]>
* Check parent first
Signed-off-by: Xuanwo <[email protected]>
* Save work
Signed-off-by: Xuanwo <[email protected]>
* Polish API
Signed-off-by: Xuanwo <[email protected]>
* fix typo
Signed-off-by: Xuanwo <[email protected]>
* don't write too much files for test
Signed-off-by: Xuanwo <[email protected]>
* Make sure base_dir has been stripped
Signed-off-by: Xuanwo <[email protected]>
* Remove debug log
Signed-off-by: Xuanwo <[email protected]>
* Make clippy happy
Signed-off-by: Xuanwo <[email protected]>
* use sqlite for test
Signed-off-by: Xuanwo <[email protected]>
* setup with sqlite
Signed-off-by: Xuanwo <[email protected]>
---------
Signed-off-by: Xuanwo <[email protected]>
---
.github/actions/setup/action.yaml | 2 +-
.github/workflows/service_test_webdav.yml | 74 +++-
core/src/raw/oio/read.rs | 6 +-
core/src/raw/path.rs | 1 -
core/src/services/webdav/backend.rs | 122 ++++---
core/src/services/webdav/error.rs | 2 +
core/src/services/webdav/list_response.rs | 500 ---------------------------
core/src/services/webdav/mod.rs | 1 -
core/src/services/webdav/pager.rs | 543 ++++++++++++++++++++++++++++--
core/tests/behavior/list.rs | 4 +-
10 files changed, 631 insertions(+), 624 deletions(-)
diff --git a/.github/actions/setup/action.yaml
b/.github/actions/setup/action.yaml
index 7d1b13f13..257e5c57c 100644
--- a/.github/actions/setup/action.yaml
+++ b/.github/actions/setup/action.yaml
@@ -35,7 +35,7 @@ runs:
# Enable backtraces
echo "RUST_BACKTRACE=1" >> $GITHUB_ENV
# Enable logging
- echo "RUST_LOG=debug" >> $GITHUB_ENV
+ echo "RUST_LOG=opendal=debug" >> $GITHUB_ENV
# Enable sparse index
echo "CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse" >> $GITHUB_ENV
diff --git a/.github/workflows/service_test_webdav.yml
b/.github/workflows/service_test_webdav.yml
index 1d445c65f..9f1be7fa8 100644
--- a/.github/workflows/service_test_webdav.yml
+++ b/.github/workflows/service_test_webdav.yml
@@ -38,11 +38,7 @@ concurrency:
jobs:
nginx:
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os:
- - ubuntu-latest
+ runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust toolchain
@@ -69,11 +65,7 @@ jobs:
OPENDAL_WEBDAV_ENDPOINT: http://127.0.0.1:8080
nginx_with_password:
- runs-on: ${{ matrix.os }}
- strategy:
- matrix:
- os:
- - ubuntu-latest
+ runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Rust toolchain
@@ -92,24 +84,44 @@ jobs:
cp `pwd`/src/services/webdav/fixtures/htpasswd /tmp/htpasswd
nginx -c
`pwd`/src/services/webdav/fixtures/nginx-with-basic-auth.conf
- - name: Test empty password
+ - name: Test with password
shell: bash
working-directory: core
run: cargo nextest run webdav
env:
OPENDAL_WEBDAV_TEST: on
OPENDAL_WEBDAV_ENDPOINT: http://127.0.0.1:8080
- OPENDAL_WEBDAV_USERNAME: foo
+ OPENDAL_WEBDAV_USERNAME: bar
+ OPENDAL_WEBDAV_PASSWORD: bar
- - name: Test with password
+ nginx_with_empty_password:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Rust toolchain
+ uses: ./.github/actions/setup
+ with:
+ need-nextest: true
+
+ - name: Install nginx full for dav_ext modules
+ run: sudo apt install nginx-full
+
+ - name: Start nginx
+ shell: bash
+ working-directory: core
+ run: |
+ mkdir -p /tmp/static
+ cp `pwd`/src/services/webdav/fixtures/htpasswd /tmp/htpasswd
+ nginx -c
`pwd`/src/services/webdav/fixtures/nginx-with-basic-auth.conf
+
+ - name: Test empty password
shell: bash
working-directory: core
run: cargo nextest run webdav
env:
OPENDAL_WEBDAV_TEST: on
OPENDAL_WEBDAV_ENDPOINT: http://127.0.0.1:8080
- OPENDAL_WEBDAV_USERNAME: bar
- OPENDAL_WEBDAV_PASSWORD: bar
+ OPENDAL_WEBDAV_USERNAME: foo
nginx_with_redirect:
runs-on: ubuntu-latest
@@ -143,3 +155,35 @@ jobs:
env:
OPENDAL_WEBDAV_TEST: on
OPENDAL_WEBDAV_ENDPOINT: http://127.0.0.1:8081
+
+ nextcloud:
+ runs-on: ubuntu-latest
+
+ services:
+ nextcloud:
+ image: nextcloud
+ ports:
+ - 8080:80
+ env:
+ SQLITE_DATABASE: nextcloud
+ NEXTCLOUD_ADMIN_USER: admin
+ NEXTCLOUD_ADMIN_PASSWORD: admin
+ options: --health-cmd="curl -f http://localhost" --health-interval=10s
--health-timeout=5s --health-retries=5
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Rust toolchain
+ uses: ./.github/actions/setup
+ with:
+ need-nextest: true
+
+ - name: Test
+ shell: bash
+ working-directory: core
+ run: |
+ cargo test webdav
+ env:
+ OPENDAL_WEBDAV_TEST: on
+ OPENDAL_WEBDAV_ENDPOINT: http://127.0.0.1:8080/remote.php/webdav/
+ OPENDAL_WEBDAV_USERNAME: admin
+ OPENDAL_WEBDAV_PASSWORD: admin
diff --git a/core/src/raw/oio/read.rs b/core/src/raw/oio/read.rs
index 299875818..9b18b31e0 100644
--- a/core/src/raw/oio/read.rs
+++ b/core/src/raw/oio/read.rs
@@ -192,7 +192,7 @@ pub trait ReadExt: Read {
ReadFuture {
reader: self,
buf,
- _pin: PhantomPinned::default(),
+ _pin: PhantomPinned,
}
}
@@ -201,7 +201,7 @@ pub trait ReadExt: Read {
SeekFuture {
reader: self,
pos,
- _pin: PhantomPinned::default(),
+ _pin: PhantomPinned,
}
}
@@ -209,7 +209,7 @@ pub trait ReadExt: Read {
fn next(&mut self) -> NextFuture<'_, Self> {
NextFuture {
reader: self,
- _pin: PhantomPinned::default(),
+ _pin: PhantomPinned,
}
}
}
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 825e2f067..64fa3c73b 100644
--- a/core/src/services/webdav/backend.rs
+++ b/core/src/services/webdav/backend.rs
@@ -16,8 +16,10 @@
// under the License.
use std::collections::HashMap;
+use std::collections::VecDeque;
use std::fmt::Debug;
use std::fmt::Formatter;
+use std::str::FromStr;
use async_trait::async_trait;
use bytes::Buf;
@@ -29,7 +31,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::*;
@@ -202,6 +204,15 @@ impl Builder for WebdavBuilder {
}
};
+ let uri = http::Uri::from_str(endpoint).map_err(|err| {
+ Error::new(ErrorKind::ConfigInvalid, "endpoint is invalid")
+ .set_source(err)
+ .with_context("service", Scheme::Webdav)
+ })?;
+ // Some webdav server may have base dir like `/remote.php/webdav/`
+ // returned in the `href`.
+ let base_dir = uri.path().trim_end_matches('/');
+
let root = normalize_root(&self.root.take().unwrap_or_default());
debug!("backend use root {}", root);
@@ -228,6 +239,7 @@ impl Builder for WebdavBuilder {
debug!("backend build finished: {:?}", &self);
Ok(WebdavBackend {
endpoint: endpoint.to_string(),
+ base_dir: base_dir.to_string(),
authorization: auth,
root,
client,
@@ -239,6 +251,7 @@ impl Builder for WebdavBuilder {
#[derive(Clone)]
pub struct WebdavBackend {
endpoint: String,
+ base_dir: String,
root: String,
client: HttpClient,
@@ -262,7 +275,7 @@ impl Accessor for WebdavBackend {
type Writer = WebdavWriter;
type BlockingWriter = ();
type Appender = ();
- type Pager = WebdavPager;
+ type Pager = Option<WebdavPager>;
type BlockingPager = ();
fn info(&self) -> AccessorInfo {
@@ -297,9 +310,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);
- self.create_internal(&abs_path).await
+ Ok(RpCreateDir::default())
}
async fn read(&self, path: &str, args: OpRead) -> Result<(RpRead,
Self::Reader)> {
@@ -322,6 +335,8 @@ impl Accessor for WebdavBackend {
));
}
+ self.ensure_parent_path(path).await?;
+
let p = build_abs_path(&self.root, path);
Ok((RpWrite::default(), WebdavWriter::new(self.clone(), args, p)))
@@ -428,19 +443,10 @@ impl Accessor for WebdavBackend {
Ok((
RpList::default(),
- WebdavPager::new(&self.root, path, result),
+ Some(WebdavPager::new(&self.base_dir, &self.root, path,
result)),
))
}
- StatusCode::NOT_FOUND if path.ends_with('/') => Ok((
- RpList::default(),
- WebdavPager::new(
- &self.root,
- path,
- Multistatus {
- response: Vec::new(),
- },
- ),
- )),
+ StatusCode::NOT_FOUND if path.ends_with('/') =>
Ok((RpList::default(), None)),
_ => Err(parse_error(resp).await?),
}
}
@@ -506,29 +512,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
}
@@ -640,53 +636,51 @@ impl WebdavBackend {
self.client.send(req).await
}
- async fn create_internal(&self, abs_path: &str) -> Result<RpCreateDir> {
- let resp = if abs_path.ends_with('/') {
- self.webdav_mkcol(abs_path, None, None, AsyncBody::Empty)
- .await?
- } else {
- self.webdav_put(abs_path, Some(0), None, None, AsyncBody::Empty)
- .await?
- };
+ async fn create_dir_internal(&self, path: &str) -> Result<()> {
+ let resp = self.webdav_mkcol(path).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 => {
+ // 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(RpCreateDir::default())
+ Ok(())
}
_ => Err(parse_error(resp).await?),
}
}
- async fn ensure_parent_path(&self, path: &str) -> Result<()> {
- if path == "/" {
- return Ok(());
- }
+ async fn ensure_parent_path(&self, mut path: &str) -> Result<()> {
+ let mut dirs = VecDeque::default();
- // create dir recursively, split path by `/` and create each dir
except the last one
- let abs_path = build_abs_path(&self.root, path);
- let abs_path = abs_path.as_str();
- let mut parts: Vec<&str> = abs_path.split('/').filter(|x|
!x.is_empty()).collect();
- if !parts.is_empty() {
- parts.pop();
- }
+ while path != "/" {
+ // check path first.
+ let parent = get_parent(path);
- let mut sub_path = String::new();
- for sub_part in parts {
- let sub_path_with_slash = sub_part.to_owned() + "/";
- sub_path.push_str(&sub_path_with_slash);
- self.create_internal(&sub_path).await?;
+ let mut header_map = HeaderMap::new();
+ // not include children
+ header_map.insert("Depth", "0".parse().unwrap());
+ header_map.insert(header::ACCEPT,
"application/xml".parse().unwrap());
+
+ let resp = self.webdav_propfind(parent, Some(header_map)).await?;
+ match resp.status() {
+ StatusCode::OK | StatusCode::MULTI_STATUS => break,
+ StatusCode::NOT_FOUND => {
+ dirs.push_front(parent);
+ path = parent
+ }
+ _ => return Err(parse_error(resp).await?),
+ }
}
+ for dir in dirs {
+ self.create_dir_internal(dir).await?;
+ }
Ok(())
}
}
diff --git a/core/src/services/webdav/error.rs
b/core/src/services/webdav/error.rs
index 6fe6b57d4..c3110b2bc 100644
--- a/core/src/services/webdav/error.rs
+++ b/core/src/services/webdav/error.rs
@@ -31,6 +31,8 @@ pub async fn parse_error(resp: Response<IncomingAsyncBody>)
-> Result<Error> {
let (kind, retryable) = match parts.status {
StatusCode::NOT_FOUND => (ErrorKind::NotFound, false),
StatusCode::FORBIDDEN => (ErrorKind::PermissionDenied, false),
+ // Allowing retry for resource locked.
+ StatusCode::LOCKED => (ErrorKind::Unexpected, true),
StatusCode::INTERNAL_SERVER_ERROR
| StatusCode::BAD_GATEWAY
| StatusCode::SERVICE_UNAVAILABLE
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..03195c069 100644
--- a/core/src/services/webdav/pager.rs
+++ b/core/src/services/webdav/pager.rs
@@ -18,23 +18,21 @@
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 {
+ base_dir: String,
root: String,
path: String,
multistates: Multistatus,
}
impl WebdavPager {
- pub fn new(root: &str, path: &str, multistates: Multistatus) -> Self {
+ pub fn new(base_dir: &str, root: &str, path: &str, multistates:
Multistatus) -> Self {
Self {
+ base_dir: base_dir.to_string(),
root: root.into(),
path: path.into(),
multistates,
@@ -50,34 +48,505 @@ impl oio::Page for WebdavPager {
};
let oes = mem::take(&mut self.multistates.response);
- let oes = oes
- .into_iter()
- .filter_map(|de| {
- let path = de.href;
-
- // Ignore the root path itself.
- if self.root == path {
- return None;
- }
-
- let normalized_path = build_rel_path(&self.root, &path);
- if normalized_path == self.path {
- // WebDav server may return the current path as an entry.
- return None;
- }
-
- let entry = if de.propstat.prop.resourcetype.value
- == Some(super::list_response::ResourceType::Collection)
- {
- oio::Entry::new(&normalized_path,
Metadata::new(EntryMode::DIR))
- } else {
- oio::Entry::new(&normalized_path,
Metadata::new(EntryMode::FILE))
- };
-
- Some(entry)
- })
- .collect();
-
- Ok(Some(oes))
+ let mut entries = Vec::with_capacity(oes.len());
+
+ for res in oes {
+ let path = res
+ .href
+ .strip_prefix(&self.base_dir)
+ .unwrap_or(res.href.as_str());
+
+ // Ignore the root path itself.
+ if self.root == path {
+ continue;
+ }
+
+ let normalized_path = build_rel_path(&self.root, path);
+ if normalized_path == self.path {
+ // WebDav server may return the current path as an entry.
+ continue;
+ }
+
+ let meta = res.parse_into_metadata()?;
+ entries.push(oio::Entry::new(&normalized_path, meta))
+ }
+
+ Ok(Some(entries))
+ }
+}
+
+#[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: EntryMode = 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/tests/behavior/list.rs b/core/tests/behavior/list.rs
index eed9021f5..bf8aa8339 100644
--- a/core/tests/behavior/list.rs
+++ b/core/tests/behavior/list.rs
@@ -88,7 +88,7 @@ pub async fn test_list_dir(op: Operator) -> Result<()> {
pub async fn test_list_rich_dir(op: Operator) -> Result<()> {
op.create_dir("test_list_rich_dir/").await?;
- let mut expected: Vec<String> = (0..=1000)
+ let mut expected: Vec<String> = (0..=100)
.map(|num| format!("test_list_rich_dir/file-{num}"))
.collect();
@@ -105,7 +105,7 @@ pub async fn test_list_rich_dir(op: Operator) -> Result<()>
{
.collect::<Vec<_>>()
.await;
- let mut objects = op.list("test_list_rich_dir/").await?;
+ let mut objects = op.with_limit(10).list("test_list_rich_dir/").await?;
let mut actual = vec![];
while let Some(o) = objects.try_next().await? {
let path = o.path().to_string();