aws-sdk-resources 2.8.4 → 2.11.632

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 (40) hide show
  1. checksums.yaml +5 -5
  2. data/lib/aws-sdk-resources/documenter/has_many_operation_documenter.rb +1 -1
  3. data/lib/aws-sdk-resources/services/s3/bucket.rb +4 -0
  4. data/lib/aws-sdk-resources/services/s3/encryption/client.rb +24 -7
  5. data/lib/aws-sdk-resources/services/s3/encryption/decrypt_handler.rb +77 -26
  6. data/lib/aws-sdk-resources/services/s3/encryption/default_cipher_provider.rb +43 -5
  7. data/lib/aws-sdk-resources/services/s3/encryption/default_key_provider.rb +2 -0
  8. data/lib/aws-sdk-resources/services/s3/encryption/encrypt_handler.rb +13 -2
  9. data/lib/aws-sdk-resources/services/s3/encryption/errors.rb +2 -0
  10. data/lib/aws-sdk-resources/services/s3/encryption/io_auth_decrypter.rb +11 -3
  11. data/lib/aws-sdk-resources/services/s3/encryption/io_decrypter.rb +11 -3
  12. data/lib/aws-sdk-resources/services/s3/encryption/io_encrypter.rb +2 -0
  13. data/lib/aws-sdk-resources/services/s3/encryption/key_provider.rb +2 -0
  14. data/lib/aws-sdk-resources/services/s3/encryption/kms_cipher_provider.rb +36 -3
  15. data/lib/aws-sdk-resources/services/s3/encryption/materials.rb +9 -7
  16. data/lib/aws-sdk-resources/services/s3/encryption/utils.rb +25 -0
  17. data/lib/aws-sdk-resources/services/s3/encryption.rb +3 -0
  18. data/lib/aws-sdk-resources/services/s3/encryptionV2/client.rb +561 -0
  19. data/lib/aws-sdk-resources/services/s3/encryptionV2/decrypt_handler.rb +214 -0
  20. data/lib/aws-sdk-resources/services/s3/encryptionV2/default_cipher_provider.rb +170 -0
  21. data/lib/aws-sdk-resources/services/s3/encryptionV2/default_key_provider.rb +40 -0
  22. data/lib/aws-sdk-resources/services/s3/encryptionV2/encrypt_handler.rb +69 -0
  23. data/lib/aws-sdk-resources/services/s3/encryptionV2/errors.rb +37 -0
  24. data/lib/aws-sdk-resources/services/s3/encryptionV2/io_auth_decrypter.rb +58 -0
  25. data/lib/aws-sdk-resources/services/s3/encryptionV2/io_decrypter.rb +37 -0
  26. data/lib/aws-sdk-resources/services/s3/encryptionV2/io_encrypter.rb +73 -0
  27. data/lib/aws-sdk-resources/services/s3/encryptionV2/key_provider.rb +31 -0
  28. data/lib/aws-sdk-resources/services/s3/encryptionV2/kms_cipher_provider.rb +169 -0
  29. data/lib/aws-sdk-resources/services/s3/encryptionV2/materials.rb +60 -0
  30. data/lib/aws-sdk-resources/services/s3/encryptionV2/utils.rb +103 -0
  31. data/lib/aws-sdk-resources/services/s3/encryption_v2.rb +24 -0
  32. data/lib/aws-sdk-resources/services/s3/file_downloader.rb +169 -0
  33. data/lib/aws-sdk-resources/services/s3/object.rb +33 -1
  34. data/lib/aws-sdk-resources/services/s3/object_multipart_copier.rb +1 -0
  35. data/lib/aws-sdk-resources/services/s3/object_summary.rb +8 -0
  36. data/lib/aws-sdk-resources/services/s3/presigned_post.rb +4 -0
  37. data/lib/aws-sdk-resources/services/s3.rb +2 -0
  38. data/lib/aws-sdk-resources/services/sns/message_verifier.rb +14 -0
  39. data/lib/aws-sdk-resources/services/sqs/queue_poller.rb +1 -1
  40. metadata +26 -8
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module S3
5
+ module EncryptionV2
6
+
7
+ # This module defines the interface required for a {Client#key_provider}.
8
+ # A key provider is any object that:
9
+ #
10
+ # * Responds to {#encryption_materials} with an {Materials} object.
11
+ #
12
+ # * Responds to {#key_for}, receiving a JSON document String,
13
+ # returning an encryption key. The returned encryption key
14
+ # must be one of:
15
+ #
16
+ # * `OpenSSL::PKey::RSA` - for asymmetric encryption
17
+ # * `String` - 32, 24, or 16 bytes long, for symmetric encryption
18
+ #
19
+ module KeyProvider
20
+
21
+ # @return [Materials]
22
+ def encryption_materials; end
23
+
24
+ # @param [String<JSON>] materials_description
25
+ # @return [OpenSSL::PKey::RSA, String] encryption_key
26
+ def key_for(materials_description); end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module Aws
6
+ module S3
7
+ module EncryptionV2
8
+ # @api private
9
+ class KmsCipherProvider
10
+
11
+ def initialize(options = {})
12
+ @kms_key_id = validate_kms_key(options[:kms_key_id])
13
+ @kms_client = options[:kms_client]
14
+ @key_wrap_schema = validate_key_wrap(
15
+ options[:key_wrap_schema]
16
+ )
17
+ @content_encryption_schema = validate_cek(
18
+ options[:content_encryption_schema]
19
+ )
20
+ end
21
+
22
+ # @return [Array<Hash,Cipher>] Creates and returns a new encryption
23
+ # envelope and encryption cipher.
24
+ def encryption_cipher(options = {})
25
+ validate_key_for_encryption
26
+ encryption_context = build_encryption_context(@content_encryption_schema, options)
27
+ key_data = @kms_client.generate_data_key(
28
+ key_id: @kms_key_id,
29
+ encryption_context: encryption_context,
30
+ key_spec: 'AES_256'
31
+ )
32
+ cipher = Utils.aes_encryption_cipher(:GCM)
33
+ cipher.key = key_data.plaintext
34
+ envelope = {
35
+ 'x-amz-key-v2' => encode64(key_data.ciphertext_blob),
36
+ 'x-amz-iv' => encode64(cipher.iv = cipher.random_iv),
37
+ 'x-amz-cek-alg' => @content_encryption_schema,
38
+ 'x-amz-tag-len' => (AES_GCM_TAG_LEN_BYTES * 8).to_s,
39
+ 'x-amz-wrap-alg' => @key_wrap_schema,
40
+ 'x-amz-matdesc' => Json.dump(encryption_context)
41
+ }
42
+ cipher.auth_data = '' # auth_data must be set after key and iv
43
+ [envelope, cipher]
44
+ end
45
+
46
+ # @return [Cipher] Given an encryption envelope, returns a
47
+ # decryption cipher.
48
+ def decryption_cipher(envelope, options = {})
49
+ encryption_context = Json.load(envelope['x-amz-matdesc'])
50
+ cek_alg = envelope['x-amz-cek-alg']
51
+
52
+ case envelope['x-amz-wrap-alg']
53
+ when 'kms'
54
+ unless options[:security_profile] == :v2_and_legacy
55
+ raise Errors::LegacyDecryptionError
56
+ end
57
+ when 'kms+context'
58
+ if cek_alg != encryption_context['aws:x-amz-cek-alg']
59
+ raise Errors::CEKAlgMismatchError
60
+ end
61
+
62
+ if encryption_context != build_encryption_context(cek_alg, options)
63
+ raise Errors::DecryptionError, 'Value of encryption context from'\
64
+ ' envelope does not match the provided encryption context'
65
+ end
66
+ when 'AES/GCM'
67
+ raise ArgumentError, 'Key mismatch - Client is configured' \
68
+ ' with a KMS key and the x-amz-wrap-alg is AES/GCM.'
69
+ when 'RSA-OAEP-SHA1'
70
+ raise ArgumentError, 'Key mismatch - Client is configured' \
71
+ ' with a KMS key and the x-amz-wrap-alg is RSA-OAEP-SHA1.'
72
+ else
73
+ raise ArgumentError, 'Unsupported wrap-alg: ' \
74
+ "#{envelope['x-amz-wrap-alg']}"
75
+ end
76
+
77
+ any_cmk_mode = false || options[:kms_allow_decrypt_with_any_cmk]
78
+ decrypt_options = {
79
+ ciphertext_blob: decode64(envelope['x-amz-key-v2']),
80
+ encryption_context: encryption_context
81
+ }
82
+ unless any_cmk_mode
83
+ decrypt_options[:key_id] = @kms_key_id
84
+ end
85
+
86
+ key = @kms_client.decrypt(decrypt_options).plaintext
87
+ iv = decode64(envelope['x-amz-iv'])
88
+ block_mode =
89
+ case cek_alg
90
+ when 'AES/CBC/PKCS5Padding'
91
+ :CBC
92
+ when 'AES/CBC/PKCS7Padding'
93
+ :CBC
94
+ when 'AES/GCM/NoPadding'
95
+ :GCM
96
+ else
97
+ type = envelope['x-amz-cek-alg'].inspect
98
+ msg = "unsupported content encrypting key (cek) format: #{type}"
99
+ raise Errors::DecryptionError, msg
100
+ end
101
+ Utils.aes_decryption_cipher(block_mode, key, iv)
102
+ end
103
+
104
+ private
105
+
106
+ def validate_key_wrap(key_wrap_schema)
107
+ case key_wrap_schema
108
+ when :kms_context then 'kms+context'
109
+ else
110
+ raise ArgumentError, "Unsupported key_wrap_schema: #{key_wrap_schema}"
111
+ end
112
+ end
113
+
114
+ def validate_cek(content_encryption_schema)
115
+ case content_encryption_schema
116
+ when :aes_gcm_no_padding
117
+ "AES/GCM/NoPadding"
118
+ else
119
+ raise ArgumentError, "Unsupported content_encryption_schema: #{content_encryption_schema}"
120
+ end
121
+ end
122
+
123
+ def validate_kms_key(kms_key_id)
124
+ if kms_key_id.nil? || kms_key_id.length.zero?
125
+ raise ArgumentError, 'KMS CMK ID was not specified. ' \
126
+ 'Please specify a CMK ID, ' \
127
+ 'or set kms_key_id: :kms_allow_decrypt_with_any_cmk to use ' \
128
+ 'any valid CMK from the object.'
129
+ end
130
+
131
+ if kms_key_id.is_a?(Symbol) && kms_key_id != :kms_allow_decrypt_with_any_cmk
132
+ raise ArgumentError, 'kms_key_id must be a valid KMS CMK or be ' \
133
+ 'set to :kms_allow_decrypt_with_any_cmk'
134
+ end
135
+ kms_key_id
136
+ end
137
+
138
+ def build_encryption_context(cek_alg, options = {})
139
+ kms_context = (options[:kms_encryption_context] || {})
140
+ .each_with_object({}) { |(k, v), h| h[k.to_s] = v }
141
+ if kms_context.include? 'aws:x-amz-cek-alg'
142
+ raise ArgumentError, 'Conflict in reserved KMS Encryption Context ' \
143
+ 'key aws:x-amz-cek-alg. This value is reserved for the S3 ' \
144
+ 'Encryption Client and cannot be set by the user.'
145
+ end
146
+ {
147
+ 'aws:x-amz-cek-alg' => cek_alg
148
+ }.merge(kms_context)
149
+ end
150
+
151
+ def encode64(str)
152
+ Base64.encode64(str).split("\n") * ""
153
+ end
154
+
155
+ def decode64(str)
156
+ Base64.decode64(str)
157
+ end
158
+
159
+ def validate_key_for_encryption
160
+ if @kms_key_id == :kms_allow_decrypt_with_any_cmk
161
+ raise ArgumentError, 'Unable to encrypt/write objects with '\
162
+ 'kms_key_id = :kms_allow_decrypt_with_any_cmk. Provide ' \
163
+ 'a valid kms_key_id on client construction.'
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module Aws
6
+ module S3
7
+ module EncryptionV2
8
+ class Materials
9
+
10
+ # @option options [required, OpenSSL::PKey::RSA, String] :key
11
+ # The master key to use for encrypting/decrypting all objects.
12
+ #
13
+ # @option options [String<JSON>] :description ('{}')
14
+ # The encryption materials description. This is must be
15
+ # a JSON document string.
16
+ #
17
+ def initialize(options = {})
18
+ @key = validate_key(options[:key])
19
+ @description = validate_desc(options[:description])
20
+ end
21
+
22
+ # @return [OpenSSL::PKey::RSA, String]
23
+ attr_reader :key
24
+
25
+ # @return [String<JSON>]
26
+ attr_reader :description
27
+
28
+ private
29
+
30
+ def validate_key(key)
31
+ case key
32
+ when OpenSSL::PKey::RSA then key
33
+ when String
34
+ if [32, 24, 16].include?(key.bytesize)
35
+ key
36
+ else
37
+ msg = 'invalid key, symmetric key required to be 16, 24, or '\
38
+ '32 bytes in length, saw length ' + key.bytesize.to_s
39
+ raise ArgumentError, msg
40
+ end
41
+ else
42
+ msg = 'invalid encryption key, expected an OpenSSL::PKey::RSA key '\
43
+ '(for asymmetric encryption) or a String (for symmetric '\
44
+ 'encryption).'
45
+ raise ArgumentError, msg
46
+ end
47
+ end
48
+
49
+ def validate_desc(description)
50
+ Json.load(description)
51
+ description
52
+ rescue Json::ParseError, EncodingError
53
+ msg = 'expected description to be a valid JSON document string'
54
+ raise ArgumentError, msg
55
+ end
56
+
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Aws
6
+ module S3
7
+ module EncryptionV2
8
+ # @api private
9
+ module Utils
10
+
11
+ class << self
12
+
13
+ def encrypt_aes_gcm(key, data, auth_data)
14
+ cipher = aes_encryption_cipher(:GCM, key)
15
+ cipher.iv = (iv = cipher.random_iv)
16
+ cipher.auth_data = auth_data
17
+
18
+ iv + cipher.update(data) + cipher.final + cipher.auth_tag
19
+ end
20
+
21
+ def encrypt_rsa(key, data, auth_data)
22
+ # Plaintext must be KeyLengthInBytes (1 Byte) + DataKey + AuthData
23
+ buf = [data.bytesize] + data.unpack('C*') + auth_data.unpack('C*')
24
+ key.public_encrypt(buf.pack('C*'), OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
25
+ end
26
+
27
+ def decrypt(key, data)
28
+ begin
29
+ case key
30
+ when OpenSSL::PKey::RSA # asymmetric decryption
31
+ key.private_decrypt(data)
32
+ when String # symmetric Decryption
33
+ cipher = aes_cipher(:decrypt, :ECB, key, nil)
34
+ cipher.update(data) + cipher.final
35
+ end
36
+ rescue OpenSSL::Cipher::CipherError
37
+ msg = 'decryption failed, possible incorrect key'
38
+ raise Errors::DecryptionError, msg
39
+ end
40
+ end
41
+
42
+ def decrypt_aes_gcm(key, data, auth_data)
43
+ # data is iv (12B) + key + tag (16B)
44
+ buf = data.unpack('C*')
45
+ iv = buf[0,12].pack('C*') # iv will always be 12 bytes
46
+ tag = buf[-16, 16].pack('C*') # tag is 16 bytes
47
+ enc_key = buf[12, buf.size - (12+16)].pack('C*')
48
+ cipher = aes_cipher(:decrypt, :GCM, key, iv)
49
+ cipher.auth_tag = tag
50
+ cipher.auth_data = auth_data
51
+ cipher.update(enc_key) + cipher.final
52
+ end
53
+
54
+ # returns the decrypted data + auth_data
55
+ def decrypt_rsa(key, enc_data)
56
+ # Plaintext must be KeyLengthInBytes (1 Byte) + DataKey + AuthData
57
+ buf = key.private_decrypt(enc_data, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING).unpack('C*')
58
+ key_length = buf[0]
59
+ data = buf[1, key_length].pack('C*')
60
+ auth_data = buf[key_length+1, buf.length - key_length].pack('C*')
61
+ [data, auth_data]
62
+ end
63
+
64
+ # @param [String] block_mode "CBC" or "ECB"
65
+ # @param [OpenSSL::PKey::RSA, String, nil] key
66
+ # @param [String, nil] iv The initialization vector
67
+ def aes_encryption_cipher(block_mode, key = nil, iv = nil)
68
+ aes_cipher(:encrypt, block_mode, key, iv)
69
+ end
70
+
71
+ # @param [String] block_mode "CBC" or "ECB"
72
+ # @param [OpenSSL::PKey::RSA, String, nil] key
73
+ # @param [String, nil] iv The initialization vector
74
+ def aes_decryption_cipher(block_mode, key = nil, iv = nil)
75
+ aes_cipher(:decrypt, block_mode, key, iv)
76
+ end
77
+
78
+ # @param [String] mode "encrypt" or "decrypt"
79
+ # @param [String] block_mode "CBC" or "ECB"
80
+ # @param [OpenSSL::PKey::RSA, String, nil] key
81
+ # @param [String, nil] iv The initialization vector
82
+ def aes_cipher(mode, block_mode, key, iv)
83
+ cipher = key ?
84
+ OpenSSL::Cipher.new("aes-#{cipher_size(key)}-#{block_mode.downcase}") :
85
+ OpenSSL::Cipher.new("aes-256-#{block_mode.downcase}")
86
+ cipher.send(mode) # encrypt or decrypt
87
+ cipher.key = key if key
88
+ cipher.iv = iv if iv
89
+ cipher
90
+ end
91
+
92
+ # @param [String] key
93
+ # @return [Integer]
94
+ # @raise ArgumentError
95
+ def cipher_size(key)
96
+ key.bytesize * 8
97
+ end
98
+
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,24 @@
1
+ module Aws
2
+ module S3
3
+ module EncryptionV2
4
+
5
+ AES_GCM_TAG_LEN_BYTES = 16
6
+ EC_USER_AGENT = 'S3CryptoV2'
7
+
8
+ autoload :Client, 'aws-sdk-resources/services/s3/encryptionV2/client'
9
+ autoload :DecryptHandler, 'aws-sdk-resources/services/s3/encryptionV2/decrypt_handler'
10
+ autoload :DefaultCipherProvider, 'aws-sdk-resources/services/s3/encryptionV2/default_cipher_provider'
11
+ autoload :DefaultKeyProvider, 'aws-sdk-resources/services/s3/encryptionV2/default_key_provider'
12
+ autoload :EncryptHandler, 'aws-sdk-resources/services/s3/encryptionV2/encrypt_handler'
13
+ autoload :Errors, 'aws-sdk-resources/services/s3/encryptionV2/errors'
14
+ autoload :IOEncrypter, 'aws-sdk-resources/services/s3/encryptionV2/io_encrypter'
15
+ autoload :IOAuthDecrypter, 'aws-sdk-resources/services/s3/encryptionV2/io_auth_decrypter'
16
+ autoload :IODecrypter, 'aws-sdk-resources/services/s3/encryptionV2/io_decrypter'
17
+ autoload :KeyProvider, 'aws-sdk-resources/services/s3/encryptionV2/key_provider'
18
+ autoload :KmsCipherProvider, 'aws-sdk-resources/services/s3/encryptionV2/kms_cipher_provider'
19
+ autoload :Materials, 'aws-sdk-resources/services/s3/encryptionV2/materials'
20
+ autoload :Utils, 'aws-sdk-resources/services/s3/encryptionV2/utils'
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,169 @@
1
+ require 'pathname'
2
+ require 'thread'
3
+ require 'set'
4
+ require 'tmpdir'
5
+
6
+ module Aws
7
+ module S3
8
+ # @api private
9
+ class FileDownloader
10
+
11
+ MIN_CHUNK_SIZE = 5 * 1024 * 1024
12
+ MAX_PARTS = 10_000
13
+ THREAD_COUNT = 10
14
+
15
+ def initialize(options = {})
16
+ @client = options[:client] || Client.new
17
+ end
18
+
19
+ # @return [Client]
20
+ attr_reader :client
21
+
22
+ 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
40
+ end
41
+ else
42
+ msg = "Invalid mode #{@mode} provided, "\
43
+ "mode should be :single_request, :get_range or :auto"
44
+ raise ArgumentError, msg
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def multipart_download
51
+ resp = @client.head_object(bucket: @bucket, key: @key, part_number: 1)
52
+ count = resp.parts_count
53
+ 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))
57
+ 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)
63
+ end
64
+ end
65
+
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)
73
+ end
74
+ end
75
+
76
+ def construct_chunks(file_size)
77
+ offset = 0
78
+ default_chunk_size = compute_chunk(file_size)
79
+ chunks = []
80
+ while offset <= file_size
81
+ progress = offset + default_chunk_size
82
+ chunks << "bytes=#{offset}-#{progress < file_size ? progress : file_size}"
83
+ offset = progress + 1
84
+ end
85
+ chunks
86
+ end
87
+
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
+ default_chunk_size = @chunk_size || [(file_size.to_f / MAX_PARTS).ceil, MIN_CHUNK_SIZE].max.to_i
93
+ end
94
+ end
95
+
96
+ def sort_files(files)
97
+ # sort file by start range count or part number
98
+ files.sort do |a, b|
99
+ a[/([^\=]+)$/].split('-')[0].to_i <=> b[/([^\=]+)$/].split('-')[0].to_i
100
+ end
101
+ end
102
+
103
+ def concatenate_parts(fileparts)
104
+ File.open(@path, 'wb')do |output_path|
105
+ sort_files(fileparts).each {|part| IO.copy_stream(part, output_path)}
106
+ end
107
+ end
108
+
109
+ def file_batches(chunks, dir, mode)
110
+ batches = []
111
+ chunks = (1..chunks) if mode.eql? 'part_number'
112
+ chunks.each_slice(@thread_count) do |slice|
113
+ batches << map_files(slice, dir, mode)
114
+ end
115
+ batches
116
+ end
117
+
118
+ def map_files(slice, dir, mode)
119
+ case mode
120
+ when 'range'
121
+ slice.inject({}) {|h, chunk| h[chunk] = File.join(dir, chunk); h}
122
+ when 'part_number'
123
+ slice.inject({}) {|h, part| h[part] = File.join(dir, "part_number=#{part}"); h}
124
+ end
125
+ end
126
+
127
+ def multithreaded_get_by_ranges(chunks)
128
+ thread_batches(chunks, 'range')
129
+ end
130
+
131
+ def multithreaded_get_by_parts(parts)
132
+ thread_batches(parts, 'part_number')
133
+ end
134
+
135
+ def thread_batches(chunks, param)
136
+ # create a tmp dir under destination dir for batches
137
+ dir = Dir.mktmpdir(nil, File.dirname(@path))
138
+ batches = file_batches(chunks, dir, param)
139
+ parts = batches.flat_map(&:values)
140
+ begin
141
+ batches.each do |batch|
142
+ threads = []
143
+ batch.each do |chunk, file|
144
+ threads << Thread.new do
145
+ resp = @client.get_object(
146
+ :bucket => @bucket,
147
+ :key => @key,
148
+ param.to_sym => chunk,
149
+ :response_target => file
150
+ )
151
+ end
152
+ end
153
+ threads.each(&:join)
154
+ end
155
+ concatenate_parts(parts)
156
+ ensure
157
+ # clean up tmp dir
158
+ FileUtils.remove_entry(dir)
159
+ end
160
+ end
161
+
162
+ def single_request
163
+ @client.get_object(
164
+ bucket: @bucket, key: @key, response_target: @path
165
+ )
166
+ end
167
+ end
168
+ end
169
+ end
@@ -252,7 +252,39 @@ module Aws
252
252
  uploader.upload(source, uploading_options.merge(bucket: bucket_name, key: key))
