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