use std::{borrow::Cow, fmt};
use crate::{
prelude::*,
client::HttpClient,
error::*,
validate::*,
};
use http_types::cache::CacheControl;
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BucketType {
#[serde(rename = "allPublic")]
Public,
#[serde(rename = "allPrivate")]
Private,
#[serde(rename = "snapshot")]
Snapshot,
}
impl fmt::Display for BucketType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Public => write!(f, "allPublic"),
Self::Private => write!(f, "allPrivate"),
Self::Snapshot => write!(f, "snapshot"),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub enum CorsOperation {
#[serde(rename = "b2_download_file_by_name")]
DownloadFileByName,
#[serde(rename = "b2_download_file_by_id")]
DownloadFileById,
#[serde(rename = "b2_upload_file")]
UploadFile,
#[serde(rename = "b2_upload_part")]
UploadPart,
#[serde(rename = "s3_delete")]
S3Delete,
#[serde(rename = "s3_get")]
S3Get,
#[serde(rename = "s3_head")]
S3Head,
#[serde(rename = "s3_post")]
S3Post,
#[serde(rename = "s3_put")]
S3Put,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CorsRule {
cors_rule_name: String,
allowed_origins: Vec<String>,
allowed_operations: Vec<CorsOperation>,
#[serde(skip_serializing_if = "Option::is_none")]
allowed_headers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
expose_headers: Option<Vec<String>>,
max_age_seconds: u16,
}
impl CorsRule {
pub fn builder() -> CorsRuleBuilder {
CorsRuleBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct CorsRuleBuilder {
name: Option<String>,
allowed_origins: Vec<String>,
allowed_operations: Vec<CorsOperation>,
allowed_headers: Option<Vec<String>>,
expose_headers: Option<Vec<String>>,
max_age: Option<u16>,
}
impl CorsRuleBuilder {
pub fn name(mut self, name: impl Into<String>)
-> Result<Self, CorsRuleValidationError> {
let name = validated_cors_rule_name(name)?;
self.name = Some(name);
Ok(self)
}
pub fn allowed_origins(mut self, origins: impl Into<Vec<String>>)
-> Result<Self, ValidationError> {
self.allowed_origins = validated_origins(origins)?;
Ok(self)
}
pub fn add_allowed_origin(mut self, origin: impl Into<String>)
-> Result<Self, ValidationError> {
let origin = origin.into();
self.allowed_origins.push(origin);
self.allowed_origins = validated_origins(self.allowed_origins)?;
Ok(self)
}
pub fn allowed_operations(mut self, ops: Vec<CorsOperation>)
-> Result<Self, ValidationError> {
if ops.is_empty() {
return Err(ValidationError::MissingData(
"There must be at least one origin covered by the rule".into()
));
}
self.allowed_operations = ops;
Ok(self)
}
pub fn add_allowed_operation(mut self, op: CorsOperation) -> Self {
self.allowed_operations.push(op);
self
}
pub fn allowed_headers<H>(mut self, headers: impl Into<Vec<String>>)
-> Result<Self, BadHeaderName> {
let headers = headers.into();
if ! headers.is_empty() {
for header in headers.iter() {
validated_http_header(header)?;
}
self.allowed_headers = Some(headers);
}
Ok(self)
}
pub fn add_allowed_header(mut self, header: impl Into<String>)
-> Result<Self, BadHeaderName> {
let header = header.into();
validated_http_header(&header)?;
let headers = self.allowed_headers.get_or_insert_with(Vec::new);
headers.push(header);
Ok(self)
}
pub fn exposed_headers(mut self, headers: impl Into<Vec<String>>)
-> Result<Self, BadHeaderName> {
let headers = headers.into();
if ! headers.is_empty() {
for header in headers.iter() {
validated_http_header(header)?;
}
self.expose_headers = Some(headers);
}
Ok(self)
}
pub fn add_exposed_header(mut self, header: impl Into<String>)
-> Result<Self, BadHeaderName> {
let header = header.into();
validated_http_header(&header)?;
let headers = self.expose_headers.get_or_insert_with(Vec::new);
headers.push(header);
Ok(self)
}
pub fn max_age(mut self, age: chrono::Duration)
-> Result<Self, ValidationError> {
if age < chrono::Duration::zero() || age > chrono::Duration::days(1) {
return Err(ValidationError::OutOfBounds(
"Age must be non-negative and no more than 1 day".into()
));
}
self.max_age = Some(age.num_seconds() as u16);
Ok(self)
}
pub fn build(self) -> Result<CorsRule, ValidationError> {
let cors_rule_name = self.name.ok_or_else(||
ValidationError::MissingData(
"The CORS rule must have a name".into()
)
)?;
let max_age_seconds = self.max_age.ok_or_else(||
ValidationError::MissingData(
"A maximum age for client caching must be specified".into()
)
)?;
if self.allowed_origins.is_empty() {
Err(ValidationError::MissingData(
"At least one origin must be allowed by the CORS rule".into()
))
} else if self.allowed_operations.is_empty() {
Err(ValidationError::MissingData(
"At least one operation must be specified".into()
))
} else {
let bytes: usize = cors_rule_name.len()
+ self.allowed_origins.iter().map(|s| s.len()).sum::<usize>()
+ self.allowed_operations.iter()
.map(|c| serde_json::to_string(c).unwrap().len())
.sum::<usize>()
+ self.allowed_headers.iter().map(|s| s.len()).sum::<usize>()
+ self.expose_headers.iter().map(|s| s.len()).sum::<usize>();
if bytes >= 1000 {
return Err(ValidationError::OutOfBounds(
"Maximum bytes of string data is 999".into()
));
}
Ok(CorsRule {
cors_rule_name,
allowed_origins: self.allowed_origins,
allowed_operations: self.allowed_operations,
allowed_headers: self.allowed_headers,
expose_headers: self.expose_headers,
max_age_seconds,
})
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LifecycleRule {
pub(crate) file_name_prefix: String,
#[serde(rename = "daysFromHidingToDeleting")]
delete_after: Option<u16>,
#[serde(rename = "daysFromUploadingToHiding")]
hide_after: Option<u16>,
}
impl std::cmp::PartialOrd for LifecycleRule {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.file_name_prefix.partial_cmp(&other.file_name_prefix)
}
}
impl std::cmp::Ord for LifecycleRule {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.file_name_prefix.cmp(&other.file_name_prefix)
}
}
impl LifecycleRule {
pub fn builder<'a>() -> LifecycleRuleBuilder<'a> {
LifecycleRuleBuilder::default()
}
}
#[derive(Default)]
pub struct LifecycleRuleBuilder<'a> {
prefix: Option<&'a str>,
delete_after: Option<u16>,
hide_after: Option<u16>,
}
impl<'a> LifecycleRuleBuilder<'a> {
pub fn filename_prefix(mut self, prefix: &'a str)
-> Result<Self, FileNameValidationError> {
self.prefix = Some(validated_file_name(prefix)?);
Ok(self)
}
pub fn hide_after_upload(mut self, days: chrono::Duration)
-> Result<Self, ValidationError> {
let days = days.num_days();
if days < 1 {
Err(ValidationError::OutOfBounds(
"Number of days must be greater than zero".into()
))
} else if days > u16::MAX.into() {
Err(ValidationError::OutOfBounds(format!(
"Number of days cannot exceed {}", days
)))
} else {
self.hide_after = Some(days as u16);
Ok(self)
}
}
pub fn delete_after_hide(mut self, days: chrono::Duration)
-> Result<Self, ValidationError> {
let days = days.num_days();
if days < 1 {
Err(ValidationError::OutOfBounds(
"Number of days must be greater than zero".into()
))
} else if days > u16::MAX.into() {
Err(ValidationError::OutOfBounds(format!(
"Number of days cannot exceed {}", days
)))
} else {
self.delete_after = Some(days as u16);
Ok(self)
}
}
pub fn build(self) -> Result<LifecycleRule, ValidationError> {
if self.prefix.is_none() {
Err(ValidationError::MissingData(
"Rule must have a filename prefix".into()
))
} else if self.hide_after.is_none() && self.delete_after.is_none() {
Err(ValidationError::Incompatible(
"The rule must have at least one of a hide or deletion rule"
.into()
))
} else {
Ok(LifecycleRule {
file_name_prefix: self.prefix.unwrap().to_owned(),
delete_after: self.delete_after,
hide_after: self.hide_after,
})
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
pub enum EncryptionAlgorithm {
#[serde(rename = "AES256")]
Aes256,
}
impl fmt::Display for EncryptionAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AES256")
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "serialization::InnerSelfEncryption")]
#[serde(into = "serialization::InnerSelfEncryption")]
pub struct SelfManagedEncryption {
pub(crate) algorithm: EncryptionAlgorithm,
pub(crate) key: String,
pub(crate) digest: String,
}
impl SelfManagedEncryption {
pub fn new(algorithm: EncryptionAlgorithm, key: impl Into<String>)
-> Self {
let key = key.into();
let digest = md5::compute(key.as_bytes());
let digest = base64::encode(digest.0);
let key = base64::encode(key.as_bytes());
Self {
algorithm,
key,
digest,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "serialization::InnerEncryptionConfig")]
#[serde(into = "serialization::InnerEncryptionConfig")]
pub enum ServerSideEncryption {
B2Managed(EncryptionAlgorithm),
SelfManaged(SelfManagedEncryption),
NoEncryption,
}
impl Default for ServerSideEncryption {
fn default() -> Self {
Self::NoEncryption
}
}
impl ServerSideEncryption {
pub(crate) fn to_headers(&self) -> Option<Vec<(&'static str, Cow<str>)>> {
match self {
Self::B2Managed(enc) => {
Some(vec![
("X-Bz-Server-Side-Encryption", Cow::from(enc.to_string()))
])
},
Self::SelfManaged(enc) => {
Some(vec![
(
"X-Bz-Server-Side-Encryption",
Cow::from(enc.algorithm.to_string())
),
(
"X-Bz-Server-Side-Encryption-Customer-Key",
Cow::from(&enc.key)
),
(
"X-Bz-Server-Side-Encryption-Customer-Key-Md5",
Cow::from(&enc.digest)
)
])
},
Self::NoEncryption => None,
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateBucket<'a> {
account_id: Option<&'a str>,
bucket_name: String,
bucket_type: BucketType,
#[serde(skip_serializing_if = "Option::is_none")]
bucket_info: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
cors_rules: Option<Vec<CorsRule>>,
file_lock_enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
lifecycle_rules: Option<Vec<LifecycleRule>>,
#[serde(skip_serializing_if = "Option::is_none")]
default_server_side_encryption: Option<ServerSideEncryption>,
}
impl<'a> CreateBucket<'a> {
pub fn builder() -> CreateBucketBuilder {
CreateBucketBuilder::default()
}
}
#[derive(Default)]
pub struct CreateBucketBuilder {
bucket_name: Option<String>,
bucket_type: Option<BucketType>,
bucket_info: Option<serde_json::Value>,
cache_control: Option<String>,
cors_rules: Option<Vec<CorsRule>>,
file_lock_enabled: bool,
lifecycle_rules: Option<Vec<LifecycleRule>>,
default_server_side_encryption: Option<ServerSideEncryption>,
}
impl CreateBucketBuilder {
pub fn name(mut self, name: impl Into<String>)
-> Result<Self, BucketValidationError> {
let name = validated_bucket_name(name)?;
self.bucket_name = Some(name);
Ok(self)
}
pub fn bucket_type(mut self, typ: BucketType)
-> Result<Self, ValidationError> {
if matches!(typ, BucketType::Snapshot) {
return Err(ValidationError::OutOfBounds(
"Bucket type must be either Public or Private".into()
));
}
self.bucket_type = Some(typ);
Ok(self)
}
pub fn bucket_info(mut self, info: serde_json::Value)
-> Result<Self, ValidationError> {
if info.is_object() {
self.bucket_info = Some(info);
Ok(self)
} else {
Err(ValidationError::BadFormat(
"Bucket info must be a JSON object".into()
))
}
}
pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
self.cache_control = Some(cache_control.value().to_string());
self
}
pub fn cors_rules(mut self, rules: impl Into<Vec<CorsRule>>)
-> Result<Self, ValidationError> {
let rules = rules.into();
if rules.len() > 100 {
return Err(ValidationError::OutOfBounds(
"A bucket can have no more than 100 CORS rules".into()
));
} else if ! rules.is_empty() {
self.cors_rules = Some(rules);
}
Ok(self)
}
pub fn with_file_lock(mut self) -> Self {
self.file_lock_enabled = true;
self
}
pub fn without_file_lock(mut self) -> Self {
self.file_lock_enabled = false;
self
}
pub fn lifecycle_rules(mut self, rules: impl Into<Vec<LifecycleRule>>)
-> Result<Self, LifecycleRuleValidationError> {
let rules = validated_lifecycle_rules(rules)?;
self.lifecycle_rules = Some(rules);
Ok(self)
}
pub fn encryption_settings(mut self, settings: ServerSideEncryption) -> Self
{
self.default_server_side_encryption = Some(settings);
self
}
pub fn build<'a>(self) -> Result<CreateBucket<'a>, ValidationError> {
let bucket_name = self.bucket_name.ok_or_else(||
ValidationError::MissingData(
"The bucket must have a name".into()
)
)?;
let bucket_type = self.bucket_type.ok_or_else(||
ValidationError::MissingData(
"The bucket must have a type set".into()
)
)?;
let bucket_info = if let Some(cache_control) = self.cache_control {
let mut info = self.bucket_info.unwrap_or_else(||
serde_json::Value::Object(serde_json::Map::new())
);
info.as_object_mut()
.map(|map| map.insert(
String::from("Cache-Control"),
serde_json::Value::String(cache_control)
));
Some(info)
} else {
self.bucket_info
};
Ok(CreateBucket {
account_id: None,
bucket_name,
bucket_type,
bucket_info,
cors_rules: self.cors_rules,
file_lock_enabled: self.file_lock_enabled,
lifecycle_rules: self.lifecycle_rules,
default_server_side_encryption: self.default_server_side_encryption,
})
}
}
#[derive(Debug, Deserialize)]
pub struct FileLockConfiguration {
#[serde(rename = "isClientAuthorizedToRead")]
can_read: bool,
#[serde(rename = "isFileLockEnabled")]
file_lock_enabled: bool,
#[serde(rename = "value")]
retention: FileRetentionPolicy,
}
impl FileLockConfiguration {
pub fn lock_is_enabled(&self) -> Option<bool> {
if self.can_read {
Some(self.file_lock_enabled)
} else {
None
}
}
pub fn retention_policy(&self) -> Option<FileRetentionPolicy> {
if self.can_read {
Some(self.retention)
} else {
None
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FileRetentionMode {
Governance,
Compliance,
}
impl fmt::Display for FileRetentionMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Governance => write!(f, "governance"),
Self::Compliance => write!(f, "compliance"),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
enum PeriodUnit { Days, Years }
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
struct Period { duration: u32, unit: PeriodUnit }
impl From<Period> for chrono::Duration {
fn from(other: Period) -> Self {
match other.unit {
PeriodUnit::Days => Self::days(other.duration as i64),
PeriodUnit::Years => Self::weeks(other.duration as i64 * 52),
}
}
}
impl From<chrono::Duration> for Period {
fn from(other: chrono::Duration) -> Self {
Self {
duration: other.num_days() as u32,
unit: PeriodUnit::Days,
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct FileRetentionPolicy {
mode: Option<FileRetentionMode>,
period: Option<Period>,
}
impl FileRetentionPolicy {
pub fn new(mode: FileRetentionMode, duration: chrono::Duration) -> Self {
Self {
mode: Some(mode),
period: Some(duration.into()),
}
}
pub fn mode(&self) -> Option<FileRetentionMode> { self.mode }
pub fn period(&self) -> Option<chrono::Duration> {
self.period.map(|p| p.into())
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BucketEncryptionInfo {
is_client_authorized_to_read: bool,
value: Option<ServerSideEncryption>,
}
impl BucketEncryptionInfo {
pub fn can_read(&self) -> bool { self.is_client_authorized_to_read }
pub fn settings(&self) -> Option<&ServerSideEncryption> {
self.value.as_ref()
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bucket {
account_id: String,
pub(crate) bucket_id: String,
bucket_name: String,
bucket_type: BucketType,
bucket_info: serde_json::Value,
cors_rules: Vec<CorsRule>,
file_lock_configuration: FileRetentionPolicy,
default_server_side_encryption: BucketEncryptionInfo,
lifecycle_rules: Vec<LifecycleRule>,
revision: u16,
options: Option<Vec<String>>,
}
impl Bucket {
pub fn account_id(&self) -> &str { &self.account_id }
pub fn bucket_id(&self) -> &str { &self.bucket_id }
pub fn name(&self) -> &str { &self.bucket_name }
pub fn bucket_type(&self) -> BucketType { self.bucket_type }
pub fn info(&self) -> &serde_json::Value { &self.bucket_info }
pub fn cors_rules(&self) -> &[CorsRule] { &self.cors_rules }
pub fn retention_policy(&self) -> FileRetentionPolicy {
self.file_lock_configuration
}
pub fn encryption_info(&self) -> &BucketEncryptionInfo {
&self.default_server_side_encryption
}
pub fn lifecycle_rules(&self) -> &[LifecycleRule] { &self.lifecycle_rules }
pub fn revision(&self) -> u16 { self.revision }
pub fn options(&self) -> Option<&Vec<String>> { self.options.as_ref() }
}
pub async fn create_bucket<C, E>(
auth: &mut Authorization<C>,
new_bucket_info: CreateBucket<'_>
) -> Result<Bucket, Error<E>>
where C: HttpClient<Error=Error<E>>,
E: fmt::Debug + fmt::Display,
{
require_capability!(auth, Capability::WriteBuckets);
if new_bucket_info.file_lock_enabled {
require_capability!(auth, Capability::WriteBucketRetentions);
}
if new_bucket_info.default_server_side_encryption.is_some() {
require_capability!(auth, Capability::WriteBucketEncryption);
}
let mut new_bucket_info = new_bucket_info;
new_bucket_info.account_id = Some(&auth.account_id);
let res = auth.client.post(auth.api_url("b2_create_bucket"))
.expect("Invalid URL")
.with_header("Authorization", &auth.authorization_token).unwrap()
.with_body_json(serde_json::to_value(new_bucket_info)?)
.send().await?;
let new_bucket: B2Result<Bucket> = serde_json::from_slice(&res)?;
new_bucket.into()
}
pub async fn delete_bucket<C, E>(
auth: &mut Authorization<C>,
bucket_id: impl AsRef<str>
) -> Result<Bucket, Error<E>>
where C: HttpClient<Error=Error<E>>,
E: fmt::Debug + fmt::Display,
{
require_capability!(auth, Capability::DeleteBuckets);
let res = auth.client.post(auth.api_url("b2_delete_bucket"))
.expect("Invalid URL")
.with_header("Authorization", &auth.authorization_token).unwrap()
.with_body_json(serde_json::json!({
"accountId": &auth.account_id,
"bucketId": bucket_id.as_ref(),
}))
.send().await?;
let new_bucket: B2Result<Bucket> = serde_json::from_slice(&res)?;
new_bucket.into()
}
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
enum BucketRef {
Id(String),
Name(String),
}
#[derive(Debug, Clone, Copy)]
enum BucketFilter {
Type(BucketType),
All,
}
impl From<&BucketType> for BucketFilter {
fn from(t: &BucketType) -> Self {
Self::Type(*t)
}
}
impl fmt::Display for BucketFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Type(t) => t.fmt(f),
Self::All => write!(f, "all"),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(into = "serialization::InnerListBuckets")]
pub struct ListBuckets<'a> {
account_id: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
bucket: Option<BucketRef>,
#[serde(skip_serializing_if = "Option::is_none")]
bucket_types: Option<Vec<BucketFilter>>,
}
impl<'a> ListBuckets<'a> {
pub fn builder() -> ListBucketsBuilder {
ListBucketsBuilder::default()
}
}
#[derive(Default)]
pub struct ListBucketsBuilder {
bucket: Option<BucketRef>,
bucket_types: Option<Vec<BucketFilter>>,
}
impl ListBucketsBuilder {
pub fn bucket_id(mut self, id: impl Into<String>) -> Self {
self.bucket = Some(BucketRef::Id(id.into()));
self
}
pub fn bucket_name(mut self, name: impl Into<String>)
-> Result<Self, BucketValidationError> {
let name = validated_bucket_name(name)?;
self.bucket = Some(BucketRef::Name(name));
Ok(self)
}
pub fn bucket_types(mut self, types: &[BucketType]) -> Self {
let types = types.iter().map(BucketFilter::from).collect();
self.bucket_types = Some(types);
self
}
pub fn with_all_bucket_types(mut self) -> Self {
self.bucket_types = Some(vec![BucketFilter::All]);
self
}
pub fn build<'a>(self) -> ListBuckets<'a> {
ListBuckets {
account_id: None,
bucket: self.bucket,
bucket_types: self.bucket_types,
}
}
}
#[derive(Debug, Default, Deserialize)]
struct BucketList {
buckets: Vec<Bucket>,
}
pub async fn list_buckets<C, E>(
auth: &mut Authorization<C>,
list_info: ListBuckets<'_>
) -> Result<Vec<Bucket>, Error<E>>
where C: HttpClient<Error=Error<E>>,
E: fmt::Debug + fmt::Display,
{
require_capability!(auth, Capability::ListBuckets);
let mut list_info = list_info;
list_info.account_id = Some(&auth.account_id);
let res = auth.client.post(auth.api_url("b2_list_buckets"))
.expect("Invalid URL")
.with_header("Authorization", &auth.authorization_token).unwrap()
.with_body_json(serde_json::to_value(list_info)?)
.send().await?;
let buckets: B2Result<BucketList> = serde_json::from_slice(&res)?;
buckets.map(|b| b.buckets).into()
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateBucket<'a> {
account_id: Option<&'a str>,
bucket_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
bucket_type: Option<BucketType>,
#[serde(skip_serializing_if = "Option::is_none")]
bucket_info: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
cors_rules: Option<Vec<CorsRule>>,
#[serde(skip_serializing_if = "Option::is_none")]
default_retention: Option<FileRetentionPolicy>,
#[serde(skip_serializing_if = "Option::is_none")]
default_server_side_encryption: Option<ServerSideEncryption>,
#[serde(skip_serializing_if = "Option::is_none")]
lifecycle_rules: Option<Vec<LifecycleRule>>,
#[serde(skip_serializing_if = "Option::is_none")]
if_revision_is: Option<u16>,
}
impl<'a> UpdateBucket<'a> {
pub fn builder() -> UpdateBucketBuilder {
UpdateBucketBuilder::default()
}
}
#[derive(Default)]
pub struct UpdateBucketBuilder {
bucket_id: Option<String>,
bucket_type: Option<BucketType>,
bucket_info: Option<serde_json::Value>,
cache_control: Option<String>,
cors_rules: Option<Vec<CorsRule>>,
default_retention: Option<FileRetentionPolicy>,
default_server_side_encryption: Option<ServerSideEncryption>,
lifecycle_rules: Option<Vec<LifecycleRule>>,
if_revision_is: Option<u16>,
}
impl UpdateBucketBuilder {
pub fn bucket_id(mut self, bucket_id: impl Into<String>) -> Self {
self.bucket_id = Some(bucket_id.into());
self
}
pub fn bucket_type(mut self, typ: BucketType)
-> Result<Self, ValidationError> {
if matches!(typ, BucketType::Snapshot) {
return Err(ValidationError::OutOfBounds(
"Bucket type must be either Public or Private".into()
));
}
self.bucket_type = Some(typ);
Ok(self)
}
pub fn bucket_info(mut self, info: serde_json::Value)
-> Self {
self.bucket_info = Some(info);
self
}
pub fn cache_control(mut self, cache_control: CacheControl) -> Self {
self.cache_control = Some(cache_control.value().to_string());
self
}
pub fn cors_rules(mut self, rules: impl Into<Vec<CorsRule>>)
-> Result<Self, ValidationError> {
let rules = rules.into();
if rules.len() > 100 {
return Err(ValidationError::OutOfBounds(
"A bucket can have no more than 100 CORS rules".into()
));
} else if ! rules.is_empty() {
self.cors_rules = Some(rules);
}
Ok(self)
}
pub fn retention_policy(mut self, policy: FileRetentionPolicy) -> Self {
self.default_retention = Some(policy);
self
}
pub fn encryption_settings(mut self, settings: ServerSideEncryption) -> Self
{
self.default_server_side_encryption = Some(settings);
self
}
pub fn lifecycle_rules(mut self, rules: impl Into<Vec<LifecycleRule>>)
-> Result<Self, LifecycleRuleValidationError> {
let rules = validated_lifecycle_rules(rules)?;
self.lifecycle_rules = Some(rules);
Ok(self)
}
pub fn if_revision_is(mut self, revision: u16) -> Self {
self.if_revision_is = Some(revision);
self
}
pub fn build<'a>(self) -> Result<UpdateBucket<'a>, ValidationError> {
let bucket_id = self.bucket_id.ok_or_else(||
ValidationError::MissingData(
"The bucket ID to update must be specified".into()
)
)?;
let bucket_info = if let Some(cache_control) = self.cache_control {
let mut info = self.bucket_info.unwrap_or_else(||
serde_json::Value::Object(serde_json::Map::new())
);
info.as_object_mut()
.map(|map| map.insert(
String::from("Cache-Control"),
serde_json::Value::String(cache_control)
));
Some(info)
} else {
self.bucket_info
};
Ok(UpdateBucket {
account_id: None,
bucket_id,
bucket_type: self.bucket_type,
bucket_info,
cors_rules: self.cors_rules,
default_retention: self.default_retention,
default_server_side_encryption: self.default_server_side_encryption,
lifecycle_rules: self.lifecycle_rules,
if_revision_is: self.if_revision_is,
})
}
}
pub async fn update_bucket<C, E>(
auth: &mut Authorization<C>,
bucket_info: UpdateBucket<'_>
) -> Result<Bucket, Error<E>>
where C: HttpClient<Error=Error<E>>,
E: fmt::Debug + fmt::Display,
{
require_capability!(auth, Capability::WriteBuckets);
if bucket_info.default_retention.is_some() {
require_capability!(auth, Capability::WriteBucketRetentions);
}
if bucket_info.default_server_side_encryption.is_some() {
require_capability!(auth, Capability::WriteBucketEncryption);
}
let mut bucket_info = bucket_info;
bucket_info.account_id = Some(&auth.account_id);
let res = auth.client.post(auth.api_url("b2_update_bucket"))
.expect("Invalid URL")
.with_header("Authorization", &auth.authorization_token).unwrap()
.with_body_json(serde_json::to_value(bucket_info)?)
.send().await?;
let bucket: B2Result<Bucket> = serde_json::from_slice(&res)?;
bucket.into()
}
mod serialization {
use std::convert::TryFrom;
use serde::{Serialize, Deserialize};
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
enum Mode {
#[serde(rename = "SSE-B2")]
B2Managed,
#[serde(rename = "SSE-C")]
SelfManaged,
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct InnerEncryptionConfig {
mode: Option<Mode>,
#[serde(skip_serializing_if = "Option::is_none")]
algorithm: Option<super::EncryptionAlgorithm>,
#[serde(skip_serializing_if = "Option::is_none")]
customer_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
customer_key_md5: Option<String>,
}
impl TryFrom<InnerEncryptionConfig> for super::ServerSideEncryption {
type Error = &'static str;
fn try_from(other: InnerEncryptionConfig) -> Result<Self, Self::Error> {
if let Some(mode) = other.mode {
if mode == Mode::B2Managed {
let algo = other.algorithm
.ok_or("Missing encryption algorithm")?;
Ok(Self::B2Managed(algo))
} else { let algorithm = other.algorithm
.ok_or("Missing encryption algorithm")?;
let key = other.customer_key
.ok_or("Missing encryption key")?;
let digest = other.customer_key_md5
.ok_or("Missing encryption key digest")?;
Ok(Self::SelfManaged(
super::SelfManagedEncryption {
algorithm,
key,
digest,
}
))
}
} else {
Ok(Self::NoEncryption)
}
}
}
impl From<super::ServerSideEncryption> for InnerEncryptionConfig {
fn from(other: super::ServerSideEncryption) -> Self {
match other {
super::ServerSideEncryption::B2Managed(algorithm) => {
Self {
mode: Some(Mode::B2Managed),
algorithm: Some(algorithm),
..Default::default()
}
},
super::ServerSideEncryption::SelfManaged(enc) => {
Self {
mode: Some(Mode::SelfManaged),
algorithm: Some(enc.algorithm),
customer_key: Some(enc.key),
customer_key_md5: Some(enc.digest),
}
},
super::ServerSideEncryption::NoEncryption => {
Self::default()
},
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct InnerSelfEncryption {
mode: Mode,
algorithm: super::EncryptionAlgorithm,
customer_key: String,
customer_key_md5: String,
}
impl TryFrom<InnerSelfEncryption> for super::SelfManagedEncryption {
type Error = &'static str;
fn try_from(other: InnerSelfEncryption) -> Result<Self, Self::Error> {
if other.mode != Mode::SelfManaged {
Err("Not a self-managed encryption configuration")
} else {
Ok(Self {
algorithm: other.algorithm,
key: other.customer_key,
digest: other.customer_key_md5,
})
}
}
}
impl From<super::SelfManagedEncryption> for InnerSelfEncryption {
fn from(other: super::SelfManagedEncryption) -> Self {
Self {
mode: Mode::SelfManaged,
algorithm: other.algorithm,
customer_key: other.key,
customer_key_md5: other.digest,
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct InnerListBuckets<'a> {
account_id: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
bucket_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
bucket_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
bucket_types: Option<Vec<String>>,
}
impl<'a> From<super::ListBuckets<'a>> for InnerListBuckets<'a> {
fn from(other: super::ListBuckets<'a>) -> Self {
use super::BucketRef;
let (bucket_id, bucket_name) = if let Some(bucket) = other.bucket {
match bucket {
BucketRef::Id(s) => (Some(s), None),
BucketRef::Name(s) => (None, Some(s)),
}
} else {
(None, None)
};
let bucket_types = other.bucket_types
.map(|t| t.into_iter()
.map(|t| t.to_string()).collect()
);
Self {
account_id: other.account_id,
bucket_id,
bucket_name,
bucket_types,
}
}
}
}
#[cfg(feature = "with_surf")]
#[cfg(test)]
mod tests_mocked {
use super::*;
use crate::{
account::Capability,
error::ErrorCode,
};
use surf_vcr::VcrMode;
use crate::test_utils::{create_test_auth, create_test_client};
#[async_std::test]
async fn create_bucket_success() -> anyhow::Result<()> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/buckets.yaml",
None, None
).await?;
let mut auth = create_test_auth(client, vec![Capability::WriteBuckets])
.await;
let req = CreateBucket::builder()
.name("testing-new-b2-client")?
.bucket_type(BucketType::Private)?
.lifecycle_rules(vec![
LifecycleRule::builder()
.filename_prefix("my-files/")?
.delete_after_hide(chrono::Duration::days(5))?
.build()?
])?
.build()?;
let bucket = create_bucket(&mut auth, req).await?;
assert_eq!(bucket.bucket_name, "testing-new-b2-client");
Ok(())
}
#[async_std::test]
async fn create_bucket_already_exists() -> anyhow::Result<()> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/buckets.yaml",
None, None
).await?;
let mut auth = create_test_auth(client, vec![Capability::WriteBuckets])
.await;
let req = CreateBucket::builder()
.name("testing-b2-client")?
.bucket_type(BucketType::Private)?
.lifecycle_rules(vec![
LifecycleRule::builder()
.filename_prefix("my-files/")?
.delete_after_hide(chrono::Duration::days(5))?
.build()?
])?
.build()?;
match create_bucket(&mut auth, req).await.unwrap_err() {
Error::B2(e) =>
assert_eq!(e.code(), ErrorCode::DuplicateBucketName),
e => panic!("Unexpected error: {:?}", e),
}
Ok(())
}
#[async_std::test]
async fn delete_bucket_success() -> anyhow::Result<()> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/buckets.yaml",
None, None
).await?;
let mut auth = create_test_auth(client, vec![Capability::DeleteBuckets])
.await;
let bucket = delete_bucket(&mut auth, "1df2dee6ab62f7f577c70e1a")
.await?;
assert_eq!(bucket.bucket_name, "testing-new-b2-client");
Ok(())
}
#[async_std::test]
async fn delete_bucket_does_not_exist() -> anyhow::Result<()> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/buckets.yaml",
None, None
).await?;
let mut auth = create_test_auth(client, vec![Capability::DeleteBuckets])
.await;
match delete_bucket(&mut auth, "1234567").await.unwrap_err() {
Error::B2(e) =>
assert_eq!(e.code(), ErrorCode::BadBucketId),
e => panic!("Unexpected error: {:?}", e),
}
Ok(())
}
#[async_std::test]
async fn test_list_buckets() -> anyhow::Result<()> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/buckets.yaml",
None, None
).await?;
let mut auth = create_test_auth(client, vec![Capability::ListBuckets])
.await;
let buckets_req = ListBuckets::builder()
.bucket_name("testing-b2-client")?
.build();
let buckets = list_buckets(&mut auth, buckets_req).await?;
assert_eq!(buckets.len(), 1);
assert_eq!(buckets[0].bucket_name, "testing-b2-client");
Ok(())
}
#[async_std::test]
async fn update_bucket_success() -> anyhow::Result<()> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/buckets.yaml",
None, None
).await?;
let mut auth = create_test_auth(client, vec![Capability::WriteBuckets])
.await;
let req = UpdateBucket::builder()
.bucket_id("8d625eb63be2775577c70e1a")
.bucket_type(BucketType::Private)?
.lifecycle_rules(vec![
LifecycleRule::builder()
.filename_prefix("my-files/")?
.delete_after_hide(chrono::Duration::days(5))?
.build()?
])?
.build()?;
let bucket = update_bucket(&mut auth, req).await?;
assert_eq!(bucket.bucket_name, "testing-b2-client");
Ok(())
}
#[async_std::test]
async fn update_bucket_conflict() -> anyhow::Result<()> {
let client = create_test_client(
VcrMode::Replay,
"test_sessions/buckets.yaml",
None, None
).await?;
let mut auth = create_test_auth(client, vec![Capability::WriteBuckets])
.await;
let req = UpdateBucket::builder()
.bucket_id("8d625eb63be2775577c70e1a")
.bucket_type(BucketType::Private)?
.if_revision_is(10)
.build()?;
match update_bucket(&mut auth, req).await.unwrap_err() {
Error::B2(e) =>
assert_eq!(e.code(), ErrorCode::Conflict),
e => panic!("Unexpected error: {:?}", e),
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{json, from_value, to_value};
#[test]
fn no_encryption_to_json() {
assert_eq!(
to_value(ServerSideEncryption::NoEncryption).unwrap(),
json!({ "mode": Option::<String>::None })
);
}
#[test]
fn no_encryption_from_json() {
let enc: ServerSideEncryption = from_value(
json!({ "mode": Option::<String>::None })
).unwrap();
assert_eq!(enc, ServerSideEncryption::NoEncryption);
}
#[test]
fn b2_encryption_to_json() {
let json = to_value(
ServerSideEncryption::B2Managed(EncryptionAlgorithm::Aes256)
).unwrap();
assert_eq!(json, json!({ "mode": "SSE-B2", "algorithm": "AES256" }));
}
#[test]
fn b2_encryption_from_json() {
let enc: ServerSideEncryption = from_value(
json!({ "mode": "SSE-B2", "algorithm": "AES256" })
).unwrap();
assert_eq!(
enc,
ServerSideEncryption::B2Managed(EncryptionAlgorithm::Aes256)
);
}
#[test]
fn self_encryption_to_json() {
let json = to_value(ServerSideEncryption::SelfManaged(
SelfManagedEncryption {
algorithm: EncryptionAlgorithm::Aes256,
key: "MY-ENCODED-KEY".into(),
digest: "ENCODED-DIGEST".into(),
}
)).unwrap();
assert_eq!(
json,
json!({
"mode": "SSE-C",
"algorithm": "AES256",
"customerKey": "MY-ENCODED-KEY",
"customerKeyMd5": "ENCODED-DIGEST",
})
);
}
#[test]
fn self_encryption_from_json() {
let enc: ServerSideEncryption = from_value(
json!({
"mode": "SSE-C",
"algorithm": "AES256",
"customerKey": "MY-ENCODED-KEY",
"customerKeyMd5": "ENCODED-DIGEST",
})
).unwrap();
assert_eq!(
enc,
ServerSideEncryption::SelfManaged(
SelfManagedEncryption {
algorithm: EncryptionAlgorithm::Aes256,
key: "MY-ENCODED-KEY".into(),
digest: "ENCODED-DIGEST".into(),
}
)
);
}
#[test]
fn deserialize_new_bucket_response() {
let info = json!({
"accountId": "abcdefg",
"bucketId": "hijklmno",
"bucketInfo": {},
"bucketName": "some-bucket-name",
"bucketType": "allPrivate",
"corsRules": [],
"defaultServerSideEncryption": {
"isClientAuthorizedToRead": true,
"value": {
"algorithm": null,
"mode": null,
},
},
"fileLockConfiguration": {
"isClientAuthorizedToRead": true,
"value": {
"defaultRetention": {
"mode": null,
"period": null,
},
"isFileLockEnabled": false,
},
},
"lifecycleRules": [
{
"daysFromHidingToDeleting": 5,
"daysFromUploadingToHiding": null,
"fileNamePrefix": "my-files",
},
],
"options": ["s3"],
"revision": 2,
});
let _: Bucket = from_value(info).unwrap();
}
#[test]
fn cors_rule_validates_origins() -> anyhow::Result<()> {
let valid_origins = [
vec!["https://*".into(), "http://*".into()],
vec!["*".into()],
vec![
"https://example.com".into(), "http://example.com:1234".into()
],
vec![
"https".into(), "http".into(), "http://example.com:1234".into()
],
vec![
"https://*:8765".into(), "http://www.example.com:4545".into()
],
vec![
"https://*.example.com".into(), "http://www.example.com".into()
],
];
for origin_list in valid_origins {
let _ = CorsRule::builder()
.allowed_origins(origin_list)?;
}
let bad_origins = [
vec!["*".into(), "https://*".into()],
vec!["ftp://example.com".into()],
vec!["ftp://*.*.example.com".into()],
vec!["https://*:8765".into(), "www.example.com:4545".into()],
vec![
"https://*:8765".into(), "https://www.example.com:4545".into()
],
];
for origin_list in bad_origins {
let rule = CorsRule::builder()
.allowed_origins(origin_list);
assert!(rule.is_err(), "{:?}", rule);
}
Ok(())
}
}