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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6c4e076840dd6671c28c99b5fca03057782e81686c0c06890b42cedd3b83788
4
- data.tar.gz: '00668582cecce526ad816a5c537469af7e646d6f64d7392655d1c7e60396393c'
3
+ metadata.gz: f67999e9a3c71ecac32f52a65dae1e4bf82e76704f97ecdaed08c84b66df5630
4
+ data.tar.gz: 19fedb6d95a7d1c14da8baa8e3f608b4c3ae8e99f76a68daa5a54efe7213858a
5
5
  SHA512:
6
- metadata.gz: fdd32b7f8547ac428bf0eb19d884860e94d1ab9471df0022356119c5ec28aca8589d1b1925d0085b2361c4c16d5898123a1fc18f57b5b2cb9ae7e53dbc4b4379
7
- data.tar.gz: 44a4592046d5d33432762cfd4d3636500f9c13db00cf5c705fbc2f472b7c322bd37bc05d9765e03079e0966dcb44d0e5cc844db303906aaa18435c8db7ee422f
6
+ metadata.gz: 42405d38134512ed6ac88372c8a2e7383835e2d3558280b8dad0570532d905c6edaba6bfb84bd3fd2478f3a4456d575da5448bb223e016b89f99a96bd00bcec2
7
+ data.tar.gz: f5148357bb3ba47ea5d730ebc123a2ab901e8c9380d7c7d6620cf6fd6623ec495f9dc0659434be42458888f6a936ffd221bd9f27591f3fbce2f0b0361fee5b9f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.5] 2025-01-10
4
+
5
+ - Allow lazy loading the signer
6
+ - Add `blob_exist?`
7
+ - Add `container_exist?`
8
+
9
+ ## [0.5.4] 2024-11-18
10
+
11
+ - Allow creating public container
12
+ - Add Azurite support
3
13
 
4
14
  ## [0.5.3] 2024-10-31
5
15
 
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
 
@@ -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
- 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
+ @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 see if the blob exist or obtain metadata such as content type, disposition, checksum or Azure custom metadata.
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
- attr_reader :account_name, :signer, :container, :http, :cloud_regions
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AzureBlob
4
- VERSION = "0.5.3"
4
+ VERSION = "0.5.5"
5
5
  end
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.3
4
+ version: 0.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joé Dupuis