test_copy.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. # Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"). You
  4. # may not use this file except in compliance with the License. A copy of
  5. # the License is located at
  6. #
  7. # http://aws.amazon.com/apache2.0/
  8. #
  9. # or in the "license" file accompanying this file. This file is
  10. # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
  11. # ANY KIND, either express or implied. See the License for the specific
  12. # language governing permissions and limitations under the License.
  13. from botocore.exceptions import ClientError
  14. from botocore.stub import Stubber
  15. from s3transfer.manager import TransferConfig, TransferManager
  16. from s3transfer.utils import MIN_UPLOAD_CHUNKSIZE
  17. from __tests__ import BaseGeneralInterfaceTest, FileSizeProvider
  18. class BaseCopyTest(BaseGeneralInterfaceTest):
  19. def setUp(self):
  20. super().setUp()
  21. self.config = TransferConfig(
  22. max_request_concurrency=1,
  23. multipart_chunksize=MIN_UPLOAD_CHUNKSIZE,
  24. multipart_threshold=MIN_UPLOAD_CHUNKSIZE * 4,
  25. )
  26. self._manager = TransferManager(self.client, self.config)
  27. # Initialize some default arguments
  28. self.bucket = 'mybucket'
  29. self.key = 'mykey'
  30. self.copy_source = {'Bucket': 'mysourcebucket', 'Key': 'mysourcekey'}
  31. self.extra_args = {}
  32. self.subscribers = []
  33. self.half_chunksize = int(MIN_UPLOAD_CHUNKSIZE / 2)
  34. self.content = b'0' * (2 * MIN_UPLOAD_CHUNKSIZE + self.half_chunksize)
  35. @property
  36. def manager(self):
  37. return self._manager
  38. @property
  39. def method(self):
  40. return self.manager.copy
  41. def create_call_kwargs(self):
  42. return {
  43. 'copy_source': self.copy_source,
  44. 'bucket': self.bucket,
  45. 'key': self.key,
  46. }
  47. def create_invalid_extra_args(self):
  48. return {'Foo': 'bar'}
  49. def create_stubbed_responses(self):
  50. return [
  51. {
  52. 'method': 'head_object',
  53. 'service_response': {'ContentLength': len(self.content)},
  54. },
  55. {'method': 'copy_object', 'service_response': {}},
  56. ]
  57. def create_expected_progress_callback_info(self):
  58. return [
  59. {'bytes_transferred': len(self.content)},
  60. ]
  61. def add_head_object_response(self, expected_params=None, stubber=None):
  62. if not stubber:
  63. stubber = self.stubber
  64. head_response = self.create_stubbed_responses()[0]
  65. if expected_params:
  66. head_response['expected_params'] = expected_params
  67. stubber.add_response(**head_response)
  68. def add_successful_copy_responses(
  69. self,
  70. expected_copy_params=None,
  71. expected_create_mpu_params=None,
  72. expected_complete_mpu_params=None,
  73. ):
  74. # Add all responses needed to do the copy of the object.
  75. # Should account for both ranged and nonranged downloads.
  76. stubbed_responses = self.create_stubbed_responses()[1:]
  77. # If the length of copy responses is greater than one then it is
  78. # a multipart copy.
  79. copy_responses = stubbed_responses[0:1]
  80. if len(stubbed_responses) > 1:
  81. copy_responses = stubbed_responses[1:-1]
  82. # Add the expected create multipart upload params.
  83. if expected_create_mpu_params:
  84. stubbed_responses[0][
  85. 'expected_params'
  86. ] = expected_create_mpu_params
  87. # Add any expected copy parameters.
  88. if expected_copy_params:
  89. for i, copy_response in enumerate(copy_responses):
  90. if isinstance(expected_copy_params, list):
  91. copy_response['expected_params'] = expected_copy_params[i]
  92. else:
  93. copy_response['expected_params'] = expected_copy_params
  94. # Add the expected complete multipart upload params.
  95. if expected_complete_mpu_params:
  96. stubbed_responses[-1][
  97. 'expected_params'
  98. ] = expected_complete_mpu_params
  99. # Add the responses to the stubber.
  100. for stubbed_response in stubbed_responses:
  101. self.stubber.add_response(**stubbed_response)
  102. def test_can_provide_file_size(self):
  103. self.add_successful_copy_responses()
  104. call_kwargs = self.create_call_kwargs()
  105. call_kwargs['subscribers'] = [FileSizeProvider(len(self.content))]
  106. future = self.manager.copy(**call_kwargs)
  107. future.result()
  108. # The HeadObject should have not happened and should have been able
  109. # to successfully copy the file.
  110. self.stubber.assert_no_pending_responses()
  111. def test_provide_copy_source_as_dict(self):
  112. self.copy_source['VersionId'] = 'mysourceversionid'
  113. expected_params = {
  114. 'Bucket': 'mysourcebucket',
  115. 'Key': 'mysourcekey',
  116. 'VersionId': 'mysourceversionid',
  117. }
  118. self.add_head_object_response(expected_params=expected_params)
  119. self.add_successful_copy_responses()
  120. future = self.manager.copy(**self.create_call_kwargs())
  121. future.result()
  122. self.stubber.assert_no_pending_responses()
  123. def test_invalid_copy_source(self):
  124. self.copy_source = ['bucket', 'key']
  125. future = self.manager.copy(**self.create_call_kwargs())
  126. with self.assertRaises(TypeError):
  127. future.result()
  128. def test_provide_copy_source_client(self):
  129. source_client = self.session.create_client(
  130. 's3',
  131. 'eu-central-1',
  132. aws_access_key_id='foo',
  133. aws_secret_access_key='bar',
  134. )
  135. source_stubber = Stubber(source_client)
  136. source_stubber.activate()
  137. self.addCleanup(source_stubber.deactivate)
  138. self.add_head_object_response(stubber=source_stubber)
  139. self.add_successful_copy_responses()
  140. call_kwargs = self.create_call_kwargs()
  141. call_kwargs['source_client'] = source_client
  142. future = self.manager.copy(**call_kwargs)
  143. future.result()
  144. # Make sure that all of the responses were properly
  145. # used for both clients.
  146. source_stubber.assert_no_pending_responses()
  147. self.stubber.assert_no_pending_responses()
  148. class TestNonMultipartCopy(BaseCopyTest):
  149. __test__ = True
  150. def test_copy(self):
  151. expected_head_params = {
  152. 'Bucket': 'mysourcebucket',
  153. 'Key': 'mysourcekey',
  154. }
  155. expected_copy_object = {
  156. 'Bucket': self.bucket,
  157. 'Key': self.key,
  158. 'CopySource': self.copy_source,
  159. }
  160. self.add_head_object_response(expected_params=expected_head_params)
  161. self.add_successful_copy_responses(
  162. expected_copy_params=expected_copy_object
  163. )
  164. future = self.manager.copy(**self.create_call_kwargs())
  165. future.result()
  166. self.stubber.assert_no_pending_responses()
  167. def test_copy_with_checksum(self):
  168. self.extra_args['ChecksumAlgorithm'] = 'crc32'
  169. expected_head_params = {
  170. 'Bucket': 'mysourcebucket',
  171. 'Key': 'mysourcekey',
  172. }
  173. expected_copy_object = {
  174. 'Bucket': self.bucket,
  175. 'Key': self.key,
  176. 'CopySource': self.copy_source,
  177. 'ChecksumAlgorithm': 'crc32',
  178. }
  179. self.add_head_object_response(expected_params=expected_head_params)
  180. self.add_successful_copy_responses(
  181. expected_copy_params=expected_copy_object
  182. )
  183. call_kwargs = self.create_call_kwargs()
  184. call_kwargs['extra_args'] = self.extra_args
  185. future = self.manager.copy(**call_kwargs)
  186. future.result()
  187. self.stubber.assert_no_pending_responses()
  188. def test_copy_with_extra_args(self):
  189. self.extra_args['MetadataDirective'] = 'REPLACE'
  190. expected_head_params = {
  191. 'Bucket': 'mysourcebucket',
  192. 'Key': 'mysourcekey',
  193. }
  194. expected_copy_object = {
  195. 'Bucket': self.bucket,
  196. 'Key': self.key,
  197. 'CopySource': self.copy_source,
  198. 'MetadataDirective': 'REPLACE',
  199. }
  200. self.add_head_object_response(expected_params=expected_head_params)
  201. self.add_successful_copy_responses(
  202. expected_copy_params=expected_copy_object
  203. )
  204. call_kwargs = self.create_call_kwargs()
  205. call_kwargs['extra_args'] = self.extra_args
  206. future = self.manager.copy(**call_kwargs)
  207. future.result()
  208. self.stubber.assert_no_pending_responses()
  209. def test_copy_maps_extra_args_to_head_object(self):
  210. self.extra_args['CopySourceSSECustomerAlgorithm'] = 'AES256'
  211. expected_head_params = {
  212. 'Bucket': 'mysourcebucket',
  213. 'Key': 'mysourcekey',
  214. 'SSECustomerAlgorithm': 'AES256',
  215. }
  216. expected_copy_object = {
  217. 'Bucket': self.bucket,
  218. 'Key': self.key,
  219. 'CopySource': self.copy_source,
  220. 'CopySourceSSECustomerAlgorithm': 'AES256',
  221. }
  222. self.add_head_object_response(expected_params=expected_head_params)
  223. self.add_successful_copy_responses(
  224. expected_copy_params=expected_copy_object
  225. )
  226. call_kwargs = self.create_call_kwargs()
  227. call_kwargs['extra_args'] = self.extra_args
  228. future = self.manager.copy(**call_kwargs)
  229. future.result()
  230. self.stubber.assert_no_pending_responses()
  231. def test_allowed_copy_params_are_valid(self):
  232. op_model = self.client.meta.service_model.operation_model('CopyObject')
  233. for allowed_upload_arg in self._manager.ALLOWED_COPY_ARGS:
  234. self.assertIn(allowed_upload_arg, op_model.input_shape.members)
  235. def test_copy_with_tagging(self):
  236. extra_args = {'Tagging': 'tag1=val1', 'TaggingDirective': 'REPLACE'}
  237. self.add_head_object_response()
  238. self.add_successful_copy_responses(
  239. expected_copy_params={
  240. 'Bucket': self.bucket,
  241. 'Key': self.key,
  242. 'CopySource': self.copy_source,
  243. 'Tagging': 'tag1=val1',
  244. 'TaggingDirective': 'REPLACE',
  245. }
  246. )
  247. future = self.manager.copy(
  248. self.copy_source, self.bucket, self.key, extra_args
  249. )
  250. future.result()
  251. self.stubber.assert_no_pending_responses()
  252. def test_raise_exception_on_s3_object_lambda_resource(self):
  253. s3_object_lambda_arn = (
  254. 'arn:aws:s3-object-lambda:us-west-2:123456789012:'
  255. 'accesspoint:my-accesspoint'
  256. )
  257. with self.assertRaisesRegex(ValueError, 'methods do not support'):
  258. self.manager.copy(self.copy_source, s3_object_lambda_arn, self.key)
  259. def test_raise_exception_on_s3_object_lambda_resource_as_source(self):
  260. source = {
  261. 'Bucket': 'arn:aws:s3-object-lambda:us-west-2:123456789012:'
  262. 'accesspoint:my-accesspoint'
  263. }
  264. with self.assertRaisesRegex(ValueError, 'methods do not support'):
  265. self.manager.copy(source, self.bucket, self.key)
  266. class TestMultipartCopy(BaseCopyTest):
  267. __test__ = True
  268. def setUp(self):
  269. super().setUp()
  270. self.config = TransferConfig(
  271. max_request_concurrency=1,
  272. multipart_threshold=1,
  273. multipart_chunksize=4,
  274. )
  275. self._manager = TransferManager(self.client, self.config)
  276. self.multipart_id = 'my-upload-id'
  277. def create_stubbed_responses(self):
  278. return [
  279. {
  280. 'method': 'head_object',
  281. 'service_response': {'ContentLength': len(self.content)},
  282. },
  283. {
  284. 'method': 'create_multipart_upload',
  285. 'service_response': {'UploadId': self.multipart_id},
  286. },
  287. {
  288. 'method': 'upload_part_copy',
  289. 'service_response': {'CopyPartResult': {'ETag': 'etag-1'}},
  290. },
  291. {
  292. 'method': 'upload_part_copy',
  293. 'service_response': {'CopyPartResult': {'ETag': 'etag-2'}},
  294. },
  295. {
  296. 'method': 'upload_part_copy',
  297. 'service_response': {'CopyPartResult': {'ETag': 'etag-3'}},
  298. },
  299. {'method': 'complete_multipart_upload', 'service_response': {}},
  300. ]
  301. def add_get_head_response_with_default_expected_params(
  302. self, extra_expected_params=None
  303. ):
  304. expected_params = {
  305. 'Bucket': 'mysourcebucket',
  306. 'Key': 'mysourcekey',
  307. }
  308. if extra_expected_params:
  309. expected_params.update(extra_expected_params)
  310. response = self.create_stubbed_responses()[0]
  311. response['expected_params'] = expected_params
  312. self.stubber.add_response(**response)
  313. def add_create_multipart_response_with_default_expected_params(
  314. self, extra_expected_params=None
  315. ):
  316. expected_params = {'Bucket': self.bucket, 'Key': self.key}
  317. if extra_expected_params:
  318. expected_params.update(extra_expected_params)
  319. response = self.create_stubbed_responses()[1]
  320. response['expected_params'] = expected_params
  321. self.stubber.add_response(**response)
  322. def add_upload_part_copy_responses_with_default_expected_params(
  323. self, extra_expected_params=None
  324. ):
  325. ranges = [
  326. 'bytes=0-5242879',
  327. 'bytes=5242880-10485759',
  328. 'bytes=10485760-13107199',
  329. ]
  330. upload_part_responses = self.create_stubbed_responses()[2:-1]
  331. for i, range_val in enumerate(ranges):
  332. upload_part_response = upload_part_responses[i]
  333. expected_params = {
  334. 'Bucket': self.bucket,
  335. 'Key': self.key,
  336. 'CopySource': self.copy_source,
  337. 'UploadId': self.multipart_id,
  338. 'PartNumber': i + 1,
  339. 'CopySourceRange': range_val,
  340. }
  341. if extra_expected_params:
  342. if 'ChecksumAlgorithm' in extra_expected_params:
  343. name = extra_expected_params['ChecksumAlgorithm']
  344. checksum_member = 'Checksum%s' % name.upper()
  345. response = upload_part_response['service_response']
  346. response['CopyPartResult'][checksum_member] = 'sum%s==' % (
  347. i + 1
  348. )
  349. else:
  350. expected_params.update(extra_expected_params)
  351. upload_part_response['expected_params'] = expected_params
  352. self.stubber.add_response(**upload_part_response)
  353. def add_complete_multipart_response_with_default_expected_params(
  354. self, extra_expected_params=None
  355. ):
  356. expected_params = {
  357. 'Bucket': self.bucket,
  358. 'Key': self.key,
  359. 'UploadId': self.multipart_id,
  360. 'MultipartUpload': {
  361. 'Parts': [
  362. {'ETag': 'etag-1', 'PartNumber': 1},
  363. {'ETag': 'etag-2', 'PartNumber': 2},
  364. {'ETag': 'etag-3', 'PartNumber': 3},
  365. ]
  366. },
  367. }
  368. if extra_expected_params:
  369. expected_params.update(extra_expected_params)
  370. response = self.create_stubbed_responses()[-1]
  371. response['expected_params'] = expected_params
  372. self.stubber.add_response(**response)
  373. def create_expected_progress_callback_info(self):
  374. # Note that last read is from the empty sentinel indicating
  375. # that the stream is done.
  376. return [
  377. {'bytes_transferred': MIN_UPLOAD_CHUNKSIZE},
  378. {'bytes_transferred': MIN_UPLOAD_CHUNKSIZE},
  379. {'bytes_transferred': self.half_chunksize},
  380. ]
  381. def add_create_multipart_upload_response(self):
  382. self.stubber.add_response(**self.create_stubbed_responses()[1])
  383. def _get_expected_params(self):
  384. # Add expected parameters to the head object
  385. expected_head_params = {
  386. 'Bucket': 'mysourcebucket',
  387. 'Key': 'mysourcekey',
  388. }
  389. # Add expected parameters for the create multipart
  390. expected_create_mpu_params = {
  391. 'Bucket': self.bucket,
  392. 'Key': self.key,
  393. }
  394. expected_copy_params = []
  395. # Add expected parameters to the copy part
  396. ranges = [
  397. 'bytes=0-5242879',
  398. 'bytes=5242880-10485759',
  399. 'bytes=10485760-13107199',
  400. ]
  401. for i, range_val in enumerate(ranges):
  402. expected_copy_params.append(
  403. {
  404. 'Bucket': self.bucket,
  405. 'Key': self.key,
  406. 'CopySource': self.copy_source,
  407. 'UploadId': self.multipart_id,
  408. 'PartNumber': i + 1,
  409. 'CopySourceRange': range_val,
  410. }
  411. )
  412. # Add expected parameters for the complete multipart
  413. expected_complete_mpu_params = {
  414. 'Bucket': self.bucket,
  415. 'Key': self.key,
  416. 'UploadId': self.multipart_id,
  417. 'MultipartUpload': {
  418. 'Parts': [
  419. {'ETag': 'etag-1', 'PartNumber': 1},
  420. {'ETag': 'etag-2', 'PartNumber': 2},
  421. {'ETag': 'etag-3', 'PartNumber': 3},
  422. ]
  423. },
  424. }
  425. return expected_head_params, {
  426. 'expected_create_mpu_params': expected_create_mpu_params,
  427. 'expected_copy_params': expected_copy_params,
  428. 'expected_complete_mpu_params': expected_complete_mpu_params,
  429. }
  430. def _add_params_to_expected_params(
  431. self, add_copy_kwargs, operation_types, new_params
  432. ):
  433. expected_params_to_update = []
  434. for operation_type in operation_types:
  435. add_copy_kwargs_key = 'expected_' + operation_type + '_params'
  436. expected_params = add_copy_kwargs[add_copy_kwargs_key]
  437. if isinstance(expected_params, list):
  438. expected_params_to_update.extend(expected_params)
  439. else:
  440. expected_params_to_update.append(expected_params)
  441. for expected_params in expected_params_to_update:
  442. expected_params.update(new_params)
  443. def test_copy(self):
  444. head_params, add_copy_kwargs = self._get_expected_params()
  445. self.add_head_object_response(expected_params=head_params)
  446. self.add_successful_copy_responses(**add_copy_kwargs)
  447. future = self.manager.copy(**self.create_call_kwargs())
  448. future.result()
  449. self.stubber.assert_no_pending_responses()
  450. def test_copy_with_extra_args(self):
  451. # This extra argument should be added to the head object,
  452. # the create multipart upload, and upload part copy.
  453. self.extra_args['RequestPayer'] = 'requester'
  454. head_params, add_copy_kwargs = self._get_expected_params()
  455. head_params.update(self.extra_args)
  456. self.add_head_object_response(expected_params=head_params)
  457. self._add_params_to_expected_params(
  458. add_copy_kwargs,
  459. ['create_mpu', 'copy', 'complete_mpu'],
  460. self.extra_args,
  461. )
  462. self.add_successful_copy_responses(**add_copy_kwargs)
  463. call_kwargs = self.create_call_kwargs()
  464. call_kwargs['extra_args'] = self.extra_args
  465. future = self.manager.copy(**call_kwargs)
  466. future.result()
  467. self.stubber.assert_no_pending_responses()
  468. def test_copy_passes_checksums(self):
  469. # This extra argument should be added to the head object,
  470. # the create multipart upload, and upload part copy.
  471. self.extra_args['ChecksumAlgorithm'] = 'sha256'
  472. self.add_get_head_response_with_default_expected_params()
  473. # ChecksumAlgorithm should be passed on the create_multipart call
  474. self.add_create_multipart_response_with_default_expected_params(
  475. self.extra_args,
  476. )
  477. # ChecksumAlgorithm should be passed to the upload_part_copy calls
  478. self.add_upload_part_copy_responses_with_default_expected_params(
  479. self.extra_args,
  480. )
  481. # The checksums should be used in the complete call like etags
  482. self.add_complete_multipart_response_with_default_expected_params(
  483. extra_expected_params={
  484. 'MultipartUpload': {
  485. 'Parts': [
  486. {
  487. 'ETag': 'etag-1',
  488. 'PartNumber': 1,
  489. 'ChecksumSHA256': 'sum1==',
  490. },
  491. {
  492. 'ETag': 'etag-2',
  493. 'PartNumber': 2,
  494. 'ChecksumSHA256': 'sum2==',
  495. },
  496. {
  497. 'ETag': 'etag-3',
  498. 'PartNumber': 3,
  499. 'ChecksumSHA256': 'sum3==',
  500. },
  501. ]
  502. }
  503. }
  504. )
  505. call_kwargs = self.create_call_kwargs()
  506. call_kwargs['extra_args'] = self.extra_args
  507. future = self.manager.copy(**call_kwargs)
  508. future.result()
  509. self.stubber.assert_no_pending_responses()
  510. def test_copy_blacklists_args_to_create_multipart(self):
  511. # This argument can never be used for multipart uploads
  512. self.extra_args['MetadataDirective'] = 'COPY'
  513. head_params, add_copy_kwargs = self._get_expected_params()
  514. self.add_head_object_response(expected_params=head_params)
  515. self.add_successful_copy_responses(**add_copy_kwargs)
  516. call_kwargs = self.create_call_kwargs()
  517. call_kwargs['extra_args'] = self.extra_args
  518. future = self.manager.copy(**call_kwargs)
  519. future.result()
  520. self.stubber.assert_no_pending_responses()
  521. def test_copy_args_to_only_create_multipart(self):
  522. self.extra_args['ACL'] = 'private'
  523. head_params, add_copy_kwargs = self._get_expected_params()
  524. self.add_head_object_response(expected_params=head_params)
  525. self._add_params_to_expected_params(
  526. add_copy_kwargs, ['create_mpu'], self.extra_args
  527. )
  528. self.add_successful_copy_responses(**add_copy_kwargs)
  529. call_kwargs = self.create_call_kwargs()
  530. call_kwargs['extra_args'] = self.extra_args
  531. future = self.manager.copy(**call_kwargs)
  532. future.result()
  533. self.stubber.assert_no_pending_responses()
  534. def test_copy_passes_args_to_create_multipart_and_upload_part(self):
  535. # This will only be used for the complete multipart upload
  536. # and upload part.
  537. self.extra_args['SSECustomerAlgorithm'] = 'AES256'
  538. head_params, add_copy_kwargs = self._get_expected_params()
  539. self.add_head_object_response(expected_params=head_params)
  540. self._add_params_to_expected_params(
  541. add_copy_kwargs,
  542. ['create_mpu', 'copy', 'complete_mpu'],
  543. self.extra_args,
  544. )
  545. self.add_successful_copy_responses(**add_copy_kwargs)
  546. call_kwargs = self.create_call_kwargs()
  547. call_kwargs['extra_args'] = self.extra_args
  548. future = self.manager.copy(**call_kwargs)
  549. future.result()
  550. self.stubber.assert_no_pending_responses()
  551. def test_copy_maps_extra_args_to_head_object(self):
  552. self.extra_args['CopySourceSSECustomerAlgorithm'] = 'AES256'
  553. head_params, add_copy_kwargs = self._get_expected_params()
  554. # The CopySourceSSECustomerAlgorithm needs to get mapped to
  555. # SSECustomerAlgorithm for HeadObject
  556. head_params['SSECustomerAlgorithm'] = 'AES256'
  557. self.add_head_object_response(expected_params=head_params)
  558. # However, it needs to remain the same for UploadPartCopy.
  559. self._add_params_to_expected_params(
  560. add_copy_kwargs, ['copy'], self.extra_args
  561. )
  562. self.add_successful_copy_responses(**add_copy_kwargs)
  563. call_kwargs = self.create_call_kwargs()
  564. call_kwargs['extra_args'] = self.extra_args
  565. future = self.manager.copy(**call_kwargs)
  566. future.result()
  567. self.stubber.assert_no_pending_responses()
  568. def test_abort_on_failure(self):
  569. # First add the head object and create multipart upload
  570. self.add_head_object_response()
  571. self.add_create_multipart_upload_response()
  572. # Cause an error on upload_part_copy
  573. self.stubber.add_client_error('upload_part_copy', 'ArbitraryFailure')
  574. # Add the abort multipart to ensure it gets cleaned up on failure
  575. self.stubber.add_response(
  576. 'abort_multipart_upload',
  577. service_response={},
  578. expected_params={
  579. 'Bucket': self.bucket,
  580. 'Key': self.key,
  581. 'UploadId': self.multipart_id,
  582. },
  583. )
  584. future = self.manager.copy(**self.create_call_kwargs())
  585. with self.assertRaisesRegex(ClientError, 'ArbitraryFailure'):
  586. future.result()
  587. self.stubber.assert_no_pending_responses()
  588. def test_mp_copy_with_tagging_directive(self):
  589. extra_args = {'Tagging': 'tag1=val1', 'TaggingDirective': 'REPLACE'}
  590. self.add_head_object_response()
  591. self.add_successful_copy_responses(
  592. expected_create_mpu_params={
  593. 'Bucket': self.bucket,
  594. 'Key': self.key,
  595. 'Tagging': 'tag1=val1',
  596. }
  597. )
  598. future = self.manager.copy(
  599. self.copy_source, self.bucket, self.key, extra_args
  600. )
  601. future.result()
  602. self.stubber.assert_no_pending_responses()