💽

AWSのS3へアップロード&DL用の署名付きURLを発行する

2023/04/20に公開

概要

非同期処理でAWSのS3へ大容量のzipファイルをアップロードし、ダウンロード用の署名付きURLを発行する

使用技術

  • Nuxt.js
  • Rails APIモード
  • Heroku
  • Docker

背景

  • 少し前にpdfを複数選択しzipファイルで一括ダウンロードできるような機能を開発した
  • 色々あり非同期処理は使用しておらず、timeoutにならない程度にDL件数に制限をかけて当時は実装
  • Herokuを使用していたが、Herokuの制限で30秒以内にレスポンスを返さなければtimeoutになるという条件があった
  • 仕様変更によりzipファイル生成の処理時間が大幅に伸びtimeoutが頻発してしまいDLが失敗するケースが増加

以上の背景から、RailsのActiveJobを使い非同期処理でzipファイルを生成し、
S3を利用してアップロード&ダウンロードを行おう、となりました。

ゴール

ゴールはざっくりこんな感じです。

  • ActiveJobで非同期処理を行いS3へ大容量ファイルをアップロード
  • S3にアップしたファイルの署名付きURLを生成する

実際にはURL生成してからWebScoketを使用して進捗をクライアントに配信しつつ、
終わったらS3のURLを返すといった処理も実装しましたが今回は割愛します。

実装

zip_download_job.rb
require 'aws-sdk-s3' # AWS S3のSDKを利用するために必要

class ZipDownloadJob < ApplicationJob
  queue_as :default
  
  def perform(ids)  
  # zip化処理開始
  # 自分の場合ではcontrollerからidsの配列を受け取ってpdf生成→zip化する処理をここに書きました
  # zip化処理終了
  
  object_content = binary_data # 上で生成したzipファイルのbinaryデータを格納
  
  current_time = Time.current # ファイル名に使用、ランダムな文字列とかでもOKだと思います
  
  # 環境によって条件分岐させる場合はこんな感じ
  case Rails.env
    when "development"
    # 本記事ではローカルだけ書きます
    # 後で使用するバケット名とかオブジェクトキーを定義 ※各環境ごとにセットしてください
    bucket_name = Rails.application.credentials.aws[:development][:bucket]
    object_key = "zip/test_#{current_time}.zip"
    
    # S3へアップロード
    object = object_uploaded?(s3_client, bucket_name, object_key, object_content)
    # 一応object作られてるか確認
    puts object

    # S3からダウンロード(署名付きURLでダウンロードURL発行)
    signer = Aws::S3::Presigner.new(client: s3_client)
    url = signer.presigned_url(:get_object,
                                bucket: bucket_name, # S3のバケット名
                                key: object_key, # S3のオブジェクトキー
                                expires_in: 600, #urlの有効期限
                                secure: true, # trueにすることでhttpsにできる
                                response_content_type: 'application/force-download') # S3のファイルをブラウザで開くのではなく、押下したらダウンロードさせたい場合
    
    when "production"
    # 本番用の処理
    # ステージングとかあれば適宜case文で分岐させる
    end
  end
  
  private
  def s3_client
    region = Rails.application.credentials.aws[:development][:region]
    access_key_id = Rails.application.credentials.aws[:development][:access_key_id]
    secret_access_key = Rails.application.credentials.aws[:development][:secret_access_key]

    Aws::S3::Client.new(
      region: region,
      access_key_id: access_key_id,
      secret_access_key: secret_access_key
    )
  end
  
  # S3へアップロード AWSのドキュメントほぼそのまま引用
  def object_uploaded?(s3_client, bucket_name, object_key, object_content)
    response = s3_client.put_object(
      bucket: bucket_name, # [String] The name of the bucket
      key: object_key, # [String] The name of the object
      body: object_content # [String] The content to add to the object
    )
    if response.etag
      return true
    else
      return false
    end
  rescue StandardError => e
    puts "Error uploading object: #{e.message}"
    return false
  end
end

最後に

Zenn初投稿です。
まだまだ経験も浅く調べつつ実装したのでもっとスマートなやり方があれば是非コメントください!

Qiitaも投稿していますのでもしよかったら覗いてやってください。
https://qiita.com/miu-P

参考資料

https://docs.aws.amazon.com/ja_jp/sdk-for-ruby/v3/developer-guide/s3-example-create-buckets.html

https://qiita.com/takeyuweb/items/b32dd7487d724faac1fe

https://qiita.com/tmiki/items/87697d3d3d5330c6fc08

Discussion