go标准库的学习-mime/multipart

参考:https://studygolang.com/pkgdoc

导入方式:

import "mime/multipart"

multipart实现了MIME的multipart解析,参见RFC 2046。该实现适用于HTTP(RFC 2388)和常见浏览器生成的multipart主体。

1.什么是multipart/form-data(来自https://blog.csdn.net/five3/article/details/7181521)

multipart/form-data的基础是post请求,即基于post请求来实现的

multipart/form-data形式的post与普通post请求的不同之处体现在请求头,请求体2个部分

1)请求头:

必须包含Content-Type信息,且其值也必须规定为multipart/form-data,同时还需要规定一个内容分割符用于分割请求体中不同参数的内容(普通post请求的参数分割符默认为&,参数与参数值的分隔符为=)。具体的头信息格式如下:

Content-Type: multipart/form-data; boundary=${bound}    

其中${bound} 是一个占位符,代表我们规定的具体分割符;可以自己任意规定,但为了避免和正常文本重复了,尽量要使用复杂一点的内容。如:--0016e68ee29c5d515f04cedf6733
比如有一个body为:

--0016e68ee29c5d515f04cedf6733\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=text\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nwords words words wor=\r\nds words words =\r\nwords words wor=\r\nds words words =\r\nwords words\r\n--0016e68ee29c5d515f04cedf6733\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=submit\r\n\r\nSubmit\r\n--0016e68ee29c5d515f04cedf6733--

2)请求体:

它也是一个字符串,不过和普通post请求体不同的是它的构造方式。普通post请求体是简单的键值对连接,格式如下:

k1=v1&k2=v2&k3=v3

而multipart/form-data则是添加了分隔符、参数描述信息等内容的构造体。具体格式如下:

--${bound}
Content-Disposition: form-data; name="Filename" //第一个参数,相当于k1;然后回车;然后是参数的值,即v1
 
HTTP.pdf //参数值v1
--${bound} //其实${bound}就相当于上面普通post请求体中的&的作用
Content-Disposition: form-data; name="file000"; filename="HTTP协议详解.pdf" //这里说明传入的是文件,下面是文件提
Content-Type: application/octet-stream //传入文件类型,如果传入的是.jpg,则这里会是image/jpeg
 
%PDF-1.5
file content
%%EOF
--${bound}
Content-Disposition: form-data; name="Upload"
 
Submit Query
--${bound}--

 ⚠️都是以${bound}为开头的,并且最后一个${bound}后面要加--

 

2.当传送的是文件时

type File

type File interface {
    io.Reader
    io.ReaderAt
    io.Seeker
    io.Closer
}

File是一个接口,实现了对一个multipart信息中文件记录的访问,只能读取文件而不能写入。它的内容可以保持在内存或者硬盘中,如果保持在硬盘中,底层类型就会是*os.File。

type FileHeader

type FileHeader struct {
    Filename string
    Header   textproto.MIMEHeader
    // 内含隐藏或非导出字段
}

FileHeader描述一个multipart请求的(一个)文件记录的信息。

func (*FileHeader) Open

func (fh *FileHeader) Open() (File, error)

Open方法打开并返回其关联的文件。

举例

net/http的方法:

func (*Request) FormFile

func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)

FormFile返回以key为键查询request.MultipartForm字段(是解析好的多部件表单,包括上传的文件只有在调用ParseMultipartForm后才有效)得到结果中的第一个文件和它的信息。

如果必要,本函数会隐式调用ParseMultipartForm和ParseForm。查询失败会返回ErrMissingFile错误。

可见其返回的文件信息,即文件句柄的类型为*multipart.FileHeader。

举例:

 通过表单上传文件,在服务器端处理文件

package main 
import(
    "fmt"
    "net/http"
    "log"
    "text/template"
    "crypto/md5"
    "time"
    "io"
    "strconv"
)

func upload(w http.ResponseWriter, r *http.Request){
    fmt.Println("method", r.Method) //获得请求的方法
    
    if r.Method == "GET"{ //
        html := `<html>
<head>
<title>上传文件</title>
</head>
<body>
<form enctype="multipart/form-data" action="http://localhost:9090/upload" method="post">
    <input type="file" name="uploadfile" />
    <input type="hidden" name="token" value="{{.}}" />
    <input type="submit" value="upload" />
</form>
</body>
</html>`
        crutime := time.Now().Unix()
        h := md5.New()
        io.WriteString(h, strconv.FormatInt(crutime, 10))
        token := fmt.Sprintf("%x", h.Sum(nil))

        t := template.Must(template.New("test").Parse(html))
        t.Execute(w, token)
    }else{
        r.ParseMultipartForm(32 << 20) //表示maxMemory,调用ParseMultipart后,上传的文件存储在maxMemory大小的内存中,如果大小超过maxMemory,剩下部分存储在系统的临时文件中
        file, handler, err := r.FormFile("uploadfile") //根据input中的name="uploadfile"来获得上传的文件句柄
        if err != nil{
            fmt.Println(err)
            return
        }
        defer file.Close()
        fmt.Fprintf(w, "%v,%s", handler.Header, handler.Filename)//得到上传文件的Header和文件名
        
        //然后打开该文件
        openFile, err := handler.Open()
        if err != nil {
            fmt.Println(err)
            return
        }
        data := make([]byte, 100)
        count, err := openFile.Read(data) //读取传入文件的内容
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Printf("read %d bytes: %q\n", count, data[:count])
    }
}

