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() {

Reply via email to