Johnman.md

プログラミングのことや個人的なことを書きます。たぶん。

Goa v3でmultipart/form-dataを扱うために必要なこと

これは Go 2 Advent Calendar 2020 の16日目の記事です。

はじめに

先日、Goaを使って実装している API で Goa v1 から v3 へバージョンアップを行ないました。

Goa では v1 と v2/v3 の間で大きく仕様が変わっっており、ところどころ詰まる点があります。 特に、multipart/form-data を使ってファイルアップロードをしたいというような場合に、実装が大きく変わって若干詰まったのでご紹介します。

Goaでのmultipart/form-dataを使ったファイルアップロード

まず、Goa v1 と v3 でどのように違いがあるかを紹介します。

v1

v1 では、design で MultipartForm() を呼び出すことで multipart/form-data を扱うことができるようになります。

また、ファイルを扱う際には File 型というプリミティブ型があり、これをペイロードの型として設定することで、multipart/form-data で送られたファイルを扱うことができるようになります。

var _ = Resource("profiles", func() {
    Action("submit", func() {
        Routing(POST("profiles"))
        Payload(ProfilePayload)
        MultipartForm() // Uses "multipart/form-data" encoding
        Description("Post accepts a multipart form encoded request")
        Response(OK, ResultMedia)
    })
})

var ProfilePayload = Type("ProfilePayload", func() {
    Attribute("name", String, "Name")
    Attribute("birthday", DateTime, "Birthday")
    Attribute("icon", File, "Icon") // Attribute "icon" contains a file
    Required("name", "birthday", "icon")
})

goa.design

v3

v3 では、MultipartForm()MultipartRequest() になっています。これはv1 と同様、design で呼び出せば問題ありません。

しかし、v1 に存在していた File 型は v3 では廃止されており、代わりに Bytes 型が追加されました。また、v1 では multipart のデコード処理を Goa が裏側で行なってくれていたのですが、v3 では自分でデコード処理を実装する必要があります。

multipart.go の実装

multipart/form-data のデコード処理を実装した multipart.go を準備します。Goa のリポジトリに例が載っているので、そちらの実装が参考になります。今回はファイルアップロードの例なので、Bytes型のファイル本体とString型のファイル名が渡ってくる場合の実装を示します(といっても、Goa の例とそこまで大きな違いはありません)。

github.com

func ImageCreateDecoderFunc(mr *multipart.Reader, p **image.ImageCreatePayload) error {
    payload := image.ImageCreatePayload{}
    for {
        part, err := mr.NextPart()
        if err == io.EOF {
            break
        }
        if err != nil {
            return fmt.Errorf("failed to load part. err: %s", err)
        }
        _, params, err := mime.ParseMediaType(part.Header.Get("Content-Disposition"))
        if err != nil {
            return fmt.Errorf("failed to parse part. err: %s", err)
        }
        switch params["name"] {
        case "inputFile":
            bytes, err := ioutil.ReadAll(part)
            if err != nil {
                return fmt.Errorf("cannot read binary. err: %s", err)
            }
            payload.InputFile = bytes
        case "inputName":
            name, err := ioutil.ReadAll(part)
            if err != nil {
                return fmt.Errorf("cannot read input name. err: %s", err)
            }
            payload.InputName = string(key)
        }
    }
    *p = &payload
    return nil
}

これで multipart 形式のデータをデコードすることができるようになります。

おわりに

今回は Goa v3 で multipart/form-data を扱う例を紹介しました。 Goa はv1 から v3 へ移行しようとすると結構大変な部分がありますし、そもそも Goa の design も学ぶ必要があります。しかし、非常に便利なフレームワークです。

ぜひ使ってみてはどうでしょうか。