アップローダーの実装
Discordの通知を完成させてからはこのブログとはまた別のStrapiを操作するエディタを作り直していました。
Strapiは便利な反面、デプロイが面倒というかデータを匿名で収集しますとかそういうのが気になり始めてもうRailsで作ったほうがいいかという結論に至りました。
今このブログを更新しているRailsは通常のRailsでWebpackerを利用していますが、rails new --api
で始めるつまるところのRails APIで別に作り始めました。
もともとStrapiの箇所はレコードもそこまで多く作っていなかったし、エディタもまだ作りかけの状態だったのでRails APIへの移行は割とすんなりいきました。
StrapiにはJWTの認証と画像のアップローダーなんかが予め用意されていましたが、当然これらは自分で用意していかなければなりません。
といってもRailsのデファクトスタンダード的なgemは当然今でもメンテナンスが続いているのでまだまだこの言語はかつてほどの人気はなくなったかもしれませんが、まだまだ有用だなと思う次第です。
私の考察はどうでもよいとして、RailsにはActive Storageがありますが個人的に出た当初に使ってこれは違うなと感じてからはShrineというライブラリを推しています。 Active Storageは今だとある程度変わってきたのかもしれませんが、それ以上にShrineはいろいろできるという記憶のもとに実装してみました。 概ねREADMEに書いてあるとおり進めていけば問題ないのですが、Rails APIで使っている以上はShrineが提供するView側のヘルパーが使えません。 そうなるとReactとShrineをどうやってつなげるかというのが今回の課題でした:
# config/initializers/shrine.rb
Shrine.plugin :upload_endpoint
まずはupload_endpoint
を有効にしました。
# routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
mount Shrine.upload_endpoint(:cache) => "/images"
end
end
end
続いてroutes.rb
にマウントします。Active StorageだとRailsが提供するURLを使うはずでしたがShrineは自分で決めることができるのでポイント高い。
<input
type="file"
onChange={(e) => {
const [file] = e.target.files;
const formData = new FormData();
formData.append('file', file);
fetch(`/api/v1/images`, {
method: 'POST',
body: formData,
})
}}
/>
あとはこんな感じのコードを書けば200
が返ってくるはずです。簡単ですね。
では何が今回辛かったのかというと、このShrineが返してくれたキャッシュ画像をサーバーに保存するにはどうすればよいのかという部分です。
喉元をすでに過ぎてしまったので解決策を書いちゃいますが、単に保存するname
要素が違っただけでした。
# app/models/blog_post.rb
class BlogPost < ApplicationRecord
include Shrine::Attachment(:image)
end
# app/controllers/api/v1/blog_post_controller.rb
class Api::V1::BlogPostsController < ApplicationController
# Only allow a trusted parameter "white list" through.
def blog_post_params
params.require(:blog_post).permit(:image)
end
end
サーバーに渡す際にparams[:image]
ではなくparams[:image_data]
として送信していたので、cache画像がそのまま保存されてしまったというわけ。
あくまでimage_data
というカラムはShrineが内部に使うもので、利用する側は直接ここにデータを入れないようにしましょうというのが今回の肝でした。
おまけ
https://stackoverflow.com/questions/17240106/what-is-the-best-way-to-convert-all-controller-params-from-camelcase-to-snake-ca
# File: config/initializers/json_param_key_transform.rb
# Transform JSON request param keys from JSON-conventional camelCase to
# Rails-conventional snake_case:
ActionDispatch::Request.parameter_parsers[:json] = lambda { |raw_post|
# Modified from action_dispatch/http/parameters.rb
data = ActiveSupport::JSON.decode(raw_post)
# Transform camelCase param keys to snake_case
if data.is_a?(Array)
data.map { |item| item.deep_transform_keys!(&:underscore) }
else
data.deep_transform_keys!(&:underscore)
end
# Return data
data.is_a?(Hash) ? data : { '_json': data }
}
RailsはRubyで書かれているのでJavaScriptのようなcamelCaseとの相性はあまりよくありません。
クライアント側でサーバーにパラメータを併せるよりも、Rails側がcamelCaseで対応すべきだということに気づきましたが、このStackOverflowの回答は素晴らしいですね。
ExpressでもSQL用にcamelとsnakeの切り分けが必要だったりしますが、Railsの場合はこうやってパッチみたいにファイルをおいておけば自動で変換してくれるので便利です。ただ今回はimage_data
をimageData
に切り替える必要はなかったのでおまけとして残します。