aws-sdk-s3 1.196.0 → 1.196.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52142f91568fe72f865055a683d9cf9f4a86c0d6446f4c7b2ab491a5b37f2cd9
4
- data.tar.gz: 06e34c3d319a400fe08d08b8aed7f49813f4cb99e6129f4eee6a2b8fdbc452a9
3
+ metadata.gz: 72291dc4a45c81045393df33c2dc26b215e8b67d50f33e097d7685da2a02ca47
4
+ data.tar.gz: eeb0aacd605389c7511cb63684362c12351c68648fa32f7d888e00ea7b0a7d9d
5
5
  SHA512:
6
- metadata.gz: a73af036ed37f5ed060d993d681e747ec554176bb09d5f4bbb77201b60280c23f47a69e5757cc9fb8f33f3e0bf971bb0fd065376a93f897990d053a157385a97
7
- data.tar.gz: d0aac2333813cd782ec4f1d0699288fcc4ed720583883a17ae77f35ccd725b5f3471d931acc2c820928859665ee13c8c8efde428cb4e638840bdb4446bb2161c
6
+ metadata.gz: 20dcb199634a44435dee8acb5d775dc9ed7d50b60c10091425416b692783e5de48ae91df2a4525c0543391584e08338a37c8c9b43dfebe60ffb609a99c535910
7
+ data.tar.gz: 2c4fbba3c97ae00405a8a2ea973145194126f027cf070229a3d80cc47900b2d8e3a1106f69f510ef4c765f090e3d56806f9b5a470449caa3ca898527377b9601
data/CHANGELOG.md CHANGED
@@ -1,6 +1,15 @@
1
1
  Unreleased Changes
2
2
  ------------------
3
3
 
4
+ 1.196.1 (2025-08-05)
5
+ ------------------
6
+
7
+ * Issue - Add range validation to multipart download to ensure all parts are successfully processed.
8
+
9
+ * Issue - When multipart uploader fails to complete multipart upload, it calls abort multipart upload.
10
+
11
+ * Issue - Clean up partially downloaded file on multipart `download_file` failure while preserving existing file.
12
+
4
13
  1.196.0 (2025-08-04)
5
14
  ------------------
6
15
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.196.0
1
+ 1.196.1
@@ -21735,7 +21735,7 @@ module Aws::S3
21735
21735
  tracer: tracer
21736
21736
  )
21737
21737
  context[:gem_name] = 'aws-sdk-s3'
21738
- context[:gem_version] = '1.196.0'
21738
+ context[:gem_version] = '1.196.1'
21739
21739
  Seahorse::Client::Request.new(handlers, context)
21740
21740
  end
21741
21741
 
@@ -404,8 +404,7 @@ module Aws
404
404
  # # small files are uploaded in a single API call
405
405
  # obj.upload_file('/path/to/file')
406
406
  #
407
- # Files larger than or equal to `:multipart_threshold` are uploaded
408
- # using the Amazon S3 multipart upload APIs.
407
+ # Files larger than or equal to `:multipart_threshold` are uploaded using the Amazon S3 multipart upload APIs.
409
408
  #
410
409
  # # large files are automatically split into parts
411
410
  # # and the parts are uploaded in parallel
@@ -421,47 +420,37 @@ module Aws
421
420
  # You can provide a callback to monitor progress of the upload:
422
421
  #
423
422
  # # bytes and totals are each an array with 1 entry per part
424
- # progress = Proc.new do |bytes, totals|
425
- # puts bytes.map.with_index { |b, i| "Part #{i+1}: #{b} / #{totals[i]}"}.join(' ') + "Total: #{100.0 * bytes.sum / totals.sum }%" }
423
+ # progress = proc do |bytes, totals|
424
+ # puts bytes.map.with_index { |b, i| "Part #{i+1}: #{b} / #{totals[i]}"}.join(' ') + "Total: #{100.0 * bytes.sum / totals.sum }%"
426
425
  # end
427
426
  # obj.upload_file('/path/to/file', progress_callback: progress)
428
427
  #
