Rails でファイルのアップロードとダウンロード

Rails でのファイルのアップロードとダウンロードを試してみます。
調べると「CarrierWave」や「ActiveStorage」を使う記事を見かけることが多いです。
今回は「CarrierWave」と「ActiveStorage」を使ってみます。

目次

参考


そもそも gem を使わないと実装できないのか?

一応 gem を使わないで実装できないのか確認したので、参考に記載します。

モデル作成

1
2
rails generate scaffold uploadfile name:string data:binary
rails db:migrate

ルーティングの編集

config/routes.rb を編集して、ファイルダウンロード用のルーティングを追加。
アップロードしたファイルを編集ということもないでしょうから、:edit と:update を resources から除きます。

config/routes.rb
1
2
3
4
Rails.application.routes.draw do
get 'uploadfiles/download/:id',to: "uploadfiles#download",as: "download"
resources :uploadfiles,only: [:index,:show,:create,:destroy]
end

以上を設定して``rails routes`を実行すると、ルーティングを確認できる。

1
2
3
4
5
6
        Prefix Verb   URI Pattern                       Controller#Action
uploadfiles GET /uploadfiles(.:format) uploadfiles#index
POST /uploadfiles(.:format) uploadfiles#create
new_uploadfile GET /uploadfiles/new(.:format) uploadfiles#new
uploadfile GET /uploadfiles/:id(.:format) uploadfiles#show
DELETE /uploadfiles/:id(.:format) uploadfiles#destroy

コントローラの編集

create アクションで、入力された名称とアップロードされたファイルの拡張子をつないで、新しいファイル名を作成。
アップロードされたファイルのデータを格納してデータベースに保存します。

app/controllers/uploadfiles_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class UploadfilesController < ApplicationController
before_action :set_uploadfile, only: [:show, :edit, :update, :destroy, :download]

#省略

def create
file={}
#登録したファイル名+アップロードしたファイルの拡張子で新しくファイル名を作成
file[:name]= "#{uploadfile_params[:name]}.#{uploadfile_params[:data].original_filename.split('.')[1]}"
#アップロードしたファイルの中身をdataに格納
file[:data]=uploadfile_params[:data].read

@uploadfile =Uploadfile.new(file)

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

def download
#呼び出されたファイルを送信
send_data(@uploadfile.data,:filename=>@uploadfile.name)
end

# 省略
end

ビューの編集

app/views/uploadfiles/index.html.erb

app/views/uploadfiles/index.html.erb はルーティングから取り除いた edit の削除、
uploadfile.dataの表示を削除します。

app/views/uploadfiles/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<p id="notice"><%= notice %></p>

<h1>Uploadfiles</h1>

<table>
<thead>
<tr>
<th>Name</th>
<th colspan="2"></th>
</tr>
</thead>

<tbody>
<% @uploadfiles.each do |uploadfile| %>
<tr>
<td><%= uploadfile.name %></td>
<td><%= link_to 'Show', uploadfile %></td>
<td><%= link_to 'Destroy', uploadfile, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>

<br>

<%= link_to 'New Uploadfile', new_uploadfile_path %>

app/views/uploadfiles/show.html.erb

app/views/uploadfiles/show.html.erb は、<%= link_to 'download', download_path(@uploadfile.id) %>を追加します。
uploadfiles_controller.rb の download で処理されるように、download_path(@uploadfile.id)の様に記述します。

app/views/uploadfiles/show.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
<p id="notice"><%= notice %></p>

<p>
<strong>Name:</strong>
<%= @uploadfile.name %>
</p>

<p>
<strong>Data:</strong>
<%= link_to 'download', download_path(@uploadfile.id) %>
</p>

<%= link_to 'Back', uploadfiles_path %>

app/views/uploadfiles/_form.html.erb

app/views/uploadfiles/_form.html.erb を編集して、以下のようにします。
ファイルアップロードの口としてのフォームには、.file_field を使用します。

app/views/uploadfiles/_form.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<%= form_with(model: uploadfile, local: true) do |form| %>
<% if uploadfile.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(uploadfile.errors.count, "error") %> prohibited this uploadfile from being saved:</h2>

<ul>
<% uploadfile.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>

<div class="field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>

<div class="field">
<%= form.label :data %>
<%= form.file_field :data %>
</div>

<div class="actions">
<%= form.submit %>
</div>
<% end %>

動作確認

rails sで起動して localhost:3000/uploadfiles にアクセスします。
以下のようになるはずです。

「New Uploadfile」を開くと、以下のように名前の入力とファイル選択ができるようになります。
「Create Uploadfile」でアップロードします。

「download」をクリックすることでアップロードしたファイルをダウンロードできます。

ファイルのアップロード、ダウンロードができました。


CarrierWave でやってみる

導入

Gemfile に以下を追記する。

Gemfileに追記
1
gem 'carrierwave', '~> 2.0'

bundle installを実行。

モデル作成

ファイルを取り扱うモデルを account として作成します。
以下のコマンドを実行。

1
2
rails generate scaffold account name:string icon:string
rails db:migrate

アップローダーの作成

以下コマンドの実行により、app/uploaders/icon_uploader.rb が作成されます。
app/uploaders/icon_uploader.rb には、IconUploader が定義されています。

1
rails generate uploader icon

アップローダーとモデルの関連付け

app/models/account.rb に、IconUploader を関連付けします。

app/models/account.rb
1
2
3
class Account < ApplicationRecord
mount_uploader :icon, IconUploader
end

ルーティングの編集

後から作る download メソッドを実行するためのルーティングを追加します。

1
2
3
4
5
6
7
Rails.application.routes.draw do
get 'accounts/download/:id',to: "accounts#download",as: "download_icon"
resources :accounts

# 省略

end

コントローラーの編集

dounload メソッドを追加。
set_account メソッドが download を呼び出したときにも実行されるようにします。

1
2
3
4
5
6
7
8
9
10
11
12
class AccountsController < ApplicationController
before_action :set_account, only: [:show, :edit, :update, :destroy,:download]

# 省略

def download
send_data(@account.icon.read,:filename=>@account.icon_identifier)
end

# 省略

end

ビューの編集

app/views/accounts/_form.html.erb は、<%= form.text_field :icon %><%= form.file_field :icon %>に書き換えます。

app/views/accounts/_form.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<%= form_with(model: account, local: true) do |form| %>
<% if account.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(account.errors.count, "error") %> prohibited this account from being saved:</h2>

<ul>
<% account.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>

<div class="field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>

<div class="field">
<%= form.label :icon %>
<%= form.file_field :icon %>
</div>

<div class="actions">
<%= form.submit %>
</div>
<% end %>

app/views/accounts/show.html.erb は<%= @account.icon %><%= image_tag @account.icon.url %>に書き換えます。
<%= link_to 'ダウンロード',download_icon_path(@account) %>を追加します。

app/views/accounts/show.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<p id="notice"><%= notice %></p>

<p>
<strong>Name:</strong>
<%= @account.name %>
</p>

<p>
<strong>Icon:</strong>
<%= image_tag @account.icon.url %>
<%= link_to 'ダウンロード',download_icon_path(@account) %>
</p>

<%= link_to 'Edit', edit_account_path(@account) %> |
<%= link_to 'Back', accounts_path %>


app/views/accounts/index.html.erb は、<%= account.icon %><%= account.icon_identifier %>に書き換えます。

app/views/accounts/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<p id="notice"><%= notice %></p>

<h1>Accounts</h1>

<table>
<thead>
<tr>
<th>Name</th>
<th>Icon</th>
<th colspan="3"></th>
</tr>
</thead>

<tbody>
<% @accounts.each do |account| %>
<tr>
<td><%= account.name %></td>
<td><%= account.icon_identifier %></td>
<td><%= link_to 'Show', account %></td>
<td><%= link_to 'Edit', edit_account_path(account) %></td>
<td><%= link_to 'Destroy', account, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>

<br>

<%= link_to 'New Account', new_account_path %>

動作確認

rails s で起動して localhost:3000/accounts にアクセスします。
以下のようになるはずです。

画像を読み込んで、「Create Account」クリックします。

読み込んだ画像が表示されます。

アップロードしたファイルはどこにあるのか?

アップロードしたファイルは、public/uploads/account/icon/[account の id]の中に保管されていました。
app/uploaders/icon_uploader.rb の中に以下の記述があります。

1
2
3
4
5
6
7
8
9
10
11
class IconUploader < CarrierWave::Uploader::Base

# 省略

storage :file

# 省略

def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end

どうやら、app/uploaders/icon_uploader.rb の記述通りの場所にファイルがあるようです。

試しに"uploads/#{model.class.to_s.underscore}_#{mounted_as}_#{model.id}"と書き換えしました。
public/uploads/accounticon[account の id]以下にファイルが作成されていました。

CarrierWave を使用してファイルのアップロードとダウンロードができました。
CarrierWave を資料を見ながら触りましたが、一瞬でファイルアップローダができるので思わず「すげー」と声が出ました。
CarrierWave を使わずに作ったときと比べて、create 時のコントローラの記述が不要ですからね。びっくりします。

CarrierWave でやってみる。(AWS S3 へアップロード)

CarrierWave はクラウドへのアップロードを備えているので、試してみます。
ここまで作った者を AWS S3 ように書き換えてみます。

導入

Gemfile に以下を追記する。

Gemfileに追記
1
gem "fog-aws"

bundle installを実行。

carrierwave 設定変更

config/initializers/carrierwave.rb を作成して、以下を記述します。

config/initializers/carrierwave.rb
1
2
3
4
5
6
7
8
9
10
11
12
CarrierWave.configure do |config|
config.fog_credentials = {
provider: 'AWS',
aws_access_key_id: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
aws_secret_access_key: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
region: '[リージョン]',
}
config.fog_directory = '[バケット名]'
config.fog_public = false
config.fog_provider = 'fog/aws'
end

app/uploaders/icon_uploader.rb の記述を変更します。
以下のようにstorage :filestorage :fogに書き換えます。

app/uploaders/icon_uploader.rb
1
2
3
4
5
6
7
class IconUploader < CarrierWave::Uploader::Base
#省略

storage :fog

#省略
end

本来は、開発なのか本番なのかで条件分けするようですが、今回は直接記述とします。

動作確認

rails s で起動して localhost:3000/accounts にアクセスし、再度ファイルをアップロードしてみます。
AWS マネジメントコンソールで S3 を開くと、アップロードしたファイルが確認できました。
確かに AWS にアップされています。

ダウンロードすることも問題ありません。
表示しているページを開発者ツールで開いて画像の URL を確認すると以下のようになっていました。

1
https://[バケット名].[リージョン名].amazonaws.com/uploads/account/icon/[accountのid]/[ファイル名]?X-Amz-Expires=600&X-Amz-Date=20191118T155402Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA5W531FYFMAMCKVF2%2F20191119%2Fap-northeast-1%2Fs1%2Faws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=1faa1bc52488c57f41c670801066887bd48d088a06b415db18a0bbf2d5dbccc0

長ーいですね。
試しにこの URL から、ファイル名の後ろのクエリ部分を除くとエラーになりました。
また、URL を保存しておいて大体 10 分後くらいに、直接ブラウザへ入力したら以下のエラーを表示しました。

発行されている URL(とクエリ)は時間限定であることを確認できました。


ActiveStorage でやってみる

導入

以下コマンドを実行。

1
2
rails active_storage:install
rails db:migrate

/config/database.yml を編集し、以下を追記。

1
2
3
local:
service: Disk
root: <%= Rails.root.join("storage") %>

モデル作成

ファイルを取り扱うモデル名を customer として作成します。
以下コマンドを実行します。

1
2
rails generate scaffold customer name:string filename:string
rails db:migrate

ActiveStrage とモデルの関連付け

app/models/customer.rb を編集します。
has_one_attached :photoを記述します。

app/models/customer.rb
1
2
3
class Customer < ApplicationRecord
has_one_attached :photo
end

ルーティングの編集

後から作る download メソッドを実行するためのルーティングを追加します。

1
2
3
4
5
6
7
Rails.application.routes.draw do
get 'customers/download/:id',to: "customers#download",as: "download_photo"
resources :customers

# 省略

end

コントローラーの編集

ActiveStrage では自動で、アップロードしたファイルの名称を補完してくれないようです。
以下のように自前で実装しました。
取得したファイルの名称を filename に保管します。

app/controllers/customers_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class CustomersController < ApplicationController
before_action :set_customer, only: [:show, :edit, :update, :destroy,:download]

# 省略

def create
@customer = Customer.new(customer_params)
@customer.filename=customer_params[:photo].original_filename

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

def update
params={
:name=>customer_params[:name],
:filename=>customer_params[:photo].original_filename,
:photo=>customer_params[:photo],
}

respond_to do |format|
if @customer.update(params)
format.html { redirect_to @customer, notice: 'Customer was successfully updated.' }
format.json { render :show, status: :ok, location: @customer }
else
format.html { render :edit }
format.json { render json: @customer.errors, status: :unprocessable_entity }
end
end
end

# 省略

def download
send_data @customer.photo.download,:filename =>@customer.filename
end

private

# 省略

def customer_params
params.require(:customer).permit(:name,:photo)
end
end

ビューの編集

app/views/customers/_form.html.erb は、filename カラムを利用者に編集させないため、編集項目から削除します。
またいつもと同様に、画像の読み込みとため.file_field を使用します。
変更したものが以下の通りです。

app/views/customers/_form.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<%= form_with(model: customer, local: true) do |form| %>
<% if customer.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(customer.errors.count, "error") %> prohibited this customer from being saved:</h2>

<ul>
<% customer.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>

<div class="field">
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<div class="field">
<%= form.label :photo %>
<%= form.file_field :photo %>
</div>

<div class="actions">
<%= form.submit %>
</div>
<% end %>

app/views/customers/show.html.erb は、画像とダウンロードリンクが表示されるように変更します。

app/views/customers/show.html.erb
1
2
3
4
5
6
7
8
9
10
11
<p id="notice"><%= notice %></p>

<p>
<strong>Name:</strong>
<%= @customer.name %>
<%= image_tag @customer.photo %>
<%= link_to "ダウンロード",download_photo_path(@customer)%>
</p>

<%= link_to 'Edit', edit_customer_path(@customer) %> |
<%= link_to 'Back', customers_path %>

app/views/customers/index.html.erb には特に変更事項ありません。

app/views/customers/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<p id="notice"><%= notice %></p>

<h1>Customers</h1>

<table>
<thead>
<tr>
<th>Name</th>
<th>Filename</th>
<th colspan="3"></th>
</tr>
</thead>

<tbody>
<% @customers.each do |customer| %>
<tr>
<td><%= customer.name %></td>
<td><%= customer.filename %></td>
<td><%= link_to 'Show', customer %></td>
<td><%= link_to 'Edit', edit_customer_path(customer) %></td>
<td><%= link_to 'Destroy', customer, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>

<br>

<%= link_to 'New Customer', new_customer_path %>

動作確認

rails s で起動して localhost:3000/customers にアクセスします。

これまで同様にファイルのアップロードとダウンロードができました。

アップロードしたファイルはどこにあるのか?

/storage 以下に 2 文字のディレクトリが 2 段階分作成され、ハッシュ化されたファイル名で保存されていました。
これだと、どのディレクトリにあるのが、どのモデルの画像なのかわからなさそうです。
修正方法は判然としませんでした。


今回は、Rails でファイルアップロードを「CarrierWave」や「ActiveStorage」でやってみました。
CarrierWave を使ったとき、README に従い操作しただけでアップローダーができたとき、思わず声が出てしまいました。
ActiveStorage も簡単ではあるもののファイル名が変更されたりしてしまうので、DB に直接 blob のカラムで保管するのと比べて優位性が見いだせていません。
このあたり理解が誤っているのであれば、改めたいところです。

ではでは。