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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +664 -0
  3. data/VERSION +1 -1
  4. data/lib/aws-sdk-s3/access_grants_credentials.rb +57 -0
  5. data/lib/aws-sdk-s3/access_grants_credentials_provider.rb +250 -0
  6. data/lib/aws-sdk-s3/bucket.rb +858 -116
  7. data/lib/aws-sdk-s3/bucket_acl.rb +32 -9
  8. data/lib/aws-sdk-s3/bucket_cors.rb +38 -13
  9. data/lib/aws-sdk-s3/bucket_lifecycle.rb +43 -12
  10. data/lib/aws-sdk-s3/bucket_lifecycle_configuration.rb +100 -13
  11. data/lib/aws-sdk-s3/bucket_logging.rb +35 -6
  12. data/lib/aws-sdk-s3/bucket_notification.rb +27 -9
  13. data/lib/aws-sdk-s3/bucket_policy.rb +79 -10
  14. data/lib/aws-sdk-s3/bucket_region_cache.rb +9 -5
  15. data/lib/aws-sdk-s3/bucket_request_payment.rb +29 -7
  16. data/lib/aws-sdk-s3/bucket_tagging.rb +35 -11
  17. data/lib/aws-sdk-s3/bucket_versioning.rb +108 -17
  18. data/lib/aws-sdk-s3/bucket_website.rb +35 -11
  19. data/lib/aws-sdk-s3/client.rb +11799 -3636
  20. data/lib/aws-sdk-s3/client_api.rb +1201 -276
  21. data/lib/aws-sdk-s3/customizations/bucket.rb +23 -47
  22. data/lib/aws-sdk-s3/customizations/errors.rb +40 -0
  23. data/lib/aws-sdk-s3/customizations/object.rb +216 -70
  24. data/lib/aws-sdk-s3/customizations/object_summary.rb +5 -0
  25. data/lib/aws-sdk-s3/customizations/object_version.rb +13 -0
  26. data/lib/aws-sdk-s3/customizations/types/permanent_redirect.rb +26 -0
  27. data/lib/aws-sdk-s3/customizations.rb +27 -29
  28. data/lib/aws-sdk-s3/default_executor.rb +103 -0
  29. data/lib/aws-sdk-s3/encryption/client.rb +6 -2
  30. data/lib/aws-sdk-s3/encryption/kms_cipher_provider.rb +13 -9
  31. data/lib/aws-sdk-s3/encryptionV2/client.rb +6 -2
  32. data/lib/aws-sdk-s3/encryptionV2/decrypt_handler.rb +1 -0
  33. data/lib/aws-sdk-s3/encryptionV2/kms_cipher_provider.rb +10 -6
  34. data/lib/aws-sdk-s3/endpoint_parameters.rb +181 -0
  35. data/lib/aws-sdk-s3/endpoint_provider.rb +716 -0
  36. data/lib/aws-sdk-s3/endpoints.rb +1518 -0
  37. data/lib/aws-sdk-s3/errors.rb +58 -0
  38. data/lib/aws-sdk-s3/express_credentials.rb +55 -0
  39. data/lib/aws-sdk-s3/express_credentials_provider.rb +59 -0
  40. data/lib/aws-sdk-s3/file_downloader.rb +241 -87
  41. data/lib/aws-sdk-s3/file_uploader.rb +16 -13
  42. data/lib/aws-sdk-s3/legacy_signer.rb +2 -1
  43. data/lib/aws-sdk-s3/multipart_download_error.rb +8 -0
  44. data/lib/aws-sdk-s3/multipart_file_uploader.rb +108 -86
  45. data/lib/aws-sdk-s3/multipart_stream_uploader.rb +110 -92
  46. data/lib/aws-sdk-s3/multipart_upload.rb +294 -19
  47. data/lib/aws-sdk-s3/multipart_upload_error.rb +3 -4
  48. data/lib/aws-sdk-s3/multipart_upload_part.rb +297 -31
  49. data/lib/aws-sdk-s3/object.rb +2224 -269
  50. data/lib/aws-sdk-s3/object_acl.rb +59 -17
  51. data/lib/aws-sdk-s3/object_copier.rb +7 -5
  52. data/lib/aws-sdk-s3/object_multipart_copier.rb +48 -23
  53. data/lib/aws-sdk-s3/object_summary.rb +1915 -220
  54. data/lib/aws-sdk-s3/object_version.rb +450 -58
  55. data/lib/aws-sdk-s3/plugins/accelerate.rb +3 -44
  56. data/lib/aws-sdk-s3/plugins/access_grants.rb +178 -0
  57. data/lib/aws-sdk-s3/plugins/arn.rb +0 -197
  58. data/lib/aws-sdk-s3/plugins/bucket_dns.rb +3 -39
  59. data/lib/aws-sdk-s3/plugins/bucket_name_restrictions.rb +1 -6
  60. data/lib/aws-sdk-s3/plugins/checksum_algorithm.rb +31 -0
  61. data/lib/aws-sdk-s3/plugins/dualstack.rb +1 -55
  62. data/lib/aws-sdk-s3/plugins/endpoints.rb +86 -0
  63. data/lib/aws-sdk-s3/plugins/expect_100_continue.rb +2 -1
  64. data/lib/aws-sdk-s3/plugins/express_session_auth.rb +88 -0
  65. data/lib/aws-sdk-s3/plugins/http_200_errors.rb +55 -18
  66. data/lib/aws-sdk-s3/plugins/iad_regional_endpoint.rb +6 -29
  67. data/lib/aws-sdk-s3/plugins/location_constraint.rb +3 -1
  68. data/lib/aws-sdk-s3/plugins/md5s.rb +10 -68
  69. data/lib/aws-sdk-s3/plugins/s3_signer.rb +42 -111
  70. data/lib/aws-sdk-s3/plugins/streaming_retry.rb +28 -9
  71. data/lib/aws-sdk-s3/plugins/url_encoded_keys.rb +2 -1
  72. data/lib/aws-sdk-s3/presigned_post.rb +99 -78
  73. data/lib/aws-sdk-s3/presigner.rb +32 -41
  74. data/lib/aws-sdk-s3/resource.rb +139 -12
  75. data/lib/aws-sdk-s3/transfer_manager.rb +304 -0
  76. data/lib/aws-sdk-s3/types.rb +10204 -5378
  77. data/lib/aws-sdk-s3.rb +35 -27
  78. data/sig/bucket.rbs +231 -0
  79. data/sig/bucket_acl.rbs +78 -0
  80. data/sig/bucket_cors.rbs +69 -0
  81. data/sig/bucket_lifecycle.rbs +88 -0
  82. data/sig/bucket_lifecycle_configuration.rbs +115 -0
  83. data/sig/bucket_logging.rbs +76 -0
  84. data/sig/bucket_notification.rbs +114 -0
  85. data/sig/bucket_policy.rbs +59 -0
  86. data/sig/bucket_request_payment.rbs +54 -0
  87. data/sig/bucket_tagging.rbs +65 -0
  88. data/sig/bucket_versioning.rbs +77 -0
  89. data/sig/bucket_website.rbs +93 -0
  90. data/sig/client.rbs +2586 -0
  91. data/sig/customizations/bucket.rbs +19 -0
  92. data/sig/customizations/object.rbs +38 -0
  93. data/sig/customizations/object_summary.rbs +35 -0
  94. data/sig/errors.rbs +44 -0
  95. data/sig/multipart_upload.rbs +120 -0
  96. data/sig/multipart_upload_part.rbs +109 -0
  97. data/sig/object.rbs +464 -0
  98. data/sig/object_acl.rbs +86 -0
  99. data/sig/object_summary.rbs +347 -0
  100. data/sig/object_version.rbs +143 -0
  101. data/sig/resource.rbs +141 -0
  102. data/sig/types.rbs +2868 -0
  103. data/sig/waiters.rbs +95 -0
  104. metadata +51 -16
  105. data/lib/aws-sdk-s3/arn/access_point_arn.rb +0 -69
  106. data/lib/aws-sdk-s3/arn/multi_region_access_point_arn.rb +0 -69
  107. data/lib/aws-sdk-s3/arn/object_lambda_arn.rb +0 -69
  108. data/lib/aws-sdk-s3/arn/outpost_access_point_arn.rb +0 -73
  109. data/lib/aws-sdk-s3/plugins/object_lambda_endpoint.rb +0 -25
