1use 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
34static PROPFIND_REQUEST: &str = r#"<?xml version="1.0" encoding="utf-8" ?><D:propfind xmlns:D="DAV:"><D:allprop/></D:propfind>"#;
38
39static HEADER_DEPTH: &str = "Depth";
49static HEADER_DESTINATION: &str = "Destination";
57static 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 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 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 let _ = self.webdav_stat(from).await?;
216 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 let _ = self.webdav_stat(from).await?;
245 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 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 Ok(_) => {
312 break;
313 }
314 Err(err) if err.kind() == ErrorKind::NotFound => {
316 dirs.push_front(path);
317 path = get_parent(path);
318 }
319 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 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 StatusCode::CREATED
359 | 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 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 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 m.set_last_modified(parse_datetime_from_rfc2822(getlastmodified)?);
431
432 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}