diff --git a/src/api/multipart.rs b/src/api/multipart.rs index 645ef56..0b4ace2 100644 --- a/src/api/multipart.rs +++ b/src/api/multipart.rs @@ -160,6 +160,9 @@ pub async fn complete_multipart_upload( let mut builder = Response::builder() .status(StatusCode::OK) .header("content-type", "application/xml"); + if let Some(vid) = &result.version_id { + builder = builder.header("x-amz-version-id", vid.as_str()); + } if let (Some(algo), Some(val)) = (&result.checksum_algorithm, &result.checksum_value) { builder = builder.header(algo.header_name(), val.as_str()); } diff --git a/tests/integration.rs b/tests/integration.rs index c0a0482..e506b64 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -643,6 +643,16 @@ fn extract_xml_tag(body: &str, tag: &str) -> Option { Some(body[from..to].to_string()) } +async fn enable_bucket_versioning(base_url: &str, bucket: &str) -> reqwest::Response { + let xml = br#"Enabled"#; + s3_request( + "PUT", + &format!("{}/{}?versioning=", base_url, bucket), + xml.to_vec(), + ) + .await +} + // ---- Tests ---- #[tokio::test] @@ -1633,6 +1643,126 @@ async fn test_multipart_complete() { assert_eq!(body.as_ref(), expected.as_slice()); } +#[tokio::test] +async fn test_multipart_complete_in_versioned_bucket_preserves_prior_versions() { + let (base_url, _tmp) = start_server().await; + assert_eq!( + s3_request("PUT", &format!("{}/mybucket", base_url), vec![]) + .await + .status(), + 200 + ); + assert_eq!( + enable_bucket_versioning(&base_url, "mybucket") + .await + .status(), + 200 + ); + + let create_v1 = s3_request( + "POST", + &format!("{}/mybucket/large.bin?uploads=", base_url), + vec![], + ) + .await; + let upload_id_v1 = extract_xml_tag(&create_v1.text().await.unwrap(), "UploadId").unwrap(); + let part_v1 = vec![b'a'; 5 * 1024 * 1024]; + let upload_part_v1 = s3_request( + "PUT", + &format!( + "{}/mybucket/large.bin?partNumber=1&uploadId={}", + base_url, upload_id_v1 + ), + part_v1.clone(), + ) + .await; + let etag_v1 = upload_part_v1 + .headers() + .get("etag") + .unwrap() + .to_str() + .unwrap() + .to_string(); + let complete_v1 = s3_request( + "POST", + &format!("{}/mybucket/large.bin?uploadId={}", base_url, upload_id_v1), + format!( + "1{}", + etag_v1 + ) + .into_bytes(), + ) + .await; + assert_eq!(complete_v1.status(), 200); + let version_id_v1 = complete_v1 + .headers() + .get("x-amz-version-id") + .expect("multipart completion should return version id") + .to_str() + .unwrap() + .to_string(); + + let create_v2 = s3_request( + "POST", + &format!("{}/mybucket/large.bin?uploads=", base_url), + vec![], + ) + .await; + let upload_id_v2 = extract_xml_tag(&create_v2.text().await.unwrap(), "UploadId").unwrap(); + let part_v2 = vec![b'b'; 5 * 1024 * 1024]; + let upload_part_v2 = s3_request( + "PUT", + &format!( + "{}/mybucket/large.bin?partNumber=1&uploadId={}", + base_url, upload_id_v2 + ), + part_v2.clone(), + ) + .await; + let etag_v2 = upload_part_v2 + .headers() + .get("etag") + .unwrap() + .to_str() + .unwrap() + .to_string(); + let complete_v2 = s3_request( + "POST", + &format!("{}/mybucket/large.bin?uploadId={}", base_url, upload_id_v2), + format!( + "1{}", + etag_v2 + ) + .into_bytes(), + ) + .await; + assert_eq!(complete_v2.status(), 200); + let version_id_v2 = complete_v2 + .headers() + .get("x-amz-version-id") + .expect("multipart overwrite should return version id") + .to_str() + .unwrap() + .to_string(); + assert_ne!(version_id_v1, version_id_v2); + + let current = s3_request("GET", &format!("{}/mybucket/large.bin", base_url), vec![]).await; + assert_eq!(current.status(), 200); + assert_eq!(current.bytes().await.unwrap().as_ref(), part_v2.as_slice()); + + let prior = s3_request( + "GET", + &format!( + "{}/mybucket/large.bin?versionId={}", + base_url, version_id_v1 + ), + vec![], + ) + .await; + assert_eq!(prior.status(), 200); + assert_eq!(prior.bytes().await.unwrap().as_ref(), part_v1.as_slice()); +} + #[tokio::test] async fn test_multipart_get_part_number() { let (base_url, _tmp) = start_server().await; @@ -3031,6 +3161,126 @@ async fn test_ec_put_and_get_object() { assert_eq!(&body[..], &data[..]); } +#[tokio::test] +async fn test_ec_multipart_complete_in_versioned_bucket_preserves_prior_versions() { + let (base_url, _tmp) = start_server_ec().await; + assert_eq!( + s3_request("PUT", &format!("{}/mybucket", base_url), vec![]) + .await + .status(), + 200 + ); + assert_eq!( + enable_bucket_versioning(&base_url, "mybucket") + .await + .status(), + 200 + ); + + let create_v1 = s3_request( + "POST", + &format!("{}/mybucket/chunked.bin?uploads=", base_url), + vec![], + ) + .await; + let upload_id_v1 = extract_xml_tag(&create_v1.text().await.unwrap(), "UploadId").unwrap(); + let part_v1 = vec![b'a'; 2048]; + let upload_part_v1 = s3_request( + "PUT", + &format!( + "{}/mybucket/chunked.bin?partNumber=1&uploadId={}", + base_url, upload_id_v1 + ), + part_v1.clone(), + ) + .await; + let etag_v1 = upload_part_v1 + .headers() + .get("etag") + .unwrap() + .to_str() + .unwrap() + .to_string(); + let complete_v1 = s3_request( + "POST", + &format!("{}/mybucket/chunked.bin?uploadId={}", base_url, upload_id_v1), + format!( + "1{}", + etag_v1 + ) + .into_bytes(), + ) + .await; + assert_eq!(complete_v1.status(), 200); + let version_id_v1 = complete_v1 + .headers() + .get("x-amz-version-id") + .expect("erasure-coded multipart completion should return version id") + .to_str() + .unwrap() + .to_string(); + + let create_v2 = s3_request( + "POST", + &format!("{}/mybucket/chunked.bin?uploads=", base_url), + vec![], + ) + .await; + let upload_id_v2 = extract_xml_tag(&create_v2.text().await.unwrap(), "UploadId").unwrap(); + let part_v2 = vec![b'b'; 3072]; + let upload_part_v2 = s3_request( + "PUT", + &format!( + "{}/mybucket/chunked.bin?partNumber=1&uploadId={}", + base_url, upload_id_v2 + ), + part_v2.clone(), + ) + .await; + let etag_v2 = upload_part_v2 + .headers() + .get("etag") + .unwrap() + .to_str() + .unwrap() + .to_string(); + let complete_v2 = s3_request( + "POST", + &format!("{}/mybucket/chunked.bin?uploadId={}", base_url, upload_id_v2), + format!( + "1{}", + etag_v2 + ) + .into_bytes(), + ) + .await; + assert_eq!(complete_v2.status(), 200); + let version_id_v2 = complete_v2 + .headers() + .get("x-amz-version-id") + .expect("erasure-coded multipart overwrite should return version id") + .to_str() + .unwrap() + .to_string(); + assert_ne!(version_id_v1, version_id_v2); + + let current = s3_request("GET", &format!("{}/mybucket/chunked.bin", base_url), vec![]).await; + assert_eq!(current.status(), 200); + assert_eq!(current.bytes().await.unwrap().as_ref(), part_v2.as_slice()); + + let prior = s3_request( + "GET", + &format!( + "{}/mybucket/chunked.bin?versionId={}", + base_url, version_id_v1 + ), + vec![], + ) + .await; + assert_eq!(prior.status(), 200); + assert_eq!(prior.bytes().await.unwrap().as_ref(), part_v1.as_slice()); +} + #[tokio::test] async fn test_ec_small_object() { let (base_url, _tmp) = start_server_ec().await;