~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/gopkg.in/juju/charmstore.v5-unstable/internal/v5/resources.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 v5 // import "gopkg.in/juju/charmstore.v5-unstable/internal/v5"
 
5
 
 
6
import (
 
7
        "encoding/hex"
 
8
        "net/http"
 
9
        "net/url"
 
10
        "path"
 
11
        "strconv"
 
12
        "strings"
 
13
 
 
14
        "github.com/juju/httprequest"
 
15
        "gopkg.in/errgo.v1"
 
16
        "gopkg.in/juju/charm.v6-unstable/resource"
 
17
        "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
 
18
 
 
19
        "gopkg.in/juju/charmstore.v5-unstable/internal/charmstore"
 
20
        "gopkg.in/juju/charmstore.v5-unstable/internal/mongodoc"
 
21
        "gopkg.in/juju/charmstore.v5-unstable/internal/router"
 
22
)
 
23
 
 
24
// POST id/resource/name
 
25
// https://github.com/juju/charmstore/blob/v5-unstable/docs/API.md#post-idresourcesname
 
26
//
 
27
// GET  id/resource/name[/revision]
 
28
// https://github.com/juju/charmstore/blob/v5-unstable/docs/API.md#get-idresourcesnamerevision
 
29
func (h *ReqHandler) serveResources(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
 
30
        // Resources are "published" using "POST id/publish" so we don't
 
31
        // support PUT here.
 
32
        // TODO(ericsnow) Support DELETE to remove a resource (like serveArchive)?
 
33
        switch req.Method {
 
34
        case "GET":
 
35
                return h.serveDownloadResource(id, w, req)
 
36
        case "POST":
 
37
                return h.serveUploadResource(id, w, req)
 
38
        default:
 
39
                return errgo.WithCausef(nil, params.ErrMethodNotAllowed, "%s not allowed", req.Method)
 
40
        }
 
41
}
 
42
 
 
43
func (h *ReqHandler) serveDownloadResource(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
 
44
        rid, err := parseResourceId(strings.TrimPrefix(req.URL.Path, "/"))
 
45
        if err != nil {
 
46
                return errgo.WithCausef(err, params.ErrNotFound, "")
 
47
        }
 
48
        ch, err := h.entityChannel(id)
 
49
        if err != nil {
 
50
                return errgo.Mask(err, errgo.Is(params.ErrNotFound))
 
51
        }
 
52
        r, err := h.Store.ResolveResource(id, rid.Name, rid.Revision, ch)
 
53
        if err != nil {
 
54
                return errgo.Mask(err, errgo.Is(params.ErrNotFound))
 
55
        }
 
56
        blob, err := h.Store.OpenResourceBlob(r)
 
57
        if err != nil {
 
58
                return errgo.Notef(err, "cannot open resource blob")
 
59
        }
 
60
        defer blob.Close()
 
61
        header := w.Header()
 
62
        setArchiveCacheControl(w.Header(), h.isPublic(id))
 
63
        header.Set(params.ContentHashHeader, blob.Hash)
 
64
 
 
65
        // TODO(rog) should we set connection=close here?
 
66
        // See https://codereview.appspot.com/5958045
 
67
        serveContent(w, req, blob.Size, blob)
 
68
        return nil
 
69
}
 
70
 
 
71
func (h *ReqHandler) serveUploadResource(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
 
72
        if id.URL.Series == "bundle" {
 
73
                return errgo.WithCausef(nil, params.ErrForbidden, "cannot upload a resource to a bundle")
 
74
        }
 
75
        name := strings.TrimPrefix(req.URL.Path, "/")
 
76
        if !validResourceName(name) {
 
77
                return badRequestf(nil, "invalid resource name")
 
78
        }
 
79
        hash := req.Form.Get("hash")
 
80
        if hash == "" {
 
81
                return badRequestf(nil, "hash parameter not specified")
 
82
        }
 
83
        if req.ContentLength == -1 {
 
84
                return badRequestf(nil, "Content-Length not specified")
 
85
        }
 
86
        e, err := h.Cache.Entity(&id.URL, charmstore.FieldSelector("charmmeta"))
 
87
        if err != nil {
 
88
                // Should never happen, as the entity will have been cached
 
89
                // when the charm URL was resolved.
 
90
                return errgo.Mask(err, errgo.Is(params.ErrNotFound))
 
91
        }
 
92
        r, ok := e.CharmMeta.Resources[name]
 
93
        if !ok {
 
94
                return errgo.WithCausef(nil, params.ErrForbidden, "resource %q not found in charm metadata", name)
 
95
        }
 
96
        if r.Type != resource.TypeFile {
 
97
                return errgo.WithCausef(nil, params.ErrForbidden, "non-file resource types not supported")
 
98
        }
 
99
        if filename := req.Form.Get("filename"); filename != "" {
 
100
                if charmExt := path.Ext(r.Path); charmExt != "" {
 
101
                        // The resource has a filename extension. Check that it matches.
 
102
                        if charmExt != path.Ext(filename) {
 
103
                                return errgo.WithCausef(nil, params.ErrForbidden, "filename extension mismatch (got %q want %q)", path.Ext(filename), charmExt)
 
104
                        }
 
105
                }
 
106
        }
 
107
        rdoc, err := h.Store.UploadResource(id, name, req.Body, hash, req.ContentLength)
 
108
        if err != nil {
 
109
                return errgo.Mask(err)
 
110
        }
 
111
        return httprequest.WriteJSON(w, http.StatusOK, &params.ResourceUploadResponse{
 
112
                Revision: rdoc.Revision,
 
113
        })
 
114
}
 
115
 
 
116
// GET id/meta/resource
 
117
// https://github.com/juju/charmstore/blob/v5-unstable/docs/API.md#get-idmetaresources
 