253
253
  true
254
254
  end
255
-
255
+
256
+ # Downloads a file in S3 to a path on disk.
257
+ #
258
+ # # small files (< 5MB) are downloaded in a single API call
259
+ # obj.download_file('/path/to/file')
260
+ #
261
+ # Files larger than 5MB are downloaded using multipart method
262
+ #
263
+ # # large files are split into parts
264
+ # # and the parts are downloaded in parallel
265
+ # obj.download_file('/path/to/very_large_file')
266
+ #
267
+ # @param [String] destination Where to download the file to
268
+ #
269
+ # @option options [String] mode `auto`, `single_request`, `get_range`
270
+ # `single_request` mode forces only 1 GET request is made in download,
271
+ # `get_range` mode allows `chunk_size` parameter to configured in
272
+ # customizing each range size in multipart_download,
273
+ # By default, `auto` mode is enabled, which performs multipart_download
274
+ #
275
+ # @option options [String] chunk_size required in get_range mode
276
+ #
277
+ # @option options [String] thread_count Customize threads used in multipart
278
+ # download, if not provided, 10 is default value
279
+ #
280
+ # @return [Boolean] Returns `true` when the file is downloaded
281
+ # without any errors.
282
+ def download_file(destination, options = {})
283
+ downloader = FileDownloader.new(client: client)
284
+ downloader.download(
285
+ destination, options.merge(bucket: bucket_name, key: key))
286
+ true
287
+ end
256
288
  end
