AWS SDK for RubyでS3のオブジェクトを扱う (まとめて削除）。

## これはなに？

AWS S3のオブジェクトをAWS SDK for Rubyで扱う時のメモです。今回は**削除**に関してのメモになります。
すでにたくさんの記事にわたしもお世話になりましたので、網羅的に参考にしたい場合はぜひ下記の記事もどうぞ！（ありがとうございます！）

- [AWS SDK for Ruby V2でS3をいろいろ使う](https://qiita.com/tamikura@github/items/a6f819a0ce2b6c79bec7)
- [これで大抵賄える！AWS SDK for Ruby V3でのS3操作まとめ！！](https://qiita.com/Kta-M/items/b9f22277e983a05389fb)

## オブジェクトの削除をしたい

基本は **オブジェクトを特定** が必要になります。
また、特定したオブジェクトに対して削除APIを呼び出す形になります。

- https://docs.aws.amazon.com/sdk-for-ruby/v2/api/Aws/S3/Object.html#delete-instance_method

こちらに関しては、ここまで。以下、「たくさんあるとき」について記載してみます。

## オブジェクトの一括削除をしたい

こちらも、基本は **オブジェクトを特定** が必要になります。

- 特定したオブジェクトに対して削除APIを個数分呼び出す
- 特定したオブジェクトを一括で削除する （一括削除のAPIを呼び出してまとめて削除する形になります）

丁寧に一回一回削除APIを呼ぶと、時間がかかりますね！（もちろん確実ではありますが）

ここでは、**一括削除のAPI** を呼び出す方法を取ってみます。
利用するのは、[AWS SDK for Ruby](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/) (version 3) です。

### 削除の流れ (オブジェクト特定 -> 一括削除)

以下のようになります。

- オブジェクトのkeyはアクセス可能なオブジェクトのURLからはわからないので、まずオブジェクトを取得しkeyを取り出します
- 複数ある場合は、Aws::S3::Bucket#objects や Aws::S3::Client#list_objects_v2 や を使います（後述）
  - Aws::S3::Client#list_objects_v2は一回につき **１０００件まで** の制約があります！
- オブジェクト、もしくはオブジェクトのkeyを配列に入れて、一括削除用のメソッドに引き渡します
  - これも、利用できるメソッドが Aws::S3::Bucketのインスタンス / Aws::S3::Clientのインスタンスの2つがあります 


#### Aws::S3::Bucket#objects を使う場合

- Aws::S3::Bucket#objects だと1000件単位かどうかを気にせず、**一括で取れる** ので、こちらを利用がいいかもしれません
  - Ref.https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Bucket.html#objects-instance_method
  - ``Client#list_objects_v2 will be called multiple times until every ObjectSummary has been yielded.`` と記載があるように、実際はAws::S3::Client#list_objects_v2を使って内部で複数回してくれています

```ruby

# ここではまず Aws::S3::Resource を使います
s3_resources = Aws::S3::Resource.new(
  access_key_id: アクセスキーID,
  secret_access_key: シークレットアクセスキー,
  region: リージョン
)

bucket_name = '作業したいバケット名'

# Aws::S3::Bucketのインスタンスを取得します
bucket = s3_resources.bucket(bucket_name)

# 特定のプレフィックス以下を対象に抽出したいときは、prefixオプションを渡せます
bucket.objects(prefix: "uploads/tmp").each do |object|
  puts object.key
end

```

#### Aws::S3::Client#list_objects_v2 を使う場合

- 1000件の制約があるので、自分で明示的にeachで回します
  - https://github.com/aws/aws-sdk-ruby/blob/c695249387feab78fe17077b5f80e8deaa6fd466/README.md#paging-responses
  - https://docs.aws.amazon.com/sdk-for-ruby/v3/api/#Paging_Responses

```ruby
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/#Paging_Responses から引用
# yields one response object per API call made, this will enumerate
# EVERY object in the named bucket

# Aws::S3::Client#list_objectsやAws::S3::Client#list_objects_v2を使うケース
s3 = Aws::S3::Client.new

s3.list_objects(bucket:'aws-sdk').each do |response|  # eachで回せば取得できます！
  puts response.contents.map(&:key)
end
```

どちらの場合も、対象のオブジェクト（複数）そのものか、keyを配列に格納しておきます。

## 取得した複数のオブジェクトを一括で消したい

では、実際に削除となります。

### 基本: オブジェクトのキー（配列）を渡して削除していく

- 基本は、オブジェクトのキー（配列）を渡して、SDK内で一括削除APIを叩いてもらいます
- ここでは、Aws::S3::Bucketのインスタンスに対してdelete_objectsを利用します
  - https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Bucket.html#delete_objects-instance_method

後述しますが、削除の場合も一括操作は**１０００件まで** の制約がありますので注意してください。

```ruby
# object_keys_list は、 こんな配列が入っている想定
# [ { "Key": "test1.txt" }, { "Key": "test2.txt" } .....  { "Key": "test2000.txt" } ]

# 1000個までが一括削除の上限なので、分割します
object_keys_list.each_slice(1000) do |object_keys|
  # Aws::S3::Bucketのインスタンスに対してdelete_objectsを呼び出す
  bucket.delete_objects(delete:{ objects: object_keys })
end
```


### プレフィックス以下のオブジェクトを一括で消したい

上記は、キーをその分だけ特定しての削除でしたが、「ある一定のパス（ディレクトリ）以下は削除したい！」という場合はこちらも利用できます。
仮想的に、ディレクトリ（フォルダ）ごと削除するようなイメージです。

- 仮想的なディレクトリの配下のオブジェクトを全部消したい場合
- prefix指定でAws::S3::ObjectSummary::Collectionを取得し、batch_delete!を実行する
  - 1000件以上ありそうな場合は注意！ 

```ruby

# ここではAws::S3::Resourceを使います
s3_resources = Aws::S3::Resource.new(
  access_key_id: アクセスキーID,
  secret_access_key: シークレットアクセスキー,
  region: リージョン
)

bucket_name = '作業したいバケット名'

# Aws::S3::Bucketのインスタンスを取得
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Resource.html#bucket-instance_method
bucket = s3_resources.bucket(bucket_name)

# 例: CarrierWave & Fogを使う場合、アップロードの際には uploads/tmp/ というディレクトリにもキャッシュが溜まる
prefix = "uploads/tmp/"

# "uploads/tmp/" を対象にする
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Bucket.html#objects-instance_method
# prefix指定をすると、その配下のオブジェクトを含んだコレクションを返す
target_s3_directory = bucket.objects({prefix: prefix})

# 取得したAws::S3::ObjectSummary::Collection に対して batc_delete! を呼ぶ
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/ObjectSummary/Collection.html

target_s3_directory.batch_delete!
```

#### ここでの実際の処理は？

- [batch_delete!](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/ObjectSummary/Collection.html#batch_delete!-instance_method) の処理では、プレフィックス配下のオブジェクトのkeyをループで取得し、「削除対象の実際のオブジェクトの配列」に格納します

```json
# こんなパラメータを作成します
{ "Objects": [ { "Key": "test1.txt" }, { "Key": "test2.txt" } ],
  "Quiet": false
}
```

- 次に、Aws::S3::Bucket(バケットオブジェクト）を利用し、Aws::S3::Bucket#delete_objects を呼びます
  - https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Bucket.html#delete_objects-instance_method
- 最終的には、Aws::S3::Client#delete_objects が呼ばれます
  - https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#delete_object-instance_method 

AWS CLIの操作の、こちらに該当します。

- https://docs.aws.amazon.com/cli/latest/reference/s3api/delete-objects.html (s3api: delete-objects)
- 以下のrubyでの処理のように、AWS CLIでも複数オブジェクトを削除するためにJSONにオブジェクトのキーを列挙する形になっています

```ruby
# Aws::S3::Client#delete_objectsを実行
resp = client.delete_objects({
  bucket: "examplebucket", 
  delete: {
    objects: [
      {
        key: "objectkey1", 
      }, 
      {
        key: "objectkey2", 
      }, 
    ], 
    quiet: false, 
  }, 
})

```

#### 注意点

CLIでも、ruby sdkでも、一括削除は１０００件が上限のため、１０００件以上ある場合は分割が必要になります。

**AWS CLI / ruby sdkの注意書き**

> The request contains a list of up to 1000 keys that you want to delete. In the XML, you provide the object key names, and optionally, version IDs if you want to delete a specific version of the object from a versioning-enabled bucket. For each key, Amazon S3 performs a delete operation and returns the result of that delete, success, or failure, in the response. Note that if the object specified in the request is not found, Amazon S3 returns the result as deleted.

REST APIの場合はJSONではなくXMLで削除対象を渡す形になります。

- https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html

## 実際の作業にあたって

わたしは心配性なので、特に削除の場合は、Rubyのコード内だけでなく、AWS CLIも使って、想定のオブジェクトが正しく取得できるか、削除できているかをチェックしています。

また、削除前にCLIで検証用にオブジェクトを退避（バックアップ）させたりしています。
もちろんうまく行くと、とても嬉しいです！

## まとめ

以上の通りですが、「１０００件」の制約を面倒とみるかどうか。
個人的には、「間違ってちがうものを消しちゃった！！！！」という惨事を食い止めることができるので、良い意味でガードがかかっていると思いました。

SDKはソースが公開されていますし、対応するAPIもドキュメントがあるので、こんなわたしでも読みやすいなあと感じています！
（もちろん公式だけでなく、先に知見を公開してくださっている皆さんのおかげです）

また、誤字や認識間違いなどあれば、ご指摘いただけると幸いです。