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