8

I want to save image file posted by JSON.

Here is the struct of the post:

type Article struct {
    Title   string `json:"title"`
    Body string `json:"body"`
    File    []byte `json:"file"`
}

And the handler is :

   func PostHandler(c *gin.Context) {
        var err error
        var json Article
        err = c.BindJSON(&json)
        if err != nil {
            log.Panic(err)
        }

    //handle photo upload
        var filename string
        file, header, err := json.File  //assignment count mismatch: 3 = 1

        if err != nil {
            fmt.Println(err)
            filename = ""

        } else {
            data, err := ioutil.ReadAll(file)
            if err != nil {
                fmt.Println(err)
                return
            }

            filename = path.Join("media", +shared.RandString(5)+path.Ext(header.Filename))

            err = ioutil.WriteFile(filename, data, 0777)
            if err != nil {
                io.WriteString(w, err.Error())
                return
            }

        }
...

But I get

assignment count mismatch: 3 = 1

I copied the file handling part from a working multipart form handler which worked fine but apparently,

file, header, err := r.FormFile("uploadfile")

can not be translated into JSON handling.

I have looked at gin docs but could not find examples involving json file handling. So how can I fix this?

Karlom
  • 13,323
  • 27
  • 72
  • 116
  • You can use JSON to upload files if you base64 encode the file into a string, then base64 decode the string into a buffer (byte array) on the server. However, I recommend that you use the "multipart/form-data" approach instead, as it's better designed to handle image files. See my answer below for details. – tim-montague Nov 14 '20 at 02:11

2 Answers2

8

Using Gin to get an uploaded file

I think your question is "Using Gin, how do I get an uploaded file?". Most developers don't upload files with JSON encoding, which could be done but requires the file to be included as a base64 string (and increases the file size about 33%).

The common (and more efficient) practice is to upload the file using the "multipart/form-data" encoding. The code that others provided file, header, err := c.Request.FormFile("file") works, but that hijacks the underlining "net/http" package that Gin extends.

My recommendation is to use ShouldBind, but you can also use the FormFile or MultipartForm methods provided by the Gin package.

Examples below. Similar (but less detailed) explanations are also offered on the Gin page https://github.com/gin-gonic/gin#model-binding-and-validation and https://github.com/gin-gonic/gin#upload-files.


Upload one file


Clients

HTML

<form action="/upload" method="POST">
  <input type="file" name="file">
  <input type="submit">
</form>

Curl

curl -X POST http://localhost:8080/upload \
  -F "file=../path-to-file/file.zip" \
  -H "Content-Type: multipart/form-data"

Server

Go

package main

import (
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
    "io/ioutil"
)

type Form struct {
    File *multipart.FileHeader `form:"file" binding:"required"`
}

func main() {
    router := gin.Default()

    // Set a lower memory limit for multipart forms (default is 32 MiB)
    // router.MaxMultipartMemory = 8 << 20  // 8 MiB

    router.POST("/upload", func(c *gin.Context) {

        // Using `ShouldBind`
        // --------------------
        var form Form
        _ := c.ShouldBind(&form)

        // Get raw file bytes - no reader method
        // openedFile, _ := form.File.Open()
        // file, _ := ioutil.ReadAll(openedFile)

        // Upload to disk
        // `form.File` has io.reader method
        // c.SaveUploadedFile(form.File, path)
        // --------------------

        // Using `FormFile`
        // --------------------
        // formFile, _ := c.FormFile("file")

        // Get raw file bytes - no reader method
        // openedFile, _ := formFile.Open()
        // file, _ := ioutil.ReadAll(openedFile)

        // Upload to disk
        // `formFile` has io.reader method
        // c.SaveUploadedFile(formFile, path)
        // --------------------
        
        c.String(http.StatusOK, "Files uploaded")
    })

    // Listen and serve on 0.0.0.0:8080
    router.Run(":8080")
}

Upload multiple files


Clients

HTML

<form action="/upload" method="POST" multiple="multiple">
  <input type="file" name="files">
  <input type="submit">
</form>

Curl

curl -X POST http://localhost:8080/upload \
  -F "files=../path-to-file/file1.zip" \
  -F "files=../path-to-file/file2.zip" \
  -H "Content-Type: multipart/form-data"

Server

Go

package main

import (
    "github.com/gin-gonic/gin"
    "log"
    "net/http"
    "io/ioutil"
)

type Form struct {
    Files []*multipart.FileHeader `form:"files" binding:"required"`
}

func main() {
    router := gin.Default()

    // Set a lower memory limit for multipart forms (default is 32 MiB)
    // router.MaxMultipartMemory = 8 << 20  // 8 MiB

    router.POST("/upload", func(c *gin.Context) {

        // Using `ShouldBind`
        // --------------------
        var form Form
        _ := c.ShouldBind(&form)

        // for _, formFile := range form.Files {

          // Get raw file bytes - no reader method
          // openedFile, _ := formFile.Open()
          // file, _ := ioutil.ReadAll(openedFile)

          // Upload to disk
          // `formFile` has io.reader method
          // c.SaveUploadedFile(formFile, path)

        // }
        // --------------------

        // Using `MultipartForm`
        // --------------------
        // form, _ := c.MultipartForm()
        // formFiles, _ := form["files[]"]

        // for _, formFile := range formFiles {

          // Get raw file bytes - no reader method
          // openedFile, _ := formFile.Open()
          // file, _ := ioutil.ReadAll(openedFile)

          // Upload to disk
          // `formFile` has io.reader method
          // c.SaveUploadedFile(formFile, path)

        // }
        // --------------------
        
        c.String(http.StatusOK, "Files uploaded")
    })

    // Listen and serve on 0.0.0.0:8080
    router.Run(":8080")
}
tim-montague
  • 16,217
  • 5
  • 62
  • 51
2

In your code you say var json Article where type article is defined as

type Article struct {
    Title   string `json:"title"`
    Body string `json:"body"`
    File    []byte `json:"file"`
}

And File is of type []byte. Type byte doesn't return anything other than what it holds

Your Article.File is not the same as r.FormFile where FormFile is a method that returns 3 items

So file, header, err := json.File isn't file, header, err := r.FormFile("foo")

See the implementation and method description from godocs -> here

reticentroot
  • 3,612
  • 2
  • 22
  • 39
  • You should probably go through the gin documentation.. Here is the spot in the docs that shows you the method for gin's FormFile https://godoc.org/github.com/gin-gonic/gin#Context.FormFile and this deals with uploads https://godoc.org/github.com/gin-gonic/gin#Context.MultipartForm.. its all there so take your time and get to know the docs. – reticentroot Jul 15 '17 at 18:53
  • Still could not figure out how to do that. I need an example. – Karlom Jul 15 '17 at 18:56
  • I tried `file, header, err := c.FormFile(json.File)` but I get this errro: `c.FormFile undefined (type *gin.Context has no field or method FormFile)` – Karlom Jul 15 '17 at 19:05
  • Read the documentation.`func (c *Context) FormFile(name string) (*multipart.FileHeader, error)` The function FormFile requires that you pass it a string. You are passing it a byte slice. `string(json.File)` Also if *gin.Context has no field or method FormFile that's a separate question because thats included in context according to the docs. – reticentroot Jul 15 '17 at 19:07
  • I also tried `file, header, err := c.FormFile("file")` and get `: c.FormFile undefined (type *gin.Context has no field or method FormFile) ` – Karlom Jul 15 '17 at 19:08
  • It looks like this has been addressed before https://stackoverflow.com/questions/38970180/golang-gin-c-param-undefined-type-gin-context-has-no-field-or-method-param chances are either your package is wrong or the version is – reticentroot Jul 15 '17 at 19:10
  • I guess the correct way is `file, header, err := c.Request.FormFile("file")`. This does not cause errors. – Karlom Jul 15 '17 at 19:10
  • c.Request.FormFile isn't "basic" gin.. It looks like somewhere the http.Requests is being wrapped in gin.. What is the github import you are using – reticentroot Jul 15 '17 at 19:12
  • `"github.com/gin-gonic/gin"` – Karlom Jul 15 '17 at 19:13
  • Then that's very odd. If it works cheers.. to be sure you are using the latest version you can use `go get -u github.com/gin-gonic/gin` – reticentroot Jul 15 '17 at 19:14