Ruby: AWS S3 のある場所のファイルを別のバケットにまとめてコピーする方法

Ruby を使って AWS S3 のある場所のファイルを別のバケットにまとめてコピーする方法を紹介します。

## 環境

Rails の中のタスクのひとつとして作成しましたが Rails なくても実装可能です。

* Rails 6.0.2.1
* Ruby 2.6.5
* Gem aws-sdk-s3 1.60.1

## やりたかったこと

S3 の `bucket/folder` にあるファイルたちを全て `another_bucket/folder` 内 に構造を保った状態でコピーしたかった。

## プログラム

コピーに使用したプログラムを紹介します。

Rails の中のタスクのひとつとして作成しました。 `bundle add aws-sdk-s3` を実行して AWS の SDK を追加しておきます。

```ruby
# S3 SDK reference: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html
require 'uri'

namespace :s3 do
  namespace :something do
    task copy: :environment do
      s3_client = build_s3_client
      copy_bucket_to_bucket(s3_client, 'bucket', 'another_bucket', 'folder/')
    end
  end

  def build_s3_client
    Aws::S3::Client.new(
      access_key_id: Rails.configuration.x.s3_user.access_key,
      secret_access_key: Rails.configuration.x.s3_user.access_secret,
      region: 'ap-northeast-1'
    )
  end

  # @param [Aws::S3::Client] s3_client
  # @param [String] source_bucket
  # @param [String] destination_bucket
  # @param, [String] prefix
  def copy_bucket_to_bucket(s3_client, source_bucket, destination_bucket, prefix = '')
    s3_client.list_objects(bucket: source_bucket, prefix: prefix).to_h[:contents].each do |object|
      puts "copy #{object[:key]}"
      option = {
        #acl: 'public-read',
        bucket: destination_bucket,
        copy_source: URI::encode("/#{source_bucket}/" << object[:key]),
        key: object[:key],
      }
      s3_client.copy_object(option)
    end
  end
end
```

`copy_bucket_to_bucket` が実際に S3 内 でのコピーを行うメソッドです。

## 注意

Bucket のデフォルトの設定では Access Denied というエラーが出るので `acl: 'public-read',` をコメントアウトしています。
下のように Bucket の設定で 2つのチェックボックスをオフにすると `acl: 'public-read'` が使えるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25846/c30fba66-0645-f4e0-76e6-ff851a80aa9e.png)

---

## 別アカウントのS3にコピーする場合

```ruby
require 'uri'
require 'aws-sdk-s3'

def build_source_s3_client
  Aws::S3::Client.new(
    access_key_id:     'SOURCE_ACCESS_KEY',
    secret_access_key: 'SOURCE_ACCESS_SECRET',
    region:            'ap-northeast-1'
  )
end

def build_destination_s3_client
  Aws::S3::Client.new(
    access_key_id:     'DESTINATION_ACCESS_KEY',
    secret_access_key: 'DESTINATION_ACCESS_SECRET',
    region:            'ap-northeast-1'
  )
end

# @param [Aws::S3::Client] source_s3_client
# @param [String] source_bucket
# @param [Aws::S3::Client] destination_s3_client
# @param [String] destination_bucket
# @param [String] prefix
def copy_bucket_to_bucket(
  source_s3_client, source_bucket, destination_s3_client, destination_bucket, prefix = '')

  source_s3_client.list_objects(bucket: source_bucket, prefix: prefix).to_h[:contents].each do |object|
    option = {
      bucket: destination_bucket,
      copy_source: URI::encode("/#{source_bucket}/" << object[:key]),
      key: object[:key],
    }

    # if already exists, skip
    contents = destination_s3_client.list_objects(bucket: destination_bucket, prefix: object[:key]).to_h[:contents]
    if contents != nil && contents.size > 0
      puts "skip #{object[:key]}"
      sleep 0.1
      next
    end

    puts "copy #{object[:key]}"
    source_s3_client.get_object(bucket: source_bucket, key: object[:key]) do |chunk|
      destination_s3_client.put_object(
        bucket: destination_bucket, key: object[:key], body: chunk)
    end
  end
end

source_s3_client      = build_source_s3_client
destination_s3_client = build_destination_s3_client
copy_bucket_to_bucket(
  source_s3_client,      'source-bucket',
  destination_s3_client, 'destination-bucket', '')
```

### 使用した環境

- ruby 2.5.3p105
- Gem aws-sdk-s3 (1.145.0)