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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a83c96b748a5659438b382a79a1af068c1552f4ee895dd16b863b00758d8d1f
4
- data.tar.gz: f6def3a97bde3b96ef53618502454b3c3bd0103e07332ed19f817d3298971860
3
+ metadata.gz: c6c4e076840dd6671c28c99b5fca03057782e81686c0c06890b42cedd3b83788
4
+ data.tar.gz: '00668582cecce526ad816a5c537469af7e646d6f64d7392655d1c7e60396393c'
5
5
  SHA512:
6
- metadata.gz: 20f51dbd1ea9661fec78d1371f52459aaf00a4a42e7274f375bd81c7cfc89bac9dfba1546af7cf71f7fea5fd8479fa7ceab9605ea1a8d0ceef7099e0212d4738
7
- data.tar.gz: 74939a0d48677079ac46585276369a4d090d8bcbdd4e0ab84d37a8a48d2e54977dec5541b823f0a08c7642787ce13041a54311963010a5449424f13a181e72de
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
- 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,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:, container:, **options)
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
- @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))
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 metada such as content type, disposition, checksum or Azure custom metadata.
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, metadata: options[:metadata], signer:).put(nil)
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, metadata: options[:metadata], signer:).put(content)
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, metadata: options[:metadata], signer:).put(content.read)
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.core.windows.net"
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
@@ -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,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(Metadata.new(metadata).headers)
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
- "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.3"
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.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 blob client
83
+ summary: Azure Blob client and Active Storage adapter
82
84
  test_files: []