aws-sdk-s3 1.10.0 → 1.208.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 (153) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +1517 -0
  3. data/LICENSE.txt +202 -0
  4. data/VERSION +1 -0
  5. data/lib/aws-sdk-s3/access_grants_credentials.rb +57 -0
  6. data/lib/aws-sdk-s3/access_grants_credentials_provider.rb +250 -0
  7. data/lib/aws-sdk-s3/bucket.rb +1062 -99
  8. data/lib/aws-sdk-s3/bucket_acl.rb +67 -17
  9. data/lib/aws-sdk-s3/bucket_cors.rb +80 -17
  10. data/lib/aws-sdk-s3/bucket_lifecycle.rb +71 -19
  11. data/lib/aws-sdk-s3/bucket_lifecycle_configuration.rb +126 -20
  12. data/lib/aws-sdk-s3/bucket_logging.rb +68 -18
  13. data/lib/aws-sdk-s3/bucket_notification.rb +56 -20
  14. data/lib/aws-sdk-s3/bucket_policy.rb +108 -17
  15. data/lib/aws-sdk-s3/bucket_region_cache.rb +11 -5
  16. data/lib/aws-sdk-s3/bucket_request_payment.rb +60 -15
  17. data/lib/aws-sdk-s3/bucket_tagging.rb +71 -17
  18. data/lib/aws-sdk-s3/bucket_versioning.rb +166 -17
  19. data/lib/aws-sdk-s3/bucket_website.rb +78 -17
  20. data/lib/aws-sdk-s3/client.rb +20068 -3879
  21. data/lib/aws-sdk-s3/client_api.rb +1957 -209
  22. data/lib/aws-sdk-s3/customizations/bucket.rb +57 -38
  23. data/lib/aws-sdk-s3/customizations/errors.rb +40 -0
  24. data/lib/aws-sdk-s3/customizations/multipart_upload.rb +2 -0
  25. data/lib/aws-sdk-s3/customizations/object.rb +338 -68
  26. data/lib/aws-sdk-s3/customizations/object_summary.rb +17 -0
  27. data/lib/aws-sdk-s3/customizations/object_version.rb +13 -0
  28. data/lib/aws-sdk-s3/customizations/types/list_object_versions_output.rb +2 -0
  29. data/lib/aws-sdk-s3/customizations/types/permanent_redirect.rb +26 -0
  30. data/lib/aws-sdk-s3/customizations.rb +30 -27
  31. data/lib/aws-sdk-s3/default_executor.rb +103 -0
  32. data/lib/aws-sdk-s3/encryption/client.rb +29 -8
  33. data/lib/aws-sdk-s3/encryption/decrypt_handler.rb +71 -29
  34. data/lib/aws-sdk-s3/encryption/default_cipher_provider.rb +45 -5
  35. data/lib/aws-sdk-s3/encryption/default_key_provider.rb +2 -0
  36. data/lib/aws-sdk-s3/encryption/encrypt_handler.rb +15 -2
  37. data/lib/aws-sdk-s3/encryption/errors.rb +2 -0
  38. data/lib/aws-sdk-s3/encryption/io_auth_decrypter.rb +11 -3
  39. data/lib/aws-sdk-s3/encryption/io_decrypter.rb +11 -3
  40. data/lib/aws-sdk-s3/encryption/io_encrypter.rb +2 -0
  41. data/lib/aws-sdk-s3/encryption/key_provider.rb +2 -0
  42. data/lib/aws-sdk-s3/encryption/kms_cipher_provider.rb +48 -11
  43. data/lib/aws-sdk-s3/encryption/materials.rb +8 -6
  44. data/lib/aws-sdk-s3/encryption/utils.rb +25 -0
  45. data/lib/aws-sdk-s3/encryption.rb +4 -0
  46. data/lib/aws-sdk-s3/encryptionV2/client.rb +645 -0
  47. data/lib/aws-sdk-s3/encryptionV2/decrypt_handler.rb +68 -0
  48. data/lib/aws-sdk-s3/encryptionV2/decryption.rb +205 -0
  49. data/lib/aws-sdk-s3/encryptionV2/default_cipher_provider.rb +187 -0
  50. data/lib/aws-sdk-s3/encryptionV2/default_key_provider.rb +40 -0
  51. data/lib/aws-sdk-s3/encryptionV2/encrypt_handler.rb +67 -0
  52. data/lib/aws-sdk-s3/encryptionV2/errors.rb +37 -0
  53. data/lib/aws-sdk-s3/encryptionV2/io_auth_decrypter.rb +58 -0
  54. data/lib/aws-sdk-s3/encryptionV2/io_decrypter.rb +37 -0
  55. data/lib/aws-sdk-s3/encryptionV2/io_encrypter.rb +75 -0
  56. data/lib/aws-sdk-s3/encryptionV2/key_provider.rb +31 -0
  57. data/lib/aws-sdk-s3/encryptionV2/kms_cipher_provider.rb +181 -0
  58. data/lib/aws-sdk-s3/encryptionV2/materials.rb +60 -0
  59. data/lib/aws-sdk-s3/encryptionV2/utils.rb +108 -0
  60. data/lib/aws-sdk-s3/encryptionV3/client.rb +885 -0
  61. data/lib/aws-sdk-s3/encryptionV3/decrypt_handler.rb +98 -0
  62. data/lib/aws-sdk-s3/encryptionV3/decryption.rb +244 -0
  63. data/lib/aws-sdk-s3/encryptionV3/default_cipher_provider.rb +159 -0
  64. data/lib/aws-sdk-s3/encryptionV3/default_key_provider.rb +35 -0
  65. data/lib/aws-sdk-s3/encryptionV3/encrypt_handler.rb +98 -0
  66. data/lib/aws-sdk-s3/encryptionV3/errors.rb +47 -0
  67. data/lib/aws-sdk-s3/encryptionV3/io_auth_decrypter.rb +60 -0
  68. data/lib/aws-sdk-s3/encryptionV3/io_decrypter.rb +35 -0
  69. data/lib/aws-sdk-s3/encryptionV3/io_encrypter.rb +84 -0
  70. data/lib/aws-sdk-s3/encryptionV3/key_provider.rb +28 -0
  71. data/lib/aws-sdk-s3/encryptionV3/kms_cipher_provider.rb +159 -0
  72. data/lib/aws-sdk-s3/encryptionV3/materials.rb +58 -0
  73. data/lib/aws-sdk-s3/encryptionV3/utils.rb +321 -0
  74. data/lib/aws-sdk-s3/encryption_v2.rb +24 -0
  75. data/lib/aws-sdk-s3/encryption_v3.rb +24 -0
  76. data/lib/aws-sdk-s3/endpoint_parameters.rb +181 -0
  77. data/lib/aws-sdk-s3/endpoint_provider.rb +886 -0
  78. data/lib/aws-sdk-s3/endpoints.rb +1544 -0
  79. data/lib/aws-sdk-s3/errors.rb +181 -1
  80. data/lib/aws-sdk-s3/event_streams.rb +69 -0
  81. data/lib/aws-sdk-s3/express_credentials.rb +55 -0
  82. data/lib/aws-sdk-s3/express_credentials_provider.rb +59 -0
  83. data/lib/aws-sdk-s3/file_downloader.rb +261 -82
  84. data/lib/aws-sdk-s3/file_part.rb +16 -13
  85. data/lib/aws-sdk-s3/file_uploader.rb +37 -22
  86. data/lib/aws-sdk-s3/legacy_signer.rb +19 -26
  87. data/lib/aws-sdk-s3/multipart_download_error.rb +8 -0
  88. data/lib/aws-sdk-s3/multipart_file_uploader.rb +142 -80
  89. data/lib/aws-sdk-s3/multipart_stream_uploader.rb +191 -0
  90. data/lib/aws-sdk-s3/multipart_upload.rb +342 -31
  91. data/lib/aws-sdk-s3/multipart_upload_error.rb +5 -4
  92. data/lib/aws-sdk-s3/multipart_upload_part.rb +387 -47
  93. data/lib/aws-sdk-s3/object.rb +2733 -204
  94. data/lib/aws-sdk-s3/object_acl.rb +112 -25
  95. data/lib/aws-sdk-s3/object_copier.rb +9 -5
  96. data/lib/aws-sdk-s3/object_multipart_copier.rb +50 -23
  97. data/lib/aws-sdk-s3/object_summary.rb +2265 -181
  98. data/lib/aws-sdk-s3/object_version.rb +542 -74
  99. data/lib/aws-sdk-s3/plugins/accelerate.rb +17 -64
  100. data/lib/aws-sdk-s3/plugins/access_grants.rb +178 -0
  101. data/lib/aws-sdk-s3/plugins/arn.rb +70 -0
  102. data/lib/aws-sdk-s3/plugins/bucket_dns.rb +7 -43
  103. data/lib/aws-sdk-s3/plugins/bucket_name_restrictions.rb +20 -3
  104. data/lib/aws-sdk-s3/plugins/checksum_algorithm.rb +31 -0
  105. data/lib/aws-sdk-s3/plugins/dualstack.rb +7 -50
  106. data/lib/aws-sdk-s3/plugins/endpoints.rb +86 -0
  107. data/lib/aws-sdk-s3/plugins/expect_100_continue.rb +5 -4
  108. data/lib/aws-sdk-s3/plugins/express_session_auth.rb +88 -0
  109. data/lib/aws-sdk-s3/plugins/get_bucket_location_fix.rb +3 -1
  110. data/lib/aws-sdk-s3/plugins/http_200_errors.rb +62 -17
  111. data/lib/aws-sdk-s3/plugins/iad_regional_endpoint.rb +44 -0
  112. data/lib/aws-sdk-s3/plugins/location_constraint.rb +5 -1
  113. data/lib/aws-sdk-s3/plugins/md5s.rb +14 -67
  114. data/lib/aws-sdk-s3/plugins/redirects.rb +5 -1
  115. data/lib/aws-sdk-s3/plugins/s3_host_id.rb +2 -0
  116. data/lib/aws-sdk-s3/plugins/s3_signer.rb +67 -93
  117. data/lib/aws-sdk-s3/plugins/sse_cpk.rb +3 -1
  118. data/lib/aws-sdk-s3/plugins/streaming_retry.rb +137 -0
  119. data/lib/aws-sdk-s3/plugins/url_encoded_keys.rb +4 -1
  120. data/lib/aws-sdk-s3/presigned_post.rb +160 -99
  121. data/lib/aws-sdk-s3/presigner.rb +178 -81
  122. data/lib/aws-sdk-s3/resource.rb +164 -15
  123. data/lib/aws-sdk-s3/transfer_manager.rb +303 -0
  124. data/lib/aws-sdk-s3/types.rb +15981 -4168
  125. data/lib/aws-sdk-s3/waiters.rb +67 -1
  126. data/lib/aws-sdk-s3.rb +46 -31
  127. data/sig/bucket.rbs +231 -0
  128. data/sig/bucket_acl.rbs +78 -0
  129. data/sig/bucket_cors.rbs +69 -0
  130. data/sig/bucket_lifecycle.rbs +88 -0
  131. data/sig/bucket_lifecycle_configuration.rbs +115 -0
  132. data/sig/bucket_logging.rbs +76 -0
  133. data/sig/bucket_notification.rbs +114 -0
  134. data/sig/bucket_policy.rbs +59 -0
  135. data/sig/bucket_request_payment.rbs +54 -0
  136. data/sig/bucket_tagging.rbs +65 -0
  137. data/sig/bucket_versioning.rbs +77 -0
  138. data/sig/bucket_website.rbs +93 -0
  139. data/sig/client.rbs +2612 -0
  140. data/sig/customizations/bucket.rbs +19 -0
  141. data/sig/customizations/object.rbs +38 -0
  142. data/sig/customizations/object_summary.rbs +35 -0
  143. data/sig/errors.rbs +44 -0
  144. data/sig/multipart_upload.rbs +120 -0
  145. data/sig/multipart_upload_part.rbs +109 -0
  146. data/sig/object.rbs +464 -0
  147. data/sig/object_acl.rbs +86 -0
  148. data/sig/object_summary.rbs +347 -0
  149. data/sig/object_version.rbs +143 -0
  150. data/sig/resource.rbs +141 -0
  151. data/sig/types.rbs +2899 -0
  152. data/sig/waiters.rbs +95 -0
  153. metadata +97 -14