429
- # @param [String, Pathname, File, Tempfile] source A file on the local
430
- # file system that will be uploaded as this object. This can either be
431
- # a String or Pathname to the file, an open File object, or an open
432
- # Tempfile object. If you pass an open File or Tempfile object, then
433
- # you are responsible for closing it after the upload completes. When
434
- # using an open Tempfile, rewind it before uploading or else the object
428
+ # @param [String, Pathname, File, Tempfile] source A file on the local file system that will be uploaded as
429
+ # this object. This can either be a String or Pathname to the file, an open File object, or an open
430
+ # Tempfile object. If you pass an open File or Tempfile object, then you are responsible for closing it
431
+ # after the upload completes. When using an open Tempfile, rewind it before uploading or else the object
435
432
  # will be empty.
436
433
  #
437
434
  # @param [Hash] options
438
- # Additional options for {Client#put_object}
439
- # when file sizes below the multipart threshold. For files larger than
440
- # the multipart threshold, options for {Client#create_multipart_upload},
441
- # {Client#complete_multipart_upload},
442
- # and {Client#upload_part} can be provided.
435
+ # Additional options for {Client#put_object} when file sizes below the multipart threshold.
436
+ # For files larger than the multipart threshold, options for {Client#create_multipart_upload},
437
+ # {Client#complete_multipart_upload}, and {Client#upload_part} can be provided.
443
438
  #
444
- # @option options [Integer] :multipart_threshold (104857600) Files larger
445
- # than or equal to `:multipart_threshold` are uploaded using the S3
446
- # multipart APIs.
447
- # Default threshold is 100MB.
439
+ # @option options [Integer] :multipart_threshold (104857600) Files larger han or equal to
440
+ # `:multipart_threshold` are uploaded using the S3 multipart APIs. Default threshold is 100MB.
448
441
  #
449
- # @option options [Integer] :thread_count (10) The number of parallel
450
- # multipart uploads. This option is not used if the file is smaller than
451
- # `:multipart_threshold`.
442
+ # @option options [Integer] :thread_count (10) The number of parallel multipart uploads.
443
+ # This option is not used if the file is smaller than `:multipart_threshold`.
452
444
  #
453
445
  # @option options [Proc] :progress_callback
454
446
  # A Proc that will be called when each chunk of the upload is sent.
455
447
  # It will be invoked with [bytes_read], [total_sizes]
456
448
  #
457
- # @raise [MultipartUploadError] If an object is being uploaded in
458
- # parts, and the upload can not be completed, then the upload is
459
- # aborted and this error is raised. The raised error has a `#errors`
460
- # method that returns the failures that caused the upload to be
461
- # aborted.
449
+ # @raise [MultipartUploadError] If an object is being uploaded in parts, and the upload can not be completed,
450
+ # then the upload is aborted and this error is raised. The raised error has a `#errors` method that
451
+ # returns the failures that caused the upload to be aborted.
462
452
  #
463
- # @return [Boolean] Returns `true` when the object is uploaded
464
- # without any errors.
453
+ # @return [Boolean] Returns `true` when the object is uploaded without any errors.
465
454
  #
466
455
  # @see Client#put_object
467
456
  # @see Client#create_multipart_upload
@@ -469,15 +458,9 @@ module Aws
469
458
  # @see Client#upload_part
470
459
  def upload_file(source, options = {})
471
460
  uploading_options = options.dup
472
- uploader = FileUploader.new(
473
- multipart_threshold: uploading_options.delete(:multipart_threshold),
474
- client: client
475
- )
461
+ uploader = FileUploader.new(multipart_threshold: uploading_options.delete(:multipart_threshold), client: client)
476
462
  response = Aws::Plugins::UserAgent.metric('RESOURCE_MODEL') do
477
- uploader.upload(
478
- source,
479
- uploading_options.merge(bucket: bucket_name, key: key)
480
- )
463
+ uploader.upload(source, uploading_options.merge(bucket: bucket_name, key: key))
481
464
  end
482
465
  yield response if block_given?
483
466
  true
@@ -488,7 +471,7 @@ module Aws
488
471
  # # small files (< 5MB) are downloaded in a single API call
489
472
  # obj.download_file('/path/to/file')
490
473
  #
491
- # Files larger than 5MB are downloaded using multipart method
474
+ # Files larger than 5MB are downloaded using multipart method:
492
475
  #
493
476
  # # large files are split into parts
494
477
  # # and the parts are downloaded in parallel
@@ -498,64 +481,56 @@ module Aws
498
481
  #
499
482
  # # bytes and part_sizes are each an array with 1 entry per part
500
483
  # # part_sizes may not be known until the first bytes are retrieved
