opendal/services/webdav/
core.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use bytes::Bytes;
19use http::header;
20use http::Request;
21use http::Response;
22use http::StatusCode;
23use serde::Deserialize;
24use std::collections::VecDeque;
25use std::fmt;
26use std::fmt::Debug;
27use std::fmt::Formatter;
28use std::sync::Arc;
29
30use super::error::parse_error;
31use crate::raw::*;
32use crate::*;
33
34/// The request to query all properties of a file or directory.
35///
36/// rfc4918 9.1: retrieve all properties define in specification
37static PROPFIND_REQUEST: &str = r#"<?xml version="1.0" encoding="utf-8" ?><D:propfind xmlns:D="DAV:"><D:allprop/></D:propfind>"#;
38
39/// The header to specify the depth of the query.
40///
41/// Valid values are `0`, `1`, `infinity`.
42///
43/// - `0`: only to the resource itself.
44/// - `1`: to the resource and its internal members only.
45/// - `infinity`: to the resource and all its members.
46///
47/// reference: [RFC4918: 10.2. Depth Header](https://datatracker.ietf.org/doc/html/rfc4918#section-10.2)
48static HEADER_DEPTH: &str = "Depth";
49/// The header to specify the destination of the query.
50///
51/// The Destination request header specifies the URI that identifies a
52/// destination resource for methods such as COPY and MOVE, which take
53/// two URIs as parameters.
54///
55/// reference: [RFC4918: 10.3.  Destination Header](https://datatracker.ietf.org/doc/html/rfc4918#section-10.3)
56static HEADER_DESTINATION: &str = "Destination";
57/// The header to specify the overwrite behavior of the query
58///
59/// The Overwrite request header specifies whether the server should
60/// overwrite a resource mapped to the destination URL during a COPY or
61/// MOVE.
62///
63/// Valid values are `T` and `F`.
64///
65/// A value of "F" states that the server must not perform the COPY or MOVE operation
66/// if the destination URL does map to a resource.
67///
68/// reference: [RFC4918: 10.6.  Overwrite Header](https://datatracker.ietf.org/doc/html/rfc4918#section-10.6)
69static HEADER_OVERWRITE: &str = "Overwrite";
70
71pub struct WebdavCore {
72    pub info: Arc<AccessorInfo>,
73    pub endpoint: String,
74    pub server_path: String,
75    pub root: String,
76    pub authorization: Option<String>,
77}
78
79impl Debug for WebdavCore {
80    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
81        f.debug_struct("WebdavCore")
82            .field("endpoint", &self.endpoint)
83            .field("root", &self.root)
84            .finish_non_exhaustive()
85    }
86}
87
88impl WebdavCore {
89    pub async fn webdav_stat(&self, path: &str) -> Result<Metadata> {
90        let path = build_rooted_abs_path(&self.root, path);
91        self.webdav_stat_rooted_abs_path(&path).await
92    }
93
94    /// Input path must be `rooted_abs_path`.
95    async fn webdav_stat_rooted_abs_path(&self, rooted_abs_path: &str) -> Result<Metadata> {
96        let url = format!("{}{}", self.endpoint, percent_encode_path(rooted_abs_path));
97        let mut req = Request::builder().method("PROPFIND").uri(url);
98
99        req = req.header(header::CONTENT_TYPE, "application/xml");
100        req = req.header(header::CONTENT_LENGTH, PROPFIND_REQUEST.len());
101        if let Some(auth) = &self.authorization {
102            req = req.header(header::AUTHORIZATION, auth);
103        }
104
105        // Only stat the resource itself.
106        req = req.header(HEADER_DEPTH, "0");
107
108        let req = req
109            .extension(Operation::Stat)
110            .body(Buffer::from(Bytes::from(PROPFIND_REQUEST)))
111            .map_err(new_request_build_error)?;
112
113        let resp = self.info.http_client().send(req).await?;
114        if !resp.status().is_success() {
115            return Err(parse_error(resp));
116        }
117
118        let bs = resp.into_body();
119
120        let result: Multistatus = deserialize_multistatus(&bs.to_bytes())?;
121        let propfind_resp = result.response.first().ok_or_else(|| {
122            Error::new(
123                ErrorKind::NotFound,
124                "propfind response is empty, the resource is not exist",
125            )
126        })?;
127
128        let metadata = parse_propstat(&propfind_resp.propstat)?;
129        Ok(metadata)
130    }
131
132    pub async fn webdav_get(
133        &self,
134        path: &str,
135        range: BytesRange,
136        _: &OpRead,
137    ) -> Result<Response<HttpBody>> {
138        let path = build_rooted_abs_path(&self.root, path);
139        let url: String = format!("{}{}", self.endpoint, percent_encode_path(&path));
140
141        let mut req = Request::get(&url);
142
143        if let Some(auth) = &self.authorization {
144            req = req.header(header::AUTHORIZATION, auth.clone())
145        }
146
147        if !range.is_full() {
148            req = req.header(header::RANGE, range.to_header());
149        }
150
151        let req = req
152            .extension(Operation::Read)
153            .body(Buffer::new())
154            .map_err(new_request_build_error)?;
155
156        self.info.http_client().fetch(req).await
157    }
158
159    pub async fn webdav_put(
160        &self,
161        path: &str,
162        size: Option<u64>,
163        args: &OpWrite,
164        body: Buffer,
165    ) -> Result<Response<Buffer>> {
166        let path = build_rooted_abs_path(&self.root, path);
167        let url = format!("{}{}", self.endpoint, percent_encode_path(&path));
168
169        let mut req = Request::put(&url);
170
171        if let Some(v) = &self.authorization {
172            req = req.header(header::AUTHORIZATION, v)
173        }
174
175        if let Some(v) = size {
176            req = req.header(header::CONTENT_LENGTH, v)
177        }
178
179        if let Some(v) = args.content_type() {
180            req = req.header(header::CONTENT_TYPE, v)
181        }
182
183        if let Some(v) = args.content_disposition() {
184            req = req.header(header::CONTENT_DISPOSITION, v)
185        }
186
187        let req = req
188            .extension(Operation::Write)
189            .body(body)
190            .map_err(new_request_build_error)?;
191
192        self.info.http_client().send(req).await
193    }
194
195    pub async fn webdav_delete(&self, path: &str) -> Result<Response<Buffer>> {
196        let path = build_rooted_abs_path(&self.root, path);
197        let url = format!("{}{}", self.endpoint, percent_encode_path(&path));
198
199        let mut req = Request::delete(&url);
200
201        if let Some(auth) = &self.authorization {
202            req = req.header(header::AUTHORIZATION, auth.clone())
203        }
204
205        let req = req
206            .extension(Operation::Delete)
207            .body(Buffer::new())
208            .map_err(new_request_build_error)?;
209
210        self.info.http_client().send(req).await
211    }
212
213    pub async fn webdav_copy(&self, from: &str, to: &str) -> Result<Response<Buffer>> {
214        // Check if source file exists.
215        let _ = self.webdav_stat(from).await?;
216        // Make sure target's dir is exist.
217        self.webdav_mkcol(get_parent(to)).await?;
218
219        let source = build_rooted_abs_path(&self.root, from);
220        let source_uri = format!("{}{}", self.endpoint, percent_encode_path(&source));
221
222        let target = build_rooted_abs_path(&self.root, to);
223        let target_uri = format!("{}{}", self.endpoint, percent_encode_path(&target));
224
225        let mut req = Request::builder().method("COPY").uri(&source_uri);
226
227        if let Some(auth) = &self.authorization {
228            req = req.header(header::AUTHORIZATION, auth);
229        }
230
231        req = req.header(HEADER_DESTINATION, target_uri);
232        req = req.header(HEADER_OVERWRITE, "T");
233
234        let req = req
235            .extension(Operation::Copy)
236            .body(Buffer::new())
237            .map_err(new_request_build_error)?;
238
239        self.info.http_client().send(req).await
240    }
241
242    pub async fn webdav_move(&self, from: &str, to: &str) -> Result<Response<Buffer>> {
243        // Check if source file exists.
244        let _ = self.webdav_stat(from).await?;
245        // Make sure target's dir is exist.
246        self.webdav_mkcol(get_parent(to)).await?;
247
248        let source = build_rooted_abs_path(&self.root, from);
249        let source_uri = format!("{}{}", self.endpoint, percent_encode_path(&source));
250
251        let target = build_rooted_abs_path(&self.root, to);
252        let target_uri = format!("{}{}", self.endpoint, percent_encode_path(&target));
253
254        let mut req = Request::builder().method("MOVE").uri(&source_uri);
255
256        if let Some(auth) = &self.authorization {
257            req = req.header(header::AUTHORIZATION, auth);
258        }
259
260        req = req.header(HEADER_DESTINATION, target_uri);
261        req = req.header(HEADER_OVERWRITE, "T");
262
263        let req = req
264            .extension(Operation::Rename)
265            .body(Buffer::new())
266            .map_err(new_request_build_error)?;
267
268        self.info.http_client().send(req).await
269    }
270
271    pub async fn webdav_list(&self, path: &str, args: &OpList) -> Result<Response<Buffer>> {
272        let path = build_rooted_abs_path(&self.root, path);
273        let url = format!("{}{}", self.endpoint, percent_encode_path(&path));
274
275        let mut req = Request::builder().method("PROPFIND").uri(&url);
276
277        req = req.header(header::CONTENT_TYPE, "application/xml");
278        req = req.header(header::CONTENT_LENGTH, PROPFIND_REQUEST.len());
279        if let Some(auth) = &self.authorization {
280            req = req.header(header::AUTHORIZATION, auth);
281        }
282
283        if args.recursive() {
284            req = req.header(HEADER_DEPTH, "infinity");
285        } else {
286            req = req.header(HEADER_DEPTH, "1");
287        }
288
289        let req = req
290            .extension(Operation::List)
291            .body(Buffer::from(Bytes::from(PROPFIND_REQUEST)))
292            .map_err(new_request_build_error)?;
293
294        self.info.http_client().send(req).await
295    }
296
297    /// Create dir recursively for given path.
298    ///
299    /// # Notes
300    ///
301    /// We only expose this method to the backend since there are dependencies on input path.
302    pub async fn webdav_mkcol(&self, path: &str) -> Result<()> {
303        let path = build_rooted_abs_path(&self.root, path);
304        let mut path = path.as_str();
305
306        let mut dirs = VecDeque::default();
307
308        loop {
309            match self.webdav_stat_rooted_abs_path(path).await {
310                // Dir exists, break the loop.
311                Ok(_) => {
312                    break;
313                }
314                // Dir not found, keep going.
315                Err(err) if err.kind() == ErrorKind::NotFound => {
316                    dirs.push_front(path);
317                    path = get_parent(path);
318                }
319                // Unexpected error found, return it.
320                Err(err) => return Err(err),
321            }
322
323            if path == "/" {
324                break;
325            }
326        }
327
328        for dir in dirs {
329            self.webdav_mkcol_rooted_abs_path(dir).await?;
330        }
331        Ok(())
332    }
333
334    /// Create a dir
335    ///
336    /// Input path must be `rooted_abs_path`
337    ///
338    /// Reference: [RFC4918: 9.3.1.  MKCOL Status Codes](https://datatracker.ietf.org/doc/html/rfc4918#section-9.3.1)
339    async fn webdav_mkcol_rooted_abs_path(&self, rooted_abs_path: &str) -> Result<()> {
340        let url = format!("{}{}", self.endpoint, percent_encode_path(rooted_abs_path));
341
342        let mut req = Request::builder().method("MKCOL").uri(&url);
343
344        if let Some(auth) = &self.authorization {
345            req = req.header(header::AUTHORIZATION, auth.clone())
346        }
347
348        let req = req
349            .extension(Operation::CreateDir)
350            .body(Buffer::new())
351            .map_err(new_request_build_error)?;
352
353        let resp = self.info.http_client().send(req).await?;
354        let status = resp.status();
355
356        match status {
357            // 201 (Created) - The collection was created.
358            StatusCode::CREATED
359            // 405 (Method Not Allowed) - MKCOL can only be executed on an unmapped URL.
360            //
361            // The MKCOL method can only be performed on a deleted or non-existent resource.
362            // This error means the directory already exists which is allowed by create_dir.
363            | StatusCode::METHOD_NOT_ALLOWED => {
364
365                Ok(())
366            }
367            _ => Err(parse_error(resp)),
368        }
369    }
370}
371
372pub fn deserialize_multistatus(bs: &[u8]) -> Result<Multistatus> {
373    let s = String::from_utf8_lossy(bs);
374    // HACKS! HACKS! HACKS!
375    //
376    // Make sure the string is escaped.
377    // Related to <https://github.com/tafia/quick-xml/issues/719>
378    //
379    // This is a temporary solution, we should find a better way to handle this.
380    let s = s.replace("&()_+-=;", "%26%28%29_%2B-%3D%3B");
381
382    quick_xml::de::from_str(&s).map_err(new_xml_deserialize_error)
383}
384
385pub fn parse_propstat(propstat: &Propstat) -> Result<Metadata> {
386    let Propstat {
387        prop:
388            Prop {
389                getlastmodified,
390                getcontentlength,
391                getcontenttype,
392                getetag,
393                resourcetype,
394                ..
395            },
396        status,
397    } = propstat;
398
399    if let [_, code, text] = status.splitn(3, ' ').collect::<Vec<_>>()[..3] {
400        // As defined in https://tools.ietf.org/html/rfc2068#section-6.1
401        let code = code.parse::<u16>().unwrap();
402        if code >= 400 {
403            return Err(Error::new(
404                ErrorKind::Unexpected,
405                format!("propfind response is unexpected: {} {}", code, text),
406            ));
407        }
408    }
409
410    let mode: EntryMode = if resourcetype.value == Some(ResourceType::Collection) {
411        EntryMode::DIR
412    } else {
413        EntryMode::FILE
414    };
415    let mut m = Metadata::new(mode);
416
417    if let Some(v) = getcontentlength {
418        m.set_content_length(v.parse::<u64>().unwrap());
419    }
420
421    if let Some(v) = getcontenttype {
422        m.set_content_type(v);
423    }
424
425    if let Some(v) = getetag {
426        m.set_etag(v);
427    }
428
429    // https://www.rfc-editor.org/rfc/rfc4918#section-14.18
430    m.set_last_modified(parse_datetime_from_rfc2822(getlastmodified)?);
431
432    // the storage services have returned all the properties
433    Ok(m)
434}
435
436#[derive(Deserialize, Debug, PartialEq, Eq, Clone, Default)]
437#[serde(default)]
438pub struct Multistatus {
439    pub response: Vec<PropfindResponse>,
440}
441
442#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
443pub struct PropfindResponse {
444    pub href: String,
445    pub propstat: Propstat,
446}
447
448#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
449pub struct Propstat {
450    pub status: String,
451    pub prop: Prop,
452}
453
454#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
455pub struct Prop {
456    pub getlastmodified: String,
457    pub getetag: Option<String>,
458    pub getcontentlength: Option<String>,
459    pub getcontenttype: Option<String>,
460    pub resourcetype: ResourceTypeContainer,
461}
462
463#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
464pub struct ResourceTypeContainer {
465    #[serde(rename = "$value")]
466    pub value: Option<ResourceType>,
467}
468
469#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
470#[serde(rename_all = "lowercase")]
471pub enum ResourceType {
472    Collection,
473}
474
475#[cfg(test)]
476mod tests {
477    use quick_xml::de::from_str;
478
479    use super::*;
480
481    #[test]
482    fn test_propstat() {
483        let xml = r#"<D:propstat>
484            <D:prop>
485                <D:displayname>/</D:displayname>
486                <D:getlastmodified>Tue, 01 May 2022 06:39:47 GMT</D:getlastmodified>
487                <D:resourcetype><D:collection/></D:resourcetype>
488                <D:lockdiscovery/>
489                <D:supportedlock>
490                    <D:lockentry>
491                        <D:lockscope><D:exclusive/></D:lockscope>
492                        <D:locktype><D:write/></D:locktype>
493                    </D:lockentry>
494                </D:supportedlock>
495            </D:prop>
496            <D:status>HTTP/1.1 200 OK</D:status>
497        </D:propstat>"#;
498
499        let propstat = from_str::<Propstat>(xml).unwrap();
500        assert_eq!(
501            propstat.prop.getlastmodified,
502            "Tue, 01 May 2022 06:39:47 GMT"
503        );
504        assert_eq!(
505            propstat.prop.resourcetype.value.unwrap(),
506            ResourceType::Collection
507        );
508
509        assert_eq!(propstat.status, "HTTP/1.1 200 OK");
510    }
511
512    #[test]
513    fn test_response_simple() {
514        let xml = r#"<D:response>
515            <D:href>/</D:href>
516            <D:propstat>
517                <D:prop>
518                    <D:displayname>/</D:displayname>
519                    <D:getlastmodified>Tue, 01 May 2022 06:39:47 GMT</D:getlastmodified>
520                    <D:resourcetype><D:collection/></D:resourcetype>
521                    <D:lockdiscovery/>
522                    <D:supportedlock>
523                        <D:lockentry>
524                            <D:lockscope><D:exclusive/></D:lockscope>
525                            <D:locktype><D:write/></D:locktype>
526                        </D:lockentry>
527                    </D:supportedlock>
528                </D:prop>
529                <D:status>HTTP/1.1 200 OK</D:status>
530            </D:propstat>
531        </D:response>"#;
532
533        let response = from_str::<PropfindResponse>(xml).unwrap();
534        assert_eq!(response.href, "/");
535
536        assert_eq!(
537            response.propstat.prop.getlastmodified,
538            "Tue, 01 May 2022 06:39:47 GMT"
539        );
540        assert_eq!(
541            response.propstat.prop.resourcetype.value.unwrap(),
542            ResourceType::Collection
543        );
544        assert_eq!(response.propstat.status, "HTTP/1.1 200 OK");
545    }
546
547    #[test]
548    fn test_response_file() {
549        let xml = r#"<D:response>
550        <D:href>/test_file</D:href>
551        <D:propstat>
552          <D:prop>
553            <D:displayname>test_file</D:displayname>
554            <D:getcontentlength>1</D:getcontentlength>
555            <D:getlastmodified>Tue, 07 May 2022 05:52:22 GMT</D:getlastmodified>
556            <D:resourcetype></D:resourcetype>
557            <D:lockdiscovery />
558            <D:supportedlock>
559              <D:lockentry>
560                <D:lockscope>
561                  <D:exclusive />
562                </D:lockscope>
563                <D:locktype>
564                  <D:write />
565                </D:locktype>
566              </D:lockentry>
567            </D:supportedlock>
568          </D:prop>
569          <D:status>HTTP/1.1 200 OK</D:status>
570        </D:propstat>
571      </D:response>"#;
572
573        let response = from_str::<PropfindResponse>(xml).unwrap();
574        assert_eq!(response.href, "/test_file");
575        assert_eq!(
576            response.propstat.prop.getlastmodified,
577            "Tue, 07 May 2022 05:52:22 GMT"
578        );
579        assert_eq!(response.propstat.prop.getcontentlength.unwrap(), "1");
580        assert_eq!(response.propstat.prop.resourcetype.value, None);
581        assert_eq!(response.propstat.status, "HTTP/1.1 200 OK");
582    }
583
584    #[test]
585    fn test_with_multiple_items_simple() {
586        let xml = r#"<D:multistatus xmlns:D="DAV:">
587        <D:response>
588        <D:href>/</D:href>
589        <D:propstat>
590            <D:prop>
591                <D:displayname>/</D:displayname>
592                <D:getlastmodified>Tue, 01 May 2022 06:39:47 GMT</D:getlastmodified>
593                <D:resourcetype><D:collection/></D:resourcetype>
594                <D:lockdiscovery/>
595                <D:supportedlock>
596                    <D:lockentry>
597                        <D:lockscope><D:exclusive/></D:lockscope>
598                        <D:locktype><D:write/></D:locktype>
599                    </D:lockentry>
600                </D:supportedlock>
601            </D:prop>
602            <D:status>HTTP/1.1 200 OK</D:status>
603        </D:propstat>
604    </D:response>
605    <D:response>
606            <D:href>/</D:href>
607            <D:propstat>
608                <D:prop>
609                    <D:displayname>/</D:displayname>
610                    <D:getlastmodified>Tue, 01 May 2022 06:39:47 GMT</D:getlastmodified>
611                    <D:resourcetype><D:collection/></D:resourcetype>
612                    <D:lockdiscovery/>
613                    <D:supportedlock>
614                        <D:lockentry>
615                            <D:lockscope><D:exclusive/></D:lockscope>
616                            <D:locktype><D:write/></D:locktype>
617                        </D:lockentry>
618                    </D:supportedlock>
619                </D:prop>
620                <D:status>HTTP/1.1 200 OK</D:status>
621            </D:propstat>
622        </D:response>
623        </D:multistatus>"#;
624
625        let multistatus = from_str::<Multistatus>(xml).unwrap();
626
627        let response = multistatus.response;
628        assert_eq!(response.len(), 2);
629        assert_eq!(response[0].href, "/");
630        assert_eq!(
631            response[0].propstat.prop.getlastmodified,
632            "Tue, 01 May 2022 06:39:47 GMT"
633        );
634    }
635
636    #[test]
637    fn test_with_multiple_items_mixed() {
638        let xml = r#"<?xml version="1.0" encoding="utf-8"?>
639        <D:multistatus xmlns:D="DAV:">
640          <D:response>
641            <D:href>/</D:href>
642            <D:propstat>
643              <D:prop>
644                <D:displayname>/</D:displayname>
645                <D:getlastmodified>Tue, 07 May 2022 06:39:47 GMT</D:getlastmodified>
646                <D:resourcetype>
647                  <D:collection />
648                </D:resourcetype>
649                <D:lockdiscovery />
650                <D:supportedlock>
651                  <D:lockentry>
652                    <D:lockscope>
653                      <D:exclusive />
654                    </D:lockscope>
655                    <D:locktype>
656                      <D:write />
657                    </D:locktype>
658                  </D:lockentry>
659                </D:supportedlock>
660              </D:prop>
661              <D:status>HTTP/1.1 200 OK</D:status>
662            </D:propstat>
663          </D:response>
664          <D:response>
665            <D:href>/testdir/</D:href>
666            <D:propstat>
667              <D:prop>
668                <D:displayname>testdir</D:displayname>
669                <D:getlastmodified>Tue, 07 May 2022 06:40:10 GMT</D:getlastmodified>
670                <D:resourcetype>
671                  <D:collection />
672                </D:resourcetype>
673                <D:lockdiscovery />
674                <D:supportedlock>
675                  <D:lockentry>
676                    <D:lockscope>
677                      <D:exclusive />
678                    </D:lockscope>
679                    <D:locktype>
680                      <D:write />
681                    </D:locktype>
682                  </D:lockentry>
683                </D:supportedlock>
684              </D:prop>
685              <D:status>HTTP/1.1 200 OK</D:status>
686            </D:propstat>
687          </D:response>
688          <D:response>
689            <D:href>/test_file</D:href>
690            <D:propstat>
691              <D:prop>
692                <D:displayname>test_file</D:displayname>
693                <D:getcontentlength>1</D:getcontentlength>
694                <D:getlastmodified>Tue, 07 May 2022 05:52:22 GMT</D:getlastmodified>
695                <D:resourcetype></D:resourcetype>
696                <D:lockdiscovery />
697                <D:supportedlock>
698                  <D:lockentry>
699                    <D:lockscope>
700                      <D:exclusive />
701                    </D:lockscope>
702                    <D:locktype>
703                      <D:write />
704                    </D:locktype>
705                  </D:lockentry>
706                </D:supportedlock>
707              </D:prop>
708              <D:status>HTTP/1.1 200 OK</D:status>
709            </D:propstat>
710          </D:response>
711        </D:multistatus>"#;
712
713        let multistatus = from_str::<Multistatus>(xml).unwrap();
714
715        let response = multistatus.response;
716        assert_eq!(response.len(), 3);
717        let first_response = &response[0];
718        assert_eq!(first_response.href, "/");
719        assert_eq!(
720            first_response.propstat.prop.getlastmodified,
721            "Tue, 07 May 2022 06:39:47 GMT"
722        );
723
724        let second_response = &response[1];
725        assert_eq!(second_response.href, "/testdir/");
726        assert_eq!(
727            second_response.propstat.prop.getlastmodified,
728            "Tue, 07 May 2022 06:40:10 GMT"
729        );
730
731        let third_response = &response[2];
732        assert_eq!(third_response.href, "/test_file");
733        assert_eq!(
734            third_response.propstat.prop.getlastmodified,
735            "Tue, 07 May 2022 05:52:22 GMT"
736        );
737    }
738
739    #[test]
740    fn test_with_multiple_items_mixed_nginx() {
741        let xml = r#"<?xml version="1.0" encoding="utf-8"?>
742      <D:multistatus xmlns:D="DAV:">
743        <D:response>
744          <D:href>/</D:href>
745          <D:propstat>
746            <D:prop>
747              <D:getlastmodified>Fri, 17 Feb 2023 03:37:22 GMT</D:getlastmodified>
748              <D:resourcetype>
749                <D:collection />
750              </D:resourcetype>
751            </D:prop>
752            <D:status>HTTP/1.1 200 OK</D:status>
753          </D:propstat>
754        </D:response>
755        <D:response>
756          <D:href>/test_file_75</D:href>
757          <D:propstat>
758            <D:prop>
759              <D:getcontentlength>1</D:getcontentlength>
760              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
761              <D:resourcetype></D:resourcetype>
762            </D:prop>
763            <D:status>HTTP/1.1 200 OK</D:status>
764          </D:propstat>
765        </D:response>
766        <D:response>
767          <D:href>/test_file_36</D:href>
768          <D:propstat>
769            <D:prop>
770              <D:getcontentlength>1</D:getcontentlength>
771              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
772              <D:resourcetype></D:resourcetype>
773            </D:prop>
774            <D:status>HTTP/1.1 200 OK</D:status>
775          </D:propstat>
776        </D:response>
777        <D:response>
778          <D:href>/test_file_38</D:href>
779          <D:propstat>
780            <D:prop>
781              <D:getcontentlength>1</D:getcontentlength>
782              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
783              <D:resourcetype></D:resourcetype>
784            </D:prop>
785            <D:status>HTTP/1.1 200 OK</D:status>
786          </D:propstat>
787        </D:response>
788        <D:response>
789          <D:href>/test_file_59</D:href>
790          <D:propstat>
791            <D:prop>
792              <D:getcontentlength>1</D:getcontentlength>
793              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
794              <D:resourcetype></D:resourcetype>
795            </D:prop>
796            <D:status>HTTP/1.1 200 OK</D:status>
797          </D:propstat>
798        </D:response>
799        <D:response>
800          <D:href>/test_file_9</D:href>
801          <D:propstat>
802            <D:prop>
803              <D:getcontentlength>1</D:getcontentlength>
804              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
805              <D:resourcetype></D:resourcetype>
806            </D:prop>
807            <D:status>HTTP/1.1 200 OK</D:status>
808          </D:propstat>
809        </D:response>
810        <D:response>
811          <D:href>/test_file_93</D:href>
812          <D:propstat>
813            <D:prop>
814              <D:getcontentlength>1</D:getcontentlength>
815              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
816              <D:resourcetype></D:resourcetype>
817            </D:prop>
818            <D:status>HTTP/1.1 200 OK</D:status>
819          </D:propstat>
820        </D:response>
821        <D:response>
822          <D:href>/test_file_43</D:href>
823          <D:propstat>
824            <D:prop>
825              <D:getcontentlength>1</D:getcontentlength>
826              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
827              <D:resourcetype></D:resourcetype>
828            </D:prop>
829            <D:status>HTTP/1.1 200 OK</D:status>
830          </D:propstat>
831        </D:response>
832        <D:response>
833          <D:href>/test_file_95</D:href>
834          <D:propstat>
835            <D:prop>
836              <D:getcontentlength>1</D:getcontentlength>
837              <D:getlastmodified>Fri, 17 Feb 2023 03:36:54 GMT</D:getlastmodified>
838              <D:resourcetype></D:resourcetype>
839            </D:prop>
840            <D:status>HTTP/1.1 200 OK</D:status>
841          </D:propstat>
842        </D:response>
843      </D:multistatus>
844      "#;
845
846        let multistatus: Multistatus = from_str(xml).unwrap();
847
848        let response = multistatus.response;
849        assert_eq!(response.len(), 9);
850
851        let first_response = &response[0];
852        assert_eq!(first_response.href, "/");
853        assert_eq!(
854            first_response.propstat.prop.getlastmodified,
855            "Fri, 17 Feb 2023 03:37:22 GMT"
856        );
857    }
858}