noggy’s blog

自分用の備忘録です。。。

13.4 マイクロポストの画像投稿

ここではあとは応用として画像付きマイクロポストを投稿できるようにする。

手順としては、開発環境用のβ版を実装し、いくつかの改善をとおして本番環境用の完成版を実装する、
画像アップロード機能を追加するためには、2つの視覚的な要素が必要。
1つは、画像をアップロードするためのフォーム、
もう1つは、投稿された画像そのもの。
[Upload image]ボタンと画像付きマイクロポストのモックアップは↓

f:id:nogicchi:20210824075831p:plain
画像付きマイクロポストを投稿したときのモックアップ

13.4.1 基本的な画像アップロード

投稿した画像を扱ったり、Micropostモデルと関連付けするために、今回はCarrierWaveという画像アップローダーを使う。
まずはcarrierwave gemをGemfileに追加。

このとき、mini_magick gemとfog gemsも含める点に注目!
これらのgemは画像をリサイズしたり、本番環境で画像をアップロードするために使う。

source 'https://rubygems.org'
:
gem 'rails', '~>5.2.6'
gem 'bcrypt', '3.1.11'
gem 'faker', '~>1.7'
gem 'carrierwave',  '1.2.2' #追加
gem 'will_paginate', '3.1.7'
gem 'bootstrap-will_paginate', '1.0.0'

group :production do
  gem 'pg', '0.20.0'
  gem 'fog','1.42' #追加
end
:

$ bundle install

Carrier Waveを導入すると、Railsのジェネレーターで画像アップローダーが生成できるようになる。
次のコマンドを実行して適用する(画像のことをimageとすると一般的過ぎるので、pictureと呼ぶ)。

rails g uploader Picture

Carrier Waveでアップロードされた画像は、Active Recordモデルの属性と関連付けされているべき。
関連付けされる属性には画像のファイル名が格納されるため、pictureのデータ型はstring型にしておく。

microposts
id integer
content text
user_id integer
created_at datetime
update_at datetime
picture string
必要となるpicture属性をMicropostモデルに追加するために、マイグレーションファイルを生成し、開発環境のDBに適用する。

$ rails g migration add_picture_to_microposts picture:string
$ rails db:migrate

CarrierWaveに画像と関連付けたモデルを伝えるためには、mount_uploaderというメソッドを使う。
このメソッドは、引数に属性名のシンボルと生成されたアップローダーのクラス名を取る。

mount_uploader :picture, PictureUploader 

(picture_uploader.rbというファイルでPictureUploaderクラスが定義されている。
13.4.2で修正するが、今はデフォルトのままでok。)Micropostモデルにアップローダーを追加した結果↓

class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }                                 
  mount_uploader :picture, PictureUploader  # 追加
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end


テストは成功

Homeページにアップローダーを追加するためには、マイクロポストのフォームにfile_fieldタグを含める必要がある。

<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
  <span class="picture">
    <%= f.file_field :picture %> #追加
  </span>
<% end %>

最後に、Webから更新できる許可リストにpicture属性を追加する。追加すると、micropost_paramsメソッドは次のようになる。

class MicropostsController < ApplicationController
  before_action :logged_in_user, only: [:create, :destroy]
  before_action :correct_user,   only: :destroy
  :
  private

    def micropost_params
      params.require(:micropost).permit(:content, :picture)   
    end

    def correct_user
      @micropost = current_user.microposts.find_by(id: params[:id])
      redirect_to root_url if @micropost.nil?
    end

一度画像がアップロードされれば、Micropostパーシャルのimage_tagヘルパーでその画像を描画できるようになる。また、画像の無い(テキストのみの)マイクロポストでは画像を表示させないようにするため、picture?という論理値を返すメソッドを使っている。

マイクロポストの画像表示を追加したコードは次↓

<li id="micropost-<%= micropost.id %>">
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content">
    <%= micropost.content %>
    <%= image_tag micropost.picture.url if micropost.picture? %> #追加
  </span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  <% if current_user?(micropost.user) %>
    <%= link_to "delete", micropost, method: :delete,
                                    data: { confirm: "You sure?" } %>
  <% end %>
  </span>
</li>

13.4.2 画像の検証

13.4.1のアップローダーにはいくつかの欠点がある。
例えば、アップロードされた画像に対する制限がないため、もしユーザーが巨大なファイルを上げたり、無効なファイルを上げると問題が発生してしまう。

この欠点を直すために、画像サイズやフォーマットに対するバリデーションを実装し、サーバー用とクライアント(ブラウザ)用の両方に追加する。

最初のバリデーションでは、有効な画像の種類を制限していくが、これはCarrierWaveのアップローダーの中に既にヒントがある。
生成されたアップローダーの中にコメントアウトされたコードがあるが、ここのコメントアウトを取り消すことで、画像のファイル名から有効な拡張子(PNG/GIF/JPEG)を検証することができる。

  storage :file

  # アップロードファイルの保存先ディレクトリは上書き可能
  # 下記はデフォルトの保存先
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # アップロード可能な拡張子のリスト
  def extension_whitelist
    %w(jpg jpeg gif png)
  end

