画像のアップロード
画像のアップロードモジュールは手法が様々存在しどのような手法を採用するか悩ましい。
最もオーソドックスな手法はブラウザーからサーバにアップロードしホスティングする手法だろう。
最近では画像は容量もあるので、サーバーではなく専用のストレージにアップロードすることが多くAWSならばS3、GCPならばGoogle Cloud Storageにホスティングすることが多い。
さらに、サーバーを介してストレージにアップロードする方法から、一時的にアクセスキーをクライアントに渡し、直接アップロードする方法など多義にわたる。
ここでは、React + Rails + Carrierwave + S3の編成で画像のアップロード機構を組んでいく。
最初は仕組みの概要が把握しやすいように、Railsサーバに直接画像をアップロードする仕組みを実装する。 それが、完成したら今度はS3にアップロードする仕組みを開発する。
Carrierwaveの導入
CarrierwaveはRailsの画像アップロードを可能にするGemである。画像をアップロードできるだけでなく、整形やサーバー以外へアップロードにも対応している。
bundle add carrierwave
アップローダーを作成する。アップローダーとはユーザーから受け取った画像ファイルをフィルターしたり、整形したり、目的の場所にアップロードしたりするクラスである。
アップローダーは画像のアップロードに関する設定を宣言する形で記述する。なので、目的別に作り分けることが多い。 コンテンツのサムネイル用のアップローダーと、ユーザーのプロフィール画像のアップローダーは別々に作る場合がある。
ここでは、基本となるbasic_uploader.rbを作る。
bin/rails g uploader Basic
ここでひとまずコミット
git add .
git commit -m "Install carrierwave and create basic uploader"
Carrierwaveの設定
最初に行うのはデフォルト画像の設定である。 デフォルト画像とは、例えばコンテンツが作られたとしても画像が設定されていない場合、代わりに参照する画像のことである。
もちろん、クライアントの実装上、ユーザーが設定していない場合は画像を表示しないなどの方法もあるが、代わりにブランクな画像を表示する時にこの設定は便利である。
具体的にはPostモデルにimage_urlカラムがあったとして、デフォルト画像を設定しておけば、画像をアップロードしなくてもこのようなjsonが返ってくる。
{
"id": 5,
"user_id": 1,
"title": "Akira",
"content": "Movie",
"image_url": "http://locahost:3000/images/default-image.png"
"created_at": "2022-06-30T04:08:18Z",
"updated_at": "2022-06-30T04:08:18Z"
}
app/uploaders/basic_uploader.rbを開いてdefault_url関数をこのように書き換える。
def default_url
ActionController::Base.helpers.asset_path("#{model.class.to_s.underscore}-default.png")
end
この画像をpublicフォルダにpost-default.pngという名前で配置する。
Postモデルにbasic uploaderをマウントするとデフォルトでは上の画像が参照される。
アップロードできる画像の種類をフィルタする。extension_whitelist関数。をコメントインして種類を宣言する。
def extension_allowlist
%w(jpg jpeg gif png)
end
最後に、画像のURLを相対パスではなく絶対パスにする設定を行う。
特に設定を行わないと、このようなjsonが戻ってきて、クライアント側でURLを補わなくてはならない。
"thumbnail": {
"url": "/post-default.png"
}
config/environments/development.rbを開いて、Rails.application.configure doの中に以下を加える。
config.asset_host = 'http://localhost:3001'
こうすると、こんな感じに返ってくる。 本番環境での設定とS3に上げた場合の設定はまた別にし直す。
"thumbnail": {
"url": "http://localhost:3001/post-default.png"
}
画像をアップロードした時の絶対パスの設定は別に行う必要があり、basic_uploaderに下のコードを加える。
def asset_host
return "http://localhost:3001"
end
Carrierwaveを使ってサーバーに画像をアップロードする設定が完了したのでコミット。
git add .
git commit -m "Setup basic uploader"
Postモデルにマウント
作成したbasic_uploaderをPostモデルにマウントする。
Postモデルには画像情報を収めるカラムがないのでthumbnailというカラムを追加する。
bin/rails g migration add_thumbnail_to_posts thumbnail:string
データベースに反映させる。
bin/rails db:migrate
app/models/post.rbを開いて、basic_uploaderをマウントする。対象となるカラムは今追加したthumbnailにする。
mount_uploader :thumbnail, BasicUploader
次に、posts_controllerでthumbnailとremove_thumbnailを許可する。
thumbnailにはクライアントから送られてくる画像ファイル情報が入ってくる。
remove_thumbnailはupdateアクションで利用され、carrierwaveが処理するフラグになり、もしもこれがtrueの場合は画像を削除できる。
def post_params
params.require(:post).permit(:user_id, :title, :content, :thumbnail, :remove_thumbnail)
end
Carrierwave経由でアップロードされた画像はpublic/uploads/いかに保存される。
将来的にはS3にアップロードするのでここは関係なくなるが、このモジュールではひとまずここにアップロードするので、gitignoreしておく。
/public/uploads
git add .
git commit -m "Create a uploading thumbnail of post"
画像アップロードフロントエンドの作成
画像APIサーバはほぼ完成したので今度はNext.js側を編集しPostの画像をアップロードできるようにする。
pages/posts/new.jsx から編集する。
onSubmit関数でフォームのデータをjsonに収めるのではなく、FormDataに収めるように修正する。
こうしないと、ファイルデータを正常にサーバに送れない。
const onSubmit = async (data) => {
if (!isSignedIn) {
createNotification('投稿にはログインが必要です')
return
}
const params = new FormData()
if (data.thumbnail.length > 0) {
params.append('post[thumbnail]', data.thumbnail[0])
}
params.append('post[title]', data.title)
params.append('post[content]', data.content)
console.log(params)
setAnime(true)
try {
const res = await createPost(params)
console.log(res)
if (res.status === 201) {
console.log('Signed in successfully!')
router.push('/')
} else {
console.log('some thig went wrong')
createNotification('予期しない問題が発生しました')
}
} catch (err) {
console.log(err)
createNotification(err.response.data[0])
}
setAnime(false)
}
続いて、DOMを編集する。タイトルの下にサムネイルのフィールドを追加する。 サムネイルは必須項目ではないので、react-hook-formのregisterにrequiredは加えない。
<Text fontSize={14} lineHeight="1.71" letterSpacing={0.7} color="dimgray" mt={30}>
サムネイル
</Text>
<Box mt={8}>
<input type="file" {...register('thumbnail')} name="thumbnail" accept="image/*" />
</Box>
![]()
次にpages/posts/[id].jsxでサムネイルが表示されるようにする。
Imageアトムコンポーネントを読み込み、JSX DOMのpost?.titleの下にサムネイルを表示する。
import { Text, Box, Link, Svg, Clickable, Image } from '../../atomic/'
...省略...
return (
...省略...
<Text fontSize={14} lineHeight="1.71" letterSpacing={0.7} color="dimgray" mt={30}>
サムネイル
</Text>
<Image src={post?.thumbnail?.url} width="100%" />
)
![]()
pages/posts/[id]/edit.jsxでサムネイルの削除と変更ができるようにする。
最初にImageアトムコンポーネントをインポートする。
import { Text, Box, Clickable, Link, TextField, TextArea, ThreeDots, Alert, Image } from '../../../atomic/'
onSubmitはnewの時と基本的には同じだが、thumbnailがあるかどうかは判定してFormDataオブジェクトに収める。
removeThumbnailが加わっている。
const onSubmit = async (data) => {
if (!isSignedIn) {
createNotification('投稿にはログインが必要です')
return
}
const params = new FormData()
if (data.thumbnail.length > 0) {
params.append('post[thumbnail]', data.thumbnail[0])
}
params.append('post[removeThumbnail]', data.removeThumbnail)
params.append('post[title]', data.title)
params.append('post[content]', data.content)
console.log(params)
setAnime(true)
try {
const res = await updatePost(id, params)
console.log(res)
if (res.status === 200) {
setPost(res?.data)
createNotification('更新しました')
console.log('Signed in successfully!')
} else {
console.log('some thig went wrong')
createNotification('予期しない問題が発生しました')
}
} catch (err) {
console.log(err)
createNotification(err.response.data[0])
}
setAnime(false)
}
DOMではpost?.titleの下に画像の表示、および変更、削除のフォームを加える。
removeThumbnailにチェックを入れて、フォームを送信すると画像が削除される。
return(
...省略...
<Text fontSize={14} lineHeight="1.71" letterSpacing={0.7} color="dimgray" mt={30}>
サムネイル
</Text>
<Box mt={8}>
<Image src={post?.thumbnail?.url} width="100%" />
<Box as="label" display="flex" alignItems="center" mt={8}>
<input type="checkbox" {...register('removeThumbnail')} />
サムネイル画像を削除
</Box>
<Box mt={8}>
<input type="file" {...register('thumbnail')} name="thumbnail" accept="image/*" />
</Box>
</Box>
...省略...
)
![]()
git add .
git commit -m "Develop post thumbnail form"
S3制御IAMの取得
前の章ではRailsサーバに画像をアップロードして表示を行ってきた。 webサービスでは画像をはじめとするメディアファイルはS3にあげるのがセオリーである。 理由はいくつかある。1つ目はAWSのEC2サーバーは容量に対する課金が厳しく、ユーザーが画像をアップロードするとそれに比例して、ランニングコストが跳ね上がってしまう。 2つ目はファイルの保存性能である。サーバーはいつどんな気に起動しなくなったり、アクセスできなくなるかわからない。 ライブラリをアップデートしたら動かなくなったり、容量が一般になって起動しなくなったりする。そういった時に画像がサーバーに残っていると取り出すのが大変である。 そこで、データベースもそうだが、基本的にサーバは処理するプログラムだけ残しておき、メディアファイルやデータベースは別に分離するのが賢い。
ここからは、ローカルのRailsサーバにアップロードしていた画像をS3にアップロードする方法を紹介していく。
最初はRailsからS3の制御ができるようするためのIAMの取得である。
AWSコンソール画面を開き、IAMの画面に移動する。