501
- # progress = Proc.new do |bytes, part_sizes, file_size|
502
- # puts bytes.map.with_index { |b, i| "Part #{i+1}: #{b} / #{part_sizes[i]}"}.join(' ') + "Total: #{100.0 * bytes.sum / file_size}%" }
484
+ # progress = proc do |bytes, part_sizes, file_size|
485
+ # puts bytes.map.with_index { |b, i| "Part #{i + 1}: #{b} / #{part_sizes[i]}" }.join(' ') + "Total: #{100.0 * bytes.sum / file_size}%"
503
486
  # end
504
487
  # obj.download_file('/path/to/file', progress_callback: progress)
505
488
  #
506
489
  # @param [String] destination Where to download the file to.
507
490
  #
508
491
  # @param [Hash] options
509
- # Additional options for {Client#get_object} and #{Client#head_object}
510
- # may be provided.
492
+ # Additional options for {Client#get_object} and #{Client#head_object} may be provided.
511
493
  #
512
- # @option options [String] mode `auto`, `single_request`, `get_range`
513
- # `single_request` mode forces only 1 GET request is made in download,
514
- # `get_range` mode allows `chunk_size` parameter to configured in
515
- # customizing each range size in multipart_download,
516
- # By default, `auto` mode is enabled, which performs multipart_download
494
+ # @option options [String] :mode ("auto") `"auto"`, `"single_request"` or `"get_range"`
517
495
  #
518
- # @option options [Integer] chunk_size required in get_range mode.
496
+ # * `auto` mode is enabled by default, which performs `multipart_download`
497
+ # * `"single_request`" mode forces only 1 GET request is made in download
498
+ # * `"get_range"` mode requires `:chunk_size` parameter to configured in customizing each range size
519
499
  #
520
- # @option options [Integer] thread_count (10) Customize threads used in
521
- # the multipart download.
500
+ # @option options [Integer] :chunk_size required in `"get_range"` mode.
522
501
  #
523
- # @option options [String] version_id The object version id used to
524
- # retrieve the object. For more about object versioning, see:
525
- # https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectVersioning.html
502
+ # @option options [Integer] :thread_count (10) Customize threads used in the multipart download.
526
503
  #
527
- # @option options [String] checksum_mode (ENABLED) When `ENABLED` and
528
- # the object has a stored checksum, it will be used to validate the
529
- # download and will raise an `Aws::Errors::ChecksumError` if
530
- # checksum validation fails. You may provide a `on_checksum_validated`
531
- # callback if you need to verify that validation occurred and which
532
- # algorithm was used. To disable checksum validation, set
533
- # `checksum_mode` to "DISABLED".
504
+ # @option options [String] :version_id The object version id used to retrieve the object.
534
505
  #
535
- # @option options [Callable] on_checksum_validated Called each time a
536
- # request's checksum is validated with the checksum algorithm and the
537
- # response. For multipart downloads, this will be called for each
538
- # part that is downloaded and validated.
506
+ # @see https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectVersioning.html ObjectVersioning
507
+ #
508
+ # @option options [String] :checksum_mode ("ENABLED")
509
+ # When `"ENABLED"` and the object has a stored checksum, it will be used to validate the download and will
510
+ # raise an `Aws::Errors::ChecksumError` if checksum validation fails. You may provide a `on_checksum_validated`
511
+ # callback if you need to verify that validation occurred and which algorithm was used.
512
+ # To disable checksum validation, set `checksum_mode` to `"DISABLED"`.
513
+ #
514
+ # @option options [Callable] :on_checksum_validated
515
+ # Called each time a request's checksum is validated with the checksum algorithm and the
516
+ # response. For multipart downloads, this will be called for each part that is downloaded and validated.
539
517
  #
540
518
  # @option options [Proc] :progress_callback
541
- # A Proc that will be called when each chunk of the download is received.
542
- # It will be invoked with [bytes_read], [part_sizes], file_size.
543
- # When the object is downloaded as parts (rather than by ranges), the
544
- # part_sizes will not be known ahead of time and will be nil in the
545
- # callback until the first bytes in the part are received.
519
+ # A Proc that will be called when each chunk of the download is received. It will be invoked with
520
+ # `bytes_read`, `part_sizes`, `file_size`. When the object is downloaded as parts (rather than by ranges),
521
+ # the `part_sizes` will not be known ahead of time and will be `nil` in the callback until the first bytes
522
+ # in the part are received.
546
523
  #
