Active Storage導入しました
背景、経緯、目的
業務でRailsを使ってWebサービスを開発しているのですが、
ユーザーの画像設定やファイル添付機能等をシンプルに実施したく、
またRailsのバージョンアップ等のタイミングもあり、 Active Storageの導入を決めました。
(CarrierWave vs Active Storageみたいな議論は本記事では扱いません)
純粋にRailsからアップロードし、AWS S3に保存するような設計です。
環境
主な使用方法
- 以下コマンド等でテーブルを作成、migrationを実施
rails active_storage:install rails db:migrate
- 各種設定 今回は開発環境ではlocal、本番環境等ではAWS S3に保存するイメージです。
# Gemfile gem 'aws-sdk-s3'
# config/environments/development.rb config.active_storage.service = :local # config/environments/production.rb config.active_storage.service = :amazon
# config/storage.yml local: service: Disk root: <%= Rails.root.join("storage") %> /storage/* !/storage/.keep amazon: service: S3 access_key_id: <%= ENV['AWS_S3_ACCESS_KEY_ID'] %> secret_access_key: <%= ENV['AWS_S3_SECRET_ACCESS_KEY'] %> region: <%= ENV['AWS_S3_REGION'] %> bucket: <%= ENV['AWS_S3_BUCKET'] %>
- 該当Modelに紐付け
# app/models/book.rb class AdminUser < ApplicationRecord # 1対1の場合 has_one_attached :image # 1対複数の場合 has_many_attached :images end
- フォーム等イメージ
-# app/views/books/edit.html.haml -# 複数の場合 multiple: trueを指定、accept: 'image/jpeg,image/png'等で受付可能なMIMEタイプ指定可能 = f.file_field :files, accept:
- 表示イメージ
-# app/views/books/show.html.haml %dl %dt 表紙イメージ %dd= image_tag @book.image
Tips
attribute代入タイミング
有名な話なのですが、Rails5.2〜6.0までは、attributeに代入した時点で、
アップロードしたファイルがストレージに保存されてしまいます。
以下のようにすることが良くあるかと思いますが、バリデーションエラー時等に、
そのままファイルが残ってしまうという問題点があります。
@book = Book.new(title: 'サンプル本', image: params[:image]) if @book.save # 省略 redirect_to dashboard_path else # 省略 render 'edit' end
Rails6.0以上ではsave後に保存されるようになったので、気にする必要は無いのですが、 それ以前のバージョンでは、以下等の対応が必要になります。
- バリデーションエラーの場合、purgeを呼び出す
@book.image.purge errors.add(:image, '指定のファイル形式ではありません。')
- バリデーション等実施、save後にattachするようにする(別途save前にimageのバリデーションも必要)
if @book.save @book.image = params[:image] if params[:image] else end
purge_later
ActiveStorageでは、
has_one_attached
、has_many_attached
において、
デフォルトでdependent: :purge_later
となっており、該当インスタンスが削除された場合、
Active Jobを介して、関連付けられているモデルと実際のリソースファイルを非同期で破棄します。
また、キューは以下のように必要に応じて指定が必要です。 (Rails6.1以降はqueues.analysis, queues.purgeに分割して指定することが推奨されています。)
# config/application.rb config.active_storage.queue = 'default'
nginx設定、413エラー等
個人的に一番ハマったポイントです。 まずデフォルトではnginxのPOST最大サイズは1MBのようです。 必要に応じて、以下のように
client_max_body_size
を指定する必要があります。
# script/nginx/nginx-development.conf
server {
listen 8080;
server_name xxx.xxx.xx.xxx;
root /app/public;
client_max_body_size 10m;
}
ただこのままでは、サイズ超過したファイルをアップロード時に、
413エラー画面がnginxのデフォルトの画面になってしまいます。
本件を解決する方法はいくつかあるかと思いますが、Webアプリ側はGET http(s)://{host}/payload_too_large
でエラーページを表示できるようにし、
413エラーを以下のようにリダイレクトを使い、
POSTをGETとし、オリジナルのエラーページを表示できるようにしています。
location / { error_page 413 @413; } location @413 { return 303 https://$host/payload_too_large; }
これで全て解決と思ったのですが、
ChromeやFirefoxでは実際にサイズ超過したファイルをアップロードすると、
数百MBまでは問題ないのですが1GB等を超過すると、以下エラーが出るようになりました。
(Safariではどれだけサイズが大きくても、問題なくリダイレクトされる)
ChromeやFirefoxでは、nginxが413エラーを返した場合も、
アップロード処理を継続してしまうという仕様のようです...
よって、JS等フロントで予め必要以上のサイズのファイルは、
アップロードできないよう制御しておくのが良さそうです。
以下サンプルではファイルの複数アップロードを想定し、
ファイル数およびファイルサイズで制御をかけています。
// book_file_upload.js document.addEventListener('DOMContentLoaded', function() { const lengthLimit = 3 const sizeLimit = 1024 * 1024 * 10 const inputTemplate = document.getElementById('book_files'); var fileLength = 0 $('.file-uploads').on('change', function(e){ var files = e.target.files; // 添付ファイルボタンのinputは空に戻す e.target.files = new DataTransfer().files; fileLength += files.length // ファイル数バリデーションエラー表示 if (fileLength > lengthLimit) { alert('1つのBookに添付できるファイル数は最大3つです。'); fileLength -= files.length return; } var error_message = '' for (let i = 0; i < files.length; i++) { var file = files[i] if (file.size > sizeLimit) { error_message = 'Bookに添付できる1ファイルの最大サイズは10MBです。'; continue; } // アップロード予定ファイルプレビュー表示 var file_preview = $('<div>', { class: 'file-preview' }); file_preview.append(`<i class= "fa fa-file"></i>${file.name}`); file_preview.append('<span class="btn-delete">削除</span>'); // ファイルを個別に削除等できるよう分けてinputへ格納 var dt = new DataTransfer(); dt.items.add(file); var input = inputTemplate.cloneNode(false); input.files = dt.files; input.id = `book_files_${file.name}`; file_preview.append(input); $('.uploaded-files').append(file_preview); }; // ファイルサイズバリデーションエラー表示 if (error_message != '') alert(error_message); }) // アップロードファイルの削除 $('.uploaded-files').on('click', '.btn-delete', function(){ fileLength -= 1 $(this).parent().remove(); }); });
spec
上記等のバリデーションテスト等で、サイズ上限のバリデーション等を試したいこともあるかと思います。
ただその為だけに、大きいサイズのファイルを用意はできないケースが多いと思うので、モック等で対応するのが良いかと思います。
let(:images) { [image] } let(:image) { Rack::Test::UploadedFile.new(Rails.root.join('app', 'assets', 'images', 'sample.png'), 'image/png') } before do allow(image).to receive(:size).and_return(11.megabytes) end
感想
- Rails6.0以降改善されているとのことですが、
やはりsave前のattribute代入でアップロードされてしまうのはやや面倒 - シンプルなアップロード機能であれば、特に問題は感じませんでした!
参考記事
Rails ガイド railsguides.jp
413エラーのリダイレクト qiita.com
413エラー、ブラウザによる違い
stackoverflow.com