azure-blob 0.4.1 → 0.5.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.
- checksums.yaml +4 -4
- data/.terraform.lock.hcl +22 -0
- data/CHANGELOG.md +15 -2
- data/README.md +94 -22
- data/Rakefile +51 -1
- data/azure-blob.gemspec +3 -2
- data/devenv.lock +11 -11
- data/devenv.nix +27 -3
- data/devenv.yaml +1 -1
- data/input.tf +44 -0
- data/lib/active_storage/service/azure_blob_service.rb +4 -4
- data/lib/azure_blob/blob.rb +7 -0
- data/lib/azure_blob/blob_list.rb +18 -0
- data/lib/azure_blob/block_list.rb +3 -1
- data/lib/azure_blob/canonicalized_headers.rb +1 -1
- data/lib/azure_blob/canonicalized_resource.rb +1 -1
- data/lib/azure_blob/client.rb +126 -5
- data/lib/azure_blob/entra_id_signer.rb +115 -0
- data/lib/azure_blob/http.rb +19 -3
- data/lib/azure_blob/identity_token.rb +65 -0
- data/lib/azure_blob/metadata.rb +1 -1
- data/lib/azure_blob/{signer.rb → shared_key_signer.rb} +6 -6
- data/lib/azure_blob/user_delegation_key.rb +67 -0
- data/lib/azure_blob/version.rb +1 -1
- data/main.tf +187 -0
- data/output.tf +30 -0
- metadata +14 -6
data/lib/azure_blob/client.rb
CHANGED
@@ -1,21 +1,48 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "signer"
|
4
3
|
require_relative "block_list"
|
5
4
|
require_relative "blob_list"
|
6
5
|
require_relative "blob"
|
7
6
|
require_relative "http"
|
7
|
+
require_relative "shared_key_signer"
|
8
|
+
require_relative "entra_id_signer"
|
8
9
|
require "time"
|
9
10
|
require "base64"
|
10
11
|
|
11
12
|
module AzureBlob
|
13
|
+
# AzureBlob Client class. You interact with the Azure Blob api
|
14
|
+
# through an instance of this class.
|
12
15
|
class Client
|
13
|
-
def initialize(account_name:, access_key:, container
|
16
|
+
def initialize(account_name:, access_key:, container:, **options)
|
14
17
|
@account_name = account_name
|
15
18
|
@container = container
|
16
|
-
|
19
|
+
|
20
|
+
@signer = !access_key.nil? && !access_key.empty? ?
|
21
|
+
AzureBlob::SharedKeySigner.new(account_name:, access_key:) :
|
22
|
+
AzureBlob::EntraIdSigner.new(account_name:, **options.slice(:principal_id))
|
17
23
|
end
|
18
24
|
|
25
|
+
# Create a blob of type block. Will automatically split the the blob in multiple block and send the blob in pieces (blocks) if the blob is too big.
|
26
|
+
#
|
27
|
+
# When the blob is small enough this method will send the blob through {Put Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-blob]
|
28
|
+
#
|
29
|
+
# If the blob is too big, the blob is split in blocks sent through a series of {Put Block}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block] requests
|
30
|
+
# followed by a {Put Block List}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list] to commit the block list.
|
31
|
+
#
|
32
|
+
# Takes a key (path), the content (String or IO object), and options.
|
33
|
+
#
|
34
|
+
# Options:
|
35
|
+
#
|
36
|
+
# [+:content_type+]
|
37
|
+
# Will be saved on the blob in Azure.
|
38
|
+
# [+:content_disposition+]
|
39
|
+
# Will be saved on the blob in Azure.
|
40
|
+
# [+:content_md5+]
|
41
|
+
# Will ensure integrity of the upload. The checksum must be a base64 digest. Can be produced with +OpenSSL::Digest::MD5.base64digest+.
|
42
|
+
# The checksum is only checked on a single upload! To verify checksum when uploading multiple blocks, call directly put_blob_block with
|
43
|
+
# a checksum for each block, then commit the blocks with commit_blob_blocks.
|
44
|
+
# [+:block_size+]
|
45
|
+
# Block size in bytes, can be used to force the method to split the upload in smaller chunk. Defaults to +AzureBlob::DEFAULT_BLOCK_SIZE+ and cannot be bigger than +AzureBlob::MAX_UPLOAD_SIZE+
|
19
46
|
def create_block_blob(key, content, options = {})
|
20
47
|
if content.size > (options[:block_size] || DEFAULT_BLOCK_SIZE)
|
21
48
|
put_blob_multiple(key, content, **options)
|
@@ -24,6 +51,18 @@ module AzureBlob
|
|
24
51
|
end
|
25
52
|
end
|
26
53
|
|
54
|
+
# Returns the full or partial content of the blob
|
55
|
+
#
|
56
|
+
# Calls to the {Get Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob] endpoint.
|
57
|
+
#
|
58
|
+
# Takes a key (path) and options.
|
59
|
+
#
|
60
|
+
# Options:
|
61
|
+
#
|
62
|
+
# [+:start+]
|
63
|
+
# Starting point in bytes
|
64
|
+
# [+:end+]
|
65
|
+
# Ending point in bytes
|
27
66
|
def get_blob(key, options = {})
|
28
67
|
uri = generate_uri("#{container}/#{key}")
|
29
68
|
|
@@ -34,6 +73,15 @@ module AzureBlob
|
|
34
73
|
Http.new(uri, headers, signer:).get
|
35
74
|
end
|
36
75
|
|
76
|
+
# Delete a blob
|
77
|
+
#
|
78
|
+
# Calls to {Delete Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/delete-blob]
|
79
|
+
#
|
80
|
+
# Takes a key (path) and options.
|
81
|
+
#
|
82
|
+
# Options:
|
83
|
+
# [+:delete_snapshots+]
|
84
|
+
# Sets the value of the x-ms-delete-snapshots header. Default to +include+
|
37
85
|
def delete_blob(key, options = {})
|
38
86
|
uri = generate_uri("#{container}/#{key}")
|
39
87
|
|
@@ -44,11 +92,28 @@ module AzureBlob
|
|
44
92
|
Http.new(uri, headers, signer:).delete
|
45
93
|
end
|
46
94
|
|
95
|
+
# Delete all blobs prefixed by the given prefix.
|
96
|
+
#
|
97
|
+
# Calls to {List blobs}[https://learn.microsoft.com/en-us/rest/api/storageservices/list-blobs]
|
98
|
+
# followed to a series of calls to {Delete Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/delete-blob]
|
99
|
+
#
|
100
|
+
# Takes a prefix and options
|
101
|
+
#
|
102
|
+
# Look delete_blob for the list of options.
|
47
103
|
def delete_prefix(prefix, options = {})
|
48
104
|
results = list_blobs(prefix:)
|
49
105
|
results.each { |key| delete_blob(key) }
|
50
106
|
end
|
51
107
|
|
108
|
+
# Returns a BlobList containing a list of keys (paths)
|
109
|
+
#
|
110
|
+
# Calls to {List blobs}[https://learn.microsoft.com/en-us/rest/api/storageservices/list-blobs]
|
111
|
+
#
|
112
|
+
# Options:
|
113
|
+
# [+:prefix+]
|
114
|
+
# Prefix of the blobs to be listed. Defaults to listing everything in the container.
|
115
|
+
# [:+max_results+]
|
116
|
+
# Maximum number of results to return per page.
|
52
117
|
def list_blobs(options = {})
|
53
118
|
uri = generate_uri(container)
|
54
119
|
query = {
|
@@ -69,6 +134,11 @@ module AzureBlob
|
|
69
134
|
BlobList.new(fetcher)
|
70
135
|
end
|
71
136
|
|
137
|
+
# Returns a Blob object without the content.
|
138
|
+
#
|
139
|
+
# Calls to {Get Blob Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-properties]
|
140
|
+
#
|
141
|
+
# This can be used to see if the blob exist or obtain metada such as content type, disposition, checksum or Azure custom metadata.
|
72
142
|
def get_blob_properties(key, options = {})
|
73
143
|
uri = generate_uri("#{container}/#{key}")
|
74
144
|
|
@@ -77,16 +147,37 @@ module AzureBlob
|
|
77
147
|
Blob.new(response)
|
78
148
|
end
|
79
149
|
|
150
|
+
# Return a URI object to a resource in the container. Takes a path.
|
151
|
+
#
|
152
|
+
# Example: +generate_uri("#{container}/#{key}")+
|
80
153
|
def generate_uri(path)
|
81
154
|
URI.parse(URI::DEFAULT_PARSER.escape(File.join(host, path)))
|
82
155
|
end
|
83
156
|
|
157
|
+
# Returns an SAS signed URI
|
158
|
+
#
|
159
|
+
# Takes a
|
160
|
+
# - key (path)
|
161
|
+
# - A permission string (+"r"+, +"rw"+)
|
162
|
+
# - expiry as a UTC iso8601 time string
|
163
|
+
# - options
|
84
164
|
def signed_uri(key, permissions:, expiry:, **options)
|
85
165
|
uri = generate_uri("#{container}/#{key}")
|
86
166
|
uri.query = signer.sas_token(uri, permissions:, expiry:, **options)
|
87
167
|
uri
|
88
168
|
end
|
89
169
|
|
170
|
+
# Creates a Blob of type append.
|
171
|
+
#
|
172
|
+
# Calls to {Put Blob}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-blob]
|
173
|
+
#
|
174
|
+
# You are expected to append blocks to the blob with append_blob_block after creating the blob.
|
175
|
+
# Options:
|
176
|
+
#
|
177
|
+
# [+:content_type+]
|
178
|
+
# Will be saved on the blob in Azure.
|
179
|
+
# [+:content_disposition+]
|
180
|
+
# Will be saved on the blob in Azure.
|
90
181
|
def create_append_blob(key, options = {})
|
91
182
|
uri = generate_uri("#{container}/#{key}")
|
92
183
|
|
@@ -101,6 +192,15 @@ module AzureBlob
|
|
101
192
|
Http.new(uri, headers, metadata: options[:metadata], signer:).put(nil)
|
102
193
|
end
|
103
194
|
|
195
|
+
# Append a block to an Append Blob
|
196
|
+
#
|
197
|
+
# Calls to {Append Block}[https://learn.microsoft.com/en-us/rest/api/storageservices/append-block]
|
198
|
+
#
|
199
|
+
# Options:
|
200
|
+
#
|
201
|
+
# [+:content_md5+]
|
202
|
+
# Will ensure integrity of the upload. The checksum must be a base64 digest. Can be produced with +OpenSSL::Digest::MD5.base64digest+.
|
203
|
+
# The checksum must be the checksum of the block not the blob.
|
104
204
|
def append_blob_block(key, content, options = {})
|
105
205
|
uri = generate_uri("#{container}/#{key}")
|
106
206
|
uri.query = URI.encode_www_form(comp: "appendblock")
|
@@ -114,6 +214,16 @@ module AzureBlob
|
|
114
214
|
Http.new(uri, headers, signer:).put(content)
|
115
215
|
end
|
116
216
|
|
217
|
+
# Uploads a block to a blob.
|
218
|
+
#
|
219
|
+
# Calls to {Put Block}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block]
|
220
|
+
#
|
221
|
+
# Returns the id of the block. Required to commit the list of blocks to a blob.
|
222
|
+
#
|
223
|
+
# Options:
|
224
|
+
#
|
225
|
+
# [+:content_md5+]
|
226
|
+
# Must be the checksum for the block not the blob. The checksum must be a base64 digest. Can be produced with +OpenSSL::Digest::MD5.base64digest+.
|
117
227
|
def put_blob_block(key, index, content, options = {})
|
118
228
|
block_id = generate_block_id(index)
|
119
229
|
uri = generate_uri("#{container}/#{key}")
|
@@ -130,6 +240,17 @@ module AzureBlob
|
|
130
240
|
block_id
|
131
241
|
end
|
132
242
|
|
243
|
+
# Commits the list of blocks to a blob.
|
244
|
+
#
|
245
|
+
# Calls to {Put Block List}[https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list]
|
246
|
+
#
|
247
|
+
# Takes a key (path) and an array of block ids
|
248
|
+
#
|
249
|
+
# Options:
|
250
|
+
#
|
251
|
+
# [+:content_md5+]
|
252
|
+
# This is the checksum for the whole blob. The checksum is saved on the blob, but it is not validated!
|
253
|
+
# Add a checksum for each block if you want Azure to validate integrity.
|
133
254
|
def commit_blob_blocks(key, block_ids, options = {})
|
134
255
|
block_list = BlockList.new(block_ids)
|
135
256
|
content = block_list.to_s
|
@@ -139,7 +260,7 @@ module AzureBlob
|
|
139
260
|
headers = {
|
140
261
|
"Content-Length": content.size,
|
141
262
|
"Content-Type": options[:content_type],
|
142
|
-
"
|
263
|
+
"x-ms-blob-content-md5": options[:content_md5],
|
143
264
|
"x-ms-blob-content-disposition": options[:content_disposition],
|
144
265
|
}
|
145
266
|
|
@@ -171,7 +292,7 @@ module AzureBlob
|
|
171
292
|
"x-ms-blob-type": "BlockBlob",
|
172
293
|
"Content-Length": content.size,
|
173
294
|
"Content-Type": options[:content_type],
|
174
|
-
"
|
295
|
+
"x-ms-blob-content-md5": options[:content_md5],
|
175
296
|
"x-ms-blob-content-disposition": options[:content_disposition],
|
176
297
|
}
|
177
298
|
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "openssl"
|
3
|
+
require "net/http"
|
4
|
+
require "rexml/document"
|
5
|
+
|
6
|
+
require_relative "canonicalized_resource"
|
7
|
+
require_relative "identity_token"
|
8
|
+
|
9
|
+
require_relative "user_delegation_key"
|
10
|
+
|
11
|
+
module AzureBlob
|
12
|
+
class EntraIdSigner # :nodoc:
|
13
|
+
attr_reader :token
|
14
|
+
attr_reader :account_name
|
15
|
+
|
16
|
+
def initialize(account_name:, principal_id: nil)
|
17
|
+
@token = AzureBlob::IdentityToken.new(principal_id:)
|
18
|
+
@account_name = account_name
|
19
|
+
end
|
20
|
+
|
21
|
+
def authorization_header(uri:, verb:, headers: {})
|
22
|
+
"Bearer #{token}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def sas_token(uri, options = {})
|
26
|
+
to_sign = [
|
27
|
+
options[:permissions],
|
28
|
+
options[:start],
|
29
|
+
options[:expiry],
|
30
|
+
CanonicalizedResource.new(uri, account_name, url_safe: false, service_name: :blob),
|
31
|
+
delegation_key.signed_oid,
|
32
|
+
delegation_key.signed_tid,
|
33
|
+
delegation_key.signed_start,
|
34
|
+
delegation_key.signed_expiry,
|
35
|
+
delegation_key.signed_service,
|
36
|
+
delegation_key.signed_version,
|
37
|
+
nil,
|
38
|
+
nil,
|
39
|
+
nil,
|
40
|
+
options[:ip],
|
41
|
+
options[:protocol],
|
42
|
+
SAS::Version,
|
43
|
+
SAS::Resources::Blob,
|
44
|
+
nil,
|
45
|
+
nil,
|
46
|
+
nil,
|
47
|
+
options[:content_disposition],
|
48
|
+
nil,
|
49
|
+
nil,
|
50
|
+
options[:content_type],
|
51
|
+
].join("\n")
|
52
|
+
|
53
|
+
query = {
|
54
|
+
SAS::Fields::Permissions => options[:permissions],
|
55
|
+
SAS::Fields::Start => options[:start],
|
56
|
+
SAS::Fields::Expiry => options[:expiry],
|
57
|
+
|
58
|
+
SAS::Fields::SignedObjectId => delegation_key.signed_oid,
|
59
|
+
SAS::Fields::SignedTenantId => delegation_key.signed_tid,
|
60
|
+
SAS::Fields::SignedKeyStartTime => delegation_key.signed_start,
|
61
|
+
SAS::Fields::SignedKeyExpiryTime => delegation_key.signed_expiry,
|
62
|
+
SAS::Fields::SignedKeyService => delegation_key.signed_service,
|
63
|
+
SAS::Fields::Signedkeyversion => delegation_key.signed_version,
|
64
|
+
|
65
|
+
|
66
|
+
SAS::Fields::SignedIp => options[:ip],
|
67
|
+
SAS::Fields::SignedProtocol => options[:protocol],
|
68
|
+
SAS::Fields::Version => SAS::Version,
|
69
|
+
SAS::Fields::Resource => SAS::Resources::Blob,
|
70
|
+
|
71
|
+
SAS::Fields::Disposition => options[:content_disposition],
|
72
|
+
SAS::Fields::Type => options[:content_type],
|
73
|
+
SAS::Fields::Signature => sign(to_sign, key: delegation_key.to_s),
|
74
|
+
|
75
|
+
}.reject { |_, value| value.nil? }
|
76
|
+
|
77
|
+
URI.encode_www_form(**query)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def delegation_key
|
83
|
+
@delegation_key ||= UserDelegationKey.new(account_name:, signer: self)
|
84
|
+
end
|
85
|
+
|
86
|
+
def sign(body, key:)
|
87
|
+
Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", key, body))
|
88
|
+
end
|
89
|
+
|
90
|
+
module SAS # :nodoc:
|
91
|
+
Version = "2024-05-04"
|
92
|
+
module Fields # :nodoc:
|
93
|
+
Permissions = :sp
|
94
|
+
Version = :sv
|
95
|
+
Start = :st
|
96
|
+
Expiry = :se
|
97
|
+
Resource = :sr
|
98
|
+
Signature = :sig
|
99
|
+
Disposition = :rscd
|
100
|
+
Type = :rsct
|
101
|
+
SignedObjectId = :skoid
|
102
|
+
SignedTenantId = :sktid
|
103
|
+
SignedKeyStartTime = :skt
|
104
|
+
SignedKeyExpiryTime = :ske
|
105
|
+
SignedKeyService = :sks
|
106
|
+
Signedkeyversion = :skv
|
107
|
+
SignedIp = :sip
|
108
|
+
SignedProtocol = :spr
|
109
|
+
end
|
110
|
+
module Resources # :nodoc:
|
111
|
+
Blob = :b
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
data/lib/azure_blob/http.rb
CHANGED
@@ -6,8 +6,15 @@ require "net/http"
|
|
6
6
|
require "rexml"
|
7
7
|
|
8
8
|
module AzureBlob
|
9
|
-
class Http
|
10
|
-
class Error < AzureBlob::Error
|
9
|
+
class Http # :nodoc:
|
10
|
+
class Error < AzureBlob::Error
|
11
|
+
attr_reader :body, :status
|
12
|
+
def initialize(body: nil, status: nil)
|
13
|
+
@body = body
|
14
|
+
@status = status
|
15
|
+
super(body)
|
16
|
+
end
|
17
|
+
end
|
11
18
|
class FileNotFoundError < Error; end
|
12
19
|
class ForbidenError < Error; end
|
13
20
|
class IntegrityError < Error; end
|
@@ -44,6 +51,15 @@ module AzureBlob
|
|
44
51
|
true
|
45
52
|
end
|
46
53
|
|
54
|
+
def post(content)
|
55
|
+
sign_request("POST") if signer
|
56
|
+
@response = http.start do |http|
|
57
|
+
http.post(uri, content, headers)
|
58
|
+
end
|
59
|
+
raise_error unless success?
|
60
|
+
response.body
|
61
|
+
end
|
62
|
+
|
47
63
|
def head
|
48
64
|
sign_request("HEAD") if signer
|
49
65
|
@response = http.start do |http|
|
@@ -91,7 +107,7 @@ module AzureBlob
|
|
91
107
|
end
|
92
108
|
|
93
109
|
def raise_error
|
94
|
-
raise error_from_response.new(@response.body)
|
110
|
+
raise error_from_response.new(body: @response.body, status: @response.code&.to_i)
|
95
111
|
end
|
96
112
|
|
97
113
|
def status
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module AzureBlob
|
4
|
+
class IdentityToken
|
5
|
+
RESOURCE_URI = "https://storage.azure.com/"
|
6
|
+
EXPIRATION_BUFFER = 600 # 10 minutes
|
7
|
+
|
8
|
+
IDENTITY_ENDPOINT = ENV["IDENTITY_ENDPOINT"] || "http://169.254.169.254/metadata/identity/oauth2/token"
|
9
|
+
API_VERSION = ENV["IDENTITY_ENDPOINT"] ? "2019-08-01" : "2018-02-01"
|
10
|
+
|
11
|
+
def initialize(principal_id: nil)
|
12
|
+
@identity_uri = URI.parse(IDENTITY_ENDPOINT)
|
13
|
+
params = {
|
14
|
+
'api-version': API_VERSION,
|
15
|
+
resource: RESOURCE_URI,
|
16
|
+
}
|
17
|
+
params[:principal_id] = principal_id if principal_id
|
18
|
+
@identity_uri.query = URI.encode_www_form(params)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
refresh if expired?
|
23
|
+
token
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def expired?
|
29
|
+
token.nil? || Time.now >= (expiration - EXPIRATION_BUFFER)
|
30
|
+
end
|
31
|
+
|
32
|
+
def refresh
|
33
|
+
headers = { "Metadata" => "true" }
|
34
|
+
headers["X-IDENTITY-HEADER"] = ENV["IDENTITY_HEADER"] if ENV["IDENTITY_HEADER"]
|
35
|
+
|
36
|
+
attempt = 0
|
37
|
+
begin
|
38
|
+
attempt += 1
|
39
|
+
response = JSON.parse(AzureBlob::Http.new(identity_uri, headers).get)
|
40
|
+
rescue AzureBlob::Http::Error => error
|
41
|
+
if should_retry?(error, attempt)
|
42
|
+
attempt = 1 if error.status == 410
|
43
|
+
delay = exponential_backoff(error, attempt)
|
44
|
+
Kernel.sleep(delay)
|
45
|
+
retry
|
46
|
+
end
|
47
|
+
raise
|
48
|
+
end
|
49
|
+
@token = response["access_token"]
|
50
|
+
@expiration = Time.at(response["expires_on"].to_i)
|
51
|
+
end
|
52
|
+
|
53
|
+
def should_retry?(error, attempt)
|
54
|
+
is_500 = error.status/500 == 1
|
55
|
+
(is_500 || [ 404, 408, 410, 429 ].include?(error.status)) && attempt < 5
|
56
|
+
end
|
57
|
+
|
58
|
+
def exponential_backoff(error, attempt)
|
59
|
+
EXPONENTIAL_BACKOFF[attempt -1] || raise(AzureBlob::Error.new("Exponential backoff out of bounds!"))
|
60
|
+
end
|
61
|
+
EXPONENTIAL_BACKOFF = [ 2, 6, 14, 30 ]
|
62
|
+
|
63
|
+
attr_reader :identity_uri, :expiration, :token
|
64
|
+
end
|
65
|
+
end
|
data/lib/azure_blob/metadata.rb
CHANGED
@@ -6,7 +6,7 @@ require_relative "canonicalized_headers"
|
|
6
6
|
require_relative "canonicalized_resource"
|
7
7
|
|
8
8
|
module AzureBlob
|
9
|
-
class
|
9
|
+
class SharedKeySigner # :nodoc:
|
10
10
|
def initialize(account_name:, access_key:)
|
11
11
|
@account_name = account_name
|
12
12
|
@access_key = Base64.decode64(access_key)
|
@@ -71,21 +71,21 @@ module AzureBlob
|
|
71
71
|
URI.encode_www_form(**query)
|
72
72
|
end
|
73
73
|
|
74
|
+
private
|
75
|
+
|
74
76
|
def sign(body)
|
75
77
|
Base64.strict_encode64(OpenSSL::HMAC.digest("sha256", access_key, body))
|
76
78
|
end
|
77
79
|
|
78
|
-
private
|
79
|
-
|
80
80
|
def sanitize_headers(headers)
|
81
81
|
headers = headers.dup
|
82
82
|
headers[:"Content-Length"] = nil if headers[:"Content-Length"].to_i == 0
|
83
83
|
headers
|
84
84
|
end
|
85
85
|
|
86
|
-
module SAS
|
86
|
+
module SAS # :nodoc:
|
87
87
|
Version = "2024-05-04"
|
88
|
-
module Fields
|
88
|
+
module Fields # :nodoc:
|
89
89
|
Permissions = :sp
|
90
90
|
Version = :sv
|
91
91
|
Expiry = :se
|
@@ -94,7 +94,7 @@ module AzureBlob
|
|
94
94
|
Disposition = :rscd
|
95
95
|
Type = :rsct
|
96
96
|
end
|
97
|
-
module Resources
|
97
|
+
module Resources # :nodoc:
|
98
98
|
Blob = :b
|
99
99
|
end
|
100
100
|
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require_relative "http"
|
2
|
+
|
3
|
+
module AzureBlob
|
4
|
+
class UserDelegationKey # :nodoc:
|
5
|
+
EXPIRATION = 25200 # 7 hours
|
6
|
+
EXPIRATION_BUFFER = 3600 # 1 hours
|
7
|
+
def initialize(account_name:, signer:)
|
8
|
+
@uri = URI.parse(
|
9
|
+
"https://#{account_name}.blob.core.windows.net/?restype=service&comp=userdelegationkey"
|
10
|
+
)
|
11
|
+
|
12
|
+
@signer = signer
|
13
|
+
|
14
|
+
refresh
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
user_delegation_key
|
19
|
+
end
|
20
|
+
|
21
|
+
def refresh
|
22
|
+
return unless expired?
|
23
|
+
now = Time.now.utc
|
24
|
+
|
25
|
+
|
26
|
+
start = now.iso8601
|
27
|
+
@expiration = (now + EXPIRATION)
|
28
|
+
expiry = @expiration.iso8601
|
29
|
+
|
30
|
+
content = <<-XML.gsub!(/[[:space:]]+/, " ").strip!
|
31
|
+
<?xml version="1.0" encoding="utf-8"?>
|
32
|
+
<KeyInfo>
|
33
|
+
<Start>#{start}</Start>
|
34
|
+
<Expiry>#{expiry}</Expiry>
|
35
|
+
</KeyInfo>
|
36
|
+
XML
|
37
|
+
|
38
|
+
response = Http.new(uri, signer:).post(content)
|
39
|
+
|
40
|
+
doc = REXML::Document.new(response)
|
41
|
+
|
42
|
+
@signed_oid = doc.get_elements("/UserDelegationKey/SignedOid").first.get_text.to_s
|
43
|
+
@signed_tid = doc.get_elements("/UserDelegationKey/SignedTid").first.get_text.to_s
|
44
|
+
@signed_start = doc.get_elements("/UserDelegationKey/SignedStart").first.get_text.to_s
|
45
|
+
@signed_expiry = doc.get_elements("/UserDelegationKey/SignedExpiry").first.get_text.to_s
|
46
|
+
@signed_service = doc.get_elements("/UserDelegationKey/SignedService").first.get_text.to_s
|
47
|
+
@signed_version = doc.get_elements("/UserDelegationKey/SignedVersion").first.get_text.to_s
|
48
|
+
@user_delegation_key = Base64.decode64(doc.get_elements("/UserDelegationKey/Value").first.get_text.to_s)
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :signed_oid,
|
52
|
+
:signed_tid,
|
53
|
+
:signed_start,
|
54
|
+
:signed_expiry,
|
55
|
+
:signed_service,
|
56
|
+
:signed_version,
|
57
|
+
:user_delegation_key
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def expired?
|
62
|
+
expiration.nil? || Time.now >= (expiration - EXPIRATION_BUFFER)
|
63
|
+
end
|
64
|
+
|
65
|
+
attr_reader :uri, :user_delegation_key, :signer, :expiration
|
66
|
+
end
|
67
|
+
end
|
data/lib/azure_blob/version.rb
CHANGED