1use 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
35static PROPFIND_REQUEST: &str = r#"<?xml version="1.0" encoding="utf-8" ?><D:propfind xmlns:D="DAV:"><D:allprop/></D:propfind>"#;
39
40static HEADER_DEPTH: &str = "Depth";
50static HEADER_DESTINATION: &str = "Destination";
58static 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 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 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 let _ = self.webdav_stat(from).await?;
217 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 let _ = self.webdav_stat(from).await?;
246 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 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 Ok(_) => {
313 break;
314 }
315 Err(err) if err.kind() == ErrorKind::NotFound => {
317 dirs.push_front(path);
318 path = get_parent(path);
319 }
320 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 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 StatusCode::CREATED
360 | 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 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 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 m.set_last_modified(parse_datetime_from_rfc2822(getlastmodified)?);
432
433 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}