~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/gopkg.in/juju/charmstore.v5-unstable/internal/v5/content.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 2014 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
        "archive/zip"
 
8
        "bytes"
 
9
        "fmt"
 
10
        "io"
 
11
        "net/http"
 
12
        "path"
 
13
        "strings"
 
14
 
 
15
        "github.com/juju/xml"
 
16
        "gopkg.in/errgo.v1"
 
17
        "gopkg.in/juju/charm.v6-unstable"
 
18
        "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
 
19
        "gopkg.in/juju/jujusvg.v1"
 
20
 
 
21
        "gopkg.in/juju/charmstore.v5-unstable/internal/charmstore"
 
22
        "gopkg.in/juju/charmstore.v5-unstable/internal/mongodoc"
 
23
        "gopkg.in/juju/charmstore.v5-unstable/internal/router"
 
24
)
 
25
 
 
26
// GET id/diagram.svg
 
27
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-iddiagramsvg
 
28
func (h *ReqHandler) serveDiagram(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
 
29
        if id.URL.Series != "bundle" {
 
30
                return errgo.WithCausef(nil, params.ErrNotFound, "diagrams not supported for charms")
 
31
        }
 
32
        entity, err := h.Cache.Entity(&id.URL, charmstore.FieldSelector("bundledata"))
 
33
        if err != nil {
 
34
                return errgo.Mask(err, errgo.Is(params.ErrNotFound))
 
35
        }
 
36
 
 
37
        var urlErr error
 
38
        // TODO consider what happens when a charm's SVG does not exist.
 
39
        canvas, err := jujusvg.NewFromBundle(entity.BundleData, func(id *charm.URL) string {
 
40
                // TODO change jujusvg so that the iconURL function can
 
41
                // return an error.
 
42
                absPath := h.Handler.rootPath + "/" + id.Path() + "/icon.svg"
 
43
                p, err := router.RelativeURLPath(req.RequestURI, absPath)
 
44
                if err != nil {
 
45
                        urlErr = errgo.Notef(err, "cannot make relative URL from %q and %q", req.RequestURI, absPath)
 
46
                }
 
47
                return p
 
48
        }, nil)
 
49
        if err != nil {
 
50
                return errgo.Notef(err, "cannot create canvas")
 
51
        }
 
52
        if urlErr != nil {
 
53
                return urlErr
 
54
        }
 
55
        setArchiveCacheControl(w.Header(), h.isPublic(id))
 
56
        w.Header().Set("Content-Type", "image/svg+xml")
 
57
        canvas.Marshal(w)
 
58
        return nil
 
59
}
 
60
 
 
61
// These are all forms of README files
 
62
// actually observed in charms in the wild.
 
63
var allowedReadMe = map[string]bool{
 
64
        "readme":          true,
 
65
        "readme.md":       true,
 
66
        "readme.rst":      true,
 
67
        "readme.ex":       true,
 
68
        "readme.markdown": true,
 
69
        "readme.txt":      true,
 
70
}
 
71
 
 
72
// GET id/readme
 
73
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idreadme
 
74
func (h *ReqHandler) serveReadMe(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
 
75
        entity, err := h.Cache.Entity(&id.URL, charmstore.FieldSelector("contents", "blobname"))
 
76
        if err != nil {
 
77
                return errgo.NoteMask(err, "cannot get README", errgo.Is(params.ErrNotFound))
 
78
        }
 
79
        isReadMeFile := func(f *zip.File) bool {
 
80
                name := strings.ToLower(path.Clean(f.Name))
 
81
                // This is the same condition currently used by the GUI.
 
82
                // TODO propagate likely content type from file extension.
 
83
                return allowedReadMe[name]
 
84
        }
 
85
        r, err := h.Store.OpenCachedBlobFile(entity, mongodoc.FileReadMe, isReadMeFile)
 
86
        if err != nil {
 
87
                return errgo.Mask(err, errgo.Is(params.ErrNotFound))
 
88
        }
 
89
        defer r.Close()
 
90
        setArchiveCacheControl(w.Header(), h.isPublic(id))
 
91
        io.Copy(w, r)
 
92
        return nil
 
93
}
 
94
 
 
95
// GET id/icon.svg
 
96
// https://github.com/juju/charmstore/blob/v4/docs/API.md#get-idiconsvg
 
97
func (h *ReqHandler) serveIcon(id *router.ResolvedURL, w http.ResponseWriter, req *http.Request) error {
 
98
        if id.URL.Series == "bundle" {
 
99
                return errgo.WithCausef(nil, params.ErrNotFound, "icons not supported for bundles")
 
100
        }
 
101
        entity, err := h.Cache.Entity(&id.URL, charmstore.FieldSelector("contents", "blobname"))
 
102
        if err != nil {
 
103
                return errgo.NoteMask(err, "cannot get icon", errgo.Is(params.ErrNotFound))
 
104
        }
 
105
        isIconFile := func(f *zip.File) bool {
 
106
                return path.Clean(f.Name) == "icon.svg"
 
107
        }
 
108
        r, err := h.Store.OpenCachedBlobFile(entity, mongodoc.FileIcon, isIconFile)
 
109
        if err != nil {
 
110
                logger.Errorf("cannot open icon.svg file for %v: %v", id, err)
 
111
                if errgo.Cause(err) != params.ErrNotFound {
 
112
                        return errgo.Mask(err)
 
113
                }
 
114
                setArchiveCacheControl(w.Header(), h.isPublic(id))
 
115
                w.Header().Set("Content-Type", "image/svg+xml")
 
116
                io.Copy(w, strings.NewReader(DefaultIcon))
 
117
                return nil
 
118
        }
 
119
        defer r.Close()
 
120
        w.Header().Set("Content-Type", "image/svg+xml")
 
121
        setArchiveCacheControl(w.Header(), h.isPublic(id))
 
122
        if err := processIcon(w, r); err != nil {
 
123
                if errgo.Cause(err) == errProbablyNotXML {
 
124
                        logger.Errorf("cannot process icon.svg from %s: %v", id, err)
 
125
                        io.Copy(w, strings.NewReader(DefaultIcon))
 
126
                        return nil
 
127
                }
 
128
                return errgo.Mask(err)
 
129
        }
 
130
        return nil
 
131
}
 
