s3からフォルダのようなkeyが帰ってくる。

# s3からファイルの取得
ユーザーがアップロードした画像を user_images というフォルダに "user_id/image_id.**" というkeyで保持しているシステムがあるとします。
保存例としてはこんな感じ。

```ruby:例
require 'aws-sdk'

client = Aws::Credentials.new(
  region: 'REGION',
  access_key_id: 'YOUR_ACCESS_KEY',
  secret_access_key: 'YOUR_SECRET_KEY'
)

client.put_object(
  :bucket => 'SOME_BUCKET',
  :key    => 'user_1/image_1.jpg',
  :body   => "FILE_PATH"
)
```

## keyの取得

このようなs3からrubyのaws-sdkで保存されているファイルを取得するために以下のようなコードを動かしました。

```ruby
client.list_objects(:bucket => 'some_bucket', :prefix => 'user_images/').contents.each do |object|
  puts object.key
end
```

user_images/ 以下のファイルのkeyを羅列するだけのコードです。


## 得られた結果
すると以下のような結果が得られました。
ユーザーが画像をアップロードする用のフォルダがあり、そこにuser_1がfile_1と2を、user_2がfile_3をアップロードしているような形です。

```:結果
user_images/
user_images/user_1/file_1.**
user_images/user_1/file_2.**
user_images/user_2/file_3.**
```

この結果を見て最初私は
「なんだこの user_images/ とかいう謎のファイルは？ フォルダか？」
「get_objectsはフォルダも回収してくるのか、邪魔だな。」
「じゃあ、フォルダが返ってきてしまう最初の行のデータは無視して2行目から利用すればいいか。」
と考えました。

しかし、ファイルもobjectとして回収するならなぜuser_1やuser_2は回収されないのか。
以下のような結果にならないのはなぜなのだろうか？

```

user_images/
user_images/user_1/  ←これと
user_images/user_1/file_A
user_images/user_1/file_B
user_images/user_2/  ←これは実際には出てこない
user_images/user_2/file_C
```

そこで少し調べてみたところ発見したのがこちらの記事

[Amazon S3における「フォルダ」という幻想をぶち壊し、その実体を明らかにする](https://dev.classmethod.jp/cloud/aws/amazon-s3-folders/)

AWSについて調べていると必ずと言っていいほど記事を見かけるクラスメソッド様の記事です。

## s3にフォルダはない

こちらの記事によると、
> Amazon S3の基礎技術は、単純なKVS（Key-Value型データストア）でしかありません。

とのこと。
つまり、barと書かれたbar.txtというファイルをfooというフォルダに保存しているように見えても、その実態は

```
(ルート)
└ foo/
  └ bar.txt
```
という階層構造を成しているのではなく


"foo/bar.txt"　というkeyを持った barというvalueを持つファイルが1つ存在するのみである。

|    key    |value|
|:---------:|:---:|
|foo/bar.txt| bar |

ということだそう。

## 「フォルダの作成」

となるとコンソールの「フォルダの作成」とは一体何をしているのかが気になるのでして、

<img width="549" alt="s3_console.png" src="https://qiita-image-store.s3.amazonaws.com/0/315470/0442e96e-45ed-9442-1b5a-ba9f23fc272f.png">

s3にはフォルダなんてものは無いという話から真っ向切って戦うこのコマンド。
実はこのコマンドこそが user_images/ なるフォルダっぽいパスが返ってくる原因なのでした。

## フォルダ(のようなもの)の正体
コンソールからフォルダの作成を実行すると、そのkeyを持ったsize 0 のファイルが作成されます。
これがフォルダっぽい何かの正体です。

ちなみにtestというフォルダが存在しない状態で test/hoge.txt というkeyのファイルを保存した結果自動的に生成されるフォルダ(っぽいもの)が user_1 や user_2 のようなlist_objectsで返ってこないものです。

こちらは、以下に含まれるファイルが全て削除されるとコンソールからはフォルダも消えてなくなります。
つまりhoge.txt削除するとtestフォルダも消えます。

```ruby
client.put_object(
  :bucket => 'SOME_BUCKET',
  :key    => 'test/hoge.txt',
  :body   => "FILE_PATH"
)

client.list_objects(:bucket => 'some_bucket').contents.each do |object|
  puts object.key
end

**********
#結果
test/hoge.txt

# test/ は発生しない。

```

以上のことから分かるのは、謎のファイルは、コンソールからフォルダの作成を実行すると発生するということです。

また、s3 のコンソールはkeyに含まれる " / " を読み取ってフォルダのように我々の目に見せてくれているというわけですね。

フォルダをコンソールから作成したり、しなかったり、2つの方法が混在してしまうと list_objects の結果がごちゃごちゃしてしまいます。

少なくとも、同じ階層のフォルダの生成方法はコンソールからか否か、統一した方がlist_objectsの使用の障害にならないと思います。

## 空ファイルの取得

ちなみに
せっかくなので、この空のファイルを取得してみます。

```ruby
puts client.get_object(:bucket => 'some_bucket', :key => 'user_images/').body
```

これで件の " user_images/ "を取得します。

結果としては
:content_length=>nil で :bodyも空っぽのモノが返ってきました。

一応、普通に取得は出来るらしい。
まぁ、list_objectsが拾ってくるのだから当然といえば当然。

# おまけ
最後に、空のファイルを上げることでコンソールからフォルダの作成をした時と同様にフォルダっぽい何かを作れないか試してみた。

バケットは今までと同じく。keyはまだ存在しないフォルダパスにして、
bodyは・・・、空ファイル? いちいち File.open しないでも "" でいいか?(適当)

```ruby
client.put_object(
  :bucket => 'some_bucket',
  :key    => 'hoge/',
  :body   => ""
)
```

結果、エラーが出ない。
コンソールを更新すると、hogeが現れる。
中身を見ると 「このパスにオブジェクトはありません。」

うまくいきました。

正直、利用方法は思いつきません。
こんなことが出来るみたいです、というお話。

ちなみに、ちゃんと空ファイルを作成してそれを送りつけてもフォルダが作れました。

```ruby
# testという名前の空ファイルを作って
File.open("./test", "w").close()

# 送りつける
client.put_object(
  :bucket => 'some_bucket',
  :key    => 'hoge/',
  :body   => File.open("./test", "w")
```
こちらもhogeというフォルダが作られる。

~~実はこのおまけ以外は調べた人間が違うというだけでほとんど参考記事の焼き増しと化しているのはここだけの秘密。~~

#参考記事
[Amazon S3における「フォルダ」という幻想をぶち壊し、その実体を明らかにする](https://dev.classmethod.jp/cloud/aws/amazon-s3-folders/)


# 追記
空ファイルがいたところでなんの問題もないと思っていたが、下記の事象に見舞われた。
公開するバケットやファイル()なら気をつけた方がいいようだ。

https://twitter.com/NTakaUV/status/1346728396131241987

# 追記2
上記の内容を記事にした

https://qiita.com/Takayuki_Nakano/items/b6ebc64107a470d1dcc6