go http form file


原文链接: go http form file

golang http client如何上传和server如何接收文件 - 简书

文件上传时 增加额外的属性

curl -F“image”= @“IMAGEFILE “-F”key“=”KEY“URL

package sample 
 
 import(
“bytes”
“fmt”
“io”
“mime / multipart”
 / http“
”os“
)
 
 func上传(url,文件字符串)(err错误){
 //准备一个您将提交的表单该网址。 
 var b bytes.Buffer 
w:= multipart.NewWriter(& b)
 //添加您的镜像文件
f,err:= os.Open(file)
 if err!= nil {
 return 
} 
 defer f.Close()
 fw,err:= w.CreateFormFile(“image”,file)
 if err!= nil {
 return 
} 
 if _,err = io.Copy(fw,f); err!= nil {
 return 
} 
 //添加其他字段
 if fw,err = w.CreateFormField(“key”); err!= nil {
 return 
} 
 if _,err = fw.Write([] byte(“KEY”)); err!= nil {
 return 
} 
 //不要忘记关闭multipart writer。 
 //如果你不关闭它,你的请求将丢失终止边界。 
 w.Close()
 
 //现在你有一个表单,你可以提交它给你的处理程序。 
 req,err:= http.NewRequest(“POST”,url,& b)
 if err!= nil {
 return 
} 
 // Don不要忘记设置内容类型,这将包含边界。 
 req.Header.Set(“Content-Type”,w.FormDataContentType())
 
 //提交请求
 client:=& http.Client {} 
 res,err:= client.Do(req)
如果err!= nil {
 return 
} 
 
 //检查响应
 if res.StatusCode!= http.StatusOK {
 err = fmt.Errorf(“bad status:%s”,res.Status)
} 
 return 
} 

简介

这篇文章主要介绍使用 Go 语言来实现客户端上传文件和服务端处理接收文件的功能。

1) Client 端上传文件:Uploading files
2) Server 端接收文件: Receving files 并且 Saving files

Client

作为 Client 端,读取本地文件,并上传到服务器,通常需要调用 Server 端提供的接口,向其发出 POST 请求。

Client 端的上传方式主要有两种:
一是 Request Body 就是整个文件内容,通过请求头(即 Header )中的 Content-Type 字段来指定文件类型。
二是用 multipart 表单方式来上传

Server

作为 Server 端,需要处理接收 Client 端上传过来的文件,并保存在服务器的某个目录中。
Server 通常需要提供一个接口供 Client 端访问,当接收到 Client 的调用请求,则需要解析这个请求,并把请求中上传过来的文件读取并保存到磁盘。

相应的,Server 端在处理 Client 端请求时也要区分两种方式

  • 如果 Client 端上传的是纯二进制流数据,那么直接读取 body 写入到本地文件中即可
  • 如果 Client 端是用 multipart 表单上传的文件,那么就解析表单,再把文件写入本地。

(1)第一种,Client 把文件内容放在整个 body 中来传送

client

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    // "time"
)

const (
    Url = "http://localhost:8080/upload"
)

// body is file content
func doUpload(filepath string) {
    file, err := os.Open(filepath)
    if err != nil {
        panic(err)
    }
    defer file.Close()

    res, err := http.Post(Url, "binary/octet-stream", file) // 第二个参数用来指定 "Content-Type"
    // resp, err := http.Post(Url, "image/jpeg", file)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()
}

func main() {
    var filename string
    if len(os.Args) < 2 {
        filename = "/home/winkee/abc.txt"
    } else {
        filename = os.Args[1]
    }
    doUpload(filename)
}

server

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
)

