diff --git a/moto/s3/models.py b/moto/s3/models.py index 431c9c988..30ecf1595 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -175,11 +175,14 @@ class FakeMultipart(BaseModel): count = 0 for pn, etag in body: part = self.parts.get(pn) - if part is None or part.etag != etag: + part_etag = None + if part is not None: + part_etag = part.etag.replace('"', '') + etag = etag.replace('"', '') + if part is None or part_etag != etag: raise InvalidPart() if last is not None and len(last.value) < UPLOAD_PART_MIN_SIZE: raise EntityTooSmall() - part_etag = part.etag.replace('"', '') md5s.extend(decode_hex(part_etag)[0]) total.extend(part.value) last = part diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 9f37791cb..6de79f874 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -225,6 +225,29 @@ def test_multipart_invalid_order(): bucket.complete_multipart_upload.when.called_with( multipart.key_name, multipart.id, xml).should.throw(S3ResponseError) +@mock_s3_deprecated +@reduced_min_part_size +def test_multipart_etag_quotes_stripped(): + # Create Bucket so that test can run + conn = boto.connect_s3('the_key', 'the_secret') + bucket = conn.create_bucket('mybucket') + + multipart = bucket.initiate_multipart_upload("the-key") + part1 = b'0' * REDUCED_PART_SIZE + etag1 = multipart.upload_part_from_file(BytesIO(part1), 1).etag + # last part, can be less than 5 MB + part2 = b'1' + etag2 = multipart.upload_part_from_file(BytesIO(part2), 2).etag + # Strip quotes from etags + etag1 = etag1.replace('"','') + etag2 = etag2.replace('"','') + xml = "{0}{1}" + xml = xml.format(1, etag1) + xml.format(2, etag2) + xml = "{0}".format(xml) + bucket.complete_multipart_upload.when.called_with( + multipart.key_name, multipart.id, xml).should_not.throw(S3ResponseError) + # we should get both parts as the key contents + bucket.get_key("the-key").etag.should.equal(EXPECTED_ETAG) @mock_s3_deprecated @reduced_min_part_size