static.go
1package pages
2
3import (
4 "crypto/sha256"
5 "embed"
6 "encoding/base32"
7 "io/fs"
8 "log/slog"
9 "path/filepath"
10 "strings"
11)
12
13var toHashCache map[string]string
14var fromHashCache map[string]string
15
16func init() {
17 toHashCache = make(map[string]string)
18 fromHashCache = make(map[string]string)
19}
20
21//go:embed static/* static/**/*
22var static embed.FS
23
24type fsys struct {
25 static embed.FS
26}
27
28var StaticFS fs.FS = &fsys{static: static}
29
30func (f *fsys) Open(name string) (fs.File, error) {
31 nname := unhashStatic(name)
32 if filepath.IsAbs(nname) {
33 nname = filepath.Join(".", nname)
34 }
35 file, err := f.static.Open(nname)
36 if err != nil {
37 return nil, err
38 }
39 return file, nil
40}
41
42func hashStatic(name string) string {
43 if cached, ok := toHashCache[name]; ok {
44 return cached
45 }
46 nname := filepath.Join(".", name)
47 data, err := static.ReadFile(nname)
48 if err != nil {
49 slog.Error("error reading static file", "name", nname)
50 return name
51 }
52 h := sha256.Sum256(data)
53 hash := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(h[:])
54 dir, file := filepath.Split(name)
55 fileBase := strings.TrimRight(file, filepath.Ext(file))
56 hashedName := filepath.Join(dir, fileBase+"-"+hash[0:9]+filepath.Ext(file))
57 toHashCache[name] = hashedName
58 fromHashCache[strings.TrimLeft(hashedName, "/")] = name
59 return hashedName
60}
61
62func unhashStatic(name string) string {
63 if cached, ok := fromHashCache[name]; ok {
64 return cached
65 }
66 parts := strings.Split(name, "-")
67 if len(parts) != 2 {
68 return name
69 }
70 originalName := parts[0] + filepath.Ext(name)
71 fromHashCache[name] = originalName
72 return originalName
73}
74
75var _ fs.FS = &fsys{}