GAE/Goで大きなファイルを扱う


※GAE/GOの入門記事も書いてみたので、はじめての方は以下を参考にしていただければ幸いです。
http://www.apps-gcp.com/gae-go-gettingstart-01/

はじめに

ファイルをGCSストレージへアップロードする機能をGAE上で構築しようと考えるとき、2つの方法があるかと思います。

  1. Client Libraryの利用
  2. BlobStoreServiceの利用

ただ、例えば1MB未満のファイルしか扱わないという仕様で収まるのであれば(1)でよいのですが、10GBクラスのファイルがアップロードされる可能性があるならば(1)は諦めるしかありません。それは(1)のパターンの場合GAEを経由してしまうため、以下のようなGAEの制約に引っかかってしまうからです。

ですので、巨大ファイルを扱う場合はGAEの制約を受けない(2)を使うことになります。今回はGAE/GO+(2)で巨大ファイルをストレージへアップロードする方法について記述させていただきます。

実は既に@sinmetalさんがGAE/Jを利用した場合の巨大ファイルのアップロード方法についてわかりやすく書いております。これから後述する内容については、言語の違いを除くとやっていることは全く同じことになります。ですので、GAE/Jの方の構築方法については@sinmetalさんの投稿記事を参考にしていただければと思います。

BlobKeyの取得がゴール

今回の説明はBlobKeyの取得がゴールとなります。当たり前ですが、ストレージにファイルがアップロードされてもシステム側から見てそのファイルを識別できなければ意味はないからです。BlobKeyの取得さえできれば、ファイルの参照や削除は簡易におこなうことができます(後述)。BlobKeyを取得するまでのプロセスは以下の通りです。それではBlobKeyを取得するまでの流れを説明させていただきます。

  1. GCSアップロード用URLの生成
  2. 生成したURLに対してファイルのアップロード
  3. コールバック先のURLでBlobKeyの取得

1. GCSアップロード用URLの生成

以下のサンプルコード(1)はアップロードURLを生成するためのコードとなります。

※1 インポートが必要なモジュール
blobstoreを利用する場合、必ず必要となるモジュールとなります。

※2 UploadURLOptionsオブジェクトの生成
ファイルの最大サイズやアップロード先のバケット(サブディレクトリ含む)を指定するためのオブジェクトです。

※3 アップロードURLの生成
アップロードURLの生成には、前述したUploadURLOptionsオブジェクトやアップロード処理後のコールバックURLを指定します。

サンプルコード(1)
package pkg

import (
    // ※1
    "appengine/blobstore"
    ...
)

func init() {
    http.HandleFunc("/", index)
    ...
}

func index(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)

    // ※2
    option := blobstore.UploadURLOptions{
        MaxUploadBytes: 1024 * 1024 * 1024,
        StorageBucket:  "tomorier001/subDir",
    }

    // ※3
    uploadUrl, err := blobstore.UploadURL(c, "/redirect", &option)
    ...
}

サンプルコード(1)のStorageBucketに指定されている「tomorier001/subDir」は「バケット名/サブディレクトリ名」の組み合わせを表します。サブディレクトリはいくらでも掘れますので、「tomorier001/subDir1/subDir2/subDir3/.../subDirN」のように指定しても問題ありません。バケット直下にファイルをアップロードしたい場合はバケット名だけStorageBucketパラメータに指定してください。

2. 生成したURLに対してファイルのアップロード

(1)で生成したURLに対してファイルのアップロードをおこないます。WEBフォームから単純に送信するもJavaScriptで送信するもそれは自由です。ただ、ここで注意が必要なのはアップロードURLの有効期限(5分くらい?)です。もし、ユーザがフォームの入力に5分以上かかってしまうような画面であるならば、データ送信時にExpiredエラーが出力されてしまいます。ですので、回避策として、アップロードするタイミングでURLを生成する方がよいかと思います。

3. コールバック先のURLでblobKeyの取得

GCSへのファイルアップロード処理が完了すると、(1)のURL生成時に指定したリダイレクト先のURL(/redirect)が呼び出されます。サンプルコード(2)はそのコールバックされる関数となります。ParseUploadによってhttpRequestからBlobInfoを引っこ抜き、指定キーからfileオブジェクトを取得しています。BlobKeyやファイルのメタデータへのアクセス方法はコード下部のログ出力部分を参考にしてください。

サンプルコード(2)
import (
    ...
    "appengine/blobstore"
)

func init() {
    ...
    http.HandleFunc("/redirect", redirect)
}

func redirect(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    blobs, _, err := blobstore.ParseUpload(r)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    file := blobs["file"]
    if len(file) == 0 {
        c.Errorf("no file uploaded")
        return
    }

    // blobKeyとメタデータの出力
    c.Infof("blobKey=> %v", file[0].BlobKey) // blobKey
    c.Infof("Filename=> %v", file[0].Filename) // ファイル名
    c.Infof("ContentType=> %v", file[0].ContentType) // ContentType
    c.Infof("Size=> %v", file[0].Size) // ファイルサイズ(byte)
    ...
}

サンプルコードを実行してみる

実際にサンプルコードをデプロイしファイルのアップロードをおこなってみました。結果は以下の通りです。想定の場所にファイルがアップロードされていることが確認できます。

ダウンロードと削除方法

GCS上のファイルに対するダウンロード・削除についてはそれぞれ以下のコードを参考にしてください。BlobKeyさえあれば簡易にファイルのダウンロード・削除がおこなえることがわかります。

ダウンロード
func download(w http.ResponseWriter, r *http.Request) {
    ...
    blobstore.Send(w, appengine.BlobKey(r.FormValue("blobKey")))
}
削除
func delete(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    ...
    blobstore.Delete(c, appengine.BlobKey(r.FormValue("blobKey")))
}

まとめ

以上で説明は終了です。ちなみに個人的な話で恐縮ですが、自分が関わっているあるプロジェクトでもファイルのアップロードにblobstoreServiceを利用しているのですが、現在数百GB以上のファイルが問題なくアップロードされ続けています。ストレージへのアップロードには失敗した記憶がありません。GAEで大きなファイルを扱う場合はぜひblobstoreをご利用ください。