Rails

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

概要

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

イメージ映像
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/tree/master/dist から下記の 2 ファイルをダウンロード
    • inline-attachment.js
    • jquery.inline-attachment.js
  • vendor/assets 配下に inline_attachment フォルダを作り、その中に 2 ファイルを配置
  • app/assets/javascripts/application.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 等に書くとよいでしょう。

$(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 タグから取得可能なのでそれを使う

その他の設定項目は 公式ドキュメント を参照してください。

ビューの実装

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

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

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

POST 先の API の実装

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

モデル

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 クラス

コントローラー

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 です。

参考