b2_client/
bucket.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//! B2 API calls for managing buckets.
7//!
8//! These functions deal with creating, deleting, and managing buckets (e.g.,
9//! setting server-side encryption and file retention rules).
10//!
11//! A B2 account has a limit of 100 buckets. All bucket names must be globally
12//! unique (unique across all accounts).
13
14use std::{borrow::Cow, fmt};
15
16use crate::{
17    prelude::*,
18    client::HttpClient,
19    error::*,
20    validate::*,
21};
22
23use http_types::cache::CacheControl;
24use serde::{Serialize, Deserialize};
25
26
27/// A bucket classification for B2 buckets.
28#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
29#[non_exhaustive]
30pub enum BucketType {
31    /// A bucket where downloads are publicly-accessible.
32    #[serde(rename = "allPublic")]
33    Public,
34    /// A bucket that restricts access to files.
35    #[serde(rename = "allPrivate")]
36    Private,
37    /// A bucket containing B2 snapshots of other buckets.
38    ///
39    /// Snapshot buckets can only be created from the Backblaze web portal.
40    #[serde(rename = "snapshot")]
41    Snapshot,
42}
43
44impl fmt::Display for BucketType {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        match self {
47            Self::Public => write!(f, "allPublic"),
48            Self::Private => write!(f, "allPrivate"),
49            Self::Snapshot => write!(f, "snapshot"),
50        }
51    }
52}
53
54/// A valid CORS operation for B2 buckets.
55#[derive(Debug, Serialize, Deserialize)]
56#[non_exhaustive]
57pub enum CorsOperation {
58    #[serde(rename = "b2_download_file_by_name")]
59    DownloadFileByName,
60    #[serde(rename = "b2_download_file_by_id")]
61    DownloadFileById,
62    #[serde(rename = "b2_upload_file")]
63    UploadFile,
64    #[serde(rename = "b2_upload_part")]
65    UploadPart,
66    // S3-compatible API operations.
67    #[serde(rename = "s3_delete")]
68    S3Delete,
69    #[serde(rename = "s3_get")]
70    S3Get,
71    #[serde(rename = "s3_head")]
72    S3Head,
73    #[serde(rename = "s3_post")]
74    S3Post,
75    #[serde(rename = "s3_put")]
76    S3Put,
77}
78
79/// A rule to determine CORS behavior of B2 buckets.
80///
81/// See <https://www.backblaze.com/b2/docs/cors_rules.html> for further
82/// information on CORS and file access via the B2 service.
83#[derive(Debug, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct CorsRule {
86    cors_rule_name: String,
87    allowed_origins: Vec<String>,
88    allowed_operations: Vec<CorsOperation>,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    allowed_headers: Option<Vec<String>>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    expose_headers: Option<Vec<String>>,
93    max_age_seconds: u16,
94}
95
96impl CorsRule {
97    /// Get a builder for a [CorsRule].
98    pub fn builder() -> CorsRuleBuilder {
99        CorsRuleBuilder::default()
100    }
101}
102
103/// Create a [CorsRule].
104///
105/// See <https://www.backblaze.com/b2/docs/cors_rules.html> for further
106/// information on CORS and file access via the B2 service.
107#[derive(Debug, Default)]
108pub struct CorsRuleBuilder {
109    name: Option<String>,
110    allowed_origins: Vec<String>,
111    allowed_operations: Vec<CorsOperation>,
112    allowed_headers: Option<Vec<String>>,
113    expose_headers: Option<Vec<String>>,
114    max_age: Option<u16>,
115}
116
117impl CorsRuleBuilder {
118    /// Create a human-recognizeable name for the CORS rule.
119    ///
120    /// Names can contains any ASCII letters, numbers, and '-'. It must be
121    /// between 6 and 50 characters, inclusive. Names beginning with "b2-" are
122    /// reserved.
123    pub fn name(mut self, name: impl Into<String>)
124    -> Result<Self, CorsRuleValidationError> {
125        let name = validated_cors_rule_name(name)?;
126        self.name = Some(name);
127        Ok(self)
128    }
129
130    /// Set the list of origins covered by this rule.
131    ///
132    /// Examples of valid origins:
133    ///
134    /// * `http://www.example.com:8000`
135    /// * `https://*.example.com`
136    /// * `https://*:8765`
137    /// * `https://*`
138    /// * `https`
139    /// * `*`
140    ///
141    /// If an entry is `*`, there can be no other entries. There can be no more
142    /// than one `https` entry. An entry cannot have more than one '*'.
143    ///
144    /// Note that an origin such as `https` is broader than an origin of
145    /// `https://*` because the latter is limited to the HTTPS scheme's default
146    /// port, but the former is valid for all ports.
147    ///
148    /// At least one origin is required in a CORS rule.
149    pub fn allowed_origins(mut self, origins: impl Into<Vec<String>>)
150    -> Result<Self, ValidationError> {
151        self.allowed_origins = validated_origins(origins)?;
152        Ok(self)
153    }
154
155    /// Add an origin to the list of allowed origins.
156    ///
157    /// Examples of valid origins:
158    ///
159    /// * `http://www.example.com:8000`
160    /// * `https://*.example.com`
161    /// * `https://*:8765`
162    /// * `https://*`
163    /// * `*`
164    ///
165    /// If an entry is `*`, there can be no other entries. There can be no more
166    /// than one `https` entry. An entry cannot have more than one '*'.
167    ///
168    /// Note that an origin such as `https` is broader than an origin of
169    /// `https://*` because the latter is limited to the HTTPS scheme's default
170    /// port, but the former is valid for all ports.
171    ///
172    /// At least one origin is required in a CORS rule.
173    ///
174    /// # Notes
175    ///
176    /// If adding multiple origins, [Self::allowed_origins] will validate the
177    /// provided origins more efficiently.
178    pub fn add_allowed_origin(mut self, origin: impl Into<String>)
179    -> Result<Self, ValidationError> {
180        let origin = origin.into();
181
182        // We push first because we need a list to be able to properly validate
183        // an added origin.
184        self.allowed_origins.push(origin);
185        self.allowed_origins = validated_origins(self.allowed_origins)?;
186
187        Ok(self)
188    }
189
190    /// Set the list of operations the CORS rule allows.
191    ///
192    /// If the provided list is empty, returns [ValidationError::MissingData].
193    pub fn allowed_operations(mut self, ops: Vec<CorsOperation>)
194    -> Result<Self, ValidationError> {
195        if ops.is_empty() {
196            return Err(ValidationError::MissingData(
197                "There must be at least one origin covered by the rule".into()
198            ));
199        }
200
201        self.allowed_operations = ops;
202        Ok(self)
203    }
204
205    /// Add a [CorsOperation] to the list of operations the CORS rule allows.
206    pub fn add_allowed_operation(mut self, op: CorsOperation) -> Self {
207        self.allowed_operations.push(op);
208        self
209    }
210
211    /// Set the list of headers allowed in a pre-flight OPTION requests'
212    /// `Access-Control-Request-Headers` header value.
213    ///
214    /// Each header may be:
215    ///
216    /// * A complete header name
217    /// * A header name ending with an asterisk (`*`) to match multiple headers
218    /// * An asterisk (`*) to match any header
219    ///
220    /// If an entry is `*`, there can be no other entries.
221    ///
222    /// The default is an empty list (no headers are allowed).
223    pub fn allowed_headers<H>(mut self, headers: impl Into<Vec<String>>)
224    -> Result<Self, BadHeaderName> {
225        let headers = headers.into();
226
227        if ! headers.is_empty() {
228            for header in headers.iter() {
229                validated_http_header(header)?;
230            }
231
232            self.allowed_headers = Some(headers);
233        }
234
235        Ok(self)
236    }
237
238    /// Add a header to the list of headers allowed in a pre-flight OPTION
239    /// requests' `Access-Control-Request-Headers` header value.
240    ///
241    /// The header may be:
242    ///
243    /// * A complete header name
244    /// * A header name ending with an asterisk (`*`) to match multipl headers
245    /// * An asterisk (`*) to match any header
246    ///
247    /// If an entry is `*`, there can be no other entries.
248    ///
249    /// By default, no headers are allowed.
250    pub fn add_allowed_header(mut self, header: impl Into<String>)
251    -> Result<Self, BadHeaderName> {
252        let header = header.into();
253        validated_http_header(&header)?;
254
255        let headers = self.allowed_headers.get_or_insert_with(Vec::new);
256        headers.push(header);
257        Ok(self)
258    }
259
260    /// Set the list of headers that may be exposed to an application inside the
261    /// client.
262    ///
263    /// Each entry must be a complete header name. If the list is empty, no
264    /// headers will be exposed.
265    pub fn exposed_headers(mut self, headers: impl Into<Vec<String>>)
266    -> Result<Self, BadHeaderName> {
267        let headers = headers.into();
268
269        if ! headers.is_empty() {
270            for header in headers.iter() {
271                validated_http_header(header)?;
272            }
273
274            self.expose_headers = Some(headers);
275        }
276
277        Ok(self)
278    }
279
280    /// Add a header that may be exposed to an application inside the client.
281    ///
282    /// Each entry must be a complete header name.
283    pub fn add_exposed_header(mut self, header: impl Into<String>)
284    -> Result<Self, BadHeaderName> {
285        let header = header.into();
286        validated_http_header(&header)?;
287
288        let headers = self.expose_headers.get_or_insert_with(Vec::new);
289        headers.push(header);
290        Ok(self)
291    }
292
293    /// Set the maximum duration the browser may cache the response to a
294    /// preflight request.
295    ///
296    /// The age must be non-negative and no more than one day.
297    pub fn max_age(mut self, age: chrono::Duration)
298    -> Result<Self, ValidationError> {
299        if age < chrono::Duration::zero() || age > chrono::Duration::days(1) {
300            return Err(ValidationError::OutOfBounds(
301                "Age must be non-negative and no more than 1 day".into()
302            ));
303        }
304
305        self.max_age = Some(age.num_seconds() as u16);
306        Ok(self)
307    }
308
309    /// Create a [CorsRule] object.
310    pub fn build(self) -> Result<CorsRule, ValidationError> {
311        let cors_rule_name = self.name.ok_or_else(||
312            ValidationError::MissingData(
313                "The CORS rule must have a name".into()
314            )
315        )?;
316
317        let max_age_seconds = self.max_age.ok_or_else(||
318            ValidationError::MissingData(
319                "A maximum age for client caching must be specified".into()
320            )
321        )?;
322
323        if self.allowed_origins.is_empty() {
324            Err(ValidationError::MissingData(
325                "At least one origin must be allowed by the CORS rule".into()
326            ))
327        } else if self.allowed_operations.is_empty() {
328            Err(ValidationError::MissingData(
329                "At least one operation must be specified".into()
330            ))
331        } else {
332            // Instead of doing all this, we could serialize to a JSON string.
333            // If we then made `CorsRule` a simple wrapper over `Value` we
334            // wouldn't even need to serialize twice.
335            let bytes: usize = cors_rule_name.len()
336                + self.allowed_origins.iter().map(|s| s.len()).sum::<usize>()
337                + self.allowed_operations.iter()
338                    .map(|c| serde_json::to_string(c).unwrap().len())
339                    .sum::<usize>()
340                + self.allowed_headers.iter().map(|s| s.len()).sum::<usize>()
341                + self.expose_headers.iter().map(|s| s.len()).sum::<usize>();
342
343            if bytes >= 1000 {
344                return Err(ValidationError::OutOfBounds(
345                    "Maximum bytes of string data is 999".into()
346                ));
347            }
348
349            Ok(CorsRule {
350                cors_rule_name,
351                allowed_origins: self.allowed_origins,
352                allowed_operations: self.allowed_operations,
353                allowed_headers: self.allowed_headers,
354                expose_headers: self.expose_headers,
355                max_age_seconds,
356            })
357        }
358    }
359}
360
361/// A rule to manage the automatic hiding or deletion of files.
362///
363/// See <https://www.backblaze.com/b2/docs/lifecycle_rules.html> for further
364/// information.
365#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct LifecycleRule {
368    pub(crate) file_name_prefix: String,
369    // The B2 docs don't give an upper limit. I can't imagine a reasonable rule
370    // requiring anything close to u16::max() but if necessary we can make these
371    // u32 in the future.
372    #[serde(rename = "daysFromHidingToDeleting")]
373    delete_after: Option<u16>,
374    #[serde(rename = "daysFromUploadingToHiding")]
375    hide_after: Option<u16>,
376}
377
378impl std::cmp::PartialOrd for LifecycleRule {
379    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
380        self.file_name_prefix.partial_cmp(&other.file_name_prefix)
381    }
382}
383
384impl std::cmp::Ord for LifecycleRule {
385    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
386        self.file_name_prefix.cmp(&other.file_name_prefix)
387    }
388}
389
390impl LifecycleRule {
391    /// Get a builder for a `LifecycleRule`.
392    pub fn builder<'a>() -> LifecycleRuleBuilder<'a> {
393        LifecycleRuleBuilder::default()
394    }
395}
396
397/// A builder for a [LifecycleRule].
398///
399/// See <https://www.backblaze.com/b2/docs/lifecycle_rules.html> for information
400/// on bucket lifecycles.
401#[derive(Default)]
402pub struct LifecycleRuleBuilder<'a> {
403    prefix: Option<&'a str>,
404    delete_after: Option<u16>,
405    hide_after: Option<u16>,
406}
407
408impl<'a> LifecycleRuleBuilder<'a> {
409    /// The filename prefix to select the files that are subject to the rule.
410    ///
411    /// A prefix of `""` will apply to all files, allowing the creation of rules
412    /// that could delete **all** files.
413    pub fn filename_prefix(mut self, prefix: &'a str)
414    -> Result<Self, FileNameValidationError> {
415        self.prefix = Some(validated_file_name(prefix)?);
416        Ok(self)
417    }
418
419    /// The number of days to hide a file after it was uploaded.
420    ///
421    /// The supplied duration will be truncated to whole days. If provided, the
422    /// number of days must be at least one.
423    ///
424    /// The maximum number of days supported is [u16::MAX].
425    pub fn hide_after_upload(mut self, days: chrono::Duration)
426    -> Result<Self, ValidationError> {
427        let days = days.num_days();
428
429        if days < 1 {
430            Err(ValidationError::OutOfBounds(
431                "Number of days must be greater than zero".into()
432            ))
433        } else if days > u16::MAX.into() {
434            Err(ValidationError::OutOfBounds(format!(
435                "Number of days cannot exceed {}", days
436            )))
437        } else {
438            self.hide_after = Some(days as u16);
439            Ok(self)
440        }
441    }
442
443    /// The number of days to delete a file after it was hidden.
444    ///
445    /// The supplied duration will be truncated to whole days. If provided, the
446    /// number of days must be at least one.
447    ///
448    /// The maximum number of days supported is [u16::MAX].
449    ///
450    /// # Notes
451    ///
452    /// The B2 service automatically hides files when a file with the same is
453    /// uploaded (e.g., when a file changes). Files can also be explicitly
454    /// hidden via [hide_file](crate::file::hide_file).
455    pub fn delete_after_hide(mut self, days: chrono::Duration)
456    -> Result<Self, ValidationError> {
457        let days = days.num_days();
458
459        if days < 1 {
460            Err(ValidationError::OutOfBounds(
461                "Number of days must be greater than zero".into()
462            ))
463        } else if days > u16::MAX.into() {
464            Err(ValidationError::OutOfBounds(format!(
465                "Number of days cannot exceed {}", days
466            )))
467        } else {
468            self.delete_after = Some(days as u16);
469            Ok(self)
470        }
471    }
472
473    /// Create a [LifecycleRule].
474    ///
475    /// # Errors
476    ///
477    /// Returns [ValidationError::MissingData] if no filename prefix is
478    /// provided, or [ValidationError::Incompatible] if the rule does not have
479    /// at least one of a [hide_after_upload](Self::hide_after_upload) or
480    /// [delete_after_hide](Self::delete_after_hide) rule set.
481    pub fn build(self) -> Result<LifecycleRule, ValidationError> {
482        if self.prefix.is_none() {
483            Err(ValidationError::MissingData(
484                "Rule must have a filename prefix".into()
485            ))
486        } else if self.hide_after.is_none() && self.delete_after.is_none() {
487            Err(ValidationError::Incompatible(
488                "The rule must have at least one of a hide or deletion rule"
489                    .into()
490            ))
491        } else {
492            Ok(LifecycleRule {
493                file_name_prefix: self.prefix.unwrap().to_owned(),
494                delete_after: self.delete_after,
495                hide_after: self.hide_after,
496            })
497        }
498    }
499}
500
501/// Valid encryption algorithms for server-side encryption.
502///
503/// AES256 is the only supported algorithm.
504#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
505pub enum EncryptionAlgorithm {
506    #[serde(rename = "AES256")]
507    Aes256,
508}
509
510impl fmt::Display for EncryptionAlgorithm {
511    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
512        write!(f, "AES256")
513    }
514}
515
516/// Configuration for client-managed server-side encryption.
517#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
518#[serde(try_from = "serialization::InnerSelfEncryption")]
519#[serde(into = "serialization::InnerSelfEncryption")]
520pub struct SelfManagedEncryption {
521    pub(crate) algorithm: EncryptionAlgorithm,
522    pub(crate) key: String,
523    pub(crate) digest: String,
524}
525
526impl SelfManagedEncryption {
527    pub fn new(algorithm: EncryptionAlgorithm, key: impl Into<String>)
528    -> Self {
529        let key = key.into();
530
531        let digest = md5::compute(key.as_bytes());
532        let digest = base64::encode(digest.0);
533
534        let key = base64::encode(key.as_bytes());
535
536        Self {
537            algorithm,
538            key,
539            digest,
540        }
541    }
542}
543
544/// Configuration for server-side encryption.
545#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
546#[serde(try_from = "serialization::InnerEncryptionConfig")]
547#[serde(into = "serialization::InnerEncryptionConfig")]
548pub enum ServerSideEncryption {
549    /// Let the B2 service manage encryption settings.
550    B2Managed(EncryptionAlgorithm),
551    /// Provide the encryption configuration for the B2 service to use.
552    SelfManaged(SelfManagedEncryption),
553    /// Do not encrypt the bucket or file.
554    NoEncryption,
555}
556
557impl Default for ServerSideEncryption {
558    fn default() -> Self {
559        Self::NoEncryption
560    }
561}
562
563impl ServerSideEncryption {
564    /// Generate the headers required when uploading files.
565    pub(crate) fn to_headers(&self) -> Option<Vec<(&'static str, Cow<str>)>> {
566        match self {
567            Self::B2Managed(enc) => {
568                Some(vec![
569                    ("X-Bz-Server-Side-Encryption", Cow::from(enc.to_string()))
570                ])
571            },
572            Self::SelfManaged(enc) => {
573                Some(vec![
574                    (
575                        "X-Bz-Server-Side-Encryption",
576                        Cow::from(enc.algorithm.to_string())
577                    ),
578                    (
579                        "X-Bz-Server-Side-Encryption-Customer-Key",
580                        Cow::from(&enc.key)
581                    ),
582                    (
583                        "X-Bz-Server-Side-Encryption-Customer-Key-Md5",
584                        Cow::from(&enc.digest)
585                    )
586                ])
587            },
588            Self::NoEncryption => None,
589        }
590    }
591}
592
593/// A request to create a new bucket.
594///
595/// Use [CreateBucketBuilder] to create a `CreateBucket`, then pass it to
596/// [create_bucket].
597#[derive(Debug, Serialize)]
598#[serde(rename_all = "camelCase")]
599pub struct CreateBucket<'a> {
600    // account_id is provided by an Authorization.
601    account_id: Option<&'a str>,
602    bucket_name: String,
603    bucket_type: BucketType,
604    #[serde(skip_serializing_if = "Option::is_none")]
605    bucket_info: Option<serde_json::Value>,
606    #[serde(skip_serializing_if = "Option::is_none")]
607    cors_rules: Option<Vec<CorsRule>>,
608    file_lock_enabled: bool,
609    #[serde(skip_serializing_if = "Option::is_none")]
610    lifecycle_rules: Option<Vec<LifecycleRule>>,
611    #[serde(skip_serializing_if = "Option::is_none")]
612    default_server_side_encryption: Option<ServerSideEncryption>,
613}
614
615impl<'a> CreateBucket<'a> {
616    pub fn builder() -> CreateBucketBuilder {
617        CreateBucketBuilder::default()
618    }
619}
620
621/// A builder for a [CreateBucket].
622///
623/// After creating the request, pass it to [create_bucket].
624///
625/// See <https://www.backblaze.com/b2/docs/b2_create_bucket.html> for further
626/// information.
627#[derive(Default)]
628pub struct CreateBucketBuilder {
629    bucket_name: Option<String>,
630    bucket_type: Option<BucketType>,
631    bucket_info: Option<serde_json::Value>,
632    cache_control: Option<String>,
633    cors_rules: Option<Vec<CorsRule>>,
634    file_lock_enabled: bool,
635    lifecycle_rules: Option<Vec<LifecycleRule>>,
636    default_server_side_encryption: Option<ServerSideEncryption>,
637}
638
639impl CreateBucketBuilder {
640    /// Create the bucket with the specified name.
641    ///
642    /// Bucket names:
643    ///
644    /// * must be globally unique
645    /// * cmust be ontain only ASCII alphanumeric text and `-`
646    /// * must be between 6 and 50 characters inclusive
647    /// * must not begin with `b2-`
648    pub fn name(mut self, name: impl Into<String>)
649    -> Result<Self, BucketValidationError> {
650        let name = validated_bucket_name(name)?;
651        self.bucket_name = Some(name);
652        Ok(self)
653    }
654
655    /// Create the bucket with the given [BucketType].
656    pub fn bucket_type(mut self, typ: BucketType)
657    -> Result<Self, ValidationError> {
658        if matches!(typ, BucketType::Snapshot) {
659            return Err(ValidationError::OutOfBounds(
660                "Bucket type must be either Public or Private".into()
661            ));
662        }
663
664        self.bucket_type = Some(typ);
665        Ok(self)
666    }
667
668    /// Use the provided information with the bucket.
669    ///
670    /// This can contain arbitrary metadata for your own use. You can also set
671    /// cache-control settings from here (but see
672    /// [cache_control](Self::cache_control)). If Cache-Control is set here and
673    /// via the `cache-control` method, the latter will override this value.
674    // TODO: Validate CORS rules if provided.
675    pub fn bucket_info(mut self, info: serde_json::Value)
676    -> Result<Self, ValidationError> {
677        if info.is_object() {
678            self.bucket_info = Some(info);
679            Ok(self)
680        } else {
681            Err(ValidationError::BadFormat(
682                "Bucket info must be a JSON object".into()
683            ))
684        }
685    }
686
687    /// Set the default Cache-Control header value for files downloaded from the
688    /// bucket.
689    pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
690        self.cache_control = Some(cache_control.value().to_string());
691        self
692    }
693
694    /// Use the provided CORS rules for the bucket.
695    ///
696    /// See <https://www.backblaze.com/b2/docs/cors_rules.html> for further
697    /// information.
698    pub fn cors_rules(mut self, rules: impl Into<Vec<CorsRule>>)
699    -> Result<Self, ValidationError> {
700        let rules = rules.into();
701
702        if rules.len() > 100 {
703            return Err(ValidationError::OutOfBounds(
704                "A bucket can have no more than 100 CORS rules".into()
705            ));
706        } else if ! rules.is_empty() {
707            self.cors_rules = Some(rules);
708        }
709
710        Ok(self)
711    }
712
713    /// Enable the file lock on the bucket.
714    ///
715    /// See <https://www.backblaze.com/b2/docs/file_lock.html> for further
716    /// information.
717    pub fn with_file_lock(mut self) -> Self {
718        self.file_lock_enabled = true;
719        self
720    }
721
722    /// Disable the file lock on the bucket.
723    ///
724    /// This is the default.
725    pub fn without_file_lock(mut self) -> Self {
726        self.file_lock_enabled = false;
727        self
728    }
729
730    /// Use the provided list of [LifecycleRule]s for the bucket.
731    ///
732    /// No file within a bucket can be subject to multiple lifecycle rules. If
733    /// any of the rules provided apply to multiple files or folders, we return
734    /// a [LifecycleRuleValidationError::ConflictingRules] with a list of the
735    /// conflicts.
736    ///
737    /// The empty string (`""`) matches all paths, so if provided it must be the
738    /// only lifecycle rule. If it is provided along with other rules, all of
739    /// those rules will be listed as a conflict.
740    ///
741    /// # Examples
742    ///
743    /// For the following input:
744    ///
745    /// ```ignore
746    /// [
747    ///     "Docs/Photos/",
748    ///     "Legal/",
749    ///     "Legal/Taxes/",
750    ///     "Archive/",
751    ///     "Archive/Temporary/",
752    /// ]
753    /// ```
754    ///
755    /// You will receive the error output:
756    ///
757    /// ```ignore
758    /// {
759    ///     "Legal/": [ "Legal/Taxes/" ],
760    ///     "Archive/": [ "Archive/Temporary/" ],
761    /// }
762    /// ```
763    ///
764    /// For the following input:
765    ///
766    /// ```ignore
767    /// [
768    ///     "Docs/Photos/",
769    ///     "Docs/",
770    ///     "Docs/Documents/",
771    ///     "Legal/Taxes/",
772    ///     "Docs/Photos/Vacations/",
773    ///     "Archive/",
774    /// ]
775    /// ```
776    ///
777    /// You will receive the error output (note the redundant listing):
778    ///
779    /// ```ignore
780    /// {
781    ///     "Docs/": [
782    ///         "Docs/Documents/",
783    ///         "Docs/Photos/",
784    ///         "Docs/Photos/Vacations/",
785    ///     ],
786    ///     "Docs/Photos/": [ "Docs/Photos/Vacations/" ],
787    /// }
788    /// ```
789    pub fn lifecycle_rules(mut self, rules: impl Into<Vec<LifecycleRule>>)
790    -> Result<Self, LifecycleRuleValidationError> {
791        let rules = validated_lifecycle_rules(rules)?;
792        self.lifecycle_rules = Some(rules);
793
794        Ok(self)
795    }
796
797    /// Use the provided encryption settings on the bucket.
798    pub fn encryption_settings(mut self, settings: ServerSideEncryption) -> Self
799    {
800        self.default_server_side_encryption = Some(settings);
801        self
802    }
803
804    /// Create a [CreateBucket].
805    pub fn build<'a>(self) -> Result<CreateBucket<'a>, ValidationError> {
806        let bucket_name = self.bucket_name.ok_or_else(||
807            ValidationError::MissingData(
808                "The bucket must have a name".into()
809            )
810        )?;
811
812        let bucket_type = self.bucket_type.ok_or_else(||
813            ValidationError::MissingData(
814                "The bucket must have a type set".into()
815            )
816        )?;
817
818        let bucket_info = if let Some(cache_control) = self.cache_control {
819            let mut info = self.bucket_info.unwrap_or_else(||
820                serde_json::Value::Object(serde_json::Map::new())
821            );
822
823            info.as_object_mut()
824                .map(|map| map.insert(
825                    String::from("Cache-Control"),
826                    serde_json::Value::String(cache_control)
827                ));
828
829            Some(info)
830        } else {
831            self.bucket_info
832        };
833
834        Ok(CreateBucket {
835            account_id: None,
836            bucket_name,
837            bucket_type,
838            bucket_info,
839            cors_rules: self.cors_rules,
840            file_lock_enabled: self.file_lock_enabled,
841            lifecycle_rules: self.lifecycle_rules,
842            default_server_side_encryption: self.default_server_side_encryption,
843        })
844    }
845}
846
847/// Information from B2 concerning a file's retention settings.
848#[derive(Debug, Deserialize)]
849pub struct FileLockConfiguration {
850    #[serde(rename = "isClientAuthorizedToRead")]
851    can_read: bool,
852    #[serde(rename = "isFileLockEnabled")]
853    file_lock_enabled: bool,
854    #[serde(rename = "value")]
855    retention: FileRetentionPolicy,
856}
857
858impl FileLockConfiguration {
859    /// Check whether a file lock is enabled.
860    ///
861    /// If not authorized to read the file lock configuration, returns `None`.
862    pub fn lock_is_enabled(&self) -> Option<bool> {
863        if self.can_read {
864            Some(self.file_lock_enabled)
865        } else {
866            None
867        }
868    }
869
870    /// Get the file lock's retention policy.
871    ///
872    /// If not authorized to read the file lock configuration, returns `None`.
873    pub fn retention_policy(&self) -> Option<FileRetentionPolicy> {
874        if self.can_read {
875            Some(self.retention)
876        } else {
877            None
878        }
879    }
880}
881
882/// The B2 mode of a file's retention policy.
883#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
884#[serde(rename_all = "lowercase")]
885pub enum FileRetentionMode {
886    Governance,
887    Compliance,
888}
889
890impl fmt::Display for FileRetentionMode {
891    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
892        match self {
893            Self::Governance => write!(f, "governance"),
894            Self::Compliance => write!(f, "compliance"),
895        }
896    }
897}
898
899#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
900enum PeriodUnit { Days, Years }
901
902#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
903struct Period { duration: u32, unit: PeriodUnit }
904
905impl From<Period> for chrono::Duration {
906    fn from(other: Period) -> Self {
907        match other.unit {
908            PeriodUnit::Days => Self::days(other.duration as i64),
909            PeriodUnit::Years => Self::weeks(other.duration as i64 * 52),
910        }
911    }
912}
913
914impl From<chrono::Duration> for Period {
915    fn from(other: chrono::Duration) -> Self {
916        Self {
917            duration: other.num_days() as u32,
918            unit: PeriodUnit::Days,
919        }
920    }
921}
922
923/// A file's B2 retention policy.
924#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
925pub struct FileRetentionPolicy {
926    // `mode` and `period` must either both be set or both be (explicitly) null
927    // in the JSON we send to B2.
928    mode: Option<FileRetentionMode>,
929    period: Option<Period>,
930}
931
932impl FileRetentionPolicy {
933    pub fn new(mode: FileRetentionMode, duration: chrono::Duration) -> Self {
934        Self {
935            mode: Some(mode),
936            period: Some(duration.into()),
937        }
938    }
939
940    pub fn mode(&self) -> Option<FileRetentionMode> { self.mode }
941
942    pub fn period(&self) -> Option<chrono::Duration> {
943        self.period.map(|p| p.into())
944    }
945}
946
947/// Response from B2 with the configured bucket encryption settings.
948#[derive(Debug, Deserialize)]
949#[serde(rename_all = "camelCase")]
950pub struct BucketEncryptionInfo {
951    is_client_authorized_to_read: bool,
952    value: Option<ServerSideEncryption>,
953}
954
955impl BucketEncryptionInfo {
956    /// True if the authorization token allows access to the encryption
957    /// settings.
958    ///
959    /// If this is `false`, then `settings` will return `None`.
960    pub fn can_read(&self) -> bool { self.is_client_authorized_to_read }
961
962    /// The [ServerSideEncryption] configuration on the bucket.
963    pub fn settings(&self) -> Option<&ServerSideEncryption> {
964        self.value.as_ref()
965    }
966}
967
968/// A B2 bucket
969#[derive(Debug, Deserialize)]
970#[serde(rename_all = "camelCase")]
971pub struct Bucket {
972    account_id: String,
973    pub(crate) bucket_id: String,
974    bucket_name: String,
975    bucket_type: BucketType,
976    bucket_info: serde_json::Value,
977    cors_rules: Vec<CorsRule>,
978    file_lock_configuration: FileRetentionPolicy,
979    default_server_side_encryption: BucketEncryptionInfo,
980    lifecycle_rules: Vec<LifecycleRule>,
981    revision: u16,
982    options: Option<Vec<String>>,
983}
984
985impl Bucket {
986    pub fn account_id(&self) -> &str { &self.account_id }
987    pub fn bucket_id(&self) -> &str { &self.bucket_id }
988    pub fn name(&self) -> &str { &self.bucket_name }
989    pub fn bucket_type(&self) -> BucketType { self.bucket_type }
990    pub fn info(&self) -> &serde_json::Value { &self.bucket_info }
991    pub fn cors_rules(&self) -> &[CorsRule] { &self.cors_rules }
992
993    pub fn retention_policy(&self) -> FileRetentionPolicy {
994        self.file_lock_configuration
995    }
996
997    pub fn encryption_info(&self) -> &BucketEncryptionInfo {
998        &self.default_server_side_encryption
999    }
1000
1001    pub fn lifecycle_rules(&self) -> &[LifecycleRule] { &self.lifecycle_rules }
1002    pub fn revision(&self) -> u16 { self.revision }
1003    pub fn options(&self) -> Option<&Vec<String>> { self.options.as_ref() }
1004}
1005
1006/// Create a new [Bucket].
1007pub async fn create_bucket<C, E>(
1008    auth: &mut Authorization<C>,
1009    new_bucket_info: CreateBucket<'_>
1010) -> Result<Bucket, Error<E>>
1011    where C: HttpClient<Error=Error<E>>,
1012          E: fmt::Debug + fmt::Display,
1013{
1014    require_capability!(auth, Capability::WriteBuckets);
1015    if new_bucket_info.file_lock_enabled {
1016        require_capability!(auth, Capability::WriteBucketRetentions);
1017    }
1018    if new_bucket_info.default_server_side_encryption.is_some() {
1019        require_capability!(auth, Capability::WriteBucketEncryption);
1020    }
1021
1022    let mut new_bucket_info = new_bucket_info;
1023    new_bucket_info.account_id = Some(&auth.account_id);
1024
1025    let res = auth.client.post(auth.api_url("b2_create_bucket"))
1026        .expect("Invalid URL")
1027        .with_header("Authorization", &auth.authorization_token).unwrap()
1028        .with_body_json(serde_json::to_value(new_bucket_info)?)
1029        .send().await?;
1030
1031    let new_bucket: B2Result<Bucket> = serde_json::from_slice(&res)?;
1032    new_bucket.into()
1033}
1034
1035/// Delete the bucket with the given ID.
1036///
1037/// Returns a [Bucket] with the information of the newly-deleted bucket.
1038///
1039/// See <https://www.backblaze.com/b2/docs/b2_delete_bucket.html> for further
1040/// information.
1041pub async fn delete_bucket<C, E>(
1042    auth: &mut Authorization<C>,
1043    bucket_id: impl AsRef<str>
1044) -> Result<Bucket, Error<E>>
1045    where C: HttpClient<Error=Error<E>>,
1046          E: fmt::Debug + fmt::Display,
1047{
1048    require_capability!(auth, Capability::DeleteBuckets);
1049
1050    let res = auth.client.post(auth.api_url("b2_delete_bucket"))
1051        .expect("Invalid URL")
1052        .with_header("Authorization", &auth.authorization_token).unwrap()
1053        .with_body_json(serde_json::json!({
1054            "accountId": &auth.account_id,
1055            "bucketId": bucket_id.as_ref(),
1056        }))
1057        .send().await?;
1058
1059    let new_bucket: B2Result<Bucket> = serde_json::from_slice(&res)?;
1060    new_bucket.into()
1061}
1062
1063// The B2 API intention is that only an ID or name is supplied when listing
1064// buckets.
1065#[derive(Debug, Clone, Serialize)]
1066#[serde(untagged)]
1067enum BucketRef {
1068    Id(String),
1069    Name(String),
1070}
1071
1072#[derive(Debug, Clone, Copy)]
1073enum BucketFilter {
1074    Type(BucketType),
1075    All,
1076}
1077
1078impl From<&BucketType> for BucketFilter {
1079    fn from(t: &BucketType) -> Self {
1080        Self::Type(*t)
1081    }
1082}
1083
1084impl fmt::Display for BucketFilter {
1085    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1086        match self {
1087            Self::Type(t) => t.fmt(f),
1088            Self::All => write!(f, "all"),
1089        }
1090    }
1091}
1092
1093/// A request to list one or all buckets.
1094///
1095/// Pass the `ListBuckets` object to [list_buckets] to obtain the desired bucket
1096/// information.
1097#[derive(Debug, Clone, Serialize)]
1098#[serde(into = "serialization::InnerListBuckets")]
1099pub struct ListBuckets<'a> {
1100    account_id: Option<&'a str>,
1101    #[serde(skip_serializing_if = "Option::is_none")]
1102    bucket: Option<BucketRef>,
1103    #[serde(skip_serializing_if = "Option::is_none")]
1104    bucket_types: Option<Vec<BucketFilter>>,
1105}
1106
1107impl<'a> ListBuckets<'a> {
1108    pub fn builder() -> ListBucketsBuilder {
1109        ListBucketsBuilder::default()
1110    }
1111}
1112
1113/// A builder for a [ListBuckets] request.
1114#[derive(Default)]
1115pub struct ListBucketsBuilder {
1116    bucket: Option<BucketRef>,
1117    bucket_types: Option<Vec<BucketFilter>>,
1118}
1119
1120impl ListBucketsBuilder {
1121    /// If provided, only list the bucket with the specified ID.
1122    ///
1123    /// This is mutually exclusive with [Self::bucket_name].
1124    pub fn bucket_id(mut self, id: impl Into<String>) -> Self {
1125        self.bucket = Some(BucketRef::Id(id.into()));
1126        self
1127    }
1128
1129    /// If provided, only list the bucket with the specified name.
1130    ///
1131    /// This is mutually exclusive with [Self::bucket_id].
1132    pub fn bucket_name(mut self, name: impl Into<String>)
1133    -> Result<Self, BucketValidationError> {
1134        let name = validated_bucket_name(name)?;
1135
1136        self.bucket = Some(BucketRef::Name(name));
1137        Ok(self)
1138    }
1139
1140    /// If provided, only list buckets of the specified [BucketType]s.
1141    ///
1142    /// By default, all buckets are listed.
1143    pub fn bucket_types(mut self, types: &[BucketType]) -> Self {
1144        let types = types.iter().map(BucketFilter::from).collect();
1145
1146        self.bucket_types = Some(types);
1147        self
1148    }
1149
1150    /// List all bucket types.
1151    pub fn with_all_bucket_types(mut self) -> Self {
1152        self.bucket_types = Some(vec![BucketFilter::All]);
1153        self
1154    }
1155
1156    /// Create a [ListBuckets].
1157    pub fn build<'a>(self) -> ListBuckets<'a> {
1158        ListBuckets {
1159            account_id: None,
1160            bucket: self.bucket,
1161            bucket_types: self.bucket_types,
1162        }
1163    }
1164}
1165
1166#[derive(Debug, Default, Deserialize)]
1167struct BucketList {
1168    buckets: Vec<Bucket>,
1169}
1170
1171/// List buckets accessible by the [Authorization] according to the filter
1172/// provided by a [ListBuckets] object.
1173///
1174/// If your `Authorization` only has access to one bucket, then attempting to
1175/// list all buckets will result in an error with
1176/// [ErrorCode::Unauthorized](crate::error::ErrorCode::Unauthorized).
1177pub async fn list_buckets<C, E>(
1178    auth: &mut Authorization<C>,
1179    list_info: ListBuckets<'_>
1180) -> Result<Vec<Bucket>, Error<E>>
1181    where C: HttpClient<Error=Error<E>>,
1182          E: fmt::Debug + fmt::Display,
1183{
1184    require_capability!(auth, Capability::ListBuckets);
1185
1186    let mut list_info = list_info;
1187    list_info.account_id = Some(&auth.account_id);
1188
1189    let res = auth.client.post(auth.api_url("b2_list_buckets"))
1190        .expect("Invalid URL")
1191        .with_header("Authorization", &auth.authorization_token).unwrap()
1192        .with_body_json(serde_json::to_value(list_info)?)
1193        .send().await?;
1194
1195    let buckets: B2Result<BucketList> = serde_json::from_slice(&res)?;
1196    buckets.map(|b| b.buckets).into()
1197}
1198
1199/// A request to update one or more settings on a [Bucket].
1200#[derive(Debug, Serialize)]
1201#[serde(rename_all = "camelCase")]
1202pub struct UpdateBucket<'a> {
1203    account_id: Option<&'a str>,
1204    bucket_id: String,
1205    #[serde(skip_serializing_if = "Option::is_none")]
1206    bucket_type: Option<BucketType>,
1207    #[serde(skip_serializing_if = "Option::is_none")]
1208    bucket_info: Option<serde_json::Value>,
1209    #[serde(skip_serializing_if = "Option::is_none")]
1210    cors_rules: Option<Vec<CorsRule>>,
1211    #[serde(skip_serializing_if = "Option::is_none")]
1212    default_retention: Option<FileRetentionPolicy>,
1213    #[serde(skip_serializing_if = "Option::is_none")]
1214    default_server_side_encryption: Option<ServerSideEncryption>,
1215    #[serde(skip_serializing_if = "Option::is_none")]
1216    lifecycle_rules: Option<Vec<LifecycleRule>>,
1217    #[serde(skip_serializing_if = "Option::is_none")]
1218    if_revision_is: Option<u16>,
1219}
1220
1221impl<'a> UpdateBucket<'a> {
1222    pub fn builder() -> UpdateBucketBuilder {
1223        UpdateBucketBuilder::default()
1224    }
1225}
1226
1227/// A builder to create an [UpdateBucket] request.
1228#[derive(Default)]
1229pub struct UpdateBucketBuilder {
1230    bucket_id: Option<String>,
1231    bucket_type: Option<BucketType>,
1232    bucket_info: Option<serde_json::Value>,
1233    cache_control: Option<String>,
1234    cors_rules: Option<Vec<CorsRule>>,
1235    default_retention: Option<FileRetentionPolicy>,
1236    default_server_side_encryption: Option<ServerSideEncryption>,
1237    lifecycle_rules: Option<Vec<LifecycleRule>>,
1238    if_revision_is: Option<u16>,
1239}
1240
1241impl UpdateBucketBuilder {
1242    /// The ID of the bucket to update.
1243    ///
1244    /// This is required.
1245    pub fn bucket_id(mut self, bucket_id: impl Into<String>) -> Self {
1246        self.bucket_id = Some(bucket_id.into());
1247        self
1248    }
1249
1250    /// Change the bucket's [type](BucketType) to the one provided.
1251    pub fn bucket_type(mut self, typ: BucketType)
1252    -> Result<Self, ValidationError> {
1253        if matches!(typ, BucketType::Snapshot) {
1254            return Err(ValidationError::OutOfBounds(
1255                "Bucket type must be either Public or Private".into()
1256            ));
1257        }
1258
1259        self.bucket_type = Some(typ);
1260        Ok(self)
1261    }
1262
1263    /// Replace the current bucket information with the specified information.
1264    ///
1265    /// This can contain arbitrary metadata for your own use. You can also set
1266    /// cache-control settings from here (but see
1267    /// [cache_control](Self::cache_control)). If Cache-Control is set here and
1268    /// via the `cache-control` method, the latter will override this value.
1269    pub fn bucket_info(mut self, info: serde_json::Value)
1270    -> Self {
1271        self.bucket_info = Some(info);
1272        self
1273    }
1274
1275    /// Set the default Cache-Control header value for files downloaded from the
1276    /// bucket.
1277    pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
1278        self.cache_control = Some(cache_control.value().to_string());
1279        self
1280    }
1281
1282    /// Replace the bucket's current provided CORS rules with the provided
1283    /// rules.
1284    ///
1285    /// See <https://www.backblaze.com/b2/docs/cors_rules.html> for further
1286    /// information.
1287    pub fn cors_rules(mut self, rules: impl Into<Vec<CorsRule>>)
1288    -> Result<Self, ValidationError> {
1289        let rules = rules.into();
1290
1291        if rules.len() > 100 {
1292            return Err(ValidationError::OutOfBounds(
1293                "A bucket can have no more than 100 CORS rules".into()
1294            ));
1295        } else if ! rules.is_empty() {
1296            self.cors_rules = Some(rules);
1297        }
1298
1299        Ok(self)
1300    }
1301
1302    /// Replace the bucket's default retention policy.
1303    ///
1304    /// The [Authorization] must have
1305    /// [Capability::WriteBucketRetentions](crate::account::Capability::WriteBucketRetentions).
1306    pub fn retention_policy(mut self, policy: FileRetentionPolicy) -> Self {
1307        self.default_retention = Some(policy);
1308        self
1309    }
1310
1311    /// Replace the bucket's server-side encryption settings.
1312    ///
1313    /// The [Authorization] must have
1314    /// [Capability::WriteBucketEncryption](crate::account::Capability::WriteBucketRetentions).
1315    pub fn encryption_settings(mut self, settings: ServerSideEncryption) -> Self
1316    {
1317        self.default_server_side_encryption = Some(settings);
1318        self
1319    }
1320
1321    /// Replace the bucket's lifecycle rules with the provided list.
1322    ///
1323    /// See the documentation for [CreateBucketBuilder::lifecycle_rules] for
1324    /// the lifecycle requirements and examples.
1325    pub fn lifecycle_rules(mut self, rules: impl Into<Vec<LifecycleRule>>)
1326    -> Result<Self, LifecycleRuleValidationError> {
1327        let rules = validated_lifecycle_rules(rules)?;
1328        self.lifecycle_rules = Some(rules);
1329
1330        Ok(self)
1331    }
1332
1333    /// Only perform the update if the bucket's current revision is the provided
1334    /// version.
1335    pub fn if_revision_is(mut self, revision: u16) -> Self {
1336        self.if_revision_is = Some(revision);
1337        self
1338    }
1339
1340    pub fn build<'a>(self) -> Result<UpdateBucket<'a>, ValidationError> {
1341        let bucket_id = self.bucket_id.ok_or_else(||
1342            ValidationError::MissingData(
1343                "The bucket ID to update must be specified".into()
1344            )
1345        )?;
1346
1347        let bucket_info = if let Some(cache_control) = self.cache_control {
1348            let mut info = self.bucket_info.unwrap_or_else(||
1349                serde_json::Value::Object(serde_json::Map::new())
1350            );
1351
1352            info.as_object_mut()
1353                .map(|map| map.insert(
1354                    String::from("Cache-Control"),
1355                    serde_json::Value::String(cache_control)
1356                ));
1357
1358            Some(info)
1359        } else {
1360            self.bucket_info
1361        };
1362
1363        Ok(UpdateBucket {
1364            account_id: None,
1365            bucket_id,
1366            bucket_type: self.bucket_type,
1367            bucket_info,
1368            cors_rules: self.cors_rules,
1369            default_retention: self.default_retention,
1370            default_server_side_encryption: self.default_server_side_encryption,
1371            lifecycle_rules: self.lifecycle_rules,
1372            if_revision_is: self.if_revision_is,
1373        })
1374    }
1375}
1376
1377/// Update one or more properties of a [Bucket].
1378///
1379/// See <https://www.backblaze.com/b2/docs/b2_update_bucket.html> for further
1380/// information.
1381pub async fn update_bucket<C, E>(
1382    auth: &mut Authorization<C>,
1383    bucket_info: UpdateBucket<'_>
1384) -> Result<Bucket, Error<E>>
1385    where C: HttpClient<Error=Error<E>>,
1386          E: fmt::Debug + fmt::Display,
1387{
1388    require_capability!(auth, Capability::WriteBuckets);
1389    if bucket_info.default_retention.is_some() {
1390        require_capability!(auth, Capability::WriteBucketRetentions);
1391    }
1392    if bucket_info.default_server_side_encryption.is_some() {
1393        require_capability!(auth, Capability::WriteBucketEncryption);
1394    }
1395
1396    let mut bucket_info = bucket_info;
1397    bucket_info.account_id = Some(&auth.account_id);
1398
1399    let res = auth.client.post(auth.api_url("b2_update_bucket"))
1400        .expect("Invalid URL")
1401        .with_header("Authorization", &auth.authorization_token).unwrap()
1402        .with_body_json(serde_json::to_value(bucket_info)?)
1403        .send().await?;
1404
1405    let bucket: B2Result<Bucket> = serde_json::from_slice(&res)?;
1406    bucket.into()
1407}
1408
1409mod serialization {
1410    //! Our public encryption configuration type is sufficiently different from
1411    //! the JSON that we cannot simply deserialize it. We use the types here as
1412    //! an intermediate step.
1413    //!
1414    //! I think we could use a manual Serialize impl; we're using these anyway
1415    //! for consistency.
1416
1417    use std::convert::TryFrom;
1418    use serde::{Serialize, Deserialize};
1419
1420
1421    #[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
1422    enum Mode {
1423        #[serde(rename = "SSE-B2")]
1424        B2Managed,
1425        #[serde(rename = "SSE-C")]
1426        SelfManaged,
1427    }
1428
1429    #[derive(Debug, Default, Serialize, Deserialize)]
1430    #[serde(rename_all = "camelCase")]
1431    pub(crate) struct InnerEncryptionConfig {
1432        mode: Option<Mode>,
1433        #[serde(skip_serializing_if = "Option::is_none")]
1434        algorithm: Option<super::EncryptionAlgorithm>,
1435        #[serde(skip_serializing_if = "Option::is_none")]
1436        customer_key: Option<String>,
1437        #[serde(skip_serializing_if = "Option::is_none")]
1438        customer_key_md5: Option<String>,
1439    }
1440
1441    impl TryFrom<InnerEncryptionConfig> for super::ServerSideEncryption {
1442        type Error = &'static str;
1443
1444        fn try_from(other: InnerEncryptionConfig) -> Result<Self, Self::Error> {
1445            if let Some(mode) = other.mode {
1446                if mode == Mode::B2Managed {
1447                    let algo = other.algorithm
1448                        .ok_or("Missing encryption algorithm")?;
1449
1450                    Ok(Self::B2Managed(algo))
1451                } else { // Mode::SelfManaged
1452                    let algorithm = other.algorithm
1453                        .ok_or("Missing encryption algorithm")?;
1454                    let key = other.customer_key
1455                        .ok_or("Missing encryption key")?;
1456                    let digest = other.customer_key_md5
1457                        .ok_or("Missing encryption key digest")?;
1458
1459                    Ok(Self::SelfManaged(
1460                        super::SelfManagedEncryption {
1461                            algorithm,
1462                            key,
1463                            digest,
1464                        }
1465                    ))
1466                }
1467            } else {
1468                Ok(Self::NoEncryption)
1469            }
1470        }
1471    }
1472
1473    impl From<super::ServerSideEncryption> for InnerEncryptionConfig {
1474        fn from(other: super::ServerSideEncryption) -> Self {
1475            match other {
1476                super::ServerSideEncryption::B2Managed(algorithm) => {
1477                    Self {
1478                        mode: Some(Mode::B2Managed),
1479                        algorithm: Some(algorithm),
1480                        ..Default::default()
1481                    }
1482                },
1483                super::ServerSideEncryption::SelfManaged(enc) => {
1484                    Self {
1485                        mode: Some(Mode::SelfManaged),
1486                        algorithm: Some(enc.algorithm),
1487                        customer_key: Some(enc.key),
1488                        customer_key_md5: Some(enc.digest),
1489                    }
1490                },
1491                super::ServerSideEncryption::NoEncryption => {
1492                    Self::default()
1493                },
1494            }
1495        }
1496    }
1497
1498    #[derive(Debug, Serialize, Deserialize)]
1499    #[serde(rename_all = "camelCase")]
1500    pub(crate) struct InnerSelfEncryption {
1501        mode: Mode,
1502        algorithm: super::EncryptionAlgorithm,
1503        customer_key: String,
1504        customer_key_md5: String,
1505    }
1506
1507    impl TryFrom<InnerSelfEncryption> for super::SelfManagedEncryption {
1508        type Error = &'static str;
1509
1510        fn try_from(other: InnerSelfEncryption) -> Result<Self, Self::Error> {
1511            if other.mode != Mode::SelfManaged {
1512                Err("Not a self-managed encryption configuration")
1513            } else {
1514                Ok(Self {
1515                    algorithm: other.algorithm,
1516                    key: other.customer_key,
1517                    digest: other.customer_key_md5,
1518                })
1519            }
1520        }
1521    }
1522
1523    impl From<super::SelfManagedEncryption> for InnerSelfEncryption {
1524        fn from(other: super::SelfManagedEncryption) -> Self {
1525            Self {
1526                mode: Mode::SelfManaged,
1527                algorithm: other.algorithm,
1528                customer_key: other.key,
1529                customer_key_md5: other.digest,
1530            }
1531        }
1532    }
1533
1534    #[derive(Debug, Serialize)]
1535    #[serde(rename_all = "camelCase")]
1536    pub(crate) struct InnerListBuckets<'a> {
1537        account_id: Option<&'a str>,
1538        #[serde(skip_serializing_if = "Option::is_none")]
1539        bucket_id: Option<String>,
1540        #[serde(skip_serializing_if = "Option::is_none")]
1541        bucket_name: Option<String>,
1542        #[serde(skip_serializing_if = "Option::is_none")]
1543        bucket_types: Option<Vec<String>>,
1544    }
1545
1546    impl<'a> From<super::ListBuckets<'a>> for InnerListBuckets<'a> {
1547        fn from(other: super::ListBuckets<'a>) -> Self {
1548            use super::BucketRef;
1549
1550            let (bucket_id, bucket_name) = if let Some(bucket) = other.bucket {
1551                match bucket {
1552                    BucketRef::Id(s) => (Some(s), None),
1553                    BucketRef::Name(s) => (None, Some(s)),
1554                }
1555            } else {
1556                (None, None)
1557            };
1558
1559            let bucket_types = other.bucket_types
1560                .map(|t| t.into_iter()
1561                    .map(|t| t.to_string()).collect()
1562                );
1563
1564            Self {
1565                account_id: other.account_id,
1566                bucket_id,
1567                bucket_name,
1568                bucket_types,
1569            }
1570        }
1571    }
1572}
1573
1574#[cfg(feature = "with_surf")]
1575#[cfg(test)]
1576mod tests_mocked {
1577    use super::*;
1578    use crate::{
1579        account::Capability,
1580        error::ErrorCode,
1581    };
1582    use surf_vcr::VcrMode;
1583
1584    use crate::test_utils::{create_test_auth, create_test_client};
1585
1586
1587    #[async_std::test]
1588    async fn create_bucket_success() -> anyhow::Result<()> {
1589        let client = create_test_client(
1590            VcrMode::Replay,
1591            "test_sessions/buckets.yaml",
1592            None, None
1593        ).await?;
1594
1595        let mut auth = create_test_auth(client, vec![Capability::WriteBuckets])
1596            .await;
1597
1598        let req = CreateBucket::builder()
1599            .name("testing-new-b2-client")?
1600            .bucket_type(BucketType::Private)?
1601            .lifecycle_rules(vec![
1602                LifecycleRule::builder()
1603                    .filename_prefix("my-files/")?
1604                    .delete_after_hide(chrono::Duration::days(5))?
1605                    .build()?
1606            ])?
1607            .build()?;
1608
1609        let bucket = create_bucket(&mut auth, req).await?;
1610        assert_eq!(bucket.bucket_name, "testing-new-b2-client");
1611
1612        Ok(())
1613    }
1614
1615    #[async_std::test]
1616    async fn create_bucket_already_exists() -> anyhow::Result<()> {
1617        // Rerunning this against the B2 API will only succeed if the bucket
1618        // already exists. An easy way to do it is to rerun the
1619        // create_bucket_success test above, then change the name here to match.
1620        //
1621        // We use a different name in this test so that we can use the same
1622        // cassette.
1623        let client = create_test_client(
1624            VcrMode::Replay,
1625            "test_sessions/buckets.yaml",
1626            None, None
1627        ).await?;
1628
1629        let mut auth = create_test_auth(client, vec![Capability::WriteBuckets])
1630            .await;
1631
1632        let req = CreateBucket::builder()
1633            .name("testing-b2-client")?
1634            .bucket_type(BucketType::Private)?
1635            .lifecycle_rules(vec![
1636                LifecycleRule::builder()
1637                    .filename_prefix("my-files/")?
1638                    .delete_after_hide(chrono::Duration::days(5))?
1639                    .build()?
1640            ])?
1641            .build()?;
1642
1643        match create_bucket(&mut auth, req).await.unwrap_err() {
1644            Error::B2(e) =>
1645                assert_eq!(e.code(), ErrorCode::DuplicateBucketName),
1646            e => panic!("Unexpected error: {:?}", e),
1647        }
1648
1649        Ok(())
1650    }
1651
1652    #[async_std::test]
1653    async fn delete_bucket_success() -> anyhow::Result<()> {
1654        // Rerunning this test against the B2 API will require updating the
1655        // bucket ID.
1656        let client = create_test_client(
1657            VcrMode::Replay,
1658            "test_sessions/buckets.yaml",
1659            None, None
1660        ).await?;
1661
1662        let mut auth = create_test_auth(client, vec![Capability::DeleteBuckets])
1663            .await;
1664
1665        let bucket = delete_bucket(&mut auth, "1df2dee6ab62f7f577c70e1a")
1666            .await?;
1667
1668        assert_eq!(bucket.bucket_name, "testing-new-b2-client");
1669
1670        Ok(())
1671    }
1672
1673    #[async_std::test]
1674    async fn delete_bucket_does_not_exist() -> anyhow::Result<()> {
1675        let client = create_test_client(
1676            VcrMode::Replay,
1677            "test_sessions/buckets.yaml",
1678            None, None
1679        ).await?;
1680
1681        let mut auth = create_test_auth(client, vec![Capability::DeleteBuckets])
1682            .await;
1683
1684        // B2 documentation says ErrorCode::BadRequest but this is what we get.
1685        match delete_bucket(&mut auth, "1234567").await.unwrap_err() {
1686            Error::B2(e) =>
1687                assert_eq!(e.code(), ErrorCode::BadBucketId),
1688            e => panic!("Unexpected error: {:?}", e),
1689        }
1690
1691        Ok(())
1692    }
1693
1694    #[async_std::test]
1695    async fn test_list_buckets() -> anyhow::Result<()> {
1696        let client = create_test_client(
1697            VcrMode::Replay,
1698            "test_sessions/buckets.yaml",
1699            None, None
1700        ).await?;
1701
1702        let mut auth = create_test_auth(client, vec![Capability::ListBuckets])
1703            .await;
1704
1705        let buckets_req = ListBuckets::builder()
1706            .bucket_name("testing-b2-client")?
1707            .build();
1708
1709        let buckets = list_buckets(&mut auth, buckets_req).await?;
1710
1711        assert_eq!(buckets.len(), 1);
1712        assert_eq!(buckets[0].bucket_name, "testing-b2-client");
1713
1714        Ok(())
1715    }
1716
1717    #[async_std::test]
1718    async fn update_bucket_success() -> anyhow::Result<()> {
1719        // To run this against the B2 API the bucket_id below needs to be
1720        // changed to a valid ID.
1721        let client = create_test_client(
1722            VcrMode::Replay,
1723            "test_sessions/buckets.yaml",
1724            None, None
1725        ).await?;
1726
1727        let mut auth = create_test_auth(client, vec![Capability::WriteBuckets])
1728            .await;
1729
1730        let req = UpdateBucket::builder()
1731            .bucket_id("8d625eb63be2775577c70e1a")
1732            .bucket_type(BucketType::Private)?
1733            .lifecycle_rules(vec![
1734                LifecycleRule::builder()
1735                    .filename_prefix("my-files/")?
1736                    .delete_after_hide(chrono::Duration::days(5))?
1737                    .build()?
1738            ])?
1739            .build()?;
1740
1741        let bucket = update_bucket(&mut auth, req).await?;
1742        assert_eq!(bucket.bucket_name, "testing-b2-client");
1743
1744        Ok(())
1745    }
1746
1747    #[async_std::test]
1748    async fn update_bucket_conflict() -> anyhow::Result<()> {
1749        // To run this against the B2 API the bucket_id below needs to be
1750        // changed to a valid ID.
1751        let client = create_test_client(
1752            VcrMode::Replay,
1753            "test_sessions/buckets.yaml",
1754            None, None
1755        ).await?;
1756
1757        let mut auth = create_test_auth(client, vec![Capability::WriteBuckets])
1758            .await;
1759
1760        let req = UpdateBucket::builder()
1761            .bucket_id("8d625eb63be2775577c70e1a")
1762            .bucket_type(BucketType::Private)?
1763            .if_revision_is(10)
1764            .build()?;
1765
1766        match update_bucket(&mut auth, req).await.unwrap_err() {
1767            Error::B2(e) =>
1768                assert_eq!(e.code(), ErrorCode::Conflict),
1769            e => panic!("Unexpected error: {:?}", e),
1770        }
1771
1772        Ok(())
1773    }
1774}
1775
1776#[cfg(test)]
1777mod tests {
1778    use super::*;
1779    use serde_json::{json, from_value, to_value};
1780
1781
1782    #[test]
1783    fn no_encryption_to_json() {
1784        assert_eq!(
1785            to_value(ServerSideEncryption::NoEncryption).unwrap(),
1786            json!({ "mode": Option::<String>::None })
1787        );
1788    }
1789
1790    #[test]
1791    fn no_encryption_from_json() {
1792        let enc: ServerSideEncryption = from_value(
1793            json!({ "mode": Option::<String>::None })
1794        ).unwrap();
1795
1796        assert_eq!(enc, ServerSideEncryption::NoEncryption);
1797    }
1798
1799    #[test]
1800    fn b2_encryption_to_json() {
1801        let json = to_value(
1802            ServerSideEncryption::B2Managed(EncryptionAlgorithm::Aes256)
1803        ).unwrap();
1804
1805        assert_eq!(json, json!({ "mode": "SSE-B2", "algorithm": "AES256" }));
1806    }
1807
1808    #[test]
1809    fn b2_encryption_from_json() {
1810        let enc: ServerSideEncryption = from_value(
1811            json!({ "mode": "SSE-B2", "algorithm": "AES256" })
1812        ).unwrap();
1813
1814        assert_eq!(
1815            enc,
1816            ServerSideEncryption::B2Managed(EncryptionAlgorithm::Aes256)
1817        );
1818    }
1819
1820    #[test]
1821    fn self_encryption_to_json() {
1822        let json = to_value(ServerSideEncryption::SelfManaged(
1823            SelfManagedEncryption {
1824                algorithm: EncryptionAlgorithm::Aes256,
1825                key: "MY-ENCODED-KEY".into(),
1826                digest: "ENCODED-DIGEST".into(),
1827            }
1828        )).unwrap();
1829
1830        assert_eq!(
1831            json,
1832            json!({
1833                "mode": "SSE-C",
1834                "algorithm": "AES256",
1835                "customerKey": "MY-ENCODED-KEY",
1836                "customerKeyMd5": "ENCODED-DIGEST",
1837            })
1838        );
1839    }
1840
1841    #[test]
1842    fn self_encryption_from_json() {
1843        let enc: ServerSideEncryption = from_value(
1844            json!({
1845                "mode": "SSE-C",
1846                "algorithm": "AES256",
1847                "customerKey": "MY-ENCODED-KEY",
1848                "customerKeyMd5": "ENCODED-DIGEST",
1849            })
1850        ).unwrap();
1851
1852        assert_eq!(
1853            enc,
1854            ServerSideEncryption::SelfManaged(
1855                SelfManagedEncryption {
1856                    algorithm: EncryptionAlgorithm::Aes256,
1857                    key: "MY-ENCODED-KEY".into(),
1858                    digest: "ENCODED-DIGEST".into(),
1859                }
1860            )
1861        );
1862    }
1863
1864    #[test]
1865    fn deserialize_new_bucket_response() {
1866        let info = json!({
1867            "accountId": "abcdefg",
1868            "bucketId": "hijklmno",
1869            "bucketInfo": {},
1870            "bucketName": "some-bucket-name",
1871            "bucketType": "allPrivate",
1872            "corsRules": [],
1873            "defaultServerSideEncryption": {
1874                "isClientAuthorizedToRead": true,
1875                "value": {
1876                    "algorithm": null,
1877                    "mode": null,
1878                },
1879            },
1880            "fileLockConfiguration": {
1881                "isClientAuthorizedToRead": true,
1882                "value": {
1883                    "defaultRetention": {
1884                        "mode": null,
1885                        "period": null,
1886                    },
1887                    "isFileLockEnabled": false,
1888                },
1889            },
1890            "lifecycleRules": [
1891                {
1892                    "daysFromHidingToDeleting": 5,
1893                    "daysFromUploadingToHiding": null,
1894                    "fileNamePrefix": "my-files",
1895                },
1896            ],
1897            "options": ["s3"],
1898            "revision": 2,
1899        });
1900
1901        let _: Bucket = from_value(info).unwrap();
1902    }
1903
1904    #[test]
1905    fn cors_rule_validates_origins() -> anyhow::Result<()> {
1906        let valid_origins = [
1907            vec!["https://*".into(), "http://*".into()],
1908            vec!["*".into()],
1909            vec![
1910                "https://example.com".into(), "http://example.com:1234".into()
1911            ],
1912            vec![
1913                "https".into(), "http".into(), "http://example.com:1234".into()
1914            ],
1915            vec![
1916                "https://*:8765".into(), "http://www.example.com:4545".into()
1917            ],
1918            vec![
1919                "https://*.example.com".into(), "http://www.example.com".into()
1920            ],
1921        ];
1922
1923        for origin_list in valid_origins {
1924            let _ = CorsRule::builder()
1925                .allowed_origins(origin_list)?;
1926        }
1927
1928        let bad_origins = [
1929            vec!["*".into(), "https://*".into()],
1930            vec!["ftp://example.com".into()],
1931            vec!["ftp://*.*.example.com".into()],
1932            vec!["https://*:8765".into(), "www.example.com:4545".into()],
1933            vec![
1934                "https://*:8765".into(), "https://www.example.com:4545".into()
1935            ],
1936        ];
1937
1938        for origin_list in bad_origins {
1939            let rule = CorsRule::builder()
1940                .allowed_origins(origin_list);
1941
1942            assert!(rule.is_err(), "{:?}", rule);
1943        }
1944
1945        Ok(())
1946    }
1947
1948    // TODO: Test CorsRuleBuilder with allowed headers, etc.
1949}