@@ -1,138 +1,317 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pathname'
2
- require 'thread'
4
+ require 'securerandom'
3
5
  require 'set'
4
- require 'tmpdir'
5
6
 
6
7
  module Aws
7
8
  module S3
8
9
  # @api private
9
10
  class FileDownloader
10
-
11
11
  MIN_CHUNK_SIZE = 5 * 1024 * 1024
12
12
  MAX_PARTS = 10_000
13
- 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)
14
15
 
15
16
  def initialize(options = {})
16
17
  @client = options[:client] || Client.new
18
+ @executor = options[:executor]
17
19
  end
18
20
 
19
21
  # @return [Client]
20
22
  attr_reader :client
21
23
 
22
24
  def download(destination, options = {})
23
- @path = destination
24
- @mode = options[:mode] || "auto"
25
- @thread_count = options[:thread_count] || THREAD_COUNT
26
- @chunk_size = options[:chunk_size]
27
- @bucket = options[:bucket]
28
- @key = options[:key]
29
-
30
- case @mode
31
- when "auto" then multipart_download
32
- when "single_request" then single_request
33
- when "get_range"
34
- if @chunk_size
35
- resp = @client.head_object(bucket: @bucket, key: @key)
36
- multithreaded_get_by_ranges(construct_chunks(resp.content_length))
37
- else
38
- msg = "In :get_range mode, :chunk_size must be provided"
39
- raise ArgumentError, msg
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)
40
34
  end
