backblaze_b2_client/
simple_client.rs

1use base64::{engine::general_purpose, Engine as _};
2use reqwest::{
3    header::{HeaderMap, HeaderName, HeaderValue},
4    Method, RequestBuilder, Response,
5};
6use serde::de::DeserializeOwned;
7use serde_json::json;
8use std::{collections::HashMap, num::NonZeroU16, str::FromStr};
9
10use crate::{
11    definitions::{
12        bodies::{
13            B2CopyFileBody, B2CopyPartBody, B2CreateBucketBody, B2CreateKeyBody,
14            B2DeleteFileVersionBody, B2FinishLargeFileBody, B2GetDownloadAuthorizationBody,
15            B2ListBucketsBody, B2StartLargeFileUploadBody, B2UpdateBucketBody,
16            B2UpdateFileLegalHoldBodyResponse, B2UpdateFileRetentionBody,
17        },
18        headers::{B2UploadFileHeaders, B2UploadPartHeaders},
19        query_params::{
20            B2DownloadFileQueryParameters, B2ListFileNamesQueryParameters,
21            B2ListFileVersionsQueryParameters, B2ListKeysParameters, B2ListPartsQueryParameters,
22            B2ListUnfinishedLargeFilesQueryParameters,
23        },
24        responses::{
25            B2AuthData, B2BucketNotificationRulesResponseBody, B2CancelLargeFileResponse,
26            B2DeleteFileVersionResponse, B2FilePart, B2GetDownloadAuthorizationBodyResponse,
27            B2GetUploadPartUrlResponse, B2GetUploadUrlResponse, B2ListBucketsResponse,
28            B2ListFileVersionsResponse, B2ListFilesResponse, B2ListKeysResponse,
29            B2ListPartsResponse, B2ListUnfinishedLargeFilesResponse, B2UpdateFileRetentionResponse,
30        },
31        shared::{
32            B2AppKey, B2Bucket, B2DownloadFileContent, B2Endpoint, B2File, B2FileDownloadDetails,
33            B2KeyCapability,
34        },
35    },
36    error::{B2Error, B2RequestError},
37    util::{B2FileStream, IntoHeaderMap, WriteLockArc},
38};
39
40use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
41
42const ENCODE_SET: &AsciiSet = &CONTROLS
43    .add(b' ')
44    .add(b'"')
45    .add(b'#')
46    .add(b'<')
47    .add(b'>')
48    .add(b'[')
49    .add(b']')
50    .add(b'{')
51    .add(b'}')
52    .add(b'|')
53    .add(b'\\')
54    .add(b'^')
55    .add(b'%')
56    .add(b'`');
57
58#[derive(Clone, Debug)]
59pub struct B2SimpleClient {
60    client: reqwest::Client,
61    auth_data: WriteLockArc<B2AuthData>,
62}
63
64impl B2SimpleClient {
65    pub async fn new<S: AsRef<str>, K: AsRef<str>>(
66        key_id: S,
67        application_key: K,
68    ) -> Result<B2SimpleClient, B2Error> {
69        let auth_token = format!(
70            "Basic {}",
71            general_purpose::STANDARD_NO_PAD.encode(format!(
72                "{}:{}",
73                key_id.as_ref(),
74                application_key.as_ref()
75            ))
76        );
77
78        let client = reqwest::Client::new();
79
80        let auth_response = client
81            .get("https://api.backblazeb2.com/b2api/v3/b2_authorize_account")
82            .header("Authorization", auth_token)
83            .send()
84            .await;
85
86        Ok(B2SimpleClient {
87            client,
88            auth_data: WriteLockArc::new(B2SimpleClient::handle_response(auth_response).await?),
89        })
90    }
91
92    pub fn auth_data(&self) -> B2AuthData {
93        (*self.auth_data).clone()
94    }
95
96    pub async fn authorize_account<S: AsRef<str>, K: AsRef<str>>(
97        &self,
98        key_id: S,
99        application_key: K,
100    ) -> Result<B2AuthData, B2Error> {
101        let auth_token = format!(
102            "Basic {}",
103            general_purpose::STANDARD_NO_PAD.encode(format!(
104                "{}:{}",
105                key_id.as_ref(),
106                application_key.as_ref()
107            ))
108        );
109
110        let auth_response = self
111            .client
112            .get("https://api.backblazeb2.com/b2api/v3/b2_authorize_account")
113            .header("Authorization", auth_token)
114            .send()
115            .await;
116
117        self.auth_data
118            .set(B2SimpleClient::handle_response(auth_response).await?)
119            .await;
120        Ok(self.auth_data())
121    }
122
123    /// [b2_cancel_large_file](https://www.backblaze.com/apidocs/b2-cancel-large-file)
124    pub async fn cancel_large_file(
125        &self,
126        file_id: String,
127    ) -> Result<B2CancelLargeFileResponse, B2Error> {
128        self.has_capabilities(&[B2KeyCapability::WriteFiles])?;
129
130        let response = self
131            .create_request_with_token(Method::POST, B2Endpoint::B2CancelLargeFile)
132            .json(&json!({ "fileId": file_id }))
133            .send()
134            .await;
135
136        B2SimpleClient::handle_response(response).await
137    }
138
139    /// [b2_copy_file](https://www.backblaze.com/apidocs/b2-copy-file)
140    pub async fn copy_file(&self, body: B2CopyFileBody) -> Result<B2File, B2Error> {
141        let mut needed_capabilities = vec![B2KeyCapability::WriteFiles];
142
143        if body.file_retention.is_some() {
144            needed_capabilities.push(B2KeyCapability::WriteFileRetentions);
145        }
146
147        if body.legal_hold.is_some() {
148            needed_capabilities.push(B2KeyCapability::WriteFileLegalHolds);
149        }
150
151        self.has_capabilities(&needed_capabilities)?;
152
153        let response = self
154            .create_request_with_token(Method::POST, B2Endpoint::B2CopyFile)
155            .json(&body)
156            .send()
157            .await;
158
159        B2SimpleClient::handle_response(response).await
160    }
161
162    /// [b2_copy_part](https://www.backblaze.com/apidocs/b2-copy-part)
163    pub async fn copy_part(&self, request_body: B2CopyPartBody) -> Result<B2FilePart, B2Error> {
164        self.has_capabilities(&[B2KeyCapability::WriteFiles])?;
165
166        let response = self
167            .create_request_with_token(Method::POST, B2Endpoint::B2CopyPart)
168            .json(&request_body)
169            .send()
170            .await;
171
172        B2SimpleClient::handle_response(response).await
173    }
174
175    /// [b2_create_bucket](https://www.backblaze.com/apidocs/b2-create-bucket)
176    pub async fn create_bucket(&self, body: B2CreateBucketBody) -> Result<B2Bucket, B2Error> {
177        let mut needed_capabilities = vec![B2KeyCapability::WriteBuckets];
178
179        if let Some(file_lock_enabled) = body.file_lock_enabled {
180            if file_lock_enabled {
181                needed_capabilities.push(B2KeyCapability::WriteBucketRetentions);
182            }
183        }
184
185        if body.default_server_side_encryption.is_some() {
186            needed_capabilities.push(B2KeyCapability::WriteBucketEncryption);
187        }
188
189        self.has_capabilities(&needed_capabilities)?;
190
191        let response = self
192            .create_request_with_token(Method::POST, B2Endpoint::B2CreateBucket)
193            .json(&body)
194            .send()
195            .await;
196
197        B2SimpleClient::handle_response(response).await
198    }
199
200    /// [b2_create_key](https://www.backblaze.com/apidocs/b2-create-key)
201    pub async fn create_key(&self, request_body: B2CreateKeyBody) -> Result<B2AppKey, B2Error> {
202        self.has_capabilities(&[B2KeyCapability::WriteKeys])?;
203
204        let response = self
205            .create_request_with_token(Method::POST, B2Endpoint::B2CreateKey)
206            .json(&request_body)
207            .send()
208            .await;
209
210        B2SimpleClient::handle_response(response).await
211    }
212
213    /// [b2_delete_bucket](https://www.backblaze.com/apidocs/b2-delete-bucket)
214    pub async fn delete_bucket(
215        &self,
216        account_id: String,
217        bucket_id: String,
218    ) -> Result<B2Bucket, B2Error> {
219        self.has_capabilities(&[B2KeyCapability::DeleteBuckets])?;
220
221        let response = self
222            .create_request_with_token(Method::POST, B2Endpoint::B2DeleteBucket)
223            .json(&json!({ "accountId": account_id, "bucketId": bucket_id }))
224            .send()
225            .await;
226
227        B2SimpleClient::handle_response(response).await
228    }
229
230    /// [b2_delete_file_version](https://www.backblaze.com/apidocs/b2-delete-file-version)
231    pub async fn delete_file_version(
232        &self,
233        request_body: B2DeleteFileVersionBody,
234    ) -> Result<B2DeleteFileVersionResponse, B2Error> {
235        self.has_capabilities(&[B2KeyCapability::DeleteFiles])?;
236
237        let response = self
238            .create_request_with_token(Method::POST, B2Endpoint::B2DeleteFileVersion)
239            .json(&request_body)
240            .send()
241            .await;
242
243        B2SimpleClient::handle_response(response).await
244    }
245
246    /// [b2_delete_key](https://www.backblaze.com/apidocs/b2-delete-key)
247    pub async fn delete_key(&self, application_key_id: String) -> Result<B2AppKey, B2Error> {
248        let response = self
249            .create_request_with_token(Method::GET, B2Endpoint::B2DeleteKey)
250            .json(&json!({ "applicationKeyId": application_key_id }))
251            .send()
252            .await;
253
254        B2SimpleClient::handle_response(response).await
255    }
256
257    /// [b2_download_file_by_id](https://www.backblaze.com/apidocs/b2-download-file-by-id)
258    pub async fn download_file_by_id(
259        &self,
260        file_id: String,
261        request_query_params: Option<B2DownloadFileQueryParameters>,
262    ) -> Result<B2DownloadFileContent, B2Error> {
263        let response = self
264            .create_request_with_token(Method::GET, B2Endpoint::B2DownloadFileById)
265            .query(&[("fileId", file_id)])
266            .query(&request_query_params)
267            .send()
268            .await;
269
270        B2SimpleClient::handle_file_response(response).await
271    }
272
273    /// [b2_download_file_by_name](https://www.backblaze.com/apidocs/b2-download-file-by-name)
274    pub async fn download_file_by_name(
275        &self,
276        bucket_name: String,
277        file_name: String,
278        request_query_params: Option<B2DownloadFileQueryParameters>,
279    ) -> Result<B2DownloadFileContent, B2Error> {
280        let response = self
281            .client
282            .get(format!(
283                "{}/file/{}/{}",
284                self.auth_data.api_info.storage_api.download_url, bucket_name, file_name
285            ))
286            .header("Authorization", self.get_authorization_token())
287            .query(&request_query_params)
288            .send()
289            .await;
290
291        B2SimpleClient::handle_file_response(response).await
292    }
293
294    /// [b2_finish_large_file](https://www.backblaze.com/apidocs/b2-finish-large-file)
295    pub async fn finish_large_file(
296        &self,
297        request_body: B2FinishLargeFileBody,
298    ) -> Result<B2File, B2Error> {
299        self.has_capabilities(&[B2KeyCapability::WriteFiles])?;
300
301        let response = self
302            .create_request_with_token(Method::POST, B2Endpoint::B2FinishLargeFile)
303            .json(&request_body)
304            .send()
305            .await;
306
307        B2SimpleClient::handle_response(response).await
308    }
309
310    /// [b2_get_bucket_notification_rules](https://www.backblaze.com/apidocs/b2-get-bucket-notification-rules)
311    pub async fn get_bucket_notification_rules(
312        &self,
313        bucket_id: String,
314    ) -> Result<B2BucketNotificationRulesResponseBody, B2Error> {
315        self.has_capabilities(&[B2KeyCapability::ReadBucketNotifications])?;
316
317        let response = self
318            .create_request_with_token(Method::GET, B2Endpoint::B2GetBucketNotificationRules)
319            .query(&[("bucketId", bucket_id)])
320            .send()
321            .await;
322
323        B2SimpleClient::handle_response(response).await
324    }
325
326    /// [b2_get_download_authorization](https://www.backblaze.com/apidocs/b2-get-download-authorization)
327    pub async fn get_download_authorization(
328        &self,
329        request_body: B2GetDownloadAuthorizationBody,
330    ) -> Result<B2GetDownloadAuthorizationBodyResponse, B2Error> {
331        self.has_capabilities(&[B2KeyCapability::ShareFiles])?;
332
333        let response = self
334            .create_request_with_token(Method::POST, B2Endpoint::B2GetDownloadAuthorization)
335            .json(&request_body)
336            .send()
337            .await;
338
339        B2SimpleClient::handle_response(response).await
340    }
341
342    /// [b2_get_file_info](https://www.backblaze.com/apidocs/b2-get-file-info)
343    pub async fn get_file_info(&self, file_id: String) -> Result<B2File, B2Error> {
344        self.has_capabilities(&[B2KeyCapability::ReadFiles])?;
345
346        let response = self
347            .create_request_with_token(Method::GET, B2Endpoint::B2GetFileInfo)
348            .query(&[("fileId", file_id)])
349            .send()
350            .await;
351
352        B2SimpleClient::handle_response(response).await
353    }
354
355    /// [b2_get_upload_part_url](https://www.backblaze.com/apidocs/b2-get-upload-part-url)
356    pub async fn get_upload_part_url(
357        &self,
358        file_id: String,
359    ) -> Result<B2GetUploadPartUrlResponse, B2Error> {
360        self.has_capabilities(&[B2KeyCapability::WriteFiles])?;
361
362        let response = self
363            .create_request_with_token(Method::GET, B2Endpoint::B2GetUploadPartUrl)
364            .query(&[("fileId", file_id)])
365            .send()
366            .await;
367
368        B2SimpleClient::handle_response(response).await
369    }
370
371    /// [b2_get_upload_url](https://www.backblaze.com/apidocs/b2-get-upload-url)
372    pub async fn get_upload_url(
373        &self,
374        bucket_id: String,
375    ) -> Result<B2GetUploadUrlResponse, B2Error> {
376        self.has_capabilities(&[B2KeyCapability::WriteFiles])?;
377
378        let response = self
379            .create_request_with_token(Method::GET, B2Endpoint::B2GetUploadUrl)
380            .query(&[("bucketId", bucket_id)])
381            .send()
382            .await;
383
384        B2SimpleClient::handle_response(response).await
385    }
386
387    /// [b2_hide_file](https://www.backblaze.com/apidocs/b2-hide-file)
388    pub async fn hide_file(&self, bucket_id: String, file_name: String) -> Result<B2File, B2Error> {
389        self.has_capabilities(&[B2KeyCapability::WriteFiles])?;
390
391        let response = self
392            .create_request_with_token(Method::POST, B2Endpoint::B2HideFile)
393            .json(&json!({ "bucketId": bucket_id, "fileName": file_name }))
394            .send()
395            .await;
396
397        B2SimpleClient::handle_response(response).await
398    }
399
400    /// [b2_list_buckets](https://www.backblaze.com/apidocs/b2-list-buckets)
401    pub async fn list_buckets(
402        &self,
403        request_body: B2ListBucketsBody,
404    ) -> Result<B2ListBucketsResponse, B2Error> {
405        self.has_capabilities(&[B2KeyCapability::ListBuckets])?;
406
407        let response = self
408            .create_request_with_token(Method::POST, B2Endpoint::B2ListBuckets)
409            .json(&request_body)
410            .send()
411            .await;
412
413        B2SimpleClient::handle_response(response).await
414    }
415
416    /// [b2_list_file_names](https://www.backblaze.com/apidocs/b2-list-file-names)
417    pub async fn list_file_names(
418        &self,
419        request_body: B2ListFileNamesQueryParameters,
420    ) -> Result<B2ListFilesResponse, B2Error> {
421        self.has_capabilities(&[B2KeyCapability::ListFiles])?;
422
423        let response = self
424            .create_request_with_token(Method::GET, B2Endpoint::B2ListFileNames)
425            .query(&request_body)
426            .send()
427            .await;
428
429        B2SimpleClient::handle_response(response).await
430    }
431
432    /// [b2_list_file_versions](https://www.backblaze.com/apidocs/b2-list-file-versions)
433    pub async fn list_file_versions(
434        &self,
435        request_body: B2ListFileVersionsQueryParameters,
436    ) -> Result<B2ListFileVersionsResponse, B2Error> {
437        self.has_capabilities(&[B2KeyCapability::ListFiles])?;
438
439        let response = self
440            .create_request_with_token(Method::GET, B2Endpoint::B2ListFileVersions)
441            .query(&request_body)
442            .send()
443            .await;
444
445        B2SimpleClient::handle_response(response).await
446    }
447
448    /// [b2_list_keys](https://www.backblaze.com/apidocs/b2-list-keys)
449    pub async fn list_keys(
450        &self,
451        request_body: B2ListKeysParameters,
452    ) -> Result<B2ListKeysResponse, B2Error> {
453        self.has_capabilities(&[B2KeyCapability::ListKeys])?;
454
455        let response = self
456            .create_request_with_token(Method::GET, B2Endpoint::B2ListKeys)
457            .query(&request_body)
458            .send()
459            .await;
460
461        B2SimpleClient::handle_response(response).await
462    }
463
464    /// [b2_list_parts](https://www.backblaze.com/apidocs/b2-list-parts)
465    pub async fn list_parts(
466        &self,
467        request_body: B2ListPartsQueryParameters,
468    ) -> Result<B2ListPartsResponse, B2Error> {
469        self.has_capabilities(&[B2KeyCapability::WriteFiles])?;
470
471        let response = self
472            .create_request_with_token(Method::GET, B2Endpoint::B2ListParts)
473            .query(&request_body)
474            .send()
475            .await;
476
477        B2SimpleClient::handle_response(response).await
478    }
479
480    /// [b2_list_unfinished_large_files](https://www.backblaze.com/apidocs/b2-list-unfinished-large-files)
481    pub async fn list_unfinished_large_files(
482        &self,
483        request_body: B2ListUnfinishedLargeFilesQueryParameters,
484    ) -> Result<B2ListUnfinishedLargeFilesResponse, B2Error> {
485        self.has_capabilities(&[B2KeyCapability::ListFiles])?;
486
487        let response = self
488            .create_request_with_token(Method::GET, B2Endpoint::B2ListUnfinishedLargeFiles)
489            .query(&request_body)
490            .send()
491            .await;
492
493        B2SimpleClient::handle_response(response).await
494    }
495
496    /// [b2_set_bucket_notification_rules](https://www.backblaze.com/apidocs/b2-set-bucket-notification-rules)
497    pub async fn set_bucket_notification_rules(
498        &self,
499        request_body: B2BucketNotificationRulesResponseBody,
500    ) -> Result<B2BucketNotificationRulesResponseBody, B2Error> {
501        self.has_capabilities(&[B2KeyCapability::WriteBucketNotifications])?;
502
503        let response = self
504            .create_request_with_token(Method::POST, B2Endpoint::B2SetBucketNotificationRules)
505            .json(&request_body)
506            .send()
507            .await;
508
509        B2SimpleClient::handle_response(response).await
510    }
511
512    /// [b2_start_large_file](https://www.backblaze.com/apidocs/b2-start-large-file)
513    pub async fn start_large_file(
514        &self,
515        request_body: B2StartLargeFileUploadBody,
516    ) -> Result<B2File, B2Error> {
517        let response = self
518            .create_request_with_token(Method::POST, B2Endpoint::B2StartLargeFile)
519            .json(&request_body)
520            .send()
521            .await;
522
523        B2SimpleClient::handle_response(response).await
524    }
525
526    /// [b2_update_bucket](https://www.backblaze.com/apidocs/b2-update-bucket)
527    pub async fn update_bucket(
528        &self,
529        request_body: B2UpdateBucketBody,
530    ) -> Result<B2Bucket, B2Error> {
531        self.has_capabilities(&[B2KeyCapability::WriteBuckets])?;
532
533        let response = self
534            .create_request_with_token(Method::POST, B2Endpoint::B2UpdateBucket)
535            .json(&request_body)
536            .send()
537            .await;
538
539        B2SimpleClient::handle_response(response).await
540    }
541
542    /// [b2_update_file_legal_hold](https://www.backblaze.com/apidocs/b2-update-file-legal-hold)
543    pub async fn update_file_legal_hold(
544        &self,
545        request_body: B2UpdateFileLegalHoldBodyResponse,
546    ) -> Result<B2UpdateFileLegalHoldBodyResponse, B2Error> {
547        self.has_capabilities(&[B2KeyCapability::WriteFileLegalHolds])?;
548
549        let response = self
550            .create_request_with_token(Method::POST, B2Endpoint::B2UpdateFileLegalHold)
551            .json(&request_body)
552            .send()
553            .await;
554
555        B2SimpleClient::handle_response(response).await
556    }
557
558    /// [b2_update_file_retention](https://www.backblaze.com/apidocs/b2-update-file-retention)
559    pub async fn update_file_retention(
560        &self,
561        request_body: B2UpdateFileRetentionBody,
562    ) -> Result<B2UpdateFileRetentionResponse, B2Error> {
563        self.has_capabilities(&[B2KeyCapability::WriteFileRetentions])?;
564
565        let response = self
566            .create_request_with_token(Method::POST, B2Endpoint::B2UpdateFileRetention)
567            .json(&request_body)
568            .send()
569            .await;
570
571        B2SimpleClient::handle_response(response).await
572    }
573
574    /// [b2_upload_file](https://www.backblaze.com/apidocs/b2-upload-file)
575    pub async fn upload_file<S: AsRef<str>, F: Into<reqwest::Body>>(
576        &self,
577        file: F,
578        upload_url: S,
579        request_headers: B2UploadFileHeaders,
580        file_info: Option<HashMap<S, impl AsRef<str>>>,
581    ) -> Result<B2File, B2Error> {
582        let file_info = match file_info {
583            Some(map) => map,
584            None => HashMap::new(),
585        };
586
587        let file_info: HashMap<_, _> = file_info
588            .iter()
589            .map(|(key, value)| {
590                let key_ref = key.as_ref();
591                (
592                    format!("X-Bz-Info-{key_ref}"),
593                    utf8_percent_encode(value.as_ref(), ENCODE_SET).to_string(),
594                )
595            })
596            .collect();
597
598        let mut request_headers = request_headers;
599
600        request_headers.file_name =
601            utf8_percent_encode(&request_headers.file_name, ENCODE_SET).to_string();
602
603        let response = self
604            .client
605            .request(Method::POST, upload_url.as_ref())
606            .headers(request_headers.into_header_map()?)
607            .headers(hash_map_to_headers(file_info))
608            .body(file)
609            .send()
610            .await;
611
612        B2SimpleClient::handle_response(response).await
613    }
614
615    /// []()
616    pub async fn upload_part<F: Into<reqwest::Body>>(
617        &self,
618        request_headers: B2UploadPartHeaders,
619        part: F,
620        upload_url: String,
621    ) -> Result<B2FilePart, B2Error> {
622        let response = self
623            .client
624            .request(Method::POST, upload_url)
625            .headers(request_headers.into_header_map()?)
626            .body(part)
627            .send()
628            .await;
629
630        B2SimpleClient::handle_response(response).await
631    }
632
633    pub fn get_authorization_token(&self) -> &str {
634        &self.auth_data.authorization_token
635    }
636
637    pub fn has_capability(&self, capability: &B2KeyCapability) -> bool {
638        self.auth_data
639            .api_info
640            .storage_api
641            .capabilities
642            .contains(capability)
643    }
644
645    pub fn has_capabilities(&self, capabilities: &[B2KeyCapability]) -> Result<(), B2Error> {
646        for capability in capabilities {
647            if !self.has_capability(capability) {
648                return Err(B2Error::MissingCapability(capability.clone()));
649            }
650        }
651
652        Ok(())
653    }
654
655    #[inline]
656    fn create_request_url(&self, api_name: B2Endpoint) -> String {
657        format!(
658            "{}/b2api/v3/{}",
659            self.auth_data.api_info.storage_api.api_url,
660            api_name.to_string()
661        )
662    }
663
664    #[inline]
665    fn create_request_with_token(&self, method: Method, api_name: B2Endpoint) -> RequestBuilder {
666        let url = self.create_request_url(api_name);
667
668        self.client
669            .request(method, url)
670            .header("Authorization", self.get_authorization_token())
671    }
672
673    #[inline]
674    async fn response_option_handling(
675        response: Result<Response, reqwest::Error>,
676    ) -> Result<Response, B2Error> {
677        let response = match response {
678            Ok(resp) => resp,
679            Err(error) => {
680                return Err(B2Error::RequestSendError(error));
681            }
682        };
683
684        let response_code = response.status().as_u16();
685
686        if response_code >= 400 {
687            let response = match response.bytes().await {
688                Ok(text) => text,
689                Err(_) => {
690                    return Err(B2Error::RequestError(B2RequestError {
691                        status: NonZeroU16::new(response_code).expect("Response code cannot be 0"),
692                        code: String::from(""),
693                        message: Some(String::from("B2Client failed to collect")),
694                    }))
695                }
696            };
697
698            let error_json: B2RequestError = match serde_json::from_slice(&response) {
699                Ok(json) => json,
700                Err(_) => B2RequestError {
701                    status: NonZeroU16::new(response_code).expect("Response code cannot be 0"),
702                    code: String::from(""),
703                    message: Some(String::from(format!(
704                        "B2Client failed to parse response as json, returned string: {}",
705                        String::from_utf8_lossy(&response)
706                    ))),
707                },
708            };
709
710            return Err(B2Error::RequestError(error_json));
711        };
712
713        Ok(response)
714    }
715
716    #[inline]
717    async fn handle_response<T: DeserializeOwned>(
718        response: Result<Response, reqwest::Error>,
719    ) -> Result<T, B2Error> {
720        let response = match B2SimpleClient::response_option_handling(response).await {
721            Ok(resp) => resp,
722            Err(error) => return Err(error),
723        };
724
725        let text = response
726            .text()
727            .await
728            .map_err(|err| B2Error::RequestSendError(err))?;
729
730        match serde_json::from_str::<T>(&text) {
731            Ok(json) => Ok(json),
732            Err(error) => Err(B2Error::JsonParseError(error)),
733        }
734    }
735
736    #[inline]
737    async fn handle_file_response(
738        response: Result<Response, reqwest::Error>,
739    ) -> Result<B2DownloadFileContent, B2Error> {
740        let response = match response {
741            Ok(resp) => resp,
742            Err(error) => return Err(B2Error::RequestSendError(error)),
743        };
744
745        let mut headers = header_map_to_hashmap(response.headers());
746        let file_name = headers.remove("x-bz-file-name").expect("should exist");
747        let file_name = utf8_percent_encode(&file_name.replace("+", " "), ENCODE_SET).to_string();
748
749        let sha1 = headers.remove("x-bz-content-sha1").expect("should exist");
750
751        let mut file_details = B2FileDownloadDetails {
752            file_id: headers.remove("x-bz-file-id").expect("should exist"),
753            file_name,
754            content_length: headers
755                .remove("content-length")
756                .expect("should exist")
757                .parse()
758                .expect("valid number"),
759            content_type: headers.remove("content-type").expect("should exist"),
760            content_sha1: if sha1 != "none" { Some(sha1) } else { None },
761            upload_timestamp: headers
762                .remove("x-bz-upload-timestamp")
763                .expect("should exist")
764                .parse()
765                .expect("valid number"),
766            file_info: None,
767        };
768
769        let mut temp_file_info: HashMap<String, String> = HashMap::new();
770        let keys: Vec<String> = headers.keys().map(|e| e.clone()).collect();
771
772        for key in keys {
773            if key.starts_with("x-bz-info-") {
774                let value = headers.remove(&key).expect("key exists");
775                let value = utf8_percent_encode(&value.replace("+", " "), ENCODE_SET).to_string();
776
777                temp_file_info.insert(key.replace("x-bz-info-", ""), value);
778            }
779        }
780
781        if temp_file_info.len() > 0 {
782            file_details.file_info = Some(temp_file_info)
783        }
784
785        let body = response.bytes_stream();
786
787        Ok(B2DownloadFileContent {
788            file: B2FileStream::new(body, file_details.content_length as usize),
789            file_details,
790            remaining_headers: headers,
791        })
792    }
793}
794
795#[inline]
796fn hash_map_to_headers<S: AsRef<str>>(map: HashMap<S, impl AsRef<str>>) -> HeaderMap {
797    map.iter()
798        .map(|(name, value)| {
799            (
800                HeaderName::from_str(name.as_ref()),
801                HeaderValue::from_str(value.as_ref()),
802            )
803        })
804        .filter_map(|(key, value)| match (key, value) {
805            (Ok(key), Ok(value)) if !value.is_empty() => Some((key, value)),
806            _ => None,
807        })
808        .collect()
809}
810
811#[inline]
812fn header_map_to_hashmap(map: &HeaderMap) -> HashMap<String, String> {
813    let mut header_hashmap = HashMap::new();
814
815    for (k, v) in map {
816        let k = k.as_str().to_owned();
817        let v = String::from_utf8_lossy(v.as_bytes()).into_owned();
818        header_hashmap.insert(k, v);
819    }
820
821    header_hashmap
822}