547
- # @return [Boolean] Returns `true` when the file is downloaded without
548
- # any errors.
524
+ # @raise [MultipartDownloadError] Raised when an object validation fails outside of service errors.
525
+ #
526
+ # @return [Boolean] Returns `true` when the file is downloaded without any errors.
549
527
  #
550
528
  # @see Client#get_object
551
529
  # @see Client#head_object
552
530
  def download_file(destination, options = {})
553
531
  downloader = FileDownloader.new(client: client)
554
532
  Aws::Plugins::UserAgent.metric('RESOURCE_MODEL') do
555
- downloader.download(
556
- destination,
557
- options.merge(bucket: bucket_name, key: key)
558
- )
533
+ downloader.download(destination, options.merge(bucket: bucket_name, key: key))
559
534
  end
560
535
  true
561
536
  end
@@ -10,6 +10,7 @@ module Aws
10
10
  autoload :FileUploader, 'aws-sdk-s3/file_uploader'
11
11
  autoload :FileDownloader, 'aws-sdk-s3/file_downloader'
12
12
  autoload :LegacySigner, 'aws-sdk-s3/legacy_signer'
13
+ autoload :MultipartDownloadError, 'aws-sdk-s3/multipart_download_error'
13
14
  autoload :MultipartFileUploader, 'aws-sdk-s3/multipart_file_uploader'
14
15
  autoload :MultipartStreamUploader, 'aws-sdk-s3/multipart_stream_uploader'
15
16
  autoload :MultipartUploadError, 'aws-sdk-s3/multipart_upload_error'
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'pathname'
4
- require 'thread'
4
+ require 'securerandom'
5
5
  require 'set'
6
- require 'tmpdir'
7
6
 
8
7
  module Aws
9
8
  module S3
@@ -12,7 +11,6 @@ module Aws
12
11
 
13
12
  MIN_CHUNK_SIZE = 5 * 1024 * 1024
14
13
  MAX_PARTS = 10_000
15
- THREAD_COUNT = 10
16
14
 
17
15
  def initialize(options = {})
18
16
  @client = options[:client] || Client.new
@@ -23,17 +21,12 @@ module Aws
23
21
 
24
22
  def download(destination, options = {})
25
23
  @path = destination
26
- @mode = options[:mode] || 'auto'
27
- @thread_count = options[:thread_count] || THREAD_COUNT
28
- @chunk_size = options[:chunk_size]
29
- @params = {
30
- bucket: options[:bucket],
31
- key: options[:key]
32
- }
33
- @params[:version_id] = options[:version_id] if options[:version_id]
34
- @on_checksum_validated = options[:on_checksum_validated]
35
- @progress_callback = options[:progress_callback]
36
-
24
+ @mode = options.delete(:mode) || 'auto'
25
+ @thread_count = options.delete(:thread_count) || 10
26
+ @chunk_size = options.delete(:chunk_size)
27
+ @on_checksum_validated = options.delete(:on_checksum_validated)
28
+ @progress_callback = options.delete(:progress_callback)
29
+ @params = options
37
30
  validate!
38
31
 
39
32
  Aws::Plugins::UserAgent.metric('S3_TRANSFER') do
@@ -41,32 +34,31 @@ module Aws
41
34
  when 'auto' then multipart_download
42
35
  when 'single_request' then single_request
43
36
  when 'get_range'
44
- if @chunk_size
45
- resp = @client.head_object(@params)
46
- multithreaded_get_by_ranges(resp.content_length, resp.etag)
47
- else
48
- msg = 'In :get_range mode, :chunk_size must be provided'
49
- raise ArgumentError, msg
50
- end
37
+ raise ArgumentError, 'In get_range mode, :chunk_size must be provided' unless @chunk_size
38
+
39
+ resp = @client.head_object(@params)
40
+ multithreaded_get_by_ranges(resp.content_length, resp.etag)
51
41
  else
52
- msg = "Invalid mode #{@mode} provided, "\
53
- 'mode should be :single_request, :get_range or :auto'
54
- raise ArgumentError, msg
42
+ raise ArgumentError, "Invalid mode #{@mode} provided, :mode should be single_request, get_range or auto"
55
43
  end
56
44
  end
45
+ File.rename(@temp_path, @path) if @temp_path
46
+ ensure
47
+ File.delete(@temp_path) if @temp_path && File.exist?(@temp_path)
57
48
  end
