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