aws-sdk-s3 1.176.1 → 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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +202 -0
  3. data/VERSION +1 -1
  4. data/lib/aws-sdk-s3/bucket.rb +86 -35
  5. data/lib/aws-sdk-s3/bucket_acl.rb +7 -6
  6. data/lib/aws-sdk-s3/bucket_cors.rb +6 -5
  7. data/lib/aws-sdk-s3/bucket_lifecycle.rb +2 -2
  8. data/lib/aws-sdk-s3/bucket_lifecycle_configuration.rb +3 -3
  9. data/lib/aws-sdk-s3/bucket_logging.rb +2 -2
  10. data/lib/aws-sdk-s3/bucket_policy.rb +6 -5
  11. data/lib/aws-sdk-s3/bucket_request_payment.rb +3 -3
  12. data/lib/aws-sdk-s3/bucket_tagging.rb +3 -3
  13. data/lib/aws-sdk-s3/bucket_versioning.rb +42 -9
  14. data/lib/aws-sdk-s3/bucket_website.rb +3 -3
  15. data/lib/aws-sdk-s3/client.rb +3038 -1226
  16. data/lib/aws-sdk-s3/client_api.rb +492 -164
  17. data/lib/aws-sdk-s3/customizations/object.rb +76 -86
  18. data/lib/aws-sdk-s3/customizations.rb +4 -1
  19. data/lib/aws-sdk-s3/default_executor.rb +103 -0
  20. data/lib/aws-sdk-s3/encryption/client.rb +2 -2
  21. data/lib/aws-sdk-s3/encryption/default_cipher_provider.rb +2 -0
  22. data/lib/aws-sdk-s3/encryption/encrypt_handler.rb +2 -0
  23. data/lib/aws-sdk-s3/encryption/kms_cipher_provider.rb +2 -0
  24. data/lib/aws-sdk-s3/encryptionV2/client.rb +98 -23
  25. data/lib/aws-sdk-s3/encryptionV2/decrypt_handler.rb +7 -162
  26. data/lib/aws-sdk-s3/encryptionV2/decryption.rb +205 -0
  27. data/lib/aws-sdk-s3/encryptionV2/default_cipher_provider.rb +17 -0
  28. data/lib/aws-sdk-s3/encryptionV2/encrypt_handler.rb +2 -0
  29. data/lib/aws-sdk-s3/encryptionV2/io_encrypter.rb +2 -0
  30. data/lib/aws-sdk-s3/encryptionV2/kms_cipher_provider.rb +8 -0
  31. data/lib/aws-sdk-s3/encryptionV2/utils.rb +5 -0
  32. data/lib/aws-sdk-s3/encryptionV3/client.rb +885 -0
  33. data/lib/aws-sdk-s3/encryptionV3/decrypt_handler.rb +98 -0
  34. data/lib/aws-sdk-s3/encryptionV3/decryption.rb +244 -0
  35. data/lib/aws-sdk-s3/encryptionV3/default_cipher_provider.rb +159 -0
  36. data/lib/aws-sdk-s3/encryptionV3/default_key_provider.rb +35 -0
  37. data/lib/aws-sdk-s3/encryptionV3/encrypt_handler.rb +98 -0
  38. data/lib/aws-sdk-s3/encryptionV3/errors.rb +47 -0
  39. data/lib/aws-sdk-s3/encryptionV3/io_auth_decrypter.rb +60 -0
  40. data/lib/aws-sdk-s3/encryptionV3/io_decrypter.rb +35 -0
  41. data/lib/aws-sdk-s3/encryptionV3/io_encrypter.rb +84 -0
  42. data/lib/aws-sdk-s3/encryptionV3/key_provider.rb +28 -0
  43. data/lib/aws-sdk-s3/encryptionV3/kms_cipher_provider.rb +159 -0
  44. data/lib/aws-sdk-s3/encryptionV3/materials.rb +58 -0
  45. data/lib/aws-sdk-s3/encryptionV3/utils.rb +321 -0
  46. data/lib/aws-sdk-s3/encryption_v2.rb +1 -0
  47. data/lib/aws-sdk-s3/encryption_v3.rb +24 -0
  48. data/lib/aws-sdk-s3/endpoint_parameters.rb +17 -17
  49. data/lib/aws-sdk-s3/endpoint_provider.rb +562 -304
  50. data/lib/aws-sdk-s3/endpoints.rb +110 -0
  51. data/lib/aws-sdk-s3/errors.rb +11 -0
  52. data/lib/aws-sdk-s3/file_downloader.rb +189 -143
  53. data/lib/aws-sdk-s3/file_uploader.rb +9 -13
  54. data/lib/aws-sdk-s3/legacy_signer.rb +2 -1
  55. data/lib/aws-sdk-s3/multipart_download_error.rb +8 -0
  56. data/lib/aws-sdk-s3/multipart_file_uploader.rb +105 -102
  57. data/lib/aws-sdk-s3/multipart_stream_uploader.rb +96 -107
  58. data/lib/aws-sdk-s3/multipart_upload.rb +50 -6
  59. data/lib/aws-sdk-s3/multipart_upload_error.rb +3 -4
  60. data/lib/aws-sdk-s3/multipart_upload_part.rb +50 -34
  61. data/lib/aws-sdk-s3/object.rb +264 -137
  62. data/lib/aws-sdk-s3/object_acl.rb +12 -6
  63. data/lib/aws-sdk-s3/object_multipart_copier.rb +2 -1
  64. data/lib/aws-sdk-s3/object_summary.rb +179 -103
  65. data/lib/aws-sdk-s3/object_version.rb +25 -23
  66. data/lib/aws-sdk-s3/plugins/checksum_algorithm.rb +31 -0
  67. data/lib/aws-sdk-s3/plugins/endpoints.rb +1 -1
  68. data/lib/aws-sdk-s3/plugins/express_session_auth.rb +11 -20
  69. data/lib/aws-sdk-s3/plugins/md5s.rb +10 -71
  70. data/lib/aws-sdk-s3/plugins/streaming_retry.rb +5 -7
  71. data/lib/aws-sdk-s3/plugins/url_encoded_keys.rb +2 -1
  72. data/lib/aws-sdk-s3/presigner.rb +4 -5
  73. data/lib/aws-sdk-s3/resource.rb +7 -1
  74. data/lib/aws-sdk-s3/transfer_manager.rb +303 -0
  75. data/lib/aws-sdk-s3/types.rb +2907 -1059
  76. data/lib/aws-sdk-s3.rb +1 -1
  77. data/sig/bucket.rbs +16 -6
  78. data/sig/bucket_acl.rbs +1 -1
  79. data/sig/bucket_cors.rbs +1 -1
  80. data/sig/bucket_lifecycle.rbs +1 -1
  81. data/sig/bucket_lifecycle_configuration.rbs +1 -1
  82. data/sig/bucket_logging.rbs +1 -1
  83. data/sig/bucket_policy.rbs +1 -1
  84. data/sig/bucket_request_payment.rbs +1 -1
  85. data/sig/bucket_tagging.rbs +1 -1
  86. data/sig/bucket_versioning.rbs +3 -3
  87. data/sig/bucket_website.rbs +1 -1
  88. data/sig/client.rbs +226 -64
  89. data/sig/errors.rbs +2 -0
  90. data/sig/multipart_upload.rbs +9 -2
  91. data/sig/multipart_upload_part.rbs +5 -1
  92. data/sig/object.rbs +31 -15
  93. data/sig/object_acl.rbs +1 -1
  94. data/sig/object_summary.rbs +22 -15
  95. data/sig/object_version.rbs +5 -2
  96. data/sig/resource.rbs +11 -2
  97. data/sig/types.rbs +281 -64
  98. metadata +26 -10
  99. data/lib/aws-sdk-s3/plugins/skip_whole_multipart_get_checksums.rb +0 -31
