Install Azurite in your preferred way: npm install azurite
Install Microsoft Azure Storage Explorer
Create some directory to run azurite from: `~/azurite`
Add storage.yml
configuration for azurite (using the default dev account and key):
azurite_emulator:
service: AzureStorage
storage_account_name: 'devstoreaccount1'
storage_access_key: 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=='
container: 'container-name'
storage_blob_host: 'http://127.0.0.1:10000/devstoreaccount1'
Update development.rb
to use azurite_emulator:
config.active_storage.service = :azurite_emulator
Start azurite from the directory you created for azurite: azurite --location ~/azurite --debug ~/azurite/debug.log
Start Azure Storage Explorer, connect to local emulator, and create container-name
blob container – the same container name you specified in the storage.yml
file.
Start uploading to Azurite.
Note for Rails 5.2
Some changes have not been backported as of this post, and you have to monkey-patch ActiveStorage file as described here – http://www.garytaylor.blog/index.php/2019/01/30/rails-active-storage-and-azure-beyond-config/ – this allows us to work with azurite locally.
If you want to use the newer azure-storage-blob
instead of the deprecated azure-storage
and you’re on Rails 5.2, you have to do a bit more monkey-patching – otherwise, you’ll start getting No such file to load — azure/storage.rb“:
Add two empty files: lib/azure/storage/core/auth/shared_access_signature.rb
, and lib/azure/storage.rb
Add this to config/initializers/active_storage_6_patch.rb (this is the current master version of the ActiveStorage module):
require "azure/storage/blob"
require 'active_storage/service/azure_storage_service'
module ActiveStorage
# Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
# See ActiveStorage::Service for the generic API documentation that applies to all services.
class Service::AzureStorageService < Service
attr_reader :client, :container, :signer
def initialize(storage_account_name:, storage_access_key:, container:, public: false, **options)
@client = Azure::Storage::Blob::BlobService.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key, **options)
@signer = Azure::Storage::Common::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
@container = container
@public = public
end
def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
instrument :upload, key: key, checksum: checksum do
handle_errors do
content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
client.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition)
end
end
end
def download(key, &block)
if block_given?
instrument :streaming_download, key: key do
stream(key, &block)
end
else
instrument :download, key: key do
handle_errors do
_, io = client.get_blob(container, key)
io.force_encoding(Encoding::BINARY)
end
end
end
end
def download_chunk(key, range)
instrument :download_chunk, key: key, range: range do
handle_errors do
_, io = client.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
io.force_encoding(Encoding::BINARY)
end
end
end
def delete(key)
instrument :delete, key: key do
client.delete_blob(container, key)
rescue Azure::Core::Http::HTTPError => e
raise unless e.type == "BlobNotFound"
# Ignore files already deleted
end
end
def delete_prefixed(prefix)
instrument :delete_prefixed, prefix: prefix do
marker = nil
loop do
results = client.list_blobs(container, prefix: prefix, marker: marker)
results.each do |blob|
client.delete_blob(container, blob.name)
end
break unless marker = results.continuation_token.presence
end
end
end
def exist?(key)
instrument :exist, key: key do |payload|
answer = blob_for(key).present?
payload[:exist] = answer
answer
end
end
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
instrument :url, key: key do |payload|
generated_url = signer.signed_uri(
uri_for(key), false,
service: "b",
permissions: "rw",
expiry: format_expiry(expires_in)
).to_s
payload[:url] = generated_url
generated_url
end
end
def headers_for_direct_upload(key, content_type:, checksum:, filename: nil, disposition: nil, **)
content_disposition = content_disposition_with(type: disposition, filename: filename) if filename
{ "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-content-disposition" => content_disposition, "x-ms-blob-type" => "BlockBlob" }
end
private
def private_url(key, expires_in:, filename:, disposition:, content_type:, **)
signer.signed_uri(
uri_for(key), false,
service: "b",
permissions: "r",
expiry: format_expiry(expires_in),
content_disposition: content_disposition_with(type: disposition, filename: filename),
content_type: content_type
).to_s
end
def public_url(key, **)
uri_for(key).to_s
end
def uri_for(key)
client.generate_uri("#{container}/#{key}")
end
def blob_for(key)
client.get_blob_properties(container, key)
rescue Azure::Core::Http::HTTPError
false
end
def format_expiry(expires_in)
expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil
end
# Reads the object for the given key in chunks, yielding each to the block.
def stream(key)
blob = blob_for(key)
chunk_size = 5.megabytes
offset = 0
raise ActiveStorage::FileNotFoundError unless blob.present?
while offset < blob.properties[:content_length]
_, chunk = client.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
yield chunk.force_encoding(Encoding::BINARY)
offset += chunk_size
end
end
def handle_errors
yield
rescue Azure::Core::Http::HTTPError => e
case e.type
when "BlobNotFound"
raise ActiveStorage::FileNotFoundError
when "Md5Mismatch"
raise ActiveStorage::IntegrityError
else
raise
end
end
end
end