iOS/Swiftで画像を送信し、GAE/GoからGCSに画像を保存する

# はじめに
[GCS](https://cloud.google.com/appengine/docs/standard/go/googlecloudstorageclient/understanding-storage-features)に画像を保存するのに結構ハマったので、簡単なサンプルでまとめる。[^1]

「iOS/Swift+[APIKit](https://github.com/ishkawa/APIKit)」と「GAE/Go+[gin](https://github.com/gin-gonic/gin)」のサンプルですが、ライブラリ等は適宜読み換えてください。

# GAE/Go
## 前提
Google Cloud Storageの[セットアップ](https://cloud.google.com/appengine/docs/standard/go/googlecloudstorageclient/setting-up-cloud-storage)を済ませていること。

## 実装
エラーハンドリングは省略しています

```gae.go
import (
	"net/http"
	"github.com/gin-gonic/gin"
)

func init() {
    router := gin.Default()
    http.Handle("/",getRouter())
}

func getRouter() *gin.Engine {
    router := gin.Default()
    router.GET("/upload/images", UploadImage)
    return router
}
```

```uploadImage.go
import (
	"io/ioutil"

	"cloud.google.com/go/storage"
	"github.com/gin-gonic/gin"
	"google.golang.org/appengine"
	"google.golang.org/appengine/file"
)

func UploadImage(c *gin.Context) {
    // 送られてきた画像データを取得する
    fileHeader, err := c.FormFile("testImage")
    imageFile, err := fileHeader.Open()
    
    // appenigineのContextを取得
    ctx := appengine.NewContext(c.Request)

    // Storage上のbucket名を取得
    bucket, err := file.DefaultBucketName(ctx)
    
    // Storageを扱うclientを取得
    client, err := storage.NewClient(ctx)
    
    // Writerの生成
    savePath := "hogehoge.png"  //「icons/hoge.png」とか書ける
    writer := client.Bucket(bucket).Object(savePath).NewWriter(ctx)
    writer.ContentType = "image/png"
    
    // []byteとして画像を取得
    byteContainer, err := ioutil.ReadAll(imageFile) // ioutil.ReadXXXは重いので要注意

    // 書き込み、保存する
    _, err := wc.Write(byteContainer)
    err = wc.Close()
}
```

## 画像のURL
画像ファイルのURLは、こんな感じで取れる

```go
func GetUserImageURL(ctx context.Context, pathInBucket string) string {
	const cloudStorageHOST = "https://storage.googleapis.com/"
	projectID := appengine.AppID(ctx)
	const projectDomain = ".appspot.com"
	return cloudStorageHOST + projectID + projectDomain + "/" + pathInBucket
}
```

# クライアント(iOS)
適当な画像をローカルのGAEにPOSTする。
nilチェックは省略しています。

```main.swift
func uploadImage() {
    let url = URL(string: "https://is1-ssl.mzstatic.com/image/thumb/Purple115/v4/f7/c4/0d/f7c40d38-759f-ae55-571a-3790b2d3aca5/source/0x0ss-85.png")!
    let data = NSData(contentsOf: url)!
    let request = SaveImageRequest(ImageData: data as Data)
    Session.send(request) { result in print(result) }
}
```

```SaveImageRequest.swift
// APIKit+Codableの構成
import APIKit

struct SaveImageResponse: Codable { //省略。受け取るレスポンスがあれば定義する }

struct SaveImageRequest: APIKit.Request {
    typealias Response = SaveImageResponse
    
    var ImageData: Data
    
    let baseURL: URL = URL(string: "http://localhost:8080")!
    var path: String {return "/upload/images"}
    let method: APIKit.HTTPMethod = .post
    
    var bodyParameters: BodyParameters? {
        return MultipartFormDataBodyParameters(
            parts: [MultipartFormDataBodyParameters.Part(
                data: ImageData,
                name: "testImage",
                mimeType: "image/png",
                fileName: "testImage.png")])
    }  
}
```

# 実行！
0) **`gcloud auth login`**と**`gcloud auth application-default login`**を叩き、<font color="ORANGE">**認証**を完了させる</font>
1) バケットを指定してサーバーを起動
   `dev_appserver.py <app.yamlのpath> --default_gcs_bucket_name <バケット名>`
2) iOSからリクエストを送信 (あるいはcurl等)
3) GCSをブラウザあるいはgsutilコマンドで確認し、画像が保存されているのを確かめる


# 参考
- Google公式リファレンス
    - [GoDoc Google Cloud Storage](https://godoc.org/cloud.google.com/go/storage)
    - [Setting Up Google Cloud Storage](https://cloud.google.com/appengine/docs/standard/go/googlecloudstorageclient/setting-up-cloud-storage)
    - [Reading and Writing to Google Cloud Storage](https://cloud.google.com/appengine/docs/standard/go/googlecloudstorageclient/read-write-to-cloud-storage)
    - [Cloud Storageの認証](https://cloud.google.com/storage/docs/authentication?hl=ja#acd-init)
    - [App Engine と Google Cloud Storage のサンプル](https://cloud.google.com/go/getting-started/using-cloud-storage#uploading_to_cloud_storage)
- [gin](https://github.com/gin-gonic/gin) 
    - [upload file sample code](https://github.com/gin-gonic/gin/blob/master/examples/upload-file/multiple/main.go)
- [APIKit](https://github.com/ishkawa/APIKit)
   - [APIKit Issue "How to send BodyParameters"](https://github.com/ishkawa/APIKit/issues/235)


[^1]: ちなみに、[Google Blobstore](https://cloud.google.com/appengine/docs/standard/go/blobstore/)は非推奨となってる。