半年前くらいからGAE/Goを結構触っているので、ちょっと書いておく。
今回は、CloudStorage上のファイルをGAE/Goで配信する方法。
概要
GoogleCloudStorageにおいてるファイル(例えば画像)をWebサービス上でユーザに配信したいということがある。
画像ならブラウザ上で表示させるだけでいいかもしれないし、その他のファイルならダウンロードさせたいし、複数ファイルならzipで固めて配信したい。
イメージ

cloud.google.com/go/storageのライブラリを利用する。
以下の例では、エラー処理などは省略する。また、普段はEchoを使ってるので以下のコードは試してないし正しく動かない部分があるかも。
コード例
画像を表示させる
画像をブラウザで表示させるケース。
特に難しい部分はなく、シンプルに書ける。
検索して出て来るサンプルだとioutil.ReadAll
を使ってることもあるけど、io.Copy
で 直接http.ResponseWriter
に書き出した方がパフォーマンス的にいいと思う。
func serveImage(res http.ResponseWriter, req *http.Request) error {
ctx := appengine.NewContext(req)
cli, _ := storage.NewClient(ctx)
reader, _ := cli.Bucket(bucket).Object(path).NewReader(ctx)
defer reader.Close()
res.Header().Set("Content-Type", reader.ContentType())
res.WriteHeader(http.StatusOK)
siz, _ := io.Copy(res, reader)
return nil
}
ファイルをダウンロードさせる
基本的には上と同じだけど、Content-Dispositionヘッダを指定して画像でもダウンロードさせられるようになる。
func serveImage(res http.ResponseWriter, req *http.Request) error {
ctx := appengine.NewContext(req)
cli, _ := storage.NewClient(ctx)
reader, _ := cli.Bucket(bucket).Object(path).NewReader(ctx)
defer reader.Close()
res.Header().Set("Content-Type", reader.ContentType())
disposition := "attachment; filename=" + fileName
res.Header().Set("Content-Disposition", disposition)
res.WriteHeader(http.StatusOK)
siz, _ := io.Copy(res, reader)
return nil
}
zipで固めてダウンロードさせる
複数のファイルをダウンロードさせたい場合、ディレクトリごとダウンロードさせたい場合などに使える。
storageのReaderと httpのResponseWriterの間にzipWriterを挟む感じ。
func serveImage(res http.ResponseWriter, req *http.Request) error {
ctx := appengine.NewContext(req)
cli, _ := storage.NewClient(ctx)
res.Header().Set("Content-Type", "application/octet-stream")
disposition := "attachment; filename=" + fileName
res.Header().Set("Content-Disposition", disposition)
res.WriteHeader(http.StatusOK)
zipWriter := zip.NewWriter(res)
defer zipWriter.Close()
for _, fname := range fileNames {
path := Path(path.Join(dir), fname))
reader, _ := cli.Bucket(bucket).Object(path).NewReader(ctx)
defer reader.Close()
zipFile, err := zipWriter.Create(fname)
_, err = io.Copy(zipFile, reader)
}
return nil
}
エラー処理がないとすごくシンプルに見えるけど、実際は if err != nil {...
というコードがあちこちに入ります。
ハマりどころ
クラウドストレージ上のファイルのサイズが 32MBを越えると、storage.NewReader
でAPI error 3 (urlfetch: UNSPECIFIED_ERROR)
というエラーになります。
また、複数のファイルをZIPする場合でも、合計のサイズが32MB超えるとResponse返すところでエラーになります。
CloudStorage -> AppEngine -> Client の2箇所とも32MB制限があります。
参考: https://cloud.google.com/appengine/quotas#Requests
エラーが分かりにくいし、32MBはちょっと少ない気がする。
しかもGAE/Goのドキュメントを読むと、Streaming Responseに対応してないので、ファイルを少しずつ読んでStreaming Responseで返すってこともできません。
参考: https://cloud.google.com/appengine/docs/standard/go/how-requests-are-handled#streaming_responses
32MB以上のファイルを扱う際には、BlobStoreを使います。
BlobStoreは以下のように使えます。
func serveImage(res http.ResponseWriter, req *http.Request) error {
ctx := appengine.NewContext(req)
bpath := path.Join("/gs", fpath)
bkey, _ := blobstore.BlobKeyForFile(ctx, bpath)
disposition := "attachment; filename=" + fileName
res.Header().Set("Content-Disposition", disposition)
blobstore.Send(res, bkey)
return nil
}
参考: The blobstore package | App Engine standard environment for Go