@@ -16682,6 +16682,130 @@ def test_copy_enc_1mb(source_mode_key, dest_mode_key):
1668216682def test_copy_enc_8mb(source_mode_key, dest_mode_key):
1668316683 _test_copy_enc(8*1024*1024, source_mode_key, dest_mode_key)
1668416684
16685+ def _test_copy_part_enc(file_size, source_mode_key, dest_mode_key, source_sc=None, dest_sc=None):
16686+ source_args = _copy_enc_source_modes[source_mode_key]
16687+ dest_args = _copy_enc_dest_modes[dest_mode_key]
16688+
16689+ bucket_name = get_new_bucket()
16690+ client = get_client()
16691+
16692+ # upload original file with source encryption
16693+ data = 'A'*file_size
16694+ args = {key: value() if callable(value) else value for key, value in source_args.get('args', {}).items()}
16695+ if source_sc:
16696+ args['StorageClass'] = source_sc
16697+ response = client.put_object(Bucket=bucket_name, Key='testobj', Body=data, **args)
16698+ assert source_args.get('assert', lambda r: True)(response)
16699+
16700+ # create a multipart upload with source encryption
16701+ dest_bucket_name = get_new_bucket()
16702+ upload_args = {key: value() if callable(value) else value for key, value in dest_args.get('args', {}).items()}
16703+ if dest_sc:
16704+ upload_args['StorageClass'] = dest_sc
16705+ response = client.create_multipart_upload(Bucket=dest_bucket_name, Key='testobj2', **upload_args)
16706+ assert dest_args.get('assert', lambda r: True)(response)
16707+ upload_id = response['UploadId']
16708+ assert len(upload_id)
16709+
16710+ parts = []
16711+
16712+ # copy the object as the part
16713+ copy_args = {key: value() if callable(value) else value for key, value in source_args.get('source_copy_args', {}).items()}
16714+ if dest_mode_key == 'sse-c':
16715+ copy_args.update(upload_args)
16716+ if dest_sc:
16717+ copy_args.pop('StorageClass', None) # StorageClass is not allowed in copy part
16718+ response = client.upload_part_copy(
16719+ Bucket=dest_bucket_name,
16720+ Key='testobj2',
16721+ PartNumber=1,
16722+ UploadId=upload_id,
16723+ CopySource={'Bucket': bucket_name, 'Key': 'testobj'},
16724+ **copy_args
16725+ )
16726+ assert dest_args.get('assert', lambda r: True)(response)
16727+ parts.append({
16728+ 'ETag': response['CopyPartResult']['ETag'],
16729+ 'PartNumber': 1
16730+ })
16731+
16732+ # add another temporary part to the upload
16733+ complete_args = {}
16734+ if dest_mode_key == 'sse-c':
16735+ complete_args.update(upload_args)
16736+ if dest_sc:
16737+ complete_args.pop('StorageClass', None) # StorageClass is not allowed in complete multipart upload
16738+ temp_part = client.upload_part(
16739+ Bucket=dest_bucket_name,
16740+ Key='testobj2',
16741+ PartNumber=2,
16742+ UploadId=upload_id,
16743+ Body='B'*file_size,
16744+ **complete_args
16745+ )
16746+ assert dest_args.get('assert', lambda r: True)(temp_part)
16747+ parts.append({
16748+ 'ETag': temp_part['ETag'],
16749+ 'PartNumber': 2
16750+ })
16751+
16752+ # complete the multipart upload
16753+ response = client.complete_multipart_upload(
16754+ Bucket=dest_bucket_name,
16755+ Key='testobj2',
16756+ UploadId=upload_id,
16757+ MultipartUpload={'Parts': parts},
16758+ **complete_args
16759+ )
16760+ assert dest_args.get('assert', lambda r: True)(response)
16761+ # verify the copy is encrypted
16762+ get_args = dest_args.get('get_args', {})
16763+ response = client.get_object(Bucket=dest_bucket_name, Key='testobj2', **get_args)
16764+ assert dest_args.get('assert', lambda r: True)(response)
16765+ body = _get_body(response)
16766+ assert body == (data + 'B'*file_size)
16767+
16768+ def generate_copy_part_enc_params():
16769+ configure()
16770+ sc = configured_storage_classes()
16771+
16772+ obj_sizes = [8*1024*1024] # min multipart is 5MB
16773+ params = []
16774+ for source_key in _copy_enc_source_modes.keys():
16775+ for dest_key in _copy_enc_dest_modes.keys():
16776+ source_marks = _copy_enc_source_modes[source_key].get('marks', [])
16777+ dest_marks = _copy_enc_dest_modes[dest_key].get('marks', [])
16778+ for source_sc in sc:
16779+ for dest_sc in sc:
16780+ additional_marks = []
16781+ if source_sc != 'STANDARD' or dest_sc != 'STANDARD':
16782+ # storage classes are not supported on AWS
16783+ additional_marks.append(pytest.mark.fails_on_aws)
16784+ for obj_size in obj_sizes:
16785+ param = pytest.param(
16786+ source_key,
16787+ dest_key,
16788+ source_sc,
16789+ dest_sc,
16790+ obj_size,
16791+ marks=[*source_marks, *dest_marks, *additional_marks]
16792+ )
16793+ params.append(param)
16794+ return params
16795+
16796+ @pytest.mark.encryption
16797+ @pytest.mark.fails_on_dbstore
16798+ @pytest.mark.parametrize(
16799+ "source_mode_key, dest_mode_key, source_storage_class, dest_storage_class, obj_size",
16800+ generate_copy_part_enc_params()
16801+ )
16802+ def test_copy_part_enc(source_mode_key, dest_mode_key, source_storage_class, dest_storage_class, obj_size):
16803+ print(
16804+ f"Testing copy part from {source_mode_key} to {dest_mode_key} with storage class "
16805+ f"{source_storage_class} -> {dest_storage_class} and object size {obj_size}"
16806+ )
16807+ _test_copy_part_enc(obj_size, source_mode_key, dest_mode_key, source_storage_class, dest_storage_class)
16808+
1668516809def generate_copy_enc_storage_class_params():
1668616810 configure()
1668716811 sc = configured_storage_classes()
0 commit comments