@@ -63,6 +63,18 @@ module Aws::S3
63
63
  end
64
64
  end
65
65
 
66
+ class CreateBucketMetadataConfiguration
67
+ def self.build(context)
68
+ Aws::S3::EndpointParameters.create(
69
+ context.config,
70
+ bucket: context.params[:bucket],
71
+ use_dual_stack: context[:use_dualstack_endpoint],
72
+ accelerate: context[:use_accelerate_endpoint],
73
+ use_s3_express_control_endpoint: true,
74
+ )
75
+ end
76
+ end
77
+
66
78
  class CreateBucketMetadataTableConfiguration
67
79
  def self.build(context)
68
80
  Aws::S3::EndpointParameters.create(
@@ -183,6 +195,18 @@ module Aws::S3
183
195
  end
184
196
  end
185
197
 
198
+ class DeleteBucketMetadataConfiguration
199
+ def self.build(context)
200
+ Aws::S3::EndpointParameters.create(
201
+ context.config,
202
+ bucket: context.params[:bucket],
203
+ use_dual_stack: context[:use_dualstack_endpoint],
204
+ accelerate: context[:use_accelerate_endpoint],
205
+ use_s3_express_control_endpoint: true,
206
+ )
207
+ end
208
+ end
209
+
186
210
  class DeleteBucketMetadataTableConfiguration
187
211
  def self.build(context)
188
212
  Aws::S3::EndpointParameters.create(
@@ -313,6 +337,17 @@ module Aws::S3
313
337
  end
314
338
  end
315
339
 
340
+ class GetBucketAbac
341
+ def self.build(context)
342
+ Aws::S3::EndpointParameters.create(
343
+ context.config,
344
+ bucket: context.params[:bucket],
345
+ use_dual_stack: context[:use_dualstack_endpoint],
346
+ accelerate: context[:use_accelerate_endpoint],
347
+ )
348
+ end
349
+ end
350
+
316
351
  class GetBucketAccelerateConfiguration
317
352
  def self.build(context)
318
353
  Aws::S3::EndpointParameters.create(
@@ -445,6 +480,18 @@ module Aws::S3
445
480
  end
446
481
  end
447
482
 
483
+ class GetBucketMetadataConfiguration
484
+ def self.build(context)
485
+ Aws::S3::EndpointParameters.create(
486
+ context.config,
487
+ bucket: context.params[:bucket],
488
+ use_dual_stack: context[:use_dualstack_endpoint],
489
+ accelerate: context[:use_accelerate_endpoint],
490
+ use_s3_express_control_endpoint: true,
491
+ )
492
+ end
493
+ end
494
+
448
495
  class GetBucketMetadataTableConfiguration
449
496
  def self.build(context)
450
497
  Aws::S3::EndpointParameters.create(
@@ -842,6 +889,17 @@ module Aws::S3
842
889
  end
843
890
  end
844
891
 
892
+ class PutBucketAbac
893
+ def self.build(context)
894
+ Aws::S3::EndpointParameters.create(
895
+ context.config,
896
+ bucket: context.params[:bucket],
897
+ use_dual_stack: context[:use_dualstack_endpoint],
898
+ accelerate: context[:use_accelerate_endpoint],
899
+ )
900
+ end
901
+ end
902
+
845
903
  class PutBucketAccelerateConfiguration
846
904
  def self.build(context)
847
905
  Aws::S3::EndpointParameters.create(
@@ -1162,6 +1220,18 @@ module Aws::S3
1162
1220
  end
1163
1221
  end
1164
1222
 
1223
+ class RenameObject
1224
+ def self.build(context)
1225
+ Aws::S3::EndpointParameters.create(
1226
+ context.config,
1227
+ bucket: context.params[:bucket],
1228
+ use_dual_stack: context[:use_dualstack_endpoint],
1229
+ accelerate: context[:use_accelerate_endpoint],
1230
+ key: context.params[:key],
1231
+ )
1232
+ end
1233
+ end
1234
+
1165
1235
  class RestoreObject
1166
1236
  def self.build(context)
1167
1237
  Aws::S3::EndpointParameters.create(
@@ -1184,6 +1254,30 @@ module Aws::S3
1184
1254
  end
1185
1255
  end
1186
1256
 
1257
+ class UpdateBucketMetadataInventoryTableConfiguration
1258
+ def self.build(context)
1259
+ Aws::S3::EndpointParameters.create(
1260
+ context.config,
1261
+ bucket: context.params[:bucket],
1262
+ use_dual_stack: context[:use_dualstack_endpoint],
1263
+ accelerate: context[:use_accelerate_endpoint],
1264
+ use_s3_express_control_endpoint: true,
1265
+ )
1266
+ end
1267
+ end
1268
+
1269
+ class UpdateBucketMetadataJournalTableConfiguration
1270
+ def self.build(context)
1271
+ Aws::S3::EndpointParameters.create(
1272
+ context.config,
1273
+ bucket: context.params[:bucket],
1274
+ use_dual_stack: context[:use_dualstack_endpoint],
1275
+ accelerate: context[:use_accelerate_endpoint],
1276
+ use_s3_express_control_endpoint: true,
1277
+ )
1278
+ end
1279
+ end
1280
+
1187
1281
  class UploadPart
1188
1282
  def self.build(context)
1189
1283
  Aws::S3::EndpointParameters.create(
@@ -1230,6 +1324,8 @@ module Aws::S3
1230
1324
  CopyObject.build(context)
1231
1325
  when :create_bucket
1232
1326
  CreateBucket.build(context)
1327
+ when :create_bucket_metadata_configuration
1328
+ CreateBucketMetadataConfiguration.build(context)
1233
1329
  when :create_bucket_metadata_table_configuration
1234
1330
  CreateBucketMetadataTableConfiguration.build(context)
1235
1331
  when :create_multipart_upload
@@ -1250,6 +1346,8 @@ module Aws::S3
1250
1346
  DeleteBucketInventoryConfiguration.build(context)
1251
1347
  when :delete_bucket_lifecycle
1252
1348
  DeleteBucketLifecycle.build(context)
1349
+ when :delete_bucket_metadata_configuration
1350
+ DeleteBucketMetadataConfiguration.build(context)
1253
1351
  when :delete_bucket_metadata_table_configuration
1254
1352
  DeleteBucketMetadataTableConfiguration.build(context)
1255
1353
  when :delete_bucket_metrics_configuration
@@ -1272,6 +1370,8 @@ module Aws::S3
1272
1370
  DeleteObjects.build(context)
1273
1371
  when :delete_public_access_block
1274
1372
  DeletePublicAccessBlock.build(context)
1373
+ when :get_bucket_abac
1374
+ GetBucketAbac.build(context)
1275
1375
  when :get_bucket_accelerate_configuration
1276
1376
  GetBucketAccelerateConfiguration.build(context)
1277
1377
  when :get_bucket_acl
@@ -1294,6 +1394,8 @@ module Aws::S3
1294
1394
  GetBucketLocation.build(context)
1295
1395
  when :get_bucket_logging
1296
1396
  GetBucketLogging.build(context)
1397
+ when :get_bucket_metadata_configuration
1398
+ GetBucketMetadataConfiguration.build(context)
1297
1399
  when :get_bucket_metadata_table_configuration
1298
1400
  GetBucketMetadataTableConfiguration.build(context)
1299
1401
  when :get_bucket_metrics_configuration
@@ -1362,6 +1464,8 @@ module Aws::S3
1362
1464
  ListObjectsV2.build(context)
1363
1465
  when :list_parts
1364
1466
  ListParts.build(context)
1467
+ when :put_bucket_abac
1468
+ PutBucketAbac.build(context)
1365
1469
  when :put_bucket_accelerate_configuration
1366
1470
  PutBucketAccelerateConfiguration.build(context)
1367
1471
  when :put_bucket_acl
@@ -1416,10 +1520,16 @@ module Aws::S3
1416
1520
  PutObjectTagging.build(context)
1417
1521
  when :put_public_access_block
1418
1522
  PutPublicAccessBlock.build(context)
1523
+ when :rename_object
1524
+ RenameObject.build(context)
1419
1525
  when :restore_object
1420
1526
  RestoreObject.build(context)
1421
1527
  when :select_object_content
1422
1528
  SelectObjectContent.build(context)
1529
+ when :update_bucket_metadata_inventory_table_configuration
1530
+ UpdateBucketMetadataInventoryTableConfiguration.build(context)
1531
+ when :update_bucket_metadata_journal_table_configuration
1532
+ UpdateBucketMetadataJournalTableConfiguration.build(context)
1423
1533
  when :upload_part
1424
1534
  UploadPart.build(context)
1425
1535
  when :upload_part_copy
@@ -30,6 +30,7 @@ module Aws::S3
30
30
  # * {BucketAlreadyExists}
31
31
  # * {BucketAlreadyOwnedByYou}
32
32
  # * {EncryptionTypeMismatch}
33
+ # * {IdempotencyParameterMismatch}
33
34
  # * {InvalidObjectState}
34
35
  # * {InvalidRequest}
35
36
  # * {InvalidWriteOffset}
@@ -76,6 +77,16 @@ module Aws::S3
76
77
  end
77
78
  end
78
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
+
79
90
  class InvalidObjectState < ServiceError
80
91
 
81
92
  # @param [Seahorse::Client::RequestContext] context
@@ -1,226 +1,270 @@
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],
32
- }
33
- @params[:version_id] = options[:version_id] if options[:version_id]
25
+ validate_destination!(destination)
26
+ opts = build_download_opts(destination, options)
27
+ validate_opts!(opts)
34
28
 
35
- # checksum_mode only supports the value "ENABLED"
36
- # falsey values (false/nil) or "DISABLED" should be considered
37
- # disabled and the api parameter should be unset.
38
- if (checksum_mode = options.fetch(:checksum_mode, 'ENABLED'))
39
- @params[:checksum_mode] = checksum_mode unless checksum_mode.upcase == 'DISABLED'
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
40
35
  end
41
- @on_checksum_validated = options[:on_checksum_validated]
36
+ File.rename(opts[:temp_path], destination) if opts[:temp_path]
37
+ ensure
38
+ cleanup_temp_file(opts)
39
+ end
42
40
 
43
- @progress_callback = options[:progress_callback]
41
+ private
44
42
 
45
- validate!
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
46
54
 
47
- Aws::Plugins::UserAgent.metric('S3_TRANSFER') do
48
- case @mode
49
- when 'auto' then multipart_download
50
- when 'single_request' then single_request
51
- when 'get_range'
52
- if @chunk_size
53
- resp = @client.head_object(@params)
54
- multithreaded_get_by_ranges(resp.content_length)
55
- else
56
- msg = 'In :get_range mode, :chunk_size must be provided'
57
- raise ArgumentError, msg
58
- end
59
- else
60
- msg = "Invalid mode #{@mode} provided, "\
61
- 'mode should be :single_request, :get_range or :auto'
62
- raise ArgumentError, msg
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
63
86
  end
64
87
  end
88
+
89
+ download_attempts.times { completion_queue.pop }
90
+ raise error unless error.nil?
65
91
  end
66
92
 
67
- private
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
68
105
 
69
- def validate!
70
- if @on_checksum_validated && @params[:checksum_mode] != 'ENABLED'
71
- raise ArgumentError, "You must set checksum_mode: 'ENABLED' " +
72
- "when providing a on_checksum_validated callback"
106
+ h[k] = opts[k] if opts.key?(k)
73
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
74
127
 
75
- if @on_checksum_validated && !@on_checksum_validated.respond_to?(:call)
76
- raise ArgumentError, 'on_checksum_validated must be callable'
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)
77
133
  end
78
134
  end
79
135
 
80
- def multipart_download
81
- resp = @client.head_object(@params.merge(part_number: 1))
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)))
82
142
  count = resp.parts_count
143
+
83
144
  if count.nil? || count <= 1
84
145
  if resp.content_length <= MIN_CHUNK_SIZE
85
- single_request
146
+ single_request(opts)
86
147
  else
87
- multithreaded_get_by_ranges(resp.content_length)
148
+ resolve_temp_path(opts)
149
+ multithreaded_get_by_ranges(resp.content_length, resp.etag, opts)
88
150
  end
89
151
  else
90
- # partNumber is an option
91
- resp = @client.head_object(@params)
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
92
154
  if resp.content_length <= MIN_CHUNK_SIZE
93
- single_request
155
+ single_request(opts)
94
156
  else
95
- compute_mode(resp.content_length, count)
157
+ compute_mode(resp.content_length, count, resp.etag, opts)
96
158
  end
97
159
  end
98
160
  end
99
161
 
100
- def compute_mode(file_size, count)
101
- chunk_size = compute_chunk(file_size)
102
- part_size = (file_size.to_f / count.to_f).ceil
103
- if chunk_size < part_size
104
- multithreaded_get_by_ranges(file_size)
105
- else
106
- multithreaded_get_by_parts(count, file_size)
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)
107
166
  end