以前作成した、ses-userのIAMがある。
今更なのだが、命名規則が間違っていた。
ses-とつけてしまうと、それだけを担うユーザーのような印象をうける。
このユーザーには各プロジェクトでのAWSサービスを利用する権限を付け加えていく。
今回なら、S3への制御が可能な権限など。
今更、名前を変えるのも面倒なので、このses-user にS3の権限を加える。
ユーザー名をクリックする。

「アクセス権限を追加」ボタンをおす。
「既存のポリシーを直接アタッチ 」を選び、検索フォームに「S3」と打ち込む。候補の中の「AmazonS3FullAccess 」を選ぶ。 チェックを入れたら、次のステップ確認をクリック。次のページで「アクセス権限を追加」 をクリック。

注意事項として、この権限はs3の全ての操作の権限が与えられるので、現実的にはもう少し強い規制がかかったポリシーに変更した方が良い。 具体的な記述の仕方は現在調べ中。

前回作成したSES用のIAMのアクセスキーとシクレットキーで同様にS3も操作できるようになった。
S3バケットの作成
Railsから画像をアップロードするバッケットを作成する。
AWSコンソール画面でS3のページに移動する。
左のメニューの「バケット」から「バケットを作成」を選ぶ。

バケットの名前は好きなものにする。webサービスの名前が決まっているならそれが良い。