@@ -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 'thread'
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
- THREAD_COUNT = 10
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
- @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],
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
- @params[:version_id] = options[:version_id] if options[:version_id]
34
-
35
- case @mode
36
- when 'auto' then multipart_download
37
- when 'single_request' then single_request
38
- when 'get_range'
39
- if @chunk_size
40
- resp = @client.head_object(@params)
41
- multithreaded_get_by_ranges(construct_chunks(resp.content_length))
42
- else
43
- msg = 'In :get_range mode, :chunk_size must be provided'
44
- raise ArgumentError, msg
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
- msg = "Invalid mode #{@mode} provided, "\
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
- private
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(@params.merge(part_number: 1))
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 < MIN_CHUNK_SIZE ?
60
- single_request :
61
- multithreaded_get_by_ranges(construct_chunks(resp.content_length))
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
- # partNumber is an option
64
- resp = @client.head_object(@params)
65
- resp.content_length < MIN_CHUNK_SIZE ?
66
- single_request :
67
- compute_mode(resp.content_length, count)
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 compute_mode(file_size, count)
72
- chunk_size = compute_chunk(file_size)
73
- part_size = (file_size.to_f / count.to_f).ceil
74
- if chunk_size < part_size
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 construct_chunks(file_size)
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
- while offset <= file_size
157
+ part_number = 1 # parts start at 1
158
+ while offset < file_size
86
159
  progress = offset + default_chunk_size