41
- else
42
- msg = "Invalid mode #{@mode} provided, "\
43
- "mode should be :single_request, :get_range or :auto"
44
- raise ArgumentError, msg
45
35
  end
36
+ File.rename(opts[:temp_path], destination) if opts[:temp_path]
37
+ ensure
38
+ cleanup_temp_file(opts)
46
39
  end
47
40
 
48
41
  private
49
42
 
50
- def multipart_download
51
- resp = @client.head_object(bucket: @bucket, key: @key, part_number: 1)
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
52
+ }
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
86
+ end
87
+ end
88
+
89
+ download_attempts.times { completion_queue.pop }
90
+ raise error unless error.nil?
91
+ end
92
+
93
+ def handle_checksum_mode_option(option_key, opts)
94
+ return false unless option_key == :checksum_mode && opts[:checksum_mode] == 'DISABLED'
95
+
96
+ msg = ':checksum_mode option is deprecated. Checksums will be validated by default. ' \
97
+ 'To disable checksum validation, set :response_checksum_validation to "when_required" on your S3 client.'
98
+ warn(msg)
99
+ true
100
+ end
101
+
102
+ def get_opts(opts)
103
+ GET_OPTIONS.each_with_object({}) do |k, h|
104
+ next if k == :checksum_mode
105
+
106
+ h[k] = opts[k] if opts.key?(k)
107
+ end
108
+ end
109
+
110
+ def head_opts(opts)
111
+ HEAD_OPTIONS.each_with_object({}) do |k, h|
112
+ next if handle_checksum_mode_option(k, opts)
113
+
114
+ h[k] = opts[k] if opts.key?(k)
115
+ end
116
+ end
117
+
118
+ def compute_chunk(chunk_size, file_size)
119
+ raise ArgumentError, ":chunk_size shouldn't exceed total file size." if chunk_size && chunk_size > file_size
120
+
121
+ chunk_size || [(file_size.to_f / MAX_PARTS).ceil, MIN_CHUNK_SIZE].max.to_i
122
+ end
123
+
124
+ def compute_mode(file_size, total_parts, etag, opts)
125
+ chunk_size = compute_chunk(opts[:chunk_size], file_size)
126
+ part_size = (file_size.to_f / total_parts).ceil
127
+
128
+ resolve_temp_path(opts)
129
+ if chunk_size < part_size
130
+ multithreaded_get_by_ranges(file_size, etag, opts)
131
+ else
132
+ multithreaded_get_by_parts(total_parts, file_size, etag, opts)
133
+ end
134
+ end
135
+
136
+ def extract_range(value)
137
+ value.match(%r{bytes (?<range>\d+-\d+)/\d+})[:range]
138
+ end
139
+
140
+ def multipart_download(opts)
141
+ resp = @client.head_object(head_opts(opts[:params].merge(part_number: 1)))
52
142
  count = resp.parts_count