2つ目のバリデーションでは、画像のサイズを制御する。

これはMicropostモデルに書き足していく。先ほどのバリデーションと異なり、ファイルサイズに対するバリデーションはRailsの既存のオプション(presencelengthなど)には無い。
したがって、手動でpicture_sizeというバリデーションを定義する。

独自のバリデーションを定義するために、今まで使っていたvalidatesではなく、validate(単数形)メソッドを使っている点に注目!

class Micropost < ApplicationRecord
  belongs_to :user
  default_scope -> { order(created_at: :desc) }  
  mount_uploader :picture, PictureUploader   
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
  validate :picture_size #追加
end

  private

    # アップロードされた画像のサイズをバリデーションする←追加
    def picture_size
      if picture.size > 5.megabytes
        errors.add(:picture, "should be less than 5MB")
      end
    end
end

validateメソッドでは、引数にシンボル(:picture_size)を取り、そのシンボル名に対応したメソッドを呼び出す。また、呼び出されたpicture_sizeメソッドでは、5MB上限とし、それを超えた場合はカスタマイズしたエラーメッセージをerrorsコレクションに追加している。

先で定義した画像のバリデーションビューに組み込むために、クライアント側に2つの処理を追加。
まずはフォーマットのバリデーションを反映するためには、file_fieldタグにacceptパラメータを付与して使う。

<%= f.file_field :picture, accept: 'image/jpeg,image/git,image/png' %>

このときacceptパラメータでは、許可したファイル形式を、MIMEタイプで指定するようにする。

次に、大きすぎるファイルサイズに対して警告を出すために、ちょっとしたJavaScript(正確にはjQuery)を書き加える。
こうすることで、長すぎるアップロード時間を防いだり、サーバーへの負荷を抑えたりすることに繋がる。

$('#micropost_picture').bind('change', function() {
 var size_in_megabytes = this.files[0].size/1024/1024;
 if (size_in_megabytes > 5) {
   alert('Maximum file size is 5MB. Please chooose a smailler file.'); 
 } 
});

上のコードではCSS idのmicropost_pictureを含んだ要素を見つけ出し、この要素を監視している。
そして、このidを持った要素とは、マイクロポストのフォームを指す(なお、ブラウザ上で画面を右クリックし、インスペクターで要素を調べることで確認できる)。つまり、このCSS idを持つ要素が変化したとき、このjQueryの関数が動き出す。
そして、もしファイルサイズが大きすぎた場合、alertメソッドで警告を出すといった仕組み。

これらの追加的なチェック機能をまとめる↓

<%= form_for(@micropost) do |f| %>
  <%= render 'shared/error_messages', object: f.object %>
  <div class="field">
    <%= f.text_area :content, placeholder: "Compose new micropost..." %>
  </div>
  <%= f.submit "Post", class: "btn btn-primary" %>
  <span class="picture">
    <%= f.file_field :picture, accept: 'image/jpeg,image/git.image/png' %>
  </span>
<% end %>
<!-- 追加ココから-->
<script type="text/javascript">
  $('micropost_picture').bind('change', function() {
   var size_in_megabytes = this.files[0].size/1024/1024;
   if (size_in_megabytes > 5) {
    alert('Maximum file size is 5MB. Please choose a smaller file.'); 
   }
  });
</script>

上のコードでは大きすぎるファイルのアップロードを完全には阻止できない。
例えば、ユーザーはアラートを無視してアップロードを強行する、といったことが可能。

今回は「上のコードでは実装はまだ不完全である」という点だけ覚えておく!
また、仮に送信フォームを使った投稿をうまく制限できても、ブラウザのインスペクタ機能でJavaScriptをいじったり、curlなどを使って直接POSTリクエストを送信する場合には対応しきれない。

13.4.3 画像のリサイズ

画像を表示させる前にサイズを変更する(リサイズする)ようにしてみる。

f:id:nogicchi:20210824095828p:plain
恐ろしく大きなアップロード画像

画像をリサイズするために、UnageMagickというプログラムを使うので、これを開発環境にインストールする。
(あとでも説明するが、本番環境がHerokuであれば、既に本番環境でImageMagickが使えるようになっている)。
クラウドIDEでは、次のコマンドであればインストールできる。

$ sudo yum install -y ImageMagick

次に、MiniMagickというImageMagickRubyをつなぐgemを使って、画像をリサイズしてみる。
MiniMagickのドキュメント(英語)を見ると、様々な方法でリサイズできることが分かる。

今回はresize_to_limit: [400, 400]という方法を使う。
これは縦横どちらかが400pxを超えていた場合、適切なサイズに縮小するオプション(ただし小さい画像であっても拡大はしない)。

class PictureUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick #追加
  process resize_to_limit: [400, 400] #追加

  storage :file

  # アップロードファイルの保存先ディレクトリは上書き可能
  # 下記はデフォルトの保存先
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # アップロード可能な拡張子のリスト
  def extension_whitelist
    %w(jpg jpeg gif png)
  end