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.
- checksums.yaml +5 -5
- data/lib/aws-sdk-resources/documenter/has_many_operation_documenter.rb +1 -1
- data/lib/aws-sdk-resources/services/s3/bucket.rb +4 -0
- data/lib/aws-sdk-resources/services/s3/encryption/client.rb +24 -7
- data/lib/aws-sdk-resources/services/s3/encryption/decrypt_handler.rb +77 -26
- data/lib/aws-sdk-resources/services/s3/encryption/default_cipher_provider.rb +43 -5
- data/lib/aws-sdk-resources/services/s3/encryption/default_key_provider.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/encrypt_handler.rb +13 -2
- data/lib/aws-sdk-resources/services/s3/encryption/errors.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/io_auth_decrypter.rb +11 -3
- data/lib/aws-sdk-resources/services/s3/encryption/io_decrypter.rb +11 -3
- data/lib/aws-sdk-resources/services/s3/encryption/io_encrypter.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/key_provider.rb +2 -0
- data/lib/aws-sdk-resources/services/s3/encryption/kms_cipher_provider.rb +36 -3
- data/lib/aws-sdk-resources/services/s3/encryption/materials.rb +9 -7
- data/lib/aws-sdk-resources/services/s3/encryption/utils.rb +25 -0
- data/lib/aws-sdk-resources/services/s3/encryption.rb +3 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/client.rb +561 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/decrypt_handler.rb +214 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/default_cipher_provider.rb +170 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/default_key_provider.rb +40 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/encrypt_handler.rb +69 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/errors.rb +37 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/io_auth_decrypter.rb +58 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/io_decrypter.rb +37 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/io_encrypter.rb +73 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/key_provider.rb +31 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/kms_cipher_provider.rb +169 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/materials.rb +60 -0
- data/lib/aws-sdk-resources/services/s3/encryptionV2/utils.rb +103 -0
- data/lib/aws-sdk-resources/services/s3/encryption_v2.rb +24 -0
- data/lib/aws-sdk-resources/services/s3/file_downloader.rb +169 -0
- data/lib/aws-sdk-resources/services/s3/object.rb +33 -1
- data/lib/aws-sdk-resources/services/s3/object_multipart_copier.rb +1 -0
- data/lib/aws-sdk-resources/services/s3/object_summary.rb +8 -0
- data/lib/aws-sdk-resources/services/s3/presigned_post.rb +4 -0
- data/lib/aws-sdk-resources/services/s3.rb +2 -0
- data/lib/aws-sdk-resources/services/sns/message_verifier.rb +14 -0
- data/lib/aws-sdk-resources/services/sqs/queue_poller.rb +1 -1
- 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
|
@@ -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
|
@@ -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:
|
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.
|