143
+
53
144
  if count.nil? || count <= 1
54
- resp.content_length < MIN_CHUNK_SIZE ?
55
- single_request :
56
- multithreaded_get_by_ranges(construct_chunks(resp.content_length))
145
+ if resp.content_length <= MIN_CHUNK_SIZE
146
+ single_request(opts)
147
+ else
148
+ resolve_temp_path(opts)
149
+ multithreaded_get_by_ranges(resp.content_length, resp.etag, opts)
150
+ end
57
151
  else
58
- # partNumber is an option
59
- resp = @client.head_object(bucket: @bucket, key: @key)
60
- resp.content_length < MIN_CHUNK_SIZE ?
61
- single_request :
62
- compute_mode(resp.content_length, count)
152
+ # covers cases when given object is not uploaded via UploadPart API
153
+ resp = @client.head_object(head_opts(opts[:params])) # partNumber is an option
154
+ if resp.content_length <= MIN_CHUNK_SIZE
155
+ single_request(opts)
156
+ else
157
+ compute_mode(resp.content_length, count, resp.etag, opts)
158
+ end
63
159
  end
64
160
  end
65
161
 
66
- def compute_mode(file_size, count)
67
- chunk_size = compute_chunk(file_size)
68
- part_size = (file_size.to_f / count.to_f).ceil
69
- if chunk_size < part_size
70
- multithreaded_get_by_ranges(construct_chunks(file_size))
71
- else
72
- multithreaded_get_by_parts(count)
162
+ def multithreaded_get_by_parts(total_parts, file_size, etag, opts)
163
+ parts = (1..total_parts).map do |part|
164
+ params = get_opts(opts[:params].merge(part_number: part, if_match: etag))
165
+ Part.new(part_number: part, params: params)
73
166
  end
