Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/api/multipart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
250 changes: 250 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,16 @@ fn extract_xml_tag(body: &str, tag: &str) -> Option<String> {
Some(body[from..to].to_string())
}

async fn enable_bucket_versioning(base_url: &str, bucket: &str) -> reqwest::Response {
let xml = br#"<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>"#;
s3_request(
"PUT",
&format!("{}/{}?versioning=", base_url, bucket),
xml.to_vec(),
)
.await
}

// ---- Tests ----

#[tokio::test]
Expand Down Expand Up @@ -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!(
"<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>{}</ETag></Part></CompleteMultipartUpload>",
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!(
"<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>{}</ETag></Part></CompleteMultipartUpload>",
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;
Expand Down Expand Up @@ -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!(
"<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>{}</ETag></Part></CompleteMultipartUpload>",
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!(
"<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>{}</ETag></Part></CompleteMultipartUpload>",
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;
Expand Down