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 +4 -4
- data/CHANGELOG.md +9 -0
- data/VERSION +1 -1
- data/lib/aws-sdk-s3/client.rb +1 -1
- data/lib/aws-sdk-s3/customizations/object.rb +50 -75
- data/lib/aws-sdk-s3/customizations.rb +1 -0
- data/lib/aws-sdk-s3/file_downloader.rb +47 -72
- data/lib/aws-sdk-s3/multipart_download_error.rb +8 -0
- data/lib/aws-sdk-s3/multipart_file_uploader.rb +31 -56
- data/lib/aws-sdk-s3/multipart_upload_error.rb +3 -4
- data/lib/aws-sdk-s3.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72291dc4a45c81045393df33c2dc26b215e8b67d50f33e097d7685da2a02ca47
|
4
|
+
data.tar.gz: eeb0aacd605389c7511cb63684362c12351c68648fa32f7d888e00ea7b0a7d9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
1
|
+
1.196.1
|
data/lib/aws-sdk-s3/client.rb
CHANGED
@@ -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.
|
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 =
|
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
|
-
#
|
431
|
-
#
|
432
|
-
#
|
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
|
-
#
|
440
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
459
|
-
#
|
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 =
|
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
|
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
|
-
#
|
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]
|
521
|
-
# the multipart download.
|
500
|
+
# @option options [Integer] :chunk_size required in `"get_range"` mode.
|
522
501
|
#
|
523
|
-
# @option options [
|
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]
|
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
|
-
#
|
536
|
-
#
|
537
|
-
#
|
538
|
-
#
|
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
|
-
#
|
543
|
-
#
|
544
|
-
#
|
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
|
-
# @
|
548
|
-
#
|
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 '
|
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
|
27
|
-
@thread_count = options
|
28
|
-
@chunk_size = options
|
29
|
-
@
|
30
|
-
|
31
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
#
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =>
|
173
|
-
# keep other threads from downloading other parts
|
174
|
-
|
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
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
@@ -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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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 =>
|
99
|
-
raise
|
100
|
-
rescue =>
|
101
|
-
msg = "failed to abort multipart upload: #{
|
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 + [
|
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.
|
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.
|
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.
|
156
|
-
|
157
|
-
|
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 =>
|
168
|
+
rescue StandardError => e
|
192
169
|
# keep other threads from uploading other parts
|
193
170
|
pending.clear!
|
194
|
-
|
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
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.
|
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
|