rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1package gallery
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7 "io/fs"
8 "path"
9 "sort"
10 "strings"
11)
12
13// Every asset is written twice: under name.<16 hex of its sha256>.ext,
14// which every page and frame links, and under its plain name, which
15// nothing links, for hotlinks and the site's own checks. The tree is
16// served by a static edge that caches assets, and with plain names a
17// deploy could pair a new page with yesterday's cached tokens.css, a
18// page of new markup drawn by old rules. A name that changes whenever
19// the bytes do cannot be served stale.
20
21// The declaration. What the tree ships under assets/ is declared apart
22// from being written: the source's css/ and js/ files and the examples'
23// pictures, which assetNames reads off the source, and the gallery's
24// own files, which the file that owns each declares at init through
25// declareAsset. assets writes exactly the declared names, and the tree
26// gate expects exactly them, so an asset the render forgot and one it
27// wrote unasked both fail the gate.
28
29// assetSource is one declared asset of the gallery's own: its plain
30// name under assets/ and how to read its bytes.
31type assetSource struct {
32 Name string
33 Body func(s *site) ([]byte, error)
34}
35
36var declaredAssets []assetSource
37
38// declareAsset registers one of the gallery's own assets. Call it from
39// an init function in the file that owns the bytes.
40func declareAsset(name string, body func(s *site) ([]byte, error)) {
41 declaredAssets = append(declaredAssets, assetSource{Name: name, Body: body})
42}
43
44// assetNames is every plain name the tree ships under assets/, sorted:
45// css/ and js/ as the source has them, images/<picture> for every file
46// under examples/assets, and the declared ones. It reads the source and
47// the declaration only, never what has been written.
48func (s *site) assetNames() ([]string, error) {
49 var names []string
50 for _, dir := range []string{"css", "js"} {
51 err := fs.WalkDir(s.src, dir, func(p string, d fs.DirEntry, err error) error {
52 if err != nil {
53 return err
54 }
55 if !d.IsDir() {
56 names = append(names, p)
57 }
58 return nil
59 })
60 if err != nil {
61 return nil, err
62 }
63 }
64 pictures, err := fs.ReadDir(s.src, "examples/assets")
65 if err != nil {
66 return nil, err
67 }
68 for _, e := range pictures {
69 if !e.IsDir() {
70 names = append(names, "images/"+e.Name())
71 }
72 }
73 for _, a := range declaredAssets {
74 names = append(names, a.Name)
75 }
76 sort.Strings(names)
77 for i := 1; i < len(names); i++ {
78 if names[i] == names[i-1] {
79 return nil, fmt.Errorf("the asset %s is declared twice", names[i])
80 }
81 }
82 return names, nil
83}
84
85// assetBody reads one declared asset's bytes: a source file by its path,
86// a picture from examples/assets, or the gallery's own through its
87// declaration.
88func (s *site) assetBody(name string) ([]byte, error) {
89 for _, a := range declaredAssets {
90 if a.Name == name {
91 return a.Body(s)
92 }
93 }
94 if strings.HasPrefix(name, "images/") {
95 return fs.ReadFile(s.src, "examples/assets/"+strings.TrimPrefix(name, "images/"))
96 }
97 return fs.ReadFile(s.src, name)
98}
99
100// hashedName is the name an asset is linked by: its stem, sixteen hex
101// digits of the sha256 of its bytes, and its extension.
102func hashedName(name string, body []byte) string {
103 sum := sha256.Sum256(body)
104 ext := path.Ext(name)
105 return strings.TrimSuffix(name, ext) + "." + hex.EncodeToString(sum[:8]) + ext
106}
107
108// putAsset writes one asset under assets/, plain and hashed, and
109// records the hashed name for the pages to link.
110func (s *site) putAsset(name string, body []byte) error {
111 hashed := hashedName(name, body)
112 s.hashed[name] = hashed
113 if err := s.put("assets/"+name, body); err != nil {
114 return err
115 }
116 return s.put("assets/"+hashed, append([]byte(nil), body...))
117}
118
119// asset is the address a page links an asset by: the hashed name under
120// the mount. A name the tree does not ship panics, because it is a typo
121// in this package's own templates, and a page linking a file that is
122// not there renders unstyled with no other sign of it.
123func (s *site) asset(name string) string {
124 hashed, ok := s.hashed[name]
125 if !ok {
126 panic(fmt.Sprintf("gallery: no asset %q in the tree", name))
127 }
128 return s.url("assets/" + hashed)
129}
130
131// Asset is the URL of one of the gallery's assets, by its plain name.
132func (p page) Asset(name string) string { return p.s.asset(name) }
133
134// Fingerprinted are the addresses, for a tree rendered for mount, of
135// every asset written under its hashed name, sorted: each hashed twin
136// the tree holds beside its plain name, with the same bytes. The name
137// is worked out from the plain file's bytes, as putAsset works it out,
138// so a name that only looks hashed, with no plain twin or with hex
139// that is not its bytes' hash, is never listed: other bytes could be
140// written under it, and a browser told it never changes would keep
141// the old ones.
142func Fingerprinted(mount string, tree map[string][]byte) []string {
143 m := cleanMount(mount)
144 var out []string
145 for name, body := range tree {
146 plain, ok := strings.CutPrefix(name, "assets/")
147 if !ok {
148 continue
149 }
150 hashed := "assets/" + hashedName(plain, body)
151 if twin, ok := tree[hashed]; ok && string(twin) == string(body) {
152 out = append(out, m+"/"+hashed)
153 }
154 }
155 sort.Strings(out)
156 return out
157}
158