会社案内資料ダウンロード

【Rails+Nuxt】RailsのActive Storage ダイレクトアップロード機能を Nuxt から使う

Rails の Active Storage ダイレクトアップロード機能を Nuxt から使う

今まで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ライブラリを用いて、クライアントからクラウドへのダイレクトアップロードを実装していきます。

Version

Nuxt2.10.0
Ruby2.6.4
Rails6.0.0

前提

Rails、Nuxtが動く状態になっていて、API通信できる状態であること。
RailsにはActiveStorageが導入済みであること、またS3の設定ができていることを前提とします。
本記事ではActiveStorageの導入に関しては説明しません。

S3

今回S3に関する説明は割愛いたします。
S3にはBucketが作成されている。
作成したS3 Bucketへの権限を持ったIAMユーザーを作成している。
状態を前提としています。

BucketのCORS設定を行なっていない場合は、下記のように設定しておきましょう。
(本番環境では適宜、設定を変更してください。)

S3にアクセスし、対象のBucketをクリック。
アクセス許可のタブを選択し、 Cross-Origin Resource Sharing (CORS)の項目へ。

[
     {
         "AllowedHeaders": [
             "*"
         ],
         "AllowedMethods": [
             "GET",
             "POST",
             "PUT"
         ],
         "AllowedOrigins": [
             "*"
         ],
         "ExposeHeaders": []
     }
 ]

Rails側の実装

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_attachedimageを定義しています。

class Cat < ApplicationRecord
   has_one_attached :image
end

APIの作成

ダイレクトアップロードに成功した後に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

CORSの対応

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

Nuxt側の処理

次にフロント側の実装を行なっていきます。
まずはテンプレートのフォーム部分から作成していきます。
デザインは今回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が実行されcatimageattachedされ、アップロード処理が完了です。


この際、cat_image_uploadsへのリクエストパラメータのimageはファイルではなく、BLOBの署名付きIDblob.signed_idを送る点がポイントになります。


signed_idを送ることでデータベースでアップロードされたファイルを関連付けています。

ダイレクトアップロードに失敗した場合は_onUploadErrorが実行されます。

動作確認

では最後に最終的な動作を見てみましょう 👀

アップロードするとプログレスバーが動いて、アップロードに成功するとメッセージが表示されていますね。

RailsのActive Storage ダイレクトアップロード機能を Nuxt から使う
動作確認 / RailsのActive Storage ダイレクトアップロード機能を Nuxt から使う


アップロードした画像を表示できることも確認できました!

S3に画像がアップロードされていることも確認してみます。
アップロードできています 👏

RailsのActive Storage ダイレクトアップロード機能を Nuxt から使う
S3 確認 / RailsのActive Storage ダイレクトアップロード機能を Nuxt から使う

DBはどうなっているでしょう?👀

RailsのActive Storage ダイレクトアップロード機能を Nuxt から使う
DB 確認 / RailsのActive Storage ダイレクトアップロード機能を Nuxt から使う

active_storage_blobsにレコードが生成されています。

RailsのActive Storage ダイレクトアップロード機能を Nuxt から使う
DB 確認 / RailsのActive Storage ダイレクトアップロード機能を Nuxt から使う

active_storage_attachmentsでちゃんと紐づけられています。

まとめ

さてさて!いかがでしたでしょうか?😊

以上、RailsのActive Storageに付属しているJavaScriptライブラリを用いて、Nuxtからクラウドへのダイレクトアップロードの実装でした!