257
289
  end
258
290
  end
@@ -1,4 +1,5 @@
1
1
  require 'thread'
2
+ require 'cgi'
2
3
 
3
4
  module Aws
4
5
  module S3
@@ -60,6 +60,14 @@ module Aws
60
60
  object.upload_file(source, options)
61
61
  end
62
62
 
63
+ # @param (see Object#download_file)
64
+ # @options (see Object#download_file)
65
+ # @return (see Object#download_file)
66
+ # @see Object#download_file
67
+ def download_file(destination, options = {})
68
+ object.download_file(destination, options)
69
+ end
70
+
63
71
  end
64
72
  end
65
73
  end
@@ -585,6 +585,10 @@ module Aws
585
585
  else
586
586
  url.path = '/' + @bucket_name
587
587
  end
588
+ if @bucket_region == 'us-east-1'
589
+ # keep legacy behavior by default
590
+ url.host = Plugins::S3IADRegionalEndpoint.legacy_host(url.host)
591
+ end
588
592
  url.to_s
589
593
  end
590
594
 
@@ -7,8 +7,10 @@ module Aws
7
7
  require 'aws-sdk-resources/services/s3/multipart_upload'
8
8
 
9
9
  autoload :Encryption, 'aws-sdk-resources/services/s3/encryption'
