azure-blob 0.5.1 → 0.5.3
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/CHANGELOG.md +13 -0
- data/README.md +17 -6
- data/lib/azure_blob/blob.rb +2 -2
- data/lib/azure_blob/client.rb +67 -11
- data/lib/azure_blob/const.rb +8 -2
- data/lib/azure_blob/container.rb +33 -0
- data/lib/azure_blob/entra_id_signer.rb +3 -1
- data/lib/azure_blob/http.rb +16 -7
- data/lib/azure_blob/tags.rb +35 -0
- data/lib/azure_blob/user_delegation_key.rb +1 -1
- data/lib/azure_blob/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6c4e076840dd6671c28c99b5fca03057782e81686c0c06890b42cedd3b83788
|
4
|
+
data.tar.gz: '00668582cecce526ad816a5c537469af7e646d6f64d7392655d1c7e60396393c'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fdd32b7f8547ac428bf0eb19d884860e94d1ab9471df0022356119c5ec28aca8589d1b1925d0085b2361c4c16d5898123a1fc18f57b5b2cb9ae7e53dbc4b4379
|
7
|
+
data.tar.gz: 44a4592046d5d33432762cfd4d3636500f9c13db00cf5c705fbc2f472b7c322bd37bc05d9765e03079e0966dcb44d0e5cc844db303906aaa18435c8db7ee422f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
|
4
|
+
## [0.5.3] 2024-10-31
|
5
|
+
|
6
|
+
- Add support for setting tags when uploading a blob
|
7
|
+
- Add get_blob_tags
|
8
|
+
|
9
|
+
## [0.5.2] 2024-09-12
|
10
|
+
|
11
|
+
- Add get_container_properties
|
12
|
+
- Add create_container
|
13
|
+
- Add delete_container
|
14
|
+
- Support for Azure China, US Gov and Germany
|
15
|
+
|
3
16
|
## [0.5.1] 2024-09-09
|
4
17
|
|
5
18
|
- Remove dev files from the release
|
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# AzureBlob
|
2
2
|
|
3
|
-
|
3
|
+
Azure Blob client and Active Storage adapter to replace the now abandoned azure-storage-blob
|
4
|
+
|
5
|
+
An Active Storage is supplied, but the gem is Rails agnostic and can be used in any Ruby project.
|
4
6
|
|
5
7
|
## Active Storage
|
6
8
|
|
@@ -12,6 +14,16 @@ To migrate from azure-storage-blob to azure-blob:
|
|
12
14
|
3. Change the `AzureStorage` service to `AzureBlob` in your Active Storage config (`config/storage.yml`)
|
13
15
|
4. Restart or deploy the app.
|
14
16
|
|
17
|
+
Example config:
|
18
|
+
|
19
|
+
```
|
20
|
+
microsoft:
|
21
|
+
service: AzureBlob
|
22
|
+
storage_account_name: account_name
|
23
|
+
storage_access_key: SECRET_KEY
|
24
|
+
container: container_name
|
25
|
+
```
|
26
|
+
|
15
27
|
### Managed Identity (Entra ID)
|
16
28
|
|
17
29
|
AzureBlob supports managed identities on :
|
@@ -22,9 +34,7 @@ AzureBlob supports managed identities on :
|
|
22
34
|
|
23
35
|
AKS support will likely require more work. Contributions are welcome.
|
24
36
|
|
25
|
-
To authenticate through managed identities instead of a shared key, omit `storage_access_key` from your `storage.yml` file
|
26
|
-
|
27
|
-
It is recommended to add the identity's `principal_id` to the config.
|
37
|
+
To authenticate through managed identities instead of a shared key, omit `storage_access_key` from your `storage.yml` file and pass in the identity `principal_id`.
|
28
38
|
|
29
39
|
ActiveStorage config example:
|
30
40
|
|
@@ -79,11 +89,12 @@ A dev environment is supplied through Nix with [devenv](https://devenv.sh/).
|
|
79
89
|
|
80
90
|
To test with Entra ID, the `AZURE_ACCESS_KEY` environment variable must be unset and the code must be ran or proxied through a VPS with the proper roles.
|
81
91
|
|
82
|
-
For cost saving, the terraform variable `create_vm`
|
83
|
-
To create the VPS, Create a var file `var.tfvars` containing:
|
92
|
+
For cost saving, the terraform variable `create_vm` and `create_app_service` are false by default.
|
93
|
+
To create the VPS and App service, Create a var file `var.tfvars` containing:
|
84
94
|
|
85
95
|
```
|
86
96
|
create_vm = true
|
97
|
+
create_app_service = true
|
87
98
|
```
|
88
99
|
and re-apply terraform: `terraform apply -var-file=var.tfvars`.
|
89
100
|
|
data/lib/azure_blob/blob.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module AzureBlob
|
4
|
-
# AzureBlob::Blob holds the
|
4
|
+
# AzureBlob::Blob holds the metadata for a given Blob.
|
5
5
|
class Blob
|
6
6
|
# You should not instanciate this object directly,
|
7
7
|
# but obtain one when calling relevant methods of AzureBlob::Client.
|
@@ -32,7 +32,7 @@ module AzureBlob
|
|
32
32
|
response.code == "200"
|
33
33
|
end
|
34
34
|
|
35
|
-
# Returns the custom Azure
|
35
|
+
# Returns the custom Azure metadata tagged on the blob.
|
36
36
|
def metadata
|
37
37
|
@metadata || response
|
38
38
|
.to_hash
|
data/lib/azure_blob/client.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
require_relative "block_list"
|
4
4
|
require_relative "blob_list"
|
5
5
|
require_relative "blob"
|
6
|
+
require_relative "container"
|
7
|
+
require_relative "tags"
|
6
8
|
require_relative "http"
|
7
9
|
require_relative "shared_key_signer"
|
8
10
|
require_relative "entra_id_signer"
|
@@ -13,13 +15,22 @@ module AzureBlob
|
|
13
15
|
# AzureBlob Client class. You interact with the Azure Blob api
|
14
16
|
# through an instance of this class.
|
15
17
|
class Client
|
16
|
-
def initialize(account_name:, access_key
|
18
|
+
def initialize(account_name:, access_key: nil, principal_id: nil, container:, **options)
|
17
19
|
@account_name = account_name
|
18
20
|
@container = container
|
21
|
+
@cloud_regions = options[:cloud_regions]&.to_sym || :global
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
+
no_access_key = access_key.nil? || access_key&.empty?
|
24
|
+
using_managed_identities = no_access_key && !principal_id.nil? || options[:use_managed_identities]
|
25
|
+
|
26
|
+
if !using_managed_identities && no_access_key
|
27
|
+
raise AzureBlob::Error.new(
|
28
|
+
"`access_key` cannot be empty. To use managed identities instead, pass a `principal_id` or set `use_managed_identities` to true."
|
29
|
+
)
|
30
|
+
end
|
31
|
+
@signer = using_managed_identities ?
|
32
|
+
AzureBlob::EntraIdSigner.new(account_name:, host:, principal_id:) :
|
33
|
+
AzureBlob::SharedKeySigner.new(account_name:, access_key:)
|
23
34
|
end
|
24
35
|
|
25
36
|
# 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.
|
@@ -138,7 +149,7 @@ module AzureBlob
|
|
138
149
|
#
|
139
150
|
# Calls to {Get Blob Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-properties]
|
140
151
|
#
|
141
|
-
# This can be used to see if the blob exist or obtain
|
152
|
+
# This can be used to see if the blob exist or obtain metadata such as content type, disposition, checksum or Azure custom metadata.
|
142
153
|
def get_blob_properties(key, options = {})
|
143
154
|
uri = generate_uri("#{container}/#{key}")
|
144
155
|
|
@@ -147,6 +158,51 @@ module AzureBlob
|
|
147
158
|
Blob.new(response)
|
148
159
|
end
|
149
160
|
|
161
|
+
# Returns the tags associated with a blob
|
162
|
+
#
|
163
|
+
# Calls to the {Get Blob Tags}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-tags] endpoint.
|
164
|
+
#
|
165
|
+
# Takes a key (path) of the blob.
|
166
|
+
#
|
167
|
+
# Returns a hash of the blob's tags.
|
168
|
+
def get_blob_tags(key)
|
169
|
+
uri = generate_uri("#{container}/#{key}?comp=tags")
|
170
|
+
response = Http.new(uri, signer:).get
|
171
|
+
|
172
|
+
Tags.from_response(response).to_h
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns a Container object.
|
176
|
+
#
|
177
|
+
# Calls to {Get Container Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-container-properties]
|
178
|
+
#
|
179
|
+
# This can be used to see if the container exist or obtain metadata.
|
180
|
+
def get_container_properties(options = {})
|
181
|
+
uri = generate_uri(container)
|
182
|
+
uri.query = URI.encode_www_form(restype: "container")
|
183
|
+
response = Http.new(uri, signer:, raise_on_error: false).head
|
184
|
+
|
185
|
+
Container.new(response)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Create the container
|
189
|
+
#
|
190
|
+
# Calls to {Create Container}[https://learn.microsoft.com/en-us/rest/api/storageservices/create-container]
|
191
|
+
def create_container(options = {})
|
192
|
+
uri = generate_uri(container)
|
193
|
+
uri.query = URI.encode_www_form(restype: "container")
|
194
|
+
response = Http.new(uri, signer:).put
|
195
|
+
end
|
196
|
+
|
197
|
+
# Delete the container
|
198
|
+
#
|
199
|
+
# Calls to {Delete Container}[https://learn.microsoft.com/en-us/rest/api/storageservices/delete-container]
|
200
|
+
def delete_container(options = {})
|
201
|
+
uri = generate_uri(container)
|
202
|
+
uri.query = URI.encode_www_form(restype: "container")
|
203
|
+
response = Http.new(uri, signer:).delete
|
204
|
+
end
|
205
|
+
|
150
206
|
# Return a URI object to a resource in the container. Takes a path.
|
151
207
|
#
|
152
208
|
# Example: +generate_uri("#{container}/#{key}")+
|
@@ -189,7 +245,7 @@ module AzureBlob
|
|
189
245
|
"x-ms-blob-content-disposition": options[:content_disposition],
|
190
246
|
}
|
191
247
|
|
192
|
-
Http.new(uri, headers,
|
248
|
+
Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(nil)
|
193
249
|
end
|
194
250
|
|
195
251
|
# Append a block to an Append Blob
|
@@ -264,7 +320,7 @@ module AzureBlob
|
|
264
320
|
"x-ms-blob-content-disposition": options[:content_disposition],
|
265
321
|
}
|
266
322
|
|
267
|
-
Http.new(uri, headers,
|
323
|
+
Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(content)
|
268
324
|
end
|
269
325
|
|
270
326
|
private
|
@@ -296,13 +352,13 @@ module AzureBlob
|
|
296
352
|
"x-ms-blob-content-disposition": options[:content_disposition],
|
297
353
|
}
|
298
354
|
|
299
|
-
Http.new(uri, headers,
|
355
|
+
Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(content.read)
|
300
356
|
end
|
301
357
|
|
302
|
-
attr_reader :account_name, :signer, :container, :http
|
303
|
-
|
304
358
|
def host
|
305
|
-
"https://#{account_name}.blob
|
359
|
+
@host ||= "https://#{account_name}.blob.#{CLOUD_REGIONS_SUFFIX[cloud_regions]}"
|
306
360
|
end
|
361
|
+
|
362
|
+
attr_reader :account_name, :signer, :container, :http, :cloud_regions
|
307
363
|
end
|
308
364
|
end
|
data/lib/azure_blob/const.rb
CHANGED
@@ -2,7 +2,13 @@
|
|
2
2
|
|
3
3
|
module AzureBlob
|
4
4
|
API_VERSION = "2024-05-04"
|
5
|
-
MAX_UPLOAD_SIZE = 256 * 1024 * 1024
|
6
|
-
DEFAULT_BLOCK_SIZE = 128 * 1024 * 1024
|
5
|
+
MAX_UPLOAD_SIZE = 256 * 1024 * 1024 # 256 Megabytes
|
6
|
+
DEFAULT_BLOCK_SIZE = 128 * 1024 * 1024 # 128 Megabytes
|
7
7
|
BLOB_SERVICE = "b"
|
8
|
+
CLOUD_REGIONS_SUFFIX = {
|
9
|
+
global: "core.windows.net",
|
10
|
+
cn: "core.chinacloudapi.cn",
|
11
|
+
de: "core.cloudapi.de",
|
12
|
+
usgovt: "core.usgovcloudapi.net",
|
13
|
+
}
|
8
14
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AzureBlob
|
4
|
+
# AzureBlob::Container holds the metadata for a given Container.
|
5
|
+
class Container
|
6
|
+
# You should not instanciate this object directly,
|
7
|
+
# but obtain one when calling relevant methods of AzureBlob::Client.
|
8
|
+
#
|
9
|
+
# Expects a Net::HTTPResponse object from a
|
10
|
+
# HEAD or GET request to a container uri.
|
11
|
+
def initialize(response)
|
12
|
+
@response = response
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def present?
|
17
|
+
response.code == "200"
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the custom Azure metadata tagged on the container.
|
21
|
+
def metadata
|
22
|
+
@metadata || response
|
23
|
+
.to_hash
|
24
|
+
.select { |key, _| key.start_with?("x-ms-meta") }
|
25
|
+
.transform_values(&:first)
|
26
|
+
.transform_keys { |key| key.delete_prefix("x-ms-meta-").to_sym }
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :response
|
32
|
+
end
|
33
|
+
end
|
@@ -12,10 +12,12 @@ module AzureBlob
|
|
12
12
|
class EntraIdSigner # :nodoc:
|
13
13
|
attr_reader :token
|
14
14
|
attr_reader :account_name
|
15
|
+
attr_reader :host
|
15
16
|
|
16
|
-
def initialize(account_name:, principal_id: nil)
|
17
|
+
def initialize(account_name:, host:, principal_id: nil)
|
17
18
|
@token = AzureBlob::IdentityToken.new(principal_id:)
|
18
19
|
@account_name = account_name
|
20
|
+
@host = host
|
19
21
|
end
|
20
22
|
|
21
23
|
def authorization_header(uri:, verb:, headers: {})
|
data/lib/azure_blob/http.rb
CHANGED
@@ -12,7 +12,10 @@ module AzureBlob
|
|
12
12
|
def initialize(body: nil, status: nil)
|
13
13
|
@body = body
|
14
14
|
@status = status
|
15
|
-
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
@body
|
16
19
|
end
|
17
20
|
end
|
18
21
|
class FileNotFoundError < Error; end
|
@@ -21,11 +24,16 @@ module AzureBlob
|
|
21
24
|
|
22
25
|
include REXML
|
23
26
|
|
24
|
-
def initialize(uri, headers = {}, signer: nil, metadata: {}, debug: false)
|
27
|
+
def initialize(uri, headers = {}, signer: nil, metadata: {}, tags: {}, debug: false, raise_on_error: true)
|
28
|
+
@raise_on_error = raise_on_error
|
25
29
|
@date = Time.now.httpdate
|
26
30
|
@uri = uri
|
27
31
|
@signer = signer
|
28
|
-
@headers = headers.merge(
|
32
|
+
@headers = headers.merge(
|
33
|
+
Metadata.new(metadata).headers,
|
34
|
+
Tags.new(tags).headers,
|
35
|
+
)
|
36
|
+
|
29
37
|
sanitize_headers
|
30
38
|
|
31
39
|
@http = Net::HTTP.new(uri.hostname, uri.port)
|
@@ -42,7 +50,7 @@ module AzureBlob
|
|
42
50
|
response.body
|
43
51
|
end
|
44
52
|
|
45
|
-
def put(content)
|
53
|
+
def put(content = "")
|
46
54
|
sign_request("PUT") if signer
|
47
55
|
@response = http.start do |http|
|
48
56
|
http.put(uri, content, headers)
|
@@ -51,7 +59,7 @@ module AzureBlob
|
|
51
59
|
true
|
52
60
|
end
|
53
61
|
|
54
|
-
def post(content)
|
62
|
+
def post(content = "")
|
55
63
|
sign_request("POST") if signer
|
56
64
|
@response = http.start do |http|
|
57
65
|
http.post(uri, content, headers)
|
@@ -107,6 +115,7 @@ module AzureBlob
|
|
107
115
|
end
|
108
116
|
|
109
117
|
def raise_error
|
118
|
+
return unless raise_on_error
|
110
119
|
raise error_from_response.new(body: @response.body, status: @response.code&.to_i)
|
111
120
|
end
|
112
121
|
|
@@ -115,13 +124,13 @@ module AzureBlob
|
|
115
124
|
end
|
116
125
|
|
117
126
|
def azure_error_code
|
118
|
-
Document.new(response.body).get_elements("//Error/Code").first.get_text.to_s
|
127
|
+
Document.new(response.body).get_elements("//Error/Code").first.get_text.to_s if response.body
|
119
128
|
end
|
120
129
|
|
121
130
|
def error_from_response
|
122
131
|
ERROR_MAPPINGS[status] || ERROR_CODE_MAPPINGS[azure_error_code] || Error
|
123
132
|
end
|
124
133
|
|
125
|
-
attr_accessor :host, :http, :signer, :response, :headers, :uri, :date
|
134
|
+
attr_accessor :host, :http, :signer, :response, :headers, :uri, :date, :raise_on_error
|
126
135
|
end
|
127
136
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "rexml/document"
|
2
|
+
|
3
|
+
module AzureBlob
|
4
|
+
class Tags # :nodoc:
|
5
|
+
def self.from_response(response)
|
6
|
+
document = REXML::Document.new(response)
|
7
|
+
tags = {}
|
8
|
+
document.elements.each("Tags/TagSet/Tag") do |tag|
|
9
|
+
key = tag.elements["Key"].text
|
10
|
+
value = tag.elements["Value"].text
|
11
|
+
tags[key] = value
|
12
|
+
end
|
13
|
+
new(tags)
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(tags = nil)
|
17
|
+
@tags = tags || {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def headers
|
21
|
+
return {} if @tags.empty?
|
22
|
+
|
23
|
+
{
|
24
|
+
"x-ms-tags":
|
25
|
+
@tags.map do |key, value|
|
26
|
+
%(#{key}=#{value})
|
27
|
+
end.join("&"),
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_h
|
32
|
+
@tags
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -6,7 +6,7 @@ module AzureBlob
|
|
6
6
|
EXPIRATION_BUFFER = 3600 # 1 hours
|
7
7
|
def initialize(account_name:, signer:)
|
8
8
|
@uri = URI.parse(
|
9
|
-
"
|
9
|
+
"#{signer.host}/?restype=service&comp=userdelegationkey"
|
10
10
|
)
|
11
11
|
|
12
12
|
@signer = signer
|
data/lib/azure_blob/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: azure-blob
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joé Dupuis
|
@@ -44,12 +44,14 @@ files:
|
|
44
44
|
- lib/azure_blob/canonicalized_resource.rb
|
45
45
|
- lib/azure_blob/client.rb
|
46
46
|
- lib/azure_blob/const.rb
|
47
|
+
- lib/azure_blob/container.rb
|
47
48
|
- lib/azure_blob/entra_id_signer.rb
|
48
49
|
- lib/azure_blob/errors.rb
|
49
50
|
- lib/azure_blob/http.rb
|
50
51
|
- lib/azure_blob/identity_token.rb
|
51
52
|
- lib/azure_blob/metadata.rb
|
52
53
|
- lib/azure_blob/shared_key_signer.rb
|
54
|
+
- lib/azure_blob/tags.rb
|
53
55
|
- lib/azure_blob/user_delegation_key.rb
|
54
56
|
- lib/azure_blob/version.rb
|
55
57
|
homepage: https://github.com/testdouble/azure-blob
|
@@ -78,5 +80,5 @@ requirements: []
|
|
78
80
|
rubygems_version: 3.3.27
|
79
81
|
signing_key:
|
80
82
|
specification_version: 4
|
81
|
-
summary: Azure
|
83
|
+
summary: Azure Blob client and Active Storage adapter
|
82
84
|
test_files: []
|