今までRailsでファイルをアップロードする場合、加工処理を行いたいためにRails側でファイルを受け取るような実装が主流でした。
ファイルアップロード → Rails → ファイルの加工 → S3へアップロード
しかし、現在は画像変換サービスを利用したり、自前でリサイズ サーバを運用したり、S3にアップロードされたらAWS Lambda側で加工処理を実行できたりと選択技が増えています。
そうすることで、Railsを通すことなくS3へアップロードできるので、今まで悩まされていたRailsサーバの画像加工処理の負荷とおさらばできます✨
また、Herokuを使用している場合、Herokuのリクエストタイムアウトは30秒なので大きいファイルをアップロードする際にタイムアウトしてしまうのを防ぐことができます。
今回弊社のリードエンジニアが、RailsのActive Storage を使用しPDFの一括ダイレクトアップロード機能を Nuxt で実装していたのですが、Active Storage のダイレクトアップロード機能を JavaScript フレームワークから利用する記事が思ったより少ない(ダイレクトアップロードを履き違えた記事まであった)といった状況でした。
なので、後学のために実装内容を確認し記事にしていこうと思います🙌
もくじ
S3へファイルをアップロードする際に、RailsのActive Storageに付属しているJavaScriptライブラリを用いて、クライアントからクラウドへのダイレクトアップロードを実装していきます。
Nuxt2.10.0
Ruby2.6.4
Rails6.0.0
Rails、Nuxtが動く状態になっていて、API通信できる状態であること。
RailsにはActiveStorageが導入済みであること、またS3の設定ができていることを前提とします。
本記事ではActiveStorageの導入に関しては説明しません。
今回S3に関する説明は割愛いたします。
S3にはBucketが作成されている。
作成したS3 Bucketへの権限を持ったIAMユーザーを作成している。
状態を前提としています。
BucketのCORS設定を行なっていない場合は、下記のように設定しておきましょう。
(本番環境では適宜、設定を変更してください。)
S3にアクセスし、対象のBucketをクリック。
アクセス許可のタブを選択し、 Cross-Origin Resource Sharing (CORS)
の項目へ。
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"POST",
"PUT"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
Railsでは、ActiveStorageが導入済みであること、またS3の設定ができていることを前提とします。
まず、今回はサンプルのためにCatモデルを作成しました。
下記のようにname
カラムを追加しています。
# == Schema Information
#
# Table name: cats
#
# id :bigint not null, primary key
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
今回は1レコードにつき、1つの添付ファイルをアップロードしますので、モデルにはhas_one_attached
でimage
を定義しています。
class Cat < ApplicationRecord
has_one_attached :image
end
ダイレクトアップロードに成功した後にimage
を更新するためのAPIを作成します。
今回はファイル名とCat.name
が一致するものを更新するようにしておきます。
module Api
module V1
class CatImageUploadsController < ::Api::ApplicationController
def create
cat = Cat.find_by!(name: name)
cat.attributes = controller_params
if cat.save
render json: { status: 200 }
else
render json: cat.errors, status: 422
end
end
private
def name
File.basename(filename, ".*")
end
def filename
params[:filename]
end
def controller_params
# imageはファイルではなくblob.signed_idが送られてきます
params.permit(:image)
end
end
end
end
Active Storageのダイレクトアップロード機能をNuxtから利用していくわけですが、そのまま使用するとInvalidAuthenticityToken
エラーになります。
direct uploadを別ドメインのajaxから利用できるようにするため、モンキーパッチを当てます。config/initializers/direct_uploads.rb
を作成し、下記のように設定します。
これはカスタムコントローラーを作成してもいいと思うので、お好みでご対応ください。
require 'active_storage/direct_uploads_controller'
class ActiveStorage::DirectUploadsController
protect_from_forgery with: :null_session
skip_before_action :verify_authenticity_token
end
次にフロント側の実装を行なっていきます。
まずはテンプレートのフォーム部分から作成していきます。
デザインは今回Buefy
を使用しています。
ファイルアップロードのためのinput
要素と、アップロードの進捗と結果を表示するためのリスト表示部分を下記のように作成します。
<template>
<div class="box">
<input
id="image-files"
ref="image_files"
type="file"
multiple
accept="image/*"
@change="onChangeFiles"
>
<table>
<thead>
<tr>
<th>ファイル名</th>
<th>進捗</th>
<th>メッセージ</th>
</tr>
</thead>
<tbody>
<tr v-for="(file, idx) in files" :key="idx">
<td>{{ file.name }}</td>
<td>
<b-progress type="is-success" :value="file.progress" show-value format="percent" />
</td>
<td>{{ file.message }}</td>
</tr>
</tbody>
</table>
</div>
</template>
続いて、dataオプションやメソッド部分、ダイレクトアップロードの処理を実装していきます。
Active Storageのダイレクトアップロード機能をJavaScriptフレームワークから利用したい場合は、DirectUpload
クラスを利用することができます。DirectUpload
をインスタンス化してそのインスタンスのcreate
メソッドを呼び出して使用します。
というわけで、rails/activestorage
をインストールします。
yarn add @rails/activestorage
OR
npm i @rails/activestorage
インストールができたら、次にDirectUploader
を作成していきます。
import { DirectUpload } from "@rails/activestorage"
class DirectUploader {
constructor(file, url, onProgressCallback) {
this.file = file
this.url = url
this.onProgressCallback = onProgressCallback
this.directUpload = new DirectUpload(this.file, this.url, this)
}
upload(onErrorCallBack, onSuccessCallback) {
// createメソッドを実行
this.directUpload.create((error, blob) => {
if (error) {
// エラー時の処理
onErrorCallBack(error, this)
} else {
// 成功時の処理
onSuccessCallback(blob, this)
}
})
}
directUploadWillStoreFileWithXHR(request) {
request.upload.addEventListener("progress", event => this.directUploadDidProgress(event))
}
directUploadDidProgress(event) {
this.onProgressCallback(event, this)
}
}
export default DirectUploader
今回はファイルアップロードの進行状況をトラッキングしたいため、DirectUpload
コンストラクタに3番目のパラメータを渡しています。
DirectUpload
はアップロード中にオブジェクトのdirectUploadWillStoreFileWithXHR
メソッドを呼び出すので、以後XHRの独自のプログレスハンドラをバインドできるようになります。
では次に、作成したDirectUploader
を呼び出して使用していきましょう。
<script>
import DirectUploader from '@/lib/DirectUploader'
export default {
data () {
return {
files: []
}
},
methods: {
onChangeFiles (_event) {
const input = this.$refs.image_files
this.uploadFiles(input.files)
// 選択されたファイルを入力からクリアしておく
input.value = null
},
uploadFiles (files) {
this.files = []
Array.from(files).forEach(file => this.uploadFile(file))
},
uploadFile (file) {
// アップロードを実行
const filename = file.name
const tmpUploadFile = { name: filename, fileSize: file.size, progress: 0, isUploading: true }
this.files.push(tmpUploadFile)
const url = 'http://localhost:3000/rails/active_storage/direct_uploads'
const directUploader = new DirectUploader(file, url, this._onUploadProgress.bind(this))
directUploader.upload(this._onUploadError.bind(this), this._onUploadSuccess.bind(this))
},
_onUploadError (error, directUploader) {
// アップロード失敗時の処理
const params = { isUploading: false, message: error }
const filename = directUploader.file.name
this._updateFiles(filename, params)
},
async _onUploadSuccess (blob, directUploader) {
// アップロード成功時の処理
const filename = directUploader.file.name
const params = { isUploading: false, message: 'アップロード完了' }
const postParams = { filename: blob.filename, image: blob.signed_id }
const url = 'http://localhost:3000/api/v1/cat_image_uploads'
try {
await this.$axios.$post(url, postParams)
this._updateFiles(filename, params)
} catch (error) {
const params = { isUploading: false, message: error }
this._updateFiles(filename, params)
}
},
_onUploadProgress (event, directUploader) {
// アップロード中の処理
const progress = this._calcProgress(event)
const params = { progress }
const filename = directUploader.file.name
this._updateFiles(filename, params)
},
_updateFiles (filename, params) {
this.files = this.files.map((file) => {
if (file.name !== filename) { return file }
return Object.assign(file, params)
})
},
_calcProgress (event) {
return (event.loaded / event.total) * 100
}
}
}
</script>
以上の処理でダイレクトアップロードは完成ですが、少し解説を。
まず、ファイルをアップロードするとonChangeFiles
メソッドが実行されます。
アップロード処理と、選択されたファイルを入力からクリアしておく処理が行われます。
アップロードはuploadFiles
により複数のファイルを一括でアップロードした場合でも対応できるようにしています。
続いてuploadFile
でアップロード処理が実行されます。
ここでDirectUploader
を使用してアップロードを行なっています。
DirectUpload
ではどのような処理になっているのでしょうか?見ていきましょう。
DirectUpload
をインスタンス化してそのインスタンスのcreate
メソッドが呼び出されると
まず/rails/active_storage/direct_uploads
にPOSTされます。
するとRails側でBlobレコードが作成されます。
そして、BlobレコードからAWSのSDKを使い、Pre-signed URL
を作成しNuxt側に返します。
Nuxt側でPre-signed URLを取得したら、そのURLを使ってS3にダイレクトアップロードが行われます。
DirectUpload
はアップロード中にdirectUploadWillStoreFileWithXHR
メソッドを呼び出します。
今回DirectUploader
をインスタンス化する際に_onUploadProgress
を渡しているので、アップロード中は_onUploadProgress
が呼び出されアップロードの進捗状況が更新されていきます。
ダイレクトアップロードが成功したら_onUploadSuccess
が実行されcat
にimage
がattached
され、アップロード処理が完了です。
この際、cat_image_uploads
へのリクエストパラメータのimage
はファイルではなく、BLOBの署名付きIDblob.signed_id
を送る点がポイントになります。
signed_id
を送ることでデータベースでアップロードされたファイルを関連付けています。
ダイレクトアップロードに失敗した場合は_onUploadError
が実行されます。
では最後に最終的な動作を見てみましょう 👀
アップロードするとプログレスバーが動いて、アップロードに成功するとメッセージが表示されていますね。
アップロードした画像を表示できることも確認できました!
S3に画像がアップロードされていることも確認してみます。
アップロードできています 👏
DBはどうなっているでしょう?👀
active_storage_blobs
にレコードが生成されています。
active_storage_attachments
でちゃんと紐づけられています。
さてさて!いかがでしたでしょうか?😊
以上、RailsのActive Storageに付属しているJavaScriptライブラリを用いて、Nuxtからクラウドへのダイレクトアップロードの実装でした!