167
+ download_with_executor(PartList.new(parts), file_size, opts)
108
168
  end
109
169
 
110
- def construct_chunks(file_size)
170
+ def multithreaded_get_by_ranges(file_size, etag, opts)
111
171
  offset = 0
112
- default_chunk_size = compute_chunk(file_size)
172
+ default_chunk_size = compute_chunk(opts[:chunk_size], file_size)
113
173
  chunks = []
174
+ part_number = 1 # parts start at 1
114
175
  while offset < file_size
115
176
  progress = offset + default_chunk_size
116
177
  progress = file_size if progress > file_size
117
- chunks << "bytes=#{offset}-#{progress - 1}"
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
118
181
  offset = progress
119
182
  end
120
- chunks
183
+ download_with_executor(PartList.new(chunks), file_size, opts)
121
184
  end
122
185
 
123
- def compute_chunk(file_size)
124
- if @chunk_size && @chunk_size > file_size
125
- raise ArgumentError, ":chunk_size shouldn't exceed total file size."
126
- else
127
- @chunk_size || [
128
- (file_size.to_f / MAX_PARTS).ceil, MIN_CHUNK_SIZE
129
- ].max.to_i
130
- 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)
131
190
  end
132
191
 
133
- def batches(chunks, mode)
134
- chunks = (1..chunks) if mode.eql? 'part_number'
135
- 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)}"
136
196
  end
