Amazon S3 でバケット内の空フォルダを一覧化するには

2021.05.31

困っていた内容

下記を参考にアカウント間でバケットのデータをコピーしましたが、コピー元とコピー先でオブジェクト数に差異が出ています。

参考:別の AWS アカウントから S3 オブジェクトをコピーするにはどうすればよいですか?

原因はコピーの際に空フォルダが消えてしまっているためでした。空フォルダを一覧化する、または空フォルダもコピーする方法はありますか?

どう対応すればいいの?

S3における空フォルダとは

まずS3の基本的な構造の話ですが、そもそもS3においてフォルダやディレクトリという概念は存在しません。例えばフルパスが「foo/bar.txt」の場合、フォルダ(foo)とファイル(bar.txt)ではなく、「foo/bar.txt」というキーの1つのオブジェクトということになります。

ただし、S3管理コンソールでは、人間に分かりやすいようスラッシュ区切りのディレクトリ階層とすることで、フォルダやファイルのように表示されており、コンソール上で「フォルダの作成」をした場合には、S3は明示的に0バイトの空フォルダを作って表現します。

詳しい話については、下記ブログをご確認ください。

以下、試しに「test1」というフォルダをコンソールで作成し、他のオブジェクトはCLIでアップロードしました。(lsの結果、空フォルダとして0バイトで存在する「test1/」に対し、「test2/」や「test3/」は単体では表示されていないことが分かります。)

cpsync といったコピーリクエストを試してみましたが、0バイトのオブジェクトはコピーされても、空フォルダ「test1/」がコピーされることはありません。

% aws s3 ls s3://ito-test-0528 --recursive --sum 
2021-05-28 18:45:11          0 empty.out
2021-05-31 11:26:48       1024 test.txt
2021-05-28 18:26:58          0 test1/
2021-05-31 11:33:39       1024 test1/test.txt
2021-05-31 11:25:21        454 test2/test.html
2021-05-28 18:46:47          0 test3/empty.out

Total Objects: 6
   Total Size: 2502

% aws s3 sync s3://ito-test-0528 s3://ito-test-0528-copy

% aws s3 ls s3://ito-test-0528-copy  --recursive --sum           
2021-05-28 18:49:13          0 empty.out
2021-05-31 11:27:45       1024 test.txt
2021-05-31 11:34:00       1024 test1/test.txt
2021-05-31 11:27:45        454 test2/test.html
2021-05-28 18:49:13          0 test3/empty.out

Total Objects: 5
   Total Size: 2502

% aws s3 cp s3://ito-test-0528/test1 s3://ito-test-0528-copy --recursive
copy: s3://ito-test-0528/test1/test.txt to s3://ito-test-0528-copy/test.txt

# コピー先バケットのlsの結果は上記と変わらず

なお、確かにオブジェクト数は異なりますがトータルサイズは同じなので、もし0バイトのファイルがコピーされたかどうかを確認する必要がなければ、こちらの数値で比較すれば済みそうですね。

空フォルダの一覧化

空フォルダの数を確かめる必要がある場合、下記のように jq を活用して簡単に一覧化できます。

aws s3api list-objects --bucket BUCKETNAME | jq -r '.Contents[] | select(.Key | endswith("/")) | select(.Size == 0) | .Key'

今回の例でさらにフォルダ「test4/」とその配下の「test4-1/」もコンソールから追加してみました。そして上記コマンドを実行すると、以下のように空フォルダのオブジェクトだけ抽出することができました。

% aws s3 ls s3://ito-test-0528 --recursive
2021-05-28 18:45:11          0 empty.out
2021-05-31 11:26:48       1024 test.txt
2021-05-28 18:26:58          0 test1/
2021-05-31 11:33:39       1024 test1/test.txt
2021-05-31 11:25:21        454 test2/test.html
2021-05-28 18:46:47          0 test3/empty.out
2021-05-31 12:06:41          0 test4/
2021-05-31 12:06:57          0 test4/test4-1/

% aws s3api list-objects --bucket ito-test-0528  | jq -r '.Contents[] | select(.Key | endswith("/")) | select(.Size == 0) | .Key'
test1/
test4/
test4/test4-1/

jq とは

jq は、JSONデータを整形するために最適なツールです。AWS CLI の結果はJSONで返されますので、必要な値を抽出したりデータを集計したりするのに活用されています。

こちらの公式サイトより各種OSへインストールしてご利用ください。MacならHomebrewより入れられます。

本当に空フォルダはコピーできないのか?

上述で空フォルダのコピーができないとお見せしましたが、念のため他の方法も試してみました。cp で指定を工夫しましたがエラーとなり、ローカル(フォルダ「test5」)からも試しましたがやはり sync では空フォルダは無視され、cp なら同様のエラーとなりました。

% aws s3 cp s3://ito-test-0528/test1 s3://ito-test-0528-copy
fatal error: An error occurred (404) when calling the HeadObject operation: Key "test1" does not exist

% aws s3 cp s3://ito-test-0528/test1/ s3://ito-test-0528-copy
copy failed: s3://ito-test-0528/test1/ to s3://ito-test-0528-copy/ Parameter validation failed:
Invalid length for parameter Key, value: 0, valid range: 1-inf

% tree test5 
test5
├── test.txt
├── test1
│ └── test.txt
└── test2

% aws s3 cp test5 s3://ito-test-0528 --recursive
upload: test5/test.txt to s3://ito-test-0528/test.txt
upload: test5/test1/test.txt to s3://ito-test-0528/test1/test.txt

調べた限り、GitHub イシューで何年も前から要望されていますが、現時点では導入されていないようです。

なお、CLIでは試した&調べた限り空フォルダのコピーはできないようですが、SDK では実現する方法があるという情報がいくつかのサイトに載っていましたので、ユースケース上どうしても必要な場合は調べて参照してみてください。(※動作未確認です)

参考資料

"名前が空の"フォルダを作ってしまった場合には:

バケットに大量オブジェクトがある場合、リストする際は料金にご注意ください: