1use 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#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
29#[non_exhaustive]
30pub enum BucketType {
31 #[serde(rename = "allPublic")]
33 Public,
34 #[serde(rename = "allPrivate")]
36 Private,
37 #[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#[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 #[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#[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 pub fn builder() -> CorsRuleBuilder {
99 CorsRuleBuilder::default()
100 }
101}
102
103#[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 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 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 pub fn add_allowed_origin(mut self, origin: impl Into<String>)
179 -> Result<Self, ValidationError> {
180 let origin = origin.into();
181
182 self.allowed_origins.push(origin);
185 self.allowed_origins = validated_origins(self.allowed_origins)?;
186
187 Ok(self)
188 }
189
190 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 pub fn add_allowed_operation(mut self, op: CorsOperation) -> Self {
207 self.allowed_operations.push(op);
208 self
209 }
210
211 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 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 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 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 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 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 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#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
366#[serde(rename_all = "camelCase")]
367pub struct LifecycleRule {
368 pub(crate) file_name_prefix: String,
369 #[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 pub fn builder<'a>() -> LifecycleRuleBuilder<'a> {
393 LifecycleRuleBuilder::default()
394 }
395}
396
397#[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 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 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 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 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#[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#[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#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
546#[serde(try_from = "serialization::InnerEncryptionConfig")]
547#[serde(into = "serialization::InnerEncryptionConfig")]
548pub enum ServerSideEncryption {
549 B2Managed(EncryptionAlgorithm),
551 SelfManaged(SelfManagedEncryption),
553 NoEncryption,
555}
556
557impl Default for ServerSideEncryption {
558 fn default() -> Self {
559 Self::NoEncryption
560 }
561}
562
563impl ServerSideEncryption {
564 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#[derive(Debug, Serialize)]
598#[serde(rename_all = "camelCase")]
599pub struct CreateBucket<'a> {
600 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#[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 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 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 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 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 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 pub fn with_file_lock(mut self) -> Self {
718 self.file_lock_enabled = true;
719 self
720 }
721
722 pub fn without_file_lock(mut self) -> Self {
726 self.file_lock_enabled = false;
727 self
728 }
729
730 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 pub fn encryption_settings(mut self, settings: ServerSideEncryption) -> Self
799 {
800 self.default_server_side_encryption = Some(settings);
801 self
802 }
803
804 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#[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 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 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#[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#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
925pub struct FileRetentionPolicy {
926 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#[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 pub fn can_read(&self) -> bool { self.is_client_authorized_to_read }
961
962 pub fn settings(&self) -> Option<&ServerSideEncryption> {
964 self.value.as_ref()
965 }
966}
967
968#[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
1006pub 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
1035pub 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#[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#[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#[derive(Default)]
1115pub struct ListBucketsBuilder {
1116 bucket: Option<BucketRef>,
1117 bucket_types: Option<Vec<BucketFilter>>,
1118}
1119
1120impl ListBucketsBuilder {
1121 pub fn bucket_id(mut self, id: impl Into<String>) -> Self {
1125 self.bucket = Some(BucketRef::Id(id.into()));
1126 self
1127 }
1128
1129 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 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 pub fn with_all_bucket_types(mut self) -> Self {
1152 self.bucket_types = Some(vec![BucketFilter::All]);
1153 self
1154 }
1155
1156 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
1171pub 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#[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#[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 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 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 pub fn bucket_info(mut self, info: serde_json::Value)
1270 -> Self {
1271 self.bucket_info = Some(info);
1272 self
1273 }
1274
1275 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 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 pub fn retention_policy(mut self, policy: FileRetentionPolicy) -> Self {
1307 self.default_retention = Some(policy);
1308 self
1309 }
1310
1311 pub fn encryption_settings(mut self, settings: ServerSideEncryption) -> Self
1316 {
1317 self.default_server_side_encryption = Some(settings);
1318 self
1319 }
1320
1321 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 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
1377pub 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 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 { 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 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 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 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 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 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 }