azure_storage_blobs/container/operations/
list_blobs.rs

1use 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                // This code will construct the "include" query pair
47                // attribute. It only allocates a Vec of references ('static
48                // str) and, finally, a single string.
49                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}