aws-sdk-s3 1.136.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +452 -1
  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 +728 -145
  7. data/lib/aws-sdk-s3/bucket_acl.rb +19 -18
  8. data/lib/aws-sdk-s3/bucket_cors.rb +22 -21
  9. data/lib/aws-sdk-s3/bucket_lifecycle.rb +23 -18
  10. data/lib/aws-sdk-s3/bucket_lifecycle_configuration.rb +76 -19
  11. data/lib/aws-sdk-s3/bucket_logging.rb +21 -14
  12. data/lib/aws-sdk-s3/bucket_notification.rb +6 -6
  13. data/lib/aws-sdk-s3/bucket_policy.rb +65 -20
  14. data/lib/aws-sdk-s3/bucket_region_cache.rb +9 -5
  15. data/lib/aws-sdk-s3/bucket_request_payment.rb +15 -15
  16. data/lib/aws-sdk-s3/bucket_tagging.rb +19 -19
  17. data/lib/aws-sdk-s3/bucket_versioning.rb +74 -41
  18. data/lib/aws-sdk-s3/bucket_website.rb +19 -19
  19. data/lib/aws-sdk-s3/client.rb +9951 -3365
  20. data/lib/aws-sdk-s3/client_api.rb +758 -164
  21. data/lib/aws-sdk-s3/customizations/bucket.rb +1 -1
  22. data/lib/aws-sdk-s3/customizations/errors.rb +15 -2
  23. data/lib/aws-sdk-s3/customizations/object.rb +87 -91
  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.rb +28 -31
  27. data/lib/aws-sdk-s3/default_executor.rb +103 -0
  28. data/lib/aws-sdk-s3/encryption/client.rb +4 -4
  29. data/lib/aws-sdk-s3/encryption/default_cipher_provider.rb +2 -0
  30. data/lib/aws-sdk-s3/encryption/encrypt_handler.rb +2 -0
  31. data/lib/aws-sdk-s3/encryption/kms_cipher_provider.rb +4 -2
  32. data/lib/aws-sdk-s3/encryptionV2/client.rb +100 -25
  33. data/lib/aws-sdk-s3/encryptionV2/decrypt_handler.rb +7 -162
  34. data/lib/aws-sdk-s3/encryptionV2/decryption.rb +205 -0
  35. data/lib/aws-sdk-s3/encryptionV2/default_cipher_provider.rb +17 -0
  36. data/lib/aws-sdk-s3/encryptionV2/encrypt_handler.rb +2 -0
  37. data/lib/aws-sdk-s3/encryptionV2/io_encrypter.rb +2 -0
  38. data/lib/aws-sdk-s3/encryptionV2/kms_cipher_provider.rb +10 -2
  39. data/lib/aws-sdk-s3/encryptionV2/utils.rb +5 -0
  40. data/lib/aws-sdk-s3/encryptionV3/client.rb +885 -0
  41. data/lib/aws-sdk-s3/encryptionV3/decrypt_handler.rb +98 -0
  42. data/lib/aws-sdk-s3/encryptionV3/decryption.rb +244 -0
  43. data/lib/aws-sdk-s3/encryptionV3/default_cipher_provider.rb +159 -0
  44. data/lib/aws-sdk-s3/encryptionV3/default_key_provider.rb +35 -0
  45. data/lib/aws-sdk-s3/encryptionV3/encrypt_handler.rb +98 -0
  46. data/lib/aws-sdk-s3/encryptionV3/errors.rb +47 -0
  47. data/lib/aws-sdk-s3/encryptionV3/io_auth_decrypter.rb +60 -0
  48. data/lib/aws-sdk-s3/encryptionV3/io_decrypter.rb +35 -0
  49. data/lib/aws-sdk-s3/encryptionV3/io_encrypter.rb +84 -0
  50. data/lib/aws-sdk-s3/encryptionV3/key_provider.rb +28 -0
  51. data/lib/aws-sdk-s3/encryptionV3/kms_cipher_provider.rb +159 -0
  52. data/lib/aws-sdk-s3/encryptionV3/materials.rb +58 -0
  53. data/lib/aws-sdk-s3/encryptionV3/utils.rb +321 -0
  54. data/lib/aws-sdk-s3/encryption_v2.rb +1 -0
  55. data/lib/aws-sdk-s3/encryption_v3.rb +24 -0
  56. data/lib/aws-sdk-s3/endpoint_parameters.rb +65 -30
  57. data/lib/aws-sdk-s3/endpoint_provider.rb +604 -227
  58. data/lib/aws-sdk-s3/endpoints.rb +655 -1261
  59. data/lib/aws-sdk-s3/errors.rb +58 -0
  60. data/lib/aws-sdk-s3/express_credentials.rb +55 -0
  61. data/lib/aws-sdk-s3/express_credentials_provider.rb +59 -0
  62. data/lib/aws-sdk-s3/file_downloader.rb +192 -147
  63. data/lib/aws-sdk-s3/file_uploader.rb +10 -14
  64. data/lib/aws-sdk-s3/legacy_signer.rb +2 -1
  65. data/lib/aws-sdk-s3/multipart_download_error.rb +8 -0
  66. data/lib/aws-sdk-s3/multipart_file_uploader.rb +106 -103
  67. data/lib/aws-sdk-s3/multipart_stream_uploader.rb +99 -109
  68. data/lib/aws-sdk-s3/multipart_upload.rb +185 -38
  69. data/lib/aws-sdk-s3/multipart_upload_error.rb +3 -4
  70. data/lib/aws-sdk-s3/multipart_upload_part.rb +205 -68
  71. data/lib/aws-sdk-s3/object.rb +2052 -315
  72. data/lib/aws-sdk-s3/object_acl.rb +46 -28
  73. data/lib/aws-sdk-s3/object_copier.rb +1 -1
  74. data/lib/aws-sdk-s3/object_multipart_copier.rb +12 -9
  75. data/lib/aws-sdk-s3/object_summary.rb +1778 -262
  76. data/lib/aws-sdk-s3/object_version.rb +400 -68
  77. data/lib/aws-sdk-s3/plugins/access_grants.rb +178 -0
  78. data/lib/aws-sdk-s3/plugins/checksum_algorithm.rb +31 -0
  79. data/lib/aws-sdk-s3/plugins/endpoints.rb +32 -208
  80. data/lib/aws-sdk-s3/plugins/express_session_auth.rb +88 -0
  81. data/lib/aws-sdk-s3/plugins/http_200_errors.rb +55 -18
  82. data/lib/aws-sdk-s3/plugins/location_constraint.rb +3 -1
  83. data/lib/aws-sdk-s3/plugins/md5s.rb +10 -70
  84. data/lib/aws-sdk-s3/plugins/s3_signer.rb +7 -2
  85. data/lib/aws-sdk-s3/plugins/streaming_retry.rb +5 -7
  86. data/lib/aws-sdk-s3/plugins/url_encoded_keys.rb +2 -1
  87. data/lib/aws-sdk-s3/presigner.rb +9 -7
  88. data/lib/aws-sdk-s3/resource.rb +127 -22
  89. data/lib/aws-sdk-s3/transfer_manager.rb +303 -0
  90. data/lib/aws-sdk-s3/types.rb +8065 -1783
  91. data/lib/aws-sdk-s3.rb +35 -31
  92. data/sig/bucket.rbs +231 -0
  93. data/sig/bucket_acl.rbs +78 -0
  94. data/sig/bucket_cors.rbs +69 -0
  95. data/sig/bucket_lifecycle.rbs +88 -0
  96. data/sig/bucket_lifecycle_configuration.rbs +115 -0
  97. data/sig/bucket_logging.rbs +76 -0
  98. data/sig/bucket_notification.rbs +114 -0
  99. data/sig/bucket_policy.rbs +59 -0
  100. data/sig/bucket_request_payment.rbs +54 -0
  101. data/sig/bucket_tagging.rbs +65 -0
  102. data/sig/bucket_versioning.rbs +77 -0
  103. data/sig/bucket_website.rbs +93 -0
  104. data/sig/client.rbs +2612 -0
  105. data/sig/customizations/bucket.rbs +19 -0
  106. data/sig/customizations/object.rbs +38 -0
  107. data/sig/customizations/object_summary.rbs +35 -0
  108. data/sig/errors.rbs +44 -0
  109. data/sig/multipart_upload.rbs +120 -0
  110. data/sig/multipart_upload_part.rbs +109 -0
  111. data/sig/object.rbs +464 -0
  112. data/sig/object_acl.rbs +86 -0
  113. data/sig/object_summary.rbs +347 -0
  114. data/sig/object_version.rbs +143 -0
  115. data/sig/resource.rbs +141 -0
  116. data/sig/types.rbs +2899 -0
  117. data/sig/waiters.rbs +95 -0
  118. metadata +61 -12
  119. data/lib/aws-sdk-s3/plugins/skip_whole_multipart_get_checksums.rb +0 -31