func main() {
    http.HandleFunc("/upload", upload)         //设置访问的路由
    err := http.ListenAndServe(":9090", nil) //设置监听的端口
    if err != nil{
        log.Fatal("ListenAndServe : ", err)
    }
} 

终端返回:

userdeMBP:go-learning user$ go run test.go
method POST
read 34 bytes: "hello\nTest the mime/multipart file"

浏览器返回:

获取其他非文件字段信息的时候就不需要调用r.ParseForm,因为在需要的时候Go自动会去调用。而且ParseMultipartForm调用一次之后,后面再调用不会再有效果

⚠️如果上面的表单form没有设置enctype="multipart/form-data"就会报错:

Content-Type isn't multipart/form-data

上传文件主要三步处理:

  • 表单中增加enctype="multipart/form-data"
  • 服务器调用r.ParseMultipartForm,把上传的文件存储在内存和临时文件中
  • 使用r.FormFile获取文件句柄,然后对文件进行存储等处理

 

3.Reader

1)Part

type Part

type Part struct {
    // 主体的头域,如果存在,是按Go的http.Header风格标准化的,如"foo-bar"改变为"Foo-Bar"。
    // 有一个特殊情况,如果"Content-Transfer-Encoding"头的值是"quoted-printable"。
    // 该头将从本map中隐藏,而主体会在调用Read时透明的解码。
    Header textproto.MIMEHeader
    // 内含隐藏或非导出字段
}

Part代表multipart主体的单独一个记录。

func (*Part) FileName

func (p *Part) FileName() string

返回Part 的Content-Disposition 头的文件名参数。

func (*Part) FormName

func (p *Part) FormName() string

如果p的Content-Disposition头值为"form-data",则返回名字参数;否则返回空字符串。

func (*Part) Read

func (p *Part) Read(d []byte) (n int, err error)

Read方法读取一个记录的主体,也就是其头域之后到下一记录之前的部分。

func (*Part) Close

func (p *Part) Close() error

 

2)Form

type Form

type Form struct {
    Value map[string][]string
    File  map[string][]*FileHeader
}

Form是一个解析过的multipart表格。它的File参数部分保存在内存或者硬盘上,可以使用*FileHeader类型属性值的Open方法访问。它的Value 参数部分保存为字符串,两者都以属性名为键。

func (*Form) RemoveAll

func (f *Form) RemoveAll() error

删除Form关联的所有临时文件。

 

3)

type Reader

type Reader struct {
    // 内含隐藏或非导出字段
}

Reader是MIME的multipart主体所有记录的迭代器。Reader的底层会根据需要解析输入,不支持Seek。

func NewReader

func NewReader(r io.Reader, boundary string) *Reader

函数使用给出的MIME边界和r创建一个multipart读取器。

边界一般从信息的"Content-Type" 头的"boundary"属性获取。可使用mime.ParseMediaType函数解析这种头域。

 

func (*Reader) ReadForm

func (r *Reader) ReadForm(maxMemory int64) (f *Form, err error)

ReadForm解析整个multipart信息中所有Content-Disposition头的值为"form-data"的记录。它会把最多maxMemory字节的文件记录保存在内存里,其余保存在硬盘的临时文件里。

func (*Reader) NextPart

func (r *Reader) NextPart() (*Part, error)

NextPart返回multipart的下一个记录或者返回错误。如果没有更多记录会返回io.EOF。

1)举例1:

package main 
import(
    "fmt"
    "log"
    "io"
    "strings"
    "net/mail"
    "mime"
    "mime/multipart"
    "io/ioutil"
)

func main() {
    msg := &mail.Message{
        Header: map[string][]string{
            "Content-Type": []string{"multipart/mixed; boundary=foo"},
        },
        Body: strings.NewReader(
            "--foo\r\nFoo: one\r\n\r\nA section\r\n" +
                "--foo\r\nFoo: two\r\n\r\nAnd another\r\n" +
                "--foo--\r\n"),
    }
    mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type"))
    if err != nil {
        log.Fatal("1 :",err)
    }
    if strings.HasPrefix(mediaType, "multipart/") {
        mr := multipart.NewReader(msg.Body, params["boundary"])
        for {
            p, err := mr.NextPart() //p的类型为Part

            if err == io.EOF {
                return
            }
            if err != nil {
                log.Fatal("2 :",err)
            }
            slurp, err := ioutil.ReadAll(p)
            if err != nil {
                log.Fatal("3 :",err)
            }
            fmt.Printf("Part %q: %q\n", p.Header.Get("Foo"), slurp)
        }
    }

}

