前回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_location2を使えば、定義ファイルと行を調べられます。

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_urlpublic_urlの違いは有効期限の有無のみで、URLの生成はgenerate_urlが担っています。MessageVerifier3Blob#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がファイルを返すまでの動きを理解することが出来ました。