167
+ download_with_executor(PartList.new(parts), file_size, opts)
74
168
  end
75
169
 
76
- def construct_chunks(file_size)
170
+ def multithreaded_get_by_ranges(file_size, etag, opts)
77
171
  offset = 0
78
- default_chunk_size = compute_chunk(file_size)
172
+ default_chunk_size = compute_chunk(opts[:chunk_size], file_size)
79
173
  chunks = []
80
- while offset <= file_size
174
+ part_number = 1 # parts start at 1
175
+ while offset < file_size
81
176
  progress = offset + default_chunk_size
82
- chunks << "bytes=#{offset}-#{progress < file_size ? progress : file_size}"
83
- offset = progress + 1
177
+ progress = file_size if progress > file_size
178
+ params = get_opts(opts[:params].merge(range: "bytes=#{offset}-#{progress - 1}", if_match: etag))
179
+ chunks << Part.new(part_number: part_number, size: (progress - offset), params: params)
180
+ part_number += 1
181
+ offset = progress
84
182
  end
85
- chunks
183
+ download_with_executor(PartList.new(chunks), file_size, opts)
86
184
  end
87
185
 
88
- def compute_chunk(file_size)
89
- if @chunk_size && @chunk_size > file_size
90
- raise ArgumentError, ":chunk_size shouldn't exceed total file size."
91
- else
92
- @chunk_size || [(file_size.to_f / MAX_PARTS).ceil, MIN_CHUNK_SIZE].max.to_i
93
- end
186
+ def range_request(opts)
187
+ resp = @client.head_object(head_opts(opts[:params]))
188
+ resolve_temp_path(opts)
189
+ multithreaded_get_by_ranges(resp.content_length, resp.etag, opts)
94
190
  end
95
191
 
96
- def batches(chunks, mode)
97
- chunks = (1..chunks) if mode.eql? 'part_number'
98
- chunks.each_slice(@thread_count).to_a
192
+ def resolve_temp_path(opts)
193
+ return if [File, Tempfile].include?(opts[:destination].class)
194
+
195
+ opts[:temp_path] ||= "#{opts[:destination]}.s3tmp.#{SecureRandom.alphanumeric(8)}"
99
196
  end
100
197
 