10
+ autoload :EncryptionV2, 'aws-sdk-resources/services/s3/encryption_v2'
10
11
  autoload :FilePart, 'aws-sdk-resources/services/s3/file_part'
11
12
  autoload :FileUploader, 'aws-sdk-resources/services/s3/file_uploader'
13
+ autoload :FileDownloader, 'aws-sdk-resources/services/s3/file_downloader'
12
14
  autoload :MultipartFileUploader, 'aws-sdk-resources/services/s3/multipart_file_uploader'
13
15
  autoload :MultipartUploadError, 'aws-sdk-resources/services/s3/multipart_upload_error'
14
16
  autoload :ObjectCopier, 'aws-sdk-resources/services/s3/object_copier'
@@ -59,6 +59,7 @@ module Aws
59
59
  # verification.
60
60
  def authenticate!(message_body)
61
61
  msg = Json.load(message_body)
62
+ msg = convert_lambda_msg(msg) if is_from_lambda(msg)
62
63
  if public_key(msg).verify(sha1, signature(msg), canonical_string(msg))
63
64
  true
64
65
  else
@@ -69,6 +70,19 @@ module Aws
69
70
 
70
71
  private
71
72
 
73
+ def is_from_lambda(message)
74
+ message.key? 'SigningCertUrl'
75
+ end
76
+
77
+ def convert_lambda_msg(message)
78
+ cert_url = message.delete('SigningCertUrl')
79
+ unsubscribe_url = message.delete('UnsubscribeUrl')
80
+
81
+ message['SigningCertURL'] = cert_url
82
+ message['UnsubscribeURL'] = unsubscribe_url
83
+ message
84
+ end
85
+
72
86
  def sha1
73
87
  OpenSSL::Digest::SHA1.new
74
88
  end
@@ -253,7 +253,7 @@ module Aws
253
253
  #
254
254
  # @return [void]
255
255
  def before_request(&block)
256
- @default_config = @default_config.with(before_request: Proc.new)
256
+ @default_config = @default_config.with(before_request: block) if block_given?
257
257
  end
258
258
 
259
259
  # Polls the queue, yielded a message, or an array of messages.