読者です 読者をやめる 読者になる 読者になる

tjun月1日記

なんでもいいので毎月書きたい

GoogleAppEngine/GoでCloudStorageのファイルを配信する方法とハマりどころ

GCP Go GAE

半年前くらいからGAE/Goを結構触っているので、ちょっと書いておく。 今回は、CloudStorage上のファイルをGAE/Goで配信する方法。

概要

GoogleCloudStorageにおいてるファイル(例えば画像)をWebサービス上でユーザに配信したいということがある。 画像ならブラウザ上で表示させるだけでいいかもしれないし、その他のファイルならダウンロードさせたいし、複数ファイルならzipで固めて配信したい。

イメージ f:id:taka-jun:20170301160414p:plain

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.NewReaderAPI error 3 (urlfetch: UNSPECIFIED_ERROR)というエラーになります。 おそらくIncomming Requestの制限に引っかかってる。

参考: 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

というわけで、大きいファイルを扱うときは注意が必要。