1use std::fmt;
19use std::fmt::Debug;
20use std::fmt::Formatter;
21use std::sync::Arc;
22use std::time::Duration;
23
24use base64::prelude::BASE64_STANDARD;
25use base64::Engine;
26use bytes::Bytes;
27use constants::X_MS_META_PREFIX;
28use http::header::HeaderName;
29use http::header::CONTENT_LENGTH;
30use http::header::CONTENT_TYPE;
31use http::header::IF_MATCH;
32use http::header::IF_MODIFIED_SINCE;
33use http::header::IF_NONE_MATCH;
34use http::header::IF_UNMODIFIED_SINCE;
35use http::HeaderValue;
36use http::Request;
37use http::Response;
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(
216 IF_MODIFIED_SINCE,
217 format_datetime_into_http_date(if_modified_since),
218 );
219 }
220
221 if let Some(if_unmodified_since) = args.if_unmodified_since() {
222 req = req.header(
223 IF_UNMODIFIED_SINCE,
224 format_datetime_into_http_date(if_unmodified_since),
225 );
226 }
227
228 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
229
230 Ok(req)
231 }
232
233 pub async fn azblob_get_blob(
234 &self,
235 path: &str,
236 range: BytesRange,
237 args: &OpRead,
238 ) -> Result<Response<HttpBody>> {
239 let mut req = self.azblob_get_blob_request(path, range, args)?;
240
241 self.sign(&mut req).await?;
242
243 self.info.http_client().fetch(req).await
244 }
245
246 pub fn azblob_put_blob_request(
247 &self,
248 path: &str,
249 size: Option<u64>,
250 args: &OpWrite,
251 body: Buffer,
252 ) -> Result<Request<Buffer>> {
253 let mut req = Request::put(self.build_path_url(path));
254
255 req = req.header(
256 HeaderName::from_static(constants::X_MS_BLOB_TYPE),
257 "BlockBlob",
258 );
259
260 if let Some(size) = size {
261 req = req.header(CONTENT_LENGTH, size)
262 }
263
264 if let Some(ty) = args.content_type() {
265 req = req.header(CONTENT_TYPE, ty)
266 }
267
268 if args.if_not_exists() {
271 req = req.header(IF_NONE_MATCH, "*");
272 }
273
274 if let Some(v) = args.if_none_match() {
275 req = req.header(IF_NONE_MATCH, v);
276 }
277
278 if let Some(cache_control) = args.cache_control() {
279 req = req.header(constants::X_MS_BLOB_CACHE_CONTROL, cache_control);
280 }
281
282 req = self.insert_sse_headers(req);
284
285 if let Some(user_metadata) = args.user_metadata() {
286 for (key, value) in user_metadata {
287 req = req.header(format!("{X_MS_META_PREFIX}{key}"), value)
288 }
289 }
290
291 let req = req.body(body).map_err(new_request_build_error)?;
293
294 Ok(req)
295 }
296
297 pub async fn azblob_put_blob(
298 &self,
299 path: &str,
300 size: Option<u64>,
301 args: &OpWrite,
302 body: Buffer,
303 ) -> Result<Response<Buffer>> {
304 let mut req = self.azblob_put_blob_request(path, size, args, body)?;
305
306 self.sign(&mut req).await?;
307 self.send(req).await
308 }
309
310 fn azblob_init_appendable_blob_request(
328 &self,
329 path: &str,
330 args: &OpWrite,
331 ) -> Result<Request<Buffer>> {
332 let mut req = Request::put(self.build_path_url(path));
333
334 req = self.insert_sse_headers(req);
336
337 req = req.header(CONTENT_LENGTH, 0);
340 req = req.header(
341 HeaderName::from_static(constants::X_MS_BLOB_TYPE),
342 "AppendBlob",
343 );
344
345 if let Some(ty) = args.content_type() {
346 req = req.header(CONTENT_TYPE, ty)
347 }
348
349 if let Some(cache_control) = args.cache_control() {
350 req = req.header(constants::X_MS_BLOB_CACHE_CONTROL, cache_control);
351 }
352
353 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
354
355 Ok(req)
356 }
357
358 pub async fn azblob_init_appendable_blob(
359 &self,
360 path: &str,
361 args: &OpWrite,
362 ) -> Result<Response<Buffer>> {
363 let mut req = self.azblob_init_appendable_blob_request(path, args)?;
364
365 self.sign(&mut req).await?;
366 self.send(req).await
367 }
368
369 fn azblob_append_blob_request(
381 &self,
382 path: &str,
383 position: u64,
384 size: u64,
385 body: Buffer,
386 ) -> Result<Request<Buffer>> {
387 let url = format!("{}?comp=appendblock", &self.build_path_url(path));
388
389 let mut req = Request::put(&url);
390
391 req = self.insert_sse_headers(req);
393
394 req = req.header(CONTENT_LENGTH, size);
395
396 req = req.header(constants::X_MS_BLOB_CONDITION_APPENDPOS, position);
397
398 let req = req.body(body).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 let req = req.body(body).map_err(new_request_build_error)?;
450
451 Ok(req)
452 }
453
454 pub async fn azblob_put_block(
455 &self,
456 path: &str,
457 block_id: Uuid,
458 size: Option<u64>,
459 args: &OpWrite,
460 body: Buffer,
461 ) -> Result<Response<Buffer>> {
462 let mut req = self.azblob_put_block_request(path, block_id, size, args, body)?;
463
464 self.sign(&mut req).await?;
465 self.send(req).await
466 }
467
468 fn azblob_complete_put_block_list_request(
469 &self,
470 path: &str,
471 block_ids: Vec<Uuid>,
472 args: &OpWrite,
473 ) -> Result<Request<Buffer>> {
474 let url = format!("{}?comp=blocklist", &self.build_path_url(path));
475
476 let req = Request::put(&url);
477
478 let mut req = self.insert_sse_headers(req);
480 if let Some(cache_control) = args.cache_control() {
481 req = req.header(constants::X_MS_BLOB_CACHE_CONTROL, cache_control);
482 }
483
484 let content = quick_xml::se::to_string(&PutBlockListRequest {
485 latest: block_ids
486 .into_iter()
487 .map(|block_id| {
488 let encoded_block_id: String = BASE64_STANDARD.encode(block_id.as_bytes());
489 encoded_block_id
490 })
491 .collect(),
492 })
493 .map_err(new_xml_serialize_error)?;
494
495 req = req.header(CONTENT_LENGTH, content.len());
496
497 let req = req
498 .body(Buffer::from(Bytes::from(content)))
499 .map_err(new_request_build_error)?;
500
501 Ok(req)
502 }
503
504 pub async fn azblob_complete_put_block_list(
505 &self,
506 path: &str,
507 block_ids: Vec<Uuid>,
508 args: &OpWrite,
509 ) -> Result<Response<Buffer>> {
510 let mut req = self.azblob_complete_put_block_list_request(path, block_ids, args)?;
511
512 self.sign(&mut req).await?;
513
514 self.send(req).await
515 }
516
517 pub fn azblob_head_blob_request(&self, path: &str, args: &OpStat) -> Result<Request<Buffer>> {
518 let mut req = Request::head(self.build_path_url(path));
519
520 req = self.insert_sse_headers(req);
522
523 if let Some(if_none_match) = args.if_none_match() {
524 req = req.header(IF_NONE_MATCH, if_none_match);
525 }
526
527 if let Some(if_match) = args.if_match() {
528 req = req.header(IF_MATCH, if_match);
529 }
530
531 let req = req.body(Buffer::new()).map_err(new_request_build_error)?;
532
533 Ok(req)
534 }
535
536 pub async fn azblob_get_blob_properties(
537 &self,
538 path: &str,
539 args: &OpStat,
540 ) -> Result<Response<Buffer>> {
541 let mut req = self.azblob_head_blob_request(path, args)?;
542
543 self.sign(&mut req).await?;
544 self.send(req).await
545 }
546
547 fn azblob_delete_blob_request(&self, path: &str) -> Result<Request<Buffer>> {
548 let req = Request::delete(self.build_path_url(path));
549
550 req.header(CONTENT_LENGTH, 0)
551 .body(Buffer::new())
552 .map_err(new_request_build_error)
553 }
554
555 pub async fn azblob_delete_blob(&self, path: &str) -> Result<Response<Buffer>> {
556 let mut req = self.azblob_delete_blob_request(path)?;
557
558 self.sign(&mut req).await?;
559 self.send(req).await
560 }
561
562 pub async fn azblob_copy_blob(&self, from: &str, to: &str) -> Result<Response<Buffer>> {
563 let source = self.build_path_url(from);
564 let target = self.build_path_url(to);
565
566 let mut req = Request::put(&target)
567 .header(constants::X_MS_COPY_SOURCE, source)
568 .header(CONTENT_LENGTH, 0)
569 .body(Buffer::new())
570 .map_err(new_request_build_error)?;
571
572 self.sign(&mut req).await?;
573 self.send(req).await
574 }
575
576 pub async fn azblob_list_blobs(
577 &self,
578 path: &str,
579 next_marker: &str,
580 delimiter: &str,
581 limit: Option<usize>,
582 ) -> Result<Response<Buffer>> {
583 let p = build_abs_path(&self.root, path);
584 let mut url = QueryPairsWriter::new(&format!("{}/{}", self.endpoint, self.container))
585 .push("restype", "container")
586 .push("comp", "list");
587
588 if !p.is_empty() {
589 url = url.push("prefix", &percent_encode_path(&p));
590 }
591 if let Some(limit) = limit {
592 url = url.push("maxresults", &limit.to_string());
593 }
594 if !delimiter.is_empty() {
595 url = url.push("delimiter", delimiter);
596 }
597 if !next_marker.is_empty() {
598 url = url.push("marker", next_marker);
599 }
600
601 let mut req = Request::get(url.finish())
602 .body(Buffer::new())
603 .map_err(new_request_build_error)?;
604
605 self.sign(&mut req).await?;
606 self.send(req).await
607 }
608
609 pub async fn azblob_batch_delete(&self, paths: &[String]) -> Result<Response<Buffer>> {
610 let url = format!(
611 "{}/{}?restype=container&comp=batch",
612 self.endpoint, self.container
613 );
614
615 let mut multipart = Multipart::new();
616
617 for (idx, path) in paths.iter().enumerate() {
618 let mut req = self.azblob_delete_blob_request(path)?;
619 self.batch_sign(&mut req).await?;
620
621 multipart = multipart.part(
622 MixedPart::from_request(req).part_header("content-id".parse().unwrap(), idx.into()),
623 );
624 }
625
626 let req = Request::post(url);
627 let mut req = multipart.apply(req)?;
628
629 self.sign(&mut req).await?;
630 self.send(req).await
631 }
632}
633
634#[derive(Default, Debug, Serialize, Deserialize)]
636#[serde(default, rename = "BlockList", rename_all = "PascalCase")]
637pub struct PutBlockListRequest {
638 pub latest: Vec<String>,
639}
640
641#[derive(Default, Debug, Deserialize)]
642#[serde(default, rename_all = "PascalCase")]
643pub struct ListBlobsOutput {
644 pub blobs: Blobs,
645 #[serde(rename = "NextMarker")]
646 pub next_marker: Option<String>,
647}
648
649#[derive(Default, Debug, Deserialize)]
650#[serde(default, rename_all = "PascalCase")]
651pub struct Blobs {
652 pub blob: Vec<Blob>,
653 pub blob_prefix: Vec<BlobPrefix>,
654}
655
656#[derive(Default, Debug, Deserialize)]
657#[serde(default, rename_all = "PascalCase")]
658pub struct BlobPrefix {
659 pub name: String,
660}
661
662#[derive(Default, Debug, Deserialize)]
663#[serde(default, rename_all = "PascalCase")]
664pub struct Blob {
665 pub properties: Properties,
666 pub name: String,
667}
668
669#[derive(Default, Debug, Deserialize)]
670#[serde(default, rename_all = "PascalCase")]
671pub struct Properties {
672 #[serde(rename = "Content-Length")]
673 pub content_length: u64,
674 #[serde(rename = "Last-Modified")]
675 pub last_modified: String,
676 #[serde(rename = "Content-MD5")]
677 pub content_md5: String,
678 #[serde(rename = "Content-Type")]
679 pub content_type: String,
680 pub etag: String,
681}
682
683#[cfg(test)]
684mod tests {
685 use bytes::Buf;
686 use bytes::Bytes;
687 use quick_xml::de;
688
689 use super::*;
690
691 #[test]
692 fn test_parse_xml() {
693 let bs = bytes::Bytes::from(
694 r#"
695 <?xml version="1.0" encoding="utf-8"?>
696 <EnumerationResults ServiceEndpoint="https://test.blob.core.windows.net/" ContainerName="myazurebucket">
697 <Prefix>dir1/</Prefix>
698 <Delimiter>/</Delimiter>
699 <Blobs>
700 <Blob>
701 <Name>dir1/2f018bb5-466f-4af1-84fa-2b167374ee06</Name>
702 <Properties>
703 <Creation-Time>Sun, 20 Mar 2022 11:29:03 GMT</Creation-Time>
704 <Last-Modified>Sun, 20 Mar 2022 11:29:03 GMT</Last-Modified>
705 <Etag>0x8DA0A64D66790C3</Etag>
706 <Content-Length>3485277</Content-Length>
707 <Content-Type>application/octet-stream</Content-Type>
708 <Content-Encoding />
709 <Content-Language />
710 <Content-CRC64 />
711 <Content-MD5>llJ/+jOlx5GdA1sL7SdKuw==</Content-MD5>
712 <Cache-Control />
713 <Content-Disposition />
714 <BlobType>BlockBlob</BlobType>
715 <AccessTier>Hot</AccessTier>
716 <AccessTierInferred>true</AccessTierInferred>
717 <LeaseStatus>unlocked</LeaseStatus>
718 <LeaseState>available</LeaseState>
719 <ServerEncrypted>true</ServerEncrypted>
720 </Properties>
721 <OrMetadata />
722 </Blob>
723 <Blob>
724 <Name>dir1/5b9432b2-79c0-48d8-90c2-7d3e153826ed</Name>
725 <Properties>
726 <Creation-Time>Tue, 29 Mar 2022 01:54:07 GMT</Creation-Time>
727 <Last-Modified>Tue, 29 Mar 2022 01:54:07 GMT</Last-Modified>
728 <Etag>0x8DA112702D88FE4</Etag>
729 <Content-Length>2471869</Content-Length>
730 <Content-Type>application/octet-stream</Content-Type>
731 <Content-Encoding />
732 <Content-Language />
733 <Content-CRC64 />
734 <Content-MD5>xmgUltSnopLSJOukgCHFtg==</Content-MD5>
735 <Cache-Control />
736 <Content-Disposition />
737 <BlobType>BlockBlob</BlobType>
738 <AccessTier>Hot</AccessTier>
739 <AccessTierInferred>true</AccessTierInferred>
740 <LeaseStatus>unlocked</LeaseStatus>
741 <LeaseState>available</LeaseState>
742 <ServerEncrypted>true</ServerEncrypted>
743 </Properties>
744 <OrMetadata />
745 </Blob>
746 <Blob>
747 <Name>dir1/b2d96f8b-d467-40d1-bb11-4632dddbf5b5</Name>
748 <Properties>
749 <Creation-Time>Sun, 20 Mar 2022 11:31:57 GMT</Creation-Time>
750 <Last-Modified>Sun, 20 Mar 2022 11:31:57 GMT</Last-Modified>
751 <Etag>0x8DA0A653DC82981</Etag>
752 <Content-Length>1259677</Content-Length>
753 <Content-Type>application/octet-stream</Content-Type>
754 <Content-Encoding />
755 <Content-Language />
756 <Content-CRC64 />
757 <Content-MD5>AxTiFXHwrXKaZC5b7ZRybw==</Content-MD5>
758 <Cache-Control />
759 <Content-Disposition />
760 <BlobType>BlockBlob</BlobType>
761 <AccessTier>Hot</AccessTier>
762 <AccessTierInferred>true</AccessTierInferred>
763 <LeaseStatus>unlocked</LeaseStatus>
764 <LeaseState>available</LeaseState>
765 <ServerEncrypted>true</ServerEncrypted>
766 </Properties>
767 <OrMetadata />
768 </Blob>
769 <BlobPrefix>
770 <Name>dir1/dir2/</Name>
771 </BlobPrefix>
772 <BlobPrefix>
773 <Name>dir1/dir21/</Name>
774 </BlobPrefix>
775 </Blobs>
776 <NextMarker />
777 </EnumerationResults>"#,
778 );
779 let out: ListBlobsOutput = de::from_reader(bs.reader()).expect("must success");
780 println!("{out:?}");
781
782 assert_eq!(
783 out.blobs
784 .blob
785 .iter()
786 .map(|v| v.name.clone())
787 .collect::<Vec<String>>(),
788 [
789 "dir1/2f018bb5-466f-4af1-84fa-2b167374ee06",
790 "dir1/5b9432b2-79c0-48d8-90c2-7d3e153826ed",
791 "dir1/b2d96f8b-d467-40d1-bb11-4632dddbf5b5"
792 ]
793 );
794 assert_eq!(
795 out.blobs
796 .blob
797 .iter()
798 .map(|v| v.properties.content_length)
799 .collect::<Vec<u64>>(),
800 [3485277, 2471869, 1259677]
801 );
802 assert_eq!(
803 out.blobs
804 .blob
805 .iter()
806 .map(|v| v.properties.content_md5.clone())
807 .collect::<Vec<String>>(),
808 [
809 "llJ/+jOlx5GdA1sL7SdKuw==".to_string(),
810 "xmgUltSnopLSJOukgCHFtg==".to_string(),
811 "AxTiFXHwrXKaZC5b7ZRybw==".to_string()
812 ]
813 );
814 assert_eq!(
815 out.blobs
816 .blob
817 .iter()
818 .map(|v| v.properties.last_modified.clone())
819 .collect::<Vec<String>>(),
820 [
821 "Sun, 20 Mar 2022 11:29:03 GMT".to_string(),
822 "Tue, 29 Mar 2022 01:54:07 GMT".to_string(),
823 "Sun, 20 Mar 2022 11:31:57 GMT".to_string()
824 ]
825 );
826 assert_eq!(
827 out.blobs
828 .blob
829 .iter()
830 .map(|v| v.properties.etag.clone())
831 .collect::<Vec<String>>(),
832 [
833 "0x8DA0A64D66790C3".to_string(),
834 "0x8DA112702D88FE4".to_string(),
835 "0x8DA0A653DC82981".to_string()
836 ]
837 );
838 assert_eq!(
839 out.blobs
840 .blob_prefix
841 .iter()
842 .map(|v| v.name.clone())
843 .collect::<Vec<String>>(),
844 ["dir1/dir2/", "dir1/dir21/"]
845 );
846 }
847
848 #[test]
864 fn test_parse_overlapped_lists() {
865 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>";
866
867 de::from_reader(Bytes::from(bs).reader()).expect("must success")
868 }
869
870 #[test]
872 fn test_serialize_put_block_list_request() {
873 let req = PutBlockListRequest {
874 latest: vec!["1".to_string(), "2".to_string(), "3".to_string()],
875 };
876
877 let actual = quick_xml::se::to_string(&req).expect("must succeed");
878
879 pretty_assertions::assert_eq!(
880 actual,
881 r#"
882 <BlockList>
883 <Latest>1</Latest>
884 <Latest>2</Latest>
885 <Latest>3</Latest>
886 </BlockList>"#
887 .replace([' ', '\n'], "")
889 .replace('"', """)
891 );
892
893 let bs = "<?xml version=\"1.0\" encoding=\"utf-8\"?>
894 <BlockList>
895 <Latest>1</Latest>
896 <Latest>2</Latest>
897 <Latest>3</Latest>
898 </BlockList>";
899
900 let out: PutBlockListRequest =
901 de::from_reader(Bytes::from(bs).reader()).expect("must success");
902 assert_eq!(
903 out.latest,
904 vec!["1".to_string(), "2".to_string(), "3".to_string()]
905 );
906 }
907}