aws-sdk-resources 2.8.4 → 2.11.632

Sign up to get free protection for your applications and to get access to all the features.
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.