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}