| 1 | package gallery |
| 2 | |
| 3 | import ( |
| 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. |
| 31 | type assetSource struct { |
| 32 | Name string |
| 33 | Body func(s *site) ([]byte, error) |
| 34 | } |
| 35 | |
| 36 | var 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. |
| 40 | func 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. |
| 48 | func (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. |
| 88 | func (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. |
| 102 | func 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. |
| 110 | func (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. |
| 123 | func (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. |
| 132 | func (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. |
| 142 | func 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 | |