GAE/PHP で Google Cloud Storage を利用する際のハマりどころ

2017年1月22日 18:56

Google App Engine(GAE)の PHP(Standard Environment)については、Google 公式文書の Google App Engine PHP Standard Environment ドキュメント が、なんだかんだ言って一番参考になる。そして今回は、こちらの Allowing Users to Upload Files に記されている手順に従い、Google Cloud Storage(GCS)へのファイルアップロードを試みた次第。

では、ツイート中にある「注意すべき点」についてフォローしておこう。

まず、<input type="file" name="uploaded_file"> について。この input タグに multiple 属性を指定すると、ファイル選択ダイアログで複数のファイルを選択できるが、これは HTML5 のフォームの拡張で追加された仕様ということと、個人的にいまいちしっくりこないのとで、指定したことはほとんどない(まったくない、かもしれない)。今回の案件では、Underscore.js のテンプレート機能を使ってアップロードするファイルの数を動的に変更しているので、<input type="file" name="uploaded_file[]"> と、name 属性を配列にした。

アップロード先の URL からリダイレクトされたファイルで、$_FILES の中身を見てみたところ、以下のようになっていた。

Array
(
    [uploaded_files] => Array
        (
            [name] => Array
                (
                    [0] => sample01.jpg
                    [1] => sample02.jpg
                )

            [type] => Array
                (
                    [0] => image/jpeg
                    [1] => image/jpeg
                )

            [tmp_name] => Array
                (
                    [0] => gs://my-app.appspot.com/.............................
                    [1] => gs://my-app.appspot.com/.............................
                )

            [error] => Array
                (
                    [0] => 0
                    [1] => 0
                )

            [size] => Array
                (
                    [0] => 99059
                    [1] => 1614074
                )

        )

)

これの何が問題かというと、てっきり以下のような構造になると思い込んでいたからだ。

Array
(
    [uploaded_files] => Array
        (
            [0] => Array
                (
                    [name] => sample01.jpg
                    [type] => image/jpeg
                    [tmp_name] => gs://my-app.appspot.com/.............................
                    [error] => 0
                    [size] => 99059
                )

            [1] => Array
                (
                    [name] => sample02.jpg
                    [type] => image/jpeg
                    [tmp_name] => gs://my-app.appspot.com/.............................
                    [error] => 0
                    [size] => 1614074
                )

        )

)

こっちのほうが以降の処理をする際も、

foreach ($_FILES['uploaded_files'] as $file) {
    // 処理
}

みたいな感じでシンプルに書けるというもの。まぁ仕様にケチをつけても仕方がないか。

tmp_name に格納されている GCS 用のスキーム gs:// で始まる名前(実際には bucket 名の後に長いユニーク文字列が続く)は、tmp_name という key で分かるとおり一時ファイルの名前である。ログを確認したところ、コネクションがクローズすると同時にこの bucket のファイルに対して DELETE リクエストが送信されている。よって、実際には削除される前に move_uploaded_file() 関数を用いて移動処理を行なう必要がある(以下はファイルをひとつだけアップロードした場合の例)。

$gs_name = $_FILES['uploaded_files']['tmp_name'];
$gs_name_new = 'gs://my-app.appspot.com/images/' . $_FILES['uploaded_files']['name'];
move_uploaded_file($gs_name, $gs_name_new);

ただし、この方法だと bucket の /images/ にすでに同じ名前のファイルが存在していた場合に問題が起きるはず(未確認)なので、uniqid() などを使ったほうが賢明だろう。もっとも、拡張子は $_FILES['uploaded_files']['name'] から抜き出すか、$_FILES['uploaded_files']['type'] から適宜変換すること。