1use 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
33static PROPFIND_REQUEST: &str = r#"<?xml version="1.0" encoding="utf-8" ?><D:propfind xmlns:D="DAV:"><D:allprop/></D:propfind>"#;
37
38static HEADER_DEPTH: &str = "Depth";
48static HEADER_DESTINATION: &str = "Destination";
56static 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 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 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 let _ = self.webdav_stat(from).await?;
215 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 let _ = self.webdav_stat(from).await?;
244 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 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 Ok(_) => {
311 break;
312 }
313 Err(err) if err.kind() == ErrorKind::NotFound => {
315 dirs.push_front(path);
316 path = get_parent(path);
317 }
318 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 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 StatusCode::CREATED
358 | 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 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 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 m.set_last_modified(Timestamp::parse_rfc2822(getlastmodified)?);
430
431 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}