package web import ( "crypto/sha256" "embed" "encoding/hex" "fmt" "io/fs" "path" "strings" ) //go:embed static var staticFS embed.FS var assetTypes = map[string]string{ ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".svg": "image/svg+xml", } type asset struct { contentType string etag string body []byte } type assetNames struct { CSS string JS string Icon string } func (n assetNames) under(prefix string) assetNames { move := func(p string) string { if p == "" { return "" } return prefix + strings.TrimPrefix(p, "/static/") } return assetNames{CSS: move(n.CSS), JS: move(n.JS), Icon: move(n.Icon)} } type assetTable struct { byPath map[string]asset names assetNames } func loadAssets(fsys fs.FS) (assetTable, error) { files, err := fs.Glob(fsys, "static/*") if err != nil { return assetTable{}, err } t := assetTable{byPath: map[string]asset{}} for _, f := range files { body, err := fs.ReadFile(fsys, f) if err != nil { return assetTable{}, err } ext := path.Ext(f) ct, ok := assetTypes[ext] if !ok { return assetTable{}, fmt.Errorf("static asset %s has no known content type", f) } sum := sha256.Sum256(body) hash := hex.EncodeToString(sum[:])[:12] stem := strings.TrimSuffix(path.Base(f), ext) p := "/static/" + stem + "." + hash + ext t.byPath[p] = asset{contentType: ct, etag: `"` + hash + `"`, body: body} switch path.Base(f) { case "tug.css": t.names.CSS = p case "enhance.js": t.names.JS = p case "favicon.svg": t.names.Icon = p } } if t.names.CSS == "" || t.names.Icon == "" { return assetTable{}, fmt.Errorf("the stylesheet and the favicon are required static assets") } return t, nil }