Go+gin on GAEでBlobstoreを使ってCloudStorageにファイルをアップする話(公式ドキュメント解説を添えて)

この記事は最終更新日から3年以上が経過しています。

はじめに

最近GAE/GOに熱があっていろいろやっていますが、CloudStorageにファイルがアップできたのでその話をします。
GAEとCloudStorageは既にセットアップが終わっているものとします。
GAEに関しては:http://www.apps-gcp.com/gae-go-gettingstart-01/
CloudStorageに関しては:https://cloud.google.com/storage/
(CloudStorageはBucketを作ったくらいしかしてないので大したことはしてないです。)

やりたいこと

GolangとGinフレームワークで構築したWebサイトで、画像をGoogleCloudStorage(以下GCS)アップロードするボタンを付ける。

アップロードするとGCSにそのファイルが格納される。
(別に必要ではないが、自分がやった方法をなぞるメモでもあるのでAjaxによって非同期的にアップロードをする)

そのURLを文字列として取得する。

(datastoreになんらかのKeyを設定してURL情報を永続化させる)

必要なこと

・GCSにアップするために使うBlobstoreの動作を理解して適切にルーティングを行えるようにする
・GolangとGinフレームワークを使ってURLごとにアクセスが分けられるようにする=ルーティング(重要)

(・一応Ajaxを使ってPOSTできるようにする。)

かなり参考にしました↓:
http://qiita.com/tomorier/items/7ee5222137651efddb33

GCSにアップするために使うBlobstoreの動作を理解する(公式ドキュメントの解説)

Blobstoreとは?

Googleが提供するキー・バリュー型のデータストアサービス。
とりあえずGCSにアップロードやダウンロードをするのにBlobstoreがあると便利かつ非常に楽ができるという感じ。
BlobstoreからBlobKeyというものが発行され、そのKeyがアップロードするファイル(Value)に1:1で結びついています。

Google App Engine includes the Blobstore service, which allows applications to serve data objects limited only by the amount of data that can be uploaded or downloaded over a single HTTP connection.

と公式ドキュメントで書かれているように、GAEでBlobstoreサービスを内包しているので、HTTPリクエストで画像をアップロードしたりダウンロードしたりなどが簡単にできるんですね。
 
なので最終的にBlobKeyさえ取得できればCloudStorageのファイルに対してダウンロードやアップロードが簡単にできるということです

 
その便利なBlobstoreなついて、個人的にクセがあるなぁと思います。
https://cloud.google.com/appengine/docs/go/blobstore/
とりあえず一度この公式サンプルを動作させてみると動きがわかるかもしれないです。
(コピペして、適当にapp.yaml書いて、goapp serveコマンドで動きます。)
それすらもめんどくさい人のためにどんな動作をしてるのか、の画像を置いておきます。

YO.png

それすらもめんどくさい人のためにどんな動作をしてるのか、の画像を置いておきます。
これを元に解説していきます。

まずアップロードURLを作ってHTMLにPOST先として埋め込む

server.go
const rootTemplateHTML = `
<html><body>
<form action="{{.}}" method="POST" enctype="multipart/form-data">
Upload File: <input type="file" name="file"><br>
<input type="submit" name="submit" value="Submit">
</form></body></html>
`
server.go
ctx := appengine.NewContext(r)
uploadURL, err := blobstore.UploadURL(ctx, "/upload", nil)
if err != nil {
        serveError(ctx, w, err)
        return
}
w.Header().Set("Content-Type", "text/html")
err = rootTemplate.Execute(w, uploadURL)
if err != nil {
        log.Errorf(ctx, "%v", err)
}

アップロードURLとは、CloudStorageにアップロードするための一時的なURLです。ここにPOSTするとGoogleCloudStorageにアップロードができます。

この一時的なURLは

http://[GAEにデプロイしたProjectID].appspot.com/_ah/upload/AMmfu6aQV3bfiaNGmzHD3IRdunw97CPTuqXnZCJsm8YHD84L4rI9YGYsx8rVRZvvGJXdF7wlfHJzmGc1yiv6RSuO4ULU74TdCD2Vp7Ygr_3Y5F6JPjxjHLHrBQSSCmsXwuNEVqhXaITJ0-7TELMnP_fzczSs271JM4xjH4LYjqQ5RHa-kV8kg1aATttriomZE25BWoXXlAgb/ALBNUaYAAAAAV2zLMTSB79AxJ9c4N5ld_KrFSjhYjok4/

みたいな感じです。このURLの文字列自体はどうでも良いです。

