From cba673a379433721cbbf4a49d62637361d3d3ad1 Mon Sep 17 00:00:00 2001 From: Henk-Jan Lebbink Date: Fri, 21 Feb 2025 02:52:07 +0100 Subject: [PATCH] Refactor set_bucket_versioning (#114) --- ...ket_encryption.rs => bucket_encryption.rs} | 0 ...ket_versioning.rs => bucket_versioning.rs} | 29 +++- src/s3/args.rs | 33 ---- src/s3/builders.rs | 2 + src/s3/builders/set_bucket_encryption.rs | 41 ++--- src/s3/builders/set_bucket_versioning.rs | 151 ++++++++++++++++++ src/s3/client.rs | 54 +------ src/s3/client/set_bucket_versioning.rs | 26 +++ src/s3/error.rs | 2 + src/s3/response.rs | 6 +- src/s3/response/get_bucket_versioning.rs | 9 +- src/s3/response/set_bucket_versioning.rs | 41 +++++ tests/tests.rs | 14 +- 13 files changed, 287 insertions(+), 121 deletions(-) rename examples/{get_bucket_encryption.rs => bucket_encryption.rs} (100%) rename examples/{get_bucket_versioning.rs => bucket_versioning.rs} (71%) create mode 100644 src/s3/builders/set_bucket_versioning.rs create mode 100644 src/s3/client/set_bucket_versioning.rs create mode 100644 src/s3/response/set_bucket_versioning.rs diff --git a/examples/get_bucket_encryption.rs b/examples/bucket_encryption.rs similarity index 100% rename from examples/get_bucket_encryption.rs rename to examples/bucket_encryption.rs diff --git a/examples/get_bucket_versioning.rs b/examples/bucket_versioning.rs similarity index 71% rename from examples/get_bucket_versioning.rs rename to examples/bucket_versioning.rs index 5fb96a3..c290990 100644 --- a/examples/get_bucket_versioning.rs +++ b/examples/bucket_versioning.rs @@ -16,7 +16,7 @@ mod common; use crate::common::{create_bucket_if_not_exists, create_client_on_play}; -use minio::s3::args::SetBucketVersioningArgs; +use minio::s3::builders::VersioningStatus; use minio::s3::response::{GetBucketVersioningResponse, SetBucketVersioningResponse}; use minio::s3::types::S3Api; use minio::s3::Client; @@ -38,27 +38,46 @@ async fn main() -> Result<(), Box> { ); let _resp: SetBucketVersioningResponse = client - .set_bucket_versioning(&SetBucketVersioningArgs::new(bucket_name, true).unwrap()) + .set_bucket_versioning(bucket_name) + .versioning_status(VersioningStatus::Enabled) + .send() .await?; let resp: GetBucketVersioningResponse = client.get_bucket_versioning(bucket_name).send().await?; log::info!( - "versioning after setting to true: status={:?}, mfa_delete={:?}", + "versioning after setting to Enabled: status={:?}, mfa_delete={:?}", resp.status, resp.mfa_delete ); let _resp: SetBucketVersioningResponse = client - .set_bucket_versioning(&SetBucketVersioningArgs::new(bucket_name, false).unwrap()) + .set_bucket_versioning(bucket_name) + .versioning_status(VersioningStatus::Suspended) + .send() .await?; let resp: GetBucketVersioningResponse = client.get_bucket_versioning(bucket_name).send().await?; log::info!( - "versioning after setting to false: status={:?}, mfa_delete={:?}", + "versioning after setting to Suspended: status={:?}, mfa_delete={:?}", + resp.status, + resp.mfa_delete + ); + + let _resp: SetBucketVersioningResponse = client + .set_bucket_versioning(bucket_name) + //.versioning_status(VersioningStatus::Suspended) + .send() + .await?; + + let resp: GetBucketVersioningResponse = + client.get_bucket_versioning(bucket_name).send().await?; + + log::info!( + "versioning after setting to None: status={:?}, mfa_delete={:?}", resp.status, resp.mfa_delete ); diff --git a/src/s3/args.rs b/src/s3/args.rs index 293426f..f665525 100644 --- a/src/s3/args.rs +++ b/src/s3/args.rs @@ -1623,39 +1623,6 @@ impl<'a> SetBucketTagsArgs<'a> { } } -/// Argument for [set_bucket_versioning()](crate::s3::client::Client::set_bucket_versioning) API -pub struct SetBucketVersioningArgs<'a> { - pub extra_headers: Option<&'a Multimap>, - pub extra_query_params: Option<&'a Multimap>, - pub region: Option<&'a str>, - pub bucket: &'a str, - pub status: bool, - pub mfa_delete: Option, -} - -impl<'a> SetBucketVersioningArgs<'a> { - /// Returns argument for [set_bucket_versioning()](crate::s3::client::Client::set_bucket_versioning) API with given bucket name and status - /// - /// # Examples - /// - /// ``` - /// use minio::s3::args::*; - /// let args = SetBucketVersioningArgs::new("my-bucket", true).unwrap(); - /// ``` - pub fn new(bucket_name: &'a str, status: bool) -> Result, Error> { - check_bucket_name(bucket_name, true)?; - - Ok(SetBucketVersioningArgs { - extra_headers: None, - extra_query_params: None, - region: None, - bucket: bucket_name, - status, - mfa_delete: None, - }) - } -} - /// Argument for [delete_object_lock_config()](crate::s3::client::Client::delete_object_lock_config) API pub type DeleteObjectLockConfigArgs<'a> = BucketArgs<'a>; diff --git a/src/s3/builders.rs b/src/s3/builders.rs index 0a23919..3532bcc 100644 --- a/src/s3/builders.rs +++ b/src/s3/builders.rs @@ -27,6 +27,7 @@ mod object_prompt; mod put_object; mod remove_objects; mod set_bucket_encryption; +mod set_bucket_versioning; pub use bucket_common::*; pub use get_bucket_encryption::*; @@ -40,3 +41,4 @@ pub use object_prompt::*; pub use put_object::*; pub use remove_objects::*; pub use set_bucket_encryption::*; +pub use set_bucket_versioning::*; diff --git a/src/s3/builders/set_bucket_encryption.rs b/src/s3/builders/set_bucket_encryption.rs index 6aded4d..bff6b26 100644 --- a/src/s3/builders/set_bucket_encryption.rs +++ b/src/s3/builders/set_bucket_encryption.rs @@ -17,7 +17,7 @@ use crate::s3::builders::SegmentedBytes; use crate::s3::error::Error; use crate::s3::response::SetBucketEncryptionResponse; use crate::s3::types::{S3Api, S3Request, SseConfig, ToS3Request}; -use crate::s3::utils::{check_bucket_name, merge, Multimap}; +use crate::s3::utils::{check_bucket_name, Multimap}; use crate::s3::Client; use bytes::Bytes; use http::Method; @@ -76,29 +76,32 @@ impl S3Api for SetBucketEncryption { impl ToS3Request for SetBucketEncryption { fn to_s3request(&self) -> Result { check_bucket_name(&self.bucket, true)?; - let mut headers = Multimap::new(); - if let Some(v) = &self.extra_headers { - merge(&mut headers, v); - } - let mut query_params = Multimap::new(); - if let Some(v) = &self.extra_query_params { - merge(&mut query_params, v); - } - query_params.insert(String::from("encryption"), String::new()); + let headers = self + .extra_headers + .as_ref() + .filter(|v| !v.is_empty()) + .cloned() + .unwrap_or_default(); + let mut query_params = self + .extra_query_params + .as_ref() + .filter(|v| !v.is_empty()) + .cloned() + .unwrap_or_default(); + + query_params.insert("encryption".into(), String::new()); let bytes: Bytes = self.config.to_xml().into(); let body: Option = Some(SegmentedBytes::from(bytes)); + let client: &Client = self.client.as_ref().ok_or(Error::NoClientProvided)?; - let req = S3Request::new( - self.client.as_ref().ok_or(Error::NoClientProvided)?, - Method::GET, - ) - .region(self.region.as_deref()) - .bucket(Some(&self.bucket)) - .query_params(query_params) - .headers(headers) - .body(body); + let req = S3Request::new(client, Method::GET) + .region(self.region.as_deref()) + .bucket(Some(&self.bucket)) + .query_params(query_params) + .headers(headers) + .body(body); Ok(req) } diff --git a/src/s3/builders/set_bucket_versioning.rs b/src/s3/builders/set_bucket_versioning.rs new file mode 100644 index 0000000..8df6d87 --- /dev/null +++ b/src/s3/builders/set_bucket_versioning.rs @@ -0,0 +1,151 @@ +// MinIO Rust Library for Amazon S3 Compatible Cloud Storage +// Copyright 2025 MinIO, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::s3::builders::SegmentedBytes; +use crate::s3::error::Error; +use crate::s3::response::SetBucketVersioningResponse; +use crate::s3::types::{S3Api, S3Request, ToS3Request}; +use crate::s3::utils::{check_bucket_name, Multimap}; +use crate::s3::Client; +use bytes::Bytes; +use http::Method; +use std::fmt; + +#[derive(Clone, Debug, PartialEq)] +pub enum VersioningStatus { + /// **Enable** object versioning in given bucket. + Enabled, + /// **Suspend** object versioning in given bucket. + Suspended, +} + +impl fmt::Display for VersioningStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VersioningStatus::Enabled => write!(f, "Enabled"), + VersioningStatus::Suspended => write!(f, "Suspended"), + } + } +} + +/// Argument builder for [set_bucket_encryption()](Client::set_bucket_encryption) API +#[derive(Clone, Debug, Default)] +pub struct SetBucketVersioning { + pub(crate) client: Option, + + pub(crate) extra_headers: Option, + pub(crate) extra_query_params: Option, + pub(crate) region: Option, + pub(crate) bucket: String, + + pub(crate) status: Option, + pub(crate) mfa_delete: Option, +} + +impl SetBucketVersioning { + pub fn new(bucket: &str) -> Self { + Self { + bucket: bucket.to_owned(), + ..Default::default() + } + } + + pub fn client(mut self, client: &Client) -> Self { + self.client = Some(client.clone()); + self + } + + pub fn extra_headers(mut self, extra_headers: Option) -> Self { + self.extra_headers = extra_headers; + self + } + + pub fn extra_query_params(mut self, extra_query_params: Option) -> Self { + self.extra_query_params = extra_query_params; + self + } + + pub fn region(mut self, region: Option) -> Self { + self.region = region; + self + } + + pub fn versioning_status(mut self, status: VersioningStatus) -> Self { + self.status = Some(status); + self + } + + pub fn mfa_delete(mut self, mfa_delete: Option) -> Self { + self.mfa_delete = mfa_delete; + self + } +} + +impl S3Api for SetBucketVersioning { + type S3Response = SetBucketVersioningResponse; +} + +impl ToS3Request for SetBucketVersioning { + fn to_s3request(&self) -> Result { + check_bucket_name(&self.bucket, true)?; + + let headers = self + .extra_headers + .as_ref() + .filter(|v| !v.is_empty()) + .cloned() + .unwrap_or_default(); + let mut query_params = self + .extra_query_params + .as_ref() + .filter(|v| !v.is_empty()) + .cloned() + .unwrap_or_default(); + + query_params.insert("versioning".into(), String::new()); + + let mut data = "".to_string(); + + if let Some(v) = self.mfa_delete { + data.push_str(""); + data.push_str(if v { "Enabled" } else { "Disabled" }); + data.push_str(""); + } + + match self.status { + Some(VersioningStatus::Enabled) => data.push_str("Enabled"), + Some(VersioningStatus::Suspended) => data.push_str("Suspended"), + None => { + return Err(Error::InvalidVersioningStatus( + "Missing VersioningStatus".into(), + )) + } + }; + + data.push_str(""); + + let body: Option = Some(SegmentedBytes::from(Bytes::from(data))); + let client: &Client = self.client.as_ref().ok_or(Error::NoClientProvided)?; + + let req = S3Request::new(client, Method::PUT) + .region(self.region.as_deref()) + .bucket(Some(&self.bucket)) + .query_params(query_params) + .headers(headers) + .body(body); + + Ok(req) + } +} diff --git a/src/s3/client.rs b/src/s3/client.rs index 7d9b81e..64c482c 100644 --- a/src/s3/client.rs +++ b/src/s3/client.rs @@ -56,6 +56,7 @@ mod object_prompt; mod put_object; mod remove_objects; mod set_bucket_encryption; +mod set_bucket_versioning; use super::builders::{ListBuckets, SegmentedBytes}; @@ -2721,59 +2722,6 @@ impl Client { }) } - pub async fn set_bucket_versioning( - &self, - args: &SetBucketVersioningArgs<'_>, - ) -> Result { - let region = self.get_region(args.bucket, args.region).await?; - - let mut headers = Multimap::new(); - if let Some(v) = &args.extra_headers { - merge(&mut headers, v); - } - - let mut query_params = Multimap::new(); - if let Some(v) = &args.extra_query_params { - merge(&mut query_params, v); - } - query_params.insert(String::from("versioning"), String::new()); - - let mut data = String::from(""); - data.push_str(""); - data.push_str(match args.status { - true => "Enabled", - false => "Suspended", - }); - data.push_str(""); - if let Some(v) = args.mfa_delete { - data.push_str(""); - data.push_str(match v { - true => "Enabled", - false => "Disabled", - }); - data.push_str(""); - } - data.push_str(""); - - let resp = self - .execute( - Method::PUT, - ®ion, - &mut headers, - &query_params, - Some(args.bucket), - None, - Some(data.into()), - ) - .await?; - - Ok(SetBucketVersioningResponse { - headers: resp.headers().clone(), - region: region.clone(), - bucket_name: args.bucket.to_string(), - }) - } - pub async fn set_object_lock_config( &self, args: &SetObjectLockConfigArgs<'_>, diff --git a/src/s3/client/set_bucket_versioning.rs b/src/s3/client/set_bucket_versioning.rs new file mode 100644 index 0000000..856d60b --- /dev/null +++ b/src/s3/client/set_bucket_versioning.rs @@ -0,0 +1,26 @@ +// MinIO Rust Library for Amazon S3 Compatible Cloud Storage +// Copyright 2025 MinIO, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! S3 APIs for bucket objects. + +use super::Client; +use crate::s3::builders::SetBucketVersioning; + +impl Client { + /// Create a SetBucketVersioning request builder. + pub fn set_bucket_versioning(&self, bucket: &str) -> SetBucketVersioning { + SetBucketVersioning::new(bucket).client(self) + } +} diff --git a/src/s3/error.rs b/src/s3/error.rs index 2f114d8..81482fb 100644 --- a/src/s3/error.rs +++ b/src/s3/error.rs @@ -110,6 +110,7 @@ pub enum Error { InvalidDateAndDays(String), InvalidLifecycleRuleId, InvalidFilter, + InvalidVersioningStatus(String), PostPolicyError(String), InvalidObjectLockConfig(String), NoClientProvided, @@ -219,6 +220,7 @@ impl fmt::Display for Error { } Error::InvalidLifecycleRuleId => write!(f, "id must be exceed 255 characters"), Error::InvalidFilter => write!(f, "only one of And, Prefix or Tag must be provided"), + Error::InvalidVersioningStatus(m) => write!(f, "{}", m), Error::PostPolicyError(m) => write!(f, "{}", m), Error::InvalidObjectLockConfig(m) => write!(f, "{}", m), Error::NoClientProvided => write!(f, "no client provided"), diff --git a/src/s3/response.rs b/src/s3/response.rs index e96c72c..02d01e6 100644 --- a/src/s3/response.rs +++ b/src/s3/response.rs @@ -41,6 +41,7 @@ mod object_prompt; mod put_object; mod remove_objects; mod set_bucket_encryption; +mod set_bucket_versioning; pub use get_bucket_encryption::GetBucketEncryptionResponse; pub use get_bucket_versioning::GetBucketVersioningResponse; @@ -56,6 +57,7 @@ pub use put_object::{ }; pub use remove_objects::{DeleteError, DeletedObject, RemoveObjectResponse, RemoveObjectsResponse}; pub use set_bucket_encryption::SetBucketEncryptionResponse; +pub use set_bucket_versioning::SetBucketVersioningResponse; #[derive(Debug)] /// Base response for bucket operation @@ -555,7 +557,6 @@ impl SelectObjectContentResponse { /// Response of [delete_bucket_encryption()](crate::s3::client::Client::delete_bucket_encryption) API pub type DeleteBucketEncryptionResponse = BucketResponse; - /// Response of [enable_object_legal_hold()](crate::s3::client::Client::enable_object_legal_hold) API pub type EnableObjectLegalHoldResponse = ObjectResponse; @@ -648,9 +649,6 @@ pub struct GetBucketTagsResponse { /// Response of [set_bucket_tags()](crate::s3::client::Client::set_bucket_tags) API pub type SetBucketTagsResponse = BucketResponse; -/// Response of [set_bucket_versioning()](crate::s3::client::Client::set_bucket_versioning) API -pub type SetBucketVersioningResponse = BucketResponse; - /// Response of [delete_object_lock_config()](crate::s3::client::Client::delete_object_lock_config) API pub type DeleteObjectLockConfigResponse = BucketResponse; diff --git a/src/s3/response/get_bucket_versioning.rs b/src/s3/response/get_bucket_versioning.rs index e479de8..de4cd40 100644 --- a/src/s3/response/get_bucket_versioning.rs +++ b/src/s3/response/get_bucket_versioning.rs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::s3::builders::VersioningStatus; use crate::s3::error::Error; use crate::s3::types::{FromS3Response, S3Request}; use crate::s3::utils::get_option_text; @@ -29,7 +30,7 @@ pub struct GetBucketVersioningResponse { pub headers: HeaderMap, pub region: String, pub bucket: String, - pub status: Option, + pub status: Option, pub mfa_delete: Option, } @@ -47,7 +48,11 @@ impl FromS3Response for GetBucketVersioningResponse { let headers = resp.headers().clone(); let body = resp.bytes().await?; let root = Element::parse(body.reader())?; - let status: Option = get_option_text(&root, "Status").map(|v| v == "Enabled"); + let status: Option = + get_option_text(&root, "Status").map(|v| match v.as_str() { + "Enabled" => VersioningStatus::Enabled, + _ => VersioningStatus::Suspended, // Default case + }); let mfa_delete: Option = get_option_text(&root, "MFADelete").map(|v| v == "Enabled"); Ok(GetBucketVersioningResponse { diff --git a/src/s3/response/set_bucket_versioning.rs b/src/s3/response/set_bucket_versioning.rs new file mode 100644 index 0000000..bb53a4a --- /dev/null +++ b/src/s3/response/set_bucket_versioning.rs @@ -0,0 +1,41 @@ +// MinIO Rust Library for Amazon S3 Compatible Cloud Storage +// Copyright 2025 MinIO, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::s3::error::Error; +use crate::s3::response::BucketResponse; +use crate::s3::types::{FromS3Response, S3Request}; +use async_trait::async_trait; + +/// Response of [set_bucket_versioning()](Client::set_bucket_versioning) API +pub type SetBucketVersioningResponse = BucketResponse; + +#[async_trait] +impl FromS3Response for SetBucketVersioningResponse { + async fn from_s3response<'a>( + req: S3Request<'a>, + resp: reqwest::Response, + ) -> Result { + let bucket: String = match req.bucket { + None => return Err(Error::InvalidBucketName("no bucket specified".to_string())), + Some(v) => v.to_string(), + }; + + Ok(SetBucketVersioningResponse { + headers: resp.headers().clone(), + region: req.get_computed_region(), + bucket_name: bucket, + }) + } +} diff --git a/tests/tests.rs b/tests/tests.rs index 7f31a69..ae79007 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -20,7 +20,7 @@ use futures_util::Stream; use http::header; use hyper::http::Method; -use minio::s3::builders::{ObjectContent, ObjectToDelete}; +use minio::s3::builders::{ObjectContent, ObjectToDelete, VersioningStatus}; use rand::{ distributions::{Alphanumeric, DistString}, rngs::SmallRng, @@ -1258,7 +1258,9 @@ impl ClientTest { .unwrap(); self.client - .set_bucket_versioning(&SetBucketVersioningArgs::new(&bucket_name, true).unwrap()) + .set_bucket_versioning(&bucket_name) + .versioning_status(VersioningStatus::Enabled) + .send() .await .unwrap(); @@ -1268,10 +1270,12 @@ impl ClientTest { .send() .await .unwrap(); - assert_eq!(resp.status, Some(true)); + assert_eq!(resp.status, Some(VersioningStatus::Enabled)); self.client - .set_bucket_versioning(&SetBucketVersioningArgs::new(&bucket_name, false).unwrap()) + .set_bucket_versioning(&bucket_name) + .versioning_status(VersioningStatus::Suspended) + .send() .await .unwrap(); @@ -1281,7 +1285,7 @@ impl ClientTest { .send() .await .unwrap(); - assert_eq!(resp.status, Some(false)); + assert_eq!(resp.status, Some(VersioningStatus::Suspended)); self.client .remove_bucket(&RemoveBucketArgs::new(&bucket_name).unwrap())