Skip to content

Commit

Permalink
S3: complete_multipart_upload() now supports IfNoneMatch-parameter (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
bblommers authored Dec 7, 2024
1 parent 6cf88ca commit 1291c55
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 4 deletions.
18 changes: 15 additions & 3 deletions moto/s3/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2524,11 +2524,23 @@ def _key_response_post(
return 200, response_headers, response

if query.get("uploadId"):
existing = self.backend.get_object(self.bucket_name, key_name)

if_none_match = self.headers.get("If-None-Match")
if if_none_match == "*":
if existing is not None and existing.multipart is None:
raise PreconditionFailed("If-None-Match")

multipart_id = query["uploadId"][0]

key = self.backend.complete_multipart_upload(
bucket_name, multipart_id, self._complete_multipart_body(body)
)
if existing is not None and existing.multipart:
# Based on testing against AWS, operation seems idempotent
# Scenario where both method-calls have a different body hasn't been tested yet
key: Optional[FakeKey] = existing
else:
key = self.backend.complete_multipart_upload(
bucket_name, multipart_id, self._complete_multipart_body(body)
)
if key is None:
return 400, {}, ""

Expand Down
79 changes: 78 additions & 1 deletion tests/test_s3/test_s3_multipart.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
)
from tests import DEFAULT_ACCOUNT_ID

from .test_s3 import add_proxy_details
from .test_s3 import add_proxy_details, s3_aws_verified

if settings.TEST_DECORATOR_MODE:
REDUCED_PART_SIZE = 256
Expand Down Expand Up @@ -158,6 +158,83 @@ def test_multipart_upload(key: str):
assert response["Body"].read() == part1 + part2


@pytest.mark.aws_verified
@s3_aws_verified
def test_duplicate_multipart_upload(bucket_name=None):
client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)

part1 = b"0" * S3_UPLOAD_PART_MIN_SIZE
part2 = b"1"
multipart = client.create_multipart_upload(Bucket=bucket_name, Key="key.txt")
kwargs = {
"Bucket": bucket_name,
"Key": "key.txt",
"UploadId": multipart["UploadId"],
}

up1 = client.upload_part(Body=BytesIO(part1), PartNumber=1, **kwargs)
up2 = client.upload_part(Body=BytesIO(part2), PartNumber=2, **kwargs)

parts = {
"Parts": [
{"ETag": up1["ETag"], "PartNumber": 1},
{"ETag": up2["ETag"], "PartNumber": 2},
]
}

resp1 = client.complete_multipart_upload(MultipartUpload=parts, **kwargs)
assert resp1["ResponseMetadata"]["HTTPStatusCode"] == 200

# We can call this method again
resp2 = client.complete_multipart_upload(
MultipartUpload=parts,
# Even with this parameter supplied
IfNoneMatch="*",
**kwargs,
)
assert resp2["ResponseMetadata"]["HTTPStatusCode"] == 200
assert resp1["ETag"] == resp2["ETag"]


@pytest.mark.aws_verified
@s3_aws_verified
def test_multipart_upload_if_none_match(bucket_name=None):
client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)

client.put_object(Bucket=bucket_name, Key="key.txt")

part1 = b"0" * S3_UPLOAD_PART_MIN_SIZE
part2 = b"1"
multipart = client.create_multipart_upload(Bucket=bucket_name, Key="key.txt")
kwargs = {
"Bucket": bucket_name,
"Key": "key.txt",
"UploadId": multipart["UploadId"],
}

up1 = client.upload_part(Body=BytesIO(part1), PartNumber=1, **kwargs)
up2 = client.upload_part(Body=BytesIO(part2), PartNumber=2, **kwargs)

parts = {
"Parts": [
{"ETag": up1["ETag"], "PartNumber": 1},
{"ETag": up2["ETag"], "PartNumber": 2},
]
}

with pytest.raises(ClientError) as exc:
client.complete_multipart_upload(
MultipartUpload=parts, IfNoneMatch="*", **kwargs
)
err = exc.value.response["Error"]
assert err["Code"] == "PreconditionFailed"
assert (
err["Message"]
== "At least one of the pre-conditions you specified did not hold"
)
assert err["Condition"] == "If-None-Match"


@mock_aws
@reduced_min_part_size
def test_multipart_upload_out_of_order():
Expand Down

0 comments on commit 1291c55

Please sign in to comment.