58
49
 
59
50
  private
60
51
 
61
52
  def validate!
62
- if @on_checksum_validated && !@on_checksum_validated.respond_to?(:call)
63
- raise ArgumentError, 'on_checksum_validated must be callable'
64
- end
53
+ return unless @on_checksum_validated && !@on_checksum_validated.respond_to?(:call)
54
+
55
+ raise ArgumentError, ':on_checksum_validated must be callable'
65
56
  end
66
57
 
67
58
  def multipart_download
68
59
  resp = @client.head_object(@params.merge(part_number: 1))
69
60
  count = resp.parts_count
61
+
70
62
  if count.nil? || count <= 1
71
63
  if resp.content_length <= MIN_CHUNK_SIZE
72
64
  single_request
@@ -74,8 +66,8 @@ module Aws
74
66
  multithreaded_get_by_ranges(resp.content_length, resp.etag)
75
67
  end
76
68
  else
77
- # partNumber is an option
78
- resp = @client.head_object(@params)
69
+ # covers cases when given object is not uploaded via UploadPart API
70
+ resp = @client.head_object(@params) # partNumber is an option
79
71
  if resp.content_length <= MIN_CHUNK_SIZE
80
72
  single_request
81
73
  else
@@ -86,7 +78,7 @@ module Aws
86
78
 
87
79
  def compute_mode(file_size, count, etag)
88
80
  chunk_size = compute_chunk(file_size)
89
- part_size = (file_size.to_f / count.to_f).ceil
81
+ part_size = (file_size.to_f / count).ceil
90
82
  if chunk_size < part_size
91
83
  multithreaded_get_by_ranges(file_size, etag)
92
84
  else
@@ -94,32 +86,10 @@ module Aws
94
86
  end
95
87
  end
96
88
 
97
- def construct_chunks(file_size)
98
- offset = 0
99
- default_chunk_size = compute_chunk(file_size)
100
- chunks = []
101
- while offset < file_size
102
- progress = offset + default_chunk_size
103
- progress = file_size if progress > file_size
104
- chunks << "bytes=#{offset}-#{progress - 1}"
105
- offset = progress
106
- end
107
- chunks
108
- end
109
-
110
89
  def compute_chunk(file_size)
111
- if @chunk_size && @chunk_size > file_size
112
- raise ArgumentError, ":chunk_size shouldn't exceed total file size."
113
- else
114
- @chunk_size || [
115
- (file_size.to_f / MAX_PARTS).ceil, MIN_CHUNK_SIZE
116
- ].max.to_i
117
- end
118
- end
90
+ raise ArgumentError, ":chunk_size shouldn't exceed total file size." if @chunk_size && @chunk_size > file_size
119
91
 
120
- def batches(chunks, mode)
121
- chunks = (1..chunks) if mode.eql? 'part_number'
122
- chunks.each_slice(@thread_count).to_a
92
+ @chunk_size || [(file_size.to_f / MAX_PARTS).ceil, MIN_CHUNK_SIZE].max.to_i
123
93
  end
124
94
 
125
95
  def multithreaded_get_by_ranges(file_size, etag)
@@ -130,12 +100,8 @@ module Aws
130
100
  while offset < file_size
131
101
  progress = offset + default_chunk_size
132
102
  progress = file_size if progress > file_size
133
- range = "bytes=#{offset}-#{progress - 1}"
134
- chunks << Part.new(
135
- part_number: part_number,
136
- size: (progress-offset),
137
- params: @params.merge(range: range, if_match: etag)
138
- )
103
+ params = @params.merge(range: "bytes=#{offset}-#{progress - 1}", if_match: etag)
104
+ chunks << Part.new(part_number: part_number, size: (progress - offset), params: params)
139
105
  part_number += 1
140
106
  offset = progress
141
107
  end
@@ -152,10 +118,11 @@ module Aws
152
118
  def download_in_threads(pending, total_size)
153
119
  threads = []
154
120
  progress = MultipartProgress.new(pending, total_size, @progress_callback) if @progress_callback
121
+ @temp_path = "#{@path}.s3tmp.#{SecureRandom.alphanumeric(8)}"
155
122
  @thread_count.times do
156
123
  thread = Thread.new do
157
124
  begin
158
- while part = pending.shift
125
+ while (part = pending.shift)
159
126
  if progress
