azure-blob 0.5.1 → 0.5.2

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: 9a83c96b748a5659438b382a79a1af068c1552f4ee895dd16b863b00758d8d1f
4
- data.tar.gz: f6def3a97bde3b96ef53618502454b3c3bd0103e07332ed19f817d3298971860
3
+ metadata.gz: 72003b15ad78aeab66adc7aca2e6786ae2921ad1469cf94145e049705f52c33c
4
+ data.tar.gz: a77f87dff2f59a22476f0c5e490540a73166cf88dbf3616de8a15a87132aaa5f
5
5
  SHA512:
6
- metadata.gz: 20f51dbd1ea9661fec78d1371f52459aaf00a4a42e7274f375bd81c7cfc89bac9dfba1546af7cf71f7fea5fd8479fa7ceab9605ea1a8d0ceef7099e0212d4738
7
- data.tar.gz: 74939a0d48677079ac46585276369a4d090d8bcbdd4e0ab84d37a8a48d2e54977dec5541b823f0a08c7642787ce13041a54311963010a5449424f13a181e72de
6
+ metadata.gz: 910c2d759b83d7b56168fd42a239ffb5da1b4aced25b8f39bc84a1beb63e736690a72586f64313d5fcebf7250808b27d15e23450e850635e24558891eed375ff
7
+ data.tar.gz: 2d4ecfb6e3e5318d8c6cb07569da7b83bccd49ee3a7d153d43c41e655474dcd43d7a73d28d68fb2baa4124326a1b41562275314348730250610420640c3dc3bd
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+
4
+ ## [0.5.2] 2024-09-12
5
+
6
+ - Add get_container_properties
7
+ - Add create_container
8
+ - Add delete_container
9
+ - Support for Azure China, US Gov and Germany
10
+
3
11
  ## [0.5.1] 2024-09-09
4
12
 
5
13
  - Remove dev files from the release
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # AzureBlob
2
2
 
