參考: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"