1use std::fmt::Debug;
19use std::sync::Arc;
20
21use base64::Engine;
22use base64::prelude::BASE64_STANDARD;
23use bytes::Bytes;
24use constants::X_MS_META_PREFIX;
25use http::HeaderValue;
26use http::Request;
27use http::Response;
28use http::header::CONTENT_LENGTH;
29use http::header::CONTENT_TYPE;
30use http::header::HeaderName;
31use http::header::IF_MATCH;
32use http::header::IF_MODIFIED_SINCE;
33use http::header::IF_NONE_MATCH;
34use http::header::IF_UNMODIFIED_SINCE;
35use reqsign::AzureStorageCredential;
36use reqsign::AzureStorageLoader;
37use reqsign::AzureStorageSigner;
38use serde::Deserialize;
39use serde::Serialize;
40use uuid::Uuid;
41
42use crate::raw::*;
43use crate::*;
44
45pub mod constants {
46 pub const X_MS_VERSION: &str = "x-ms-version";
48
49 pub const X_MS_BLOB_TYPE: &str = "x-ms-blob-type";
50 pub const X_MS_COPY_SOURCE: &str = "x-ms-copy-source";
51 pub const X_MS_BLOB_CACHE_CONTROL: &str = "x-ms-blob-cache-control";
52 pub const X_MS_BLOB_CONDITION_APPENDPOS: &str = "x-ms-blob-condition-appendpos";
53 pub const X_MS_META_PREFIX: &str = "x-ms-meta-";
54
55 pub const X_MS_VERSION_ID: &str = "x-ms-version-id";
57
58 pub const X_MS_ENCRYPTION_KEY: &str = "x-ms-encryption-key";
60 pub const X_MS_ENCRYPTION_KEY_SHA256: &str = "x-ms-encryption-key-sha256";
61 pub const X_MS_ENCRYPTION_ALGORITHM: &str = "x-ms-encryption-algorithm";
62}
63
64pub struct AzblobCore {
65 pub info: Arc<AccessorInfo>,
66 pub container: String,
67 pub root: String,
68 pub endpoint: String,
69 pub encryption_key: Option<HeaderValue>,
70 pub encryption_key_sha256: Option<HeaderValue>,
71 pub encryption_algorithm: Option<HeaderValue>,
72 pub loader: AzureStorageLoader,
73 pub signer: AzureStorageSigner,
74}
75
76impl Debug for AzblobCore {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.debug_struct("AzblobCore")
79 .field("container", &self.container)
80 .field("root", &self.root)
81 .field("endpoint", &self.endpoint)
82 .finish_non_exhaustive()
83 }
84}
85
86impl AzblobCore {
87 async fn load_credential(&self) -> Result<AzureStorageCredential> {
88 let cred = self
89 .loader
90 .load()
91 .await
92 .map_err(new_request_credential_error)?;
93
94 if let Some(cred) = cred {
95 Ok(cred)
96 } else {
97 Err(Error::new(
98 ErrorKind::ConfigInvalid,
99 "no valid credential found",
100 ))
101 }
102 }
103
104 pub async fn sign_query<T>(&self, req: &mut Request<T>) -> Result<()> {
105 let cred = self.load_credential().await?;
106
107 self.signer
108 .sign_query(req, Duration::from_secs(3600), &cred)
109 .map_err(new_request_sign_error)
110 }
111
112 pub async fn sign<T>(&self, req: &mut Request<T>) -> Result<()> {
113 let cred = self.load_credential().await?;
114 req.headers_mut().insert(
116 HeaderName::from_static(constants::X_MS_VERSION),
117 HeaderValue::from_static("2022-11-02"),
123 );
124 self.signer.sign(req, &cred).map_err(new_request_sign_error)
125 }
126
127 async fn batch_sign<T>(&self, req: &mut Request<T>) -> Result<()> {
128 let cred = self.load_credential().await?;
129 self.signer.sign(req, &cred).map_err(new_request_sign_error)
130 }
131
132 #[inline]
133 pub async fn send(&self, req: Request<Buffer>) -> Result<Response<Buffer>> {
134 self.info.http_client().send(req).await
135 }
136
137 pub fn insert_sse_headers(&self, mut req: http::request::Builder) -> http::request::Builder {
138 if let Some(v) = &self.encryption_key {
139 let mut v = v.clone();
140 v.set_sensitive(true);
141
142 req = req.header(HeaderName::from_static(constants::X_MS_ENCRYPTION_KEY), v)
143 }
144
145 if let Some(v) = &self.encryption_key_sha256 {
146 let mut v = v.clone();
147 v.set_sensitive(true);
148
149 req = req.header(
150 HeaderName::from_static(constants::X_MS_ENCRYPTION_KEY_SHA256),
151 v,
152 )
153 }
154
155 if let Some(v) = &self.encryption_algorithm {
156 let mut v = v.clone();
157 v.set_sensitive(true);
158
159 req = req.header(
160 HeaderName::from_static(constants::X_MS_ENCRYPTION_ALGORITHM),
161 v,
162 )
163 }
164
165 req
166 }
167}
168
169impl AzblobCore {
170 fn build_path_url(&self, path: &str) -> String {
171 format!(
172 "{}/{}/{}",
173 self.endpoint,
174 self.container,
175 percent_encode_path(&build_abs_path(&self.root, path))
176 )
177 }
178
179 pub fn azblob_get_blob_request(
180 &self,
181 path: &str,
182 range: BytesRange,
183 args: &OpRead,
184 ) -> Result<Request<Buffer>> {
185 let mut url = self.build_path_url(path);
186
187 if let Some(override_content_disposition) = args.override_content_disposition() {
188 url.push_str(&format!(
189 "?rscd={}",
190 percent_encode_path(override_content_disposition)
191 ));
192 }
193
194 let mut req = Request::get(&url);
195
196 req = self.insert_sse_headers(req);
198
199 if !range.is_full() {
200 req = req.header(http::header::RANGE, range.to_header());
201 }
202
203 if let Some(if_none_match) = args.if_none_match() {
204 req = req.header(IF_NONE_MATCH, if_none_match);
205 }
206
207 if let Some(if_match) = args.if_match() {
208 req = req.header(IF_MATCH, if_match);
209 }
210
211 if let Some(if_modified_since) = args.if_modified_since() {
212 req = req.header(IF_MODIFIED_SINCE, if_modified_since.format_http_date());
213 }
214
215 if let Some(if_unmodified_since) = args.if_unmodified_since() {
216 req = req.header(IF_UNMODIFIED_SINCE, if_unmodified_since.format_http_date());
217 }
218
219 let req = req
220 .extension(Operation::Read)
221 .body(Buffer::new())
222 .map_err(new_request_build_error)?;
223
224 Ok(req)
225 }
226
227 pub async fn azblob_get_blob(
228 &self,
229 path: &str,
230 range: BytesRange,
231 args: &OpRead,
232 ) -> Result<Response<HttpBody>> {
233 let mut req = self.azblob_get_blob_request(path, range, args)?;
234
235 self.sign(&mut req).await?;
236
237 self.info.http_client().fetch(req).await
238 }
239
240 pub fn azblob_put_blob_request(
241 &self,
242 path: &str,
243 size: Option<u64>,
244 args: &OpWrite,
245 body: Buffer,
246 ) -> Result<Request<Buffer>> {
247 let mut req = Request::put(self.build_path_url(path));
248
249 req = req.header(
250 HeaderName::from_static(constants::X_MS_BLOB_TYPE),
251 "BlockBlob",
252 );
253
254 if let Some(size) = size {
255 req = req.header(CONTENT_LENGTH, size)
256 }
257
258 if let Some(ty) = args.content_type() {
259 req = req.header(CONTENT_TYPE, ty)
260 }
261
262 if args.if_not_exists() {
265 req = req.header(IF_NONE_MATCH, "*");
266 }
267
268 if let Some(v) = args.if_none_match() {
269 req = req.header(IF_NONE_MATCH, v);
270 }
271
272 if let Some(cache_control) = args.cache_control() {
273 req = req.header(constants::X_MS_BLOB_CACHE_CONTROL, cache_control);
274 }
275
276 req = self.insert_sse_headers(req);
278
279 if let Some(user_metadata) = args.user_metadata() {
280 for (key, value) in user_metadata {
281 req = req.header(format!("{X_MS_META_PREFIX}{key}"), value)
282 }
283 }
284
285 let req = req
286 .extension(Operation::Write)
287 .body(body)
288 .map_err(new_request_build_error)?;
289
290 Ok(req)
291 }
292
293 pub async fn azblob_put_blob(
294 &self,
295 path: &str,
296 size: Option<u64>,
297 args: &OpWrite,
298 body: Buffer,
299 ) -> Result<Response<Buffer>> {
300 let mut req = self.azblob_put_blob_request(path, size, args, body)?;
301
302 self.sign(&mut req).await?;
303 self.send(req).await
304 }
305
306 fn azblob_init_appendable_blob_request(
324 &self,
325 path: &str,
326 args: &OpWrite,
327 ) -> Result<Request<Buffer>> {
328 let mut req = Request::put(self.build_path_url(path));
329
330 req = self.insert_sse_headers(req);
332
333 req = req.header(CONTENT_LENGTH, 0);
336 req = req.header(
337 HeaderName::from_static(constants::X_MS_BLOB_TYPE),
338 "AppendBlob",
339 );
340
341 if let Some(ty) = args.content_type() {
342 req = req.header(CONTENT_TYPE, ty)
343 }
344
345 if let Some(cache_control) = args.cache_control() {
346 req = req.header(constants::X_MS_BLOB_CACHE_CONTROL, cache_control);
347 }
348
349 let req = req
350 .extension(Operation::Write)
351 .body(Buffer::new())
352 .map_err(new_request_build_error)?;
353
354 Ok(req)
355 }
356
357 pub async fn azblob_init_appendable_blob(
358 &self,
359 path: &str,
360 args: &OpWrite,
361 ) -> Result<Response<Buffer>> {
362 let mut req = self.azblob_init_appendable_blob_request(path, args)?;
363
364 self.sign(&mut req).await?;
365 self.send(req).await
366 }
367
368 fn azblob_append_blob_request(
380 &self,
381 path: &str,
382 position: u64,
383 size: u64,
384 body: Buffer,
385 ) -> Result<Request<Buffer>> {
386 let url = format!("{}?comp=appendblock", &self.build_path_url(path));
387
388 let mut req = Request::put(&url)
389 .header(CONTENT_LENGTH, size)
390 .header(constants::X_MS_BLOB_CONDITION_APPENDPOS, position);
391
392 req = self.insert_sse_headers(req);
394
395 let req = req
396 .extension(Operation::Write)
397 .body(body)
398 .map_err(new_request_build_error)?;
399
400 Ok(req)
401 }
402
403 pub async fn azblob_append_blob(
404 &self,
405 path: &str,
406 position: u64,
407 size: u64,
408 body: Buffer,
409 ) -> Result<Response<Buffer>> {
410 let mut req = self.azblob_append_blob_request(path, position, size, body)?;
411
412 self.sign(&mut req).await?;
413 self.send(req).await
414 }
415
416 pub fn azblob_put_block_request(
417 &self,
418 path: &str,
419 block_id: Uuid,
420 size: Option<u64>,
421 args: &OpWrite,
422 body: Buffer,
423 ) -> Result<Request<Buffer>> {
424 let url = QueryPairsWriter::new(&self.build_path_url(path))
427 .push("comp", "block")
428 .push(
429 "blockid",
430 &percent_encode_path(&BASE64_STANDARD.encode(block_id.as_bytes())),
431 )
432 .finish();
433
434 let mut req = Request::put(&url);
435 req = self.insert_sse_headers(req);
437
438 if let Some(cache_control) = args.cache_control() {
439 req = req.header(constants::X_MS_BLOB_CACHE_CONTROL, cache_control);
440 }
441 if let Some(size) = size {
442 req = req.header(CONTENT_LENGTH, size)
443 }
444
445 if let Some(ty) = args.content_type() {
446 req = req.header(CONTENT_TYPE, ty)
447 }
448
449 let req = req
450 .extension(Operation::Write)
451 .body(body)
452 .map_err(new_request_build_error)?;
453
454 Ok(req)
455 }
456
457 pub async fn azblob_put_block(
458 &self,
459 path: &str,
460 block_id: Uuid,
461 size: Option<u64>,
462 args: &OpWrite,
463 body: Buffer,
464 ) -> Result<Response<Buffer>> {
465 let mut req = self.azblob_put_block_request(path, block_id, size, args, body)?;
466
467 self.sign(&mut req).await?;
468 self.send(req).await
469 }
470
471 fn azblob_complete_put_block_list_request(
472 &self,
473 path: &str,
474 block_ids: Vec<Uuid>,
475 args: &OpWrite,
476 ) -> Result<Request<Buffer>> {
477 let url = format!("{}?comp=blocklist", &self.build_path_url(path));
478
479 let req = Request::put(&url);
480
481 let mut req = self.insert_sse_headers(req);
483 if let Some(cache_control) = args.cache_control() {
484 req = req.header(constants::X_MS_BLOB_CACHE_CONTROL, cache_control);
485 }
486
487 let content = quick_xml::se::to_string(&PutBlockListRequest {
488 latest: block_ids
489 .into_iter()
490 .map(|block_id| {
491 let encoded_block_id: String = BASE64_STANDARD.encode(block_id.as_bytes());
492 encoded_block_id
493 })
494 .collect(),
495 })
496 .map_err(new_xml_serialize_error)?;
497
498 req = req.header(CONTENT_LENGTH, content.len());
499
500 let req = req
501 .extension(Operation::Write)
502 .body(Buffer::from(Bytes::from(content)))
503 .map_err(new_request_build_error)?;
504
505 Ok(req)
506 }
507
508 pub async fn azblob_complete_put_block_list(
509 &self,
510 path: &str,
511 block_ids: Vec<Uuid>,
512 args: &OpWrite,
513 ) -> Result<Response<Buffer>> {
514 let mut req = self.azblob_complete_put_block_list_request(path, block_ids, args)?;
515
516 self.sign(&mut req).await?;
517
518 self.send(req).await
519 }
520
521 pub fn azblob_head_blob_request(&self, path: &str, args: &OpStat) -> Result<Request<Buffer>> {
522 let mut req = Request::head(self.build_path_url(path));
523
524 req = self.insert_sse_headers(req);
526
527 if let Some(if_none_match) = args.if_none_match() {
528 req = req.header(IF_NONE_MATCH, if_none_match);
529 }
530
531 if let Some(if_match) = args.if_match() {
532 req = req.header(IF_MATCH, if_match);
533 }
534
535 let req = req
536 .extension(Operation::Stat)
537 .body(Buffer::new())
538 .map_err(new_request_build_error)?;
539
540 Ok(req)
541 }
542
543 pub async fn azblob_get_blob_properties(
544 &self,
545 path: &str,
546 args: &OpStat,
547 ) -> Result<Response<Buffer>> {
548 let mut req = self.azblob_head_blob_request(path, args)?;
549
550 self.sign(&mut req).await?;
551 self.send(req).await
552 }
553
554 fn azblob_delete_blob_request(&self, path: &str) -> Result<Request<Buffer>> {
555 Request::delete(self.build_path_url(path))
556 .header(CONTENT_LENGTH, 0)
557 .extension(Operation::Delete)
558 .body(Buffer::new())
559 .map_err(new_request_build_error)
560 }
561
562 pub async fn azblob_delete_blob(&self, path: &str) -> Result<Response<Buffer>> {
563 let mut req = self.azblob_delete_blob_request(path)?;
564
565 self.sign(&mut req).await?;
566 self.send(req).await
567 }
568
569 pub async fn azblob_copy_blob(
570 &self,
571 from: &str,
572 to: &str,
573 args: OpCopy,
574 ) -> Result<Response<Buffer>> {
575 let source = self.build_path_url(from);
576 let target = self.build_path_url(to);
577
578 let mut req = Request::put(&target)
579 .header(constants::X_MS_COPY_SOURCE, source)
580 .header(CONTENT_LENGTH, 0);
581
582 if args.if_not_exists() {
584 req = req.header(IF_NONE_MATCH, "*");
585 }
586
587 let mut req = req
588 .extension(Operation::Copy)
589 .body(Buffer::new())
590 .map_err(new_request_build_error)?;
591
592 self.sign(&mut req).await?;
593 self.send(req).await
594 }
595
596 pub async fn azblob_list_blobs(
597 &self,
598 path: &str,
599 next_marker: &str,
600 delimiter: &str,
601 limit: Option<usize>,
602 ) -> Result<Response<Buffer>> {
603 let p = build_abs_path(&self.root, path);
604 let mut url = QueryPairsWriter::new(&format!("{}/{}", self.endpoint, self.container))
605 .push("restype", "container")
606 .push("comp", "list");
607
608 if !p.is_empty() {
609 url = url.push("prefix", &percent_encode_path(&p));
610 }
611 if let Some(limit) = limit {
612 url = url.push("maxresults", &limit.to_string());
613 }
614 if !delimiter.is_empty() {
615 url = url.push("delimiter", delimiter);
616 }
617 if !next_marker.is_empty() {
618 url = url.push("marker", next_marker);
619 }
620
621 let mut req = Request::get(url.finish())
622 .extension(Operation::List)
623 .body(Buffer::new())
624 .map_err(new_request_build_error)?;
625
626 self.sign(&mut req).await?;
627 self.send(req).await
628 }
629
630 pub async fn azblob_batch_delete(&self, paths: &[String]) -> Result<Response<Buffer>> {
631 let url = format!(
632 "{}/{}?restype=container&comp=batch",
633 self.endpoint, self.container
634 );
635
636 let mut multipart = Multipart::new();
637
638 for (idx, path) in paths.iter().enumerate() {
639 let mut req = self.azblob_delete_blob_request(path)?;
640 self.batch_sign(&mut req).await?;
641
642 multipart = multipart.part(
643 MixedPart::from_request(req).part_header("content-id".parse().unwrap(), idx.into()),
644 );
645 }
646
647 let req = Request::post(url);
648 let mut req = multipart.apply(req)?;
649
650 self.sign(&mut req).await?;
651 self.send(req).await
652 }
653}
654
655#[derive(Default, Debug, Serialize, Deserialize)]
657#[serde(default, rename = "BlockList", rename_all = "PascalCase")]
658pub struct PutBlockListRequest {
659 pub latest: Vec<String>,
660}
661
662#[derive(Default, Debug, Deserialize)]
663#[serde(default, rename_all = "PascalCase")]
664pub struct ListBlobsOutput {
665 pub blobs: Blobs,
666 #[serde(rename = "NextMarker")]
667 pub next_marker: Option<String>,
668}
669
670#[derive(Default, Debug, Deserialize)]
671#[serde(default, rename_all = "PascalCase")]
672pub struct Blobs {
673 pub blob: Vec<Blob>,
674 pub blob_prefix: Vec<BlobPrefix>,
675}
676
677#[derive(Default, Debug, Deserialize)]
678#[serde(default, rename_all = "PascalCase")]
679pub struct BlobPrefix {
680 pub name: String,
681}
682
683#[derive(Default, Debug, Deserialize)]
684#[serde(default, rename_all = "PascalCase")]
685pub struct Blob {
686 pub properties: Properties,
687 pub name: String,
688}
689
690#[derive(Default, Debug, Deserialize)]
691#[serde(default, rename_all = "PascalCase")]
692pub struct Properties {
693 #[serde(rename = "Content-Length")]
694 pub content_length: u64,
695 #[serde(rename = "Last-Modified")]
696 pub last_modified: String,
697 #[serde(rename = "Content-MD5")]
698 pub content_md5: String,
699 #[serde(rename = "Content-Type")]
700 pub content_type: String,
701 pub etag: String,
702}
703
704#[cfg(test)]
705mod tests {
706 use bytes::Buf;
707 use bytes::Bytes;
708 use quick_xml::de;
709
710 use super::*;
711
712 #[test]
713 fn test_parse_xml() {
714 let bs = bytes::Bytes::from(
715 r#"
716 <?xml version="1.0" encoding="utf-8"?>
717 <EnumerationResults ServiceEndpoint="https://test.blob.core.windows.net/" ContainerName="myazurebucket">
718 <Prefix>dir1/</Prefix>
719 <Delimiter>/</Delimiter>
720 <Blobs>
721 <Blob>
722 <Name>dir1/2f018bb5-466f-4af1-84fa-2b167374ee06</Name>
723 <Properties>
724 <Creation-Time>Sun, 20 Mar 2022 11:29:03 GMT</Creation-Time>
725 <Last-Modified>Sun, 20 Mar 2022 11:29:03 GMT</Last-Modified>
726 <Etag>0x8DA0A64D66790C3</Etag>
727 <Content-Length>3485277</Content-Length>
728 <Content-Type>application/octet-stream</Content-Type>
729 <Content-Encoding />
730 <Content-Language />
731 <Content-CRC64 />
732 <Content-MD5>llJ/+jOlx5GdA1sL7SdKuw==</Content-MD5>
733 <Cache-Control />
734 <Content-Disposition />
735 <BlobType>BlockBlob</BlobType>
736 <AccessTier>Hot</AccessTier>
737 <AccessTierInferred>true</AccessTierInferred>
738 <LeaseStatus>unlocked</LeaseStatus>
739 <LeaseState>available</LeaseState>
740 <ServerEncrypted>true</ServerEncrypted>
741 </Properties>
742 <OrMetadata />
743 </Blob>
744 <Blob>
745 <Name>dir1/5b9432b2-79c0-48d8-90c2-7d3e153826ed</Name>
746 <Properties>
747 <Creation-Time>Tue, 29 Mar 2022 01:54:07 GMT</Creation-Time>
748 <Last-Modified>Tue, 29 Mar 2022 01:54:07 GMT</Last-Modified>
749 <Etag>0x8DA112702D88FE4</Etag>
750 <Content-Length>2471869</Content-Length>
751 <Content-Type>application/octet-stream</Content-Type>
752 <Content-Encoding />
753 <Content-Language />
754 <Content-CRC64 />
755 <Content-MD5>xmgUltSnopLSJOukgCHFtg==</Content-MD5>
756 <Cache-Control />
757 <Content-Disposition />
758 <BlobType>BlockBlob</BlobType>
759 <AccessTier>Hot</AccessTier>
760 <AccessTierInferred>true</AccessTierInferred>
761 <LeaseStatus>unlocked</LeaseStatus>
762 <LeaseState>available</LeaseState>
763 <ServerEncrypted>true</ServerEncrypted>
764 </Properties>
765 <OrMetadata />
766 </Blob>
767 <Blob>
768 <Name>dir1/b2d96f8b-d467-40d1-bb11-4632dddbf5b5</Name>
769 <Properties>
770 <Creation-Time>Sun, 20 Mar 2022 11:31:57 GMT</Creation-Time>
771 <Last-Modified>Sun, 20 Mar 2022 11:31:57 GMT</Last-Modified>
772 <Etag>0x8DA0A653DC82981</Etag>
773 <Content-Length>1259677</Content-Length>
774 <Content-Type>application/octet-stream</Content-Type>
775 <Content-Encoding />
776 <Content-Language />
777 <Content-CRC64 />
778 <Content-MD5>AxTiFXHwrXKaZC5b7ZRybw==</Content-MD5>
779 <Cache-Control />
780 <Content-Disposition />
781 <BlobType>BlockBlob</BlobType>
782 <AccessTier>Hot</AccessTier>
783 <AccessTierInferred>true</AccessTierInferred>
784 <LeaseStatus>unlocked</LeaseStatus>
785 <LeaseState>available</LeaseState>
786 <ServerEncrypted>true</ServerEncrypted>
787 </Properties>
788 <OrMetadata />
789 </Blob>
790 <BlobPrefix>
791 <Name>dir1/dir2/</Name>
792 </BlobPrefix>
793 <BlobPrefix>
794 <Name>dir1/dir21/</Name>
795 </BlobPrefix>
796 </Blobs>
797 <NextMarker />
798 </EnumerationResults>"#,
799 );
800 let out: ListBlobsOutput = de::from_reader(bs.reader()).expect("must success");
801 println!("{out:?}");
802
803 assert_eq!(
804 out.blobs
805 .blob
806 .iter()
807 .map(|v| v.name.clone())
808 .collect::<Vec<String>>(),
809 [
810 "dir1/2f018bb5-466f-4af1-84fa-2b167374ee06",
811 "dir1/5b9432b2-79c0-48d8-90c2-7d3e153826ed",
812 "dir1/b2d96f8b-d467-40d1-bb11-4632dddbf5b5"
813 ]
814 );
815 assert_eq!(
816 out.blobs
817 .blob
818 .iter()
819 .map(|v| v.properties.content_length)
820 .collect::<Vec<u64>>(),
821 [3485277, 2471869, 1259677]
822 );
823 assert_eq!(
824 out.blobs
825 .blob
826 .iter()
827 .map(|v| v.properties.content_md5.clone())
828 .collect::<Vec<String>>(),
829 [
830 "llJ/+jOlx5GdA1sL7SdKuw==".to_string(),
831 "xmgUltSnopLSJOukgCHFtg==".to_string(),
832 "AxTiFXHwrXKaZC5b7ZRybw==".to_string()
833 ]
834 );
835 assert_eq!(
836 out.blobs
837 .blob
838 .iter()
839 .map(|v| v.properties.last_modified.clone())
840 .collect::<Vec<String>>(),
841 [
842 "Sun, 20 Mar 2022 11:29:03 GMT".to_string(),
843 "Tue, 29 Mar 2022 01:54:07 GMT".to_string(),
844 "Sun, 20 Mar 2022 11:31:57 GMT".to_string()
845 ]
846 );
847 assert_eq!(
848 out.blobs
849 .blob
850 .iter()
851 .map(|v| v.properties.etag.clone())
852 .collect::<Vec<String>>(),
853 [
854 "0x8DA0A64D66790C3".to_string(),
855 "0x8DA112702D88FE4".to_string(),
856 "0x8DA0A653DC82981".to_string()
857 ]
858 );
859 assert_eq!(
860 out.blobs
861 .blob_prefix
862 .iter()
863 .map(|v| v.name.clone())
864 .collect::<Vec<String>>(),
865 ["dir1/dir2/", "dir1/dir21/"]
866 );
867 }
868
869 #[test]
885 fn test_parse_overlapped_lists() {
886 let bs = "<?xml version=\"1.0\" encoding=\"utf-8\"?><EnumerationResults ServiceEndpoint=\"https://test.blob.core.windows.net/\" ContainerName=\"test\"><Prefix>9f7075e1-84d0-45ca-8196-ab9b71a8ef97/x/</Prefix><Delimiter>/</Delimiter><Blobs><Blob><Name>9f7075e1-84d0-45ca-8196-ab9b71a8ef97/x/</Name><Properties><Creation-Time>Thu, 01 Sep 2022 07:26:49 GMT</Creation-Time><Last-Modified>Thu, 01 Sep 2022 07:26:49 GMT</Last-Modified><Etag>0x8DA8BEB55D0EA35</Etag><Content-Length>0</Content-Length><Content-Type>application/octet-stream</Content-Type><Content-Encoding /><Content-Language /><Content-CRC64 /><Content-MD5>1B2M2Y8AsgTpgAmY7PhCfg==</Content-MD5><Cache-Control /><Content-Disposition /><BlobType>BlockBlob</BlobType><AccessTier>Hot</AccessTier><AccessTierInferred>true</AccessTierInferred><LeaseStatus>unlocked</LeaseStatus><LeaseState>available</LeaseState><ServerEncrypted>true</ServerEncrypted></Properties><OrMetadata /></Blob><BlobPrefix><Name>9f7075e1-84d0-45ca-8196-ab9b71a8ef97/x/x/</Name></BlobPrefix><Blob><Name>9f7075e1-84d0-45ca-8196-ab9b71a8ef97/x/y</Name><Properties><Creation-Time>Thu, 01 Sep 2022 07:26:50 GMT</Creation-Time><Last-Modified>Thu, 01 Sep 2022 07:26:50 GMT</Last-Modified><Etag>0x8DA8BEB55D99C08</Etag><Content-Length>0</Content-Length><Content-Type>application/octet-stream</Content-Type><Content-Encoding /><Content-Language /><Content-CRC64 /><Content-MD5>1B2M2Y8AsgTpgAmY7PhCfg==</Content-MD5><Cache-Control /><Content-Disposition /><BlobType>BlockBlob</BlobType><AccessTier>Hot</AccessTier><AccessTierInferred>true</AccessTierInferred><LeaseStatus>unlocked</LeaseStatus><LeaseState>available</LeaseState><ServerEncrypted>true</ServerEncrypted></Properties><OrMetadata /></Blob></Blobs><NextMarker /></EnumerationResults>";
887
888 de::from_reader(Bytes::from(bs).reader()).expect("must success")
889 }
890
891 #[test]
893 fn test_serialize_put_block_list_request() {
894 let req = PutBlockListRequest {
895 latest: vec!["1".to_string(), "2".to_string(), "3".to_string()],
896 };
897
898 let actual = quick_xml::se::to_string(&req).expect("must succeed");
899
900 pretty_assertions::assert_eq!(
901 actual,
902 r#"
903 <BlockList>
904 <Latest>1</Latest>
905 <Latest>2</Latest>
906 <Latest>3</Latest>
907 </BlockList>"#
908 .replace([' ', '\n'], "")
910 .replace('"', """)
912 );
913
914 let bs = "<?xml version=\"1.0\" encoding=\"utf-8\"?>
915 <BlockList>
916 <Latest>1</Latest>
917 <Latest>2</Latest>
918 <Latest>3</Latest>
919 </BlockList>";
920
921 let out: PutBlockListRequest =
922 de::from_reader(Bytes::from(bs).reader()).expect("must success");
923 assert_eq!(
924 out.latest,
925 vec!["1".to_string(), "2".to_string(), "3".to_string()]
926 );
927 }
928}