さて、そのアップロードURLを作るための関数
uploadURL, err := blobstore.UploadURL(ctx, "/upload", nil)
の引数に関してですが、
・第1引数:AppengineのContext
・第2引数:アップロードURLにPOSTが成功したらリダイレクトで移動するURL
・第3引数:オプション(ファイルの大きさ制限とかGCS上のどのバケットにファイルをアップロードするかなど) - 参考
となっています。

特筆すべきは、この第二引数[アップロードURLにPOSTが成功したらリダイレクトで移動するURL]です。
最初なぜ存在するかわからなかったのですが、よくよく考えてみたら例えば普通にPOSTしたらそのPOST先にページが移動しちゃいますよね。
で、今回は一時的なURLをPOST先として選んでいますので、そのままでは一時的なURLのページに飛んでしまいます。なのでそれを制御するためにこの第二引数があるということですね。
冷静に考えてみればだいぶありがたみのある引数でした。

"アップロードURLにPOSTが成功した後に移動するURL"にアクセスしたらBlobKeyを取得するようにする

後述しますが、Ginフレームワークでは予め「特定のURLにHTTPリクエスト(GET,POST,DELETEなど)を送ると特定のメッソドを呼び出す」ということを設定できます。
上述した公式サンプルではHandleFuncを使っていますが、まぁこれでもやってることはほぼ同じです。

なので今回は/uploadにGETリクエストが送られたらBlobKeyが取得できるようにしましょう。
その時使うメソッドは公式によれば

server.go
func handleUpload(w http.ResponseWriter, r *http.Request) {
        ctx := appengine.NewContext(r)
        blobs, _, err := blobstore.ParseUpload(r)
        if err != nil {
                serveError(ctx, w, err)
                return
        }
        file := blobs["file"]
        if len(file) == 0 {
                log.Errorf(ctx, "no file uploaded")
                http.Redirect(w, r, "/", http.StatusFound)
                return
        }
        http.Redirect(w, r, "/serve/?blobKey="+string(file[0].BlobKey), http.StatusFound)
}

となっています。

blobs, _, err := blobstore.ParseUpload(r)
で、Blob情報の配列を取得し、その後
file := blobs["file"]
で特定の情報を取り出します。
この"file"は
<input type="file" name="file">
のname要素で定義されたところです。

つまり、<input type="file" name="negipoyoc">みたいな名前であれば
file := blobs["negipoyoc"]で情報を取得することができます。

そしてこれにより、めでたくstring(file[0].BlobKey)でBlobKeyを取得することができました!
 
 
 
このメソッドの最後で、リダイレクトしていますがこのリダイレクト先にアクセスすると、以下のようなメソッドが実行されます。
server.go
func handleServe(w http.ResponseWriter, r *http.Request) {
blobstore.Send(w, appengine.BlobKey(r.FormValue("blobKey")))
}

これは、さきほどのBlobKeyを使ってGCSにアクセスし、Keyに紐付けられたValue(ファイル)をダウンロードするということをしています。

動作をまとめると

・アップロードURLを作る
・アップロードURLにPOSTが成功したら特定のURLに飛ぶような設定を行う
・HTMLにアップロードURLを埋め込んで、POSTを待つ。
・そのアップロードURLにPOSTが飛んできたら自動で設定したURLに飛ぶ
・その設定したURLに飛んだ時(ファイルのPOSTが来た時)Blobkeyを取得するメソッドを呼ぶ

という感じですね。

GolangとGinフレームワークを使ってURLごとにアクセスが分けられるようにする

先ほどの説明で、BlobKey作成にはルーティングが重要であることが理解ったかと思います。
で、本当は自分のシステムを踏まえて詳しく解説したかったのですが、かなり長かったので要所だけ取り出します。
以下メモのノリで書くので読みづらいかもしれないです。

やりたいこと

・ginでルーティング(アップロードページ,POSTが来た時のページ,画像ダウンロードページ)
・アップロードURLやBlobKeyを作成するメソッドを作る
・アップロードページでAjaxでPOSTできるようにする。
・データストアにBlobKeyをPutして永続化

ginでルーティング(アップロードページ,POSTが来た時のページ,画像ダウンロードページ)

server.go
func init(){
    server.GET("/submit", SubmitFormRoute)//アップロードページ
    server.GET("/tool/uploadfile", MakeBlobKey)//POSTが来た時のページ
    server.GET("/imgs/",ServeImage)//画像ダウンロードページ
}

func SubmitFormRoute(g *gin.Context) {
    url := GCSUploader.MakeURL(g)//自作の関数です。

    server.SetHTMLTemplate(templates["submit"])//submit.htmlをテンプレートに読み込ませる
    g.HTML(200, "_base.html", gin.H{
        "UploadURL" : url,
    })
}

