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"

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM