Skip to content

Commit e87667e

Browse files
authored
Merge pull request #6 from igosneves/add-configuration-storage-host
Add support to storage_blob_host configuration
2 parents 2b24179 + 01fc08f commit e87667e

File tree

15 files changed

+115
-14
lines changed

15 files changed

+115
-14
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,7 @@ devenv.local.nix
2323
terraform.tfstate
2424
terraform.tfstate.backup
2525
.terraform.tfstate.lock.info
26-
*.tfvars
26+
*.tfvars
27+
28+
__azurite_db*
29+
__blobstorage__/

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## [Unreleased]
22

3+
- Allow creating public container
4+
- Add Azurite support
35

46
## [0.5.3] 2024-10-31
57

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ prod:
4646
principal_id: 71b34410-4c50-451d-b456-95ead1b18cce
4747
```
4848

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+
4973
## Standalone
5074

5175
Instantiate a client with your account name, an access key and the container name:

Rakefile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ require "minitest/test_task"
55
require "azure_blob"
66
require_relative "test/support/app_service_vpn"
77
require_relative "test/support/azure_vm_vpn"
8+
require_relative "test/support/azurite"
89

910
Minitest::TestTask.create(:test_rails) do
1011
self.test_globs = [ "test/rails/**/test_*.rb",
@@ -39,6 +40,28 @@ ensure
3940
vpn.kill
4041
end
4142

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+
4265
task :test_entra_id do |t|
4366
ENV["AZURE_ACCESS_KEY"] = nil
4467
Rake::Task["test"].execute

devenv.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
sshuttle
1616
sshpass
1717
rsync
18+
azurite
1819
];
1920

2021
languages.ruby.enable = true;

lib/active_storage/service/azure_blob_service.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ module ActiveStorage
3535
class Service::AzureBlobService < Service
3636
attr_reader :client, :container, :signer
3737

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)
3939
@container = container
4040
@public = public
4141
@client = AzureBlob::Client.new(
4242
account_name: storage_account_name,
4343
access_key: storage_access_key,
4444
container: container,
45+
host: storage_blob_host,
4546
**options)
4647
end
4748

lib/azure_blob/client.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ module AzureBlob
1515
# AzureBlob Client class. You interact with the Azure Blob api
1616
# through an instance of this class.
1717
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)
1919
@account_name = account_name
2020
@container = container
21+
@host = host
2122
@cloud_regions = options[:cloud_regions]&.to_sym || :global
2223

2324
no_access_key = access_key.nil? || access_key&.empty?
@@ -29,8 +30,8 @@ def initialize(account_name:, access_key: nil, principal_id: nil, container:, **
2930
)
3031
end
3132
@signer = using_managed_identities ?
32-
AzureBlob::EntraIdSigner.new(account_name:, host:, principal_id:) :
33-
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)
3435
end
3536

3637
# 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.
@@ -190,8 +191,12 @@ def get_container_properties(options = {})
190191
# Calls to {Create Container}[https://learn.microsoft.com/en-us/rest/api/storageservices/create-container]
191192
def create_container(options = {})
192193
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+
193198
uri.query = URI.encode_www_form(restype: "container")
194-
response = Http.new(uri, signer:).put
199+
response = Http.new(uri, headers, signer:).put
195200
end
196201

197202
# Delete the container

lib/azure_blob/shared_key_signer.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
module AzureBlob
99
class SharedKeySigner # :nodoc:
10-
def initialize(account_name:, access_key:)
10+
def initialize(account_name:, access_key:, host:)
1111
@account_name = account_name
1212
@access_key = Base64.decode64(access_key)
13+
@host = host
14+
@remove_prefix = @host.include?("/#{@account_name}")
1315
end
1416

1517
def authorization_header(uri:, verb:, headers: {})
@@ -39,6 +41,11 @@ def authorization_header(uri:, verb:, headers: {})
3941
end
4042

4143
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+
4249
to_sign = [
4350
options[:permissions],
4451
options[:start],
@@ -99,6 +106,6 @@ module Resources # :nodoc:
99106
end
100107
end
101108

102-
attr_reader :access_key, :account_name
109+
attr_reader :access_key, :account_name, :remove_prefix
103110
end
104111
end

test/client/test_client.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ def setup
1313
@access_key = ENV["AZURE_ACCESS_KEY"]
1414
@container = ENV["AZURE_PRIVATE_CONTAINER"]
1515
@principal_id = ENV["AZURE_PRINCIPAL_ID"]
16+
@host = ENV["STORAGE_BLOB_HOST"]
1617
@client = AzureBlob::Client.new(
1718
account_name: @account_name,
1819
access_key: @access_key,
1920
container: @container,
2021
principal_id: @principal_id,
22+
host: @host,
2123
)
2224
@key = "test client##{name}"
2325
@content = "Some random content #{Random.rand(200)}"
@@ -104,11 +106,13 @@ def test_upload_integrity_block
104106
end
105107

106108
def test_upload_raise_on_invalid_checksum_blob
109+
skip if ENV["TESTING_AZURITE"]
107110
checksum = OpenSSL::Digest::MD5.base64digest(content + "a")
108111
assert_raises(AzureBlob::Http::IntegrityError) { client.create_block_blob(key, content, content_md5: checksum) }
109112
end
110113

111114
def test_upload_raise_on_invalid_checksum_block
115+
skip if ENV["TESTING_AZURITE"]
112116
checksum = OpenSSL::Digest::MD5.base64digest(content + "a")
113117
assert_raises(AzureBlob::Http::IntegrityError) { client.put_blob_block(key, 0, content, content_md5: checksum) }
114118
end
@@ -338,6 +342,7 @@ def test_create_container
338342
access_key: @access_key,
339343
container: Random.alphanumeric(20).tr("0-9", "").downcase,
340344
principal_id: @principal_id,
345+
host: @host,
341346
)
342347
container = client.get_container_properties
343348
refute container.present?

test/rails/controllers/direct_uploads_controller_test.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@ class ActiveStorage::AzureBlobDirectUploadsControllerTest < ActionDispatch::Inte
2323
post rails_direct_uploads_url, params: { blob: {
2424
filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain", metadata: metadata, } }
2525

26+
host = @config[:host] || "https://#{@config[:storage_account_name]}.blob.core.windows.net"
27+
2628
response.parsed_body.tap do |details|
2729
assert_equal ActiveStorage::Blob.find(details["id"]), ActiveStorage::Blob.find_signed!(details["signed_id"])
2830
assert_equal "hello.txt", details["filename"]
2931
assert_equal 6, details["byte_size"]
3032
assert_equal checksum, details["checksum"]
3133
assert_equal metadata, details["metadata"]
3234
assert_equal "text/plain", details["content_type"]
33-
assert_match %r{#{@config[:storage_account_name]}\.blob\.core\.windows\.net/#{@config[:container]}}, details["direct_upload"]["url"]
35+
assert details["direct_upload"]["url"].start_with?("#{host}/#{@config[:container]}")
3436
assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum, "x-ms-blob-content-disposition" => "inline; filename=\"hello.txt\"; filename*=UTF-8''hello.txt", "x-ms-blob-type" => "BlockBlob" }, details["direct_upload"]["headers"])
3537
end
3638
end

0 commit comments

Comments
 (0)