「オブジェクト所有者」を「ACL 有効」にする。 これは、その説明にも書いてあるように、無効にしていると、今現在awsコンソールにアクセスしているアカウントのみが S3の管理権限を持つようになる。今回の場合IAMを取得したRailsからも管理できるようにしたいので、「ACL 有効」にしておく。

次に「このバケットのブロックパブリックアクセス設定」の「パブリックアクセスをすべてブロック」のチェックを外し、 下2つのチェックのみにする。
「現在の設定により、このバケットとバケット内のオブジェクトが公開される可能性があることを承認します。」にチェックを入れる。

アクセス権限の内容は読んでも意味がわからない 一応参考までにこんな記事もある。
S3のブロックパブリックアクセスが怖くなくなったAWS S3
そのほかの項目はデフォルトで良い。 多くの項目は後から変更できるし、S3のデフォルトの設定は最も厳しいものになっているから安全である。
一番下までスクロールして、「バケットを作成」ボタンを押す。

fog-aws
fog-awsはCarrierwaveを拡張してS3にも画像をアップロードできるGemである。 fog-awsをさらにラッパーしてawsだろうが、gcpだろうがazureだろうがアップロードできるようにしたのがfogというものだ。 少し前のrailsでは便利さ故にfogが使われていた。最近になり、利用しないプロバイダーも含んだfogは無駄が多いということで、fog-awsを利用するようになった。
まずはインストール。
bundle add fog-aws
app/uploaders/basic_uploader.rbを開いて、storageをfileからfogにする。
# Choose what kind of storage to use for this uploader:
# storage :file
storage :fog
現状の設定では開発環境でも、本番環境でも同じS3のロケーションにファイルをアップロードするので、 下手をすると、開発環境でアップロードした画像が本番環境の画像を置き換える可能性がある。
対処方法は2つある。
1つはこのように、ローカルではS3にアップロードせずにRailsサーバーにアップロードする。 多くのサイトではこのような設定が推奨されている。というのも、ローカルの開発環境で毎度画像をs3にアップロードするのは無駄だからだ。 なので、将来的にはこちらの設定にしていくと思われる。
if Rails.env.development?
storage :file
elsif Rails.env.test?
storage :file
else
storage :fog
end
今現在は、ちゃんとS3にローカルの開発環境からアップロードできるかを確認したいので、 store_dir関数で保存先を環境によって書き換えることで対処する。
def store_dir
if Rails.env.development?
"uploads/dev/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
else
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
end
app/uploaders/basic_uploader.rbのstore_dirを上のコードのように書き換えておく。
今はbasic_uploaderのasset_host関数でhttp://localhost:3001/uploads/posts/34/hoge.pngみたいな完全なURLを返すようになっている。
s3にアップロードする場合はfog-awsが代わりにこの設定をしてくれているので、ここはコメントアウトする。
# def asset_host
# return "http://localhost:3001"
# end
次に、carrierwaveの設定ファイルを設置する。
touch config/initializers/carrierwave.rb
中身をこのようにする。
CarrierWave.configure do |config|
config.fog_credentials = {
provider: 'AWS',
aws_access_key_id: Rails.application.credentials.aws[:access_key_id],
aws_secret_access_key: Rails.application.credentials.aws[:secret_access_key],
region: 'ap-northeast-1'
}
config.fog_directory = '自分が作成したバケット名'
end
設定が完了したので、ブラウザからアップロードしてみる。


問題なく動作したので、コミット。
git add .
git commit -m "Upload images for S3"
ちなみに、postの画像は編集画面で画像を削除したり、postを削除すると自動的にS3からも削除される。