160
127
  part.params[:on_chunk_received] =
161
128
  proc do |_chunk, bytes, total|
@@ -163,16 +130,17 @@ module Aws
163
130
  end
164
131
  end
165
132
  resp = @client.get_object(part.params)
166
- write(resp)
133
+ range = extract_range(resp.content_range)
134
+ validate_range(range, part.params[:range]) if part.params[:range]
135
+ write(resp.body, range)
167
136
  if @on_checksum_validated && resp.checksum_validated
168
137
  @on_checksum_validated.call(resp.checksum_validated, resp)
169
138
  end
170
139
  end
171
140
  nil
172
- rescue => error
173
- # keep other threads from downloading other parts
174
- pending.clear!
175
- raise error
141
+ rescue StandardError => e
142
+ pending.clear! # keep other threads from downloading other parts
143
+ raise e
176
144
  end
177
145
  end
178
146
  threads << thread
@@ -180,21 +148,27 @@ module Aws
180
148
  threads.map(&:value).compact
181
149
  end
182
150
 
183
- def write(resp)
184
- range, _ = resp.content_range.split(' ').last.split('/')
185
- head, _ = range.split('-').map {|s| s.to_i}
186
- File.write(@path, resp.body.read, head)
151
+ def extract_range(value)
152
+ value.match(%r{bytes (?<range>\d+-\d+)/\d+})[:range]
153
+ end
154
+
155
+ def validate_range(actual, expected)
156
+ return if actual == expected.match(/bytes=(?<range>\d+-\d+)/)[:range]
157
+
158
+ raise MultipartDownloadError, "multipart download failed: expected range of #{expected} but got #{actual}"
159
+ end
160
+
161
+ def write(body, range)
162
+ File.write(@temp_path, body.read, range.split('-').first.to_i)
187
163
  end
188
164
 
189
165
  def single_request
190
166
  params = @params.merge(response_target: @path)
191
167
  params[:on_chunk_received] = single_part_progress if @progress_callback
192
168
  resp = @client.get_object(params)
193
-
194
169
  return resp unless @on_checksum_validated
195
170
 
196
171
  @on_checksum_validated.call(resp.checksum_validated, resp) if resp.checksum_validated
197
-
198
172
  resp
199
173
  end
200
174
 
@@ -204,6 +178,7 @@ module Aws
204
178
  end
205
179
  end
206
180
 
181
+ # @api private
207
182
  class Part < Struct.new(:part_number, :size, :params)
208
183
  include Aws::Structure
209
184
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module S3
5
+ # Raised when multipart download fails to complete.
6
+ class MultipartDownloadError < StandardError; end
7
+ end
8
+ end
@@ -10,23 +10,15 @@ module Aws
10
10
 
11
11
  MIN_PART_SIZE = 5 * 1024 * 1024 # 5MB
12
12
 
13
- FILE_TOO_SMALL = "unable to multipart upload files smaller than 5MB"
14
-
15
13
  MAX_PARTS = 10_000
16
14
 
17
15
  THREAD_COUNT = 10
18
16
 
19
- CREATE_OPTIONS = Set.new(
20
- Client.api.operation(:create_multipart_upload).input.shape.member_names
21
- )
17
+ CREATE_OPTIONS = Set.new(Client.api.operation(:create_multipart_upload).input.shape.member_names)
22
18
 
23
- COMPLETE_OPTIONS = Set.new(
24
- Client.api.operation(:complete_multipart_upload).input.shape.member_names
25
- )
19
+ COMPLETE_OPTIONS = Set.new(Client.api.operation(:complete_multipart_upload).input.shape.member_names)
26
20
 
27
- UPLOAD_PART_OPTIONS = Set.new(
28
- Client.api.operation(:upload_part).input.shape.member_names
29
- )
21
+ UPLOAD_PART_OPTIONS = Set.new(Client.api.operation(:upload_part).input.shape.member_names)
30
22
 
