github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/resource/api/upload.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package api 5 6 import ( 7 "fmt" 8 "io" 9 "net/http" 10 "strconv" 11 12 "github.com/juju/errors" 13 charmresource "gopkg.in/juju/charm.v6-unstable/resource" 14 "gopkg.in/juju/names.v2" 15 16 "github.com/juju/juju/resource" 17 ) 18 19 // UploadRequest defines a single upload request. 20 type UploadRequest struct { 21 // Service is the application ID. 22 Service string 23 24 // Name is the resource name. 25 Name string 26 27 // Filename is the name of the file as it exists on disk. 28 Filename string 29 30 // Size is the size of the uploaded data, in bytes. 31 Size int64 32 33 // Fingerprint is the fingerprint of the uploaded data. 34 Fingerprint charmresource.Fingerprint 35 36 // PendingID is the pending ID to associate with this upload, if any. 37 PendingID string 38 } 39 40 // NewUploadRequest generates a new upload request for the given resource. 41 func NewUploadRequest(service, name, filename string, r io.ReadSeeker) (UploadRequest, error) { 42 if !names.IsValidApplication(service) { 43 return UploadRequest{}, errors.Errorf("invalid application %q", service) 44 } 45 46 content, err := resource.GenerateContent(r) 47 if err != nil { 48 return UploadRequest{}, errors.Trace(err) 49 } 50 51 ur := UploadRequest{ 52 Service: service, 53 Name: name, 54 Filename: filename, 55 Size: content.Size, 56 Fingerprint: content.Fingerprint, 57 } 58 return ur, nil 59 } 60 61 // ExtractUploadRequest pulls the required info from the HTTP request. 62 func ExtractUploadRequest(req *http.Request) (UploadRequest, error) { 63 var ur UploadRequest 64 65 if req.Header.Get(HeaderContentLength) == "" { 66 req.Header.Set(HeaderContentLength, fmt.Sprint(req.ContentLength)) 67 } 68 69 ctype := req.Header.Get(HeaderContentType) 70 if ctype != ContentTypeRaw { 71 return ur, errors.Errorf("unsupported content type %q", ctype) 72 } 73 74 service, name := ExtractEndpointDetails(req.URL) 75 fingerprint := req.Header.Get(HeaderContentSha384) // This parallels "Content-MD5". 76 sizeRaw := req.Header.Get(HeaderContentLength) 77 pendingID := req.URL.Query().Get(QueryParamPendingID) 78 79 fp, err := charmresource.ParseFingerprint(fingerprint) 80 if err != nil { 81 return ur, errors.Annotate(err, "invalid fingerprint") 82 } 83 84 filename, err := extractFilename(req) 85 if err != nil { 86 return ur, errors.Trace(err) 87 } 88 89 size, err := strconv.ParseInt(sizeRaw, 10, 64) 90 if err != nil { 91 return ur, errors.Annotate(err, "invalid size") 92 } 93 94 ur = UploadRequest{ 95 Service: service, 96 Name: name, 97 Filename: filename, 98 Size: size, 99 Fingerprint: fp, 100 PendingID: pendingID, 101 } 102 return ur, nil 103 } 104 105 func extractFilename(req *http.Request) (string, error) { 106 disp := req.Header.Get(HeaderContentDisposition) 107 108 // the first value returned here is the media type name (e.g. "form-data"), 109 // but we don't really care. 110 _, vals, err := parseMediaType(disp) 111 if err != nil { 112 return "", errors.Annotate(err, "badly formatted Content-Disposition") 113 } 114 115 param, ok := vals[filenameParamForContentDispositionHeader] 116 if !ok { 117 return "", errors.Errorf("missing filename in resource upload request") 118 } 119 120 filename, err := decodeParam(param) 121 if err != nil { 122 return "", errors.Annotatef(err, "couldn't decode filename %q from upload request", param) 123 } 124 return filename, nil 125 } 126 127 func setFilename(filename string, req *http.Request) { 128 filename = encodeParam(filename) 129 130 disp := formatMediaType( 131 MediaTypeFormData, 132 map[string]string{filenameParamForContentDispositionHeader: filename}, 133 ) 134 135 req.Header.Set(HeaderContentDisposition, disp) 136 } 137 138 // filenameParamForContentDispositionHeader is the name of the parameter that 139 // contains the name of the file being uploaded, see mime.FormatMediaType and 140 // RFC 1867 (http://tools.ietf.org/html/rfc1867): 141 // 142 // The original local file name may be supplied as well, either as a 143 // 'filename' parameter either of the 'content-disposition: form-data' 144 // header or in the case of multiple files in a 'content-disposition: 145 // file' header of the subpart. 146 const filenameParamForContentDispositionHeader = "filename" 147 148 // HTTPRequest generates a new HTTP request. 149 func (ur UploadRequest) HTTPRequest() (*http.Request, error) { 150 // TODO(ericsnow) What about the rest of the URL? 151 urlStr := NewEndpointPath(ur.Service, ur.Name) 152 153 // TODO(natefinch): Use http.MethodPut when we upgrade to go1.5+. 154 req, err := http.NewRequest(MethodPut, urlStr, nil) 155 if err != nil { 156 return nil, errors.Trace(err) 157 } 158 159 req.Header.Set(HeaderContentType, ContentTypeRaw) 160 req.Header.Set(HeaderContentSha384, ur.Fingerprint.String()) 161 req.Header.Set(HeaderContentLength, fmt.Sprint(ur.Size)) 162 setFilename(ur.Filename, req) 163 164 req.ContentLength = ur.Size 165 166 if ur.PendingID != "" { 167 query := req.URL.Query() 168 query.Set(QueryParamPendingID, ur.PendingID) 169 req.URL.RawQuery = query.Encode() 170 } 171 172 return req, nil 173 } 174 175 type encoder interface { 176 Encode(charset, s string) string 177 } 178 179 type decoder interface { 180 Decode(s string) (string, error) 181 } 182 183 func encodeParam(s string) string { 184 return getEncoder().Encode("utf-8", s) 185 } 186 187 func decodeParam(s string) (string, error) { 188 decoded, err := getDecoder().Decode(s) 189 190 // If encoding is not required, the encoder will return the original string. 191 // However, the decoder doesn't expect that, so it barfs on non-encoded 192 // strings. To detect if a string was not encoded, we simply try encoding 193 // again, if it returns the same string, we know it wasn't encoded. 194 if err != nil && s == encodeParam(s) { 195 return s, nil 196 } 197 return decoded, err 198 }