func UploadHandler(w http.ResponseWriter, r *http.Request) {
        fmt.Println(r.Header["Content-Type"])

        // create a temporary file to hold the content
    f, err := os.OpenFile("received.tmp", os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer f.Close()

    n, err := io.Copy(f, r.Body)
    if err != nil {
        panic(err)
    }
        log.Printf("%d bytes are recieved.\n", n)
    // w.Write([]byte(fmt.Sprintf("%d bytes are recieved.\n", n)))
}

func main() {
    http.HandleFunc("/upload", UploadHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

(2)第二种,Client 通过 multipart 表单上传文件

Client 通过 multipart.Writer 的 CreateFormFile() 函数把本地文件写入 Form 中。并设置请求头的 Content-Type 为 writer.FormDataContentType(),然后发送请求。
需要注意的是:发送请求之前需要把 Writer 关闭。

package main

import (
    "bytes"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
)

func exitIfErr(err error) {
    if err != nil {
        panic(err)
    }
    return
}

func multipartUpload(destURL string, f io.Reader, fields map[string]string) (*http.Response, error) {
    body := &bytes.Buffer{}
    writer := multipart.NewWriter(body)
    fw, err := writer.CreateFormFile("file", fields["filename"])
    if err != nil {
        return nil, fmt.Errorf("CreateFormFile %v", err)
    }

    _, err = io.Copy(fw, f)
    if err != nil {
        return nil, fmt.Errorf("copying fileWriter %v", err)
    }

    for k, v := range fields {
        _ = writer.WriteField(k, v)
    }

    err = writer.Close() // close writer before POST request
    if err != nil {
        return nil, fmt.Errorf("writerClose: %v", err)
    }

    resp, err := http.Post(destURL, writer.FormDataContentType(), body)
    if err != nil {
        return nil, err
    }

    return resp, nil

    // req, err := http.NewRequest("POST", destURL, body)
    // if err != nil {
    //  return nil, err
    // }

    // req.Header.Set("Content-Type", writer.FormDataContentType())

    // if req.Close && req.Body != nil {
    //  defer req.Body.Close()
    // }

    // return http.DefaultClient.Do(req)
}

func main() {
    var filename string
    if len(os.Args) < 2 {
        filename = "/home/winkee/abc.txt"
    } else {
        filename = os.Args[1]
    }

    f, err := os.Open(filename)
    exitIfErr(err)
    defer f.Close()

    fields := map[string]string{
        "filename": filename,
    }
    res, err := multipartUpload("http://localhost:8080/uploadform", f, fields)
    exitIfErr(err)
    fmt.Println("res: ", res)
}

CreateFormFile() 函数原型:

// CreateFormFile is a convenience wrapper around CreatePart. It creates
// a new form-data header with the provided field name and file name.
func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error) {
    h := make(textproto.MIMEHeader)
    h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(fieldname), escapeQuotes(filename)))
    h.Set("Content-Type", "application/octet-stream")
    return w.CreatePart(h)
}

第二个参数,实际上是时需要带上来的 filename 域所需的值(也就是文件名),
比如,客户端发送 POST 请求进行上传时,在 Body 中就会把 filename=FieldValue 写上,
而在发送 GET 请求进行下载时,则是在 url 中把 filename 拼上。

如果想要再上传文件的时候,同时传递一些其他的参数,那么就应该使用 writer.WriteField() 函数,如下:

for k, v := range fields {
    _ = writer.WriteField(k, v)
}

这样,形成的 request body 内容如下:

image.png

注意:
如果要指定上传的每个部分的Content-Type,则需要重写multipart.Writer的CreateFormField和CreateFormFile方法

func CreateFormFile(fieldname, filename, contentType string, w *multipart.Writer) (io.Writer, error) {
    h := make(textproto.MIMEHeader)
    h.Set("Content-Disposition",
        fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
            escapeQuotes(fieldname), escapeQuotes(filename)))
    h.Set("Content-Type", contentType)
    return w.CreatePart(h)
}

server

package main

import (
    "fmt"
    "io"
    "log"
    "path/filepath"
    "net/http"
    "os"
    "strings"
)

const (
    DefaultUploadDir = "/home/winkee"   
)

func ReceiveFormFile(w http.ResponseWriter, r *http.Request) {
    // if err = req.ParseMultipartForm(2 << 10); err != nil {  
    //    status = http.StatusInternalServerError  
    //    return  
    // }  

    // r.Method should be "POST"
    file, header, err := r.FormFile("file")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    nameParts := strings.Split(header.Filename, ".")
    filename := nameParts[1]
    savedPath := filepath.Join(DefaultUploadDir, filename)
    f, err := os.OpenFile(savedPath, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        panic(err)
    }
    defer f.Close()
    _, err = io.Copy(f, file)
    if err != nil {
        panic(err)
    }

    return
}

