write ahead log

ロールフォワード用

Railsでファイルアップロードを実装する

入門シリーズが続いている.

CarrierWave使うサンプル多いので, 使わないで実装してみる.

Railsは5.1.6

サンプルの内容

掲示板的に

  • メッセージ
  • 画像

のCRUDができるものにする.

とりあえず足場を作る

CRUD実装自体は目的ではないのでScaffoldで.

$ rails g scaffold message message:string
$ rails db:migrate

とりあえずメッセージ登録ができるようになる.

画像をDBへ保存するか, ファイル保存してパスを保存するか

一般的にはパス保存といわれている気がするし, 迷ったけど今回はDBへ保存する実装にした.

理由は

  • バックアップとリカバリが楽
  • アクセス制御も楽

特にアクセス制御についてはパスを保存の場合は, どうやってうまく制御するのかがわからなかった.

publicに入れてパスを保存というのが一般的なのかな?という気もするけど, エンタープライズだと画像もアクセス制御に気を付けないとならない.

UUIDみたいな当てようもないものをファイル名にしてパスを保存したりするのだろうか?

この辺よくわからない.知りたいなぁ.

まぁ, Oracleもポジショントークではあるもののこんな事言ってますし, DB保存もありではないかと.

データベースに画像を格納するメリット

画像保存用のモデルを追加する

imageモデルを作成し, 上述で作成したmessageモデルと関連させることにする.

$ rails g model image message:references filename:string data:binary
$ rails db:migrate

アソシエーションも追加.

# app/models/message.rb
class Message < ApplicationRecord
  has_one :images
end
class Image < ApplicationRecord
  belongs_to :message
end

アップロード機能を作る

下ごしらえが済んだので実装する.

view

_form.html.erbにfile_fieldを加えてやる.

1対1でアソシエーション張ったので一応fields_for使った.

  <div class="field">
    <%= form.label :message %>
    <%= form.text_field :message, id: :message_message %>
  </div>

  <!-- アップロード用に追加 -->
  <%= form.fields_for(message.image) do |f| %>
    <div class="field">
      <%= f.label :image %>
      <%= f.file_field :data, id: :image_data %>
    </div>
  <% end %>

controller

登録フォームを呼び出すときにイメージモデルのインスタンスを用意しておく.

  # GET /messages/new
  def new
    @message = Message.new
    @message.build_image  # 追加
  end

追加時にファイルを保存するように変更.

まずbuild_imageでImageモデルのインスタンスを用意しておく.

ストロングパラメータも新しく用意した.

で, データを取得してDBへ入れたいのだけど, image_params[:data]で取得できるのはActionDispatch::Http::UploadedFileのインスタンスらしい.

アップロード画像は付加情報も含めインスタンスで表現される.(実際のファイルは仮ファイルで別の場所へ保存)

ファイルのデータはこのインスタンスからreadメソッドで読み込める.

ファイル名はoriginal_filenameメソッドで取得できる.

リファレンス読むとcontent_typeも取得できるので, このサンプルのImageモデルに拡張子の項目を追加しなかったことを若干後悔.

  # POST /messages
  # POST /messages.json
  def create
    @message = Message.new(message_params)
    # ここから
    @image = @message.build_image
    @image.data = image_params[:image][:data].read
    @image.filename = image_params[:image][:data].original_filename
    # ここまで追加

    respond_to do |format|
      if @message.save
        format.html { redirect_to @message, notice: 'Message was successfully created.' }
        format.json { render :show, status: :created, location: @message }
      else
        format.html { render :new }
        format.json { render json: @message.errors, status: :unprocessable_entity }
      end
    end
  end

  private

    # Never trust parameters from the scary internet, only allow the white list through.
    def message_params
      params.require(:message).permit(:message)
    end

    # 追加したパラメータ取得メソッド
    def image_params
      params.require(:message).permit(image: [:data])
    end

ここまででアップロードは動作した.

若干ごちゃつく.

表示する

せっかくなら表示もさせたいので.

routeの追加

config/routes.rbへアップロードした画像を得るimageというルートを追加する.

  resources :messages do
    member do
      get 'image'
    end
  end

viewの変更

app/views/messages/show.html.erbへ表示用のタグを追加.

<p>
  <strong>Message:</strong>
  <%= @message.message %>
</p>

<%= image_tag(image_message_path(@message), alt: @message.image.filename) %>

controllerの変更

app/controllers/messages_controller.rbへ画像取得のアクションを追加する.

  # scaffoldで生成したサンプルなのでbefore_actionにimageも追加してやる
  before_action :set_message, only: [:show, :edit, :update, :destroy, :image]

  [中略]

  # GET /messages/1/image
  def image
    send_data @message.image.data, type: 'image/jpeg'
  end

ダウンロードリンクをつける

ついでに試した.

view

app/views/messages/show.html.erb

<%= image_tag(image_message_path(@message), alt: @message.image.filename) %>

<!-- 追加 -->
<%= link_to 'download', image_message_path(@message) %>

controller

app/controllers/messages_controller.rbのimageアクションを変更.

ファイル名をURLエンコードしてsend_dataへ渡してやるだけ.

  # GET /messages/1/image
  def image
    filename = ERB::Util.url_encode(@message.image.filename)
    send_data @message.image.data, type: 'image/jpeg', filename: filename
  end