github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/apiserver/httpattachment/attachment.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // Package httpattachment provides facilities for attaching a streaming
     5  // blob of data and associated metadata to an HTTP API request,
     6  // and for reading that blob on the server side.
     7  package httpattachment
     8  
     9  import (
    10  	"bytes"
    11  	"encoding/json"
    12  	"io"
    13  	"mime"
    14  	"mime/multipart"
    15  	"net/http"
    16  	"net/textproto"
    17  	"strings"
    18  
    19  	"github.com/juju/errors"
    20  	"github.com/juju/juju/apiserver/params"
    21  )
    22  
    23  // NewBody returns an HTTP request body and content type
    24  // suitable for using to make an HTTP request containing
    25  // the given attached body data and JSON-marshaled metadata.
    26  //
    27  // The name parameter is used to identify the attached "file", so
    28  // a filename is an appropriate value.
    29  func NewBody(attached io.ReadSeeker, meta interface{}, name string) (body io.ReadSeeker, contentType string, err error) {
    30  	var parts bytes.Buffer
    31  
    32  	// Set up the multi-part portion of the body.
    33  	writer := multipart.NewWriter(&parts)
    34  
    35  	// Set the metadata part.
    36  	header := make(textproto.MIMEHeader)
    37  	header.Set("Content-Disposition", `form-data; name="metadata"`)
    38  	header.Set("Content-Type", params.ContentTypeJSON)
    39  	part, err := writer.CreatePart(header)
    40  	if err != nil {
    41  		return nil, "", errors.Trace(err)
    42  	}
    43  	if err := json.NewEncoder(part).Encode(meta); err != nil {
    44  		return nil, "", errors.Trace(err)
    45  	}
    46  
    47  	// Set the attached part.
    48  	_, err = writer.CreateFormFile("attached", name)
    49  	if err != nil {
    50  		return nil, "", errors.Trace(err)
    51  	}
    52  
    53  	// We don't actually write the reader's data to the part.
    54  	// Instead We use a chained reader to facilitate streaming
    55  	// directly from the reader.
    56  	//
    57  	// Technically this is boundary-breaking, as the knowledge of
    58  	// how to make multipart archives should be kept to the
    59  	// mime/multipart package, but doing it this way means we don't
    60  	// need to return a Writer which would be harder to turn into
    61  	// a ReadSeeker.
    62  	return newMultiReaderSeeker(
    63  		bytes.NewReader(parts.Bytes()),
    64  		attached,
    65  		strings.NewReader("\r\n--"+writer.Boundary()+"--\r\n"),
    66  	), writer.FormDataContentType(), nil
    67  }
    68  
    69  type multiReaderSeeker struct {
    70  	readers []io.ReadSeeker
    71  	index   int
    72  }
    73  
    74  // mewMultiReaderSeeker returns an io.ReadSeeker implementation that
    75  // reads from all the given readers in turn. Its Seek method can be used
    76  // to seek to the start, but returns an error if used to seek anywhere
    77  // else (this corresponds with the needs of httpbakery.Client.DoWithBody
    78  // which needs to re-read the body when retrying the request).
    79  func newMultiReaderSeeker(readers ...io.ReadSeeker) *multiReaderSeeker {
    80  	return &multiReaderSeeker{
    81  		readers: readers,
    82  	}
    83  }
    84  
    85  // Read implements io.Reader.Read.
    86  func (r *multiReaderSeeker) Read(buf []byte) (int, error) {
    87  	if r.index >= len(r.readers) {
    88  		return 0, io.EOF
    89  	}
    90  	n, err := r.readers[r.index].Read(buf)
    91  	if err == io.EOF {
    92  		r.index++
    93  		err = nil
    94  	}
    95  	return n, err
    96  }
    97  
    98  // Read implements io.Seeker.Seek. It can only be used to seek to the
    99  // start.
   100  func (r *multiReaderSeeker) Seek(offset int64, whence int) (int64, error) {
   101  	if offset != 0 || whence != 0 {
   102  		return 0, errors.New("cannot only seek to the start of multipart reader")
   103  	}
   104  	for _, reader := range r.readers {
   105  		if _, err := reader.Seek(0, 0); err != nil {
   106  			return 0, errors.Trace(err)
   107  		}
   108  	}
   109  	r.index = 0
   110  	return 0, nil
   111  }
   112  
   113  // Get extracts the attached file and its metadata from the multipart
   114  // data in the request. The metadata is JSON-unmarshaled into the value
   115  // pointed to by metaResult.
   116  func Get(req *http.Request, metaResult interface{}) (io.ReadCloser, error) {
   117  	ctype := req.Header.Get("Content-Type")
   118  	mediaType, cParams, err := mime.ParseMediaType(ctype)
   119  	if err != nil {
   120  		return nil, errors.Annotate(err, "while parsing content type header")
   121  	}
   122  
   123  	if !strings.HasPrefix(mediaType, "multipart/") {
   124  		return nil, errors.Errorf("expected multipart Content-Type, got %q", mediaType)
   125  	}
   126  	reader := multipart.NewReader(req.Body, cParams["boundary"])
   127  
   128  	// Extract the metadata.
   129  	part, err := reader.NextPart()
   130  	if err != nil {
   131  		if err == io.EOF {
   132  			return nil, errors.New("missing metadata")
   133  		}
   134  		return nil, errors.Trace(err)
   135  	}
   136  
   137  	if err := checkContentType(part.Header, params.ContentTypeJSON); err != nil {
   138  		return nil, errors.Trace(err)
   139  	}
   140  	if err := json.NewDecoder(part).Decode(metaResult); err != nil {
   141  		return nil, errors.Trace(err)
   142  	}
   143  
   144  	// Extract the archive.
   145  	part, err = reader.NextPart()
   146  	if err != nil {
   147  		if err == io.EOF {
   148  			return nil, errors.New("missing archive")
   149  		}
   150  		return nil, errors.Trace(err)
   151  	}
   152  	if err := checkContentType(part.Header, params.ContentTypeRaw); err != nil {
   153  		return nil, errors.Trace(err)
   154  	}
   155  	// We're not going to worry about verifying that the file matches the
   156  	// metadata (e.g. size, checksum).
   157  	archive := part
   158  
   159  	// We are going to trust that there aren't any more attachments after
   160  	// the file. If there are, we ignore them.
   161  
   162  	return archive, nil
   163  }
   164  
   165  func checkContentType(h textproto.MIMEHeader, expected string) error {
   166  	ctype := h.Get("Content-Type")
   167  	if ctype != expected {
   168  		return errors.Errorf("expected Content-Type %q, got %q", expected, ctype)
   169  	}
   170  	return nil
   171  }