func MakeBlobKey(g *gin.Context){
    blobKey := GCSUploader.GetBlobKey(g)
    g.String(200,blobKey)
}

func ServeImage(g * gin.Context){
    bk,_ := g.GetQuery("blobKey")
    blobstore.Send(g.Writer,appengine.BlobKey(bk))
}

これでOK。
/submitにGETアクセスがきたらsubmit.htmlを読むようにする。その時urlを埋め込む

アップロードURLやBlobKeyを作成するメソッドを作る

GCSUploader.go
package GCSUploader

import(
    "github.com/gin-gonic/gin"
    "google.golang.org/appengine"
    "google.golang.org/appengine/blobstore"
    "errors"
    "log"
    "fmt"
    "net/url"
)
//アップロードURLを作る
func MakeURL(g *gin.Context) *url.URL{
    c := appengine.NewContext(g.Request)

    option := blobstore.UploadURLOptions{
        MaxUploadBytes: 1024 * 1024 * 1024,
        StorageBucket:  "poyocbucket", //GCSに作ったバケット名を設定する
    }
    //UploadURLが成功したら/tool/uploadfileにリダイレクト(=GETリクエスト)
    uploadURL,err := blobstore.UploadURL(c,"/tool/uploadfile",&option)
          if err != nil {
            log.Fatalln("upload fail")
            return nil
        }
    return uploadURL
}
//ファイルをアップロードしてBlob情報を取得する
func UploadFile(g *gin.Context) (*blobstore.BlobInfo , error){
        blobs, _, err := blobstore.ParseUpload(g.Request)
        if err != nil {
                return nil,errors.New("Upload Error")
        }
        file := blobs["recipeimage"]

        if len(file) == 0 {
                return nil,errors.New("No File Uploaded")
        }
        return file[0] ,nil
}
//BlobKeyを文字列で返す。
func GetBlobKey(g *gin.Context) string{

    var file *blobstore.BlobInfo
    var err error
    if file,err = UploadFile(g); err!= nil {
        log.Fatalf("Error Occured %v",err)
    }
    return fmt.Sprint(file.BlobKey)
}

server.goと合わせてみると、画像がPOSTされたら最終的にBlobKeyが文字列で返却されるようになっている。
このことは以下のAjaxでPOSTできるようにしたことと関係があるので以下を引き続きどうぞ。

アップロードページでAjaxでPOSTできるようにする。

submit.html
    <form action="{{.UploadURL}}" method="POST" id="recipeform" enctype="multipart/form-data">
        <input type="file"  accept="image/jpeg,image/png" name="recipeimage" id="myfile" required>
    </form> 
    <input type="hidden" name="imgurl" id="imgurl" value="" />


<script>
$('#recipeform').submit(function() { 
    var options = { 
        success: makeUrl
    }; 
    $(this).ajaxSubmit(options);
    return false; 
});

$('#myfile').change(function() {
        if ( !this.files.length ) {
            return;
        }
        $('#recipeform').submit();
    }
);

function makeUrl(key){
    console.log(key);
    $("#imgurl").val("/imgs/?blobKey="+key);
}
</script>

Ajaxを用いている。なぜならここではかけないが他にもPOSTするものがあるから。
これは何をしているかというと、画像を選んだ瞬間にその画像ファイルだけPOSTが発生するようにしている。
で、そのSubmitが成功したらBlobKey(文字列)がかえってくるはずなので、そのKeyからURLを生成し、input hiddenのid=imgurlに格納している。
multipart/form-dataはAjaxではPluginを使わないとうまくいかないようだったので、使った。 - 参考

このURLにアクセスすると、server.goに書いたServeImageメソッドによって画像をダウンロードすることができる。
つまり、このURLは
<img src="[一連の流れで生成したGCS上の画像ファイルのURL]">
とすれば、HTMLにその画像を表示できるというわけだ。

以下はおまけ

datastoreでURLを永続化

submit.htmlで
<input type="hidden" name="imgurl" id="imgurl" value="" />
のValueにURLがセットされている。なのでこれをPOSTして、POSTされたら発動するメソッドを定義(以下)

tekitou.go
func POSTed() string {
    httpRequest := g.Request
    httpRequest.ParseForm()

    imageURL := httpRequest.Form["imgurl"][0]
 return imageURL

これでURLの文字列がとれているのであとは↓を参考にしてdatastoreにPutしてゆけ…
参考:http://knightso.hateblo.jp/entry/2014/05/19/102249

以上長々とありがとうございました。

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
コメント
この記事にコメントはありません。
あなたもコメントしてみませんか :)
すでにアカウントを持っている方は
ユーザーは見つかりませんでした