github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 "mime" 10 "net/http" 11 12 "github.com/juju/errors" 13 charmresource "gopkg.in/juju/charm.v6/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 // Application is the application ID. 22 Application 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(application, name, filename string, r io.ReadSeeker) (UploadRequest, error) { 42 if !names.IsValidApplication(application) { 43 return UploadRequest{}, errors.Errorf("invalid application %q", application) 44 } 45 46 content, err := resource.GenerateContent(r) 47 if err != nil { 48 return UploadRequest{}, errors.Trace(err) 49 } 50 51 ur := UploadRequest{ 52 Application: application, 53 Name: name, 54 Filename: filename, 55 Size: content.Size, 56 Fingerprint: content.Fingerprint, 57 } 58 return ur, nil 59 } 60 61 func setFilename(filename string, req *http.Request) { 62 filename = mime.BEncoding.Encode("utf-8", filename) 63 64 disp := mime.FormatMediaType( 65 MediaTypeFormData, 66 map[string]string{FilenameParamForContentDispositionHeader: filename}, 67 ) 68 69 req.Header.Set(HeaderContentDisposition, disp) 70 } 71 72 // FilenameParamForContentDispositionHeader is the name of the parameter that 73 // contains the name of the file being uploaded, see mime.FormatMediaType and 74 // RFC 1867 (http://tools.ietf.org/html/rfc1867): 75 // 76 // The original local file name may be supplied as well, either as a 77 // 'filename' parameter either of the 'content-disposition: form-data' 78 // header or in the case of multiple files in a 'content-disposition: 79 // file' header of the subpart. 80 const FilenameParamForContentDispositionHeader = "filename" 81 82 // HTTPRequest generates a new HTTP request. 83 func (ur UploadRequest) HTTPRequest() (*http.Request, error) { 84 // TODO(ericsnow) What about the rest of the URL? 85 urlStr := NewEndpointPath(ur.Application, ur.Name) 86 87 req, err := http.NewRequest(http.MethodPut, urlStr, nil) 88 if err != nil { 89 return nil, errors.Trace(err) 90 } 91 92 req.Header.Set(HeaderContentType, ContentTypeRaw) 93 req.Header.Set(HeaderContentSha384, ur.Fingerprint.String()) 94 req.Header.Set(HeaderContentLength, fmt.Sprint(ur.Size)) 95 setFilename(ur.Filename, req) 96 97 req.ContentLength = ur.Size 98 99 if ur.PendingID != "" { 100 query := req.URL.Query() 101 query.Set(QueryParamPendingID, ur.PendingID) 102 req.URL.RawQuery = query.Encode() 103 } 104 105 return req, nil 106 }