diff --git a/moto/s3/responses.py b/moto/s3/responses.py index d34efc69..1a43884e 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -1750,8 +1750,15 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): upload_id = query["uploadId"][0] key = self.backend.complete_multipart(bucket_name, upload_id, body) template = self.response_template(S3_MULTIPART_COMPLETE_RESPONSE) - return template.render( - bucket_name=bucket_name, key_name=key.name, etag=key.etag + headers = {} + if key.version_id: + headers["x-amz-version-id"] = key.version_id + return ( + 200, + headers, + template.render( + bucket_name=bucket_name, key_name=key.name, etag=key.etag + ), ) elif "restore" in query: es = minidom.parseString(body).getElementsByTagName("Days") diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index d2215d8c..630baf1c 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -255,6 +255,23 @@ def test_multipart_etag(): bucket.get_key("the-key").etag.should.equal(EXPECTED_ETAG) +@mock_s3_deprecated +@reduced_min_part_size +def test_multipart_version(): + # Create Bucket so that test can run + conn = boto.connect_s3("the_key", "the_secret") + bucket = conn.create_bucket("mybucket") + bucket.configure_versioning(versioning=True) + multipart = bucket.initiate_multipart_upload("the-key") + part1 = b"0" * REDUCED_PART_SIZE + multipart.upload_part_from_file(BytesIO(part1), 1) + # last part, can be less than 5 MB + part2 = b"1" + multipart.upload_part_from_file(BytesIO(part2), 2) + resp = multipart.complete_upload() + resp.version_id.should_not.be.none + + @mock_s3_deprecated @reduced_min_part_size def test_multipart_invalid_order(): @@ -2569,6 +2586,54 @@ def test_boto3_multipart_etag(): resp["ETag"].should.equal(EXPECTED_ETAG) +@mock_s3 +@reduced_min_part_size +def test_boto3_multipart_version(): + # Create Bucket so that test can run + s3 = boto3.client("s3", region_name=DEFAULT_REGION_NAME) + s3.create_bucket(Bucket="mybucket") + + s3.put_bucket_versioning( + Bucket="mybucket", VersioningConfiguration={"Status": "Enabled"} + ) + + upload_id = s3.create_multipart_upload(Bucket="mybucket", Key="the-key")["UploadId"] + part1 = b"0" * REDUCED_PART_SIZE + etags = [] + etags.append( + s3.upload_part( + Bucket="mybucket", + Key="the-key", + PartNumber=1, + UploadId=upload_id, + Body=part1, + )["ETag"] + ) + # last part, can be less than 5 MB + part2 = b"1" + etags.append( + s3.upload_part( + Bucket="mybucket", + Key="the-key", + PartNumber=2, + UploadId=upload_id, + Body=part2, + )["ETag"] + ) + response = s3.complete_multipart_upload( + Bucket="mybucket", + Key="the-key", + UploadId=upload_id, + MultipartUpload={ + "Parts": [ + {"ETag": etag, "PartNumber": i} for i, etag in enumerate(etags, 1) + ] + }, + ) + + response["VersionId"].should.should_not.be.none + + @mock_s3 @reduced_min_part_size def test_boto3_multipart_part_size():