~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/resource/api/upload.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
}