diff --git a/src/s3/error.rs b/src/s3/error.rs index 7d43120..c5a238b 100644 --- a/src/s3/error.rs +++ b/src/s3/error.rs @@ -46,6 +46,7 @@ pub enum ErrorCode { NotSupported, BucketNotEmpty, BucketAlreadyOwnedByYou, + InvalidWriteOffset, OtherError(String), } @@ -76,6 +77,7 @@ impl ErrorCode { "notsupported" => ErrorCode::NotSupported, "bucketnotempty" => ErrorCode::BucketNotEmpty, "bucketalreadyownedbyyou" => ErrorCode::BucketAlreadyOwnedByYou, + "invalidwriteoffset" => ErrorCode::InvalidWriteOffset, v => ErrorCode::OtherError(v.to_owned()), } diff --git a/src/s3/response/get_bucket_lifecycle.rs b/src/s3/response/get_bucket_lifecycle.rs index 7cca980..e1e858a 100644 --- a/src/s3/response/get_bucket_lifecycle.rs +++ b/src/s3/response/get_bucket_lifecycle.rs @@ -65,8 +65,8 @@ impl FromS3Response for GetBucketLifecycleResponse { let headers: HeaderMap = mem::take(resp.headers_mut()); let config: LifecycleConfig = { let body = resp.bytes().await?; - let mut root = Element::parse(body.reader())?; - LifecycleConfig::from_xml(&mut root)? + let root = Element::parse(body.reader())?; + LifecycleConfig::from_xml(&root)? }; let updated_at: Option> = headers .get("x-minio-lifecycleconfig-updatedat") diff --git a/tests/test_append_object.rs b/tests/test_append_object.rs index 34de11f..014e718 100644 --- a/tests/test_append_object.rs +++ b/tests/test_append_object.rs @@ -14,6 +14,7 @@ // limitations under the License. use minio::s3::builders::ObjectContent; +use minio::s3::error::{Error, ErrorCode}; use minio::s3::response::{ AppendObjectResponse, GetObjectResponse, PutObjectContentResponse, PutObjectResponse, StatObjectResponse, @@ -25,8 +26,51 @@ use minio_common::test_context::TestContext; use minio_common::utils::rand_object_name; use tokio::sync::mpsc; +/// create an object with the given content and check that it is created correctly +async fn create_object_helper( + content: &str, + bucket_name: &str, + object_name: &str, + ctx: &TestContext, +) { + let data: SegmentedBytes = SegmentedBytes::from(content.to_string()); + let size = content.len() as u64; + // create an object (with put) that contains "aaaa" + let resp: PutObjectResponse = ctx + .client + .put_object(bucket_name, object_name, data) + .send() + .await + .unwrap(); + assert_eq!(resp.bucket, bucket_name); + assert_eq!(resp.object, object_name); + + let resp: GetObjectResponse = ctx + .client + .get_object(bucket_name, object_name) + .send() + .await + .unwrap(); + assert_eq!(resp.bucket, bucket_name); + assert_eq!(resp.object, object_name); + assert_eq!(resp.object_size, size); + + // double check that the content we just have put is "aaaa" + let content1: String = String::from_utf8( + resp.content + .to_segmented_bytes() + .await + .unwrap() + .to_bytes() + .to_vec(), + ) + .unwrap(); + assert_eq!(content, content1); +} + +/// Append to the end of an existing object (happy flow) #[tokio::test(flavor = "multi_thread", worker_threads = 10)] -async fn append_object() { +async fn append_object_0() { let ctx = TestContext::new_from_env(); if !ctx.client.is_minio_express() { println!("Skipping test because it is NOT running in MinIO Express mode"); @@ -38,44 +82,16 @@ async fn append_object() { let content1 = "aaaa"; let content2 = "bbbb"; - let size = content1.len() as u64; - let data1: SegmentedBytes = SegmentedBytes::from(content1.to_string()); + + create_object_helper(content1, &bucket_name, &object_name, &ctx).await; + + // now append "bbbb" to the end of the object let data2: SegmentedBytes = SegmentedBytes::from(content2.to_string()); - - let resp: PutObjectResponse = ctx - .client - .put_object(&bucket_name, &object_name, data1) - .send() - .await - .unwrap(); - assert_eq!(resp.bucket, bucket_name); - assert_eq!(resp.object, object_name); - - let resp: GetObjectResponse = ctx - .client - .get_object(&bucket_name, &object_name) - .send() - .await - .unwrap(); - assert_eq!(resp.bucket, bucket_name); - assert_eq!(resp.object, object_name); - assert_eq!(resp.object_size, size); - - let content: String = String::from_utf8( - resp.content - .to_segmented_bytes() - .await - .unwrap() - .to_bytes() - .to_vec(), - ) - .unwrap(); - assert_eq!(content, content1); - + let offset_bytes = size; let resp: AppendObjectResponse = ctx .client - .append_object(&bucket_name, &object_name, data2, size) + .append_object(&bucket_name, &object_name, data2, offset_bytes) .send() .await .unwrap(); @@ -90,6 +106,7 @@ async fn append_object() { .await .unwrap(); + // retrieve the content of the object and check that it is "aaaabbbb" let content: String = String::from_utf8( resp.content .to_segmented_bytes() @@ -106,6 +123,214 @@ async fn append_object() { assert_eq!(resp.object_size, size * 2); } +/// Append to the beginning of an existing object (happy flow) +#[tokio::test(flavor = "multi_thread", worker_threads = 10)] +async fn append_object_1() { + let ctx = TestContext::new_from_env(); + if !ctx.client.is_minio_express() { + println!("Skipping test because it is NOT running in MinIO Express mode"); + return; + } + + let (bucket_name, _cleanup) = ctx.create_bucket_helper().await; + let object_name = rand_object_name(); + + let content1 = "aaaa"; + let content2 = "bbbb"; + let size = content1.len() as u64; + + create_object_helper(content1, &bucket_name, &object_name, &ctx).await; + + // now append "bbbb" to the beginning of the object + let data2: SegmentedBytes = SegmentedBytes::from(content2.to_string()); + let offset_bytes = 0; // byte 0, thus the beginning of the file + let resp: AppendObjectResponse = ctx + .client + .append_object(&bucket_name, &object_name, data2, offset_bytes) + .send() + .await + .unwrap(); + assert_eq!(resp.bucket, bucket_name); + assert_eq!(resp.object, object_name); + assert_eq!(resp.object_size, size); + + let resp: GetObjectResponse = ctx + .client + .get_object(&bucket_name, &object_name) + .send() + .await + .unwrap(); + + // retrieve the content of the object and check that it is "bbbb" + let content: String = String::from_utf8( + resp.content + .to_segmented_bytes() + .await + .unwrap() + .to_bytes() + .to_vec(), + ) + .unwrap(); + assert_eq!(content, content2); + assert_eq!(resp.bucket, bucket_name); + assert_eq!(resp.object, object_name); + assert_eq!(resp.object_size, size); +} + +/// Append to the middle of an existing object (error InvalidWriteOffset) +#[tokio::test(flavor = "multi_thread", worker_threads = 10)] +async fn append_object_2() { + let ctx = TestContext::new_from_env(); + if !ctx.client.is_minio_express() { + println!("Skipping test because it is NOT running in MinIO Express mode"); + return; + } + + let (bucket_name, _cleanup) = ctx.create_bucket_helper().await; + let object_name = rand_object_name(); + + let content1 = "aaaa"; + let content2 = "bbbb"; + let size = content1.len() as u64; + + create_object_helper(content1, &bucket_name, &object_name, &ctx).await; + + // now try to append "bbbb" to the object at an invalid offset + let offset_bytes = size - 1; + let data2: SegmentedBytes = SegmentedBytes::from(content2.to_string()); + let resp: Result = ctx + .client + .append_object(&bucket_name, &object_name, data2, offset_bytes) + .send() + .await; + + match resp { + Ok(v) => panic!("append object should have failed; got value: {:?}", v), + Err(Error::S3Error(e)) => { + assert_eq!(e.code, ErrorCode::InvalidWriteOffset); + } + Err(e) => panic!("append object should have failed; got error: {:?}", e), + } +} + +/// Append beyond the size of an existing object (error InvalidWriteOffset) +#[tokio::test(flavor = "multi_thread", worker_threads = 10)] +async fn append_object_3() { + let ctx = TestContext::new_from_env(); + if !ctx.client.is_minio_express() { + println!("Skipping test because it is NOT running in MinIO Express mode"); + return; + } + + let (bucket_name, _cleanup) = ctx.create_bucket_helper().await; + let object_name = rand_object_name(); + + let content1 = "aaaa"; + let content2 = "bbbb"; + let size = content1.len() as u64; + + create_object_helper(content1, &bucket_name, &object_name, &ctx).await; + + // now try to append "bbbb" to the object at an invalid offset + let data2: SegmentedBytes = SegmentedBytes::from(content2.to_string()); + let offset_bytes = size + 1; + let resp: Result = ctx + .client + .append_object(&bucket_name, &object_name, data2, offset_bytes) + .send() + .await; + + match resp { + Ok(v) => panic!("append object should have failed; got value: {:?}", v), + Err(Error::S3Error(e)) => { + assert_eq!(e.code, ErrorCode::InvalidWriteOffset); + } + Err(e) => panic!("append object should have failed; got error: {:?}", e), + } +} + +/// Append to the beginning/end of a non-existing object (happy flow) +#[tokio::test(flavor = "multi_thread", worker_threads = 10)] +async fn append_object_4() { + let ctx = TestContext::new_from_env(); + if !ctx.client.is_minio_express() { + println!("Skipping test because it is NOT running in MinIO Express mode"); + return; + } + + let (bucket_name, _cleanup) = ctx.create_bucket_helper().await; + let object_name = rand_object_name(); + + let content1 = "aaaa"; + let size = content1.len() as u64; + let data1: SegmentedBytes = SegmentedBytes::from(content1.to_string()); + + // now append "bbbb" to the beginning of the object + let offset_bytes = 0; // byte 0, thus the beginning of the file + let resp: AppendObjectResponse = ctx + .client + .append_object(&bucket_name, &object_name, data1, offset_bytes) + .send() + .await + .unwrap(); + assert_eq!(resp.bucket, bucket_name); + assert_eq!(resp.object, object_name); + assert_eq!(resp.object_size, size); + + let resp: GetObjectResponse = ctx + .client + .get_object(&bucket_name, &object_name) + .send() + .await + .unwrap(); + + // retrieve the content of the object and check that it is "aaaa" + let content: String = String::from_utf8( + resp.content + .to_segmented_bytes() + .await + .unwrap() + .to_bytes() + .to_vec(), + ) + .unwrap(); + assert_eq!(content, content1); + assert_eq!(resp.bucket, bucket_name); + assert_eq!(resp.object, object_name); + assert_eq!(resp.object_size, size); +} + +/// Append beyond the size of a non-existing object (error NoSuchKey) +#[tokio::test(flavor = "multi_thread", worker_threads = 10)] +async fn append_object_5() { + let ctx = TestContext::new_from_env(); + if !ctx.client.is_minio_express() { + println!("Skipping test because it is NOT running in MinIO Express mode"); + return; + } + + let (bucket_name, _cleanup) = ctx.create_bucket_helper().await; + let object_name = rand_object_name(); + + let content1 = "aaaa"; + let data1: SegmentedBytes = SegmentedBytes::from(content1.to_string()); + + let offset_bytes = 1; // byte 1, thus beyond the current length of the (non-existing) file + let resp: Result = ctx + .client + .append_object(&bucket_name, &object_name, data1, offset_bytes) + .send() + .await; + + match resp { + Ok(v) => panic!("append object should have failed; got value: {:?}", v), + Err(Error::S3Error(e)) => { + assert_eq!(e.code, ErrorCode::NoSuchKey); + } + Err(e) => panic!("append object should have failed; got error: {:?}", e), + } +} + #[tokio::test(flavor = "multi_thread", worker_threads = 10)] async fn append_object_content_0() { let ctx = TestContext::new_from_env(); @@ -119,39 +344,9 @@ async fn append_object_content_0() { let content1 = "aaaaa"; let content2 = "bbbbb"; - let size = content1.len() as u64; - let data1: SegmentedBytes = SegmentedBytes::from(content1.to_string()); - //let data2: SegmentedBytes = SegmentedBytes::from(content2.to_string()); - let resp: PutObjectResponse = ctx - .client - .put_object(&bucket_name, &object_name, data1) - .send() - .await - .unwrap(); - assert_eq!(resp.bucket, bucket_name); - assert_eq!(resp.object, object_name); - - let resp: GetObjectResponse = ctx - .client - .get_object(&bucket_name, &object_name) - .send() - .await - .unwrap(); - assert_eq!(resp.bucket, bucket_name); - assert_eq!(resp.object, object_name); - - let content: String = String::from_utf8( - resp.content - .to_segmented_bytes() - .await - .unwrap() - .to_bytes() - .to_vec(), - ) - .unwrap(); - assert_eq!(content, content1); + create_object_helper(content1, &bucket_name, &object_name, &ctx).await; let resp: AppendObjectResponse = ctx .client