#はじめに
CarrierWaveでファイルアップロード機能を実装していましたが、数メガの写真をまとめてアップロードするとコケることがたびたびあったのでなんとかしたいと思いました。
やりたいこと
ここでやりたいことは以下の二つです。
- アップロード中にプログレスバーを出す
- アップロードする前にファイルを小さくして大きなファイルをアップロードしないようにする
Jquery File Uploadを使うとこの2つができそうだったのでこちらを使うことにしました。
jQuery File Upload RailsというRails用のgemもあるのでそちらを使っても良いかもしれません。
私はこちらの存在を後から知ったのでgemは使っていません。
CarrierWaveの設定方法は解説しているところがたくさんあるのでここでは割愛します。
環境
ruby 2.3.1
rails 5.0.0
carrier_wave 1.0.0beta
基本実装
基本のドキュメント
Jquery File Uploaderのセッティングは本家のWikiにあった以下の記事とデモのソースを参考にして行いましたがうまくいかないところがあったので変更点をまとめます。
Rails setup for V6 | jQuery-File-Upload (Wiki)
デモページ
※デモのソースにはblueimp.github.ioからいくつもjsを引っ張ってきていますが、これらは各jsを本家からダウンロードしてきて使いましょう。
起こった問題 ファイルのアップロードはできているのにEmpty file upload resultと出る
原因:コントローラーでセーブした際のjsonの内容が間違っている
Wikiの記事ではPictureモデルで返すjsonの内容を以下のように設定していました。
def to_jq_upload
{
"name" => read_attribute(:avatar),
"size" => avatar.size,
"url" => avatar.url,
"thumbnail_url" => avatar.thumb.url,
"delete_url" => picture_path(:id => id),
"delete_type" => "DELETE"
}
end
しかし参考サイトによると、このattribute名はどうも古い名前のようで、jsonの内容は以下のような形にしなければならないということでした。
{"files": [
{
"name": "picture1.jpg",
"size": 902604,
"url": "http:\/\/example.org\/files\/picture1.jpg",
"thumbnailUrl": "http:\/\/example.org\/files\/thumbnail\/picture1.jpg",
"deleteUrl": "http:\/\/example.org\/files\/picture1.jpg",
"deleteType": "DELETE"
},
{
"name": "picture2.jpg",
"size": 841946,
"url": "http:\/\/example.org\/files\/picture2.jpg",
"thumbnailUrl": "http:\/\/example.org\/files\/thumbnail\/picture2.jpg",
"deleteUrl": "http:\/\/example.org\/files\/picture2.jpg",
"deleteType": "DELETE"
}
]}
参考
jQuery File Upload “Error - Empty file upload result” - Rails Application | Stack overflow
Using jQuery File Upload (UI version) with a custom server-side upload handler
最新バージョンでのコード
Rails setup for V6の内容から変更する必要がある箇所は以下のとおりです。
他の部分は上記ドキュメントの通りでOKです。
def to_jq_upload
{
"name" => read_attribute(:avatar_name),
"size" => avatar.size,
"url" => avatar.url,
"thumbnailUrl" => avatar.thumb.url, # 変更
"deleteUrl" => picture_path(:id => id), # 変更
"deleteType" => "DELETE" # 変更
}
end
def create
@picture = Picture.new(picture_params)
if @picture.save
json = Hash[files: [@picture.to_jq_upload]].to_json # 変更 旧:[@picture.to_jq_upload].to_json
respond_to do |format|
format.html {
render :json => json,
:content_type => 'text/html',
:layout => false
}
format.json {
render :json => json
}
end
else
render :json => [{:error => "custom_failure"}], :status => 304
end
end
アップロード前にクライアントサイドで写真をリサイズ
基本はこちらのWikiに Client side Image Resizing
オプションはこちら Image Preview & Resize Options
こんな感じにするとアップロード前にリサイズしてくれます。
$(document).on('ready', function() {
'use strict';
// Initialize the jQuery File Upload widget:
$('#fileupload').fileupload({
disableImageResize: /Android(?!.*Chrome)|Opera/
.test(window.navigator && navigator.userAgent),
imageMaxWidth: 720,
imageMaxHeight: 480,
imageCrop: true // Force cropped images
});
});
サムネイルなども作る場合は、この形でJqueryFileUploadが元ファイルからweb閲覧サイズへの縮小をクライアントサイドで行い、アップロード後にCarrierWaveがサムネイルを作るといった流れになります。
nested formで複数の写真をまとめてアップロード
Spotモデルが複数のPictureモデルを子モデルとして持ちます。
だいたいこんな感じに実装しました。
class Spot < ActiveRecord::Base
has_many :pictures, as: :target
accepts_nested_attributes_for :pictures
end
class Picture < ActiveRecord::Base
include Rails.application.routes.url_helpers
belongs_to :target, polymorphic: true
mount_uploader :file, PictureUploader
def to_jq_upload
{
"name" => read_attribute(:file),
"size" => file.size,
"url" => file.url,
"thumbnailUrl" => file.thumb.url,
"deleteUrl" => picture_path(:id => id),
"deleteType" => "DELETE"
}
end
end
class SpotsController < ApplicationController
def update
@spot.attributes = spot_params
if @spot.save
json = Hash[files: [@spot.pictures.last.to_jq_upload]].to_json # ここポイント
respond_to do |format|
format.html {
render :json => json,
:content_type => 'text/html',
:layout => false
}
format.json {
render :json => json
}
end
else
render :json => [{:error => "custom_failure"}], :status => 304
end
end
private
def spot_params
params.require(:spot).permit(pictures_attributes: [:file]) # ここポイント
end
end
複数のファイルを同時にアップロードしますが、実際に挙動としては1ファイルにつき1回updateが呼び出されています。
なのでspot.picturesの中で最後に追加されたものをjsonで渡してあげれば良いということです。
viewの方はデモページのソースからデザイン以外で改変する部分は特にありませんでしたのでそちらを参照してください。
うまくいかなかったところ
解決できなかった問題ですが、何かの参考になるかもしれないので記録として残しておきます。
Deleteボタンが機能しない
Basic Plus UIバージョンで試していましたが、Deleteボタンを押すと以下のようなエラーが出てきました。
Can't verify CSRF token authenticity.
ActionController::InvalidAuthenticityToken
Ajaxでdestroyしようとすると結構面倒なんですよね。
とりあえず以下のことは試してみました。
- button_tagに
remote: true
を追加してみる → 効果なし - form_forに
remote: true, authenticity_token: true
を追加してみる → アップロードもできなくなった
Basic Plus UIを使う場合アップロード前に予めプレビューが出ています。
この状況でアップロード後すぐにAjaxでファイルを削除機能つける必要あるかなあということで、削除前に確認も出ずにサクッと消えてしまうのもどうかなということもあり私の場合はアップロード画面でAjax削除機能はつけないことにしました。
参考
ActionController::InvalidAuthenticityToken when disable JS/Ajax request | Stack overflow
CarrierWaveの複数ファイルアップロードに対応できなかった
CarrierWaveの以前のバージョンではmount_uploader
で1カラム1ファイルの保存しかできませんでしたが、1.0.0ではmount_uploaders
でArray形式のカラムに複数のファイルを保存できるようになりました。
しかし、Arrayカラムへの複数ファイルの保存のやり方がわからず以前からこのArrayへの複数ファイルの保存で別の問題があったので1カラム1ファイルにするよう変更しました。
余談:CarrierWave公式の複数ファイルアップロードで抱えてた問題
一つのカラムに複数のファイルをまとめてあげられるのは楽な面もありますが、以下の2点はかなり大きな問題だったので導入の際にはよく検討したほうがいいのではないかと思います。
- 各ファイルごとにアップロードしたユーザーやキャプションなどの項目をつけるのが困難
- まとめてアップロードして、失敗したファイルがあった場合に失敗したファイルだけど消すことができなかった
以上です