Raw
 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}