@@ -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,227 +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],
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]
53
+ end
34
54
 
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'
40
- end
41
- @on_checksum_validated = options[:on_checksum_validated]
42
-
43
- @progress_callback = options[:progress_callback]
44
-
45
- validate!
46
-
47
- Aws::Plugins::UserAgent.feature('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
- thread.abort_on_exception = true
194
- threads << thread
195
- end
196
- threads.map(&:value).compact
197
221
  end
198
222
 
199
- def write(resp)
200
- range, _ = resp.content_range.split(' ').last.split('/')
201
- head, _ = range.split('-').map {|s| s.to_i}
202
- 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)
203
227
  end
204
228
 
205
- def single_request
206
- params = @params.merge(response_target: @path)
207
- params[:on_chunk_received] = single_part_progress if @progress_callback
208
- 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)
209
232
 
210
- return resp unless @on_checksum_validated
233
+ raise ArgumentError, "Invalid destination, expected #{valid_types.join(', ')} but got: #{destination.class}"
234
+ end
211
235
 
212
- if resp.checksum_validated
213
- @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'
214
239
  end
215
240
 
216
- resp
217
- 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
218
246
 
219
- def single_part_progress
220
- proc do |_chunk, bytes_read, total_size|
221
- @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'
222
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}"
223
260
  end
224
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
225
268
  class Part < Struct.new(:part_number, :size, :params)
226
269
  include Aws::Structure
227
270
  end
@@ -252,7 +295,7 @@ module Aws
252
295
  end
253
296
 
254
297
  # @api private
255
- class MultipartProgress
298
+ class MultipartProgress
256
299
  def initialize(parts, total_size, progress_callback)
257
300
  @bytes_received = Array.new(parts.size, 0)
258
301
  @part_sizes = parts.map(&:size)
@@ -260,6 +303,8 @@ module Aws
260
303
  @progress_callback = progress_callback
261
304
  end
262
305
 
306
+ attr_reader :progress_callback
307
+
263
308
  def call(part_number, bytes_received, total)
264
309
  # part numbers start at 1
265
310
  @bytes_received[part_number - 1] = bytes_received