137
197
 
138
- def multithreaded_get_by_ranges(file_size)
139
- offset = 0
140
- default_chunk_size = compute_chunk(file_size)
141
- chunks = []
142
- part_number = 1 # parts start at 1
143
- while offset < file_size
144
- progress = offset + default_chunk_size
145
- progress = file_size if progress > file_size
146
- range = "bytes=#{offset}-#{progress - 1}"
147
- chunks << Part.new(
148
- part_number: part_number,
149
- size: (progress-offset),
150
- params: @params.merge(range: range)
151
- )
152
- part_number += 1
153
- offset = progress
154
- end
155
- download_in_threads(PartList.new(chunks), file_size)
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
156
206
  end
157
207
 
158
- def multithreaded_get_by_parts(n_parts, total_size)
159
- parts = (1..n_parts).map do |part|
160
- Part.new(part_number: part, params: @params.merge(part_number: part))
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)
161
211
  end
162
- download_in_threads(PartList.new(parts), total_size)
163
212
  end
164
213
 
165
- def download_in_threads(pending, total_size)
166
- threads = []
167
- if @progress_callback
168
- progress = MultipartProgress.new(pending, total_size, @progress_callback)
169
- end
170
- @thread_count.times do
171
- thread = Thread.new do
172
- begin
173
- while part = pending.shift
174
- if progress
175
- part.params[:on_chunk_received] =
176
- proc do |_chunk, bytes, total|
177
- progress.call(part.part_number, bytes, total)
178
- end
179
- end
180
- resp = @client.get_object(part.params)
181
- write(resp)
182
- if @on_checksum_validated && resp.checksum_validated
183
- @on_checksum_validated.call(resp.checksum_validated, resp)
184
- end
185
- end
186
- nil
187
- rescue => error
188
- # keep other threads from downloading other parts
189
- pending.clear!
190
- raise error
191
- 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)
192
220
  end
