goa で multipart/form-data を扱う

multipart/form-data という MIME タイプがあります。これは主に HTML フォームの内容をサーバに対して POST する際に用いられます。

goa の Request Context には http.Request が埋まっているので、そこから MultipartReader()ParseMultipartForm() を呼び出すことで multipart/form-data を扱うことができます。以下は goa の公式サンプル収録されているコードです。

func (c *ImageController) Upload(ctx *app.UploadImageContext) error {
    // Assumes the image is under multipart section named "file"
    reader, err := ctx.MultipartReader()
    if err != nil {
        return goa.ErrBadRequest("failed to load multipart request: %s", err)
    }
    if reader == nil {
        return goa.ErrBadRequest("not a multipart request")
    }
    var images []*app.ImageMedia
    for {
        p, err := reader.NextPart()
        if err == io.EOF {
            break
        }
        if err != nil {
            return goa.ErrBadRequest("failed to load part: %s", err)
        }
        f, err := os.OpenFile("./images/"+p.FileName(), os.O_WRONLY|os.O_CREATE, 0666)
        if err != nil {
            return fmt.Errorf("failed to save file: %s", err) // causes a 500 response
        }
        defer f.Close()
        io.Copy(f, p)
        data := c.saveImage(p.FileName())
        images = append(images, &app.ImageMedia{ID: data.ID, Filename: data.Filename, UploadedAt: data.UploadedAt})
    }
    return ctx.OK(images)
}

multipart.Reader を介して、各 part の内容を取り出しています。

このように、従来から goa では multipart/form-data を扱うことができたのですが、これはあくまでサーバ実装の話です。 design (設計) の中で multipart/form-data であることを明示するための DSL は用意されていなかったので、 Swagger にも formData として定義を出力することはできませんでした。

しかし今回、この機能に関する MultipartForm という新しい DSL が追加になりました。今後は design レベルで multipart/form-data を取り扱うことができます。

github.com

MultipartForm の使用例について、まずは design から見てみましょう。

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

var ProfilePayload = Type("ProfilePayload", func() {
    Attribute("name", String, "Name")
    Attribute("birthday", DateTime, "Birthday")
    Required("name", "birthday")
})

MultipartFormAction DSL の中で使用します。これによって goa は当該 Action の Payload を multipart/form-data として扱うようになります。

これを元に出力される Swagger 定義は以下のようになります。

  /profiles:
    post:
      description: Post accepts a multipart form encoded request
      operationId: profiles#submit
      parameters:
      - description: Birthday
        in: formData
        name: birthday
        required: true
        type: string
      - description: Name
        in: formData
        name: name
        required: true
        type: string
      produces:
      - application/vnd.goa.example.form
      responses:
        "200":
          description: OK
          schema:
            $ref: '#/definitions/ResultMedia'
      schemes:
      - http
      summary: submit profiles
      tags:
      - profiles

parameters に書かれている各パラメータが in: formData になっています。これは OpenAPI Specification Version 2 で規定されている form-data を表す定義になります。

最後に controllers の実装です。冒頭に記述した通り、従来は multipart をパースして各 part をひとつずつ取り出す必要がありました。今回の機能追加後、コードは以下の形になります。

func (c *ProfilesController) Submit(ctx *app.SubmitProfilesContext) error {
    res := &app.ResultMedia{
        Name:     ctx.Payload.Name,
        Birthday: ctx.Payload.Birthday,
    }
    return ctx.OK(res)
}

これだけです。 multipart のパースは、このコードに到達する前に goa が自動で行います。各 part の内容は既に ctx.Payload のフィールドにセットされているので、それを使ったコードを書くだけで良いのです。

より詳細な例は以下をご覧ください。

github.com