From d25ee53f6db1b4e5c1248db69b961af56628937d Mon Sep 17 00:00:00 2001 From: Shouichi Kamiya Date: Fri, 23 Feb 2024 11:37:07 +0900 Subject: [PATCH] =?UTF-8?q?Active=20Storage=E3=81=AE=E4=BB=95=E7=B5=84?= =?UTF-8?q?=E3=81=BF=E3=81=9D=E3=81=AE2=E3=82=92=E8=BF=BD=E5=8A=A0=20(#25)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: anipos-bulldozer[bot] <56748120+anipos-bulldozer[bot]@users.noreply.github.com> --- ...4-02-23-active-storage-internals-part-2.md | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 _posts/2024-02-23-active-storage-internals-part-2.md diff --git a/_posts/2024-02-23-active-storage-internals-part-2.md b/_posts/2024-02-23-active-storage-internals-part-2.md new file mode 100644 index 0000000..402fd3a --- /dev/null +++ b/_posts/2024-02-23-active-storage-internals-part-2.md @@ -0,0 +1,177 @@ +--- +layout: post +title: "Active Storageの仕組み(その2)" +author: "@shouichi" +date: 2024-02-23 11:34:36 +09:00 +tags: rails +--- + +前回[^1]に引き続きActive Storageの仕組みを見てみましょう。前回は`image_tag user.avatar`が、`ActiveStorage::Blobs::RedirectController#show`にroutingされることを突き止めました。今回はその中身を見ることから始めてみましょう。 + +[^1]: [Active Storageの仕組み(その1)]({% link _posts/2024-02-16-active-storage-internals-part-1.md %}) + +**※抜粋するRailsのコードは説明のために大幅に編集・簡略化してあります。** + +```ruby +class ActiveStorage::Blobs::RedirectController < ActiveStorage::BaseController + def show + redirect_to @blob.url + end +end +``` + +`ActiveStorage::Blob#url`でURLを生成し、そこにリダイレクトしています。 + +```ruby +class ActiveStorage::Blob < ActiveStorage::Record + def url + service.url key + end +end +``` + +`ActiveStorage::Blob#service`を呼び出しているだけで、その実体は`ActiveStorage::Service::DiskService`です。 + +```ruby +irb> User.new.build_avatar_blob.service +=> # +``` + +```ruby +module ActiveStorage + class Service::DiskService < Service + end +end +``` + +さて、`ActiveStorage::Service::DiskService`のファイルを読んでも`#url`は見当たりません。困ったようですが、Rubyの`source_location`[^2]を使えば、定義ファイルと行を調べられます。 + +[^2]: [class Method - Documentation for Ruby 3.3](https://docs.ruby-lang.org/en/3.3/Method.html#method-i-source_location) + +```ruby +irb> User.new.build_avatar_blob.service.method(:url).source_location +=> ["activestorage-7.1.3/lib/active_storage/service.rb", 119] +``` + +親クラスに定義されていました、その中身を確認してみましょう。 + +```ruby +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`に戻りましょう。 + +```ruby +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を生成しています。 + +[^3]: [ActiveSupport::MessageVerifier](https://api.rubyonrails.org/classes/ActiveSupport/MessageVerifier.html) + +`rails_disk_service_url`はActive Storageの`routes.rb`で定義されています。 + +```ruby +get "/disk/:encoded_key/*filename" => "active_storage/disk#show", + as: :rails_disk_service +``` + +Routing先のcontrollerを見てみましょう。 + +```ruby +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`に委譲しています。 + +```ruby +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](GitHub[^5][^6]に背景が書いておらず、はっきりとした理由は不明です)。 + +[^4]: [How many files can I put in a directory?](https://stackoverflow.com/questions/466521/how-many-files-can-i-put-in-a-directory) +[^5]: [Add Active Storage to Rails](https://github.com/rails/rails/pull/30019) +[^6]: [Add Active Storage to Rails](https://github.com/rails/rails/pull/30020) + +これにてActive Storageがファイルを返すまでの動きを理解することが出来ました。