Railsで開発環境はpublicに書き込むS3にアクセスするクラスを書いた。

## はじめに
S3へのアクセスは開発環境ではローカルに書き込む事が多い。なので、ステージングや本番環境ではS3へアクセスして開発環境ではローカルに書き込むクラスを書いた。

`aws-sdk`を使っているので、バージョンによって異なる動作をするので注意。

```rb:Gemfile
gem 'aws-sdk'
```

下記のバージョンで動作確認してます。

```Gemfile.lock
    aws-sdk (2.1.24)
      aws-sdk-resources (= 2.1.24)
    aws-sdk-core (2.1.24)
      jmespath (~> 1.0)
    aws-sdk-resources (2.1.24)
      aws-sdk-core (= 2.1.24)
```

## S3Accessor
最低限の機能を兼ね揃えたクラスはこちら

```rb:s3_accessor.rb
# S3へのアクセッサー
# S3が全公開されている事が前提です。(HTTPアクセスを利用してます。)
class S3Accessor
  require 'aws-sdk'

  # S3のルートパスを返却します。
  def self.root_url(root_url = "/")
    if S3Accessor.s3_use?
      # S3を使用している場合はS3のドメイン+バケット名を返却する。
      return "https://s3-#{AppConfig.get(:s3, :region)}.amazonaws.com/#{S3Accessor.bucket}/"
    else
      # S3を使用しない場合はルートパスを返す。
      return root_url
    end
  end

  # S3に保存されているデータを取得します。
  def self.get(path)
    # 冒頭に"/"がある場合は削除する。
    s3_path = path.index("/") == 0 ? path.sub("/", "") : path

    # S3の使用可否で分岐
    if S3Accessor.s3_use?
      # S3を使用する場合(aws-sdkでアクセス出来るかもしれ無いが・・・)

      # S3からデータを取得します。
      file_data = nil
      uri = URI.parse(S3Accessor.root_url + s3_path)
      Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') { |http|
        # 送信
        response = http.get(uri.request_uri)

        # 取得したデータを保存します。
        if response.code == "200"
          # 返却用ファイルイメージを読み込みます。
          file_data = response.body
        else
          raise "S3 Access Error! response code:#{response.code}"
        end
      }
      return file_data
    else
      # 開発環境などでS3を使用しない場合

      # ファイルを読み込みます。
      file_data = nil
      File::open("#{Rails.root}/public/#{s3_path}") {|f|
        file_data = f.read
      }

      # 読み込んだファイルを返却する。
      return file_data
    end
  end

  # 指定のファイルをS3へアップします。
  # 同名のキーがある場合は上書きします。
  def self.put(path, filename, data)
    # 冒頭に"/"がある場合は削除する。
    s3_path = path.index("/") == 0 ? path.sub("/", "") : path
    # 末尾に"/"がない場合は付与する。
    s3_path << "/" unless s3_path.rindex("/") == s3_path.length

    # S3の使用可否で分岐
    if S3Accessor.s3_use?
      # S3を使用する場合

      # S3のインスタンスを取得
      s3 = S3Accessor.s3_instance

      # バケットを取得してファイルアップを行います。
      obj = s3.bucket(S3Accessor.bucket).object("#{s3_path}#{filename}")
      obj.put(body: data)
    else
      # 開発環境などでS3を使用しない場合

      # ローカルに保存する場合はpublic配下に配置する。
      local_path = "#{Rails.root}/public/#{s3_path}"

      # ファイルパスがない場合は作成する。
      FileUtils.mkdir_p(local_path) unless FileTest.exist?(local_path)

      # ファイルを書き込みます。
      File.open("#{local_path}#{filename}", 'wb') do|f|
        f.write(data)
      end
    end
  end

  # S3のデータを削除します。
  def self.delete(path)
    # 冒頭に"/"がある場合は削除する。
    s3_path = path.index("/") == 0 ? path.sub("/", "") : path

    # S3の使用可否で分岐
    if S3Accessor.s3_use?
      # S3を使用する場合

      # S3のインスタンスを取得
      s3 = S3Accessor.s3_instance

      # バケットを取得してファイルアップを行います。
      obj = s3.bucket(S3Accessor.bucket).object(path)
      obj.delete
    else
      # ローカルに保存する場合はpublic配下のファイルを削除する。
      full_path = "#{Rails.root}/public/#{s3_path}"
      # ファイルを削除する。
      begin
        File.delete full_path
      rescue => e
        Rails.logger.error("file delete error #{e}")
      end
    end
  end

  private

  # S3のインスタンスを取得します。
  def self.s3_instance
    Aws::S3::Resource.new(access_key_id: AppConfig.get(:s3, :access_key_id),
                          secret_access_key: AppConfig.get(:s3, :secret_access_key),
                          region: AppConfig.get(:s3, :region))
  end

  # S3を使用するか？
  def self.s3_use?
    AppConfig.get(:env, Rails.env, :s3, :use)
  end

  # S3のバケット名を取得
  def self.bucket
    AppConfig.get(:env, Rails.env, :s3, :bucket)
  end

end
```

`AppConfig`に関しては見ればなんとなくわかると思いますが、後述します。

## 使用方法
S3を使う場合は、開発環境ではローカルにファイルを保存したい場合があります。
そのような要望にも対応してます。

### ファイルアップロード

```rb
S3Accessor.put("file_path", "file_name", file_data)
```

file_dataにはバイナリを指定します。
`params[:file]`でファイルアップロードを行った場合は以下のようにファイルをアップします。

```rb
S3Accessor.put("file_path", "file_name", params[:file].read)
```

### ファイル参照
`erb`などでファイルを表示する場合は以下のようにします。

```erb:test.html.erb
<%= image_tag "#{S3Accessor.root_url(root_url)}file_path/file_name" %>
```

開発環境でファイルを表示するために`root_url`を渡してます。
絶対パスが不要の場合は引数なしでOKです。

### ファイルデータ取得
バイナリデータが必要な場合は以下のアクセスで取得します。
※HTTPリクエストで取りに行ってるので、ここはイケて無い・・・

```rb
S3Accessor.get("file_path/file_name")
```

### ファイル削除
S3にはフォルダの概念が無いのですが、開発環境の場合はフォルダが残ります。

```rb
S3Accessor.delete("file_path/file_name")
```

### おまけ
上記にある`AppConfig`は以下のようなコードで対応してます。

```rb:app_config.rb
class AppConfig
  # データを取得します。
  def self.get(*keys)
    yml = AppConfig.yml["app"]
    keys.each { |key|
      yml = yml[key.to_s]
    }
    return yml
  end

  private

  # ymlデータを取得します。
  def self.yml
    # yamlファイルは毎回読む。
    YAML.load_file("#{Rails.root}/config/app_config.yml")
  end
end
```

読み込むyamlは以下のようになってます。

```yaml:app_config.yml
app:
  # S3関連
  s3:
    # 接続情報
    access_key_id: 'access_key_id'
    secret_access_key: 'secret_access_key'
    region: 'ap-northeast-1'

  # 環境ごとに変化する設定値
  env:
    development:
      s3:
        use: false
        bucket: "bucket-develop"
    production:
      s3:
        use: true
        bucket: "bucket-production"
```

`development/s3/use`で`S3`の使用可否を切り替えれます。
幾つか動かして無いプログラムがあるので、バグがあるかもしれません・・・。(苦笑)