192 lines
4.6 KiB
Go
192 lines
4.6 KiB
Go
|
// 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 69 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) > 69 {
|
||
|
return errors.New("mime: invalid boundary length")
|
||
|
}
|
||
|
for _, b := range boundary {
|
||
|
if 'A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' {
|
||
|
continue
|
||
|
}
|
||
|
switch b {
|
||
|
case '\'', '(', ')', '+', '_', ',', '-', '.', '/', ':', '=', '?':
|
||
|
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 {
|
||
|
return "multipart/form-data; boundary=" + w.boundary
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|