101
- def multithreaded_get_by_ranges(chunks)
102
- thread_batches(chunks, 'range')
198
+ def single_request(opts)
199
+ params = get_opts(opts[:params]).merge(response_target: opts[:destination])
200
+ params[:on_chunk_received] = single_part_progress(opts) if opts[:progress_callback]
201
+ resp = @client.get_object(params)
202
+ return resp unless opts[:on_checksum_validated]
203
+
204
+ opts[:on_checksum_validated].call(resp.checksum_validated, resp) if resp.checksum_validated
205
+ resp
103
206
  end
104
207
 
105
- def multithreaded_get_by_parts(parts)
106
- thread_batches(parts, 'part_number')
208
+ def single_part_progress(opts)
209
+ proc do |_chunk, bytes_read, total_size|
210
+ opts[:progress_callback].call([bytes_read], [total_size], total_size)
211
+ end
107
212
  end
108
213
 
109
- def thread_batches(chunks, param)
110
- batches(chunks, param).each do |batch|
111
- threads = []
112
- batch.each do |chunk|
113
- threads << Thread.new do
114
- resp = @client.get_object(
115
- :bucket => @bucket,
116
- :key => @key,
117
- param.to_sym => chunk
118
- )
119
- write(resp)
120
- end
214
+ def update_progress(progress, part)
215
+ return unless progress.progress_callback
216
+
217
+ part.params[:on_chunk_received] =
218
+ proc do |_chunk, bytes, total|
219
+ progress.call(part.part_number, bytes, total)
121
220
  end
122
- threads.each(&:join)
221
+ end
222
+
223
+ def execute_checksum_callback(resp, opts)
224
+ return unless opts[:on_checksum_validated] && resp.checksum_validated
225
+
226
+ opts[:on_checksum_validated].call(resp.checksum_validated, resp)
227
+ end
228
+
229
+ def validate_destination!(destination)
230
+ valid_types = [String, Pathname, File, Tempfile]
231
+ return if valid_types.include?(destination.class)
232
+
233
+ raise ArgumentError, "Invalid destination, expected #{valid_types.join(', ')} but got: #{destination.class}"
234
+ end
235
+
236
+ def validate_opts!(opts)
237
+ if opts[:on_checksum_validated] && !opts[:on_checksum_validated].respond_to?(:call)
238
+ raise ArgumentError, ':on_checksum_validated must be callable'
239
+ end
240
+
241
+ valid_modes = %w[auto get_range single_request]
242
+ unless valid_modes.include?(opts[:mode])
243
+ msg = "Invalid mode #{opts[:mode]} provided, :mode should be single_request, get_range or auto"
244
+ raise ArgumentError, msg
245
+ end
246
+
247
+ if opts[:mode] == 'get_range' && opts[:chunk_size].nil?
248
+ raise ArgumentError, 'In get_range mode, :chunk_size must be provided'
123
249
  end
250
+
251
+ if opts[:chunk_size] && opts[:chunk_size] <= 0
252
+ raise ArgumentError, ':chunk_size must be positive'
253
+ end
254
+ end
255
+
256
+ def validate_range(actual, expected)
257
+ return if actual == expected.match(/bytes=(?<range>\d+-\d+)/)[:range]
258
+
259
+ raise MultipartDownloadError, "multipart download failed: expected range of #{expected} but got #{actual}"
124
260
  end
125
261
 
126
- def write(resp)
127
- range, _ = resp.content_range.split(" ").last.split("/")
128
- head, _ = range.split("-").map {|s| s.to_i}
129
- IO.write(@path, resp.body.read, head)
262
+ def write(body, range, opts)
263
+ path = opts[:temp_path] || opts[:destination]
264
+ File.write(path, body.read, range.split('-').first.to_i)
265
+ end
266
+
267
+ # @api private
268
+ class Part < Struct.new(:part_number, :size, :params)
269
+ include Aws::Structure
270
+ end
271
+
272
+ # @api private
273
+ class PartList
274
+ include Enumerable
275
+ def initialize(parts = [])
276
+ @parts = parts
277
+ @mutex = Mutex.new
278
+ end
279
+
280
+ def shift
281
+ @mutex.synchronize { @parts.shift }
282
+ end
283
+
284
+ def size
285
+ @mutex.synchronize { @parts.size }
286
+ end
287
+
288
+ def clear!
289
+ @mutex.synchronize { @parts.clear }
290
+ end
291
+
292
+ def each(&block)
293
+ @mutex.synchronize { @parts.each(&block) }
294
+ end
130
295
  end
