aws-sdk-s3 1.103.0 → 1.202.0
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 +664 -0
- data/VERSION +1 -1
- data/lib/aws-sdk-s3/access_grants_credentials.rb +57 -0
- data/lib/aws-sdk-s3/access_grants_credentials_provider.rb +250 -0
- data/lib/aws-sdk-s3/bucket.rb +858 -116
- data/lib/aws-sdk-s3/bucket_acl.rb +32 -9
- data/lib/aws-sdk-s3/bucket_cors.rb +38 -13
- data/lib/aws-sdk-s3/bucket_lifecycle.rb +43 -12
- data/lib/aws-sdk-s3/bucket_lifecycle_configuration.rb +100 -13
- data/lib/aws-sdk-s3/bucket_logging.rb +35 -6
- data/lib/aws-sdk-s3/bucket_notification.rb +27 -9
- data/lib/aws-sdk-s3/bucket_policy.rb +79 -10
- data/lib/aws-sdk-s3/bucket_region_cache.rb +9 -5
- data/lib/aws-sdk-s3/bucket_request_payment.rb +29 -7
- data/lib/aws-sdk-s3/bucket_tagging.rb +35 -11
- data/lib/aws-sdk-s3/bucket_versioning.rb +108 -17
- data/lib/aws-sdk-s3/bucket_website.rb +35 -11
- data/lib/aws-sdk-s3/client.rb +11799 -3636
- data/lib/aws-sdk-s3/client_api.rb +1201 -276
- data/lib/aws-sdk-s3/customizations/bucket.rb +23 -47
- data/lib/aws-sdk-s3/customizations/errors.rb +40 -0
- data/lib/aws-sdk-s3/customizations/object.rb +216 -70
- data/lib/aws-sdk-s3/customizations/object_summary.rb +5 -0
- data/lib/aws-sdk-s3/customizations/object_version.rb +13 -0
- data/lib/aws-sdk-s3/customizations/types/permanent_redirect.rb +26 -0
- data/lib/aws-sdk-s3/customizations.rb +27 -29
- data/lib/aws-sdk-s3/default_executor.rb +103 -0
- data/lib/aws-sdk-s3/encryption/client.rb +6 -2
- data/lib/aws-sdk-s3/encryption/kms_cipher_provider.rb +13 -9
- data/lib/aws-sdk-s3/encryptionV2/client.rb +6 -2
- data/lib/aws-sdk-s3/encryptionV2/decrypt_handler.rb +1 -0
- data/lib/aws-sdk-s3/encryptionV2/kms_cipher_provider.rb +10 -6
- data/lib/aws-sdk-s3/endpoint_parameters.rb +181 -0
- data/lib/aws-sdk-s3/endpoint_provider.rb +716 -0
- data/lib/aws-sdk-s3/endpoints.rb +1518 -0
- data/lib/aws-sdk-s3/errors.rb +58 -0
- data/lib/aws-sdk-s3/express_credentials.rb +55 -0
- data/lib/aws-sdk-s3/express_credentials_provider.rb +59 -0
- data/lib/aws-sdk-s3/file_downloader.rb +241 -87
- data/lib/aws-sdk-s3/file_uploader.rb +16 -13
- data/lib/aws-sdk-s3/legacy_signer.rb +2 -1
- data/lib/aws-sdk-s3/multipart_download_error.rb +8 -0
- data/lib/aws-sdk-s3/multipart_file_uploader.rb +108 -86
- data/lib/aws-sdk-s3/multipart_stream_uploader.rb +110 -92
- data/lib/aws-sdk-s3/multipart_upload.rb +294 -19
- data/lib/aws-sdk-s3/multipart_upload_error.rb +3 -4
- data/lib/aws-sdk-s3/multipart_upload_part.rb +297 -31
- data/lib/aws-sdk-s3/object.rb +2224 -269
- data/lib/aws-sdk-s3/object_acl.rb +59 -17
- data/lib/aws-sdk-s3/object_copier.rb +7 -5
- data/lib/aws-sdk-s3/object_multipart_copier.rb +48 -23
- data/lib/aws-sdk-s3/object_summary.rb +1915 -220
- data/lib/aws-sdk-s3/object_version.rb +450 -58
- data/lib/aws-sdk-s3/plugins/accelerate.rb +3 -44
- data/lib/aws-sdk-s3/plugins/access_grants.rb +178 -0
- data/lib/aws-sdk-s3/plugins/arn.rb +0 -197
- data/lib/aws-sdk-s3/plugins/bucket_dns.rb +3 -39
- data/lib/aws-sdk-s3/plugins/bucket_name_restrictions.rb +1 -6
- data/lib/aws-sdk-s3/plugins/checksum_algorithm.rb +31 -0
- data/lib/aws-sdk-s3/plugins/dualstack.rb +1 -55
- data/lib/aws-sdk-s3/plugins/endpoints.rb +86 -0
- data/lib/aws-sdk-s3/plugins/expect_100_continue.rb +2 -1
- data/lib/aws-sdk-s3/plugins/express_session_auth.rb +88 -0
- data/lib/aws-sdk-s3/plugins/http_200_errors.rb +55 -18
- data/lib/aws-sdk-s3/plugins/iad_regional_endpoint.rb +6 -29
- data/lib/aws-sdk-s3/plugins/location_constraint.rb +3 -1
- data/lib/aws-sdk-s3/plugins/md5s.rb +10 -68
- data/lib/aws-sdk-s3/plugins/s3_signer.rb +42 -111
- data/lib/aws-sdk-s3/plugins/streaming_retry.rb +28 -9
- data/lib/aws-sdk-s3/plugins/url_encoded_keys.rb +2 -1
- data/lib/aws-sdk-s3/presigned_post.rb +99 -78
- data/lib/aws-sdk-s3/presigner.rb +32 -41
- data/lib/aws-sdk-s3/resource.rb +139 -12
- data/lib/aws-sdk-s3/transfer_manager.rb +304 -0
- data/lib/aws-sdk-s3/types.rb +10204 -5378
- data/lib/aws-sdk-s3.rb +35 -27
- data/sig/bucket.rbs +231 -0
- data/sig/bucket_acl.rbs +78 -0
- data/sig/bucket_cors.rbs +69 -0
- data/sig/bucket_lifecycle.rbs +88 -0
- data/sig/bucket_lifecycle_configuration.rbs +115 -0
- data/sig/bucket_logging.rbs +76 -0
- data/sig/bucket_notification.rbs +114 -0
- data/sig/bucket_policy.rbs +59 -0
- data/sig/bucket_request_payment.rbs +54 -0
- data/sig/bucket_tagging.rbs +65 -0
- data/sig/bucket_versioning.rbs +77 -0
- data/sig/bucket_website.rbs +93 -0
- data/sig/client.rbs +2586 -0
- data/sig/customizations/bucket.rbs +19 -0
- data/sig/customizations/object.rbs +38 -0
- data/sig/customizations/object_summary.rbs +35 -0
- data/sig/errors.rbs +44 -0
- data/sig/multipart_upload.rbs +120 -0
- data/sig/multipart_upload_part.rbs +109 -0
- data/sig/object.rbs +464 -0
- data/sig/object_acl.rbs +86 -0
- data/sig/object_summary.rbs +347 -0
- data/sig/object_version.rbs +143 -0
- data/sig/resource.rbs +141 -0
- data/sig/types.rbs +2868 -0
- data/sig/waiters.rbs +95 -0
- metadata +51 -16
- data/lib/aws-sdk-s3/arn/access_point_arn.rb +0 -69
- data/lib/aws-sdk-s3/arn/multi_region_access_point_arn.rb +0 -69
- data/lib/aws-sdk-s3/arn/object_lambda_arn.rb +0 -69
- data/lib/aws-sdk-s3/arn/outpost_access_point_arn.rb +0 -73
- data/lib/aws-sdk-s3/plugins/object_lambda_endpoint.rb +0 -25
data/lib/aws-sdk-s3/errors.rb
CHANGED
|
@@ -29,12 +29,17 @@ module Aws::S3
|
|
|
29
29
|
# ## Error Classes
|
|
30
30
|
# * {BucketAlreadyExists}
|
|
31
31
|
# * {BucketAlreadyOwnedByYou}
|
|
32
|
+
# * {EncryptionTypeMismatch}
|
|
33
|
+
# * {IdempotencyParameterMismatch}
|
|
32
34
|
# * {InvalidObjectState}
|
|
35
|
+
# * {InvalidRequest}
|
|
36
|
+
# * {InvalidWriteOffset}
|
|
33
37
|
# * {NoSuchBucket}
|
|
34
38
|
# * {NoSuchKey}
|
|
35
39
|
# * {NoSuchUpload}
|
|
36
40
|
# * {ObjectAlreadyInActiveTierError}
|
|
37
41
|
# * {ObjectNotInActiveTierError}
|
|
42
|
+
# * {TooManyParts}
|
|
38
43
|
#
|
|
39
44
|
# Additionally, error classes are dynamically generated for service errors based on the error code
|
|
40
45
|
# if they are not defined above.
|
|
@@ -62,6 +67,26 @@ module Aws::S3
|
|
|
62
67
|
end
|
|
63
68
|
end
|
|
64
69
|
|
|
70
|
+
class EncryptionTypeMismatch < ServiceError
|
|
71
|
+
|
|
72
|
+
# @param [Seahorse::Client::RequestContext] context
|
|
73
|
+
# @param [String] message
|
|
74
|
+
# @param [Aws::S3::Types::EncryptionTypeMismatch] data
|
|
75
|
+
def initialize(context, message, data = Aws::EmptyStructure.new)
|
|
76
|
+
super(context, message, data)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class IdempotencyParameterMismatch < ServiceError
|
|
81
|
+
|
|
82
|
+
# @param [Seahorse::Client::RequestContext] context
|
|
83
|
+
# @param [String] message
|
|
84
|
+
# @param [Aws::S3::Types::IdempotencyParameterMismatch] data
|
|
85
|
+
def initialize(context, message, data = Aws::EmptyStructure.new)
|
|
86
|
+
super(context, message, data)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
65
90
|
class InvalidObjectState < ServiceError
|
|
66
91
|
|
|
67
92
|
# @param [Seahorse::Client::RequestContext] context
|
|
@@ -82,6 +107,26 @@ module Aws::S3
|
|
|
82
107
|
end
|
|
83
108
|
end
|
|
84
109
|
|
|
110
|
+
class InvalidRequest < ServiceError
|
|
111
|
+
|
|
112
|
+
# @param [Seahorse::Client::RequestContext] context
|
|
113
|
+
# @param [String] message
|
|
114
|
+
# @param [Aws::S3::Types::InvalidRequest] data
|
|
115
|
+
def initialize(context, message, data = Aws::EmptyStructure.new)
|
|
116
|
+
super(context, message, data)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class InvalidWriteOffset < ServiceError
|
|
121
|
+
|
|
122
|
+
# @param [Seahorse::Client::RequestContext] context
|
|
123
|
+
# @param [String] message
|
|
124
|
+
# @param [Aws::S3::Types::InvalidWriteOffset] data
|
|
125
|
+
def initialize(context, message, data = Aws::EmptyStructure.new)
|
|
126
|
+
super(context, message, data)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
85
130
|
class NoSuchBucket < ServiceError
|
|
86
131
|
|
|
87
132
|
# @param [Seahorse::Client::RequestContext] context
|
|
@@ -132,5 +177,18 @@ module Aws::S3
|
|
|
132
177
|
end
|
|
133
178
|
end
|
|
134
179
|
|
|
180
|
+
class TooManyParts < ServiceError
|
|
181
|
+
|
|
182
|
+
# @param [Seahorse::Client::RequestContext] context
|
|
183
|
+
# @param [String] message
|
|
184
|
+
# @param [Aws::S3::Types::TooManyParts] data
|
|
185
|
+
def initialize(context, message, data = Aws::EmptyStructure.new)
|
|
186
|
+
super(context, message, data)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
135
190
|
end
|
|
136
191
|
end
|
|
192
|
+
|
|
193
|
+
# Load customizations if they exist
|
|
194
|
+
require 'aws-sdk-s3/customizations/errors'
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
module Aws
|
|
6
|
+
module S3
|
|
7
|
+
# @api private
|
|
8
|
+
class ExpressCredentials
|
|
9
|
+
include CredentialProvider
|
|
10
|
+
include RefreshingCredentials
|
|
11
|
+
|
|
12
|
+
SYNC_EXPIRATION_LENGTH = 60 # 1 minute
|
|
13
|
+
ASYNC_EXPIRATION_LENGTH = 120 # 2 minutes
|
|
14
|
+
|
|
15
|
+
def initialize(options = {})
|
|
16
|
+
@client = options[:client]
|
|
17
|
+
@create_session_params = {}
|
|
18
|
+
options.each_pair do |key, value|
|
|
19
|
+
if self.class.create_session_options.include?(key)
|
|
20
|
+
@create_session_params[key] = value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
@async_refresh = true
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [S3::Client]
|
|
28
|
+
attr_reader :client
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def refresh
|
|
33
|
+
c = @client.create_session(@create_session_params).credentials
|
|
34
|
+
@credentials = Credentials.new(
|
|
35
|
+
c.access_key_id,
|
|
36
|
+
c.secret_access_key,
|
|
37
|
+
c.session_token
|
|
38
|
+
)
|
|
39
|
+
@expiration = c.expiration
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
|
|
44
|
+
# @api private
|
|
45
|
+
def create_session_options
|
|
46
|
+
@cso ||= begin
|
|
47
|
+
input = S3::Client.api.operation(:create_session).input
|
|
48
|
+
Set.new(input.shape.member_names)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aws
|
|
4
|
+
module S3
|
|
5
|
+
# @api private
|
|
6
|
+
def self.express_credentials_cache
|
|
7
|
+
@express_credentials_cache ||= LRUCache.new(max_entries: 100)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Returns Credentials class for S3 Express. Accepts CreateSession
|
|
11
|
+
# params as options. See {Client#create_session} for details.
|
|
12
|
+
class ExpressCredentialsProvider
|
|
13
|
+
# @param [Hash] options
|
|
14
|
+
# @option options [Client] :client The S3 client used to create the
|
|
15
|
+
# session.
|
|
16
|
+
# @option options [String] :session_mode (see: {Client#create_session})
|
|
17
|
+
# @option options [Boolean] :caching (true) When true, credentials will
|
|
18
|
+
# be cached.
|
|
19
|
+
# @option options [Callable] :before_refresh Proc called before
|
|
20
|
+
# credentials are refreshed.
|
|
21
|
+
def initialize(options = {})
|
|
22
|
+
@client = options.delete(:client)
|
|
23
|
+
@caching = options.delete(:caching) != false
|
|
24
|
+
@options = options
|
|
25
|
+
return unless @caching
|
|
26
|
+
|
|
27
|
+
@cache = Aws::S3.express_credentials_cache
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def express_credentials_for(bucket)
|
|
31
|
+
if @caching
|
|
32
|
+
cached_credentials_for(bucket)
|
|
33
|
+
else
|
|
34
|
+
new_credentials_for(bucket)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
attr_accessor :client
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def cached_credentials_for(bucket)
|
|
43
|
+
if @cache.key?(bucket)
|
|
44
|
+
@cache[bucket]
|
|
45
|
+
else
|
|
46
|
+
@cache[bucket] = new_credentials_for(bucket)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def new_credentials_for(bucket)
|
|
51
|
+
ExpressCredentials.new(
|
|
52
|
+
bucket: bucket,
|
|
53
|
+
client: @client,
|
|
54
|
+
**@options
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -1,146 +1,300 @@
|
|
|
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
|
|
10
9
|
# @api private
|
|
11
10
|
class FileDownloader
|
|
12
|
-
|
|
13
11
|
MIN_CHUNK_SIZE = 5 * 1024 * 1024
|
|
14
12
|
MAX_PARTS = 10_000
|
|
15
|
-
|
|
13
|
+
HEAD_OPTIONS = Set.new(Client.api.operation(:head_object).input.shape.member_names)
|
|
14
|
+
GET_OPTIONS = Set.new(Client.api.operation(:get_object).input.shape.member_names)
|
|
16
15
|
|
|
17
16
|
def initialize(options = {})
|
|
18
17
|
@client = options[:client] || Client.new
|
|
18
|
+
@executor = options[:executor]
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# @return [Client]
|
|
22
22
|
attr_reader :client
|
|
23
23
|
|
|
24
24
|
def download(destination, options = {})
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
validate_destination!(destination)
|
|
26
|
+
opts = build_download_opts(destination, options)
|
|
27
|
+
validate_opts!(opts)
|
|
28
|
+
|
|
29
|
+
Aws::Plugins::UserAgent.metric('S3_TRANSFER') do
|
|
30
|
+
case opts[:mode]
|
|
31
|
+
when 'auto' then multipart_download(opts)
|
|
32
|
+
when 'single_request' then single_request(opts)
|
|
33
|
+
when 'get_range' then range_request(opts)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
File.rename(opts[:temp_path], destination) if opts[:temp_path]
|
|
37
|
+
ensure
|
|
38
|
+
cleanup_temp_file(opts)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def build_download_opts(destination, opts)
|
|
44
|
+
{
|
|
45
|
+
destination: destination,
|
|
46
|
+
mode: opts.delete(:mode) || 'auto',
|
|
47
|
+
chunk_size: opts.delete(:chunk_size),
|
|
48
|
+
on_checksum_validated: opts.delete(:on_checksum_validated),
|
|
49
|
+
progress_callback: opts.delete(:progress_callback),
|
|
50
|
+
params: opts,
|
|
51
|
+
temp_path: nil
|
|
32
52
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def cleanup_temp_file(opts)
|
|
56
|
+
return unless opts
|
|
57
|
+
|
|
58
|
+
temp_file = opts[:temp_path]
|
|
59
|
+
File.delete(temp_file) if temp_file && File.exist?(temp_file)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def download_with_executor(part_list, total_size, opts)
|
|
63
|
+
download_attempts = 0
|
|
64
|
+
completion_queue = Queue.new
|
|
65
|
+
abort_download = false
|
|
66
|
+
error = nil
|
|
67
|
+
progress = MultipartProgress.new(part_list, total_size, opts[:progress_callback])
|
|
68
|
+
|
|
69
|
+
while (part = part_list.shift)
|
|
70
|
+
break if abort_download
|
|
71
|
+
|
|
72
|
+
download_attempts += 1
|
|
73
|
+
@executor.post(part) do |p|
|
|
74
|
+
update_progress(progress, p)
|
|
75
|
+
resp = @client.get_object(p.params)
|
|
76
|
+
range = extract_range(resp.content_range)
|
|
77
|
+
validate_range(range, p.params[:range]) if p.params[:range]
|
|
78
|
+
write(resp.body, range, opts)
|
|
79
|
+
|
|
80
|
+
execute_checksum_callback(resp, opts)
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
abort_download = true
|
|
83
|
+
error = e
|
|
84
|
+
ensure
|
|
85
|
+
completion_queue << :done
|
|
45
86
|
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
download_attempts.times { completion_queue.pop }
|
|
90
|
+
raise error unless error.nil?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def get_opts(opts)
|
|
94
|
+
GET_OPTIONS.each_with_object({}) { |k, h| h[k] = opts[k] if opts.key?(k) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def head_opts(opts)
|
|
98
|
+
HEAD_OPTIONS.each_with_object({}) { |k, h| h[k] = opts[k] if opts.key?(k) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def compute_chunk(chunk_size, file_size)
|
|
102
|
+
raise ArgumentError, ":chunk_size shouldn't exceed total file size." if chunk_size && chunk_size > file_size
|
|
103
|
+
|
|
104
|
+
chunk_size || [(file_size.to_f / MAX_PARTS).ceil, MIN_CHUNK_SIZE].max.to_i
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def compute_mode(file_size, total_parts, etag, opts)
|
|
108
|
+
chunk_size = compute_chunk(opts[:chunk_size], file_size)
|
|
109
|
+
part_size = (file_size.to_f / total_parts).ceil
|
|
110
|
+
|
|
111
|
+
resolve_temp_path(opts)
|
|
112
|
+
if chunk_size < part_size
|
|
113
|
+
multithreaded_get_by_ranges(file_size, etag, opts)
|
|
46
114
|
else
|
|
47
|
-
|
|
48
|
-
'mode should be :single_request, :get_range or :auto'
|
|
49
|
-
raise ArgumentError, msg
|
|
115
|
+
multithreaded_get_by_parts(total_parts, file_size, etag, opts)
|
|
50
116
|
end
|
|
51
117
|
end
|
|
52
118
|
|
|
53
|
-
|
|
119
|
+
def extract_range(value)
|
|
120
|
+
value.match(%r{bytes (?<range>\d+-\d+)/\d+})[:range]
|
|
121
|
+
end
|
|
54
122
|
|
|
55
|
-
def multipart_download
|
|
56
|
-
resp = @client.head_object(
|
|
123
|
+
def multipart_download(opts)
|
|
124
|
+
resp = @client.head_object(head_opts(opts[:params].merge(part_number: 1)))
|
|
57
125
|
count = resp.parts_count
|
|
126
|
+
|
|
58
127
|
if count.nil? || count <= 1
|
|
59
|
-
resp.content_length
|
|
60
|
-
single_request
|
|
61
|
-
|
|
128
|
+
if resp.content_length <= MIN_CHUNK_SIZE
|
|
129
|
+
single_request(opts)
|
|
130
|
+
else
|
|
131
|
+
resolve_temp_path(opts)
|
|
132
|
+
multithreaded_get_by_ranges(resp.content_length, resp.etag, opts)
|
|
133
|
+
end
|
|
62
134
|
else
|
|
63
|
-
#
|
|
64
|
-
resp = @client.head_object(
|
|
65
|
-
resp.content_length
|
|
66
|
-
single_request
|
|
67
|
-
|
|
135
|
+
# covers cases when given object is not uploaded via UploadPart API
|
|
136
|
+
resp = @client.head_object(head_opts(opts[:params])) # partNumber is an option
|
|
137
|
+
if resp.content_length <= MIN_CHUNK_SIZE
|
|
138
|
+
single_request(opts)
|
|
139
|
+
else
|
|
140
|
+
compute_mode(resp.content_length, count, resp.etag, opts)
|
|
141
|
+
end
|
|
68
142
|
end
|
|
69
143
|
end
|
|
70
144
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
multithreaded_get_by_ranges(construct_chunks(file_size))
|
|
76
|
-
else
|
|
77
|
-
multithreaded_get_by_parts(count)
|
|
145
|
+
def multithreaded_get_by_parts(total_parts, file_size, etag, opts)
|
|
146
|
+
parts = (1..total_parts).map do |part|
|
|
147
|
+
params = get_opts(opts[:params].merge(part_number: part, if_match: etag))
|
|
148
|
+
Part.new(part_number: part, params: params)
|
|
78
149
|
end
|
|
150
|
+
download_with_executor(PartList.new(parts), file_size, opts)
|
|
79
151
|
end
|
|
80
152
|
|
|
81
|
-
def
|
|
153
|
+
def multithreaded_get_by_ranges(file_size, etag, opts)
|
|
82
154
|
offset = 0
|
|
83
|
-
default_chunk_size = compute_chunk(file_size)
|
|
155
|
+
default_chunk_size = compute_chunk(opts[:chunk_size], file_size)
|
|
84
156
|
chunks = []
|
|
85
|
-
|
|
157
|
+
part_number = 1 # parts start at 1
|
|
158
|
+
while offset < file_size
|
|
86
159
|
progress = offset + default_chunk_size
|
|
87
|
-
|
|
88
|
-
|
|
160
|
+
progress = file_size if progress > file_size
|
|
161
|
+
params = get_opts(opts[:params].merge(range: "bytes=#{offset}-#{progress - 1}", if_match: etag))
|
|
162
|
+
chunks << Part.new(part_number: part_number, size: (progress - offset), params: params)
|
|
163
|
+
part_number += 1
|
|
164
|
+
offset = progress
|
|
89
165
|
end
|
|
90
|
-
chunks
|
|
166
|
+
download_with_executor(PartList.new(chunks), file_size, opts)
|
|
91
167
|
end
|
|
92
168
|
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
chunk_size = @chunk_size || [
|
|
98
|
-
(file_size.to_f / MAX_PARTS).ceil,
|
|
99
|
-
MIN_CHUNK_SIZE
|
|
100
|
-
].max.to_i
|
|
101
|
-
chunk_size -= 1 if file_size % chunk_size == 1
|
|
102
|
-
chunk_size
|
|
103
|
-
end
|
|
169
|
+
def range_request(opts)
|
|
170
|
+
resp = @client.head_object(head_opts(opts[:params]))
|
|
171
|
+
resolve_temp_path(opts)
|
|
172
|
+
multithreaded_get_by_ranges(resp.content_length, resp.etag, opts)
|
|
104
173
|
end
|
|
105
174
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
|
|
175
|
+
def resolve_temp_path(opts)
|
|
176
|
+
return if [File, Tempfile].include?(opts[:destination].class)
|
|
177
|
+
|
|
178
|
+
opts[:temp_path] ||= "#{opts[:destination]}.s3tmp.#{SecureRandom.alphanumeric(8)}"
|
|
109
179
|
end
|
|
110
180
|
|
|
111
|
-
def
|
|
112
|
-
|
|
181
|
+
def single_request(opts)
|
|
182
|
+
params = get_opts(opts[:params]).merge(response_target: opts[:destination])
|
|
183
|
+
params[:on_chunk_received] = single_part_progress(opts) if opts[:progress_callback]
|
|
184
|
+
resp = @client.get_object(params)
|
|
185
|
+
return resp unless opts[:on_checksum_validated]
|
|
186
|
+
|
|
187
|
+
opts[:on_checksum_validated].call(resp.checksum_validated, resp) if resp.checksum_validated
|
|
188
|
+
resp
|
|
113
189
|
end
|
|
114
190
|
|
|
115
|
-
def
|
|
116
|
-
|
|
191
|
+
def single_part_progress(opts)
|
|
192
|
+
proc do |_chunk, bytes_read, total_size|
|
|
193
|
+
opts[:progress_callback].call([bytes_read], [total_size], total_size)
|
|
194
|
+
end
|
|
117
195
|
end
|
|
118
196
|
|
|
119
|
-
def
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
@params.merge(param.to_sym => chunk)
|
|
126
|
-
)
|
|
127
|
-
write(resp)
|
|
128
|
-
end
|
|
197
|
+
def update_progress(progress, part)
|
|
198
|
+
return unless progress.progress_callback
|
|
199
|
+
|
|
200
|
+
part.params[:on_chunk_received] =
|
|
201
|
+
proc do |_chunk, bytes, total|
|
|
202
|
+
progress.call(part.part_number, bytes, total)
|
|
129
203
|
end
|
|
130
|
-
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def execute_checksum_callback(resp, opts)
|
|
207
|
+
return unless opts[:on_checksum_validated] && resp.checksum_validated
|
|
208
|
+
|
|
209
|
+
opts[:on_checksum_validated].call(resp.checksum_validated, resp)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def validate_destination!(destination)
|
|
213
|
+
valid_types = [String, Pathname, File, Tempfile]
|
|
214
|
+
return if valid_types.include?(destination.class)
|
|
215
|
+
|
|
216
|
+
raise ArgumentError, "Invalid destination, expected #{valid_types.join(', ')} but got: #{destination.class}"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def validate_opts!(opts)
|
|
220
|
+
if opts[:on_checksum_validated] && !opts[:on_checksum_validated].respond_to?(:call)
|
|
221
|
+
raise ArgumentError, ':on_checksum_validated must be callable'
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
valid_modes = %w[auto get_range single_request]
|
|
225
|
+
unless valid_modes.include?(opts[:mode])
|
|
226
|
+
msg = "Invalid mode #{opts[:mode]} provided, :mode should be single_request, get_range or auto"
|
|
227
|
+
raise ArgumentError, msg
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
if opts[:mode] == 'get_range' && opts[:chunk_size].nil?
|
|
231
|
+
raise ArgumentError, 'In get_range mode, :chunk_size must be provided'
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
if opts[:chunk_size] && opts[:chunk_size] <= 0
|
|
235
|
+
raise ArgumentError, ':chunk_size must be positive'
|
|
131
236
|
end
|
|
132
237
|
end
|
|
133
238
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
239
|
+
def validate_range(actual, expected)
|
|
240
|
+
return if actual == expected.match(/bytes=(?<range>\d+-\d+)/)[:range]
|
|
241
|
+
|
|
242
|
+
raise MultipartDownloadError, "multipart download failed: expected range of #{expected} but got #{actual}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def write(body, range, opts)
|
|
246
|
+
path = opts[:temp_path] || opts[:destination]
|
|
247
|
+
File.write(path, body.read, range.split('-').first.to_i)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# @api private
|
|
251
|
+
class Part < Struct.new(:part_number, :size, :params)
|
|
252
|
+
include Aws::Structure
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# @api private
|
|
256
|
+
class PartList
|
|
257
|
+
include Enumerable
|
|
258
|
+
def initialize(parts = [])
|
|
259
|
+
@parts = parts
|
|
260
|
+
@mutex = Mutex.new
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def shift
|
|
264
|
+
@mutex.synchronize { @parts.shift }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def size
|
|
268
|
+
@mutex.synchronize { @parts.size }
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def clear!
|
|
272
|
+
@mutex.synchronize { @parts.clear }
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def each(&block)
|
|
276
|
+
@mutex.synchronize { @parts.each(&block) }
|
|
277
|
+
end
|
|
138
278
|
end
|
|
139
279
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
280
|
+
# @api private
|
|
281
|
+
class MultipartProgress
|
|
282
|
+
def initialize(parts, total_size, progress_callback)
|
|
283
|
+
@bytes_received = Array.new(parts.size, 0)
|
|
284
|
+
@part_sizes = parts.map(&:size)
|
|
285
|
+
@total_size = total_size
|
|
286
|
+
@progress_callback = progress_callback
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
attr_reader :progress_callback
|
|
290
|
+
|
|
291
|
+
def call(part_number, bytes_received, total)
|
|
292
|
+
# part numbers start at 1
|
|
293
|
+
@bytes_received[part_number - 1] = bytes_received
|
|
294
|
+
# part size may not be known until we get the first response
|
|
295
|
+
@part_sizes[part_number - 1] ||= total
|
|
296
|
+
@progress_callback.call(@bytes_received, @part_sizes, @total_size)
|
|
297
|
+
end
|
|
144
298
|
end
|
|
145
299
|
end
|
|
146
300
|
end
|
|
@@ -7,23 +7,21 @@ module Aws
|
|
|
7
7
|
# @api private
|
|
8
8
|
class FileUploader
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
DEFAULT_MULTIPART_THRESHOLD = 100 * 1024 * 1024
|
|
11
11
|
|
|
12
12
|
# @param [Hash] options
|
|
13
13
|
# @option options [Client] :client
|
|
14
14
|
# @option options [Integer] :multipart_threshold (104857600)
|
|
15
15
|
def initialize(options = {})
|
|
16
|
-
@options = options
|
|
17
16
|
@client = options[:client] || Client.new
|
|
18
|
-
@
|
|
19
|
-
|
|
17
|
+
@executor = options[:executor]
|
|
18
|
+
@multipart_threshold = options[:multipart_threshold] || DEFAULT_MULTIPART_THRESHOLD
|
|
20
19
|
end
|
|
21
20
|
|
|
22
21
|
# @return [Client]
|
|
23
22
|
attr_reader :client
|
|
24
23
|
|
|
25
|
-
# @return [Integer] Files larger than or equal to this in bytes are uploaded
|
|
26
|
-
# using a {MultipartFileUploader}.
|
|
24
|
+
# @return [Integer] Files larger than or equal to this in bytes are uploaded using a {MultipartFileUploader}.
|
|
27
25
|
attr_reader :multipart_threshold
|
|
28
26
|
|
|
29
27
|
# @param [String, Pathname, File, Tempfile] source The file to upload.
|
|
@@ -32,20 +30,25 @@ module Aws
|
|
|
32
30
|
# @option options [Proc] :progress_callback
|
|
33
31
|
# A Proc that will be called when each chunk of the upload is sent.
|
|
34
32
|
# It will be invoked with [bytes_read], [total_sizes]
|
|
33
|
+
# @option options [Integer] :thread_count
|
|
34
|
+
# The thread count to use for multipart uploads. Ignored for
|
|
35
|
+
# objects smaller than the multipart threshold.
|
|
35
36
|
# @return [void]
|
|
36
37
|
def upload(source, options = {})
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
Aws::Plugins::UserAgent.metric('S3_TRANSFER') do
|
|
39
|
+
if File.size(source) >= @multipart_threshold
|
|
40
|
+
MultipartFileUploader.new(client: @client, executor: @executor).upload(source, options)
|
|
41
|
+
else
|
|
42
|
+
put_object(source, options)
|
|
43
|
+
end
|
|
41
44
|
end
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
private
|
|
45
48
|
|
|
46
|
-
def open_file(source)
|
|
47
|
-
if String
|
|
48
|
-
File.open(source, 'rb'
|
|
49
|
+
def open_file(source, &block)
|
|
50
|
+
if source.is_a?(String) || source.is_a?(Pathname)
|
|
51
|
+
File.open(source, 'rb', &block)
|
|
49
52
|
else
|
|
50
53
|
yield(source)
|
|
51
54
|
end
|