You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

724 lines
29 KiB

4 years ago
  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. import math
  14. from botocore.compat import six
  15. from s3transfer.compat import seekable, readable
  16. from s3transfer.futures import IN_MEMORY_UPLOAD_TAG
  17. from s3transfer.tasks import Task
  18. from s3transfer.tasks import SubmissionTask
  19. from s3transfer.tasks import CreateMultipartUploadTask
  20. from s3transfer.tasks import CompleteMultipartUploadTask
  21. from s3transfer.utils import get_callbacks
  22. from s3transfer.utils import get_filtered_dict
  23. from s3transfer.utils import DeferredOpenFile, ChunksizeAdjuster
  24. class AggregatedProgressCallback(object):
  25. def __init__(self, callbacks, threshold=1024 * 256):
  26. """Aggregates progress updates for every provided progress callback
  27. :type callbacks: A list of functions that accepts bytes_transferred
  28. as a single argument
  29. :param callbacks: The callbacks to invoke when threshold is reached
  30. :type threshold: int
  31. :param threshold: The progress threshold in which to take the
  32. aggregated progress and invoke the progress callback with that
  33. aggregated progress total
  34. """
  35. self._callbacks = callbacks
  36. self._threshold = threshold
  37. self._bytes_seen = 0
  38. def __call__(self, bytes_transferred):
  39. self._bytes_seen += bytes_transferred
  40. if self._bytes_seen >= self._threshold:
  41. self._trigger_callbacks()
  42. def flush(self):
  43. """Flushes out any progress that has not been sent to its callbacks"""
  44. if self._bytes_seen > 0:
  45. self._trigger_callbacks()
  46. def _trigger_callbacks(self):
  47. for callback in self._callbacks:
  48. callback(bytes_transferred=self._bytes_seen)
  49. self._bytes_seen = 0
  50. class InterruptReader(object):
  51. """Wrapper that can interrupt reading using an error
  52. It uses a transfer coordinator to propogate an error if it notices
  53. that a read is being made while the file is being read from.
  54. :type fileobj: file-like obj
  55. :param fileobj: The file-like object to read from
  56. :type transfer_coordinator: s3transfer.futures.TransferCoordinator
  57. :param transfer_coordinator: The transfer coordinator to use if the
  58. reader needs to be interrupted.
  59. """
  60. def __init__(self, fileobj, transfer_coordinator):
  61. self._fileobj = fileobj
  62. self._transfer_coordinator = transfer_coordinator
  63. def read(self, amount=None):
  64. # If there is an exception, then raise the exception.
  65. # We raise an error instead of returning no bytes because for
  66. # requests where the content length and md5 was sent, it will
  67. # cause md5 mismatches and retries as there was no indication that
  68. # the stream being read from encountered any issues.
  69. if self._transfer_coordinator.exception:
  70. raise self._transfer_coordinator.exception
  71. return self._fileobj.read(amount)
  72. def seek(self, where):
  73. self._fileobj.seek(where)
  74. def tell(self):
  75. return self._fileobj.tell()
  76. def close(self):
  77. self._fileobj.close()
  78. def __enter__(self):
  79. return self
  80. def __exit__(self, *args, **kwargs):
  81. self.close()
  82. class UploadInputManager(object):
  83. """Base manager class for handling various types of files for uploads
  84. This class is typically used for the UploadSubmissionTask class to help
  85. determine the following:
  86. * How to determine the size of the file
  87. * How to determine if a multipart upload is required
  88. * How to retrieve the body for a PutObject
  89. * How to retrieve the bodies for a set of UploadParts
  90. The answers/implementations differ for the various types of file inputs
  91. that may be accepted. All implementations must subclass and override
  92. public methods from this class.
  93. """
  94. def __init__(self, osutil, transfer_coordinator, bandwidth_limiter=None):
  95. self._osutil = osutil
  96. self._transfer_coordinator = transfer_coordinator
  97. self._bandwidth_limiter = bandwidth_limiter
  98. @classmethod
  99. def is_compatible(cls, upload_source):
  100. """Determines if the source for the upload is compatible with manager
  101. :param upload_source: The source for which the upload will pull data
  102. from.
  103. :returns: True if the manager can handle the type of source specified
  104. otherwise returns False.
  105. """
  106. raise NotImplementedError('must implement _is_compatible()')
  107. def stores_body_in_memory(self, operation_name):
  108. """Whether the body it provides are stored in-memory
  109. :type operation_name: str
  110. :param operation_name: The name of the client operation that the body
  111. is being used for. Valid operation_names are ``put_object`` and
  112. ``upload_part``.
  113. :rtype: boolean
  114. :returns: True if the body returned by the manager will be stored in
  115. memory. False if the manager will not directly store the body in
  116. memory.
  117. """
  118. raise NotImplemented('must implement store_body_in_memory()')
  119. def provide_transfer_size(self, transfer_future):
  120. """Provides the transfer size of an upload
  121. :type transfer_future: s3transfer.futures.TransferFuture
  122. :param transfer_future: The future associated with upload request
  123. """
  124. raise NotImplementedError('must implement provide_transfer_size()')
  125. def requires_multipart_upload(self, transfer_future, config):
  126. """Determines where a multipart upload is required
  127. :type transfer_future: s3transfer.futures.TransferFuture
  128. :param transfer_future: The future associated with upload request
  129. :type config: s3transfer.manager.TransferConfig
  130. :param config: The config associated to the transfer manager
  131. :rtype: boolean
  132. :returns: True, if the upload should be multipart based on
  133. configuartion and size. False, otherwise.
  134. """
  135. raise NotImplementedError('must implement requires_multipart_upload()')
  136. def get_put_object_body(self, transfer_future):
  137. """Returns the body to use for PutObject
  138. :type transfer_future: s3transfer.futures.TransferFuture
  139. :param transfer_future: The future associated with upload request
  140. :type config: s3transfer.manager.TransferConfig
  141. :param config: The config associated to the transfer manager
  142. :rtype: s3transfer.utils.ReadFileChunk
  143. :returns: A ReadFileChunk including all progress callbacks
  144. associated with the transfer future.
  145. """
  146. raise NotImplementedError('must implement get_put_object_body()')
  147. def yield_upload_part_bodies(self, transfer_future, chunksize):
  148. """Yields the part number and body to use for each UploadPart
  149. :type transfer_future: s3transfer.futures.TransferFuture
  150. :param transfer_future: The future associated with upload request
  151. :type chunksize: int
  152. :param chunksize: The chunksize to use for this upload.
  153. :rtype: int, s3transfer.utils.ReadFileChunk
  154. :returns: Yields the part number and the ReadFileChunk including all
  155. progress callbacks associated with the transfer future for that
  156. specific yielded part.
  157. """
  158. raise NotImplementedError('must implement yield_upload_part_bodies()')
  159. def _wrap_fileobj(self, fileobj):
  160. fileobj = InterruptReader(fileobj, self._transfer_coordinator)
  161. if self._bandwidth_limiter:
  162. fileobj = self._bandwidth_limiter.get_bandwith_limited_stream(
  163. fileobj, self._transfer_coordinator, enabled=False)
  164. return fileobj
  165. def _get_progress_callbacks(self, transfer_future):
  166. callbacks = get_callbacks(transfer_future, 'progress')
  167. # We only want to be wrapping the callbacks if there are callbacks to
  168. # invoke because we do not want to be doing any unnecessary work if
  169. # there are no callbacks to invoke.
  170. if callbacks:
  171. return [AggregatedProgressCallback(callbacks)]
  172. return []
  173. def _get_close_callbacks(self, aggregated_progress_callbacks):
  174. return [callback.flush for callback in aggregated_progress_callbacks]
  175. class UploadFilenameInputManager(UploadInputManager):
  176. """Upload utility for filenames"""
  177. @classmethod
  178. def is_compatible(cls, upload_source):
  179. return isinstance(upload_source, six.string_types)
  180. def stores_body_in_memory(self, operation_name):
  181. return False
  182. def provide_transfer_size(self, transfer_future):
  183. transfer_future.meta.provide_transfer_size(
  184. self._osutil.get_file_size(
  185. transfer_future.meta.call_args.fileobj))
  186. def requires_multipart_upload(self, transfer_future, config):
  187. return transfer_future.meta.size >= config.multipart_threshold
  188. def get_put_object_body(self, transfer_future):
  189. # Get a file-like object for the given input
  190. fileobj, full_size = self._get_put_object_fileobj_with_full_size(
  191. transfer_future)
  192. # Wrap fileobj with interrupt reader that will quickly cancel
  193. # uploads if needed instead of having to wait for the socket
  194. # to completely read all of the data.
  195. fileobj = self._wrap_fileobj(fileobj)
  196. callbacks = self._get_progress_callbacks(transfer_future)
  197. close_callbacks = self._get_close_callbacks(callbacks)
  198. size = transfer_future.meta.size
  199. # Return the file-like object wrapped into a ReadFileChunk to get
  200. # progress.
  201. return self._osutil.open_file_chunk_reader_from_fileobj(
  202. fileobj=fileobj, chunk_size=size, full_file_size=full_size,
  203. callbacks=callbacks, close_callbacks=close_callbacks)
  204. def yield_upload_part_bodies(self, transfer_future, chunksize):
  205. full_file_size = transfer_future.meta.size
  206. num_parts = self._get_num_parts(transfer_future, chunksize)
  207. for part_number in range(1, num_parts + 1):
  208. callbacks = self._get_progress_callbacks(transfer_future)
  209. close_callbacks = self._get_close_callbacks(callbacks)
  210. start_byte = chunksize * (part_number - 1)
  211. # Get a file-like object for that part and the size of the full
  212. # file size for the associated file-like object for that part.
  213. fileobj, full_size = self._get_upload_part_fileobj_with_full_size(
  214. transfer_future.meta.call_args.fileobj, start_byte=start_byte,
  215. part_size=chunksize, full_file_size=full_file_size)
  216. # Wrap fileobj with interrupt reader that will quickly cancel
  217. # uploads if needed instead of having to wait for the socket
  218. # to completely read all of the data.
  219. fileobj = self._wrap_fileobj(fileobj)
  220. # Wrap the file-like object into a ReadFileChunk to get progress.
  221. read_file_chunk = self._osutil.open_file_chunk_reader_from_fileobj(
  222. fileobj=fileobj, chunk_size=chunksize,
  223. full_file_size=full_size, callbacks=callbacks,
  224. close_callbacks=close_callbacks)
  225. yield part_number, read_file_chunk
  226. def _get_deferred_open_file(self, fileobj, start_byte):
  227. fileobj = DeferredOpenFile(
  228. fileobj, start_byte, open_function=self._osutil.open)
  229. return fileobj
  230. def _get_put_object_fileobj_with_full_size(self, transfer_future):
  231. fileobj = transfer_future.meta.call_args.fileobj
  232. size = transfer_future.meta.size
  233. return self._get_deferred_open_file(fileobj, 0), size
  234. def _get_upload_part_fileobj_with_full_size(self, fileobj, **kwargs):
  235. start_byte = kwargs['start_byte']
  236. full_size = kwargs['full_file_size']
  237. return self._get_deferred_open_file(fileobj, start_byte), full_size
  238. def _get_num_parts(self, transfer_future, part_size):
  239. return int(
  240. math.ceil(transfer_future.meta.size / float(part_size)))
  241. class UploadSeekableInputManager(UploadFilenameInputManager):
  242. """Upload utility for an open file object"""
  243. @classmethod
  244. def is_compatible(cls, upload_source):
  245. return readable(upload_source) and seekable(upload_source)
  246. def stores_body_in_memory(self, operation_name):
  247. if operation_name == 'put_object':
  248. return False
  249. else:
  250. return True
  251. def provide_transfer_size(self, transfer_future):
  252. fileobj = transfer_future.meta.call_args.fileobj
  253. # To determine size, first determine the starting position
  254. # Seek to the end and then find the difference in the length
  255. # between the end and start positions.
  256. start_position = fileobj.tell()
  257. fileobj.seek(0, 2)
  258. end_position = fileobj.tell()
  259. fileobj.seek(start_position)
  260. transfer_future.meta.provide_transfer_size(
  261. end_position - start_position)
  262. def _get_upload_part_fileobj_with_full_size(self, fileobj, **kwargs):
  263. # Note: It is unfortunate that in order to do a multithreaded
  264. # multipart upload we cannot simply copy the filelike object
  265. # since there is not really a mechanism in python (i.e. os.dup
  266. # points to the same OS filehandle which causes concurrency
  267. # issues). So instead we need to read from the fileobj and
  268. # chunk the data out to seperate file-like objects in memory.
  269. data = fileobj.read(kwargs['part_size'])
  270. # We return the length of the data instead of the full_file_size
  271. # because we partitioned the data into seperate BytesIO objects
  272. # meaning the BytesIO object has no knowledge of its start position
  273. # relative the input source nor access to the rest of the input
  274. # source. So we must treat it as its own standalone file.
  275. return six.BytesIO(data), len(data)
  276. def _get_put_object_fileobj_with_full_size(self, transfer_future):
  277. fileobj = transfer_future.meta.call_args.fileobj
  278. # The current position needs to be taken into account when retrieving
  279. # the full size of the file.
  280. size = fileobj.tell() + transfer_future.meta.size
  281. return fileobj, size
  282. class UploadNonSeekableInputManager(UploadInputManager):
  283. """Upload utility for a file-like object that cannot seek."""
  284. def __init__(self, osutil, transfer_coordinator, bandwidth_limiter=None):
  285. super(UploadNonSeekableInputManager, self).__init__(
  286. osutil, transfer_coordinator, bandwidth_limiter)
  287. self._initial_data = b''
  288. @classmethod
  289. def is_compatible(cls, upload_source):
  290. return readable(upload_source)
  291. def stores_body_in_memory(self, operation_name):
  292. return True
  293. def provide_transfer_size(self, transfer_future):
  294. # No-op because there is no way to do this short of reading the entire
  295. # body into memory.
  296. return
  297. def requires_multipart_upload(self, transfer_future, config):
  298. # If the user has set the size, we can use that.
  299. if transfer_future.meta.size is not None:
  300. return transfer_future.meta.size >= config.multipart_threshold
  301. # This is tricky to determine in this case because we can't know how
  302. # large the input is. So to figure it out, we read data into memory
  303. # up until the threshold and compare how much data was actually read
  304. # against the threshold.
  305. fileobj = transfer_future.meta.call_args.fileobj
  306. threshold = config.multipart_threshold
  307. self._initial_data = self._read(fileobj, threshold, False)
  308. if len(self._initial_data) < threshold:
  309. return False
  310. else:
  311. return True
  312. def get_put_object_body(self, transfer_future):
  313. callbacks = self._get_progress_callbacks(transfer_future)
  314. close_callbacks = self._get_close_callbacks(callbacks)
  315. fileobj = transfer_future.meta.call_args.fileobj
  316. body = self._wrap_data(
  317. self._initial_data + fileobj.read(), callbacks, close_callbacks)
  318. # Zero out the stored data so we don't have additional copies
  319. # hanging around in memory.
  320. self._initial_data = None
  321. return body
  322. def yield_upload_part_bodies(self, transfer_future, chunksize):
  323. file_object = transfer_future.meta.call_args.fileobj
  324. part_number = 0
  325. # Continue reading parts from the file-like object until it is empty.
  326. while True:
  327. callbacks = self._get_progress_callbacks(transfer_future)
  328. close_callbacks = self._get_close_callbacks(callbacks)
  329. part_number += 1
  330. part_content = self._read(file_object, chunksize)
  331. if not part_content:
  332. break
  333. part_object = self._wrap_data(
  334. part_content, callbacks, close_callbacks)
  335. # Zero out part_content to avoid hanging on to additional data.
  336. part_content = None
  337. yield part_number, part_object
  338. def _read(self, fileobj, amount, truncate=True):
  339. """
  340. Reads a specific amount of data from a stream and returns it. If there
  341. is any data in initial_data, that will be popped out first.
  342. :type fileobj: A file-like object that implements read
  343. :param fileobj: The stream to read from.
  344. :type amount: int
  345. :param amount: The number of bytes to read from the stream.
  346. :type truncate: bool
  347. :param truncate: Whether or not to truncate initial_data after
  348. reading from it.
  349. :return: Generator which generates part bodies from the initial data.
  350. """
  351. # If the the initial data is empty, we simply read from the fileobj
  352. if len(self._initial_data) == 0:
  353. return fileobj.read(amount)
  354. # If the requested number of bytes is less thant the amount of
  355. # initial data, pull entirely from initial data.
  356. if amount <= len(self._initial_data):
  357. data = self._initial_data[:amount]
  358. # Truncate initial data so we don't hang onto the data longer
  359. # than we need.
  360. if truncate:
  361. self._initial_data = self._initial_data[amount:]
  362. return data
  363. # At this point there is some initial data left, but not enough to
  364. # satisfy the number of bytes requested. Pull out the remaining
  365. # initial data and read the rest from the fileobj.
  366. amount_to_read = amount - len(self._initial_data)
  367. data = self._initial_data + fileobj.read(amount_to_read)
  368. # Zero out initial data so we don't hang onto the data any more.
  369. if truncate:
  370. self._initial_data = b''
  371. return data
  372. def _wrap_data(self, data, callbacks, close_callbacks):
  373. """
  374. Wraps data with the interrupt reader and the file chunk reader.
  375. :type data: bytes
  376. :param data: The data to wrap.
  377. :type callbacks: list
  378. :param callbacks: The callbacks associated with the transfer future.
  379. :type close_callbacks: list
  380. :param close_callbacks: The callbacks to be called when closing the
  381. wrapper for the data.
  382. :return: Fully wrapped data.
  383. """
  384. fileobj = self._wrap_fileobj(six.BytesIO(data))
  385. return self._osutil.open_file_chunk_reader_from_fileobj(
  386. fileobj=fileobj, chunk_size=len(data), full_file_size=len(data),
  387. callbacks=callbacks, close_callbacks=close_callbacks)
  388. class UploadSubmissionTask(SubmissionTask):
  389. """Task for submitting tasks to execute an upload"""
  390. UPLOAD_PART_ARGS = [
  391. 'SSECustomerKey',
  392. 'SSECustomerAlgorithm',
  393. 'SSECustomerKeyMD5',
  394. 'RequestPayer',
  395. ]
  396. COMPLETE_MULTIPART_ARGS = [
  397. 'RequestPayer'
  398. ]
  399. def _get_upload_input_manager_cls(self, transfer_future):
  400. """Retieves a class for managing input for an upload based on file type
  401. :type transfer_future: s3transfer.futures.TransferFuture
  402. :param transfer_future: The transfer future for the request
  403. :rtype: class of UploadInputManager
  404. :returns: The appropriate class to use for managing a specific type of
  405. input for uploads.
  406. """
  407. upload_manager_resolver_chain = [
  408. UploadFilenameInputManager,
  409. UploadSeekableInputManager,
  410. UploadNonSeekableInputManager
  411. ]
  412. fileobj = transfer_future.meta.call_args.fileobj
  413. for upload_manager_cls in upload_manager_resolver_chain:
  414. if upload_manager_cls.is_compatible(fileobj):
  415. return upload_manager_cls
  416. raise RuntimeError(
  417. 'Input %s of type: %s is not supported.' % (
  418. fileobj, type(fileobj)))
  419. def _submit(self, client, config, osutil, request_executor,
  420. transfer_future, bandwidth_limiter=None):
  421. """
  422. :param client: The client associated with the transfer manager
  423. :type config: s3transfer.manager.TransferConfig
  424. :param config: The transfer config associated with the transfer
  425. manager
  426. :type osutil: s3transfer.utils.OSUtil
  427. :param osutil: The os utility associated to the transfer manager
  428. :type request_executor: s3transfer.futures.BoundedExecutor
  429. :param request_executor: The request executor associated with the
  430. transfer manager
  431. :type transfer_future: s3transfer.futures.TransferFuture
  432. :param transfer_future: The transfer future associated with the
  433. transfer request that tasks are being submitted for
  434. """
  435. upload_input_manager = self._get_upload_input_manager_cls(
  436. transfer_future)(
  437. osutil, self._transfer_coordinator, bandwidth_limiter)
  438. # Determine the size if it was not provided
  439. if transfer_future.meta.size is None:
  440. upload_input_manager.provide_transfer_size(transfer_future)
  441. # Do a multipart upload if needed, otherwise do a regular put object.
  442. if not upload_input_manager.requires_multipart_upload(
  443. transfer_future, config):
  444. self._submit_upload_request(
  445. client, config, osutil, request_executor, transfer_future,
  446. upload_input_manager)
  447. else:
  448. self._submit_multipart_request(
  449. client, config, osutil, request_executor, transfer_future,
  450. upload_input_manager)
  451. def _submit_upload_request(self, client, config, osutil, request_executor,
  452. transfer_future, upload_input_manager):
  453. call_args = transfer_future.meta.call_args
  454. # Get any tags that need to be associated to the put object task
  455. put_object_tag = self._get_upload_task_tag(
  456. upload_input_manager, 'put_object')
  457. # Submit the request of a single upload.
  458. self._transfer_coordinator.submit(
  459. request_executor,
  460. PutObjectTask(
  461. transfer_coordinator=self._transfer_coordinator,
  462. main_kwargs={
  463. 'client': client,
  464. 'fileobj': upload_input_manager.get_put_object_body(
  465. transfer_future),
  466. 'bucket': call_args.bucket,
  467. 'key': call_args.key,
  468. 'extra_args': call_args.extra_args
  469. },
  470. is_final=True
  471. ),
  472. tag=put_object_tag
  473. )
  474. def _submit_multipart_request(self, client, config, osutil,
  475. request_executor, transfer_future,
  476. upload_input_manager):
  477. call_args = transfer_future.meta.call_args
  478. # Submit the request to create a multipart upload.
  479. create_multipart_future = self._transfer_coordinator.submit(
  480. request_executor,
  481. CreateMultipartUploadTask(
  482. transfer_coordinator=self._transfer_coordinator,
  483. main_kwargs={
  484. 'client': client,
  485. 'bucket': call_args.bucket,
  486. 'key': call_args.key,
  487. 'extra_args': call_args.extra_args,
  488. }
  489. )
  490. )
  491. # Submit requests to upload the parts of the file.
  492. part_futures = []
  493. extra_part_args = self._extra_upload_part_args(call_args.extra_args)
  494. # Get any tags that need to be associated to the submitted task
  495. # for upload the data
  496. upload_part_tag = self._get_upload_task_tag(
  497. upload_input_manager, 'upload_part')
  498. size = transfer_future.meta.size
  499. adjuster = ChunksizeAdjuster()
  500. chunksize = adjuster.adjust_chunksize(config.multipart_chunksize, size)
  501. part_iterator = upload_input_manager.yield_upload_part_bodies(
  502. transfer_future, chunksize)
  503. for part_number, fileobj in part_iterator:
  504. part_futures.append(
  505. self._transfer_coordinator.submit(
  506. request_executor,
  507. UploadPartTask(
  508. transfer_coordinator=self._transfer_coordinator,
  509. main_kwargs={
  510. 'client': client,
  511. 'fileobj': fileobj,
  512. 'bucket': call_args.bucket,
  513. 'key': call_args.key,
  514. 'part_number': part_number,
  515. 'extra_args': extra_part_args
  516. },
  517. pending_main_kwargs={
  518. 'upload_id': create_multipart_future
  519. }
  520. ),
  521. tag=upload_part_tag
  522. )
  523. )
  524. complete_multipart_extra_args = self._extra_complete_multipart_args(
  525. call_args.extra_args)
  526. # Submit the request to complete the multipart upload.
  527. self._transfer_coordinator.submit(
  528. request_executor,
  529. CompleteMultipartUploadTask(
  530. transfer_coordinator=self._transfer_coordinator,
  531. main_kwargs={
  532. 'client': client,
  533. 'bucket': call_args.bucket,
  534. 'key': call_args.key,
  535. 'extra_args': complete_multipart_extra_args,
  536. },
  537. pending_main_kwargs={
  538. 'upload_id': create_multipart_future,
  539. 'parts': part_futures
  540. },
  541. is_final=True
  542. )
  543. )
  544. def _extra_upload_part_args(self, extra_args):
  545. # Only the args in UPLOAD_PART_ARGS actually need to be passed
  546. # onto the upload_part calls.
  547. return get_filtered_dict(extra_args, self.UPLOAD_PART_ARGS)
  548. def _extra_complete_multipart_args(self, extra_args):
  549. return get_filtered_dict(extra_args, self.COMPLETE_MULTIPART_ARGS)
  550. def _get_upload_task_tag(self, upload_input_manager, operation_name):
  551. tag = None
  552. if upload_input_manager.stores_body_in_memory(operation_name):
  553. tag = IN_MEMORY_UPLOAD_TAG
  554. return tag
  555. class PutObjectTask(Task):
  556. """Task to do a nonmultipart upload"""
  557. def _main(self, client, fileobj, bucket, key, extra_args):
  558. """
  559. :param client: The client to use when calling PutObject
  560. :param fileobj: The file to upload.
  561. :param bucket: The name of the bucket to upload to
  562. :param key: The name of the key to upload to
  563. :param extra_args: A dictionary of any extra arguments that may be
  564. used in the upload.
  565. """
  566. with fileobj as body:
  567. client.put_object(Bucket=bucket, Key=key, Body=body, **extra_args)
  568. class UploadPartTask(Task):
  569. """Task to upload a part in a multipart upload"""
  570. def _main(self, client, fileobj, bucket, key, upload_id, part_number,
  571. extra_args):
  572. """
  573. :param client: The client to use when calling PutObject
  574. :param fileobj: The file to upload.
  575. :param bucket: The name of the bucket to upload to
  576. :param key: The name of the key to upload to
  577. :param upload_id: The id of the upload
  578. :param part_number: The number representing the part of the multipart
  579. upload
  580. :param extra_args: A dictionary of any extra arguments that may be
  581. used in the upload.
  582. :rtype: dict
  583. :returns: A dictionary representing a part::
  584. {'Etag': etag_value, 'PartNumber': part_number}
  585. This value can be appended to a list to be used to complete
  586. the multipart upload.
  587. """
  588. with fileobj as body:
  589. response = client.upload_part(
  590. Bucket=bucket, Key=key,
  591. UploadId=upload_id, PartNumber=part_number,
  592. Body=body, **extra_args)
  593. etag = response['ETag']
  594. return {'ETag': etag, 'PartNumber': part_number}