azure-blob 0.5.2 → 0.5.4

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: 72003b15ad78aeab66adc7aca2e6786ae2921ad1469cf94145e049705f52c33c
4
- data.tar.gz: a77f87dff2f59a22476f0c5e490540a73166cf88dbf3616de8a15a87132aaa5f
3
+ metadata.gz: 15d8cb5bf91f904bff341c26f5f6b6392abbb14e0de1f092fab8697cabab75b1
4
+ data.tar.gz: de847a4e6a9925b31ed1900dea10e8e0d078277f4af4e4e7f434b00e4e0bc298
5
5
  SHA512:
6
- metadata.gz: 910c2d759b83d7b56168fd42a239ffb5da1b4aced25b8f39bc84a1beb63e736690a72586f64313d5fcebf7250808b27d15e23450e850635e24558891eed375ff
7
- data.tar.gz: 2d4ecfb6e3e5318d8c6cb07569da7b83bccd49ee3a7d153d43c41e655474dcd43d7a73d28d68fb2baa4124326a1b41562275314348730250610420640c3dc3bd
6
+ metadata.gz: ef60c4be02b9725ba53209234d471a71451fe7270a58c4fad908c5e06669cb686290cd7ce75e57dfced02fc1d1efee85e3201d0d05b868881abe16e13fb77e68
7
+ data.tar.gz: 1460b8c46b849b0e1a89e2e628cf20dad0ba223dc3e090e7f8ce3bc16d124db497f0785144eed3a8c48210e2d085bb4c178f74d9ff18c77da98ce21d65f6af45
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.4] 2024-11-18
4
+
5
+ - Allow creating public container
6
+ - Add Azurite support
7
+
8
+ ## [0.5.3] 2024-10-31
9
+
10
+ - Add support for setting tags when uploading a blob
11
+ - Add get_blob_tags
3
12
 
4
13
  ## [0.5.2] 2024-09-12
5
14
 
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:
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
 
@@ -4,6 +4,7 @@ require_relative "block_list"
4
4
  require_relative "blob_list"
5
5
  require_relative "blob"
6
6
  require_relative "container"
7
+ require_relative "tags"
7
8
  require_relative "http"
8
9
  require_relative "shared_key_signer"
9
10
  require_relative "entra_id_signer"
@@ -14,9 +15,10 @@ module AzureBlob
14
15
  # AzureBlob Client class. You interact with the Azure Blob api
15
16
  # through an instance of this class.
16
17
  class Client
17
- 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)
18
19
  @account_name = account_name
19
20
  @container = container
21
+ @host = host
20
22
  @cloud_regions = options[:cloud_regions]&.to_sym || :global
21
23
 
22
24
  no_access_key = access_key.nil? || access_key&.empty?
@@ -28,8 +30,8 @@ module AzureBlob
28
30
  )
29
31
  end
30
32
  @signer = using_managed_identities ?
31
- AzureBlob::EntraIdSigner.new(account_name:, host:, principal_id: ) :
32
- AzureBlob::SharedKeySigner.new(account_name:, access_key:)
33
+ AzureBlob::EntraIdSigner.new(account_name:, host: self.host, principal_id:) :
34
+ AzureBlob::SharedKeySigner.new(account_name:, access_key:, host: self.host)
33
35
  end
34
36
 
35
37
  # 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.
@@ -157,6 +159,20 @@ module AzureBlob
157
159
  Blob.new(response)
158
160
  end
159
161
 
162
+ # Returns the tags associated with a blob
163
+ #
164
+ # Calls to the {Get Blob Tags}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-blob-tags] endpoint.
165
+ #
166
+ # Takes a key (path) of the blob.
167
+ #
168
+ # Returns a hash of the blob's tags.
169
+ def get_blob_tags(key)
170
+ uri = generate_uri("#{container}/#{key}?comp=tags")
171
+ response = Http.new(uri, signer:).get
172
+
173
+ Tags.from_response(response).to_h
174
+ end
175
+
160
176
  # Returns a Container object.
161
177
  #
162
178
  # Calls to {Get Container Properties}[https://learn.microsoft.com/en-us/rest/api/storageservices/get-container-properties]
@@ -175,8 +191,12 @@ module AzureBlob
175
191
  # Calls to {Create Container}[https://learn.microsoft.com/en-us/rest/api/storageservices/create-container]
176
192
  def create_container(options = {})
177
193
  uri = generate_uri(container)
194
+ headers = {}
195
+ headers[:"x-ms-blob-public-access"] = "blob" if options[:public_access]
196
+ headers[:"x-ms-blob-public-access"] = options[:public_access] if ["container","blob"].include?(options[:public_access])
197
+
178
198
  uri.query = URI.encode_www_form(restype: "container")
179
- response = Http.new(uri, signer:).put
199
+ response = Http.new(uri, headers, signer:).put
180
200
  end
181
201
 
182
202
  # Delete the container
@@ -230,7 +250,7 @@ module AzureBlob
230
250
  "x-ms-blob-content-disposition": options[:content_disposition],
231
251
  }
232
252
 
233
- Http.new(uri, headers, metadata: options[:metadata], signer:).put(nil)
253
+ Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(nil)
234
254
  end
235
255
 
236
256
  # Append a block to an Append Blob
@@ -305,7 +325,7 @@ module AzureBlob
305
325
  "x-ms-blob-content-disposition": options[:content_disposition],
306
326
  }
307
327
 
308
- Http.new(uri, headers, metadata: options[:metadata], signer:).put(content)
328
+ Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(content)
309
329
  end
310
330
 
311
331
  private
@@ -337,7 +357,7 @@ module AzureBlob
337
357
  "x-ms-blob-content-disposition": options[:content_disposition],
338
358
  }
339
359
 
340
- Http.new(uri, headers, metadata: options[:metadata], signer:).put(content.read)
360
+ Http.new(uri, headers, signer:, **options.slice(:metadata, :tags)).put(content.read)
341
361
  end
342
362
 
343
363
  def host
@@ -24,12 +24,16 @@ module AzureBlob
24
24
 
25
25
  include REXML
26
26
 
27
- def initialize(uri, headers = {}, signer: nil, metadata: {}, debug: false, raise_on_error: true)
27
+ def initialize(uri, headers = {}, signer: nil, metadata: {}, tags: {}, debug: false, raise_on_error: true)
28
28
  @raise_on_error = raise_on_error
29
29
  @date = Time.now.httpdate
30
30
  @uri = uri
31
31
  @signer = signer
32
- @headers = headers.merge(Metadata.new(metadata).headers)
32
+ @headers = headers.merge(
33
+ Metadata.new(metadata).headers,
34
+ Tags.new(tags).headers,
35
+ )
36
+
33
37
  sanitize_headers
34
38
 
35
39
  @http = Net::HTTP.new(uri.hostname, uri.port)
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AzureBlob
4
- VERSION = "0.5.2"
4
+ VERSION = "0.5.4"
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.2
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joé Dupuis
@@ -51,6 +51,7 @@ files:
51
51
  - lib/azure_blob/identity_token.rb
52
52
  - lib/azure_blob/metadata.rb
53
53
  - lib/azure_blob/shared_key_signer.rb
54
+ - lib/azure_blob/tags.rb
54
55
  - lib/azure_blob/user_delegation_key.rb
55
56
  - lib/azure_blob/version.rb
56
57
  homepage: https://github.com/testdouble/azure-blob