87
- chunks << "bytes=#{offset}-#{progress < file_size ? progress : file_size}"
88
- offset = progress + 1
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 compute_chunk(file_size)
94
- if @chunk_size && @chunk_size > file_size
95
- raise ArgumentError, ":chunk_size shouldn't exceed total file size."
96
- else
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 batches(chunks, mode)
107
- chunks = (1..chunks) if mode.eql? 'part_number'
108
- chunks.each_slice(@thread_count).to_a
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 multithreaded_get_by_ranges(chunks)
112
- thread_batches(chunks, 'range')
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 multithreaded_get_by_parts(parts)
116
- thread_batches(parts, 'part_number')
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 thread_batches(chunks, param)
120
- batches(chunks, param).each do |batch|
121
- threads = []
122
- batch.each do |chunk|
123
- threads << Thread.new do
124
- resp = @client.get_object(
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
- threads.each(&:join)
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 write(resp)
135
- range, _ = resp.content_range.split(' ').last.split('/')
136
- head, _ = range.split('-').map {|s| s.to_i}
137
- IO.write(@path, resp.body.read, head)
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
- def single_request
141
- @client.get_object(
142
- @params.merge(response_target: @path)
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
- ONE_HUNDRED_MEGABYTES = 100 * 1024 * 1024
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
- @multipart_threshold = options[:multipart_threshold] ||
19
- ONE_HUNDRED_MEGABYTES
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
- if File.size(source) >= multipart_threshold
38
- MultipartFileUploader.new(@options).upload(source, options)
39
- else
40
- put_object(source, options)
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 === source || Pathname === source
48
- File.open(source, 'rb') { |file| yield(file) }
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
@@ -3,7 +3,8 @@
3
3
  require 'set'
4
4
  require 'time'
5
5
  require 'openssl'
6
- require 'cgi'
6
+ require "cgi/escape"
7
+ require "cgi/util" if RUBY_VERSION < "3.5"
7
8
  require 'aws-sdk-core/query'
8
9
 
9
10
  module Aws
@@ -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