132
 
 
133
var errProbablyNotXML = errgo.New("probably not XML")
 
134
 
 
135
const svgNamespace = "http://www.w3.org/2000/svg"
 
136
 
 
137
// processIcon reads an icon SVG from r and writes
 
138
// it to w, making any changes that need to be made.
 
139
// Currently it adds a viewBox attribute to the <svg>
 
140
// element if necessary.
 
141
// If there is an error processing the XML before
 
142
// the first token has been written, it returns an error
 
143
// with errProbablyNotXML as the cause.
 
144
func processIcon(w io.Writer, r io.Reader) error {
 
145
        // Arrange to save all the content that we find up
 
146
        // until the first <svg> element. Then we'll stitch it
 
147
        // back together again for the actual processing.
 
148
        var saved bytes.Buffer
 
149
        dec := xml.NewDecoder(io.TeeReader(r, &saved))
 
150
        dec.DefaultSpace = svgNamespace
 
151
        found, changed := false, false
 
152
        for !found {
 
153
                tok, err := dec.Token()
 
154
                if err == io.EOF {
 
155
                        break
 
156
                }
 
157
                if err != nil {
 
158
                        return errgo.WithCausef(err, errProbablyNotXML, "")
 
159
                }
 
160
                _, found, changed = ensureViewbox(tok)
 
161
        }
 
162
        if !found {
 
163
                return errgo.WithCausef(nil, errProbablyNotXML, "no <svg> element found")
 
164
        }
 
165
        // Stitch the input back together again so we can
 
166
        // write the output without buffering it in memory.
 
167
        r = io.MultiReader(&saved, r)
 
168
        if !found || !changed {
 
169
                _, err := io.Copy(w, r)
 
170
                return err
 
171
        }
 
172
        return processNaive(w, r)
 
173
}
 
174
 
 
175
// processNaive is like processIcon but processes all of the
 
176
// XML elements. It does not return errProbablyNotXML
 
177
// on error because it may have written arbitrary XML
 
178
// to w, at which point writing an alternative response would
 
179
// be unwise.
 
180
func processNaive(w io.Writer, r io.Reader) error {
 
181
        dec := xml.NewDecoder(r)
 
182
        dec.DefaultSpace = svgNamespace
 
183
        enc := xml.NewEncoder(w)
 
184
        found := false
 
185
        for {
 
186
                tok, err := dec.Token()
 
187
                if err == io.EOF {
 
188
                        break
 
189
                }
 
190
                if err != nil {
 
191
                        return fmt.Errorf("failed to read token: %v", err)
 
192
                }
 
193
                if !found {
 
194
                        tok, found, _ = ensureViewbox(tok)
 
195
                }
 
196
                if err := enc.EncodeToken(tok); err != nil {
 
197
                        return fmt.Errorf("cannot encode token %#v: %v", tok, err)
 
198
                }
 
199
        }
 
200
        if err := enc.Flush(); err != nil {
 
201
                return fmt.Errorf("cannot flush output: %v", err)
 
202
        }
 
203
        return nil
 
204
}
 
205
 
 
206
func ensureViewbox(tok0 xml.Token) (_ xml.Token, found, changed bool) {
 
207
        tok, ok := tok0.(xml.StartElement)
 
208
        if !ok || tok.Name.Space != svgNamespace || tok.Name.Local != "svg" {
 
209
                return tok0, false, false
 
210
        }
 
211
        var width, height string
 
212
        for _, attr := range tok.Attr {
 
213
                if attr.Name.Space != "" {
 
214
                        continue
 
215
                }
 
216
                switch attr.Name.Local {
 
217
                case "width":
 
218
                        width = attr.Value
 
219
                case "height":
 
220
                        height = attr.Value
 
221
                case "viewBox":
 
222
                        return tok, true, false
 
223
                }
 
224
        }
 
225
        if width == "" || height == "" {
 
226
                // Width and/or height have not been specified,
 
227
                // so leave viewbox unspecified too.
 
228
                return tok, true, false
 
229
        }
 
230
        tok.Attr = append(tok.Attr, xml.Attr{
 
231
                Name: xml.Name{
 
232
                        Local: "viewBox",
 
233
                },
 
234
                Value: fmt.Sprintf("0 0 %s %s", width, height),
 
235
        })
 
236
        return tok, true, true
 
237
}