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
@@ -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.
@@ -37,12 +35,10 @@ module Aws
37
35
  # objects smaller than the multipart threshold.
38
36
  # @return [void]
39
37
  def upload(source, options = {})
40
- Aws::Plugins::UserAgent.feature('s3-transfer') do
41
- if File.size(source) >= multipart_threshold
42
- MultipartFileUploader.new(@options).upload(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)
43
41
  else
44
- # remove multipart parameters not supported by put_object
45
- options.delete(:thread_count)
46
42
  put_object(source, options)
47
43
  end
48
44
  end
@@ -50,9 +46,9 @@ module Aws
50
46
 
51
47
  private
52
48
 
53
- def open_file(source)
54
- if String === source || Pathname === source
55
- 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)
56
52
  else
57
53
  yield(source)
58
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
@@ -7,34 +7,21 @@ module Aws
7
7
  module S3
8
8
  # @api private
9
9
  class MultipartFileUploader
10
-
11
10
  MIN_PART_SIZE = 5 * 1024 * 1024 # 5MB
12
-
13
- FILE_TOO_SMALL = "unable to multipart upload files smaller than 5MB"
14
-
15
11
  MAX_PARTS = 10_000
16
-
17
- THREAD_COUNT = 10
18
-
19
- # @api private
20
- CREATE_OPTIONS = Set.new(
21
- Client.api.operation(:create_multipart_upload).input.shape.member_names
22
- )
23
-
24
- COMPLETE_OPTIONS = Set.new(
25
- Client.api.operation(:complete_multipart_upload).input.shape.member_names
26
- )
27
-
28
- # @api private
29
- UPLOAD_PART_OPTIONS = Set.new(
30
- Client.api.operation(:upload_part).input.shape.member_names
12
+ CREATE_OPTIONS = Set.new(Client.api.operation(:create_multipart_upload).input.shape.member_names)
13
+ COMPLETE_OPTIONS = Set.new(Client.api.operation(:complete_multipart_upload).input.shape.member_names)
14
+ UPLOAD_PART_OPTIONS = Set.new(Client.api.operation(:upload_part).input.shape.member_names)
15
+ CHECKSUM_KEYS = Set.new(
16
+ Client.api.operation(:upload_part).input.shape.members.map do |n, s|
17
+ n if s.location == 'header' && s.location_name.start_with?('x-amz-checksum-')
18
+ end.compact
31
19
  )
32
20
 
33
21
  # @option options [Client] :client
34
- # @option options [Integer] :thread_count (THREAD_COUNT)
35
22
  def initialize(options = {})
36
23
  @client = options[:client] || Client.new
37
- @thread_count = options[:thread_count] || THREAD_COUNT
24
+ @executor = options[:executor]
38
25
  end
39
26
 
40
27
  # @return [Client]
@@ -48,13 +35,12 @@ module Aws
48
35
  # It will be invoked with [bytes_read], [total_sizes]
49
36
  # @return [Seahorse::Client::Response] - the CompleteMultipartUploadResponse
50
37
  def upload(source, options = {})
51
- if File.size(source) < MIN_PART_SIZE
52
- raise ArgumentError, FILE_TOO_SMALL
53
- else
54
- upload_id = initiate_upload(options)
55
- parts = upload_parts(upload_id, source, options)
56
- complete_upload(upload_id, parts, options)
57
- end
38
+ file_size = File.size(source)
39
+ raise ArgumentError, 'unable to multipart upload files smaller than 5MB' if file_size < MIN_PART_SIZE
40
+
41
+ upload_id = initiate_upload(options)
42
+ parts = upload_parts(upload_id, source, file_size, options)
43
+ complete_upload(upload_id, parts, file_size, options)
58
44
  end
59
45
 
60
46
  private
@@ -63,19 +49,21 @@ module Aws
63
49
  @client.create_multipart_upload(create_opts(options)).upload_id
64
50
  end
65
51
 
66
- def complete_upload(upload_id, parts, options)
52
+ def complete_upload(upload_id, parts, file_size, options)
67
53
  @client.complete_multipart_upload(
68
- **complete_opts(options).merge(
69
- upload_id: upload_id,
70
- multipart_upload: { parts: parts }
71
- )
54
+ **complete_opts(options),
55
+ upload_id: upload_id,
56
+ multipart_upload: { parts: parts },
57
+ mpu_object_size: file_size
72
58
  )
59
+ rescue StandardError => e
60
+ abort_upload(upload_id, options, [e])
73
61
  end
74
62
 
75
- def upload_parts(upload_id, source, options)
76
- pending = PartList.new(compute_parts(upload_id, source, options))
63
+ def upload_parts(upload_id, source, file_size, options)
77
64
  completed = PartList.new
78
- errors = upload_in_threads(pending, completed, options)
65
+ pending = PartList.new(compute_parts(upload_id, source, file_size, options))
66
+ errors = upload_with_executor(pending, completed, options)
79
67
  if errors.empty?
80
68
  completed.to_a.sort_by { |part| part[:part_number] }
81
69
  else
@@ -84,34 +72,30 @@ module Aws
84
72
  end
85
73
 
86
74
  def abort_upload(upload_id, options, errors)
87
- @client.abort_multipart_upload(
88
- bucket: options[:bucket],
89
- key: options[:key],
90
- upload_id: upload_id
91
- )
92
- msg = "multipart upload failed: #{errors.map(&:message).join("; ")}"
75
+ @client.abort_multipart_upload(bucket: options[:bucket], key: options[:key], upload_id: upload_id)
76
+ msg = "multipart upload failed: #{errors.map(&:message).join('; ')}"
93
77
  raise MultipartUploadError.new(msg, errors)
94
- rescue MultipartUploadError => error
95
- raise error
96
- rescue => error
97
- msg = "failed to abort multipart upload: #{error.message}"
98
- raise MultipartUploadError.new(msg, errors + [error])
78
+ rescue MultipartUploadError => e
79
+ raise e
80
+ rescue StandardError => e
81
+ msg = "failed to abort multipart upload: #{e.message}. " \
82
+ "Multipart upload failed: #{errors.map(&:message).join('; ')}"
83
+ raise MultipartUploadError.new(msg, errors + [e])
99
84
  end
100
85
 
101
- def compute_parts(upload_id, source, options)
102
- size = File.size(source)
103
- default_part_size = compute_default_part_size(size)
86
+ def compute_parts(upload_id, source, file_size, options)
87
+ default_part_size = compute_default_part_size(file_size)
104
88
  offset = 0
105
89
  part_number = 1
106
90
  parts = []
107
- while offset < size
91
+ while offset < file_size
108
92
  parts << upload_part_opts(options).merge(
109
93
  upload_id: upload_id,
110
94
  part_number: part_number,
111
95
  body: FilePart.new(
112
96
  source: source,
113
97
  offset: offset,
114
- size: part_size(size, default_part_size, offset)
98
+ size: part_size(file_size, default_part_size, offset)
115
99
  )
116
100
  )
117
101
  part_number += 1
@@ -120,69 +104,79 @@ module Aws
120
104
  parts
121
105
  end
122
106
 
107
+ def checksum_key?(key)
108
+ CHECKSUM_KEYS.include?(key)
109
+ end
110
+
111
+ def has_checksum_key?(keys)
112
+ keys.any? { |key| checksum_key?(key) }
113
+ end
114
+
115
+ def checksum_not_required?(options)
116
+ @client.config.request_checksum_calculation == 'when_required' && !options[:checksum_algorithm]
117
+ end
118
+
123
119
  def create_opts(options)
124
- CREATE_OPTIONS.inject({}) do |hash, key|
125
- hash[key] = options[key] if options.key?(key)
126
- hash
120
+ opts = {}
121
+ unless checksum_not_required?(options)
122
+ opts[:checksum_algorithm] = Aws::Plugins::ChecksumAlgorithm::DEFAULT_CHECKSUM
127
123
  end
124
+ opts[:checksum_type] = 'FULL_OBJECT' if has_checksum_key?(options.keys)
125
+ CREATE_OPTIONS.each_with_object(opts) { |k, h| h[k] = options[k] if options.key?(k) }
128
126
  end
129
127
 
130
128
  def complete_opts(options)
131
- COMPLETE_OPTIONS.inject({}) do |hash, key|
132
- hash[key] = options[key] if options.key?(key)
133
- hash
134
- end
129
+ opts = {}
130
+ opts[:checksum_type] = 'FULL_OBJECT' if has_checksum_key?(options.keys)
131
+ COMPLETE_OPTIONS.each_with_object(opts) { |k, h| h[k] = options[k] if options.key?(k) }
135
132
  end
136
133
 
137
134
  def upload_part_opts(options)
138
- UPLOAD_PART_OPTIONS.inject({}) do |hash, key|
139
- hash[key] = options[key] if options.key?(key)
140
- hash
135
+ UPLOAD_PART_OPTIONS.each_with_object({}) do |key, hash|
136
+ # don't pass through checksum calculations
137
+ hash[key] = options[key] if options.key?(key) && !checksum_key?(key)
141
138
  end
142
139
  end
143
140
 
144
- def upload_in_threads(pending, completed, options)
145
- threads = []
146
- if (callback = options[:progress_callback])
147
- progress = MultipartProgress.new(pending, callback)
148
- end
149
- @thread_count.times do
150
- thread = Thread.new do
151
- begin
152
- while part = pending.shift
153
- if progress
154
- part[:on_chunk_sent] =
155
- proc do |_chunk, bytes, _total|
156
- progress.call(part[:part_number], bytes)
157
- end
158
- end
159
- resp = @client.upload_part(part)
160
- part[:body].close
161
- completed_part = {etag: resp.etag, part_number: part[:part_number]}
162
-
163
- # get the requested checksum from the response
164
- if part[:checksum_algorithm]
165
- k = "checksum_#{part[:checksum_algorithm].downcase}".to_sym
166
- completed_part[k] = resp[k]
167
- end
168
-
169
- completed.push(completed_part)
170
- end
171
- nil
172
- rescue => error
173
- # keep other threads from uploading other parts
174
- pending.clear!
175
- error
176
- end
141
+ def upload_with_executor(pending, completed, options)
142
+ upload_attempts = 0
143
+ completion_queue = Queue.new
144
+ abort_upload = false
145
+ errors = []
146
+ progress = MultipartProgress.new(pending, options[:progress_callback])
147
+
148
+ while (part = pending.shift)
149
+ break if abort_upload
150
+
151
+ upload_attempts += 1
152
+ @executor.post(part) do |p|
153
+ update_progress(progress, p)
154
+ resp = @client.upload_part(p)
155
+ p[:body].close
156
+ completed_part = { etag: resp.etag, part_number: p[:part_number] }
157
+ apply_part_checksum(resp, completed_part)
158
+ completed.push(completed_part)
159
+ rescue StandardError => e
160
+ abort_upload = true
161
+ errors << e
162
+ ensure
163
+ completion_queue << :done
177
164
  end
178
- thread.abort_on_exception = true
179
- threads << thread
180
165
  end
181
- threads.map(&:value).compact
166
+
167
+ upload_attempts.times { completion_queue.pop }
168
+ errors
169
+ end
170
+
171
+ def apply_part_checksum(resp, part)
172
+ return unless (checksum = resp.context.params[:checksum_algorithm])
173
+
174
+ k = :"checksum_#{checksum.downcase}"
175
+ part[k] = resp.send(k)
182
176
  end
183
177
 
184
- def compute_default_part_size(source_size)
185
- [(source_size.to_f / MAX_PARTS).ceil, MIN_PART_SIZE].max.to_i
178
+ def compute_default_part_size(file_size)
179
+ [(file_size.to_f / MAX_PARTS).ceil, MIN_PART_SIZE].max.to_i
186
180
  end
187
181
 
188
182
  def part_size(total_size, part_size, offset)
@@ -193,9 +187,17 @@ module Aws
193
187
  end
194
188
  end
195
189
 
190
+ def update_progress(progress, part)
191
+ return unless progress.progress_callback
192
+
193
+ part[:on_chunk_sent] =
194
+ proc do |_chunk, bytes, _total|
195
+ progress.call(part[:part_number], bytes)
196
+ end
197
+ end
198
+
196
199
  # @api private
197
200
  class PartList
198
-
199
201
  def initialize(parts = [])
200
202
  @parts = parts
201
203
  @mutex = Mutex.new
@@ -224,7 +226,6 @@ module Aws
224
226
  def to_a
225
227
  @mutex.synchronize { @parts.dup }
226
228
  end
227
-
228
229
  end
229
230
 
230
231
  # @api private
@@ -235,6 +236,8 @@ module Aws
235
236
  @progress_callback = progress_callback
236
237
  end
237
238
 
239
+ attr_reader :progress_callback
240
+
238
241
  def call(part_number, bytes_read)
239
242
  # part numbers start at 1
240
243
  @bytes_sent[part_number - 1] = bytes_read
@@ -243,4 +246,4 @@ module Aws
243
246
  end
244
247
  end
245
248
  end
246
- end
249
+ end
@@ -9,33 +9,18 @@ module Aws
9
9
  module S3
10
10
  # @api private
11
11
  class MultipartStreamUploader
12
- # api private
13
- PART_SIZE = 5 * 1024 * 1024 # 5MB
14
12
 
15
- # api private
16
- THREAD_COUNT = 10
17
-
18
- # api private
19
- TEMPFILE_PREIX = 'aws-sdk-s3-upload_stream'.freeze
20
-
21
- # @api private
22
- CREATE_OPTIONS =
23
- Set.new(Client.api.operation(:create_multipart_upload).input.shape.member_names)
24
-
25
- # @api private
26
- UPLOAD_PART_OPTIONS =
27
- Set.new(Client.api.operation(:upload_part).input.shape.member_names)
28
-
29
- # @api private
30
- COMPLETE_UPLOAD_OPTIONS =
31
- Set.new(Client.api.operation(:complete_multipart_upload).input.shape.member_names)
13
+ DEFAULT_PART_SIZE = 5 * 1024 * 1024 # 5MB
14
+ CREATE_OPTIONS = Set.new(Client.api.operation(:create_multipart_upload).input.shape.member_names)
15
+ UPLOAD_PART_OPTIONS = Set.new(Client.api.operation(:upload_part).input.shape.member_names)
16
+ COMPLETE_UPLOAD_OPTIONS = Set.new(Client.api.operation(:complete_multipart_upload).input.shape.member_names)
32
17
 
33
18
  # @option options [Client] :client
34
19
  def initialize(options = {})
35
20
  @client = options[:client] || Client.new
21
+ @executor = options[:executor]
36
22
  @tempfile = options[:tempfile]
37
- @part_size = options[:part_size] || PART_SIZE
38
- @thread_count = options[:thread_count] || THREAD_COUNT
23
+ @part_size = options[:part_size] || DEFAULT_PART_SIZE
39
24
  end
40
25
 
41
26
  # @return [Client]
@@ -45,7 +30,7 @@ module Aws
45
30
  # @option options [required,String] :key
46
31
  # @return [Seahorse::Client::Response] - the CompleteMultipartUploadResponse
47
32
  def upload(options = {}, &block)
48
- Aws::Plugins::UserAgent.feature('s3-transfer') do
33
+ Aws::Plugins::UserAgent.metric('S3_TRANSFER') do
49
34
  upload_id = initiate_upload(options)
50
35
  parts = upload_parts(upload_id, options, &block)
51
36
  complete_upload(upload_id, parts, options)
@@ -60,85 +45,80 @@ module Aws
60
45
 
61
46
  def complete_upload(upload_id, parts, options)
62
47
  @client.complete_multipart_upload(
63
- **complete_opts(options).merge(
64
- upload_id: upload_id,
65
- multipart_upload: { parts: parts }
66
- )
48
+ **complete_opts(options).merge(upload_id: upload_id, multipart_upload: { parts: parts })
67
49
  )
50
+ rescue StandardError => e
51
+ abort_upload(upload_id, options, [e])
68
52
  end
69
53
 
70
54
  def upload_parts(upload_id, options, &block)
71
- completed = Queue.new
72
- thread_errors = []
73
- errors = begin
55
+ completed_parts = Queue.new
56
+ errors = []
57
+
58
+ begin
74
59
  IO.pipe do |read_pipe, write_pipe|
75
- threads = upload_in_threads(
76
- read_pipe, completed,
77
- upload_part_opts(options).merge(upload_id: upload_id),
78
- thread_errors)
79
- begin
80
- block.call(write_pipe)
81
- ensure
82
- # Ensure the pipe is closed to avoid https://github.com/jruby/jruby/issues/6111
83
- write_pipe.close
60
+ upload_thread = Thread.new do
61
+ upload_with_executor(
62
+ read_pipe,
63
+ completed_parts,
64
+ errors,
65
+ upload_part_opts(options).merge(upload_id: upload_id)
66
+ )
84
67
  end
85
- threads.map(&:value).compact
68
+
69
+ block.call(write_pipe)
70
+ ensure
71
+ # Ensure the pipe is closed to avoid https://github.com/jruby/jruby/issues/6111
72
+ write_pipe.close
73
+ upload_thread.join
86
74
  end
87
- rescue => e
88
- thread_errors + [e]
75
+ rescue StandardError => e
76
+ errors << e
89
77
  end
78
+ return ordered_parts(completed_parts) if errors.empty?
90
79
 
91
- if errors.empty?
92
- Array.new(completed.size) { completed.pop }.sort_by { |part| part[:part_number] }
93
- else
94
- abort_upload(upload_id, options, errors)
95
- end
80
+ abort_upload(upload_id, options, errors)
96
81
  end
97
82
 
98
83
  def abort_upload(upload_id, options, errors)
99
- @client.abort_multipart_upload(
100
- bucket: options[:bucket],
101
- key: options[:key],
102
- upload_id: upload_id
103
- )
104
- msg = "multipart upload failed: #{errors.map(&:message).join("; ")}"
84
+ @client.abort_multipart_upload(bucket: options[:bucket], key: options[:key], upload_id: upload_id)
85
+ msg = "multipart upload failed: #{errors.map(&:message).join('; ')}"
105
86
  raise MultipartUploadError.new(msg, errors)
106
- rescue MultipartUploadError => error
107
- raise error
108
- rescue => error
109
- msg = "failed to abort multipart upload: #{error.message}"
110
- raise MultipartUploadError.new(msg, errors + [error])
87
+ rescue MultipartUploadError => e
88
+ raise e
89
+ rescue StandardError => e
90
+ msg = "failed to abort multipart upload: #{e.message}. "\
91
+ "Multipart upload failed: #{errors.map(&:message).join('; ')}"
92
+ raise MultipartUploadError.new(msg, errors + [e])
111
93
  end
112
94
 
113
95
  def create_opts(options)
114
- CREATE_OPTIONS.inject({}) do |hash, key|
96
+ CREATE_OPTIONS.each_with_object({}) do |key, hash|
115
97
  hash[key] = options[key] if options.key?(key)
116
- hash
117
98
  end
118
99
  end
119
100
 
120
101
  def upload_part_opts(options)
121
- UPLOAD_PART_OPTIONS.inject({}) do |hash, key|
102
+ UPLOAD_PART_OPTIONS.each_with_object({}) do |key, hash|
122
103
  hash[key] = options[key] if options.key?(key)
123
- hash
124
104
  end
125
105
  end
126
106
 
127
107
  def complete_opts(options)
128
- COMPLETE_UPLOAD_OPTIONS.inject({}) do |hash, key|
108
+ COMPLETE_UPLOAD_OPTIONS.each_with_object({}) do |key, hash|
129
109
  hash[key] = options[key] if options.key?(key)
130
- hash
131
110
  end
132
111
  end
133
112
 
134
113
  def read_to_part_body(read_pipe)
135
114
  return if read_pipe.closed?
136
- temp_io = @tempfile ? Tempfile.new(TEMPFILE_PREIX) : StringIO.new(String.new)
115
+
116
+ temp_io = @tempfile ? Tempfile.new('aws-sdk-s3-upload_stream') : StringIO.new(String.new)
137
117
  temp_io.binmode
138
118
  bytes_copied = IO.copy_stream(read_pipe, temp_io, @part_size)
139
119
  temp_io.rewind
140
- if bytes_copied == 0
141
- if Tempfile === temp_io
120
+ if bytes_copied.zero?
121
+ if temp_io.is_a?(Tempfile)
142
122
  temp_io.close
143
123
  temp_io.unlink
144
124
  end
@@ -148,52 +128,62 @@ module Aws
148
128
  end
149
129
  end
150
130
 
151
- def upload_in_threads(read_pipe, completed, options, thread_errors)
152
- mutex = Mutex.new
131
+ def upload_with_executor(read_pipe, completed, errors, options)
132
+ completion_queue = Queue.new
133
+ queued_parts = 0
153
134
  part_number = 0
154
- @thread_count.times.map do
155
- thread = Thread.new do
156
- begin
157
- loop do
158
- body, thread_part_number = mutex.synchronize do
159
- [read_to_part_body(read_pipe), part_number += 1]
160
- end
161
- break unless (body || thread_part_number == 1)
162
- begin
163
- part = options.merge(
164
- body: body,
165
- part_number: thread_part_number,
166
- )
167
- resp = @client.upload_part(part)
168
- completed_part = {etag: resp.etag, part_number: part[:part_number]}
169
-
170
- # get the requested checksum from the response
171
- if part[:checksum_algorithm]
172
- k = "checksum_#{part[:checksum_algorithm].downcase}".to_sym
173
- completed_part[k] = resp[k]
174
- end
175
- completed.push(completed_part)
176
- ensure
177
- if Tempfile === body
178
- body.close
179
- body.unlink
180
- elsif StringIO === body
181
- body.string.clear
182
- end
183
- end
184
- end
185
- nil
186
- rescue => error
187
- # keep other threads from uploading other parts
188
- mutex.synchronize do
189
- thread_errors.push(error)
190
- read_pipe.close_read unless read_pipe.closed?
191
- end
192
- error
135
+ mutex = Mutex.new
136
+ loop do
137
+ part_body, current_part_num = mutex.synchronize do
138
+ [read_to_part_body(read_pipe), part_number += 1]
139
+ end
140
+ break unless part_body || current_part_num == 1
141
+
142
+ queued_parts += 1
143
+ @executor.post(part_body, current_part_num, options) do |body, num, opts|
144
+ part = opts.merge(body: body, part_number: num)
145
+ resp = @client.upload_part(part)
146
+ completed_part = create_completed_part(resp, part)
147
+ completed.push(completed_part)
148
+ rescue StandardError => e
149
+ mutex.synchronize do
150
+ errors.push(e)
151
+ read_pipe.close_read unless read_pipe.closed?
193
152
  end
153
+ ensure
154
+ clear_body(body)
155
+ completion_queue << :done
194
156
  end
195
- thread.abort_on_exception = true
196
- thread
157
+ end
158
+ queued_parts.times { completion_queue.pop }
159
+ end
160
+
161
+ def create_completed_part(resp, part)
162
+ completed_part = { etag: resp.etag, part_number: part[:part_number] }
163
+ return completed_part unless part[:checksum_algorithm]
164
+
165
+ # get the requested checksum from the response
166
+ k = "checksum_#{part[:checksum_algorithm].downcase}".to_sym
167
+ completed_part[k] = resp[k]
168
+ completed_part
169
+ end
170
+
171
+ def ordered_parts(parts)
172
+ sorted = []
173
+ until parts.empty?
174
+ part = parts.pop
175
+ index = sorted.bsearch_index { |p| p[:part_number] >= part[:part_number] } || sorted.size
176
+ sorted.insert(index, part)
177
+ end
178
+ sorted
179
+ end
180
+
181
+ def clear_body(body)
182
+ if body.is_a?(Tempfile)
183
+ body.close
184
+ body.unlink
185
+ elsif body.is_a?(StringIO)
186
+ body.string.clear
197
187
  end
198
188
  end
199
189
  end