diff --git a/Cargo.toml b/Cargo.toml index c231e3b..ff36571 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,33 +23,33 @@ rustls-tls = ["reqwest/rustls-tls"] [dependencies] async-recursion = "1.1.1" -async-trait = "0.1.83" +async-trait = "0.1.86" base64 = "0.22.1" byteorder = "1.5.0" -bytes = "1.8.0" +bytes = "1.9.0" chrono = "0.4.39" crc = "3.2.1" dashmap = "6.1.0" derivative = "2.2.0" -env_logger = "0.11.5" +env_logger = "0.11.6" futures-util = "0.3.31" hex = "0.4.3" hmac = "0.12.1" home = "0.5.9" http = "1.2.0" -hyper = { version = "1.5.1", features = ["full"] } +hyper = { version = "1.6.0", features = ["full"] } lazy_static = "1.5.0" -log = "0.4.22" +log = "0.4.25" md5 = "0.7.0" multimap = "0.10.0" -os_info = "3.9.0" +os_info = "3.9.2" percent-encoding = "2.3.1" rand = { version = "0.8.5", features = ["small_rng"] } regex = "1.11.1" -serde = { version = "1.0.216", features = ["derive"] } -serde_json = "1.0.133" +serde = { version = "1.0.217", features = ["derive"] } +serde_json = "1.0.138" sha2 = "0.10.8" -tokio = { version = "1.42.0", features = ["full"] } +tokio = { version = "1.43.0", features = ["full"] } tokio-stream = "0.1.17" tokio-util = { version = "0.7.13", features = ["io"] } urlencoding = "2.1.3" diff --git a/examples/common.rs b/examples/common.rs index 4616f9b..8a393d1 100644 --- a/examples/common.rs +++ b/examples/common.rs @@ -27,8 +27,7 @@ pub async fn create_bucket_if_not_exists( // Check 'bucket_name' bucket exist or not. let exists: bool = client .bucket_exists(&BucketExistsArgs::new(bucket_name).unwrap()) - .await - .unwrap(); + .await?; // Make 'bucket_name' bucket if not exist. if !exists { diff --git a/examples/get_bucket_encryption.rs b/examples/get_bucket_encryption.rs index 4ecccc3..d2968c7 100644 --- a/examples/get_bucket_encryption.rs +++ b/examples/get_bucket_encryption.rs @@ -13,12 +13,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::common::{create_bucket_if_not_exists, create_client_on_play}; -use minio::s3::builders::GetBucketEncryption; -use minio::s3::Client; - mod common; +use crate::common::{create_bucket_if_not_exists, create_client_on_play}; +use minio::s3::response::{GetBucketEncryptionResponse, SetBucketEncryptionResponse}; +use minio::s3::types::{S3Api, SseConfig}; +use minio::s3::Client; + #[tokio::main] async fn main() -> Result<(), Box> { env_logger::init(); // Note: set environment variable RUST_LOG="INFO" to log info and higher @@ -27,9 +28,19 @@ async fn main() -> Result<(), Box> { let bucket_name: &str = "encryption-rust-bucket"; create_bucket_if_not_exists(bucket_name, &client).await?; - let be: GetBucketEncryption = client.get_bucket_encryption(bucket_name); + let resp: GetBucketEncryptionResponse = + client.get_bucket_encryption(bucket_name).send().await?; + log::info!("encryption before: config={:?}", resp.config,); - log::info!("{:?}", be); + let _resp: SetBucketEncryptionResponse = client + .set_bucket_encryption(bucket_name) + .config(SseConfig::default()) + .send() + .await?; + + let resp: GetBucketEncryptionResponse = + client.get_bucket_encryption(bucket_name).send().await?; + log::info!("encryption after: config={:?}", resp.config,); Ok(()) } diff --git a/examples/get_bucket_versioning.rs b/examples/get_bucket_versioning.rs index 9ad1e84..5fb96a3 100644 --- a/examples/get_bucket_versioning.rs +++ b/examples/get_bucket_versioning.rs @@ -16,7 +16,9 @@ mod common; use crate::common::{create_bucket_if_not_exists, create_client_on_play}; -use minio::s3::builders::GetBucketVersioning; +use minio::s3::args::SetBucketVersioningArgs; +use minio::s3::response::{GetBucketVersioningResponse, SetBucketVersioningResponse}; +use minio::s3::types::S3Api; use minio::s3::Client; #[tokio::main] @@ -27,9 +29,39 @@ async fn main() -> Result<(), Box> { let bucket_name: &str = "versioning-rust-bucket"; create_bucket_if_not_exists(bucket_name, &client).await?; - let bv: GetBucketVersioning = client.get_bucket_versioning(bucket_name); + let resp: GetBucketVersioningResponse = + client.get_bucket_versioning(bucket_name).send().await?; + log::info!( + "versioning before: status={:?}, mfa_delete={:?}", + resp.status, + resp.mfa_delete + ); - log::info!("{:?}", bv); + let _resp: SetBucketVersioningResponse = client + .set_bucket_versioning(&SetBucketVersioningArgs::new(bucket_name, true).unwrap()) + .await?; + + let resp: GetBucketVersioningResponse = + client.get_bucket_versioning(bucket_name).send().await?; + + log::info!( + "versioning after setting to true: status={:?}, mfa_delete={:?}", + resp.status, + resp.mfa_delete + ); + + let _resp: SetBucketVersioningResponse = client + .set_bucket_versioning(&SetBucketVersioningArgs::new(bucket_name, false).unwrap()) + .await?; + + let resp: GetBucketVersioningResponse = + client.get_bucket_versioning(bucket_name).send().await?; + + log::info!( + "versioning after setting to false: status={:?}, mfa_delete={:?}", + resp.status, + resp.mfa_delete + ); Ok(()) } diff --git a/src/s3/args.rs b/src/s3/args.rs index a46dd46..293426f 100644 --- a/src/s3/args.rs +++ b/src/s3/args.rs @@ -20,7 +20,7 @@ use crate::s3::signer::post_presign_v4; use crate::s3::sse::{Sse, SseCustomerKey}; use crate::s3::types::{ Directive, LifecycleConfig, NotificationConfig, ObjectLockConfig, Part, ReplicationConfig, - Retention, RetentionMode, SelectRequest, SseConfig, + Retention, RetentionMode, SelectRequest, }; use crate::s3::utils::{ b64encode, check_bucket_name, merge, to_amz_date, to_http_header_value, to_iso8601utc, @@ -1318,42 +1318,6 @@ impl<'a> ComposeObjectArgs<'a> { /// Argument for [delete_bucket_encryption()](crate::s3::client::Client::delete_bucket_encryption) API pub type DeleteBucketEncryptionArgs<'a> = BucketArgs<'a>; -#[derive(Clone, Debug)] -/// Argument for [set_bucket_encryption()](crate::s3::client::Client::set_bucket_encryption) API -pub struct SetBucketEncryptionArgs<'a> { - pub extra_headers: Option<&'a Multimap>, - pub extra_query_params: Option<&'a Multimap>, - pub region: Option<&'a str>, - pub bucket: &'a str, - pub config: &'a SseConfig, -} - -impl<'a> SetBucketEncryptionArgs<'a> { - /// Returns argument for [set_bucket_encryption()](crate::s3::client::Client::set_bucket_encryption) API with given bucket name and configuration - /// - /// # Examples - /// - /// ``` - /// use minio::s3::args::*; - /// use minio::s3::types::SseConfig; - /// let args = SetBucketEncryptionArgs::new("my-bucket", &SseConfig::s3()).unwrap(); - /// ``` - pub fn new( - bucket_name: &'a str, - config: &'a SseConfig, - ) -> Result, Error> { - check_bucket_name(bucket_name, true)?; - - Ok(SetBucketEncryptionArgs { - extra_headers: None, - extra_query_params: None, - region: None, - bucket: bucket_name, - config, - }) - } -} - /// Argument for [enable_object_legal_hold()](crate::s3::client::Client::enable_object_legal_hold) API pub type EnableObjectLegalHoldArgs<'a> = ObjectVersionArgs<'a>; diff --git a/src/s3/builders.rs b/src/s3/builders.rs index 7e59bbf..0a23919 100644 --- a/src/s3/builders.rs +++ b/src/s3/builders.rs @@ -26,6 +26,7 @@ mod object_content; mod object_prompt; mod put_object; mod remove_objects; +mod set_bucket_encryption; pub use bucket_common::*; pub use get_bucket_encryption::*; @@ -38,3 +39,4 @@ pub use object_content::*; pub use object_prompt::*; pub use put_object::*; pub use remove_objects::*; +pub use set_bucket_encryption::*; diff --git a/src/s3/builders/set_bucket_encryption.rs b/src/s3/builders/set_bucket_encryption.rs new file mode 100644 index 0000000..6aded4d --- /dev/null +++ b/src/s3/builders/set_bucket_encryption.rs @@ -0,0 +1,105 @@ +// 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::SetBucketEncryptionResponse; +use crate::s3::types::{S3Api, S3Request, SseConfig, ToS3Request}; +use crate::s3::utils::{check_bucket_name, merge, Multimap}; +use crate::s3::Client; +use bytes::Bytes; +use http::Method; + +/// Argument builder for [set_bucket_encryption()](Client::set_bucket_encryption) API +#[derive(Clone, Debug, Default)] +pub struct SetBucketEncryption { + client: Option, + + extra_headers: Option, + extra_query_params: Option, + region: Option, + bucket: String, + + config: SseConfig, +} + +impl SetBucketEncryption { + 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 config(mut self, config: SseConfig) -> Self { + self.config = config; + self + } +} + +impl S3Api for SetBucketEncryption { + type S3Response = SetBucketEncryptionResponse; +} + +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 bytes: Bytes = self.config.to_xml().into(); + let body: Option = Some(SegmentedBytes::from(bytes)); + + 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); + + Ok(req) + } +} diff --git a/src/s3/client.rs b/src/s3/client.rs index f2b9648..7d9b81e 100644 --- a/src/s3/client.rs +++ b/src/s3/client.rs @@ -55,6 +55,7 @@ mod listen_bucket_notification; mod object_prompt; mod put_object; mod remove_objects; +mod set_bucket_encryption; use super::builders::{ListBuckets, SegmentedBytes}; @@ -650,7 +651,7 @@ impl Client { Ok(AbortMultipartUploadResponse { headers: resp.headers().clone(), region: region.clone(), - bucket_name: args.bucket.to_string(), + bucket: args.bucket.to_string(), object_name: args.object.to_string(), upload_id: args.upload_id.to_string(), }) @@ -1164,7 +1165,7 @@ impl Client { Ok(CreateMultipartUploadResponse { headers: header_map.clone(), region: region.clone(), - bucket_name: args.bucket.to_string(), + bucket: args.bucket.to_string(), object_name: args.object.to_string(), upload_id: get_text(&root, "UploadId")?, }) @@ -2523,42 +2524,6 @@ impl Client { }) } - pub async fn set_bucket_encryption( - &self, - args: &SetBucketEncryptionArgs<'_>, - ) -> 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("encryption"), String::new()); - - let resp = self - .execute( - Method::PUT, - ®ion, - &mut headers, - &query_params, - Some(args.bucket), - None, - Some(args.config.to_xml().into()), - ) - .await?; - - Ok(SetBucketEncryptionResponse { - headers: resp.headers().clone(), - region: region.clone(), - bucket_name: args.bucket.to_string(), - }) - } - pub async fn set_bucket_lifecycle( &self, args: &SetBucketLifecycleArgs<'_>, diff --git a/src/s3/client/set_bucket_encryption.rs b/src/s3/client/set_bucket_encryption.rs new file mode 100644 index 0000000..6120219 --- /dev/null +++ b/src/s3/client/set_bucket_encryption.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::SetBucketEncryption; + +impl Client { + /// Create a SetBucketEncryption request builder. + pub fn set_bucket_encryption(&self, bucket: &str) -> SetBucketEncryption { + SetBucketEncryption::new(bucket).client(self) + } +} diff --git a/src/s3/response.rs b/src/s3/response.rs index e4121dc..e96c72c 100644 --- a/src/s3/response.rs +++ b/src/s3/response.rs @@ -40,6 +40,7 @@ mod listen_bucket_notification; mod object_prompt; mod put_object; mod remove_objects; +mod set_bucket_encryption; pub use get_bucket_encryption::GetBucketEncryptionResponse; pub use get_bucket_versioning::GetBucketVersioningResponse; @@ -54,6 +55,7 @@ pub use put_object::{ UploadPartResponse2, }; pub use remove_objects::{DeleteError, DeletedObject, RemoveObjectResponse, RemoveObjectsResponse}; +pub use set_bucket_encryption::SetBucketEncryptionResponse; #[derive(Debug)] /// Base response for bucket operation @@ -84,7 +86,7 @@ pub struct ObjectResponse { pub struct UploadIdResponse { pub headers: HeaderMap, pub region: String, - pub bucket_name: String, + pub bucket: String, pub object_name: String, pub upload_id: String, } @@ -554,9 +556,6 @@ impl SelectObjectContentResponse { /// Response of [delete_bucket_encryption()](crate::s3::client::Client::delete_bucket_encryption) API pub type DeleteBucketEncryptionResponse = BucketResponse; -/// Response of [set_bucket_encryption()](crate::s3::client::Client::set_bucket_encryption) API -pub type SetBucketEncryptionResponse = BucketResponse; - /// Response of [enable_object_legal_hold()](crate::s3::client::Client::enable_object_legal_hold) API pub type EnableObjectLegalHoldResponse = ObjectResponse; diff --git a/src/s3/response/get_bucket_encryption.rs b/src/s3/response/get_bucket_encryption.rs index c8e5547..a81b94b 100644 --- a/src/s3/response/get_bucket_encryption.rs +++ b/src/s3/response/get_bucket_encryption.rs @@ -38,6 +38,11 @@ impl FromS3Response for GetBucketEncryptionResponse { 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(), + }; + let headers = resp.headers().clone(); let body = resp.bytes().await?; let mut root = Element::parse(body.reader())?; @@ -55,7 +60,7 @@ impl FromS3Response for GetBucketEncryptionResponse { Ok(GetBucketEncryptionResponse { headers, region: req.get_computed_region(), - bucket: req.bucket.unwrap().to_string(), // TODO remove unwrap + bucket, config: SseConfig { sse_algorithm: get_text(sse_by_default, "SSEAlgorithm")?, kms_master_key_id: get_option_text(sse_by_default, "KMSMasterKeyID"), diff --git a/src/s3/response/get_bucket_versioning.rs b/src/s3/response/get_bucket_versioning.rs index d4d4330..e479de8 100644 --- a/src/s3/response/get_bucket_versioning.rs +++ b/src/s3/response/get_bucket_versioning.rs @@ -39,16 +39,23 @@ impl FromS3Response for GetBucketVersioningResponse { 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(), + }; + 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 mfa_delete: Option = get_option_text(&root, "MFADelete").map(|v| v == "Enabled"); Ok(GetBucketVersioningResponse { headers, region: req.get_computed_region(), - bucket: req.bucket.unwrap().to_string(), // TODO remove unwrap - status: get_option_text(&root, "Status").map(|v| v == "Enabled"), - mfa_delete: get_option_text(&root, "MFADelete").map(|v| v == "Enabled"), + bucket, + status, + mfa_delete, }) } } diff --git a/src/s3/response/set_bucket_encryption.rs b/src/s3/response/set_bucket_encryption.rs new file mode 100644 index 0000000..99d2bb9 --- /dev/null +++ b/src/s3/response/set_bucket_encryption.rs @@ -0,0 +1,70 @@ +// 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::types::{FromS3Response, S3Request, SseConfig}; +use crate::s3::utils::{get_option_text, get_text}; +use async_trait::async_trait; +use bytes::Buf; +use http::HeaderMap; +use xmltree::Element; + +/// Response of +/// [set_bucket_encryption()](crate::s3::client::Client::set_bucket_encryption) +/// API +#[derive(Clone, Debug)] +pub struct SetBucketEncryptionResponse { + pub headers: HeaderMap, + pub region: String, + pub bucket: String, + pub config: SseConfig, +} + +#[async_trait] +impl FromS3Response for SetBucketEncryptionResponse { + 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(), + }; + + let headers = resp.headers().clone(); + let body = resp.bytes().await?; + let mut root = Element::parse(body.reader())?; + + let rule = root + .get_mut_child("Rule") + .ok_or(Error::XmlError(String::from(" tag not found")))?; + + let sse_by_default = rule + .get_mut_child("ApplyServerSideEncryptionByDefault") + .ok_or(Error::XmlError(String::from( + " tag not found", + )))?; + + Ok(SetBucketEncryptionResponse { + headers, + region: req.get_computed_region(), + bucket, + config: SseConfig { + sse_algorithm: get_text(sse_by_default, "SSEAlgorithm")?, + kms_master_key_id: get_option_text(sse_by_default, "KMSMasterKeyID"), + }, + }) + } +} diff --git a/src/s3/types.rs b/src/s3/types.rs index c001408..a29449e 100644 --- a/src/s3/types.rs +++ b/src/s3/types.rs @@ -790,7 +790,7 @@ impl fmt::Display for Directive { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] /// Server-side information configuration pub struct SseConfig { pub sse_algorithm: String, diff --git a/tests/tests.rs b/tests/tests.rs index 6afba58..7f31a69 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1268,7 +1268,7 @@ impl ClientTest { .send() .await .unwrap(); - assert!(resp.status.unwrap_or_default()); + assert_eq!(resp.status, Some(true)); self.client .set_bucket_versioning(&SetBucketVersioningArgs::new(&bucket_name, false).unwrap()) @@ -1281,7 +1281,7 @@ impl ClientTest { .send() .await .unwrap(); - assert!(!resp.status.unwrap_or_default()); + assert_eq!(resp.status, Some(false)); self.client .remove_bucket(&RemoveBucketArgs::new(&bucket_name).unwrap())