MirrorService#mirror が Blob メタデータを転送するように修正

rails/rails

MirrorService#mirror がコピー先のストレージへ content_typefilenamedispositioncustom_metadata を渡さず、application/octet-stream で保存されていた問題を解消します。Blob のメタデータを取得して転送することで、ミラー先でもオリジナルと同一のヘッダーが保持されます。

背景

ActiveStorage の MirrorService#mirror は非同期ジョブ MirrorJob から呼び出され、ミラー先サービスへの uploadchecksum: だけを渡していました。その結果、S3・Azure・GCS などヘッダーを保持できるストレージでは content_type が未設定となり、デフォルトの application/octet-stream が付与されました。これにより、CDN のカスタムドメイン経由や <img> タグで直接バケットを参照した場合に、ブラウザがコンテンツを正しく解釈できませんでした。

この不整合は Issue #57270 で報告され、Rails 7.1.5.1 の本番環境で Cloudflare R2 + カスタムドメイン CDN を利用しているケースで確認されています。メタデータが失われると、画像や動画のインライン表示だけでなく、範囲リクエストやキャッシュヒット率にも影響が出るため、根本的な修正が求められました。

技術的な変更

ActiveStorage::Blob#service_metadata がプライベートから公開メソッドへ変更されました。# :nodoc: が付与され、外部から直接呼び出すことは想定されませんが、同一モジュール内の他クラスから安全に利用できます。メソッドは forcibly_serve_as_binary?allowed_inline? の条件に応じて、返すハッシュに dispositionfilename を含めるかを分岐させます。

@@
   def service_metadata # :nodoc:
     if forcibly_serve_as_binary?
       { content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
     elsif !allowed_inline?
       { content_type: content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
     else
       { content_type: content_type, custom_metadata: custom_metadata }
     end
   end

同時に、同メソッドの古いプライベート実装が削除され、#service_metadata の唯一の定義は上記に集約されました。これによりコードベースの重複が解消され、メタデータ取得ロジックの一元管理が実現します。

@@
-    def service_metadata
-      if forcibly_serve_as_binary?
-        { content_type: ActiveStorage.binary_content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
-      elsif !allowed_inline?
-        { content_type: content_type, disposition: :attachment, filename: filename, custom_metadata: custom_metadata }
-      else
-        { content_type: content_type, custom_metadata: custom_metadata }
-      end
-    end

MirrorService#mirror では、対象キーに紐付く Blob を検索し、取得した service_metadataupload のキーワード引数として展開するよう改修されました。Blob が見つからない場合は空ハッシュ {} を使用し、従来通りメタデータなしでアップロードします。

@@
-          mirrors_in_need_of_mirroring = mirrors_needing_mirroring(key)
-          if mirrors_in_need_of_mirroring.any?
+          mirrors_in_need_of_mirroring = mirrors_needing_mirroring(key)
+          if mirrors_in_need_of_mirroring.any?
+            metadata = ActiveStorage::Blob.find_by(key: key)&.service_metadata || {}
@@
-                service.upload key, StringIO.new(content), checksum: checksum
+                service.upload key, StringIO.new(content), checksum: checksum, **metadata

テストスイートにも mirroring forwards blob metadata to the mirror services という新規テストが追加され、各ミラーサービスの upload へ渡されたオプションに content_typefilenamedisposition が正しく含まれることを検証しています。また、Blob 行が存在しないケースでも例外が発生せずにアップロードが継続されることを保証するテストも併せて提供されています。

@@
-  test "mirroring forwards blob metadata to the mirror services" do
+  test "mirroring forwards blob metadata to the mirror services" do
@@
-    received.each do |opts|
-      assert_equal "text/plain", opts[:content_type]
-      assert_equal "report.txt", opts[:filename].to_s
-      assert_equal :attachment, opts[:disposition]
-    end
+    received.each do |opts|
+      assert_equal "text/plain", opts[:content_type]
+      assert_equal "report.txt", opts[:filename].to_s
+      assert_equal :attachment, opts[:disposition]
+    end

設計判断

Blob#service_metadata を公開メソッドとして提供 した点は、既存コードベースで send を使ってプライベートメソッドにアクセスする手法と比較して、可読性と保守性が向上するという判断に基づきます。unfurlupload_without_unfurlingcomposemirror_later が同様に # :nodoc: 付きで公開されていることから、内部的に共有すべきロジックは明示的に公開しつつ、外部 API としては文書化しない方針が一貫しています。

メタデータ取得が失敗した場合に 空ハッシュ {} にフォールバック する実装は、キーが孤立した状態(例:手動で削除された Blob)でもミラー処理が中断されないようにする安全策です。これにより、既存のミラー機構に対する破壊的変更を回避しつつ、正常系でのヘッダー保持という新機能を提供しています。

まとめ

MirrorService#mirror が Blob の service_metadata を転送するようになったことで、S3・Azure・GCS などのミラー先でもオリジナルと同一のコンテンツヘッダーが保持され、application/octet-stream に起因する表示不具合が解消されます。Blob#service_metadata の公開と安全なフォールバック設計により、既存コードへの影響を最小限に抑えつつ、ミラー機構全体の一貫性が向上しました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
6150d473

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文・背景・技術的な変更・設計判断(任意)・まとめの3部構成が明確に存在し、各要素が適切に記述されています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックは ````ruby:ファイルパス```` 形式で正しく記述され、PRリンクも正しいMarkdown記法で記載されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

対象はRails/ActiveStorageに精通したエンジニアであり、専門用語の使用や説明レベルは適切です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論・各論・結論の流れを保ち、段落はトピックセンテンスで始まり1段落1トピック・6文未満で整理されています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のコードブロックは提供されたDiffと一致しており、追加・削除内容ともに正しく反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

service_metadata、content_type、disposition などの用語は正しく使用され、PRでの記述と合致しています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な変更点や影響について、PRの記述とDiffに基づいた正確な説明がなされています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事の主張はすべてPRのタイトル・説明・Diffで裏付けられており、根拠のない推測はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号 #57595、Issue番号 #57270 などの数値は正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルはPRの内容「Forward blob metadata to MirrorService mirrors」を的確に日本語化しており、一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

バージョンサポートやリリース日程など、PRに記載されていない外部情報は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の歪曲はなく、PRの記述と整合しています。