azure-blob 0.5.1 → 0.5.3

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: 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: []