読者です 読者をやめる 読者になる 読者になる

手取り足取りプログラミング

cloud9、railsについて自分がつまずいた所を詳しく説明します。なぜなら一番読み返すのは自分だから。

【Rails】ファイルをDBに保存・ダウンロードする方法

はじめに

概要

ファイルをDBに保存し、保存したファイルをダウンロードするサンプルを作成します。

バージョン

ruby 2.3.0
Rails 4.2.5

前提条件

下記記事内のCloud9でテンプレートとして用意したプロジェクトをクローンして作成しています。
yonayonaru.hatenablog.com

ソースコード

ソースコードはERBとSlim両方用意していますが、ブログ内ではSlimで説明します。
ERB版
github.com

Slim版
github.com


コマンド

scaffoldで画面を作成します。

  • description:ファイルの説明
  • name:ファイル名
  • file:ファイルのデータ
  • content_type:コンテンツタイプ

rails g scaffold file_store description name file:binary content_type
rake db:migrate

Model

ファイルを必須にします。
※別に必須にしなくてもいいけど、ファイルがない状態だとダウンロードリンクをクリックするとエラーになる。回避するためにviewでif文が必要。こっちのが楽という理由

app/models/file_store.rb

class FileStore < ActiveRecord::Base
  validates :file, presence: true
end

Controller

3か所追加修正します。

  • 赤字1つ目(上の方) before_actionにdownloadを追加します。
  • 赤字2つ目(真ん中より下) downloadメソッドを用意します。viewでこのメソッドを呼ぶとダウンロードすることができます。
  • 赤字3つ目(下の方) file_store_params内の1行目permitからname、file、content_typeを削除し、その下で別途設定しています。nameとcontent_typeは画面で入力しないし、fileはバイナリなので正しく値がセットされないためです。

app/controllers/file_stores_controller.rb

class FileStoresController < ApplicationController
  before_action :set_file_store, only: [:show, :edit, :update, :destroy, :download]

  # GET /file_stores
  # GET /file_stores.json
  def index
    @file_stores = FileStore.all
  end

  # GET /file_stores/1
  # GET /file_stores/1.json
  def show
  end

  # GET /file_stores/new
  def new
    @file_store = FileStore.new
  end

  # GET /file_stores/1/edit
  def edit
  end

  # POST /file_stores
  # POST /file_stores.json
  def create
    @file_store = FileStore.new(file_store_params)

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

  # PATCH/PUT /file_stores/1
  # PATCH/PUT /file_stores/1.json
  def update
    respond_to do |format|
      if @file_store.update(file_store_params)
        format.html { redirect_to @file_store, notice: 'File store was successfully updated.' }
        format.json { render :show, status: :ok, location: @file_store }
      else
        format.html { render :edit }
        format.json { render json: @file_store.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /file_stores/1
  # DELETE /file_stores/1.json
  def destroy
    @file_store.destroy
    respond_to do |format|
      format.html { redirect_to file_stores_url, notice: 'File store was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  def download
    send_data(@file_store.file, type: @file_store.content_type, filename: @file_store.name, disposition: :attachment)
  end
  
  private
    # Use callbacks to share common setup or constraints between actions.
    def set_file_store
      @file_store = FileStore.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def file_store_params
      file_store = params.require(:file_store).permit(:description)
      if params[:file_store][:file]
        file_store[:name] = params[:file_store][:file].original_filename
        file_store[:content_type] = params[:file_store][:file].content_type
        file_store[:file] = params[:file_store][:file].read
      end
      file_store
    end
end

View

index.html

  • 赤字1つ目 bootstrapのレイアウトを当てるために .tableを追加しています。デザインの問題なのでもちろんなくてもいい。
  • 赤字2つ目 間に合った th name を削除しています。
  • 赤字3つ目 name部分を削除しています。link_toの部分がダウンロードのリンクとなります。target: "_blank"がないとダウンロードではなく画面遷移してファイルの内容が表示されます。

app/views/file_stores/index.html.slim

h1 Listing file_stores

table.table
  thead
    tr
      th Description
      th File
      th Content type
      th
      th
      th

  tbody
    - @file_stores.each do |file_store|
      tr
        td = file_store.description
        td = link_to file_store.name, download_file_store_path(file_store), target: "_blank"
        td = file_store.content_type
        td = link_to 'Show', file_store
        td = link_to 'Edit', edit_file_store_path(file_store)
        td = link_to 'Destroy', file_store, data: { confirm: 'Are you sure?' }, method: :delete

br

= link_to 'New File store', new_file_store_path

_form.html

登録、更新部分です。

  • 赤字部分 name、context_typeを削除しています。また、f.text_field : file を f.file_field_:file に変更しています。これでファイル選択が表示されます。

app/views/file_stores/_form.html.slim

= form_for @file_store do |f|
  - if @file_store.errors.any?
    #error_explanation
      h2 = "#{pluralize(@file_store.errors.count, "error")} prohibited this file_store from being saved:"
      ul
        - @file_store.errors.full_messages.each do |message|
          li = message

  .field
    = f.label :description
    = f.text_field :description
  .field
    = f.label :file
    = f.file_field :file
  .actions = f.submit

show.html

  • 赤字部分 name部分を削除しています。index.html と同じようダウンロードのリンクを表示するため link_to 〜 に変更しています。

app/views/file_stores/show.html.slim

p#notice = notice

p
  strong Description:
  = @file_store.description
p
  strong File:
  = link_to @file_store.name, download_file_store_path(@file_store), target: "_blank"
p
  strong Content type:
  = @file_store.content_type

= link_to 'Edit', edit_file_store_path(@file_store)
'|
= link_to 'Back', file_stores_path

ルーティング

コントローラに追加したdownloadをroutesに追加します。
また、indexページをrootに設定しておきます。

config/routes.rb

Rails.application.routes.draw do
  resources :file_stores do
    member { get :download }
  end
  
  root 'file_stores#index'
end

完成

以上で完成です。
indexページだけ貼っておきます。
f:id:yonayonaru:20170309224722j:plain