azure_storage_blobs/blob/operations/
get_blob.rs

1use crate::prelude::*;
2use azure_core::{
3    error::Error, headers::*, prelude::*, Pageable, RequestId, Response as AzureResponse,
4    ResponseBody,
5};
6use time::OffsetDateTime;
7
8const DEFAULT_CHUNK_SIZE: u64 = 0x1000 * 0x1000;
9
10operation! {
11    #[stream]
12    GetBlob,
13    client: BlobClient,
14    ?range: Range,
15    ?blob_versioning: BlobVersioning,
16    ?lease_id: LeaseId,
17    ?chunk_size: u64,
18    ?encryption_key: CPKInfo,
19    ?if_modified_since: IfModifiedSinceCondition,
20    ?if_match: IfMatchCondition,
21    ?if_tags: IfTags,
22}
23
24impl GetBlobBuilder {
25    pub fn into_stream(self) -> Pageable<GetBlobResponse, Error> {
26        let make_request = move |continuation: Option<Range>| {
27            let this = self.clone();
28            let mut ctx = self.context.clone();
29            async move {
30                let mut url = this.client.url()?;
31
32                let range = match continuation {
33                    Some(range) => range,
34                    None => initial_range(
35                        this.chunk_size.unwrap_or(DEFAULT_CHUNK_SIZE),
36                        this.range.clone(),
37                    ),
38                };
39
40                this.blob_versioning.append_to_url_query(&mut url);
41
42                let mut headers = Headers::new();
43                for (name, value) in range.as_headers() {
44                    headers.insert(name, value);
45                }
46
47                headers.add(this.lease_id);
48                headers.add(this.encryption_key.as_ref());
49                headers.add(this.if_modified_since);
50                headers.add(this.if_match.clone());
51                headers.add(this.if_tags.clone());
52
53                let mut request =
54                    BlobClient::finalize_request(url, azure_core::Method::Get, headers, None)?;
55
56                let response = this.client.send(&mut ctx, &mut request).await?;
57
58                GetBlobResponse::try_from(this, response)
59            }
60        };
61        Pageable::new(make_request)
62    }
63}
64
65#[derive(Debug)]
66pub struct GetBlobResponse {
67    pub request_id: RequestId,
68    pub blob: Blob,
69    pub data: ResponseBody,
70    pub date: OffsetDateTime,
71    pub content_range: Option<Range>,
72    pub remaining_range: Option<Range>,
73}
74
75impl GetBlobResponse {
76    fn try_from(request: GetBlobBuilder, response: AzureResponse) -> azure_core::Result<Self> {
77        let headers = response.headers();
78
79        let request_id = request_id_from_headers(headers)?;
80        let date = date_from_headers(headers)?;
81
82        let content_range = headers.get_optional_as(&CONTENT_RANGE)?;
83
84        let remaining_range = remaining_range(
85            request.chunk_size.unwrap_or(DEFAULT_CHUNK_SIZE),
86            request.range,
87            content_range,
88        );
89        let blob = Blob::from_headers(request.client.blob_name(), headers)?;
90        let data = response.into_body();
91
92        Ok(Self {
93            request_id,
94            blob,
95            data,
96            date,
97            content_range: content_range.map(|cr| Range::new(cr.start(), cr.end())),
98            remaining_range,
99        })
100    }
101}
102
103impl Continuable for GetBlobResponse {
104    type Continuation = Range;
105    fn continuation(&self) -> Option<Self::Continuation> {
106        self.remaining_range.clone()
107    }
108}
109
110// calculate the first Range for use at the beginning of the Pageable.
111fn initial_range(chunk_size: u64, request_range: Option<Range>) -> Range {
112    match request_range {
113        Some(Range::Range(x)) => {
114            let len = std::cmp::min(x.end - x.start, chunk_size);
115            (x.start..x.start + len).into()
116        }
117        Some(Range::RangeFrom(x)) => (x.start..x.start + chunk_size).into(),
118        None => Range::new(0, chunk_size),
119    }
120}
121
122// After each request, calculate how much data is left to be read based on the
123// requested chunk size, requested range, and Content-Range header from the response.
124//
125// The Content-Range response is authoritative for the current size of the blob,
126// which we use that to determine the next chunk size.  If the Content-Range is
127// missing from the response, we assume the response had the entire blob.
128//
129// If the Content-Range indicates the response was at the end of the blob or
130// user's requested slice, we return None to indicate the response is complete.
131//
132// The remaining range is calculated from immediately after the response until
133// the end of the requested range or chunk size, which ever is smaller.
134fn remaining_range(
135    chunk_size: u64,
136    base_range: Option<Range>,
137    content_range: Option<ContentRange>,
138) -> Option<Range> {
139    // if there was no content range in the response, assume the entire blob was
140    // returned.
141    let content_range = content_range?;
142
143    // if the next byte is at or past the total length, then we're done.
144    if content_range.end() + 1 >= content_range.total_length() {
145        return None;
146    }
147
148    // if the user didn't specify a range, assume the entire size
149    let requested_range = base_range.unwrap_or_else(|| Range::new(0, content_range.total_length()));
150
151    // if the response said the end of the blob was downloaded, we're done
152    // Note, we add + 1, as we don't need to re-fetch the last
153    // byte of the previous request.
154    let after = content_range.end() + 1;
155
156    let remaining_size = match requested_range {
157        Range::Range(x) => {
158            if after >= x.end {
159                return None;
160            }
161            x.end - after
162        }
163        // no requested end
164        Range::RangeFrom(_) => after,
165    };
166
167    let size = std::cmp::min(remaining_size, chunk_size);
168
169    Some(Range::new(after, after + size))
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_initial_range() -> azure_core::Result<()> {
178        let result = initial_range(3, Some(Range::new(0, 10)));
179        let expected = Range::new(0, 3);
180        assert_eq!(result, expected);
181
182        let result = initial_range(3, Some(Range::new(3, 10)));
183        let expected = Range::new(3, 6);
184        assert_eq!(result, expected);
185
186        let result = initial_range(3, None);
187        let expected = Range::new(0, 3);
188        assert_eq!(result, expected);
189        Ok(())
190    }
191    #[test]
192    fn test_remaining_range() -> azure_core::Result<()> {
193        let result = remaining_range(3, None, None);
194        assert!(result.is_none());
195
196        let result = remaining_range(3, Some(Range::new(0, 10)), None);
197        assert!(result.is_none());
198
199        let result = remaining_range(
200            3,
201            Some(Range::new(0, 10)),
202            Some(ContentRange::new(0, 3, 10)),
203        );
204        assert_eq!(result, Some(Range::new(4, 7)));
205
206        let result = remaining_range(
207            3,
208            Some(Range::new(0, 10)),
209            Some(ContentRange::new(0, 10, 10)),
210        );
211        assert!(result.is_none());
212
213        let result = remaining_range(3, None, Some(ContentRange::new(0, 10, 10)));
214        assert!(result.is_none());
215
216        let result = remaining_range(3, None, Some(ContentRange::new(0, 10, 20)));
217        assert_eq!(result, Some(Range::new(11, 14)));
218
219        let result = remaining_range(
220            20,
221            Some(Range::new(5, 15)),
222            Some(ContentRange::new(5, 14, 20)),
223        );
224        assert_eq!(result, None);
225
226        Ok(())
227    }
228}