概要
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
- 標題の機能を実現してくれる 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 に下記を追記
//= 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 モデルにマスアサインメントできるようにしている
- POST リクエストにおけるファイルデータのフィールド名
-
allowedTypes
- 許可する拡張子
-
extraHeaders
- ここで書いた項目がPOST 時のリクエストヘッダに追加される
- 上記の例のように
X-CSRF-Token
を付けないと Rails の CSRF チェックに引っかかってサーバーエラーになるので注意! -
form_for
等で作ったフォームなら自動的に hidden フィールドでトークンを付与してくれるが、今回は手動で付与する必要がある - トークンは
meta
タグから取得可能なのでそれを使う
- 上記の例のように
- ここで書いた項目がPOST 時のリクエストヘッダに追加される
その他の設定項目は 公式ドキュメント を参照してください。
ビューの実装
ここまでの作業の確認がてら、ビューを実装します。といっても、先ほど指定した .uploadable
クラスを設定した要素(テキストエリアが無難でしょう)を用意するだけです。
<textarea class="uploadable"></textarea>
試しにドラッグ & ドロップしてみましょう。一瞬だけアップロード中を表す文字列が挿入されたあと、POST 先が未実装なのでサーバーエラーが返ってきて、最終的には何も残らなければ OK です。
POST 先の API の実装
ドラッグ & ドロップでファイルが POST されるようになったので、それを受け取って S3 に保存する処理を行う API が必要です。下記のようにモデルとコントローラーを実装します。
モデル
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 クラス
コントローラー
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 はサーバーエラー時には何もしないので、エラー処理も雑でいいと思う
- 保存成功時には
- 先述の InlineAttachment の設定で
uploadFieldName
を変更したので、ここでasset = Asset.new(asset_params)
というマスアサインメントが実行できている
動作確認
以上で実装は終わりです。ドラッグ & ドロップしてみて、ファイルがアップロードされ、img タグが挿入されれば OK です。