131
296
 
132
- def single_request
133
- @client.get_object(
134
- bucket: @bucket, key: @key, response_target: @path
135
- )
297
+ # @api private
298
+ class MultipartProgress
299
+ def initialize(parts, total_size, progress_callback)
300
+ @bytes_received = Array.new(parts.size, 0)
301
+ @part_sizes = parts.map(&:size)
302
+ @total_size = total_size
303
+ @progress_callback = progress_callback
304
+ end
305
+
306
+ attr_reader :progress_callback
307
+
308
+ def call(part_number, bytes_received, total)
309
+ # part numbers start at 1
310
+ @bytes_received[part_number - 1] = bytes_received
311
+ # part size may not be known until we get the first response
312
+ @part_sizes[part_number - 1] ||= total
313
+ @progress_callback.call(@bytes_received, @part_sizes, @total_size)
314
+ end
136
315
  end
137
316
  end
138
317
  end
@@ -1,15 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Aws
2
4
  module S3
3
5
 
4
- # A utility class that provides an IO-like interface to a portion of
5
- # a file on disk.
6
+ # A utility class that provides an IO-like interface to a portion of a file
7
+ # on disk.
6
8
  # @api private
7
9
  class FilePart
8
10
 
9
- # @option options [required,String,Pathname,File,Tempfile] :source
10
- # @option options [required,Integer] :offset The file part will read
11
+ # @option options [required, String, Pathname, File, Tempfile] :source
12
+ # The file to upload.
13
+ #
14
+ # @option options [required, Integer] :offset The file part will read
11
15
  # starting at this byte offset.
12
- # @option options [required,Integer] :size The maximum number of bytes to
16
+ #
17
+ # @option options [required, Integer] :size The maximum number of bytes to
13
18
  # read from the `:offset`.
14
19
  def initialize(options = {})
15
20
  @source = options[:source]
@@ -19,7 +24,7 @@ module Aws
19
24
  @file = nil
20
25
  end
21
26
 
22
- # @return [String,Pathname,File,Tempfile]
27
+ # @return [String, Pathname, File, Tempfile]
23
28
  attr_reader :source
24
29
 
25
30
  # @return [Integer]
@@ -56,14 +61,12 @@ module Aws
56
61
  end
57
62
 
58
63
  def read_from_file(bytes, output_buffer)
59
- if bytes
60
- data = @file.read([remaining_bytes, bytes].min)
61
- data = nil if data == ''
62
- else
63
- data = @file.read(remaining_bytes)
64
- end
64
+ length = [remaining_bytes, *bytes].min
65
+ data = @file.read(length, output_buffer)
66
+
65
67
  @position += data ? data.bytesize : 0
66
- output_buffer ? output_buffer.replace(data || '') : data
68
+
69
+ data.to_s unless bytes && (data.nil? || data.empty?)
67
70
  end
68
71
 
69
72
  def remaining_bytes
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'pathname'
2
4
 
3
5
  module Aws
@@ -5,54 +7,67 @@ module Aws
5
7
  # @api private
6
8
  class FileUploader
7
9
 
8
- FIFTEEN_MEGABYTES = 15 * 1024 * 1024
10
+ DEFAULT_MULTIPART_THRESHOLD = 100 * 1024 * 1024
9
11
 
12
+ # @param [Hash] options
10
13
  # @option options [Client] :client
11
- # @option options [Integer] :multipart_threshold Files greater than
12
- # `:multipart_threshold` bytes are uploaded using S3 multipart APIs.
14
+ # @option options [Integer] :multipart_threshold (104857600)
13
15
  def initialize(options = {})
14
- @options = options
15
16
  @client = options[:client] || Client.new
16
- @multipart_threshold = options[:multipart_threshold] || FIFTEEN_MEGABYTES
17
+ @executor = options[:executor]
18
+ @multipart_threshold = options[:multipart_threshold] || DEFAULT_MULTIPART_THRESHOLD
17
19
  end
18
20
 
19
21
  # @return [Client]
20
22
  attr_reader :client
21
23
 
22
- # @return [Integer] Files larger than this in bytes are uploaded
23
- # using a {MultipartFileUploader}.
24
+ # @return [Integer] Files larger than or equal to this in bytes are uploaded using a {MultipartFileUploader}.
24
25
  attr_reader :multipart_threshold