193
- threads << thread
194
- end
195
- threads.map(&:value).compact
196
221
  end
197
222
 
198
- def write(resp)
199
- range, _ = resp.content_range.split(' ').last.split('/')
200
- head, _ = range.split('-').map {|s| s.to_i}
201
- File.write(@path, resp.body.read, head)
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)
202
227
  end
203
228
 
204
- def single_request
205
- params = @params.merge(response_target: @path)
206
- params[:on_chunk_received] = single_part_progress if @progress_callback
207
- resp = @client.get_object(params)
229
+ def validate_destination!(destination)
230
+ valid_types = [String, Pathname, File, Tempfile]
231
+ return if valid_types.include?(destination.class)
208
232
 
209
- return resp unless @on_checksum_validated
233
+ raise ArgumentError, "Invalid destination, expected #{valid_types.join(', ')} but got: #{destination.class}"
234
+ end
210
235
 
211
- if resp.checksum_validated
212
- @on_checksum_validated.call(resp.checksum_validated, resp)
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'
213
239
  end
214
240
 
215
- resp
216
- end
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
217
246
 
218
- def single_part_progress
219
- proc do |_chunk, bytes_read, total_size|
220
- @progress_callback.call([bytes_read], [total_size], total_size)
247
+ if opts[:mode] == 'get_range' && opts[:chunk_size].nil?
248
+ raise ArgumentError, 'In get_range mode, :chunk_size must be provided'
221
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}"
222
260
  end
223
261
 
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
224
268
  class Part < Struct.new(:part_number, :size, :params)
225
269
  include Aws::Structure
226
270
  end
@@ -251,7 +295,7 @@ module Aws
251
295
  end
252
296
 
253
297
  # @api private
254
- class MultipartProgress
298
+ class MultipartProgress
255
299
  def initialize(parts, total_size, progress_callback)
256
300
  @bytes_received = Array.new(parts.size, 0)
257
301
  @part_sizes = parts.map(&:size)
@@ -259,6 +303,8 @@ module Aws
259
303
  @progress_callback = progress_callback
260
304
  end
261
305
 
306
+ attr_reader :progress_callback
307
+
262
308
  def call(part_number, bytes_received, total)
263
309
  # part numbers start at 1
264
310
  @bytes_received[part_number - 1] = bytes_received