1package web
2
3import (
4 "crypto/sha256"
5 "embed"
6 "encoding/hex"
7 "fmt"
8 "io/fs"
9 "path"
10 "strings"
11)
12
13//go:embed static
14var staticFS embed.FS
15
16var assetTypes = map[string]string{
17 ".css": "text/css; charset=utf-8",
18 ".js": "text/javascript; charset=utf-8",
19 ".svg": "image/svg+xml",
20}
21
22type asset struct {
23 contentType string
24 etag string
25 body []byte
26}
27
28type assetNames struct {
29 CSS string
30 JS string
31 Icon string
32}
33
34func (n assetNames) under(prefix string) assetNames {
35 move := func(p string) string {
36 if p == "" {
37 return ""
38 }
39 return prefix + strings.TrimPrefix(p, "/static/")
40 }
41 return assetNames{CSS: move(n.CSS), JS: move(n.JS), Icon: move(n.Icon)}
42}
43
44type assetTable struct {
45 byPath map[string]asset
46 names assetNames
47}
48
49func loadAssets(fsys fs.FS) (assetTable, error) {
50 files, err := fs.Glob(fsys, "static/*")
51 if err != nil {
52 return assetTable{}, err
53 }
54 t := assetTable{byPath: map[string]asset{}}
55 for _, f := range files {
56 body, err := fs.ReadFile(fsys, f)
57 if err != nil {
58 return assetTable{}, err
59 }
60 ext := path.Ext(f)
61 ct, ok := assetTypes[ext]
62 if !ok {
63 return assetTable{}, fmt.Errorf("static asset %s has no known content type", f)
64 }
65 sum := sha256.Sum256(body)
66 hash := hex.EncodeToString(sum[:])[:12]
67 stem := strings.TrimSuffix(path.Base(f), ext)
68 p := "/static/" + stem + "." + hash + ext
69 t.byPath[p] = asset{contentType: ct, etag: `"` + hash + `"`, body: body}
70 switch path.Base(f) {
71 case "tug.css":
72 t.names.CSS = p
73 case "enhance.js":
74 t.names.JS = p
75 case "favicon.svg":
76 t.names.Icon = p
77 }
78 }
79 if t.names.CSS == "" || t.names.Icon == "" {
80 return assetTable{}, fmt.Errorf("the stylesheet and the favicon are required static assets")
81 }
82 return t, nil
83}