3
- This gem was built to replace azure-storage-blob (deprecated) in Active Storage, but was written to be Rails agnostic.
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` is false by default.
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
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AzureBlob
4
- # AzureBlob::Blob holds the metada for a given Blob.
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 metada tagged on the blob.
35
+ # Returns the custom Azure metadata tagged on the blob.
36
36
  def metadata
37
37
  @metadata || response
38
38
  .to_hash
@@ -3,6 +3,7 @@
3
3
  require_relative "block_list"
4
4
  require_relative "blob_list"
5
5
  require_relative "blob"
6
+ require_relative "container"
6
7
  require_relative "http"
7
8
  require_relative "shared_key_signer"
8
9
  require_relative "entra_id_signer"
@@ -13,13 +14,22 @@ module AzureBlob
13
14
  # AzureBlob Client class. You interact with the Azure Blob api
14
15
  # through an instance of this class.
15
16
  class Client
16
- def initialize(account_name:, access_key:, container:, **options)
17
+ def initialize(account_name:, access_key: nil, principal_id: nil, container:, **options)
17
18
  @account_name = account_name
18
19
  @container = container
20
+ @cloud_regions = options[:cloud_regions]&.to_sym || :global
19
21
 
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))
22
+ no_access_key = access_key.nil? || access_key&.empty?
23
+ using_managed_identities = no_access_key && !principal_id.nil? || options[:use_managed_identities]
24
+
25
+ if !using_managed_identities && no_access_key
26
+ raise AzureBlob::Error.new(
27
+ "`access_key` cannot be empty. To use managed identities instead, pass a `principal_id` or set `use_managed_identities` to true."
28
+ )
29
+ end
30
+ @signer = using_managed_identities ?
31
+ AzureBlob::EntraIdSigner.new(account_name:, host:, principal_id: ) :
32
+ AzureBlob::SharedKeySigner.new(account_name:, access_key:)
23
33
  end
24
34
 
25
35
  # 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 +148,7 @@ module AzureBlob
138
148
  #
139
149
  # Calls to {Get Blob Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-properties]
140
150
  #
141
- # This can be used to see if the blob exist or obtain metada such as content type, disposition, checksum or Azure custom metadata.
151
+ # This can be used to see if the blob exist or obtain metadata such as content type, disposition, checksum or Azure custom metadata.
142
152
  def get_blob_properties(key, options = {})
143
153
  uri = generate_uri("#{container}/#{key}")
144
154
 
@@ -147,6 +157,37 @@ module AzureBlob
147
157
  Blob.new(response)
148
158
  end
149
159
 
160
+ # Returns a Container object.
161
+ #
162
+ # Calls to {Get Container Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-container-properties]
163
+ #
164
+ # This can be used to see if the container exist or obtain metadata.
165
+ def get_container_properties(options = {})
166
+ uri = generate_uri(container)
167
+ uri.query = URI.encode_www_form(restype: "container")
168
+ response = Http.new(uri, signer:, raise_on_error: false).head
169
+
170
+ Container.new(response)
171
+ end
172
+
173
+ # Create the container
174
+ #
175
+ # Calls to {Create Container}[https://learn.microsoft.com/en-us/rest/api/storageservices/create-container]
176
+ def create_container(options = {})
177
+ uri = generate_uri(container)
178
+ uri.query = URI.encode_www_form(restype: "container")
179
+ response = Http.new(uri, signer:).put
180
+ end
181
+
182
+ # Delete the container
183
+ #
184
+ # Calls to {Delete Container}[https://learn.microsoft.com/en-us/rest/api/storageservices/delete-container]
185
+ def delete_container(options = {})
186
+ uri = generate_uri(container)
187
+ uri.query = URI.encode_www_form(restype: "container")
188
+ response = Http.new(uri, signer:).delete
189
+ end
190
+
150
191
  # Return a URI object to a resource in the container. Takes a path.
151
192
  #
152
193
  # Example: +generate_uri("#{container}/#{key}")+
@@ -299,10 +340,10 @@ module AzureBlob
299
340
  Http.new(uri, headers, metadata: options[:metadata], signer:).put(content.read)
300
341
  end
301
342
 
302
- attr_reader :account_name, :signer, :container, :http
303
-
304
343
  def host
305
- "https://#{account_name}.blob.core.windows.net"
344
+ @host ||= "https://#{account_name}.blob.#{CLOUD_REGIONS_SUFFIX[cloud_regions]}"
306
345
  end
346
+
347
+ attr_reader :account_name, :signer, :container, :http, :cloud_regions
307
348
  end
308
349
  end
@@ -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: {})
@@ -12,7 +12,10 @@ module AzureBlob
12
12
  def initialize(body: nil, status: nil)
13
13
  @body = body
14
14
  @status = status
15
- super(body)
15
+ end
16
+
17
+ def inspect
18
+ @body
16
19
  end
17
20
  end
18
21
  class FileNotFoundError < Error; end
@@ -21,7 +24,8 @@ 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: {}, 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
@@ -42,7 +46,7 @@ module AzureBlob
42
46
  response.body
43
47
  end
44
48
 
45
- def put(content)
49
+ def put(content = "")
46
50
  sign_request("PUT") if signer
47
51
  @response = http.start do |http|
48
52
  http.put(uri, content, headers)
@@ -51,7 +55,7 @@ module AzureBlob
51
55
  true
52
56
  end
53
57
 
54
- def post(content)
58
+ def post(content = "")
55
59
  sign_request("POST") if signer
56
60
  @response = http.start do |http|
57
61
  http.post(uri, content, headers)
@@ -107,6 +111,7 @@ module AzureBlob
107
111
  end
108
112
 
109
113
  def raise_error
114
+ return unless raise_on_error
110
115
  raise error_from_response.new(body: @response.body, status: @response.code&.to_i)
111
116
  end
112
117
 
@@ -115,13 +120,13 @@ module AzureBlob
115
120
  end
116
121
 
117
122
  def azure_error_code
118
- Document.new(response.body).get_elements("//Error/Code").first.get_text.to_s
123
+ Document.new(response.body).get_elements("//Error/Code").first.get_text.to_s if response.body
119
124
  end
120
125
 
121
126
  def error_from_response
122
127
  ERROR_MAPPINGS[status] || ERROR_CODE_MAPPINGS[azure_error_code] || Error
123
128
  end
124
129
 
125
- attr_accessor :host, :http, :signer, :response, :headers, :uri, :date
130
+ attr_accessor :host, :http, :signer, :response, :headers, :uri, :date, :raise_on_error
126
131
  end
127
132
  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
- "https://#{account_name}.blob.core.windows.net/?restype=service&comp=userdelegationkey"
9
+ "#{signer.host}/?restype=service&comp=userdelegationkey"
10
10
  )
11
11
 
12
12
  @signer = signer
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AzureBlob
4
- VERSION = "0.5.1"
4
+ VERSION = "0.5.2"
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.1
4
+ version: 0.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joé Dupuis
@@ -44,6 +44,7 @@ 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
@@ -78,5 +79,5 @@ requirements: []
78
79
  rubygems_version: 3.3.27
79
80
  signing_key:
80
81
  specification_version: 4
81
- summary: Azure blob client
82
+ summary: Azure Blob client and Active Storage adapter
82
83
  test_files: []