25
26
 
26
- # @param [String,Pathname,File,Tempfile] source
27
- # @option options [required,String] :bucket
28
- # @option options [required,String] :key
27
+ # @param [String, Pathname, File, Tempfile] source The file to upload.
28
+ # @option options [required, String] :bucket The bucket to upload to.
29
+ # @option options [required, String] :key The key for the object.
30
+ # @option options [Proc] :progress_callback
31
+ # A Proc that will be called when each chunk of the upload is sent.
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.
29
36
  # @return [void]
30
37
  def upload(source, options = {})
31
- if File.size(source) >= multipart_threshold
32
- MultipartFileUploader.new(@options).upload(source, options)
33
- else
34
- 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
35
44
  end
36
45
  end
37
46
 
38
47
  private
39
48
 
49
+ def open_file(source, &block)
50
+ if source.is_a?(String) || source.is_a?(Pathname)
51
+ File.open(source, 'rb', &block)
52
+ else
53
+ yield(source)
54
+ end
55
+ end
56
+
40
57
  def put_object(source, options)
58
+ if (callback = options.delete(:progress_callback))
59
+ options[:on_chunk_sent] = single_part_progress(callback)
60
+ end
41
61
  open_file(source) do |file|
42
62
  @client.put_object(options.merge(body: file))
43
63
  end
44
64
  end
45
65
 
46
- def open_file(source)
47
- if String === source || Pathname === source
48
- file = File.open(source, 'rb')
49
- yield(file)
50
- file.close
51
- else
52
- yield(source)
66
+ def single_part_progress(progress_callback)
67
+ proc do |_chunk, bytes_read, total_size|
68
+ progress_callback.call([bytes_read], [total_size])
53
69
  end
54
70
  end
55
-
56
71
  end
57
72
  end
58
73
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
  require 'time'
3
5
  require 'openssl'
4
- require 'cgi'
5
- require 'webrick/httputils'
6
+ require "cgi/escape"
7
+ require "cgi/util" if RUBY_VERSION < "3.5"
6
8
  require 'aws-sdk-core/query'
7
9
 
8
10
  module Aws
@@ -155,33 +157,24 @@ module Aws
155
157
  end
156
158
 
157
159
  def uri_escape(s)
158
-
159
160
  #URI.escape(s)
160
161
 
161
- # URI.escape is deprecated, replacing it with escape from webrick
162
- # to squelch the massive number of warnings generated from Ruby.
163
- # The following script was used to determine the differences
164
- # between the various escape methods available. The webrick
165
- # escape only had two differences and it is available in the
166
- # standard lib.
167
- #
168
- # (0..255).each {|c|
169
- # s = [c].pack("C")
170
- # e = [
171
- # CGI.escape(s),
172
- # ERB::Util.url_encode(s),
173
- # URI.encode_www_form_component(s),
174
- # WEBrick::HTTPUtils.escape_form(s),
175
- # WEBrick::HTTPUtils.escape(s),
176
- # URI.escape(s),
177
- # ]
178
- # next if e.uniq.length == 1
179
- # puts("%5s %5s %5s %5s %5s %5s %5s" % ([s.inspect] + e))
180
- # }
181
- #
182
- WEBrick::HTTPUtils.escape(s).gsub('%5B', '[').gsub('%5D', ']')
162
+ # (0..255).each {|c|
163
+ # s = [c].pack("C")
164
+ # e = [
165
+ # CGI.escape(s),
166
+ # ERB::Util.url_encode(s),
167
+ # URI.encode_www_form_component(s),
168
+ # WEBrick::HTTPUtils.escape_form(s),
169
+ # WEBrick::HTTPUtils.escape(s),
170
+ # URI.escape(s),
171
+ # URI::DEFAULT_PARSER.escape(s)
172
+ # ]
173
+ # next if e.uniq.length == 1
174
+ # puts("%5s %5s %5s %5s %5s %5s %5s %5s" % ([s.inspect] + e))
175
+ # }
176
+ URI::DEFAULT_PARSER.escape(s)
183
177
  end
184
-
185
178
  end
186
179
  end
187
180
  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