func main() {
    http.HandleFunc("/uploadform", ReceiveFormFile)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

这里的 header.Filename 需要注意,客户端上传时指定的 filename 可能是 ../../../../../test/hello.txt 这种格式,如果不做 sanitize 的话,保存的时候直接用这个 filename 显然会出问题。

server 的另外一种处理

func ReceiveFormFile(w http.ResponseWriter, r *http.Request) {
    const _24K = (1 << 10) * 24  // 24 MB
    if err = req.ParseMultipartForm(_24K); nil != err {  
        status = http.StatusInternalServerError  
        return  
    }  
    for _, fheaders := range req.MultipartForm.File {  
        for _, hdr := range fheaders {  
            // open uploaded  
            var infile multipart.File  
            if infile, err = hdr.Open(); nil != err {  
                status = http.StatusInternalServerError  
                return  
            }  
            // open destination  
            var outfile *os.File  
            if outfile, err = os.Create("./uploaded/" + hdr.Filename); nil != err {  
                status = http.StatusInternalServerError  
                return  
            }  
            // 32K buffer copy  
            var written int64  
            if written, err = io.Copy(outfile, infile); nil != err {  
                status = http.StatusInternalServerError  
                return  
            }  
            res.Write([]byte("uploaded file:" + hdr.Filename + ";length:" + strconv.Itoa(int(written))))  
        }  
    }  
}

解析

http 包中的 Request 结构体提供了 ParseForm() 和 ParseMultipartForm() 两个函数。
在理解它们之间的区别之前,先来看一下 Request 结构体的定义,如下:

type Request struct {   
    // Form contains the parsed form data, including both the URL
    // field's query parameters and the POST or PUT form data.
    // This field is only available after ParseForm is called.
    // The HTTP client ignores Form and uses Body instead.
    Form url.Values

    // PostForm contains the parsed form data from POST, PATCH,
    // or PUT body parameters.
    //
    // This field is only available after ParseForm is called.
    // The HTTP client ignores PostForm and uses Body instead.
    PostForm url.Values

    // MultipartForm is the parsed multipart form, including file uploads.
    // This field is only available after ParseMultipartForm is called.
    // The HTTP client ignores MultipartForm and uses Body instead.
    MultipartForm *multipart.Form
    ...
}

可见,Request 结构体中,定义了 3 个不同类型的 Form 相关的数据
Form 是一个通用的数据类型 url.Values,原型如下:

type Values map[string][]string

1) 对于 Form 和 PostForm 来说,一个是 GET url raw 请求中带的参数,另一个是在 POST body 中带的参数,格式都一样,比如 ?name=xxx&age=18 等。服务端在接收到请求时,通过调用 r.ParseForm()来获得客户端传上来的请求参数。
2) 对于这个 MultipartForm,是用户客户端上传文件时使用的,服务端收到客户端的请求时,通过调用 r.ParseMultipartForm() 函数来获得相应的文件内容。r.ParseMultipartForm() 在内部会调用 r.ParseForm() 来进行部分解析。

multipart.Form 则是在 url.Values 的基础上,再添加了一个包含了文件信息的数据类型,原型如下:

// Form is a parsed multipart form.
// Its File parts are stored either in memory or on disk,
// and are accessible via the *FileHeader's Open method.
// Its Value parts are stored as strings.
// Both are keyed by field name.
type Form struct {
    Value map[string][]string
    File  map[string][]*FileHeader
}

通常,服务端在收到客户端上传的文件请求时,先调用 r.ParseMultipartForm(maxMemorySize) ,然后调用 r.FormFile("uploadfile") 来获得其中的对应控件名的文件(注:multipartForm 可以包含多个文件)。
实际上, r.FormFile(") 内部也会调用 r.ParseMultipartForm(),因此,即使不先调用 r.ParseMultipartForm(), 而直接调用 r.FormFile("") 也是可以的。
但是,如果文件太大的话,可能会出错,因此最好还是先调用 r.ParseMultipartForm() 控制读入内存的大小。

默认的最大是 10 M,也就是:

maxFormSize = int64(10 << 20)
`