azure_storage_blobs/container/operations/
list_blobs.rs1use crate::prelude::*;
2use azure_core::{
3 error::Error,
4 headers::{date_from_headers, request_id_from_headers, Headers},
5 prelude::*,
6 Method, Pageable, RequestId, Response as AzureResponse,
7};
8use time::OffsetDateTime;
9
10operation! {
11 #[stream]
12 ListBlobs,
13 client: ContainerClient,
14 ?prefix: Prefix,
15 ?delimiter: Delimiter,
16 ?max_results: MaxResults,
17 ?include_snapshots: bool,
18 ?include_metadata: bool,
19 ?include_uncommitted_blobs: bool,
20 ?include_copy: bool,
21 ?include_deleted: bool,
22 ?include_tags: bool,
23 ?include_versions: bool,
24 ?marker: NextMarker,
25}
26
27impl ListBlobsBuilder {
28 pub fn into_stream(self) -> Pageable<ListBlobsResponse, Error> {
29 let make_request = move |continuation: Option<NextMarker>| {
30 let this = self.clone();
31 let mut ctx = self.context.clone();
32 async move {
33 let mut url = this.client.url()?;
34
35 url.query_pairs_mut().append_pair("restype", "container");
36 url.query_pairs_mut().append_pair("comp", "list");
37
38 if let Some(next_marker) = continuation.or(this.marker) {
39 next_marker.append_to_url_query(&mut url);
40 }
41
42 this.prefix.append_to_url_query(&mut url);
43 this.delimiter.append_to_url_query(&mut url);
44 this.max_results.append_to_url_query(&mut url);
45
46 let mut optional_includes = Vec::new();
50 if this.include_snapshots.unwrap_or(false) {
51 optional_includes.push("snapshots");
52 }
53 if this.include_metadata.unwrap_or(false) {
54 optional_includes.push("metadata");
55 }
56 if this.include_uncommitted_blobs.unwrap_or(false) {
57 optional_includes.push("uncommittedblobs");
58 }
59 if this.include_copy.unwrap_or(false) {
60 optional_includes.push("copy");
61 }
62 if this.include_deleted.unwrap_or(false) {
63 optional_includes.push("deleted");
64 }
65 if this.include_tags.unwrap_or(false) {
66 optional_includes.push("tags");
67 }
68 if this.include_versions.unwrap_or(false) {
69 optional_includes.push("versions");
70 }
71 if !optional_includes.is_empty() {
72 url.query_pairs_mut()
73 .append_pair("include", &optional_includes.join(","));
74 }
75
76 let mut request =
77 ContainerClient::finalize_request(url, Method::Get, Headers::new(), None)?;
78
79 let response = this.client.send(&mut ctx, &mut request).await?;
80
81 ListBlobsResponse::try_from(response).await
82 }
83 };
84
85 Pageable::new(make_request)
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct ListBlobsResponse {
91 pub prefix: Option<String>,
92 pub max_results: Option<u32>,
93 pub delimiter: Option<String>,
94 pub next_marker: Option<NextMarker>,
95 pub blobs: Blobs,
96 pub request_id: RequestId,
97 pub date: OffsetDateTime,
98}
99
100#[derive(Debug, Clone, PartialEq, Deserialize)]
101#[serde(rename_all = "PascalCase")]
102struct ListBlobsResponseInternal {
103 pub prefix: Option<String>,
104 pub max_results: Option<u32>,
105 pub delimiter: Option<String>,
106 pub next_marker: Option<String>,
107 pub blobs: Blobs,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Default)]
111#[serde(rename_all = "PascalCase")]
112pub struct Blobs {
113 #[serde(rename = "$value", default)]
114 pub items: Vec<BlobItem>,
115}
116
117impl Blobs {
118 pub fn blobs(&self) -> impl Iterator<Item = &Blob> {
119 self.items.iter().filter_map(|item| match item {
120 BlobItem::Blob(blob) => Some(blob),
121 BlobItem::BlobPrefix(_) => None,
122 })
123 }
124
125 pub fn prefixes(&self) -> impl Iterator<Item = &BlobPrefix> {
126 self.items.iter().filter_map(|item| match item {
127 BlobItem::BlobPrefix(prefix) => Some(prefix),
128 BlobItem::Blob(_) => None,
129 })
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
134#[serde(rename_all = "PascalCase")]
135#[allow(clippy::large_enum_variant)]
136pub enum BlobItem {
137 Blob(Blob),
138 BlobPrefix(BlobPrefix),
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
142#[serde(rename_all = "PascalCase")]
143pub struct BlobPrefix {
144 pub name: String,
145}
146
147impl ListBlobsResponse {
148 pub async fn try_from(response: AzureResponse) -> azure_core::Result<Self> {
149 let (_, headers, body) = response.deconstruct();
150 let list_blobs_response_internal: ListBlobsResponseInternal = body.xml().await?;
151
152 let next_marker = match list_blobs_response_internal.next_marker {
153 Some(ref nm) if nm.is_empty() => None,
154 Some(nm) => Some(nm.into()),
155 None => None,
156 };
157
158 Ok(Self {
159 request_id: request_id_from_headers(&headers)?,
160 date: date_from_headers(&headers)?,
161 prefix: list_blobs_response_internal.prefix,
162 max_results: list_blobs_response_internal.max_results,
163 delimiter: list_blobs_response_internal.delimiter,
164 blobs: list_blobs_response_internal.blobs,
165 next_marker,
166 })
167 }
168}
169
170impl Continuable for ListBlobsResponse {
171 type Continuation = NextMarker;
172 fn continuation(&self) -> Option<Self::Continuation> {
173 self.next_marker.clone()
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use azure_core::xml::read_xml;
180 use bytes::Bytes;
181
182 use super::*;
183
184 #[test]
185 fn deserde_azure() {
186 const S: &str = "<?xml version=\"1.0\" encoding=\"utf-8\"?>
187<EnumerationResults ServiceEndpoint=\"https://azureskdforrust.blob.core.windows.net/\" ContainerName=\"osa2\">
188 <Blobs>
189 <Blob>
190 <Name>blob0.txt</Name>
191 <Properties>
192 <Creation-Time>Thu, 01 Jul 2021 10:44:59 GMT</Creation-Time>
193 <Last-Modified>Thu, 01 Jul 2021 10:44:59 GMT</Last-Modified>
194 <Expiry-Time>Thu, 07 Jul 2022 14:38:48 GMT</Expiry-Time>
195 <Etag>0x8D93C7D4629C227</Etag>
196 <Content-Length>8</Content-Length>
197 <Content-Type>text/plain</Content-Type>
198 <Content-Encoding />
199 <Content-Language />
200 <Content-CRC64 />
201 <Content-MD5>rvr3UC1SmUw7AZV2NqPN0g==</Content-MD5>
202 <Cache-Control />
203 <Content-Disposition />
204 <BlobType>BlockBlob</BlobType>
205 <AccessTier>Hot</AccessTier>
206 <AccessTierInferred>true</AccessTierInferred>
207 <LeaseStatus>unlocked</LeaseStatus>
208 <LeaseState>available</LeaseState>
209 <ServerEncrypted>true</ServerEncrypted>
210 </Properties>
211 <Metadata><userkey>uservalue</userkey></Metadata>
212 <OrMetadata />
213 </Blob>
214 <Blob>
215 <Name>blob1.txt</Name>
216 <Properties>
217 <Creation-Time>Thu, 01 Jul 2021 10:44:59 GMT</Creation-Time>
218 <Last-Modified>Thu, 01 Jul 2021 10:44:59 GMT</Last-Modified>
219 <Etag>0x8D93C7D463004D6</Etag>
220 <Content-Length>8</Content-Length>
221 <Content-Type>text/plain</Content-Type>
222 <Content-Encoding />
223 <Content-Language />
224 <Content-CRC64 />
225 <Content-MD5>rvr3UC1SmUw7AZV2NqPN0g==</Content-MD5>
226 <Cache-Control />
227 <Content-Disposition />
228 <BlobType>BlockBlob</BlobType>
229 <AccessTier>Hot</AccessTier>
230 <AccessTierInferred>true</AccessTierInferred>
231 <LeaseStatus>unlocked</LeaseStatus>
232 <LeaseState>available</LeaseState>
233 <ServerEncrypted>true</ServerEncrypted>
234 </Properties>
235 <OrMetadata />
236 </Blob>
237 <Blob>
238 <Name>blob2.txt</Name>
239 <Properties>
240 <Creation-Time>Thu, 01 Jul 2021 10:44:59 GMT</Creation-Time>
241 <Last-Modified>Thu, 01 Jul 2021 10:44:59 GMT</Last-Modified>
242 <Etag>0x8D93C7D4636478A</Etag>
243 <Content-Length>8</Content-Length>
244 <Content-Type>text/plain</Content-Type>
245 <Content-Encoding />
246 <Content-Language />
247 <Content-CRC64 />
248 <Content-MD5>rvr3UC1SmUw7AZV2NqPN0g==</Content-MD5>
249 <Cache-Control />
250 <Content-Disposition />
251 <BlobType>BlockBlob</BlobType>
252 <AccessTier>Hot</AccessTier>
253 <AccessTierInferred>true</AccessTierInferred>
254 <LeaseStatus>unlocked</LeaseStatus>
255 <LeaseState>available</LeaseState>
256 <ServerEncrypted>true</ServerEncrypted>
257 </Properties>
258 <OrMetadata />
259 </Blob>
260 </Blobs>
261 <NextMarker />
262</EnumerationResults>";
263
264 let bytes = Bytes::from(S);
265 let _list_blobs_response_internal: ListBlobsResponseInternal = read_xml(&bytes).unwrap();
266 }
267
268 #[test]
269 fn deserde_azurite() {
270 const S: &str = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
271<EnumerationResults ServiceEndpoint=\"http://127.0.0.1:10000/devstoreaccount1\" ContainerName=\"osa2\">
272 <Prefix/>
273 <Marker/>
274 <MaxResults>5000</MaxResults>
275 <Delimiter/>
276 <Blobs>
277 <Blob>
278 <Name>blob0.txt</Name>
279 <Properties>
280 <Creation-Time>Thu, 01 Jul 2021 10:45:02 GMT</Creation-Time>
281 <Last-Modified>Thu, 01 Jul 2021 10:45:02 GMT</Last-Modified>
282 <Etag>0x228281B5D517B20</Etag>
283 <Content-Length>8</Content-Length>
284 <Content-Type>text/plain</Content-Type>
285 <Content-MD5>rvr3UC1SmUw7AZV2NqPN0g==</Content-MD5>
286 <BlobType>BlockBlob</BlobType>
287 <LeaseStatus>unlocked</LeaseStatus>
288 <LeaseState>available</LeaseState>
289 <ServerEncrypted>true</ServerEncrypted>
290 <AccessTier>Hot</AccessTier>
291 <AccessTierInferred>true</AccessTierInferred>
292 <AccessTierChangeTime>Thu, 01 Jul 2021 10:45:02 GMT</AccessTierChangeTime>
293 </Properties>
294 </Blob>
295 <Blob>
296 <Name>blob1.txt</Name>
297 <Properties>
298 <Creation-Time>Thu, 01 Jul 2021 10:45:02 GMT</Creation-Time>
299 <Last-Modified>Thu, 01 Jul 2021 10:45:02 GMT</Last-Modified>
300 <Etag>0x1DD959381A8A860</Etag>
301 <Content-Length>8</Content-Length>
302 <Content-Type>text/plain</Content-Type>
303 <Content-MD5>rvr3UC1SmUw7AZV2NqPN0g==</Content-MD5>
304 <BlobType>BlockBlob</BlobType>
305 <LeaseStatus>unlocked</LeaseStatus>
306 <LeaseState>available</LeaseState>
307 <ServerEncrypted>true</ServerEncrypted>
308 <AccessTier>Hot</AccessTier>
309 <AccessTierInferred>true</AccessTierInferred>
310 <AccessTierChangeTime>Thu, 01 Jul 2021 10:45:02 GMT</AccessTierChangeTime>
311 </Properties>
312 </Blob>
313 <Blob>
314 <Name>blob2.txt</Name>
315 <Properties>
316 <Creation-Time>Thu, 01 Jul 2021 10:45:02 GMT</Creation-Time>
317 <Last-Modified>Thu, 01 Jul 2021 10:45:02 GMT</Last-Modified>
318 <Etag>0x1FBE9C9B0C7B650</Etag>
319 <Content-Length>8</Content-Length>
320 <Content-Type>text/plain</Content-Type>
321 <Content-MD5>rvr3UC1SmUw7AZV2NqPN0g==</Content-MD5>
322 <BlobType>BlockBlob</BlobType>
323 <LeaseStatus>unlocked</LeaseStatus>
324 <LeaseState>available</LeaseState>
325 <ServerEncrypted>true</ServerEncrypted>
326 <AccessTier>Hot</AccessTier>
327 <AccessTierInferred>true</AccessTierInferred>
328 <AccessTierChangeTime>Thu, 01 Jul 2021 10:45:02 GMT</AccessTierChangeTime>
329 </Properties>
330 </Blob>
331 </Blobs>
332 <NextMarker/>
333</EnumerationResults>";
334
335 let bytes = Bytes::from(S);
336 let _list_blobs_response_internal: ListBlobsResponseInternal = read_xml(&bytes).unwrap();
337 }
338
339 #[test]
340 fn deserde_properties_with_non_existent_field() {
341 const XML: &str = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
342 <EnumerationResults ServiceEndpoint=\"http://127.0.0.1:10000/devstoreaccount1\" ContainerName=\"temp\">
343 <Prefix>b39bc5c9-0f31-459c-a271-828467105470/</Prefix>
344 <Marker/>
345 <MaxResults>5000</MaxResults>
346 <Delimiter/>
347 <Blobs>
348 <Blob>
349 <Name>b39bc5c9-0f31-459c-a271-828467105470/corrupted_data_2020-01-02T03_04_05.json</Name>
350 <Properties>
351 <Creation-Time>Mon, 02 Oct 2023 20:00:31 GMT</Creation-Time>
352 <Last-Modified>Mon, 02 Oct 2023 20:00:31 GMT</Last-Modified>
353 <Etag>0x23D9DB658CF7480</Etag>
354 <Content-Length>0</Content-Length>
355 <Content-Type>application/octet-stream</Content-Type>
356 <Content-Encoding/>
357 <Content-Language/>
358 <Content-CRC64/>
359 <Content-MD5/>
360 <Cache-Control/>
361 <Content-Disposition/>
362 <BlobType>BlockBlob</BlobType>
363 <AccessTier>Hot</AccessTier>
364 <AccessTierInferred>true</AccessTierInferred>
365 <LeaseStatus>unlocked</LeaseStatus>
366 <LeaseState>available</LeaseState>
367 <ServerEncrypted>true</ServerEncrypted>
368 <ResourceType>file</ResourceType>
369 <NotRealProperty>notRealValue</NotRealProperty>
370 </Properties>
371 </Blob>
372 </Blobs>
373 <NextMarker/>
374 </EnumerationResults>";
375
376 let bytes = Bytes::from(XML);
377 let _list_blobs_response_internal: ListBlobsResponseInternal = read_xml(&bytes).unwrap();
378 }
379
380 #[test]
381 fn deserde_azurite_without_server_encrypted() {
382 const S: &str = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
383 <EnumerationResults ServiceEndpoint=\"http://127.0.0.1:10000/devstoreaccount1\" ContainerName=\"temp\">
384 <Prefix>b39bc5c9-0f31-459c-a271-828467105470/</Prefix>
385 <Marker/>
386 <MaxResults>5000</MaxResults>
387 <Delimiter/>
388 <Blobs>
389 <Blob>
390 <Name>b39bc5c9-0f31-459c-a271-828467105470/corrupted_data_2020-01-02T03_04_05.json</Name>
391 <Properties>
392 <Creation-Time>Sat, 18 Feb 2023 22:39:00 GMT</Creation-Time>
393 <Last-Modified>Sat, 18 Feb 2023 22:39:00 GMT</Last-Modified>
394 <Etag>0x23D9DB658CF7480</Etag>
395 <Content-Length>64045</Content-Length>
396 <Content-Type>application/octet-stream</Content-Type>
397 <BlobType>BlockBlob</BlobType>
398 <LeaseStatus>unlocked</LeaseStatus>
399 <LeaseState>available</LeaseState>
400 <AccessTier>Hot</AccessTier>
401 <AccessTierInferred>true</AccessTierInferred>
402 </Properties>
403 </Blob>
404 </Blobs>
405 <NextMarker/>
406 </EnumerationResults>";
407
408 let bytes = Bytes::from(S);
409 let _list_blobs_response_internal: ListBlobsResponseInternal = read_xml(&bytes).unwrap();
410 }
411
412 #[test]
413 fn parse_xml_with_blob_prefix() {
414 const XML: &[u8] = br#"<?xml version="1.0" encoding="utf-8"?>
415 <EnumerationResults ServiceEndpoint="https://sisuautomatedtest.blob.core.windows.net/" ContainerName="lowlatencyrequests">
416 <Prefix>get-most-recent-key-5/</Prefix>
417 <Delimiter>/</Delimiter>
418 <Blobs>
419 <Blob>
420 <Name>get-most-recent-key-5/2021-08-04-testfile1</Name>
421 <Properties>
422 <Creation-Time>Tue, 13 Sep 2022 08:20:48 GMT</Creation-Time>
423 <Last-Modified>Tue, 13 Sep 2022 08:20:48 GMT</Last-Modified>
424 <Etag>0x8DA9560DD170CFD</Etag>
425 <Content-Length>19</Content-Length>
426 <Content-Type>text/plain</Content-Type>
427 <Content-Encoding />
428 <Content-Language />
429 <Content-CRC64 />
430 <Content-MD5>3X/+gWTy92gIJFXx57gLYA==</Content-MD5>
431 <Cache-Control />
432 <Content-Disposition />
433 <BlobType>BlockBlob</BlobType>
434 <AccessTier>Hot</AccessTier>
435 <AccessTierInferred>true</AccessTierInferred>
436 <LeaseStatus>unlocked</LeaseStatus>
437 <LeaseState>available</LeaseState>
438 <ServerEncrypted>true</ServerEncrypted>
439 </Properties>
440 <OrMetadata />
441 </Blob>
442 <BlobPrefix>
443 <Name>get-most-recent-key-5/2021-08-04T21:48:48.592953Z-15839722113750148182/</Name>
444 </BlobPrefix>
445 <Blob>
446 <Name>get-most-recent-key-5/2021-09-04-testfile2</Name>
447 <Properties>
448 <Creation-Time>Tue, 13 Sep 2022 08:07:01 GMT</Creation-Time>
449 <Last-Modified>Tue, 13 Sep 2022 08:19:21 GMT</Last-Modified>
450 <Etag>0x8DA9560A916932D</Etag>
451 <Content-Length>19</Content-Length>
452 <Content-Type>text/plain</Content-Type>
453 <Content-Encoding />
454 <Content-Language />
455 <Content-CRC64 />
456 <Content-MD5>b0CPJB6eDfKUzzW7dlboKQ==</Content-MD5>
457 <Cache-Control />
458 <Content-Disposition />
459 <BlobType>BlockBlob</BlobType>
460 <AccessTier>Hot</AccessTier>
461 <AccessTierInferred>true</AccessTierInferred>
462 <LeaseStatus>unlocked</LeaseStatus>
463 <LeaseState>available</LeaseState>
464 <ServerEncrypted>true</ServerEncrypted>
465 </Properties>
466 <OrMetadata />
467 </Blob>
468 <Blob>
469 <Name>get-most-recent-key-5/2022-08-04-testfile3</Name>
470 <Properties>
471 <Creation-Time>Tue, 13 Sep 2022 08:07:01 GMT</Creation-Time>
472 <Last-Modified>Tue, 13 Sep 2022 08:19:21 GMT</Last-Modified>
473 <Etag>0x8DA9560A91F9296</Etag>
474 <Content-Length>34</Content-Length>
475 <Content-Type>text/plain</Content-Type>
476 <Content-Encoding />
477 <Content-Language />
478 <Content-CRC64 />
479 <Content-MD5>1F1MssyZOvhY4OZevHWEsw==</Content-MD5>
480 <Cache-Control />
481 <Content-Disposition />
482 <BlobType>BlockBlob</BlobType>
483 <AccessTier>Hot</AccessTier>
484 <AccessTierInferred>true</AccessTierInferred>
485 <LeaseStatus>unlocked</LeaseStatus>
486 <LeaseState>available</LeaseState>
487 <ServerEncrypted>true</ServerEncrypted>
488 </Properties>
489 <OrMetadata />
490 </Blob>
491 </Blobs>
492 <NextMarker />
493 </EnumerationResults>"#;
494
495 let _list_blobs_response_internal: ListBlobsResponseInternal = read_xml(XML).unwrap();
496 }
497}