1
// Copyright 2016 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
4
package v5 // import "gopkg.in/juju/charmstore.v5-unstable/internal/v5"
14
"github.com/juju/httprequest"
16
"gopkg.in/juju/charm.v6-unstable/resource"
17
"gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
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"
24
// POST id/resource/name
25
// https://github.com/juju/charmstore/blob/v5-unstable/docs/API.md#post-idresourcesname
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
32
// TODO(ericsnow) Support DELETE to remove a resource (like serveArchive)?
35
return h.serveDownloadResource(id, w, req)
37
return h.serveUploadResource(id, w, req)
39
return errgo.WithCausef(nil, params.ErrMethodNotAllowed, "%s not allowed", req.Method)
43
func (h *ReqHandler) serveDownloadResource(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
44
rid, err := parseResourceId(strings.TrimPrefix(req.URL.Path, "/"))
46
return errgo.WithCausef(err, params.ErrNotFound, "")
48
ch, err := h.entityChannel(id)
50
return errgo.Mask(err, errgo.Is(params.ErrNotFound))
52
r, err := h.Store.ResolveResource(id, rid.Name, rid.Revision, ch)
54
return errgo.Mask(err, errgo.Is(params.ErrNotFound))
56
blob, err := h.Store.OpenResourceBlob(r)
58
return errgo.Notef(err, "cannot open resource blob")
62
setArchiveCacheControl(w.Header(), h.isPublic(id))
63
header.Set(params.ContentHashHeader, blob.Hash)
65
// TODO(rog) should we set connection=close here?
66
// See https://codereview.appspot.com/5958045
67
serveContent(w, req, blob.Size, blob)
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")
75
name := strings.TrimPrefix(req.URL.Path, "/")
76
if !validResourceName(name) {
77
return badRequestf(nil, "invalid resource name")
79
hash := req.Form.Get("hash")
81
return badRequestf(nil, "hash parameter not specified")
83
if req.ContentLength == -1 {
84
return badRequestf(nil, "Content-Length not specified")
86
e, err := h.Cache.Entity(&id.URL, charmstore.FieldSelector("charmmeta"))
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))
92
r, ok := e.CharmMeta.Resources[name]
94
return errgo.WithCausef(nil, params.ErrForbidden, "resource %q not found in charm metadata", name)
96
if r.Type != resource.TypeFile {
97
return errgo.WithCausef(nil, params.ErrForbidden, "non-file resource types not supported")
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)
107
rdoc, err := h.Store.UploadResource(id, name, req.Body, hash, req.ContentLength)
109
return errgo.Mask(err)
111
return httprequest.WriteJSON(w, http.StatusOK, ¶ms.ResourceUploadResponse{
112
Revision: rdoc.Revision,
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" {
122
ch, err := h.entityChannel(id)
124
return nil, errgo.Mask(err)
126
resources, err := h.Store.ListResources(id, ch)
128
return nil, errgo.Mask(err)
130
results := make([]params.Resource, len(resources))
131
for i, res := range resources {
132
result, err := fromResourceDoc(res, entity.CharmMeta.Resources)
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)
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.
152
return nil, errgo.Mask(err)
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" {
161
rid, err := parseResourceId(strings.TrimPrefix(path, "/"))
163
return nil, errgo.WithCausef(err, params.ErrNotFound, "")
165
ch, err := h.entityChannel(id)
167
return nil, errgo.Mask(err)
169
doc, err := h.Store.ResolveResource(id, rid.Name, rid.Revision, ch)
171
if errgo.Cause(err) != params.ErrNotFound || rid.Revision != -1 {
172
return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound))
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))
181
doc = &mongodoc.Resource{
186
result, err := fromResourceDoc(doc, entity.CharmMeta.Resources)
188
return nil, errgo.Mask(err, errgo.Is(params.ErrNotFound))
193
func fromResourceDoc(doc *mongodoc.Resource, resources map[string]resource.Meta) (*params.Resource, error) {
194
meta, ok := resources[doc.Name]
196
return nil, errgo.WithCausef(nil, params.ErrNotFound, "resource %q not found in charm", doc.Name)
198
r := ¶ms.Resource{
201
Type: meta.Type.String(),
203
Description: meta.Description,
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
211
rawHash, err := hex.DecodeString(doc.BlobHash)
213
return nil, errgo.Notef(err, "cannot decode blob hash %q", doc.BlobHash)
216
r.Fingerprint = rawHash
217
r.Revision = doc.Revision
221
func parseResourceId(path string) (mongodoc.ResourceRevision, error) {
222
i := strings.Index(path, "/")
224
return mongodoc.ResourceRevision{
229
revno, err := strconv.Atoi(path[i+1:])
231
return mongodoc.ResourceRevision{}, errgo.Newf("malformed revision number")
234
return mongodoc.ResourceRevision{}, errgo.Newf("negative revision number")
236
return mongodoc.ResourceRevision{
242
func validResourceName(name string) bool {
243
// TODO we should probably be more restrictive than this.
244
return !strings.Contains(name, "/")