S3のファイル一覧・ダウンロード機能を雑に作る【Ruby on Rails】
要約
Ruby on RailsのActive Storageを使わずにaws-sdk-rubyで実装します。
Active Storage
Ruby on Railsの機能で、ファイルのアップロード・レコード(Active Recordオブジェクト)に紐付けた保存をしてくれるものです。
Amazon S3やGoogle Cloud Storageに対応しているほか、開発環境向けに保存先としてローカルディスクを指定することもできます。
今回やりたかったこと
ユーザーはファイル(主にPDF)をアップロードせず、一方的にファイルを配信する
→ファイルの一覧が見れてそこからダウンロードができれば良い
新たなファイルを配信する頻度は低い
→アップロード機能はRailsアプリ上に作らなくても良い
方針
今回の範囲では、Active Storageを使用せず以下の構成でも十分そうです。
- ファイルの配置場所:Amazon S3
- ファイル一覧:S3のListObjectsでバケット内のオブジェクト一覧を取得
- ファイルダウンロード:S3のオブジェクトキーをリクエストで受け取りGetObjectで内容を取得
- ファイルアップロード:依頼されたファイルを開発者がS3バケット配置する
インフラ→バックエンド→フロントの順に実装していきます。
S3バケットの用意
適当な名前でS3バケットを作成します。
他のリソースをTerraformで管理しているため、今回はTerraformで用意しました。
resource "aws_s3_bucket" "example" {
bucket = "bucket-for-uploading-files"
}
また、Railsアプリの動いているEC2やECSなどにS3にアクセスする権限がなければ設定します。
今回必要なのは、対象バケットへの権限(s3:ListBucketとs3::GetObject)です。
RailsでS3を使用する準備
RcontrollerからAWSのclientを呼び出せるよう、gemのインストールとclientの設定を追加します。
今回はS3だけ使用できれば良いので、S3用のgemをインストールします。
gem 'aws-sdk-s3', '~> 1'
bundle install
したらコードを書いていきます。
initializers配下でクレデンシャルの設定とlib配下でs3 client用のファイルを用意し、起動時に読み込むようにします。
require 'aws-sdk-s3'
AWS.config.update({
region: 'ap-northeast-1',
credentials: Aws::Credentials.new(ENV['SECRET_KEY_ID'], ENV['SECRET_ACCESS_KEY']),
})
require 'aws-sdk-s3'
module S3
BUCKET_NAME = ENV['BUCKET_NAME']
def self.client
Aws::S3::Client.new
end
end
作成したs3.rbを読み込む設定を忘れないようにします。
...
config.autoload_paths += %W(#{config.root}/lib)
...
ファイル一覧APIの実装
必要なルーティングとコントローラーを作成します。今回はfilesというcontrollerのindexにファイル一覧APIを、showにファイルダウンロードAPIを実装します。
ファイル一覧APIは、今回作成したバケットにあるファイルの情報のうち、ファイル名と最終更新日時を返却します。
def index
client = S3::client
files = client.list_objects_v2(bucket: S3::BUCKET_NAME).contents
res = files.map do |file|
{
file_name: file.key,
last_modified: file.last_modified,
}
end
render json: res
end
ファイルダウンロードAPIの実装
ファイルダウンロードAPIは、オブジェクトキー(実質ファイル名)をパラメータに、ファイルの中身をBase64で返却します。
def show
object_key = params[:key]
unless object_key.present?
return status: 400, json: { message: 'keyは必須パラメータです' }
end
client = S3::client
begin
object = client.get_object(key: S3::BUCKET_NAME, key: object_key)
rescue Aws::S3::Errors::NoSuchKey
render status: 400, json: { message: '該当のファイルが存在しません' }
end
file = OpenStruct.new(
name: object_key,
data: Base64.strict_encode64(object.body.read),
)
res =
{
file_name: file.name
data: file.data
}
render json: res
end
フロント
フロントはRuby on RailsでなくReactで書いている、かつ今回の話題からは若干逸れるため、方針を軽く書いておきます。
- ファイル一覧画面でファイル一覧APIにリクエストして表示する
- リストに「表示」などボタンを用意して、クリックするとダウンロードAPIにリクエストする
- 受け取ったファイルのBase64形式のデータを別タブで表示する
備考
ダウンロードAPIとしては、Base64ではなくファイルデータをそのまま返却する方法もありそうですが、今回はBase64で渡してフロントの別タブで開かせることにしました。
デメリットとしては、開いたタブとファイルの名前をこちらで指定できず、その場で生成されたUUIDになってしまうようです。
ローカルでの開発にはS3を使用せず、S3互換のAPIを提供するMinIOを使用しました。
Discussion