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