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 160fbae2c fix(webdav): support multiple propstat entries (#7823)
160fbae2c is described below
commit 160fbae2cd46643aef140717c144f80611bca882
Author: ZeonX <[email protected]>
AuthorDate: Thu Jun 25 18:42:21 2026 +0800
fix(webdav): support multiple propstat entries (#7823)
---
core/services/webdav/src/core.rs | 141 +++++++++++++++++++++++++++++--------
core/services/webdav/src/lister.rs | 2 +-
2 files changed, 113 insertions(+), 30 deletions(-)
diff --git a/core/services/webdav/src/core.rs b/core/services/webdav/src/core.rs
index 75d6dc0f6..3f07c86c7 100644
--- a/core/services/webdav/src/core.rs
+++ b/core/services/webdav/src/core.rs
@@ -155,7 +155,7 @@ impl WebdavCore {
)
})?;
- let mut metadata = parse_propstat(&propfind_resp.propstat)?;
+ let mut metadata = parse_propstats(&propfind_resp.propstat)?;
// Parse user metadata from the raw XML response using configured
namespace
let user_metadata = parse_user_metadata_from_xml(&xml_str,
&self.user_metadata_uri);
@@ -843,12 +843,36 @@ pub fn parse_propstat(propstat: &Propstat) ->
Result<Metadata> {
}
// https://www.rfc-editor.org/rfc/rfc4918#section-14.18
+ let Some(getlastmodified) = getlastmodified else {
+ return Err(Error::new(
+ ErrorKind::Unexpected,
+ "propfind response missing getlastmodified",
+ ));
+ };
m.set_last_modified(Timestamp::parse_rfc2822(getlastmodified)?);
// the storage services have returned all the properties
Ok(m)
}
+pub fn parse_propstats(propstats: &[Propstat]) -> Result<Metadata> {
+ let mut last_error = None;
+
+ for propstat in propstats {
+ match parse_propstat(propstat) {
+ Ok(metadata) => return Ok(metadata),
+ Err(err) => last_error = Some(err),
+ }
+ }
+
+ Err(last_error.unwrap_or_else(|| {
+ Error::new(
+ ErrorKind::Unexpected,
+ "propfind response does not contain propstat",
+ )
+ }))
+}
+
#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
#[serde(default)]
pub struct Multistatus {
@@ -858,7 +882,8 @@ pub struct Multistatus {
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct PropfindResponse {
pub href: String,
- pub propstat: Propstat,
+ #[serde(default)]
+ pub propstat: Vec<Propstat>,
}
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
@@ -869,14 +894,15 @@ pub struct Propstat {
#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct Prop {
- pub getlastmodified: String,
+ pub getlastmodified: Option<String>,
pub getetag: Option<String>,
pub getcontentlength: Option<String>,
pub getcontenttype: Option<String>,
+ #[serde(default)]
pub resourcetype: ResourceTypeContainer,
}
-#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
+#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct ResourceTypeContainer {
#[serde(rename = "$value")]
pub value: Option<ResourceType>,
@@ -898,7 +924,7 @@ mod tests {
Propstat {
status: status.to_string(),
prop: Prop {
- getlastmodified: "Tue, 01 May 2022 06:39:47 GMT".to_string(),
+ getlastmodified: Some("Sun, 01 May 2022 06:39:47
GMT".to_string()),
getetag: None,
getcontentlength: getcontentlength.map(str::to_string),
getcontenttype: None,
@@ -927,12 +953,12 @@ mod tests {
let propstat = from_str::<Propstat>(xml).unwrap();
assert_eq!(
- propstat.prop.getlastmodified,
- "Tue, 01 May 2022 06:39:47 GMT"
+ propstat.prop.getlastmodified.as_deref(),
+ Some("Tue, 01 May 2022 06:39:47 GMT")
);
assert_eq!(
- propstat.prop.resourcetype.value.unwrap(),
- ResourceType::Collection
+ propstat.prop.resourcetype.value,
+ Some(ResourceType::Collection)
);
assert_eq!(propstat.status, "HTTP/1.1 200 OK");
@@ -977,16 +1003,17 @@ mod tests {
let response = from_str::<PropfindResponse>(xml).unwrap();
assert_eq!(response.href, "/");
+ assert_eq!(response.propstat.len(), 1);
assert_eq!(
- response.propstat.prop.getlastmodified,
- "Tue, 01 May 2022 06:39:47 GMT"
+ response.propstat[0].prop.getlastmodified.as_deref(),
+ Some("Tue, 01 May 2022 06:39:47 GMT")
);
assert_eq!(
- response.propstat.prop.resourcetype.value.unwrap(),
- ResourceType::Collection
+ response.propstat[0].prop.resourcetype.value,
+ Some(ResourceType::Collection)
);
- assert_eq!(response.propstat.status, "HTTP/1.1 200 OK");
+ assert_eq!(response.propstat[0].status, "HTTP/1.1 200 OK");
}
#[test]
@@ -1018,12 +1045,68 @@ mod tests {
let response = from_str::<PropfindResponse>(xml).unwrap();
assert_eq!(response.href, "/test_file");
assert_eq!(
- response.propstat.prop.getlastmodified,
- "Tue, 07 May 2022 05:52:22 GMT"
+ response.propstat[0].prop.getlastmodified.as_deref(),
+ Some("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");
+ assert_eq!(
+ response.propstat[0].prop.getcontentlength.as_deref(),
+ Some("1")
+ );
+ assert_eq!(response.propstat[0].prop.resourcetype.value, None);
+ assert_eq!(response.propstat[0].status, "HTTP/1.1 200 OK");
+ }
+
+ #[test]
+ fn test_response_with_multiple_propstat() {
+ let xml = r#"<D:response>
+ <D:href>/webdav/example/</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:resourcetype><D:collection/></D:resourcetype>
+ <D:displayname>example-folder</D:displayname>
+ <D:getlastmodified>Thu, 25 Jun 2026 06:23:30
GMT</D:getlastmodified>
+ <D:getetag>"example-etag"</D:getetag>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ <D:propstat>
+ <D:prop>
+ <D:creationdate/>
+ <D:getcontentlength/>
+ <D:getcontenttype/>
+ <D:getcontentlanguage/>
+ <D:source/>
+ </D:prop>
+ <D:status>HTTP/1.1 404 Not Found</D:status>
+ </D:propstat>
+ </D:response>"#;
+
+ let response = from_str::<PropfindResponse>(xml).unwrap();
+ assert_eq!(response.propstat.len(), 2);
+
+ let meta = parse_propstats(&response.propstat).unwrap();
+ assert!(meta.is_dir());
+ assert_eq!(meta.etag(), Some("\"example-etag\""));
+ }
+
+ #[test]
+ fn test_parse_propstats_skips_failed_propstat() {
+ let propstats = vec![
+ Propstat {
+ status: "HTTP/1.1 404 Not Found".to_string(),
+ prop: Prop {
+ getlastmodified: None,
+ getetag: None,
+ getcontentlength: None,
+ getcontenttype: None,
+ resourcetype: ResourceTypeContainer::default(),
+ },
+ },
+ new_propstat("HTTP/1.1 200 OK", None),
+ ];
+
+ let meta = parse_propstats(&propstats).unwrap();
+ assert!(meta.is_file());
}
#[test]
@@ -1073,8 +1156,8 @@ mod tests {
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"
+ response[0].propstat[0].prop.getlastmodified.as_deref(),
+ Some("Tue, 01 May 2022 06:39:47 GMT")
);
}
@@ -1162,22 +1245,22 @@ mod tests {
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"
+ first_response.propstat[0].prop.getlastmodified.as_deref(),
+ Some("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"
+ second_response.propstat[0].prop.getlastmodified.as_deref(),
+ Some("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"
+ third_response.propstat[0].prop.getlastmodified.as_deref(),
+ Some("Tue, 07 May 2022 05:52:22 GMT")
);
}
@@ -1296,8 +1379,8 @@ mod tests {
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"
+ first_response.propstat[0].prop.getlastmodified.as_deref(),
+ Some("Fri, 17 Feb 2023 03:37:22 GMT")
);
}
diff --git a/core/services/webdav/src/lister.rs
b/core/services/webdav/src/lister.rs
index 74cda5582..7ef339f17 100644
--- a/core/services/webdav/src/lister.rs
+++ b/core/services/webdav/src/lister.rs
@@ -78,7 +78,7 @@ impl oio::PageList for WebdavLister {
.unwrap_or(&res.href)
.to_string();
- let meta = parse_propstat(&res.propstat)?;
+ let meta = parse_propstats(&res.propstat)?;
// Append `/` to path if it's a dir
if !path.ends_with('/') && meta.is_dir() {