🪣

S3のファイル一覧・ダウンロード機能を雑に作る【Ruby on Rails】

2024/04/27に公開

要約

Ruby on RailsのActive Storageを使わずにaws-sdk-rubyで実装します。
https://railsguides.jp/active_storage_overview.html
https://github.com/aws/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をインストールします。
https://github.com/aws/aws-sdk-ruby

Gemfile
gem 'aws-sdk-s3', '~> 1'

bundle installしたらコードを書いていきます。

initializers配下でクレデンシャルの設定とlib配下でs3 client用のファイルを用意し、起動時に読み込むようにします。

config/initializers/aws.rb
require 'aws-sdk-s3'

AWS.config.update({
  region: 'ap-northeast-1',
  credentials: Aws::Credentials.new(ENV['SECRET_KEY_ID'], ENV['SECRET_ACCESS_KEY']),
})
lib/s3.rb
require 'aws-sdk-s3'

module S3
  BUCKET_NAME = ENV['BUCKET_NAME']
  def self.client
    Aws::S3::Client.new
  end
end

作成したs3.rbを読み込む設定を忘れないようにします。

config/application.rb
...
  config.autoload_paths += %W(#{config.root}/lib)
...

ファイル一覧APIの実装

必要なルーティングとコントローラーを作成します。今回はfilesというcontrollerのindexにファイル一覧APIを、showにファイルダウンロードAPIを実装します。
ファイル一覧APIは、今回作成したバケットにあるファイルの情報のうち、ファイル名と最終更新日時を返却します。
https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#list_objects_v2-instance_method

files_controller.rb
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で返却します。

https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#get_object-instance_method

files_controller.rb
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を使用しました。
https://min.io/

Discussion