118
func (h *ReqHandler) metaResources(entity *mongodoc.Entity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
 
119
        if entity.URL.Series == "bundle" {
 
120
                return nil, nil
 
121
        }
 
122
        ch, err := h.entityChannel(id)
 
123
        if err != nil {
 
124
                return nil, errgo.Mask(err)
 
125
        }
 
126
        resources, err := h.Store.ListResources(id, ch)
 
127
        if err != nil {
 
128
                return nil, errgo.Mask(err)
 
129
        }
 
130
        results := make([]params.Resource, len(resources))
 
131
        for i, res := range resources {
 
132
                result, err := fromResourceDoc(res, entity.CharmMeta.Resources)
 
133
                if err != nil {
 
134
                        return nil, err
 
135
                }
 
136
                results[i] = *result
 
137
        }
 
138
        return results, nil
 
139
}
 
140
 
 
141
// GET id/meta/resource/*name*[/*revision]
 
142
// https://github.com/juju/charmstore/blob/v5-unstable/docs/API.md#get-idmetaresourcesnamerevision
 
143
func (h *ReqHandler) metaResourcesSingle(entity *mongodoc.Entity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
 
144
        data, err := h.metaResourcesSingle0(entity, id, path, flags, req)
 
145
        if err != nil {
 
146
                if errgo.Cause(err) == params.ErrNotFound {
 
147
                        logger.Infof("replacing not-found error on %s/meta/resources%s: %v (%#v)", id.URL.Path(), path, err, err)
 
148
                        // It's a not-found error; return nothing
 
149
                        // so that it's OK to use this in a bulk meta request.
 
150
                        return nil, nil
 
151
                }
 
152
                return nil, errgo.Mask(err)
 
153
        }
 
154
        return data, nil
 
155
}
 
156
 
 
157
func (h *ReqHandler) metaResourcesSingle0(entity *mongodoc.Entity, id *router.ResolvedURL, path string, flags url.Values, req *http.Request) (interface{}, error) {
 
158
        if id.URL.Series == "bundle" {
 
159
                return nil, nil
 
160
        }
 
161
        rid, err := parseResourceId(strings.TrimPrefix(path, "/"))
 
162
        if err != nil {
 
163
                return nil, errgo.WithCausef(err, params.ErrNotFound, "")
 
164
        }
 
165
        ch, err := h.entityChannel(id)
 
166
        if err != nil {
 
167
                return nil, errgo.Mask(err)
 
168
        }
 
169
        doc, err := h.Store.ResolveResource(id, rid.Name, rid.Revision, ch)
 
170
        if err != nil {
 
171
                if errgo.Cause(err) != params.ErrNotFound || rid.Revision != -1 {
 
172
                        return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound))
 
173
                }
 
174
                // The resource wasn't found and we're not asking for a specific
 
175
                // revision. If the resource actually exists in the charm metadata,
 
176
                // return a placeholder document as would be returned by
 
177
                // the /meta/resources (ListResources) endpoint.
 
178
                if _, ok := entity.CharmMeta.Resources[rid.Name]; !ok {
 
179
                        return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound))
 
180
                }
 
181
                doc = &mongodoc.Resource{
 
182
                        Name:     rid.Name,
 
183
                        Revision: -1,
 
184
                }
 
185
        }
 
186
        result, err := fromResourceDoc(doc, entity.CharmMeta.Resources)
 
187
        if err != nil {
 
188
                return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound))
 
189
        }
 
190
        return result, nil
 
191
}
 
192
 
 
193
func fromResourceDoc(doc *mongodoc.Resource, resources map[string]resource.Meta) (*params.Resource, error) {
 
194
        meta, ok := resources[doc.Name]
 
195
        if !ok {
 
196
                return nil, errgo.WithCausef(nil, params.ErrNotFound, "resource %q not found in charm", doc.Name)
 
197
        }
 
198
        r := &params.Resource{
 
199
                Name:        doc.Name,
 
200
                Revision:    -1,
 
201
                Type:        meta.Type.String(),
 
202
                Path:        meta.Path,
 
203
                Description: meta.Description,
 
204
        }
 
205
        if doc.BlobHash == "" {
 
206
                // No hash implies that there is no file (the entry
 
207
                // is just a placeholder), so we don't fill in
 
208
                // blob details.
 
209
                return r, nil
 
210
        }
 
211
        rawHash, err := hex.DecodeString(doc.BlobHash)
 
212
        if err != nil {
 
213
                return nil, errgo.Notef(err, "cannot decode blob hash %q", doc.BlobHash)
 
214
        }
 
215
        r.Size = doc.Size
 
216
        r.Fingerprint = rawHash
 
217
        r.Revision = doc.Revision
 
218
        return r, nil
 
219
}
 
220
 
 
221
func parseResourceId(path string) (mongodoc.ResourceRevision, error) {
 
222
        i := strings.Index(path, "/")
 
223
        if i == -1 {
 
224
                return mongodoc.ResourceRevision{
 
225
                        Name:     path,
 
226
                        Revision: -1,
 
227
                }, nil
 
228
        }
 
229
        revno, err := strconv.Atoi(path[i+1:])
 
230
        if err != nil {
 
231
                return mongodoc.ResourceRevision{}, errgo.Newf("malformed revision number")
 
232
        }
 
233
        if revno < 0 {
 
234
                return mongodoc.ResourceRevision{}, errgo.Newf("negative revision number")
 
235
        }
 
236
        return mongodoc.ResourceRevision{
 
237
                Name:     path[0:i],
 
238
                Revision: revno,
 
239
        }, nil
 
240
}
 
241
 
 
242
func validResourceName(name string) bool {
 
243
        // TODO we should probably be more restrictive than this.
 
244
        return !strings.Contains(name, "/")
 
245
}