1
// Copyright 2016 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
12
"github.com/juju/errors"
13
charmresource "gopkg.in/juju/charm.v6-unstable/resource"
14
"gopkg.in/juju/names.v2"
16
"github.com/juju/juju/resource"
19
// UploadRequest defines a single upload request.
20
type UploadRequest struct {
21
// Service is the application ID.
24
// Name is the resource name.
27
// Filename is the name of the file as it exists on disk.
30
// Size is the size of the uploaded data, in bytes.
33
// Fingerprint is the fingerprint of the uploaded data.
34
Fingerprint charmresource.Fingerprint
36
// PendingID is the pending ID to associate with this upload, if any.
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)
46
content, err := resource.GenerateContent(r)
48
return UploadRequest{}, errors.Trace(err)
56
Fingerprint: content.Fingerprint,
61
// ExtractUploadRequest pulls the required info from the HTTP request.
62
func ExtractUploadRequest(req *http.Request) (UploadRequest, error) {
65
if req.Header.Get(HeaderContentLength) == "" {
66
req.Header.Set(HeaderContentLength, fmt.Sprint(req.ContentLength))
69
ctype := req.Header.Get(HeaderContentType)
70
if ctype != ContentTypeRaw {
71
return ur, errors.Errorf("unsupported content type %q", ctype)
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)
79
fp, err := charmresource.ParseFingerprint(fingerprint)
81
return ur, errors.Annotate(err, "invalid fingerprint")
84
filename, err := extractFilename(req)
86
return ur, errors.Trace(err)
89
size, err := strconv.ParseInt(sizeRaw, 10, 64)
91
return ur, errors.Annotate(err, "invalid size")
100
PendingID: pendingID,
105
func extractFilename(req *http.Request) (string, error) {
106
disp := req.Header.Get(HeaderContentDisposition)
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)
112
return "", errors.Annotate(err, "badly formatted Content-Disposition")
115
param, ok := vals[filenameParamForContentDispositionHeader]
117
return "", errors.Errorf("missing filename in resource upload request")
120
filename, err := decodeParam(param)
122
return "", errors.Annotatef(err, "couldn't decode filename %q from upload request", param)
127
func setFilename(filename string, req *http.Request) {
128
filename = encodeParam(filename)
130
disp := formatMediaType(
132
map[string]string{filenameParamForContentDispositionHeader: filename},
135
req.Header.Set(HeaderContentDisposition, disp)
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):
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"
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)
153
// TODO(natefinch): Use http.MethodPut when we upgrade to go1.5+.
154
req, err := http.NewRequest(MethodPut, urlStr, nil)
156
return nil, errors.Trace(err)
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)
164
req.ContentLength = ur.Size
166
if ur.PendingID != "" {
167
query := req.URL.Query()
168
query.Set(QueryParamPendingID, ur.PendingID)
169
req.URL.RawQuery = query.Encode()
175
type encoder interface {
176
Encode(charset, s string) string
179
type decoder interface {
180
Decode(s string) (string, error)
183
func encodeParam(s string) string {
184
return getEncoder().Encode("utf-8", s)
187
func decodeParam(s string) (string, error) {
188
decoded, err := getDecoder().Decode(s)
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) {