Amazon S3における「フォルダ」という幻想をぶち壊し、その実体を明らかにする
よく訓練されたアップル信者、都元です。Amazon S3について細かい説明は不要かと思いますが、要するにファイルストレージです。HTTPベースでファイルをアップロードでき、そしてダウンロードできるサービスですね。
古くから、データはシリアライズされた形式でファイルという単位に格納し、管理されてきました。ローカルマシン内でファイルを管理する仕組みがファイルシステムで、その多くにはフォルダという階層構造を扱う仕組みが備わっています。
Amazon S3も、Management Consoleによってフォルダを作成し、その中にさらにフォルダを作成したり、ファイルを格納できたりします。しかし。
Amazon S3には実はフォルダという概念は無い
のです。Amazon S3の基礎技術は、単純なKVS(Key-Value型データストア)でしかありません。例えば下記のようなフォルダ(と我々が認識している)構造があったとします。(本エントリーでは、bar.txtにはbar、baz.txtにはbazっていう文字が入っていると単純に考えることにします。)
(ルート) └ foo/ └ bar.txt
ただこれは、我々がこのように認識しているだけであって、S3的には単純に下記のような情報を保持しているに過ぎません。S3においては基本的に、/に特別な意味は無いのです。
キー(フルパス名) | バリュー(ファイルの内容) |
---|---|
foo/bar.txt | bar |
Amazon S3における空フォルダの表現
さて、S3がこのような構造で情報を保持しているとなると、次に気になるのは空フォルダの扱いです。ファイルの実体が無ければ、空のフォルダを表現できません。
管理コンソールから空のフォルダを作る
例えば管理コンソールから、新しく作ったばかりのバケットのルート直下に foo という名前の空フォルダを作った時、S3上ではファイルサイズ0のfoo/という要素を作成します。
キー(フルパス名) | バリュー(ファイルの内容) |
---|---|
foo/ | (空) |
$ aws s3api list-objects --bucket example { "CommonPrefixes": [], "Contents": [ { "LastModified": "2014-09-18T07:01:52.000Z", "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "StorageClass": "STANDARD", "Key": "foo/", "Owner": { "DisplayName": "-", "ID": "-" }, "Size": 0 } ] }
なるほど。
管理コンソールから、既存の空フォルダの中にファイルを配置する(ケース1)
そのフォルダ(?)の中にbar.txtを配置するとこうなります。
キー(フルパス名) | バリュー(ファイルの内容) |
---|---|
foo/ | (空) |
foo/bar.txt | bar |
$ aws s3api list-objects --bucket example { "CommonPrefixes": [], "Contents": [ { "LastModified": "2014-09-18T07:01:52.000Z", "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "StorageClass": "STANDARD", "Key": "foo/", "Owner": { "DisplayName": "-", "ID": "-" }, "Size": 0 }, { "LastModified": "2014-09-18T07:03:46.000Z", "ETag": "\"3b1ba3d6b442f606ec52f29783ec3715\"", "StorageClass": "STANDARD", "Key": "foo/bar.txt", "Owner": { "DisplayName": "-", "ID": "-" }, "Size": 4 } ] }
管理コンソールからの認識ではファイルは1つしか無いような気がしますが、S3上ではこのような2要素が管理されています。
aws-cliを使って、存在しないフォルダに直接ファイルを配置する(ケース2)
では一方、新しく作ったばかりのまっさらなバケットのルート直下に、aws-cliから下記のようなコマンドでファイルを配置するとどうなるでしょうか。
$ echo bar >bar.txt $ aws s3api put-object --bucket example --key "foo/bar.txt" --body bar.txt
結果はこう。
キー(フルパス名) | バリュー(ファイルの内容) |
---|---|
foo/bar.txt | bar |
$ aws s3api list-objects --bucket example { "CommonPrefixes": [], "Contents": [ { "LastModified": "2014-09-18T07:05:07.000Z", "ETag": "\"3b1ba3d6b442f606ec52f29783ec3715\"", "StorageClass": "STANDARD", "Key": "foo/bar.txt", "Owner": { "DisplayName": "-", "ID": "-" }, "Size": 4 } ] }
oh... ケース1と2の間に、我々の頭の中では大きな違いは無いのですが、S3的にはちょっと違った状況になっているのですね。まぁ、大して大きな問題にはならないと思いますが。
ちなみにケース2の状態で、管理コンソールからbar.txtを削除すると、foo/が新たに出来上がります。
要素のリストアップ
さて。ここからはもうちょっと複雑な環境を前提にします。こんな感じ。
(ルート) ├ foo/ │ ├ bar/ │ │ └ qux.txt │ ├ baz/ │ ├ quux/ │ │ ├ corge.txt │ │ └ grault.txt │ └ garply.txt └ waldo.txt
つまりS3上は、こう。(敢えてフォルダを表す空ファイルが有ったり無かったりする状況を混在させてます。)
キー(フルパス名) | バリュー(ファイルの内容) |
---|---|
foo/ | (空) |
foo/bar/qux.txt | qux |
foo/baz/ | (空) |
foo/quux/ | (空) |
foo/quux/corge.txt | corge |
foo/quux/grault.txt | grault |
foo/garply.txt | garply |
waldo.txt | waldo |
全要素のリストアップ
この状態について、全ファイルを一覧するコマンド(APIアクション)とパラメータは、今までやってきた通り、こんな感じですね。(出力を簡素化するために、jqでキーのみを出力しています。)
$ aws s3api list-objects --bucket example | jq ".Contents[].Key" "foo/" "foo/bar/qux.txt" "foo/baz/" "foo/garply.txt" "foo/quux/" "foo/quux/corge.txt" "foo/quux/grault.txt" "waldo.txt"
8件の結果が表示されました。
prefixを指定して先頭一致絞り込み
次に、foo/フォルダ内のファイルのみをリストアップしたい場合は、prefixというオプション(パラメータ)を付与します。
$ aws s3api list-objects --bucket example --prefix "foo/" | jq ".Contents[].Key" "foo/" "foo/bar/qux.txt" "foo/baz/" "foo/garply.txt" "foo/quux/" "foo/quux/corge.txt" "foo/quux/grault.txt"
このように、prefixに指定した場合、その文字列から始まるキーを持つ要素7件が一覧されます。もちろんprefix指定を/で終わらせる必要は無く、こんな指定もOKです。
$ aws s3api list-objects --bucket cm-miyamoto-test2 --prefix "foo/b" | jq ".Contents[].Key" "foo/bar/qux.txt" "foo/baz/"
あるフォルダ直下の要素のみをリストアップするには
さて、ファイルシステム上にこのような構造があった時、カレントディレクトリがfooの場合、その時リストアップしたいのは「その直下の要素のみ」であるのが一般的でしょう。上記のようにprefixで絞り込むだけでは、例えばfoo/bar/フォルダの中にファイルが10000個入っていた場合、裏側の処理は少々面倒そうですし、データの扱いとしても非効率的です。
ここで、先ほどからlist-objectsの結果にひっそりと存在するCommonPrefixesという出力が関係してきます。先ほど7件の結果が表示されたコマンドに--delimiter "/"というオプションを追加して実行してみます。
$ aws s3api list-objects --bucket example --prefix "foo/" --delimiter "/" { "CommonPrefixes": [ { "Prefix": "foo/bar/" }, { "Prefix": "foo/baz/" }, { "Prefix": "foo/quux/" } ], "Contents": [ { "LastModified": "2014-09-18T07:39:48.000Z", "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "StorageClass": "STANDARD", "Key": "foo/", "Owner": { "DisplayName": "-", "ID": "-" }, "Size": 0 }, { "LastModified": "2014-09-18T07:41:49.000Z", "ETag": "\"cda2d9c3869a5fa5c4e09340afa8062b\"", "StorageClass": "STANDARD", "Key": "foo/garply.txt", "Owner": { "DisplayName": "-", "ID": "-" }, "Size": 7 } ] }
なんと、Contentsとして出力されたのは、foo/フォルダそのものと、その直下にあるgarply.txtだけに限定されました。そしてCommonPrefixesとして、foo直下にある3つのフォルダbar, baz, quuxが一覧されています。
このようにlist-objectsアクションに対してdelimiterパラメータでパスの区切り文字を指定すると、各要素についてprefixより後に最初に現れるdelimiterまでの文字列でグルーピングし………。言葉では少々説明しづらいわけですが、とにかくこのように「フォルダとして認識できるもののリスト」をCommonPrefixesに出力できるわけです。
Amazon S3管理コンソール
以上のように、S3は単純なKey-Value型ストレージでしかないのですが、list-objectアクションにprefixやdelimiterというパラメータがあることによって、特定のフォルダ内の直下要素のみをリストアップするような挙動が実現できるようになっています。/をパス区切り文字として特別扱いしているのはAmazon S3ではなく、その上の「管理コンソール」なのです。
Amazon S3管理コンソールは、これらの仕組みを利用して、純粋なS3としては概念が存在しない「フォルダ」という幻を我々に見せてくれているのでした。
おまけ
全く実用性はありませんが、下記コマンド結果について、なぜこのような出力となったのか、各自考察してみてください。
このおまけが楽しめた方は、こちらへどうぞ。
$ aws s3api list-objects --bucket example --prefix "fo" --delimiter "q" { "CommonPrefixes": [ { "Prefix": "foo/bar/q" }, { "Prefix": "foo/q" } ], "Contents": [ { "LastModified": "2014-09-18T07:39:48.000Z", "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "StorageClass": "STANDARD", "Key": "foo/", "Owner": { "DisplayName": "-", "ID": "-" }, "Size": 0 }, { "LastModified": "2014-09-18T07:48:02.000Z", "ETag": "\"d41d8cd98f00b204e9800998ecf8427e\"", "StorageClass": "STANDARD", "Key": "foo/baz/", "Owner": { "DisplayName": "-", "ID": "-" }, "Size": 0 }, { "LastModified": "2014-09-18T07:41:49.000Z", "ETag": "\"cda2d9c3869a5fa5c4e09340afa8062b\"", "StorageClass": "STANDARD", "Key": "foo/garply.txt", "Owner": { "DisplayName": "-", "ID": "-" }, "Size": 7 } ] }