31
23
  CHECKSUM_KEYS = Set.new(
32
24
  Client.api.operation(:upload_part).input.shape.members.map do |n, s|
@@ -52,13 +44,11 @@ module Aws
52
44
  # It will be invoked with [bytes_read], [total_sizes]
53
45
  # @return [Seahorse::Client::Response] - the CompleteMultipartUploadResponse
54
46
  def upload(source, options = {})
55
- if File.size(source) < MIN_PART_SIZE
56
- raise ArgumentError, FILE_TOO_SMALL
57
- else
58
- upload_id = initiate_upload(options)
59
- parts = upload_parts(upload_id, source, options)
60
- complete_upload(upload_id, parts, options)
61
- end
47
+ raise ArgumentError, 'unable to multipart upload files smaller than 5MB' if File.size(source) < MIN_PART_SIZE
48
+
49
+ upload_id = initiate_upload(options)
50
+ parts = upload_parts(upload_id, source, options)
51
+ complete_upload(upload_id, parts, source, options)
62
52
  end
63
53
 
64
54
  private
@@ -67,18 +57,21 @@ module Aws
67
57
  @client.create_multipart_upload(create_opts(options)).upload_id
68
58
  end
69
59
 
70
- def complete_upload(upload_id, parts, options)
60
+ def complete_upload(upload_id, parts, source, options)
71
61
  @client.complete_multipart_upload(
72
62
  **complete_opts(options).merge(
73
63
  upload_id: upload_id,
74
- multipart_upload: { parts: parts }
64
+ multipart_upload: { parts: parts },
65
+ mpu_object_size: File.size(source)
75
66
  )
76
67
  )
68
+ rescue StandardError => e
69
+ abort_upload(upload_id, options, [e])
77
70
  end
78
71
 
79
72
  def upload_parts(upload_id, source, options)
80
- pending = PartList.new(compute_parts(upload_id, source, options))
81
73
  completed = PartList.new
74
+ pending = PartList.new(compute_parts(upload_id, source, options))
82
75
  errors = upload_in_threads(pending, completed, options)
83
76
  if errors.empty?
84
77
  completed.to_a.sort_by { |part| part[:part_number] }
@@ -88,19 +81,15 @@ module Aws
88
81
  end
89
82
 
90
83
  def abort_upload(upload_id, options, errors)
91
- @client.abort_multipart_upload(
92
- bucket: options[:bucket],
93
- key: options[:key],
94
- upload_id: upload_id
95
- )
84
+ @client.abort_multipart_upload(bucket: options[:bucket], key: options[:key], upload_id: upload_id)
96
85
  msg = "multipart upload failed: #{errors.map(&:message).join('; ')}"
97
86
  raise MultipartUploadError.new(msg, errors)
98
- rescue MultipartUploadError => error
99
- raise error
100
- rescue => error
101
- msg = "failed to abort multipart upload: #{error.message}. "\
87
+ rescue MultipartUploadError => e
88
+ raise e
89
+ rescue StandardError => e
90
+ msg = "failed to abort multipart upload: #{e.message}. "\
102
91
  "Multipart upload failed: #{errors.map(&:message).join('; ')}"
103
- raise MultipartUploadError.new(msg, errors + [error])
92
+ raise MultipartUploadError.new(msg, errors + [e])
104
93
  end
105
94
 
106
95
  def compute_parts(upload_id, source, options)
@@ -113,11 +102,7 @@ module Aws
113
102
  parts << upload_part_opts(options).merge(
114
103
  upload_id: upload_id,
115
104
  part_number: part_number,
116
- body: FilePart.new(
117
- source: source,
118
- offset: offset,
119
- size: part_size(size, default_part_size, offset)
120
- )
105
+ body: FilePart.new(source: source, offset: offset, size: part_size(size, default_part_size, offset))
121
106
  )
122
107
  part_number += 1
123
108
  offset += default_part_size
@@ -136,28 +121,23 @@ module Aws
136
121
  def create_opts(options)
137
122
  opts = { checksum_algorithm: Aws::Plugins::ChecksumAlgorithm::DEFAULT_CHECKSUM }
138
123
  opts[:checksum_type] = 'FULL_OBJECT' if has_checksum_key?(options.keys)
139
- CREATE_OPTIONS.inject(opts) do |hash, key|
124
+ CREATE_OPTIONS.each_with_object(opts) do |key, hash|
140
125
  hash[key] = options[key] if options.key?(key)
141
- hash
142
126
  end
143
127
  end
144
128
 
145
129
  def complete_opts(options)
146
130
  opts = {}
147
131
  opts[:checksum_type] = 'FULL_OBJECT' if has_checksum_key?(options.keys)
148
- COMPLETE_OPTIONS.inject(opts) do |hash, key|
132
+ COMPLETE_OPTIONS.each_with_object(opts) do |key, hash|
149
133
  hash[key] = options[key] if options.key?(key)
150
- hash
151
134
  end
152
135
  end
153
136
 
154
137
  def upload_part_opts(options)
155
- UPLOAD_PART_OPTIONS.inject({}) do |hash, key|
156
- if options.key?(key)
157
- # don't pass through checksum calculations
158
- hash[key] = options[key] unless checksum_key?(key)
159
- end
160
- hash
138
+ UPLOAD_PART_OPTIONS.each_with_object({}) do |key, hash|
139
+ # don't pass through checksum calculations
140
+ hash[key] = options[key] if options.key?(key) && !checksum_key?(key)
161
141
  end
162
142
  end
163
143
 
@@ -169,7 +149,7 @@ module Aws
169
149
  options.fetch(:thread_count, @thread_count).times do
170
150
  thread = Thread.new do
171
151
  begin
172
- while part = pending.shift
152
+ while (part = pending.shift)
173
153
  if progress
174
154
  part[:on_chunk_sent] =
175
155
  proc do |_chunk, bytes, _total|
@@ -178,20 +158,17 @@ module Aws
178
158
  end
179
159
  resp = @client.upload_part(part)
180
160
  part[:body].close
181
- completed_part = {
182
- etag: resp.etag,
183
- part_number: part[:part_number]
184
- }
161
+ completed_part = { etag: resp.etag, part_number: part[:part_number] }
185
162
  algorithm = resp.context.params[:checksum_algorithm]
186
163
  k = "checksum_#{algorithm.downcase}".to_sym
187
164
  completed_part[k] = resp.send(k)
188
165
  completed.push(completed_part)
189
166
  end
190
167
  nil
191
- rescue => error
168
+ rescue StandardError => e
192
169
  # keep other threads from uploading other parts
193
170
  pending.clear!
194
- error
171
+ e
195
172
  end
196
173
  end
197
174
  threads << thread
@@ -213,7 +190,6 @@ module Aws
213
190
 
214
191
  # @api private
215
192
  class PartList
216
-
217
193
  def initialize(parts = [])
218
194
  @parts = parts
219
195
  @mutex = Mutex.new
@@ -242,7 +218,6 @@ module Aws
242
218
  def to_a
243
219
  @mutex.synchronize { @parts.dup }
244
220
  end
245
-
246
221
  end
247
222
 
248
223
  # @api private
@@ -261,4 +236,4 @@ module Aws
261
236
  end
262
237
  end
263
238
  end
264
- end
239
+ end
@@ -2,17 +2,16 @@
2
2
 
3
3
  module Aws
4
4
  module S3
5
+ # Raise when multipart upload fails to complete.
5
6
  class MultipartUploadError < StandardError
6
7
 
7
- def initialize(message, errors)
8
+ def initialize(message, errors = [])
8
9
  @errors = errors
9
10
  super(message)
10
11
  end
11
12
 
12
- # @return [Array<StandardError>] The list of errors encountered
13
- # when uploading or aborting the upload.
13
+ # @return [Array<StandardError>] The list of errors encountered when uploading or aborting the upload.
14
14
  attr_reader :errors
15
-
16
15
  end
17
16
  end
18
17
  end
data/lib/aws-sdk-s3.rb CHANGED
@@ -75,7 +75,7 @@ module Aws::S3
75
75
  autoload :ObjectVersion, 'aws-sdk-s3/object_version'
76
76
  autoload :EventStreams, 'aws-sdk-s3/event_streams'
77
77
 
78
- GEM_VERSION = '1.196.0'
78
+ GEM_VERSION = '1.196.1'
79
79
 
80
80
  end
81
81
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-sdk-s3
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.196.0
4
+ version: 1.196.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amazon Web Services
@@ -134,6 +134,7 @@ files:
134
134
  - lib/aws-sdk-s3/file_part.rb
135
135
  - lib/aws-sdk-s3/file_uploader.rb
136
136
  - lib/aws-sdk-s3/legacy_signer.rb
137
+ - lib/aws-sdk-s3/multipart_download_error.rb
137
138
  - lib/aws-sdk-s3/multipart_file_uploader.rb
138
139
  - lib/aws-sdk-s3/multipart_stream_uploader.rb
139
140
  - lib/aws-sdk-s3/multipart_upload.rb