「テキストエリアにファイルをドラッグ & ドロップすると S3 にアップロードされて markdown の書式で画像が挿入されるやつ」を Rails で実装

# 概要

Qiita や Github でおなじみの、「テキストエリアにファイルをドラッグ & ドロップすると S3 にアップロードされて markdown の書式で画像が挿入されるやつ」を Rails で実装したので、手順をまとめます。

![イメージ映像](https://f.cloud.github.com/assets/21/678/248aac6a-40a2-11e2-9a76-fd59ded28bbe.gif)
（https://github.com/Rovak/InlineAttachment より引用）

# TL;DR

* JS 部分は InlineAttachment を使うと便利
    * オプションの `extraHeaders` で CSRF 対策用のトークンを付与する必要あり
* Rails 側では以下のような API を実装すれば OK
    * POST でファイルを受け取って、
    * それを S3 にアップロードし、
    * JSON を返す
        * `filename` という項目に画像の URL を含める

# 外部ライブラリのインストール

## InlineAttachment

### とは

* https://github.com/Rovak/InlineAttachment
* 標題の機能を実現してくれる JS のライブラリ

### 手順

* https://github.com/Rovak/InlineAttachment/tree/master/dist から下記の 2 ファイルをダウンロード
    * inline-attachment.js
    * jquery.inline-attachment.js
* vendor/assets 配下に inline_attachment フォルダを作り、その中に 2 ファイルを配置
* app/assets/javascripts/application.js に下記を追記

```js
//= require inline-attachment
//= require jquery.inline-attachment
```

## aws-sdk-s3

### とは

* https://github.com/aws/aws-sdk-ruby
* あえて説明することもないが、AWS 公式の SDK
* S3 にアップロードする処理に使う
    * 他に使わないのであれば、`aws-sdk` ではなく `aws-sdk-s3` をインストールする方が依存が少なくて済む

### 手順

* Gemfile に下記を追記（バージョンは適宜修正してください）

```
gem 'aws-sdk-s3', '~> 1.17.0
```

* `bundle install` を実行
* config/initializers/aws-sdk-s3.rb を作り、下記を記載

```
Aws.config.update({
  region: 'ap-northeast-1',
  credentials: Aws::Credentials.new(【キー】, 【シークレット】)
})
```

# InlineAttachment の設定

下記のような JS を書くことで、`.uploadable` クラスが設定された要素にファイルをドラッグ & ドロップすると、/path/to/create に POST リクエストが飛ぶようになります。サイト全体で有効にしたければ、app/assets/javascripts/application.js 等に書くとよいでしょう。

```js
$(function(){
  $('.uploadable').inlineattachment({
    urlText: '<img src="{filename}">',
    uploadUrl: "/path/to/create",
    uploadFieldName: "asset[file]",
    allowedTypes: ['image/jpeg', 'image/png', 'image/jpg', 'image/gif'],
    extraHeaders: {"X-CSRF-Token": $("meta[name=csrf-token]").attr("content")}
  });
});
```

先に言ってしまうと、このあと Rails 側では

* /path/to/create というパスで POST でファイルを受け取って、
* それを S3 にアップロードし、
* JSON を返す

という API を実装することになります。

## 設定項目の解説

* `urlText`
    * アップロード成功後にテキストエリアに挿入する文字列
        * `{filename}` 部分は、アップロード後にサーバー（Rails アプリ）から返した JSON の `filename` の値が埋め込まれる
        * 上記の例では img タグが挿入されるようにしている
* `uploadUrl`
    * ドラッグ & ドロップ後にファイルを POST する URL
* `uploadFieldName`
    * POST リクエストにおけるファイルデータのフィールド名
        * 上記の例では、あとで asset モデルにマスアサインメントできるようにしている
* `allowedTypes`
    * 許可する拡張子
* `extraHeaders`
    * ここで書いた項目がPOST 時のリクエストヘッダに追加される
        * 上記の例のように `X-CSRF-Token` を付けないと Rails の CSRF チェックに引っかかってサーバーエラーになるので注意！
        * `form_for` 等で作ったフォームなら自動的に hidden フィールドでトークンを付与してくれるが、今回は手動で付与する必要がある
        * トークンは `meta` タグから取得可能なのでそれを使う


その他の設定項目は [公式ドキュメント](http://inlineattachment.readthedocs.io/en/latest/pages/configuration.html) を参照してください。

# ビューの実装

ここまでの作業の確認がてら、ビューを実装します。といっても、先ほど指定した `.uploadable` クラスを設定した要素（テキストエリアが無難でしょう）を用意するだけです。

```html
<textarea class="uploadable"></textarea>
```

試しにドラッグ & ドロップしてみましょう。一瞬だけアップロード中を表す文字列が挿入されたあと、POST 先が未実装なのでサーバーエラーが返ってきて、最終的には何も残らなければ OK です。

# POST 先の API の実装

ドラッグ & ドロップでファイルが POST されるようになったので、それを受け取って S3 に保存する処理を行う API が必要です。下記のようにモデルとコントローラーを実装します。

## モデル

```ruby:app/models/asset.rb
class Asset
  include ActiveModel::Model

  attr_accessor :file
  attr_reader   :url

  BucketName = 'your.bucket.name'
  BasePath   = 'assets/'

  def save
    # 重複を避けるためにタイムスタンプを使う
    filename = Time.zone.now.strftime('%Y%m%d%H%M%S%6N') + File.extname(@file.original_filename)
    obj = s3.bucket(BucketName).object(BasePath + filename)
    obj.upload_file(@file.tempfile)

    # なぜか http の URL が返ってくるので手動で置換する
    # see: https://github.com/aws/aws-sdk-ruby/issues/1389
    @url = obj.public_url(virtual_host: true).gsub(/^http:/, 'https:')
  end

  private
  def s3
    @s3 ||= Aws::S3::Resource.new
  end
end
```

### ポイント

* アップロードさえできればよいので、ActiveRecord を継承しないモデルを実装した
* POST されてくるファイルは [ActionDispatch::Http::UploadedFile](https://api.rubyonrails.org/classes/ActionDispatch/Http/UploadedFile.html) クラス

## コントローラー

```ruby:app/controllers/assets_controller.rb
class AssetsController < ApplicationController
  def create
    asset = Asset.new(asset_params)

    unless [
      'image/png',
      'image/gif',
      'image/jpeg',
      'image/tiff',
    ].include?(asset.file.content_type)
      render json: { error: "file type (#{asset.file.content_type}) is not allowed" }, status: 500 and return
    end

    asset.save
    render json: { filename: asset.url }
  end

  private

  def asset_params
    params.require(:asset).permit(:file)
  end
end
```

### ポイント

* 最終的に JSON を返す
    * 保存成功時には `filename` という項目を含める
        * これが InlineAttachment の `urlText` に使われる
    * 保存失敗時はなんでもよい
        * 先述のとおり InlineAttachment はサーバーエラー時には何もしないので、エラー処理も雑でいいと思う
* 先述の InlineAttachment の設定で `uploadFieldName` を変更したので、ここで `asset = Asset.new(asset_params)` というマスアサインメントが実行できている

# 動作確認

以上で実装は終わりです。ドラッグ & ドロップしてみて、ファイルがアップロードされ、img タグが挿入されれば OK です。

# 参考

* http://inlineattachment.readthedocs.io/en/latest/pages/configuration.html
* https://easyramble.com/about-asset-pipeline.html
* https://stackoverflow.com/q/7203304