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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}