tests cleanup; cargo clippy fixes, minor doc updates (#177)

* Tests cleanup; cargo clippy fixes, minor doc updates
* updated label checker workflow
This commit is contained in:
Henk-Jan Lebbink 2025-07-11 11:08:22 +02:00 committed by GitHub
parent e0a77fcb1a
commit e244229490
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 326 additions and 216 deletions

View File

@ -6,14 +6,12 @@ on:
- synchronize - synchronize
- labeled - labeled
- unlabeled - unlabeled
jobs: jobs:
check_labels: check_labels:
name: Check for labels name: Check for labels
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: docker://agilepathway/pull-request-label-checker:latest - uses: docker://agilepathway/pull-request-label-checker:latest
with: with:
one_of: highlight,breaking-change,security-fix,enhancement,bug any_of: highlight,breaking-change,security-fix,enhancement,bug,cleanup-rewrite,regression-fix,codex
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -41,9 +41,10 @@ hmac = { version = "0.12.1", optional = true }
hyper = { version = "1.6.0", features = ["full"] } hyper = { version = "1.6.0", features = ["full"] }
lazy_static = "1.5.0" lazy_static = "1.5.0"
log = "0.4.27" log = "0.4.27"
md5 = "0.7.0" md5 = "0.8.0"
multimap = "0.10.1" multimap = "0.10.1"
percent-encoding = "2.3.1" percent-encoding = "2.3.1"
url = "2.5.4"
rand = { version = "0.8.5", features = ["small_rng"] } rand = { version = "0.8.5", features = ["small_rng"] }
regex = "1.11.1" regex = "1.11.1"
ring = { version = "0.17.14", optional = true, default-features = false, features = ["alloc"] } ring = { version = "0.17.14", optional = true, default-features = false, features = ["alloc"] }

View File

@ -14,6 +14,7 @@ chrono = "0.4.41"
reqwest = "0.12.20" reqwest = "0.12.20"
http = "1.3.1" http = "1.3.1"
futures = "0.3.31" futures = "0.3.31"
uuid = { version = "1.17.0", features = ["v4"] }
[lib] [lib]
name = "minio_common" name = "minio_common"

View File

@ -15,16 +15,24 @@
use http::{Response as HttpResponse, StatusCode}; use http::{Response as HttpResponse, StatusCode};
use minio::s3::error::Error; use minio::s3::error::Error;
use rand::distributions::{Alphanumeric, DistString}; use rand::distributions::Standard;
use rand::{Rng, thread_rng};
use uuid::Uuid;
pub fn rand_bucket_name() -> String { pub fn rand_bucket_name() -> String {
Alphanumeric format!("test-bucket-{}", Uuid::new_v4())
.sample_string(&mut rand::thread_rng(), 8)
.to_lowercase()
} }
pub fn rand_object_name() -> String { pub fn rand_object_name() -> String {
Alphanumeric.sample_string(&mut rand::thread_rng(), 20) format!("test-object-{}", Uuid::new_v4())
}
pub fn rand_object_name_utf8(len: usize) -> String {
let rng = thread_rng();
rng.sample_iter::<char, _>(Standard)
.filter(|c| !c.is_control())
.take(len)
.collect()
} }
pub async fn get_bytes_from_response(v: Result<reqwest::Response, Error>) -> bytes::Bytes { pub async fn get_bytes_from_response(v: Result<reqwest::Response, Error>) -> bytes::Bytes {

View File

@ -209,7 +209,7 @@ impl DeleteObjects {
} }
/// Enable verbose mode (defaults to false). If enabled, the response will /// Enable verbose mode (defaults to false). If enabled, the response will
/// include the keys of objects that were successfully deleted. Otherwise /// include the keys of objects that were successfully deleted. Otherwise,
/// only objects that encountered an error are returned. /// only objects that encountered an error are returned.
pub fn verbose_mode(mut self, verbose_mode: bool) -> Self { pub fn verbose_mode(mut self, verbose_mode: bool) -> Self {
self.verbose_mode = verbose_mode; self.verbose_mode = verbose_mode;

View File

@ -57,35 +57,33 @@ impl Client {
bucket: S, bucket: S,
) -> Result<DeleteBucketResponse, Error> { ) -> Result<DeleteBucketResponse, Error> {
let bucket: String = bucket.into(); let bucket: String = bucket.into();
if self.is_minio_express().await { let is_express = self.is_minio_express().await;
let mut stream = self.list_objects(&bucket).to_stream().await;
while let Some(items) = stream.next().await { let mut stream = self
let mut resp = self .list_objects(&bucket)
.delete_objects_streaming( .include_versions(!is_express)
&bucket, .recursive(true)
items?.contents.into_iter().map(ObjectToDelete::from),
)
.to_stream() .to_stream()
.await; .await;
if is_express {
while let Some(items) = stream.next().await {
let object_names = items?.contents.into_iter().map(ObjectToDelete::from);
let mut resp = self
.delete_objects_streaming(&bucket, object_names)
.bypass_governance_mode(false) // Express does not support governance mode
.to_stream()
.await;
while let Some(item) = resp.next().await { while let Some(item) = resp.next().await {
let _resp: DeleteObjectsResponse = item?; let _resp: DeleteObjectsResponse = item?;
} }
} }
} else { } else {
let mut stream = self
.list_objects(&bucket)
.include_versions(true)
.recursive(true)
.to_stream()
.await;
while let Some(items) = stream.next().await { while let Some(items) = stream.next().await {
let object_names = items?.contents.into_iter().map(ObjectToDelete::from);
let mut resp = self let mut resp = self
.delete_objects_streaming( .delete_objects_streaming(&bucket, object_names)
&bucket,
items?.contents.into_iter().map(ObjectToDelete::from),
)
.bypass_governance_mode(true) .bypass_governance_mode(true)
.to_stream() .to_stream()
.await; .await;
@ -117,16 +115,41 @@ impl Client {
} }
} }
} }
let request: DeleteBucket = self.delete_bucket(bucket);
let request: DeleteBucket = self.delete_bucket(&bucket);
match request.send().await { match request.send().await {
Ok(resp) => Ok(resp), Ok(resp) => Ok(resp),
Err(Error::S3Error(e)) => { Err(Error::S3Error(mut e)) => {
if e.code == ErrorCode::NoSuchBucket { if matches!(e.code, ErrorCode::NoSuchBucket) {
Ok(DeleteBucketResponse { Ok(DeleteBucketResponse {
request: Default::default(), //TODO consider how to handle this request: Default::default(), //TODO consider how to handle this
body: Bytes::new(), body: Bytes::new(),
headers: e.headers, headers: e.headers,
}) })
} else if let ErrorCode::BucketNotEmpty(reason) = &e.code {
// for convenience, add the first 5 documents that were are still in the bucket
// to the error message
let mut stream = self
.list_objects(&bucket)
.include_versions(!is_express)
.recursive(true)
.to_stream()
.await;
let mut objs = Vec::new();
while let Some(items_result) = stream.next().await {
if let Ok(items) = items_result {
objs.extend(items.contents);
if objs.len() >= 5 {
break;
}
}
// else: silently ignore the error and keep looping
}
let new_reason = format!("{reason}: found content: {objs:?}");
e.code = ErrorCode::BucketNotEmpty(new_reason);
Err(Error::S3Error(e))
} else { } else {
Err(Error::S3Error(e)) Err(Error::S3Error(e))
} }

View File

@ -66,7 +66,7 @@ impl Client {
/// Creates a [`DeleteObjectsStreaming`] request builder to delete a stream of objects from an S3 bucket. /// Creates a [`DeleteObjectsStreaming`] request builder to delete a stream of objects from an S3 bucket.
/// ///
/// to execute the request, call [`DeleteObjectsStreaming::to_stream()`](crate::s3::types::S3Api::send), /// To execute the request, call [`DeleteObjectsStreaming::to_stream()`](crate::s3::types::S3Api::send),
/// which returns a [`Result`] containing a [`DeleteObjectsResponse`](crate::s3::response::DeleteObjectsResponse). /// which returns a [`Result`] containing a [`DeleteObjectsResponse`](crate::s3::response::DeleteObjectsResponse).
pub fn delete_objects_streaming<S: Into<String>, D: Into<ObjectsStream>>( pub fn delete_objects_streaming<S: Into<String>, D: Into<ObjectsStream>>(
&self, &self,

View File

@ -44,7 +44,7 @@ pub enum ErrorCode {
ResourceConflict, ResourceConflict,
AccessDenied, AccessDenied,
NotSupported, NotSupported,
BucketNotEmpty, BucketNotEmpty(String), // String contains optional reason msg
BucketAlreadyOwnedByYou, BucketAlreadyOwnedByYou,
InvalidWriteOffset, InvalidWriteOffset,
@ -75,7 +75,7 @@ impl ErrorCode {
"resourceconflict" => ErrorCode::ResourceConflict, "resourceconflict" => ErrorCode::ResourceConflict,
"accessdenied" => ErrorCode::AccessDenied, "accessdenied" => ErrorCode::AccessDenied,
"notsupported" => ErrorCode::NotSupported, "notsupported" => ErrorCode::NotSupported,
"bucketnotempty" => ErrorCode::BucketNotEmpty, "bucketnotempty" => ErrorCode::BucketNotEmpty("".to_string()),
"bucketalreadyownedbyyou" => ErrorCode::BucketAlreadyOwnedByYou, "bucketalreadyownedbyyou" => ErrorCode::BucketAlreadyOwnedByYou,
"invalidwriteoffset" => ErrorCode::InvalidWriteOffset, "invalidwriteoffset" => ErrorCode::InvalidWriteOffset,

View File

@ -480,5 +480,5 @@ impl LifecycleRule {
fn parse_iso8601(date_str: &str) -> Result<chrono::DateTime<chrono::Utc>, Error> { fn parse_iso8601(date_str: &str) -> Result<chrono::DateTime<chrono::Utc>, Error> {
chrono::DateTime::parse_from_rfc3339(date_str) chrono::DateTime::parse_from_rfc3339(date_str)
.map(|dt| dt.with_timezone(&chrono::Utc)) .map(|dt| dt.with_timezone(&chrono::Utc))
.map_err(|_| Error::XmlError(format!("Invalid date format: {}", date_str))) .map_err(|_| Error::XmlError(format!("Invalid date format: {date_str}")))
} }

View File

@ -50,7 +50,7 @@ impl FromS3Response for BucketExistsResponse {
body: resp.bytes().await?, body: resp.bytes().await?,
exists: true, exists: true,
}), }),
Err(Error::S3Error(e)) if e.code == ErrorCode::NoSuchBucket => Ok(Self { Err(Error::S3Error(e)) if matches!(e.code, ErrorCode::NoSuchBucket) => Ok(Self {
request, request,
headers: e.headers, headers: e.headers,
body: Bytes::new(), body: Bytes::new(),

View File

@ -48,7 +48,7 @@ impl FromS3Response for DeleteBucketPolicyResponse {
headers: mem::take(resp.headers_mut()), headers: mem::take(resp.headers_mut()),
body: resp.bytes().await?, body: resp.bytes().await?,
}), }),
Err(Error::S3Error(e)) if e.code == ErrorCode::NoSuchBucketPolicy => Ok(Self { Err(Error::S3Error(e)) if matches!(e.code, ErrorCode::NoSuchBucketPolicy) => Ok(Self {
request, request,
headers: e.headers, headers: e.headers,
body: Bytes::new(), body: Bytes::new(),

View File

@ -49,7 +49,7 @@ impl FromS3Response for DeleteBucketReplicationResponse {
body: resp.bytes().await?, body: resp.bytes().await?,
}), }),
Err(Error::S3Error(e)) Err(Error::S3Error(e))
if e.code == ErrorCode::ReplicationConfigurationNotFoundError => if matches!(e.code, ErrorCode::ReplicationConfigurationNotFoundError) =>
{ {
Ok(Self { Ok(Self {
request, request,

View File

@ -85,7 +85,10 @@ impl FromS3Response for GetBucketEncryptionResponse {
body: resp.bytes().await?, body: resp.bytes().await?,
}), }),
Err(Error::S3Error(e)) Err(Error::S3Error(e))
if e.code == ErrorCode::ServerSideEncryptionConfigurationNotFoundError => if matches!(
e.code,
ErrorCode::ServerSideEncryptionConfigurationNotFoundError
) =>
{ {
Ok(Self { Ok(Self {
request, request,

View File

@ -48,7 +48,7 @@ impl GetBucketPolicyResponse {
/// for accessing the bucket and its contents. /// for accessing the bucket and its contents.
pub fn config(&self) -> Result<&str, Error> { pub fn config(&self) -> Result<&str, Error> {
std::str::from_utf8(&self.body).map_err(|e| { std::str::from_utf8(&self.body).map_err(|e| {
Error::Utf8Error(format!("Failed to parse bucket policy as UTF-8: {}", e).into()) Error::Utf8Error(format!("Failed to parse bucket policy as UTF-8: {e}").into())
}) })
} }
} }
@ -65,7 +65,7 @@ impl FromS3Response for GetBucketPolicyResponse {
headers: mem::take(resp.headers_mut()), headers: mem::take(resp.headers_mut()),
body: resp.bytes().await?, body: resp.bytes().await?,
}), }),
Err(Error::S3Error(e)) if e.code == ErrorCode::NoSuchBucketPolicy => Ok(Self { Err(Error::S3Error(e)) if matches!(e.code, ErrorCode::NoSuchBucketPolicy) => Ok(Self {
request, request,
headers: e.headers, headers: e.headers,
body: Bytes::from_static("{}".as_ref()), body: Bytes::from_static("{}".as_ref()),

View File

@ -54,7 +54,7 @@ impl FromS3Response for GetBucketTaggingResponse {
headers: mem::take(resp.headers_mut()), headers: mem::take(resp.headers_mut()),
body: resp.bytes().await?, body: resp.bytes().await?,
}), }),
Err(Error::S3Error(e)) if e.code == ErrorCode::NoSuchTagSet => Ok(Self { Err(Error::S3Error(e)) if matches!(e.code, ErrorCode::NoSuchTagSet) => Ok(Self {
request, request,
headers: e.headers, headers: e.headers,
body: Bytes::new(), body: Bytes::new(),

View File

@ -40,7 +40,7 @@ impl GetObjectPromptResponse {
/// This method retrieves the content of the object as a UTF-8 encoded string. /// This method retrieves the content of the object as a UTF-8 encoded string.
pub fn prompt_response(&self) -> Result<&str, Error> { pub fn prompt_response(&self) -> Result<&str, Error> {
std::str::from_utf8(&self.body).map_err(|e| { std::str::from_utf8(&self.body).map_err(|e| {
Error::Utf8Error(format!("Failed to parse prompt_response as UTF-8: {}", e).into()) Error::Utf8Error(format!("Failed to parse prompt_response as UTF-8: {e}").into())
}) })
} }
} }

View File

@ -83,7 +83,9 @@ impl FromS3Response for GetObjectRetentionResponse {
headers: mem::take(resp.headers_mut()), headers: mem::take(resp.headers_mut()),
body: resp.bytes().await?, body: resp.bytes().await?,
}), }),
Err(Error::S3Error(e)) if e.code == ErrorCode::NoSuchObjectLockConfiguration => { Err(Error::S3Error(e))
if matches!(e.code, ErrorCode::NoSuchObjectLockConfiguration) =>
{
Ok(Self { Ok(Self {
request, request,
headers: e.headers, headers: e.headers,

View File

@ -10,18 +10,12 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//! Response types for ListObjects APIs
use crate::impl_has_s3fields; use crate::impl_has_s3fields;
use crate::s3::error::Error;
use crate::s3::response::a_response_traits::HasS3Fields; use crate::s3::response::a_response_traits::HasS3Fields;
use crate::s3::{ use crate::s3::types::{FromS3Response, ListEntry, S3Request};
error::Error, use crate::s3::utils::xml::{Element, MergeXmlElements};
types::{FromS3Response, ListEntry, S3Request}, use crate::s3::utils::{from_iso8601utc, parse_tags, urldecode};
utils::{
from_iso8601utc, parse_tags, urldecode,
xml::{Element, MergeXmlElements},
},
};
use async_trait::async_trait; use async_trait::async_trait;
use bytes::{Buf, Bytes}; use bytes::{Buf, Bytes};
use reqwest::header::HeaderMap; use reqwest::header::HeaderMap;

View File

@ -80,7 +80,7 @@ impl SegmentedBytes {
impl fmt::Display for SegmentedBytes { impl fmt::Display for SegmentedBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match std::str::from_utf8(self.to_bytes().as_ref()) { match std::str::from_utf8(self.to_bytes().as_ref()) {
Ok(s) => write!(f, "{}", s), Ok(s) => write!(f, "{s}"),
Err(_) => Ok(()), // or: write!(f, "<invalid utf8>") Err(_) => Ok(()), // or: write!(f, "<invalid utf8>")
} }
} }

View File

@ -255,14 +255,14 @@ pub fn check_bucket_name(bucket_name: impl AsRef<str>, strict: bool) -> Result<(
)); ));
} }
if bucket_name_len < 3 { if bucket_name_len < 3 {
return Err(Error::InvalidBucketName( return Err(Error::InvalidBucketName(format!(
"bucket name cannot be less than 3 characters".into(), "bucket name ('{bucket_name}') cannot be less than 3 characters"
)); )));
} }
if bucket_name_len > 63 { if bucket_name_len > 63 {
return Err(Error::InvalidBucketName( return Err(Error::InvalidBucketName(format!(
"Bucket name cannot be greater than 63 characters".into(), "Bucket name ('{bucket_name}') cannot be greater than 63 characters"
)); )));
} }
lazy_static! { lazy_static! {
@ -274,8 +274,8 @@ pub fn check_bucket_name(bucket_name: impl AsRef<str>, strict: bool) -> Result<(
} }
if IPV4_REGEX.is_match(bucket_name) { if IPV4_REGEX.is_match(bucket_name) {
return Err(Error::InvalidBucketName(String::from( return Err(Error::InvalidBucketName(format!(
"bucket name cannot be an IP address", "bucket name ('{bucket_name}') cannot be an IP address"
))); )));
} }
@ -288,12 +288,14 @@ pub fn check_bucket_name(bucket_name: impl AsRef<str>, strict: bool) -> Result<(
if strict { if strict {
if !VALID_BUCKET_NAME_STRICT_REGEX.is_match(bucket_name) { if !VALID_BUCKET_NAME_STRICT_REGEX.is_match(bucket_name) {
return Err(Error::InvalidBucketName(format!( return Err(Error::InvalidBucketName(format!(
"bucket name ('{bucket_name}') does not follow S3 standards strictly", "bucket name ('{bucket_name}') does not follow S3 standards strictly, according to {}",
*VALID_BUCKET_NAME_STRICT_REGEX
))); )));
} }
} else if !VALID_BUCKET_NAME_REGEX.is_match(bucket_name) { } else if !VALID_BUCKET_NAME_REGEX.is_match(bucket_name) {
return Err(Error::InvalidBucketName(format!( return Err(Error::InvalidBucketName(format!(
"bucket name ('{bucket_name}') does not follow S3 standards" "bucket name ('{bucket_name}') does not follow S3 standards, according to {}",
*VALID_BUCKET_NAME_REGEX
))); )));
} }
@ -301,13 +303,20 @@ pub fn check_bucket_name(bucket_name: impl AsRef<str>, strict: bool) -> Result<(
} }
pub fn check_object_name(object_name: impl AsRef<str>) -> Result<(), Error> { pub fn check_object_name(object_name: impl AsRef<str>) -> Result<(), Error> {
if object_name.as_ref().is_empty() { let object_name: &str = object_name.as_ref();
Err(Error::InvalidObjectName( let object_name_n_bytes = object_name.len();
if object_name_n_bytes == 0 {
return Err(Error::InvalidObjectName(
"object name cannot be empty".into(), "object name cannot be empty".into(),
)) ));
} else {
Ok(())
} }
if object_name_n_bytes > 1024 {
return Err(Error::InvalidObjectName(format!(
"Object name ('{object_name}') cannot be greater than 1024 bytes"
)));
}
Ok(())
} }
/// Gets text value of given XML element for given tag. /// Gets text value of given XML element for given tag.

View File

@ -15,11 +15,13 @@
use minio::s3::client::DEFAULT_REGION; use minio::s3::client::DEFAULT_REGION;
use minio::s3::error::{Error, ErrorCode}; use minio::s3::error::{Error, ErrorCode};
use minio::s3::response::a_response_traits::{HasBucket, HasRegion}; use minio::s3::response::a_response_traits::{HasBucket, HasObject, HasRegion};
use minio::s3::response::{BucketExistsResponse, CreateBucketResponse, DeleteBucketResponse}; use minio::s3::response::{
BucketExistsResponse, CreateBucketResponse, DeleteBucketResponse, PutObjectContentResponse,
};
use minio::s3::types::S3Api; use minio::s3::types::S3Api;
use minio_common::test_context::TestContext; use minio_common::test_context::TestContext;
use minio_common::utils::rand_bucket_name; use minio_common::utils::{rand_bucket_name, rand_object_name};
#[minio_macros::test(no_bucket)] #[minio_macros::test(no_bucket)]
async fn bucket_create(ctx: TestContext) { async fn bucket_create(ctx: TestContext) {
@ -41,7 +43,7 @@ async fn bucket_create(ctx: TestContext) {
ctx.client.create_bucket(&bucket_name).send().await; ctx.client.create_bucket(&bucket_name).send().await;
match resp { match resp {
Ok(_) => panic!("Bucket already exists, but was created again"), Ok(_) => panic!("Bucket already exists, but was created again"),
Err(Error::S3Error(e)) if e.code == ErrorCode::BucketAlreadyOwnedByYou => { Err(Error::S3Error(e)) if matches!(e.code, ErrorCode::BucketAlreadyOwnedByYou) => {
// this is expected, as the bucket already exists // this is expected, as the bucket already exists
} }
Err(e) => panic!("Unexpected error: {:?}", e), Err(e) => panic!("Unexpected error: {:?}", e),
@ -57,7 +59,7 @@ async fn bucket_delete(ctx: TestContext) {
ctx.client.delete_bucket(&bucket_name).send().await; ctx.client.delete_bucket(&bucket_name).send().await;
match resp { match resp {
Ok(_) => panic!("Bucket does not exist, but was removed"), Ok(_) => panic!("Bucket does not exist, but was removed"),
Err(Error::S3Error(e)) if e.code == ErrorCode::NoSuchBucket => { Err(Error::S3Error(e)) if matches!(e.code, ErrorCode::NoSuchBucket) => {
// this is expected, as the bucket does not exist // this is expected, as the bucket does not exist
} }
Err(e) => panic!("Unexpected error: {:?}", e), Err(e) => panic!("Unexpected error: {:?}", e),
@ -85,3 +87,40 @@ async fn bucket_delete(ctx: TestContext) {
assert_eq!(resp.bucket(), bucket_name); assert_eq!(resp.bucket(), bucket_name);
assert_eq!(resp.region(), ""); assert_eq!(resp.region(), "");
} }
#[minio_macros::test(no_bucket)]
async fn bucket_delete_and_purge_1(ctx: TestContext) {
let bucket_name = rand_bucket_name();
// create a new bucket
let resp: CreateBucketResponse = ctx.client.create_bucket(&bucket_name).send().await.unwrap();
assert_eq!(resp.bucket(), bucket_name);
assert_eq!(resp.region(), DEFAULT_REGION);
// add some objects to the bucket
for _ in 0..5 {
let object_name = rand_object_name();
let resp: PutObjectContentResponse = ctx
.client
.put_object_content(&bucket_name, &object_name, "Hello, World!")
.send()
.await
.unwrap();
assert_eq!(resp.bucket(), bucket_name);
assert_eq!(resp.object(), object_name);
}
// try to remove the bucket without purging, this should fail because the bucket is not empty
let resp: Result<DeleteBucketResponse, Error> =
ctx.client.delete_bucket(&bucket_name).send().await;
assert!(resp.is_err());
// try to remove the bucket with purging, this should succeed
let resp: DeleteBucketResponse = ctx
.client
.delete_and_purge_bucket(&bucket_name)
.await
.unwrap();
assert_eq!(resp.bucket(), bucket_name);
}

View File

@ -38,12 +38,7 @@ async fn list_buckets(ctx: TestContext) {
for bucket in resp.buckets().unwrap().iter() { for bucket in resp.buckets().unwrap().iter() {
if names.contains(&bucket.name) { if names.contains(&bucket.name) {
count += 1; count += 1;
} // else if bucket.name.len() == 8 { }
// match ctx.client.delete_and_purge_bucket(&bucket.name).await {
// Ok(_) => println!("Deleted bucket: {}", bucket.name),
// Err(e) => println!("Failed to delete bucket {}: {}", bucket.name, e)
// }
//}
} }
assert_eq!(guards.len(), N_BUCKETS); assert_eq!(guards.len(), N_BUCKETS);
assert_eq!(count, N_BUCKETS); assert_eq!(count, N_BUCKETS);

View File

@ -41,10 +41,10 @@ async fn list_objects(
let is_express = ctx.client.is_minio_express().await; let is_express = ctx.client.is_minio_express().await;
if is_express && !express { if is_express && !express {
println!("Skipping test because it is running in MinIO Express mode"); eprintln!("Skipping test because it is running in MinIO Express mode");
return; return;
} else if !is_express && express { } else if !is_express && express {
println!("Skipping test because it is NOT running in MinIO Express mode"); eprintln!("Skipping test because it is NOT running in MinIO Express mode");
return; return;
} }
@ -97,29 +97,29 @@ async fn list_objects(
assert_eq!(names_set_after, names_set_before); assert_eq!(names_set_after, names_set_before);
} }
#[minio_macros::test] #[minio_macros::test(skip_if_express)]
async fn list_objects_v1_no_versions(ctx: TestContext, bucket_name: String) { async fn list_objects_v1_no_versions(ctx: TestContext, bucket_name: String) {
list_objects(true, false, false, 5, 5, ctx, bucket_name).await; list_objects(true, false, false, 5, 5, ctx, bucket_name).await;
} }
#[minio_macros::test] #[minio_macros::test(skip_if_express)]
async fn list_objects_v1_with_versions(ctx: TestContext, bucket_name: String) { async fn list_objects_v1_with_versions(ctx: TestContext, bucket_name: String) {
list_objects(true, true, false, 5, 5, ctx, bucket_name).await; list_objects(true, true, false, 5, 5, ctx, bucket_name).await;
} }
#[minio_macros::test] #[minio_macros::test(skip_if_express)]
async fn list_objects_v2_no_versions(ctx: TestContext, bucket_name: String) { async fn list_objects_v2_no_versions(ctx: TestContext, bucket_name: String) {
list_objects(false, false, false, 5, 5, ctx, bucket_name).await; list_objects(false, false, false, 5, 5, ctx, bucket_name).await;
} }
#[minio_macros::test] #[minio_macros::test(skip_if_express)]
async fn list_objects_v2_with_versions(ctx: TestContext, bucket_name: String) { async fn list_objects_v2_with_versions(ctx: TestContext, bucket_name: String) {
list_objects(false, true, false, 5, 5, ctx, bucket_name).await; list_objects(false, true, false, 5, 5, ctx, bucket_name).await;
} }
/// Test for S3-Express: List objects with S3-Express are only supported with V2 API, without /// Test for S3-Express: List objects with S3-Express are only supported with V2 API, without
/// versions, and yield unsorted results. /// versions, and yield results that need not be sorted.
#[minio_macros::test] #[minio_macros::test(skip_if_not_express)]
async fn list_objects_express(ctx: TestContext, bucket_name: String) { async fn list_objects_express(ctx: TestContext, bucket_name: String) {
list_objects(false, false, true, 5, 5, ctx, bucket_name).await; list_objects(false, false, true, 5, 5, ctx, bucket_name).await;
} }

133
tests/test_object_delete.rs Normal file
View File

@ -0,0 +1,133 @@
// 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 async_std::stream::StreamExt;
use minio::s3::builders::ObjectToDelete;
use minio::s3::response::a_response_traits::{HasBucket, HasObject};
use minio::s3::response::{
DeleteObjectResponse, DeleteObjectsResponse, DeleteResult, PutObjectContentResponse,
};
use minio::s3::types::{S3Api, ToStream};
use minio_common::test_context::TestContext;
use minio_common::utils::rand_object_name_utf8;
async fn create_object(
ctx: &TestContext,
bucket_name: &str,
object_name: &str,
) -> PutObjectContentResponse {
let resp: PutObjectContentResponse = ctx
.client
.put_object_content(bucket_name, object_name, "hello world")
.send()
.await
.unwrap();
assert_eq!(resp.bucket(), bucket_name);
assert_eq!(resp.object(), object_name);
resp
}
#[minio_macros::test]
async fn delete_object(ctx: TestContext, bucket_name: String) {
let object_name = rand_object_name_utf8(20);
let _resp = create_object(&ctx, &bucket_name, &object_name).await;
let resp: DeleteObjectResponse = ctx
.client
.delete_object(&bucket_name, &object_name)
.send()
.await
.unwrap();
assert_eq!(resp.bucket(), bucket_name);
}
#[minio_macros::test]
async fn delete_object_with_whitespace(ctx: TestContext, bucket_name: String) {
let object_name = format!(" {}", rand_object_name_utf8(20));
let _resp = create_object(&ctx, &bucket_name, &object_name).await;
let resp: DeleteObjectResponse = ctx
.client
.delete_object(&bucket_name, &object_name)
.send()
.await
.unwrap();
assert_eq!(resp.bucket(), bucket_name);
}
#[minio_macros::test]
async fn delete_objects(ctx: TestContext, bucket_name: String) {
const OBJECT_COUNT: usize = 3;
let mut names: Vec<String> = Vec::new();
for _ in 1..=OBJECT_COUNT {
let object_name = rand_object_name_utf8(20);
let _resp = create_object(&ctx, &bucket_name, &object_name).await;
names.push(object_name);
}
let del_items: Vec<ObjectToDelete> = names
.iter()
.map(|v| ObjectToDelete::from(v.as_str()))
.collect();
let resp: DeleteObjectsResponse = ctx
.client
.delete_objects::<&String, ObjectToDelete>(&bucket_name, del_items)
.verbose_mode(true) // Enable verbose mode to get detailed response
.send()
.await
.unwrap();
let deleted_names: Vec<DeleteResult> = resp.result().unwrap();
assert_eq!(deleted_names.len(), OBJECT_COUNT);
for obj in deleted_names.iter() {
assert!(obj.is_deleted());
}
}
#[minio_macros::test]
async fn delete_objects_streaming(ctx: TestContext, bucket_name: String) {
const OBJECT_COUNT: usize = 3;
let mut names: Vec<String> = Vec::new();
for _ in 1..=OBJECT_COUNT {
let object_name = rand_object_name_utf8(20);
let _resp = create_object(&ctx, &bucket_name, &object_name).await;
names.push(object_name);
}
let del_items: Vec<ObjectToDelete> = names
.iter()
.map(|v| ObjectToDelete::from(v.as_str()))
.collect();
let mut resp = ctx
.client
.delete_objects_streaming(&bucket_name, del_items.into_iter())
.verbose_mode(true)
.to_stream()
.await;
let mut del_count = 0;
while let Some(item) = resp.next().await {
let res = item.unwrap();
let del_result = res.result().unwrap();
del_count += del_result.len();
for obj in del_result.into_iter() {
assert!(obj.is_deleted());
}
}
assert_eq!(del_count, 3);
}

View File

@ -15,9 +15,8 @@
use http::header; use http::header;
use minio::s3::builders::{MIN_PART_SIZE, ObjectContent}; use minio::s3::builders::{MIN_PART_SIZE, ObjectContent};
use minio::s3::error::{Error, ErrorCode};
use minio::s3::response::a_response_traits::{ use minio::s3::response::a_response_traits::{
HasBucket, HasEtagFromHeaders, HasIsDeleteMarker, HasObject, HasS3Fields, HasVersion, HasBucket, HasEtagFromHeaders, HasIsDeleteMarker, HasObject, HasS3Fields,
}; };
use minio::s3::response::{DeleteObjectResponse, PutObjectContentResponse, StatObjectResponse}; use minio::s3::response::{DeleteObjectResponse, PutObjectContentResponse, StatObjectResponse};
use minio::s3::types::S3Api; use minio::s3::types::S3Api;
@ -26,16 +25,13 @@ use minio_common::test_context::TestContext;
use minio_common::utils::rand_object_name; use minio_common::utils::rand_object_name;
use tokio::sync::mpsc; use tokio::sync::mpsc;
#[minio_macros::test] async fn test_put_object(ctx: &TestContext, bucket_name: &str, object_name: &str) {
async fn put_object(ctx: TestContext, bucket_name: String) {
let object_name: String = rand_object_name();
let size = 16_u64; let size = 16_u64;
let resp: PutObjectContentResponse = ctx let resp: PutObjectContentResponse = ctx
.client .client
.put_object_content( .put_object_content(
&bucket_name, bucket_name,
&object_name, object_name,
ObjectContent::new_from_stream(RandSrc::new(size), Some(size)), ObjectContent::new_from_stream(RandSrc::new(size), Some(size)),
) )
.send() .send()
@ -48,35 +44,26 @@ async fn put_object(ctx: TestContext, bucket_name: String) {
let resp: StatObjectResponse = ctx let resp: StatObjectResponse = ctx
.client .client
.stat_object(&bucket_name, &object_name) .stat_object(bucket_name, object_name)
.send() .send()
.await .await
.unwrap(); .unwrap();
assert_eq!(resp.bucket(), bucket_name); assert_eq!(resp.bucket(), bucket_name);
assert_eq!(resp.object(), object_name); assert_eq!(resp.object(), object_name);
assert_eq!(resp.size().unwrap(), size); assert_eq!(resp.size().unwrap(), size);
let resp: DeleteObjectResponse = ctx
.client
.delete_object(&bucket_name, &object_name)
.send()
.await
.unwrap();
assert!(resp.version_id().is_none());
// Validate delete succeeded.
let resp: Result<StatObjectResponse, Error> = ctx
.client
.stat_object(&bucket_name, &object_name)
.send()
.await;
match resp.err().unwrap() {
Error::S3Error(er) => {
assert_eq!(er.code, ErrorCode::NoSuchKey)
} }
e => panic!("Unexpected error {:?}", e),
/// Test putting an object into a bucket and verifying its existence.
#[minio_macros::test]
async fn put_object_1(ctx: TestContext, bucket_name: String) {
test_put_object(&ctx, &bucket_name, &rand_object_name()).await;
} }
/// Test putting an object with a name that contains special characters.
#[minio_macros::test]
async fn put_object_2(ctx: TestContext, bucket_name: String) {
test_put_object(&ctx, &bucket_name, "name with+spaces").await;
test_put_object(&ctx, &bucket_name, "name%20with%2Bspaces").await;
} }
#[minio_macros::test] #[minio_macros::test]
@ -108,14 +95,6 @@ async fn put_object_multipart(ctx: TestContext, bucket_name: String) {
assert_eq!(resp.bucket(), bucket_name); assert_eq!(resp.bucket(), bucket_name);
assert_eq!(resp.object(), object_name); assert_eq!(resp.object(), object_name);
assert_eq!(resp.size().unwrap(), size); assert_eq!(resp.size().unwrap(), size);
let resp: DeleteObjectResponse = ctx
.client
.delete_object(&bucket_name, &object_name)
.send()
.await
.unwrap();
assert_eq!(resp.version_id(), None);
} }
#[minio_macros::test] #[minio_macros::test]
@ -192,14 +171,6 @@ async fn put_object_content_2(ctx: TestContext, bucket_name: String) {
.unwrap(); .unwrap();
assert_eq!(resp.size().unwrap(), *size); assert_eq!(resp.size().unwrap(), *size);
assert_eq!(resp.etag().unwrap(), etag); assert_eq!(resp.etag().unwrap(), etag);
let resp: DeleteObjectResponse = ctx
.client
.delete_object(&bucket_name, &object_name)
.send()
.await
.unwrap();
assert_eq!(resp.version_id(), None);
} }
} }
@ -247,11 +218,6 @@ async fn put_object_content_3(ctx: TestContext, bucket_name: String) {
.unwrap(); .unwrap();
assert_eq!(resp.size().unwrap(), sizes[idx]); assert_eq!(resp.size().unwrap(), sizes[idx]);
assert_eq!(resp.etag().unwrap(), etag); assert_eq!(resp.etag().unwrap(), etag);
client
.delete_object(&test_bucket, &object_name)
.send()
.await
.unwrap();
idx += 1; idx += 1;
} }

View File

@ -1,62 +0,0 @@
// 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 async_std::stream::StreamExt;
use minio::s3::builders::ObjectToDelete;
use minio::s3::response::PutObjectContentResponse;
use minio::s3::response::a_response_traits::{HasBucket, HasObject};
use minio::s3::types::ToStream;
use minio_common::test_context::TestContext;
use minio_common::utils::rand_object_name;
#[minio_macros::test]
async fn remove_objects(ctx: TestContext, bucket_name: String) {
let mut names: Vec<String> = Vec::new();
for _ in 1..=3 {
let object_name = rand_object_name();
let resp: PutObjectContentResponse = ctx
.client
.put_object_content(&bucket_name, &object_name, "")
.send()
.await
.unwrap();
assert_eq!(resp.bucket(), bucket_name);
assert_eq!(resp.object(), object_name);
names.push(object_name);
}
let del_items: Vec<ObjectToDelete> = names
.iter()
.map(|v| ObjectToDelete::from(v.as_str()))
.collect();
let mut resp = ctx
.client
.delete_objects_streaming(&bucket_name, del_items.into_iter())
.verbose_mode(true)
.to_stream()
.await;
let mut del_count = 0;
while let Some(item) = resp.next().await {
let res = item.unwrap();
let del_result = res.result().unwrap();
del_count += del_result.len();
for obj in del_result.into_iter() {
assert!(obj.is_deleted());
}
}
assert_eq!(del_count, 3);
}