Jquery File Upload + CarrierWaveで爆速ファイルアップロード

#はじめに
CarrierWaveでファイルアップロード機能を実装していましたが、数メガの写真をまとめてアップロードするとコケることがたびたびあったのでなんとかしたいと思いました。

### やりたいこと
ここでやりたいことは以下の二つです。

- アップロード中にプログレスバーを出す
- アップロードする前にファイルを小さくして大きなファイルをアップロードしないようにする

[Jquery File Upload](https://github.com/blueimp/jQuery-File-Upload)を使うとこの２つができそうだったのでこちらを使うことにしました。
[jQuery File Upload Rails](https://github.com/tors/jquery-fileupload-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)](https://github.com/blueimp/jQuery-File-Upload/wiki/Rails-setup-for-V6)
[デモページ](https://blueimp.github.io/jQuery-File-Upload/)

※デモのソースにはblueimp.github.ioからいくつもjsを引っ張ってきていますが、これらは各jsを本家からダウンロードしてきて使いましょう。

----
**起こった問題** ファイルのアップロードはできているのにEmpty file upload resultと出る
**原因**：コントローラーでセーブした際のjsonの内容が間違っている

----


Wikiの記事ではPictureモデルで返すjsonの内容を以下のように設定していました。

```ruby:Picture.rb
  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の内容は以下のような形にしなければならないということでした。


```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](http://stackoverflow.com/questions/31800345/jquery-file-upload-error-empty-file-upload-result-rails-application)
[Using jQuery File Upload (UI version) with a custom server-side upload handler](https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#using-jquery-file-upload-ui-version-with-a-custom-server-side-upload-handler)

## 最新バージョンでのコード
[Rails setup for V6](https://github.com/blueimp/jQuery-File-Upload/wiki/Rails-setup-for-V6)の内容から変更する必要がある箇所は以下のとおりです。
他の部分は上記ドキュメントの通りでOKです。

```ruby:Picture.rb
  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
```

```ruby:pictures_controller.rb
  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](https://github.com/blueimp/jQuery-File-Upload/wiki/Client-side-Image-Resizing) 

オプションはこちら　[Image Preview & Resize Options](https://github.com/blueimp/jQuery-File-Upload/wiki/Options#image-preview--resize-options)

こんな感じにするとアップロード前にリサイズしてくれます。

```main.js
$(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モデルを子モデルとして持ちます。
だいたいこんな感じに実装しました。

```rails:spot.rb
class Spot < ActiveRecord::Base
  has_many :pictures, as: :target
  accepts_nested_attributes_for :pictures
end
```

```rails:picture.rb
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
```

```rails:spots_controller.rb
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
```
複数のファイルを同時にアップロードしますが、実際に挙動としては１ファイルにつき１回updateが呼び出されています。
なのでspot.picturesの中で最後に追加されたものをjsonで渡してあげれば良いということです。

viewの方は[デモページ](https://blueimp.github.io/jQuery-File-Upload/)のソースからデザイン以外で改変する部分は特にありませんでしたのでそちらを参照してください。

# うまくいかなかったところ
解決できなかった問題ですが、何かの参考になるかもしれないので記録として残しておきます。

## 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](http://stackoverflow.com/questions/19452853/actioncontrollerinvalidauthenticitytoken-when-disable-js-ajax-request/24284110)

## CarrierWaveの複数ファイルアップロードに対応できなかった
CarrierWaveの以前のバージョンでは`mount_uploader`で1カラム1ファイルの保存しかできませんでしたが、1.0.0では`mount_uploaders`でArray形式のカラムに複数のファイルを保存できるようになりました。
しかし、Arrayカラムへの複数ファイルの保存のやり方がわからず以前からこのArrayへの複数ファイルの保存で別の問題があったので1カラム1ファイルにするよう変更しました。

**余談：CarrierWave公式の複数ファイルアップロードで抱えてた問題**
一つのカラムに複数のファイルをまとめてあげられるのは楽な面もありますが、以下の２点はかなり大きな問題だったので導入の際にはよく検討したほうがいいのではないかと思います。

1. 各ファイルごとにアップロードしたユーザーやキャプションなどの項目をつけるのが困難
1. まとめてアップロードして、失敗したファイルがあった場合に失敗したファイルだけど消すことができなかった

----
以上です