goa で multipart/form-data を扱う (ファイルアップロード編)

前回 goa で multipart/form-data を扱う方法についてご紹介しました。

tchssk.hatenablog.com

multipart/form-data に関連してよく取り上げられるのがファイルアップロードですね。 HTML フォームを介したサーバへのファイル送信は多くのウェブサービスで利用されています。しかし前回の記事ではその話については言及しませんでした。理由は「ファイル」を表すデータ型がまだ goa に存在しなかったからです。

goa では Type DSL を使ってユーザ型を定義します。また、ユーザ型の各要素を定義するには Attribute DSL を使用します。

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

Attribute の 2 番目の引数がデータ型です。上記の StringDateTime を含む、以下 7 種類が用意されています

  • Boolean
  • Integer
  • Number
  • String
  • DateTime
  • UUID
  • Any

そして今回、新たに 8 種類目の File 型が追加されました。

github.com

では先ほどの design に File 型の要素を追加してみましょう。

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

ここから生成される Go の構造体は以下の形になります。

// ProfilePayload user type.
type ProfilePayload struct {
    // Birthday
    Birthday time.Time `form:"birthday" json:"birthday" xml:"birthday"`
    // Icon
    Icon *multipart.FileHeader `form:"icon,omitempty" json:"icon,omitempty" xml:"icon,omitempty"`
    // Name
    Name string `form:"name" json:"name" xml:"name"`
}

Icon の型が *multipart.FileHeader になっていますね。これは標準パッケージの mime/multipart で提供されている構造体で、ファイル名、 MIME ヘッダ、ファイルサイズを含んでいます。

type FileHeader struct {
        Filename string
        Header   textproto.MIMEHeader
        Size     int64
}

また、この構造体には Open() というメソッドが用意されており、ファイルを multipart.File として取得することが可能です。 multipart.Fileio.Reader などを実装しているので通常のファイルと同じようにように操作することができます。

以上を踏まえて、アップロードされたファイルをサーバのローカルディレクトリに保存する処理を controller に実装してみましょう。

func (c *ProfilesController) Submit(ctx *app.SubmitProfilesContext) error {
    res := &app.ResultMedia{
        Name:     ctx.Payload.Name,
        Birthday: ctx.Payload.Birthday,
    }
    file, err := ctx.Payload.Icon.Open()
    if err != nil {
        return err
    }
    defer file.Close()
    f, err := os.OpenFile("./icons/"+ctx.Payload.Icon.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, file)
    return ctx.OK(res)
}

前述の通り ctx.Payload.Icon*multipart.FileHeader となっているので Open()multipart.File を取得しています。あとはアップロードされたものと同じ名前のファイルを os.OpenFile() で作成し io.Copy() で書き込むだけです。

この例は既に公式サンプルにも収録されているので以下からご覧ください。

github.com

ファイルアップロードにも対応した goa が益々便利ですね!