返回:

userdeMBP:go-learning user$ go run test.go
Part "one": "A section"
Part "two": "And another"

2)举例2:

package main 
import(
    "fmt"
    "log"
    "io"
    "strings"
    "bytes"
    "os"
    "mime/multipart"
)

const (
    fileaContents = "This is a test file."
    filebContents = "Another test file."
    textaValue    = "foo"
    textbValue    = "bar"
    boundary      = `MyBoundary`
)

const message = `
--MyBoundary
Content-Disposition: form-data; name="filea"; filename="filea.txt"
Content-Type: text/plain

` + fileaContents + `
--MyBoundary
Content-Disposition: form-data; name="fileb"; filename="fileb.txt"
Content-Type: text/plain

` + filebContents + `
--MyBoundary
Content-Disposition: form-data; name="texta"

` + textaValue + `
--MyBoundary
Content-Disposition: form-data; name="textb"

` + textbValue + `
--MyBoundary--
`

func testFile(fh *multipart.FileHeader, efn, econtent string) multipart.File{
    if fh.Filename != efn {
        fmt.Printf("filename = %q, want %q\n", fh.Filename, efn)
    }else{
        fmt.Printf("filename = %q\n", fh.Filename)
    }
    if fh.Size != int64(len(econtent)) {
        fmt.Printf("size = %d, want %d\n", fh.Size, len(econtent))
    }else{
        fmt.Printf("size = %d\n", fh.Size)
    }

    f, err := fh.Open()
    if err != nil {
        log.Fatal("opening file:", err)
    }
    b := new(bytes.Buffer)
    _, err = io.Copy(b, f) //复制文件中的内容到b中

    if err != nil {
        log.Fatal("copying contents:", err)
    }
    if g := b.String(); g != econtent {
        fmt.Printf("contents = %q, want %q\n", g, econtent)
    }else{
        fmt.Printf("contents = %q\n", g)
    }
    return f
}

func main() {
    b := strings.NewReader(strings.Replace(message, "\n", "\r\n", -1))
    r := multipart.NewReader(b, boundary)
    f, err := r.ReadForm(25) //f为Form类型
    if err != nil {
        log.Fatal("ReadForm:", err)
    }
    defer f.RemoveAll() //最后删除Form关联的所有临时文件

    //读取Form表格中的内容
    if g, e := f.Value["texta"][0], textaValue; g != e {
        fmt.Printf("texta value = %q, want %q\n", g, e)
    }else{
        fmt.Printf("texta value = %q\n", g)
    }
    if g, e := f.Value["textb"][0], textbValue; g != e {
        fmt.Printf("texta value = %q, want %q\n", g, e)
    }else{
        fmt.Printf("textb value = %q\n", g)
    }

    fd := testFile(f.File["filea"][0], "filea.txt", fileaContents) 
    if _, ok := fd.(*os.File); ok { //查看fd是否为*os.File类型
        fmt.Printf("file is *os.File, should not be")
    }
    fd.Close()
    fd = testFile(f.File["fileb"][0], "fileb.txt", filebContents)
    if _, ok := fd.(*os.File); !ok {
        fmt.Printf("file has unexpected underlying type %T", fd)
    }
    fd.Close()

}

返回:

userdeMBP:go-learning user$ go run test.go
texta value = "foo"
textb value = "bar"
filename = "filea.txt"
size = 20
contents = "This is a test file."
filename = "fileb.txt"
size = 18
contents = "Another test file."

 

4.Writer

type Writer

type Writer struct {
    // 内含隐藏或非导出字段
}

Writer类型用于生成multipart信息。

func NewWriter

func NewWriter(w io.Writer) *Writer

NewWriter函数返回一个设定了一个随机边界的Writer,数据写入w。

func (*Writer) FormDataContentType

func (w *Writer) FormDataContentType() string

方法返回w对应的HTTP multipart请求的Content-Type的值,多以multipart/form-data起始。

func (*Writer) Boundary

func (w *Writer) Boundary() string

方法返回该Writer的边界。

func (*Writer) SetBoundary

func (w *Writer) SetBoundary(boundary string) error

SetBoundary方法重写Writer默认的随机生成的边界为提供的boundary参数。方法必须在创建任何记录之前调用,boundary只能包含特定的ascii字符,并且长度应在1-69字节之间。

func (*Writer) CreatePart

