| // Copyright 2011 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package multipart | |
| import ( | |
| "bytes" | |
| "crypto/rand" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "net/textproto" | |
| "sort" | |
| "strings" | |
| ) | |
| // A Writer generates multipart messages. | |
| type Writer struct { | |
| w io.Writer | |
| boundary string | |
| lastpart *part | |
| } | |
| // NewWriter returns a new multipart Writer with a random boundary, | |
| // writing to w. | |
| func NewWriter(w io.Writer) *Writer { | |
| return &Writer{ | |
| w: w, | |
| boundary: randomBoundary(), | |
| } | |
| } | |
| // Boundary returns the Writer's boundary. | |
| func (w *Writer) Boundary() string { | |
| return w.boundary | |
| } | |
| // SetBoundary overrides the Writer's default randomly-generated | |
| // boundary separator with an explicit value. | |
| // | |
| // SetBoundary must be called before any parts are created, may only | |
| // contain certain ASCII characters, and must be non-empty and | |
| // at most 70 bytes long. | |
| func (w *Writer) SetBoundary(boundary string) error { | |
| if w.lastpart != nil { | |
| return errors.New("mime: SetBoundary called after write") | |
| } | |
| // rfc2046#section-5.1.1 | |
| if len(boundary) < 1 || len(boundary) > 70 { | |
| return errors.New("mime: invalid boundary length") | |
| } | |
| end := len(boundary) - 1 | |
| for i, b := range boundary { | |
| if 'A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' { | |
| continue | |
| } | |
| switch b { | |
| case '\'', '(', ')', '+', '_', ',', '-', '.', '/', ':', '=', '?': | |
| continue | |
| case ' ': | |
| if i != end { | |
| continue | |
| } | |
| } | |
| return errors.New("mime: invalid boundary character") | |
| } | |
| w.boundary = boundary | |
| return nil | |
| } | |
| // FormDataContentType returns the Content-Type for an HTTP | |
| // multipart/form-data with this Writer's Boundary. | |
| func (w *Writer) FormDataContentType() string { | |
| b := w.boundary | |
| // We must quote the boundary if it contains any of the | |
| // tspecials characters defined by RFC 2045, or space. | |
| if strings.ContainsAny(b, `()<>@,;:\"/[]?= `) { | |
| b = `"` + b + `"` | |
| } | |
| return "multipart/form-data; boundary=" + b | |
| } | |
| func randomBoundary() string { | |
| var buf [30]byte | |
| _, err := io.ReadFull(rand.Reader, buf[:]) | |
| if err != nil { | |
| panic(err) | |
| } | |
| return fmt.Sprintf("%x", buf[:]) | |
| } | |
| // CreatePart creates a new multipart section with the provided | |
| // header. The body of the part should be written to the returned | |
| // Writer. After calling CreatePart, any previous part may no longer | |
| // be written to. | |
| func (w *Writer) CreatePart(header textproto.MIMEHeader) (io.Writer, error) { | |
| if w.lastpart != nil { | |
| if err := w.lastpart.close(); err != nil { | |
| return nil, err | |
| } | |
| } | |
| var b bytes.Buffer | |
| if w.lastpart != nil { | |
| fmt.Fprintf(&b, "\r\n--%s\r\n", w.boundary) | |
| } else { | |
| fmt.Fprintf(&b, "--%s\r\n", w.boundary) | |
| } | |
| keys := make([]string, 0, len(header)) | |
| for k := range header { | |
| keys = append(keys, k) | |
| } | |
| sort.Strings(keys) | |
| for _, k := range keys { | |
| for _, v := range header[k] { | |
| fmt.Fprintf(&b, "%s: %s\r\n", k, v) | |
| } | |
| } | |
| fmt.Fprintf(&b, "\r\n") | |
| _, err := io.Copy(w.w, &b) | |
| if err != nil { | |
| return nil, err | |
| } | |
| p := &part{ | |
| mw: w, | |
| } | |
| w.lastpart = p | |
| return p, nil | |
| } | |
| var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") | |
| func escapeQuotes(s string) string { | |
| return quoteEscaper.Replace(s) | |
| } | |
| // CreateFormFile is a convenience wrapper around CreatePart. It creates | |
| // a new form-data header with the provided field name and file name. | |
| func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error) { | |
| h := make(textproto.MIMEHeader) | |
| h.Set("Content-Disposition", | |
| fmt.Sprintf(`form-data; name="%s"; filename="%s"`, | |
| escapeQuotes(fieldname), escapeQuotes(filename))) | |
| h.Set("Content-Type", "application/octet-stream") | |
| return w.CreatePart(h) | |
| } | |
| // CreateFormField calls CreatePart with a header using the | |
| // given field name. | |
| func (w *Writer) CreateFormField(fieldname string) (io.Writer, error) { | |
| h := make(textproto.MIMEHeader) | |
| h.Set("Content-Disposition", | |
| fmt.Sprintf(`form-data; name="%s"`, escapeQuotes(fieldname))) | |
| return w.CreatePart(h) | |
| } | |
| // WriteField calls CreateFormField and then writes the given value. | |
| func (w *Writer) WriteField(fieldname, value string) error { | |
| p, err := w.CreateFormField(fieldname) | |
| if err != nil { | |
| return err | |
| } | |
| _, err = p.Write([]byte(value)) | |
| return err | |
| } | |
| // Close finishes the multipart message and writes the trailing | |
| // boundary end line to the output. | |
| func (w *Writer) Close() error { | |
| if w.lastpart != nil { | |
| if err := w.lastpart.close(); err != nil { | |
| return err | |
| } | |
| w.lastpart = nil | |
| } | |
| _, err := fmt.Fprintf(w.w, "\r\n--%s--\r\n", w.boundary) | |
| return err | |
| } | |
| type part struct { | |
| mw *Writer | |
| closed bool | |
| we error // last error that occurred writing | |
| } | |
| func (p *part) close() error { | |
| p.closed = true | |
| return p.we | |
| } | |
| func (p *part) Write(d []byte) (n int, err error) { | |
| if p.closed { | |
| return 0, errors.New("multipart: can't write to finished part") | |
| } | |
| n, err = p.mw.w.Write(d) | |
| if err != nil { | |
| p.we = err | |
| } | |
| return | |
| } |