Railsで作成したCSVをS3に出力する方法

先日Railsで作成したCSVをS3にアップロードする機能を実装しました。
思っていたよりも考慮することが多かったので、内容をまとめておこうと思います。

# 環境
Rails 5.2.3
Ruby 2.6.3

# 前提
CSVをExcelで確認することは想定していない

※この前提は重要です。今回まとめた方法で出力されたcsvファイルはExcelで確認すると文字化けしたりして、期待した表示ができません。Excelで確認できるようにするには別途考慮が必要です。

# gemの導入
RailsからAWSへアクセスするためのgemをインストールする必要があります。
Gemfileに以下を追加します。

```ruby:Gemfile
gem 'aws-sdk', '~> 3'
```

# 共通クラスの作成
今回作成するCSV出力機能は共通ロジックとして使用したかったので、`/app/lib`に`output_csv.rb`というファイルを作成します。
output_csv.rbには以下のようにクラスを定義します。
このクラスのメソッドは全てクラスメソッドにします。

```ruby:app/lib/output_csv.rb
require 'csv'
require 'nkf'

class OutputCsv
  class << self
    # ここにメソッドを追加していく
  end
end
```
※ requireしているcsvとnkfは後ほど使用します。

# S3への出力メソッド実装

S3に出力するメソッドを作成します。
パラメータで受け取ったCSV文字列をS3に出力します。
CSV文字列作成部分は後述します。

```ruby:app/lib/output_csv.rb
# CSV出力メソッド -- ①
def save_csv(data)
  # ファイル名は 'ランダム文字列_タイムスタンプ.csv' とする
  file_name = "#{SecureRandom.urlsafe_base64(9)}_#{Time.current.strftime('%Y%m%d%H%M%S')}.csv"
  # バケット直下のcsvという名前のフォルダ配下に出力する
  file_full_path = "csv/#{file_name}"

  # 開発環境ではローカルに、本番環境ではS3にCSV出力する -- ②
  if Rails.env.development?
    File.open(Rails.root.join('tmp', file_name), 'w') do |f|
      # NKFを使用して文字コードを変換 -- ③
      f.puts(NKF.nkf('-x -w', data))
    end
  else
    # S3クライアントのインスタンス作成 -- ④
    s3 = Aws::S3::Client.new
    # S3にCSVを出力 -- ⑤
    s3.put_object(bucket: ENV['AWS_S3_BUCKET'],
                  key: file_full_path,
                  body: NKF.nkf('-x -w', data),
                  content_type: 'text/csv')
  end
end
```

① CSV出力メソッドの定義です。パラメータ(data)で出力するCSV文字列を受け取ります。

```ruby
def save_csv(data)
```

② 今回は開発環境ではローカルに、本番環境ではS3に出力するようにしています。開発環境でもS3に出力したい場合、この分岐は不要です。

③ [NKF](https://docs.ruby-lang.org/ja/latest/class/NKF.html)を使用して文字列を変換しています。

```ruby
f.puts(NKF.nkf('-x -w', data))
```
第1引数の-xと-wは以下のような変換を指定しています。
-x: 半角カタカナを全角カタカナに変換せずに出力
-w: utf-8で出力

④ S3クライアントのインスタンスを作成しています。これを使用してS3にアクセスします。

```ruby
s3 = Aws::S3::Client.new
```

注意点として、ここではコンストラクタにパラメータを渡していません。
理由はこのRailsアプリケーションが動作するEC2インスタンスのロールにS3へ書き込みをする権限が付与されているため、権限について考慮する必要がないためです。

ロールではなく、IAMユーザーの権限でS3にアクセスしたい場合は、以下のようにアクセスキーとシークレットキーを指定してあげる必要があります。

```ruby
s3 = Aws::S3::Client.new(
  access_key_id: 'your_access_key_id',
  secret_access_key: 'your_secret_access_key'
)
```

⑤ S3にCSVファイルを作成します。

```ruby
s3.put_object(bucket: ENV['AWS_S3_BUCKET'],
              key: file_full_path,
              body: NKF.nkf('-x -w', data),
              content_type: 'text/csv')
```

put_objectというメソッドを使用しています。
各パラメータの内容は以下の通りです。

・bucket: 出力するバケットの名称です。ここでは環境変数`AWS_S3_BUCKET`に設定されている想定で記述しています。
・key: 出力するディレクトリ名 + ファイル名を指定します。
・body: 上記で説明したNKFで変換した文字列を出力します。
・content_type: ファイル形式を指定しています。ここで明示的にcsvファイルであることを指定しないとS3上でCSVファイルとして認識されませんでした。

# CSV文字列の作成
CSV文字列を作成し、その作成した文字列を上記で作成したsave_csvメソッドに渡してCSVファイルを出力するメソッドを作成します。

作成するメソッドでは、パラメータでヘッダー項目、データ項目を受け取り、それをCSV文字列に変換してファイル出力します。

```ruby:app/lib/output_csv.rb
# CSV文字列作成メソッド -- ①
def execute(headers, values)
  output = CSV.generate do |csv|
    # ヘッダー出力 -- ②
    csv << headers
    # データ項目の出力 -- ③
    values.each do |value|
      csv << value
    end
  end

  # CSVファイル出力 -- ④
  save_csv(output)
end
```
① CSV文字列作成メソッドの定義です。
パラメータでヘッダーとデータ項目の配列を受け取ります。
例えば、以下のようなCSVを作成したい場合

```
id,name,age
1,hoge,20
2,fuga,31
3,foo,43
```
以下のような配列を作成してパラメータに渡します。

```ruby
# ヘッダー
headers = ['id', 'name','age']
# データ項目
values = []
values.push([1, 'hoge', 20])
values.push([2, 'fuga', 31])
values.push([3, 'foo', 43])

OutputCsv.execute(headers, values)
```
② パラメータで受け取ったヘッダーの値をCSVにセットしています。

③ パラメータで受け取ったデータ項目をCSVにセットしています。配列になっているため、１行ずつ取り出してセットしています。

④ 上記で作成したsave_csvメソッドに作成したCSV文字列を渡してS3に出力します。

# 全コード
全てのコードは以下の通りです。
save_csvメソッドは外部から呼び出す想定はないため、privateにしています。

```ruby:app/lib/output_csv.rb
require 'csv'
require 'nkf'

class OutputCsv
  class << self
    def execute(headers, values)
      output = CSV.generate do |csv|
        # ヘッダー出力
        csv << headers
        # データ項目の出力
        values.each do |value|
          csv << value
        end
      end

      # CSVファイル出力
      save_csv(output)
    end

    private

    def save_csv(data)
      # ファイル名は 'ランダム文字列_タイムスタンプ.csv' とする
      file_name = "#{SecureRandom.urlsafe_base64(9)}_#{Time.current.strftime('%Y%m%d%H%M%S')}.csv"
      # バケット直下のcsvという名前のフォルダ配下に出力する
      file_full_path = "csv/#{file_name}"

      # 開発環境ではローカルに、本番環境ではS3にCSV出力する
      if Rails.env.development?
        File.open(Rails.root.join('tmp', file_name), 'w') do |f|
          # NKFを使用して文字コードを変換
          f.puts(NKF.nkf('-x -w', data))
        end
      else
        # S3クライアントのインスタンス作成
        s3 = Aws::S3::Client.new
        # S3にCSVを出力
        s3.put_object(bucket: ENV['AWS_S3_BUCKET'],
                      key: file_full_path,
                      body: NKF.nkf('-x -w', data),
                      content_type: 'text/csv')
      end
    end
  end
end
```

# 使用方法
### CSVファイル出力
S3にファイルを出力します。

```ruby
headers = ['id', 'name','age']
values = []
values.push([1, 'あいうえお', 20])
values.push([2, 'かきくけこ', 31])
values.push([3, 'さしすせそ', 43])

OutputCsv.execute(headers, values)
```

### S3に出力されたファイルを確認
指定したバケット、ディレクトリに以下の通りCSVファイルが出力されます。

![スクリーンショット 2020-09-18 16.26.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279050/3f08df30-bf8b-62c9-1bf2-17b90ad6e55d.png)

### ファイルの中身を確認
S3に出力されたファイルをダウンロードして内容を確認すると以下のようになります。
![スクリーンショット 2020-09-18 16.30.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279050/35ef8c3f-8fce-c633-477f-aae185330301.png)