DRYな備忘録

Don't Repeat Yourself.

【GCP】AppEngine GoからCloudStorage上にファイルをREADしたりWRITEしたり

前々回のエントリでは、GAE/GoがWebサーバとしてちゃんと動くことが確認できたし、前回のエントリでは、GAE/Goからメールを送ることが確認できたので、今回はGAEからGoogleCloudStorage上にファイルをアップしたりそれを読んだりしてみたい。

参考

サンプル読んだ感じざっくりとした流れ

  1. *http.Requestから、AppEngineのContextを取得する *1
  2. Contextから、DefaultBucketNameを取得する *2
    • AppEngineへのリクエストなので、app_idに紐付いたbucket名を取得するんではないかとおもわれる
    • GetDefaultBucketName
    • そうっぽい
  3. Contextから、GCSのクライアントを初期化する *3
    • まあお作法とかAPIのフォーマットとかが詰まってんだろうな
    • defer client.Close() 注意
  4. いろいろできる模様 *4
    1. ファイルの作成 *5
      • クライアントにバケット名を食わせて、*BucketHandleというのを取得し
      • BucketHandleにオブジェクト名を食わせて、*ObjectHandleというのを取得し
      • ObjectHandleにContextを食わせると、io.WriteCloserなものが得られるっぽい
        • この時点でいわゆるローカルでos.Fileを扱うのとなんら変わらない感じにできたので、あとはWriteすればいいと思う
      • os.Create的なことはしなくていいのかな。ObjectHandleをつくった段階でObjectがあるていでやっていいっぽいな
    2. ファイルの読み込み *6
      • 同様にclient.Bucket(name).Object(objName).NewReader(ctx)ってして
      • io.ReadCloserなものが得られるっぽいので、あとはos.Fileと同様

書いてみる

サンプル

まずは、プロジェクトのダッシュボードから、CloudStorageを追加。デフォルトでBucketが2つできるはず。

サンプルは、これAppEngine GoでHello,Worldやってみたログ - DRYな備忘録をベースに、分かりやすいようにだらだらと書いた

package cloudstoragex

import (
    "io/ioutil"
    "net/http"

    "google.golang.org/appengine"
    "google.golang.org/appengine/file"
    "google.golang.org/cloud/storage"

    "github.com/otiai10/marmoset"
)

const objname = "foo/bar/baz.txt"

func init() {

    marmoset.LoadViews("./views")

    router := marmoset.NewRouter()

    // READするほう
    router.GET("/", func(w http.ResponseWriter, r *http.Request) {

        // Requestから、Contextを取得
        ctx := appengine.NewContext(r)

        // Contextから、default bucket nameを取得
        bucketname, err := file.DefaultBucketName(ctx)
        if err != nil {
            marmoset.Render(w).HTML("index", map[string]interface{}{"error": "Failed to get bucket name: " + err.Error()})
            return
        }

        // Contextから、Clientの取得
        client, err := storage.NewClient(ctx)
        if err != nil {
            marmoset.Render(w).HTML("index", map[string]interface{}{"error": "Failed to get client: " + err.Error()})
            return
        }

        // クライアントにバケット名を食わせてBucketHandleを取得し、
        // それにオブジェクト名を食わせてObjectHandleを取得し、
        // そこにContextを食わせてObjectへのReaderを取得
        reader, err := client.Bucket(bucketname).Object(objname).NewReader(ctx)
        if err != nil {
            marmoset.Render(w).HTML("index", map[string]interface{}{"error": "Failed to get reader: " + err.Error()})
            return
        }
        defer reader.Close()

        // CloudStorage上のObjectの、コンテンツの読み込み
        body, err := ioutil.ReadAll(reader)
        if err != nil {
            marmoset.Render(w).HTML("index", map[string]interface{}{"error": err.Error()})
            return
        }

        marmoset.Render(w).HTML("index", map[string]interface{}{
            "objname": objname,
            "content": string(body),
        })
    })

    // WRITEするほう
    router.POST("/", func(w http.ResponseWriter, r *http.Request) {

        // まずはリクエストパラメータのmultipartのパースとファイルオブジェクトの取得
        f, h, err := r.FormFile("foo")
        if err != nil {
            marmoset.Render(w).HTML("index", map[string]interface{}{"error": "Failed to extract file : " + err.Error()})
            return
        }

        ctx := appengine.NewContext(r)

        // {{{ このへんはGETと同様なのでコメント割愛
        bucketname, err := file.DefaultBucketName(ctx)
        if err != nil {
            marmoset.Render(w).HTML("index", map[string]interface{}{"error": "Failed to get bucket name: " + err.Error()})
            return
        }

        client, err := storage.NewClient(ctx)
        if err != nil {
            marmoset.Render(w).HTML("index", map[string]interface{}{"error": "Failed to get client: " + err.Error()})
            return
        }
        // }}}

        // アップロードされたファイルのコンテンツを読む
        body, err := ioutil.ReadAll(f)
        if err != nil {
            marmoset.Render(w).HTML("index", map[string]interface{}{"error": "Failed to read uploaded file: " + err.Error()})
            return
        }

        // Writer取得
        writer := client.Bucket(bucketname).Object(objname).NewWriter(ctx)
        writer.ContentType = "text/plain"
        defer writer.Close()

        // コンテンツを書き込む
        if _, err := writer.Write(body); err != nil {
            marmoset.Render(w).HTML("index", map[string]interface{}{"error": "Failed to write to object: " + err.Error()})
            return
        }

        marmoset.Render(w).HTML("index", map[string]interface{}{
            "success": "Successfully uploaded file: " + h.Filename,
            "objname": h.Filename,
            "content": string(body),
        })
    })

    http.Handle("/", router)
}

ローカルで動かすとReadで401とか返ってくるのはさておき、とりあえずデプロイしたら動いた。

f:id:otiai10:20160403013906p:plain

ソースコード

github.com

雑感

  • 開発段階で、ローカルからもGCSにRead/Writeしたいんだけど、どうすんだろ
  • staging.{app_id}っていうbucketも同時につくられる
    • これを指定するのは手動でstaging.プレフィックスをつければいいんだろうか
    • GetDefaultBucketNameがあるんだから、GetStagingBucketNameみたいなのあるのかな?

DRYな備忘録として