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 }