LoginSignup
20
17

More than 5 years have passed since last update.

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

Posted at

概要

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

参考

20
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
17