azure-blob 0.5.3 → 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +37 -0
- data/Rakefile +23 -0
- data/lib/active_storage/service/azure_blob_service.rb +2 -1
- data/lib/azure_blob/client.rb +48 -16
- data/lib/azure_blob/shared_key_signer.rb +9 -2
- data/lib/azure_blob/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f67999e9a3c71ecac32f52a65dae1e4bf82e76704f97ecdaed08c84b66df5630
|
4
|
+
data.tar.gz: 19fedb6d95a7d1c14da8baa8e3f608b4c3ae8e99f76a68daa5a54efe7213858a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 42405d38134512ed6ac88372c8a2e7383835e2d3558280b8dad0570532d905c6edaba6bfb84bd3fd2478f3a4456d575da5448bb223e016b89f99a96bd00bcec2
|
7
|
+
data.tar.gz: f5148357bb3ba47ea5d730ebc123a2ab901e8c9380d7c7d6620cf6fd6623ec495f9dc0659434be42458888f6a936ffd221bd9f27591f3fbce2f0b0361fee5b9f
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -46,6 +46,30 @@ prod:
|
|
46
46
|
principal_id: 71b34410-4c50-451d-b456-95ead1b18cce
|
47
47
|
```
|
48
48
|
|
49
|
+
### Azurite
|
50
|
+
|
51
|
+
To use Azurite, pass the `storage_blob_host` config key with the Azurite URL (`http://127.0.0.1:10000/devstoreaccount1` by default)
|
52
|
+
and the Azurite credentials (`devstoreaccount1` and `Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==` by default).
|
53
|
+
|
54
|
+
Example:
|
55
|
+
|
56
|
+
```
|
57
|
+
dev:
|
58
|
+
service: AzureBlob
|
59
|
+
container: container_name
|
60
|
+
storage_account_name: devstoreaccount1
|
61
|
+
storage_access_key: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
62
|
+
storage_blob_host: http://127.0.0.1:10000/devstoreaccount1
|
63
|
+
```
|
64
|
+
|
65
|
+
You'll have to create the container before you can start uploading files.
|
66
|
+
You can do so using Azure CLI, Azure Storage Explorer, or by running:
|
67
|
+
|
68
|
+
`bin/rails runner "ActiveStorage::Blob.service.client.tap{|client| client.create_container unless client.get_container_properties.present?}.tap { |client| puts 'done!' if client.get_container_properties.present?}"`
|
69
|
+
|
70
|
+
Make sure that `config.active_storage.service = :dev` is set to your azurite configuration.
|
71
|
+
Container names can't have any special characters, or you'll get an error.
|
72
|
+
|
49
73
|
## Standalone
|
50
74
|
|
51
75
|
Instantiate a client with your account name, an access key and the container name:
|
@@ -71,6 +95,19 @@ client.delete_blob(path)
|
|
71
95
|
|
72
96
|
For the full list of methods: https://www.rubydoc.info/gems/azure-blob/AzureBlob/Client
|
73
97
|
|
98
|
+
## options
|
99
|
+
|
100
|
+
### Lazy loading
|
101
|
+
|
102
|
+
The client is configured to raise an error early for missing credentials, causing it to crash before becoming healthy. This behavior can sometimes be undesirable, such as during assets precompilation.
|
103
|
+
|
104
|
+
To enable lazy loading and ignore missing credentials, set the `lazy` option:
|
105
|
+
|
106
|
+
`AzureBlob::Client.new(account_name: nil, access_key: nil, container: nil, lazy: true)`
|
107
|
+
|
108
|
+
or add `lazy: true` to your `config/storage.yml` for Active Storage.
|
109
|
+
|
110
|
+
|
74
111
|
## Contributing
|
75
112
|
|
76
113
|
### Dev environment
|
data/Rakefile
CHANGED
@@ -5,6 +5,7 @@ require "minitest/test_task"
|
|
5
5
|
require "azure_blob"
|
6
6
|
require_relative "test/support/app_service_vpn"
|
7
7
|
require_relative "test/support/azure_vm_vpn"
|
8
|
+
require_relative "test/support/azurite"
|
8
9
|
|
9
10
|
Minitest::TestTask.create(:test_rails) do
|
10
11
|
self.test_globs = [ "test/rails/**/test_*.rb",
|
@@ -39,6 +40,28 @@ ensure
|
|
39
40
|
vpn.kill
|
40
41
|
end
|
41
42
|
|
43
|
+
task :test_azurite do |t|
|
44
|
+
azurite = Azurite.new
|
45
|
+
# Azurite well-known credentials
|
46
|
+
# https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage#well-known-storage-account-and-key
|
47
|
+
account_name = ENV["AZURE_ACCOUNT_NAME"] = "devstoreaccount1"
|
48
|
+
access_key = ENV["AZURE_ACCESS_KEY"] = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
49
|
+
host = ENV["STORAGE_BLOB_HOST"] = "http://127.0.0.1:10000/devstoreaccount1"
|
50
|
+
ENV["TESTING_AZURITE"] = "true"
|
51
|
+
|
52
|
+
# Create containers
|
53
|
+
private_container = AzureBlob::Client.new(account_name:, access_key:, host:, container: ENV["AZURE_PRIVATE_CONTAINER"])
|
54
|
+
public_container = AzureBlob::Client.new(account_name:, access_key:, host:, container: ENV["AZURE_PUBLIC_CONTAINER"])
|
55
|
+
# public_container.delete_container
|
56
|
+
private_container.create_container unless private_container.get_container_properties.present?
|
57
|
+
public_container.create_container(public_access: true) unless public_container.get_container_properties.present?
|
58
|
+
|
59
|
+
Rake::Task["test_client"].execute
|
60
|
+
Rake::Task["test_rails"].execute
|
61
|
+
ensure
|
62
|
+
azurite.kill
|
63
|
+
end
|
64
|
+
|
42
65
|
task :test_entra_id do |t|
|
43
66
|
ENV["AZURE_ACCESS_KEY"] = nil
|
44
67
|
Rake::Task["test"].execute
|
@@ -35,13 +35,14 @@ module ActiveStorage
|
|
35
35
|
class Service::AzureBlobService < Service
|
36
36
|
attr_reader :client, :container, :signer
|
37
37
|
|
38
|
-
def initialize(storage_account_name:, storage_access_key: nil, container:, public: false, **options)
|
38
|
+
def initialize(storage_account_name:, storage_access_key: nil, container:, storage_blob_host: nil, public: false, **options)
|
39
39
|
@container = container
|
40
40
|
@public = public
|
41
41
|
@client = AzureBlob::Client.new(
|
42
42
|
account_name: storage_account_name,
|
43
43
|
access_key: storage_access_key,
|
44
44
|
container: container,
|
45
|
+
host: storage_blob_host,
|
45
46
|
**options)
|
46
47
|
end
|
47
48
|
|
data/lib/azure_blob/client.rb
CHANGED
@@ -15,22 +15,15 @@ module AzureBlob
|
|
15
15
|
# AzureBlob Client class. You interact with the Azure Blob api
|
16
16
|
# through an instance of this class.
|
17
17
|
class Client
|
18
|
-
def initialize(account_name:, access_key: nil, principal_id: nil, container:, **options)
|
18
|
+
def initialize(account_name:, access_key: nil, principal_id: nil, container:, host: nil, **options)
|
19
19
|
@account_name = account_name
|
20
20
|
@container = container
|
21
|
+
@host = host
|
21
22
|
@cloud_regions = options[:cloud_regions]&.to_sym || :global
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
+
@access_key = access_key
|
24
|
+
@principal_id = principal_id
|
25
|
+
@use_managed_identities = options[:use_managed_identities]
|
26
|
+
signer unless options[:lazy]
|
34
27
|
end
|
35
28
|
|
36
29
|
# 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.
|
@@ -149,7 +142,8 @@ module AzureBlob
|
|
149
142
|
#
|
150
143
|
# Calls to {Get Blob Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-properties]
|
151
144
|
#
|
152
|
-
# This can be used to
|
145
|
+
# This can be used to obtain metadata such as content type, disposition, checksum or Azure custom metadata.
|
146
|
+
# To check for blob presence, look for `blob_exist?` as `get_blob_properties` raises on missing blob.
|
153
147
|
def get_blob_properties(key, options = {})
|
154
148
|
uri = generate_uri("#{container}/#{key}")
|
155
149
|
|
@@ -158,6 +152,15 @@ module AzureBlob
|
|
158
152
|
Blob.new(response)
|
159
153
|
end
|
160
154
|
|
155
|
+
# Returns a boolean indicating if the blob exists.
|
156
|
+
#
|
157
|
+
# Calls to {Get Blob Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-properties]
|
158
|
+
def blob_exist?(key, options = {})
|
159
|
+
get_blob_properties(key, options).present?
|
160
|
+
rescue AzureBlob::Http::FileNotFoundError
|
161
|
+
false
|
162
|
+
end
|
163
|
+
|
161
164
|
# Returns the tags associated with a blob
|
162
165
|
#
|
163
166
|
# Calls to the {Get Blob Tags}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-tags] endpoint.
|
@@ -185,13 +188,24 @@ module AzureBlob
|
|
185
188
|
Container.new(response)
|
186
189
|
end
|
187
190
|
|
191
|
+
# Returns a boolean indicating if the container exists.
|
192
|
+
#
|
193
|
+
# Calls to {Get Container Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-container-properties]
|
194
|
+
def container_exist?(options = {})
|
195
|
+
get_container_properties(options = {}).present?
|
196
|
+
end
|
197
|
+
|
188
198
|
# Create the container
|
189
199
|
#
|
190
200
|
# Calls to {Create Container}[https://learn.microsoft.com/en-us/rest/api/storageservices/create-container]
|
191
201
|
def create_container(options = {})
|
192
202
|
uri = generate_uri(container)
|
203
|
+
headers = {}
|
204
|
+
headers[:"x-ms-blob-public-access"] = "blob" if options[:public_access]
|
205
|
+
headers[:"x-ms-blob-public-access"] = options[:public_access] if ["container","blob"].include?(options[:public_access])
|
206
|
+
|
193
207
|
uri.query = URI.encode_www_form(restype: "container")
|
194
|
-
response = Http.new(uri, signer:).put
|
208
|
+
response = Http.new(uri, headers, signer:).put
|
195
209
|
end
|
196
210
|
|
197
211
|
# Delete the container
|
@@ -359,6 +373,24 @@ module AzureBlob
|
|
359
373
|
@host ||= "https://#{account_name}.blob.#{CLOUD_REGIONS_SUFFIX[cloud_regions]}"
|
360
374
|
end
|
361
375
|
|
362
|
-
|
376
|
+
def signer
|
377
|
+
@signer ||=
|
378
|
+
begin
|
379
|
+
no_access_key = access_key.nil? || access_key&.empty?
|
380
|
+
using_managed_identities = no_access_key && !principal_id.nil? || use_managed_identities
|
381
|
+
|
382
|
+
if !using_managed_identities && no_access_key
|
383
|
+
raise AzureBlob::Error.new(
|
384
|
+
"`access_key` cannot be empty. To use managed identities instead, pass a `principal_id` or set `use_managed_identities` to true."
|
385
|
+
)
|
386
|
+
end
|
387
|
+
|
388
|
+
using_managed_identities ?
|
389
|
+
AzureBlob::EntraIdSigner.new(account_name:, host:, principal_id:) :
|
390
|
+
AzureBlob::SharedKeySigner.new(account_name:, access_key:, host:)
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
attr_reader :account_name, :container, :http, :cloud_regions, :access_key, :principal_id, :use_managed_identities
|
363
395
|
end
|
364
396
|
end
|
@@ -7,9 +7,11 @@ require_relative "canonicalized_resource"
|
|
7
7
|
|
8
8
|
module AzureBlob
|
9
9
|
class SharedKeySigner # :nodoc:
|
10
|
-
def initialize(account_name:, access_key:)
|
10
|
+
def initialize(account_name:, access_key:, host:)
|
11
11
|
@account_name = account_name
|
12
12
|
@access_key = Base64.decode64(access_key)
|
13
|
+
@host = host
|
14
|
+
@remove_prefix = @host.include?("/#{@account_name}")
|
13
15
|
end
|
14
16
|
|
15
17
|
def authorization_header(uri:, verb:, headers: {})
|
@@ -39,6 +41,11 @@ module AzureBlob
|
|
39
41
|
end
|
40
42
|
|
41
43
|
def sas_token(uri, options = {})
|
44
|
+
if remove_prefix
|
45
|
+
uri = uri.clone
|
46
|
+
uri.path = uri.path.delete_prefix("/#{account_name}")
|
47
|
+
end
|
48
|
+
|
42
49
|
to_sign = [
|
43
50
|
options[:permissions],
|
44
51
|
options[:start],
|
@@ -99,6 +106,6 @@ module AzureBlob
|
|
99
106
|
end
|
100
107
|
end
|
101
108
|
|
102
|
-
attr_reader :access_key, :account_name
|
109
|
+
attr_reader :access_key, :account_name, :remove_prefix
|
103
110
|
end
|
104
111
|
end
|
data/lib/azure_blob/version.rb
CHANGED