Echo web framework binding FormFile

Issue

I am using the Echo framework for a post endpoint that accepts the form data. I am using a Struct as a binding model to extract the form data. my binding model and upload handler code looks like below.

 type FormModel struct {
    ID string                `form:"ID"`
    FirstName string                `form:"FirstName"`
    File      *multipart.FileHeader `form:"myFileName"`
}

func (cs *handler) uploadForm(c echo.Context) error {
s := new(FormModel)
if err := c.Bind(s); err != nil {
    return nil
}

fileHandler, err := c.FormFile("myFileName")

I am able to get the form Values like ID and FirstName with binding. But I am not able to get the file during binding. I have to use fileHandler, err := c.FormFile("myFileName") to get the file. is there any way to get the file Info with in the binding model?

Solution

Echo does not support binding multipart.Form.File data by default, you need to re-binder to implement the interface.

An additional layer of bind FormFile is encapsulated for echo Bind. If the attribute type of a structure pointer type is *multipart.FileHeader or []*multipart.FileHeader, this attribute will be set to the value of FormFile using reflection.

I probably realized the function. I haven’t used echo and didn’t test it, but the idea is correct.

last update: add example and fix code to build. Thanks @vicTROLLA for pointing out the typeMultipartFileHeader type definition error.

package main

import (
    "bytes"
    "fmt"
    "github.com/labstack/echo"
    "io/ioutil"
    "mime/multipart"
    "net/http"
    "reflect"
    "strings"
    "time"
)

var (
    typeMultipartFileHeader      = reflect.TypeOf((*multipart.FileHeader)(nil))
    typeMultipartSliceFileHeader = reflect.TypeOf(([]*multipart.FileHeader)(nil))
)

type File struct {
    File  *multipart.FileHeader   `form:"file"`
    Files []*multipart.FileHeader `form:"files"`
}

func main() {
    app := echo.New()
    // warp bind suppet bind FormFile
    app.Binder = NewBindFile(app.Binder)
    app.Any("/file", func(ctx echo.Context) error {
        var file File
        err := ctx.Bind(&file)
        if err != nil {
            return err
        }

        readfile := func(file *multipart.FileHeader) {
            f, err := file.Open()
            if err != nil {
                fmt.Printf("open %s error: %s\n", file.Filename, err.Error())
                return
            }
            body, err := ioutil.ReadAll(f)
            fmt.Printf("read file %s error: %v body: %s\n", file.Filename, err, body)
        }

        readfile(file.File)
        for _, file := range file.Files {
            readfile(file)
        }
        return err
    })

    go func() {
        time.Sleep(200 * time.Millisecond)
        buf := bytes.NewBuffer(nil)
        w := multipart.NewWriter(buf)
        part, _ := w.CreateFormFile("file", "file")
        part.Write([]byte("this one file"))
        part, _ = w.CreateFormFile("files", "files")
        part.Write([]byte("fils part 1"))
        part, _ = w.CreateFormFile("files", "files")
        part.Write([]byte("fils part 2"))
        part, _ = w.CreateFormFile("files", "files")
        part.Write([]byte("fils part 3"))
        part, _ = w.CreateFormFile("files", "files")
        part.Write([]byte("fils part 4"))
        w.Close()
        http.Post("http://localhost:1323/file", w.FormDataContentType(), buf)
    }()

    app.Start(":1323")
}

type BindFunc func(interface{}, echo.Context) error

func (fn BindFunc) Bind(i interface{}, ctx echo.Context) error {
    return fn(i, ctx)
}

func NewBindFile(b echo.Binder) echo.Binder {
    return BindFunc(func(i interface{}, ctx echo.Context) error {
        err := b.Bind(i, ctx)
        if err == nil {
            ctype := ctx.Request().Header.Get(echo.HeaderContentType)
            // if bind form
            if strings.HasPrefix(ctype, echo.MIMEApplicationForm) || strings.HasPrefix(ctype, echo.MIMEMultipartForm) {
                // get form files
                var form *multipart.Form
                form, err = ctx.MultipartForm()
                if err == nil {
                    err = EchoBindFile(i, ctx, form.File)
                }
            }
        }
        return err
    })
}

func EchoBindFile(i interface{}, ctx echo.Context, files map[string][]*multipart.FileHeader) error {
    iValue := reflect.Indirect(reflect.ValueOf(i))
    // check bind type is struct pointer
    if iValue.Kind() != reflect.Struct {
        return fmt.Errorf("BindFile input not is struct pointer, indirect type is %s", iValue.Type().String())
    }

    iType := iValue.Type()
    for i := 0; i < iType.NumField(); i++ {
        fType := iType.Field(i)
        // check canset field
        fValue := iValue.Field(i)
        if !fValue.CanSet() {
            continue
        }
        // revc type must *multipart.FileHeader or []*multipart.FileHeader
        switch fType.Type {
        case typeMultipartFileHeader:
            file := getFiles(files, fType.Name, fType.Tag.Get("form"))
            if len(file) > 0 {
                fValue.Set(reflect.ValueOf(file[0]))
            }
        case typeMultipartSliceFileHeader:
            file := getFiles(files, fType.Name, fType.Tag.Get("form"))
            if len(file) > 0 {
                fValue.Set(reflect.ValueOf(file))
            }
        }
    }
    return nil
}

func getFiles(files map[string][]*multipart.FileHeader, names ...string) []*multipart.FileHeader {
    for _, name := range names {
        file, ok := files[name]
        if ok {
            return file
        }
    }
    return nil
}

Answered By – eudore

Answer Checked By – Senaida (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.