b2_client/lib.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 License, v. 2.0. If a copy of the MPL was not distributed with this
3 file, You can obtain one at http://mozilla.org/MPL/2.0/.
4*/
5
6//! A Backblaze B2 API library that can send and receive data via arbitrary HTTP
7//! clients.
8//!
9//! The full API is supported.
10//!
11//! # Examples
12//!
13//! ```no_run
14//! # fn calculate_sha1(data: &[u8]) -> String { String::default() }
15//! # use anyhow;
16//! use std::env;
17//! use b2_client as b2;
18//!
19//! # #[cfg(feature = "with_surf")]
20//! # async fn upload_file() -> anyhow::Result<()> {
21//! let key = env::var("B2_KEY").ok().unwrap();
22//! let key_id = env::var("B2_KEY_ID").ok().unwrap();
23//!
24//! let client = b2::client::SurfClient::default();
25//! let mut auth = b2::authorize_account(client, &key, &key_id).await?;
26//!
27//! let mut upload_auth = b2::get_upload_authorization_by_id(
28//! &mut auth,
29//! "my-bucket-id"
30//! ).await?;
31//!
32//! let file = b2::UploadFile::builder()
33//! .file_name("my-file.txt")?
34//! .sha1_checksum("61b8d6600ac94d912874f569a9341120f680c9f8")
35//! .build()?;
36//!
37//! let data = b"very important information";
38//!
39//! let file_info = b2::upload_file(&mut upload_auth, file, data).await?;
40//!
41//! # Ok(())
42//! # }
43//! ```
44//!
45//! # Differences from the B2 Service API
46//!
47//! * The B2 endpoint `b2_get_upload_part_url` is
48//! [get_upload_part_authorization].
49//! * The B2 endpoint `b2_get_upload_url` is [get_upload_authorization].
50//! * The word "file" is often added for clarity; e.g., the B2 endpoint
51//! `b2_copy_part` is [copy_file_part].
52
53// Increase the recursion limit for a macro in a test in validate.rs.
54#![cfg_attr(test, recursion_limit = "256")]
55
56
57pub mod account;
58pub mod bucket;
59pub mod file;
60
61pub mod client;
62pub mod error;
63
64mod types;
65mod validate;
66
67pub mod prelude {
68 #![allow(unused_imports)]
69
70 pub(crate) use super::{
71 account::{Authorization, Capability},
72 types::{B2Result, Duration},
73 require_capability,
74 };
75}
76
77macro_rules! require_capability {
78 ($auth:expr, $cap:expr) => {
79 if ! $auth.has_capability($cap) {
80 return Err($crate::error::Error::Unauthorized($cap));
81 }
82 }
83}
84pub(crate) use require_capability;
85
86
87pub use account::*;
88pub use bucket::*;
89pub use file::*;
90
91pub use client::HttpClient;
92pub use error::Error;
93
94#[cfg(all(test, feature = "with_surf"))]
95pub(crate) mod test_utils {
96 use std::boxed::Box;
97 use crate::{
98 account::{Authorization, Capability, Capabilities},
99 client::SurfClient,
100 };
101 use surf_vcr::*;
102 use surf::http::Method;
103
104
105 /// Create a SurfClient with the surf-vcr middleware.
106 ///
107 /// We remove the following data from the recorded sessions:
108 ///
109 /// * From request/response headers and bodies:
110 /// * `accountId`
111 /// * `authorizationToken`
112 /// * `keys` dictionary (response only)
113 ///
114 /// The `keys` dictionary in a response is replaced with a single key object
115 /// containing fake data.
116 ///
117 /// The potentially-senstive data that we do not remove includes:
118 ///
119 /// * From request/response bodies:
120 /// * `applicationKeyId`
121 /// * `bucketId`
122 ///
123 /// You may optionally pass in functions to make additional modifications to
124 /// a response or request as needed for specific tests. Note that if the
125 /// response body is not valid JSON, nothing in the response can be
126 /// modified.
127 pub async fn create_test_client(
128 mode: VcrMode, cassette: &'static str,
129 req_mod: Option<Box<dyn Fn(&mut VcrRequest) + Send + Sync + 'static>>,
130 res_mod: Option<Box<dyn Fn(&mut VcrResponse) + Send + Sync + 'static>>,
131 ) -> std::result::Result<SurfClient, VcrError> {
132 #![allow(clippy::option_map_unit_fn)]
133
134 let vcr = VcrMiddleware::new(mode, cassette).await.unwrap()
135 .with_modify_request(move |req| {
136 let val = match req.method {
137 Method::Get => "Basic hidden-account-id".into(),
138 _ => "hidden-authorization-token".into(),
139 };
140
141 req.headers.entry("authorization".into())
142 .and_modify(|v| *v = vec![val]);
143
144 req.headers.entry("user-agent".into())
145 .and_modify(|v| {
146 // We need to replace the version number with a constant
147 // value.
148
149 let range = if v[0].len() > 7 {
150 let start = v[0][7..]
151 .find(|c| char::is_ascii_digit(&c))
152 .expect("User-agent string is incorrect");
153
154 let end = v[0][start..].find(';')
155 .expect("User-agent string is incorrect");
156
157 Some((start + 7, end + start))
158 } else {
159 None
160 };
161
162 if let Some((start, end)) = range {
163 v[0].replace_range(start..end, "version");
164 }
165 });
166
167 if let Body::Str(body) = &mut req.body {
168 let body_json: Result<serde_json::Value, _> =
169 serde_json::from_str(body);
170
171 if let Ok(mut body) = body_json {
172 body.get_mut("accountId")
173 .map(|v| *v =
174 serde_json::json!("hidden-account-id"));
175
176 req.body = Body::Str(body.to_string());
177 }
178 };
179
180 if let Some(req_mod) = req_mod.as_ref() {
181 req_mod(req);
182 }
183 })
184 .with_modify_response(move |res| {
185 // If the response isn't JSON, there's nothing we need to
186 // modify.
187 let mut json: serde_json::Value = match &mut res.body {
188 Body::Str(s) => match serde_json::from_str(s) {
189 Ok(json) => json,
190 Err(_) => return,
191 },
192 _ => return,
193 };
194
195 json = hide_response_account_id(json);
196
197 json.get_mut("authorizationToken")
198 .map(|v| *v = serde_json::json!(
199 "hidden-authorization-token")
200 );
201
202 json.get_mut("keys")
203 .map(|v| *v = serde_json::json!([{
204 "accountId": "hidden-account-id",
205 "applicationKeyId": "hidden-app-key-id",
206 "bucketId": "abcdefghijklmnop",
207 "capabilities": [
208 "listFiles",
209 "readFiles",
210 ],
211 "expirationTimestamp": null,
212 "keyName": "dev-b2-client-tester",
213 "namePrefix": null,
214 "options": ["s3"],
215 "nextApplicationId": null,
216 }]));
217
218 res.body = Body::Str(json.to_string());
219
220 if let Some(res_mod) = res_mod.as_ref() {
221 res_mod(res);
222 }
223 });
224
225 let surf = surf::Client::new()
226 .with(vcr);
227
228 let client = SurfClient::default()
229 .with_client(surf);
230
231 Ok(client)
232 }
233
234 fn hide_response_account_id(mut json: serde_json::Value)
235 -> serde_json::Value {
236 #![allow(clippy::option_map_unit_fn)]
237
238 if let Some(buckets) = json.get_mut("buckets")
239 .and_then(|b| b.as_array_mut())
240 {
241 for bucket in buckets.iter_mut() {
242 bucket.get_mut("accountId")
243 .map(|v| *v = serde_json::json!("hidden-account-id"));
244 }
245 }
246
247 json.get_mut("accountId")
248 .map(|v| *v = serde_json::json!("hidden-account-id"));
249
250 json
251 }
252
253 /// Create an [Authorization] with the specified capabilities.
254 ///
255 /// If the `B2_CLIENT_TEST_KEY` and `B2_CLIENT_TEST_KEY_ID` environment
256 /// variables are set, their values are used to make an authorization
257 /// request against the B2 API.
258 ///
259 /// Otherwise, a fake authorization is created with values usable for
260 /// pre-recorded sessions in unit tests.
261 pub async fn create_test_auth(
262 client: SurfClient,
263 capabilities: Vec<Capability>
264 ) -> Authorization<SurfClient> {
265 use super::account::authorize_account;
266
267 let key = std::env::var("B2_CLIENT_TEST_KEY").ok();
268 let key_id = std::env::var("B2_CLIENT_TEST_KEY_ID").ok();
269
270 assert!(key.as_ref().xor(key_id.as_ref()).is_none(),
271 concat!(
272 "Either both or neither of the B2_CLIENT_TEST_KEY and ",
273 "B2_CLIENT_TEST_KEY_ID environment variables must be set"
274 )
275 );
276
277 if let Some(key) = key {
278 let auth = authorize_account(client, &key, &key_id.unwrap())
279 .await.unwrap();
280
281 for cap in capabilities {
282 assert!(auth.capabilities().has_capability(cap));
283 }
284
285 auth
286 } else {
287 Authorization::new(
288 client,
289 "some-account-id".into(),
290 "some-key-id".into(),
291 Capabilities::new(capabilities, None, None, None),
292 "https://api002.backblazeb2.com".into(),
293 "https://f002.backblazeb2.com".into(),
294 100000000,
295 5000000,
296 "https://s3.us-west-002.backblazeb2.com".into(),
297 )
298 }
299 }
300}