active storageをインストール
$ rails active_storage:install $ rails db:migrate
model, migrationを作成
$ rails g model Hoge $ rails db:migrate
Modelを作成
# app/models/hoge.rb class Hoge < ApplicationRecord MAX_IMAGE_SIZE = 2 # 2MBまで has_one_attached :image end
Controllerを作成
$ rails g controller Hoges new create
# app/controllers/hoges_controller.rb class HogesController < ApplicationController def new @hoge = Hoge.new end def create @hoge = Hoge.new(hoge_params) if @hoge.save redirect_to '/' else render :new end end private def hoge_params params.require(:hoge).permit(:image) end end
Routesを定義
Rails.application.routes.draw do resources :hoges, only: [:new, :create] end
Viewを作成
# app/views/hoges/new.html.erb <%= form_with model: @hoge, url: hoges_path, data: { turbo: false } do |f| %> <% aspect_width = 1 %> <% aspect_height = 1 %> <div data-controller='image-preview' data-image-preview-max-image-size-value="<%= Hoge:MAX_IMAGE_SIZE %>" data-image-preview-aspect-width-value="<%= aspect_width %>" data-image-preview-aspect-height-value="<%= aspect_height %>"> <%= f.file_field :image, accept: 'image/jpeg,image/png', data: { action: 'change->image-preview#preview', target: 'image-preview.image' } %> </div> <%= f.submit '保存' %> <% end %>
image_preview_controller.jsを使用するため、<div data-controller='image-preview'>
で囲みます。また、アスペクト比(data-image-preview-aspect-width-value
, data-image-preview-aspect-height-value
)や上限サイズ(data-image-preview-max-image-size-value
)の値を渡しています。
JSを作成
// app/javascript/controllers/image_preview_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static values = { maxImageSize: Number, aspectWidth: Number, aspectHeight: Number } static targets = ["image"] preview(event) { // this.imageTarget.files[0].sizeはバイト単位だから、÷1024でキロバイト、さらに÷1024でメガバイトに変換 var size = this.imageTarget.files[0].size / 1024 / 1024; if (size > this.maxImageSizeValue) { alert(`ファイルサイズは${this.maxImageSizeValue}MB以下です。`); // 選択されたファイルを空にする this.imageTarget.value = ''; return } const input = event.target if (input.files && input.files[0]) { const reader = new FileReader() // image.onload内のthisは画像自体のことなので使い分けるため_this変数にする const _this = this; const aspectWidth = this.aspectWidthValue; const aspectHeight = this.aspectHeightValue; reader.onload = (e) => { const image = new Image(); image.onload = function () { // view側からdata-image-preview-aspect-width-valueやdata-image-preview-aspect-height-valueが渡されなかった場合、this.aspectWidthValue・this.aspectHeightValueは0が入る if (aspectWidth != 0 && aspectHeight != 0 && this.width / this.height !== aspectWidth / aspectHeight) { alert(`アスペクト比は${aspectWidth}:${aspectHeight}である必要があります。`); _this.imageTarget.value = ''; return; } } image.src = e.target.result } reader.readAsDataURL(input.files[0]) } } }
これで上限以上のサイズや指定以外のアスペクト比の画像をアップロードしようとした場合にアラートを表示して阻止できます。今回は割愛しましたがgemなどを使ってファイルサイズやアスペクト比をモデル単位でバリデーションすると更に安全になります。