func (w *Writer) CreatePart(header textproto.MIMEHeader) (io.Writer, error)

CreatePart方法使用提供的header创建一个新的multipart记录。该记录的主体应该写入返回的Writer接口。调用本方法后,任何之前的记录都不能再写入。

func (*Writer) CreateFormField

func (w *Writer) CreateFormField(fieldname string) (io.Writer, error)

CreateFormField方法使用给出的属性名调用CreatePart方法。

func (*Writer) CreateFormFile

func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error)

CreateFormFile是CreatePart方法的包装, 使用给出的属性名和文件名创建一个新的form-data头。

func (*Writer) WriteField

func (w *Writer) WriteField(fieldname, value string) error

WriteField方法调用CreateFormField并写入给出的value。

func (*Writer) Close

func (w *Writer) Close() error

Close方法结束multipart信息,并将结尾的边界写入底层io.Writer接口。

举例:

package main 
import(
    "fmt"
    "log"
    "bytes"
    "mime/multipart"
    "io/ioutil"
)

func main() {
    fileContents := []byte("my file contents")

    var b bytes.Buffer
    w := multipart.NewWriter(&b) //返回一个设定了一个随机boundary的Writer w,并将数据写入&b
    {
        part, err := w.CreateFormFile("myfile", "my-file.txt")//使用给出的属性名(对应name)和文件名(对应filename)创建一个新的form-data头,part为io.Writer类型
        if err != nil {
            fmt.Printf("CreateFormFile: %v\n", err)
        }
        part.Write(fileContents) //然后将文件的内容添加到form-data头中
        err = w.WriteField("key", "val") //WriteField方法调用CreateFormField,设置属性名(对应name)为"key",并在下一行写入该属性值对应的value = "val"
        if err != nil {
            fmt.Printf("WriteField: %v\n", err)
        }
        err = w.Close()
        if err != nil {
            fmt.Printf("Close: %v\n", err)
        }
        s := b.String()
        if len(s) == 0 {
            fmt.Println("String: unexpected empty result")
        }
        if s[0] == '\r' || s[0] == '\n' {
            log.Fatal("String: unexpected newline")
        }
        fmt.Println(s)
    }
    fmt.Println(w.Boundary()) //随机生成的boundary为284b0f2fc979a7e51d4e056a96b32ea8f8d94301287968d78723bd0113e9
    r := multipart.NewReader(&b, w.Boundary())

    part, err := r.NextPart()
    if err != nil {
        fmt.Printf("part 1: %v\n", err)
    }
    if g, e := part.FormName(), "myfile"; g != e {
        fmt.Printf("part 1: want form name %q, got %q\n", e, g)
    }else{
        fmt.Printf("part 1: want form name %q\n", e)
    }
    slurp, err := ioutil.ReadAll(part)
    if err != nil {
        fmt.Printf("part 1: ReadAll: %v\n", err)
    }
    if e, g := string(fileContents), string(slurp); e != g {
        fmt.Printf("part 1: want contents %q, got %q\n", e, g)
    }else{
        fmt.Printf("part 1: want contents %q\n", e)
    }

    part, err = r.NextPart()
    if err != nil {
        fmt.Printf("part 2: %v\n", err)
    }
    if g, e := part.FormName(), "key"; g != e {
        fmt.Printf("part 2: want form name %q, got %q\n", e, g)
    }else{
        fmt.Printf("part 2: want form name %q\n", e)
    }
    slurp, err = ioutil.ReadAll(part)
    if err != nil {
        fmt.Printf("part 2: ReadAll: %v\n", err)
    }
    if e, g := "val", string(slurp); e != g {
        fmt.Printf("part 2: want contents %q, got %q\n", e, g)
    }else{
        fmt.Printf("part 1: want contents %q\n", e)
    }

    part, err = r.NextPart() //上面的例子只有两个part
    if part != nil || err == nil {
        fmt.Printf("expected end of parts; got %v, %v\n", part, err)
    }

}

返回:

userdeMBP:go-learning user$ go run test.go
--284b0f2fc979a7e51d4e056a96b32ea8f8d94301287968d78723bd0113e9
Content-Disposition: form-data; name="myfile"; filename="my-file.txt"
Content-Type: application/octet-stream

my file contents
--284b0f2fc979a7e51d4e056a96b32ea8f8d94301287968d78723bd0113e9
Content-Disposition: form-data; name="key"

val
--284b0f2fc979a7e51d4e056a96b32ea8f8d94301287968d78723bd0113e9--

284b0f2fc979a7e51d4e056a96b32ea8f8d94301287968d78723bd0113e9
part 1: want form name "myfile"
part 1: want contents "my file contents"
part 2: want form name "key"
part 1: want contents "val"

 

posted @ 2019-02-28 23:13  慢行厚积  阅读(20921)  评论(0编辑  收藏  举报