• 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
    Content-Type: text/plain; charset=ISO-8859-1
    Content-Disposition: form-data; name=text
    Content-Transfer-Encoding: quoted-printable
    
    words words words wor=
    ds words words =
    words words wor=
    ds words words =
    words words
    --0016e68ee29c5d515f04cedf6733
    Content-Type: text/plain; charset=ISO-8859-1
    Content-Disposition: form-data; name=submit
    
    Submit
    --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
    ", 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
    Test 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
    Foo: one
    
    A section
    " +
                    "--foo
    Foo: two
    
    And another
    " +
                    "--foo--
    "),
        }
        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
    ", 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
    ", fh.Filename, efn)
        }else{
            fmt.Printf("filename = %q
    ", fh.Filename)
        }
        if fh.Size != int64(len(econtent)) {
            fmt.Printf("size = %d, want %d
    ", fh.Size, len(econtent))
        }else{
            fmt.Printf("size = %d
    ", 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
    ", g, econtent)
        }else{
            fmt.Printf("contents = %q
    ", g)
        }
        return f
    }
    
    func main() {
        b := strings.NewReader(strings.Replace(message, "
    ", "
    ", -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
    ", g, e)
        }else{
            fmt.Printf("texta value = %q
    ", g)
        }
        if g, e := f.Value["textb"][0], textbValue; g != e {
            fmt.Printf("texta value = %q, want %q
    ", g, e)
        }else{
            fmt.Printf("textb value = %q
    ", 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
    ", err)
            }
            part.Write(fileContents) //然后将文件的内容添加到form-data头中
            err = w.WriteField("key", "val") //WriteField方法调用CreateFormField,设置属性名(对应name)为"key",并在下一行写入该属性值对应的value = "val"
            if err != nil {
                fmt.Printf("WriteField: %v
    ", err)
            }
            err = w.Close()
            if err != nil {
                fmt.Printf("Close: %v
    ", err)
            }
            s := b.String()
            if len(s) == 0 {
                fmt.Println("String: unexpected empty result")
            }
            if s[0] == '
    ' || s[0] == '
    ' {
                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
    ", err)
        }
        if g, e := part.FormName(), "myfile"; g != e {
            fmt.Printf("part 1: want form name %q, got %q
    ", e, g)
        }else{
            fmt.Printf("part 1: want form name %q
    ", e)
        }
        slurp, err := ioutil.ReadAll(part)
        if err != nil {
            fmt.Printf("part 1: ReadAll: %v
    ", err)
        }
        if e, g := string(fileContents), string(slurp); e != g {
            fmt.Printf("part 1: want contents %q, got %q
    ", e, g)
        }else{
            fmt.Printf("part 1: want contents %q
    ", e)
        }
    
        part, err = r.NextPart()
        if err != nil {
            fmt.Printf("part 2: %v
    ", err)
        }
        if g, e := part.FormName(), "key"; g != e {
            fmt.Printf("part 2: want form name %q, got %q
    ", e, g)
        }else{
            fmt.Printf("part 2: want form name %q
    ", e)
        }
        slurp, err = ioutil.ReadAll(part)
        if err != nil {
            fmt.Printf("part 2: ReadAll: %v
    ", err)
        }
        if e, g := "val", string(slurp); e != g {
            fmt.Printf("part 2: want contents %q, got %q
    ", e, g)
        }else{
            fmt.Printf("part 1: want contents %q
    ", e)
        }
    
        part, err = r.NextPart() //上面的例子只有两个part
        if part != nil || err == nil {
            fmt.Printf("expected end of parts; got %v, %v
    ", 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"
  • 相关阅读:
    vscode终端无法使用webpack命令
    vscode 常用配置
    git中fatal: Authentication failed的问题
    vue项目初始化步骤
    Windwos安装Node.js和npm的详细步骤
    node安装教程,全局安装vue,webpack/cli,创建一个vue项目(详细步骤)
    VsCode 自动生成文件头部注释和函数注释
    微信小程序使用字体图标
    微信小程序第三方框架
    查看最近访问的文件目录或文件
  • 原文地址:https://www.cnblogs.com/wanghui-garcia/p/10402796.html
Copyright © 2020-2023  润新知