Active Storageの仕組み(その2)
前回1に引き続きActive Storageの仕組みを見てみましょう。前回はimage_tag user.avatar
が、ActiveStorage::Blobs::RedirectController#show
にroutingされることを突き止めました。今回はその中身を見ることから始めてみましょう。
※抜粋するRailsのコードは説明のために大幅に編集・簡略化してあります。
class ActiveStorage::Blobs::RedirectController < ActiveStorage::BaseController
def show
redirect_to @blob.url
end
end
ActiveStorage::Blob#url
でURLを生成し、そこにリダイレクトしています。
class ActiveStorage::Blob < ActiveStorage::Record
def url
service.url key
end
end
ActiveStorage::Blob#service
を呼び出しているだけで、その実体はActiveStorage::Service::DiskService
です。
irb> User.new.build_avatar_blob.service
=> #<ActiveStorage::Service::DiskService:0x00007fbe6e1eff58 @name=:local, @public=false>
module ActiveStorage
class Service::DiskService < Service
end
end
さて、ActiveStorage::Service::DiskService
のファイルを読んでも#url
は見当たりません。困ったようですが、Rubyのsource_location
2を使えば、定義ファイルと行を調べられます。
irb> User.new.build_avatar_blob.service.method(:url).source_location
=> ["activestorage-7.1.3/lib/active_storage/service.rb", 119]
親クラスに定義されていました、その中身を確認してみましょう。
module ActiveStorage
class Service
def url(key, **options)
if public?
public_url(key, **options)
else
private_url(key, **options)
end
end
def public_url(key, **)
raise NotImplementedError
end
def private_url(key, **)
raise NotImplementedError
end
end
end
実装は子クラスにする想定のようです。ActiveStorage::Service::DiskService
に戻りましょう。
module ActiveStorage
class Service::DiskService < Service
def private_url(key, expires_in:)
generate_url(key, expires_in: expires_in)
end
def public_url(key, filename:)
generate_url(key, expires_in: nil)
end
def generate_url(key, expires_in:)
verified_key_with_expiration = ActiveStorage.verifier.generate(
{
key: key,
},
expires_in: expires_in,
)
url_helpers.rails_disk_service_url(verified_key_with_expiration)
end
end
end
private_url
とpublic_url
の違いは有効期限の有無のみで、URLの生成はgenerate_url
が担っています。MessageVerifier
3でBlob#key
を署名、URL helperでURLを生成しています。
rails_disk_service_url
はActive Storageのroutes.rb
で定義されています。
get "/disk/:encoded_key/*filename" => "active_storage/disk#show",
as: :rails_disk_service
Routing先のcontrollerを見てみましょう。
class ActiveStorage::DiskController < ActiveStorage::BaseController
def show
if key = decode_verified_key
serve_file named_disk_service(key[:service_name]).path_for(key[:key]))
else
head :not_found
end
end
private
def named_disk_service(name)
ActiveStorage::Blob.services.fetch(name) do
ActiveStorage::Blob.service
end
end
def decode_verified_key
key = ActiveStorage.verifier.verified(params[:encoded_key])
key&.deep_symbolize_keys
end
def serve_file(path)
Rack::Files.new(nil).serving(request, path).tap do |(status, headers, body)|
self.status = status
self.response_body = body
headers.each do |name, value|
response.headers[name] = value
end
end
end
end
MessageVerifier
で署名したBlob#key
を取り出し、DiskService#path_for
で対応するファイルのパスを得ています。実際にファイルを送る処理はRack::Files
に委譲しています。
module ActiveStorage
class Service::DiskService < Service
def path_for(key)
File.join root, folder_for(key), key
end
def folder_for(key)
[ key[0..1], key[2..3] ].join("/")
end
end
end
ファイルのパスを返しているだけですが、フォルダを階層化している点は注目に値します。これは1つのフォルダに置けるファイル数の上限と、パフォーマンスのためと思われます4(GitHub56に背景が書いておらず、はっきりとした理由は不明です)。
これにてActive Storageがファイルを返すまでの動きを理解することが出来ました。