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"