admin: pre-gzip embedded static assets, add cache headers (#9918)

The admin UI served embedded static files uncompressed and without
cache headers: embed.FS has zero mod times, so no Last-Modified, no
ETag, no 304s -- every page load re-downloaded ~700KB of css/js in
full, which gets painful over slow or tunneled links.

Gzip the static tree at generation time (go generate ./weed/admin)
and embed only the compressed mirror, shrinking the binary ~1.5MB.
The handler hands the pre-compressed bytes to gzip-capable clients,
decompresses for the rest, and sets Cache-Control, per-variant
content-hash ETags and Vary so repeat loads revalidate with a 304.
bootstrap.min.css goes 232KB -> 30KB on the wire.

A drift test keeps static_gz/ in sync with static/.
This commit is contained in:
Chris Lu
2026-06-10 12:54:36 -07:00
committed by GitHub
parent c2271d59bb
commit e56a1c4c05
24 changed files with 406 additions and 17 deletions
+7 -1
View File
@@ -21,11 +21,17 @@ install-templ:
# Generate templ files
.PHONY: generate
generate: install-templ
generate: install-templ static-gz
@echo "Generating templ files..."
@templ generate ./view
@echo "Generated: $(TEMPL_GO_FILES)"
# Regenerate the gzipped static mirror embedded into the binary
.PHONY: static-gz
static-gz:
@echo "Generating gzipped static assets..."
@go run gen_static_gz.go
# Clean generated files
.PHONY: clean-templ
clean-templ:
+58
View File
@@ -0,0 +1,58 @@
//go:build ignore
// Regenerates static_gz/, the gzipped mirror of static/ that is embedded
// into the binary. Run after changing anything under static/:
//
// go generate ./weed/admin
package main
import (
"bytes"
"compress/gzip"
"fmt"
"io/fs"
"os"
"path/filepath"
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
if err := os.RemoveAll("static_gz"); err != nil {
return err
}
return filepath.WalkDir("static", func(p string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
data, err := os.ReadFile(p)
if err != nil {
return err
}
var buf bytes.Buffer
zw, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
if err != nil {
return err
}
if _, err := zw.Write(data); err != nil {
return err
}
if err := zw.Close(); err != nil {
return err
}
rel, err := filepath.Rel("static", p)
if err != nil {
return err
}
out := filepath.Join("static_gz", rel+".gz")
if err := os.MkdirAll(filepath.Dir(out), 0755); err != nil {
return err
}
return os.WriteFile(out, buf.Bytes(), 0644)
})
}
+7 -7
View File
@@ -2,13 +2,13 @@ package admin
import (
"embed"
"io/fs"
)
//go:embed static/*
var StaticFS embed.FS
//go:generate go run gen_static_gz.go
// GetStaticFS returns the embedded static filesystem
func GetStaticFS() (fs.FS, error) {
return fs.Sub(StaticFS, "static")
}
// staticGzFS is the gzipped mirror of static/, generated by gen_static_gz.go.
// Only the compressed copies are embedded; StaticHandler decompresses for
// clients that do not accept gzip.
//
//go:embed all:static_gz
var staticGzFS embed.FS
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+116
View File
@@ -0,0 +1,116 @@
package admin
import (
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"io"
"io/fs"
"net/http"
"path"
"strconv"
"strings"
"sync"
"time"
)
// Assets only change with the binary; the ETag revalidates across upgrades.
const staticCacheControl = "public, max-age=3600"
type staticAsset struct {
name string // base name, drives Content-Type in http.ServeContent
gz []byte
etag string
etagGz string
}
// identity returns the uncompressed bytes.
func (a *staticAsset) identity() ([]byte, error) {
zr, err := gzip.NewReader(bytes.NewReader(a.gz))
if err != nil {
return nil, err
}
defer zr.Close()
return io.ReadAll(zr)
}
var staticAssets = sync.OnceValue(func() map[string]*staticAsset {
assets := make(map[string]*staticAsset)
err := fs.WalkDir(staticGzFS, "static_gz", func(p string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
gz, err := fs.ReadFile(staticGzFS, p)
if err != nil {
return err
}
key := strings.TrimSuffix(strings.TrimPrefix(p, "static_gz/"), ".gz")
sum := sha256.Sum256(gz)
hash := hex.EncodeToString(sum[:8])
assets[key] = &staticAsset{
name: path.Base(key),
gz: gz,
etag: `"` + hash + `"`,
etagGz: `"` + hash + `-gz"`,
}
return nil
})
if err != nil {
panic("walk embedded static assets: " + err.Error())
}
return assets
})
// StaticHandler serves the embedded admin static assets, which are gzipped
// at generation time. Gzip-capable clients get the compressed bytes as-is;
// others get them decompressed on the fly.
func StaticHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := strings.TrimPrefix(path.Clean("/"+r.URL.Path), "/")
asset, ok := staticAssets()[key]
if !ok {
http.NotFound(w, r)
return
}
h := w.Header()
h.Set("Cache-Control", staticCacheControl)
h.Add("Vary", "Accept-Encoding")
if acceptsGzip(r) {
h.Set("Content-Encoding", "gzip")
h.Set("ETag", asset.etagGz)
// ServeContent skips Content-Length when Content-Encoding is set
if r.Header.Get("Range") == "" {
h.Set("Content-Length", strconv.Itoa(len(asset.gz)))
}
http.ServeContent(w, r, asset.name, time.Time{}, bytes.NewReader(asset.gz))
return
}
data, err := asset.identity()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
h.Set("ETag", asset.etag)
http.ServeContent(w, r, asset.name, time.Time{}, bytes.NewReader(data))
})
}
func acceptsGzip(r *http.Request) bool {
for _, part := range strings.Split(r.Header.Get("Accept-Encoding"), ",") {
token, attr, hasAttr := strings.Cut(strings.TrimSpace(part), ";")
if strings.TrimSpace(token) != "gzip" {
continue
}
if !hasAttr {
return true
}
q, ok := strings.CutPrefix(strings.TrimSpace(attr), "q=")
if !ok {
return true
}
v, err := strconv.ParseFloat(q, 64)
return err == nil && v > 0
}
return false
}
+215
View File
@@ -0,0 +1,215 @@
package admin
import (
"bytes"
"compress/gzip"
"io"
"io/fs"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
// TestStaticGzMirror fails when static_gz/ is out of sync with static/.
// Regenerate with: go generate ./weed/admin
func TestStaticGzMirror(t *testing.T) {
sources := make(map[string][]byte)
err := filepath.WalkDir("static", func(p string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
data, err := os.ReadFile(p)
if err != nil {
return err
}
rel, err := filepath.Rel("static", p)
if err != nil {
return err
}
sources[filepath.ToSlash(rel)] = data
return nil
})
if err != nil {
t.Fatal(err)
}
if len(sources) == 0 {
t.Fatal("no files under static/")
}
mirrored := 0
err = filepath.WalkDir("static_gz", func(p string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
rel, err := filepath.Rel("static_gz", p)
if err != nil {
return err
}
key := strings.TrimSuffix(filepath.ToSlash(rel), ".gz")
want, ok := sources[key]
if !ok {
t.Errorf("static_gz/%s has no source in static/", rel)
return nil
}
mirrored++
gz, err := os.ReadFile(p)
if err != nil {
return err
}
zr, err := gzip.NewReader(bytes.NewReader(gz))
if err != nil {
return err
}
defer zr.Close()
got, err := io.ReadAll(zr)
if err != nil {
return err
}
if !bytes.Equal(got, want) {
t.Errorf("static_gz/%s does not match static/%s", rel, key)
}
return nil
})
if err != nil {
t.Fatal(err)
}
if mirrored != len(sources) {
t.Errorf("static_gz mirrors %d of %d files under static/", mirrored, len(sources))
}
}
func fetchStatic(t *testing.T, path string, header map[string]string) *http.Response {
t.Helper()
req := httptest.NewRequest("GET", path, nil)
for k, v := range header {
req.Header.Set(k, v)
}
w := httptest.NewRecorder()
StaticHandler().ServeHTTP(w, req)
return w.Result()
}
func TestStaticHandlerGzip(t *testing.T) {
resp := fetchStatic(t, "/css/bootstrap.min.css", map[string]string{"Accept-Encoding": "gzip, deflate, br"})
if resp.StatusCode != http.StatusOK {
t.Fatalf("status %d", resp.StatusCode)
}
if got := resp.Header.Get("Content-Encoding"); got != "gzip" {
t.Fatalf("Content-Encoding %q", got)
}
if resp.Header.Get("Cache-Control") != staticCacheControl {
t.Errorf("Cache-Control %q", resp.Header.Get("Cache-Control"))
}
if resp.Header.Get("Vary") != "Accept-Encoding" {
t.Errorf("Vary %q", resp.Header.Get("Vary"))
}
if resp.Header.Get("ETag") == "" {
t.Error("missing ETag")
}
if got := resp.Header.Get("Content-Type"); !strings.HasPrefix(got, "text/css") {
t.Errorf("Content-Type %q", got)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if resp.Header.Get("Content-Length") == "" {
t.Error("missing Content-Length")
}
zr, err := gzip.NewReader(bytes.NewReader(body))
if err != nil {
t.Fatal(err)
}
defer zr.Close()
got, err := io.ReadAll(zr)
if err != nil {
t.Fatal(err)
}
want, err := os.ReadFile("static/css/bootstrap.min.css")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, want) {
t.Error("gzip body does not decompress to the source file")
}
}
func TestStaticHandlerIdentity(t *testing.T) {
resp := fetchStatic(t, "/css/bootstrap.min.css", nil)
if resp.StatusCode != http.StatusOK {
t.Fatalf("status %d", resp.StatusCode)
}
if got := resp.Header.Get("Content-Encoding"); got != "" {
t.Fatalf("Content-Encoding %q", got)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
want, err := os.ReadFile("static/css/bootstrap.min.css")
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(body, want) {
t.Error("identity body does not match the source file")
}
}
func TestStaticHandlerNotModified(t *testing.T) {
first := fetchStatic(t, "/css/admin.css", map[string]string{"Accept-Encoding": "gzip"})
etag := first.Header.Get("ETag")
if etag == "" {
t.Fatal("missing ETag")
}
resp := fetchStatic(t, "/css/admin.css", map[string]string{"Accept-Encoding": "gzip", "If-None-Match": etag})
if resp.StatusCode != http.StatusNotModified {
t.Fatalf("status %d", resp.StatusCode)
}
if resp.Header.Get("Content-Length") != "" {
t.Errorf("304 carries Content-Length %q", resp.Header.Get("Content-Length"))
}
}
func TestStaticHandlerVariantETags(t *testing.T) {
gz := fetchStatic(t, "/css/admin.css", map[string]string{"Accept-Encoding": "gzip"})
id := fetchStatic(t, "/css/admin.css", nil)
if gz.Header.Get("ETag") == id.Header.Get("ETag") {
t.Error("gzip and identity variants share an ETag")
}
}
func TestStaticHandlerNotFound(t *testing.T) {
for _, p := range []string{"/css/missing.css", "/css/", "/", "/../static_embed.go"} {
if resp := fetchStatic(t, p, nil); resp.StatusCode != http.StatusNotFound {
t.Errorf("%s: status %d", p, resp.StatusCode)
}
}
}
func TestAcceptsGzip(t *testing.T) {
cases := []struct {
header string
want bool
}{
{"", false},
{"gzip", true},
{"gzip, deflate, br", true},
{"br;q=1.0, gzip;q=0.8", true},
{"gzip;q=0", false},
{"gzip;q=0.0", false},
{"deflate", false},
{"x-gzip-like", false},
}
for _, c := range cases {
r := httptest.NewRequest("GET", "/", nil)
if c.header != "" {
r.Header.Set("Accept-Encoding", c.header)
}
if got := acceptsGzip(r); got != c.want {
t.Errorf("acceptsGzip(%q) = %v, want %v", c.header, got, c.want)
}
}
}
+3 -9
View File
@@ -360,15 +360,9 @@ func startAdminServer(ctx context.Context, options AdminOptions, enableUI bool,
SameSite: http.SameSiteLaxMode,
}
// Static files - serve from embedded filesystem
staticFS, err := admin.GetStaticFS()
if err != nil {
log.Printf("Warning: Failed to load embedded static files: %v", err)
} else {
staticHandler := http.FileServer(http.FS(staticFS))
r.Handle("/static", http.RedirectHandler("/static/", http.StatusMovedPermanently))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", staticHandler))
}
// Static files - pre-gzipped and embedded in the binary
r.Handle("/static", http.RedirectHandler("/static/", http.StatusMovedPermanently))
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", admin.StaticHandler()))
// Create admin server (plugin is always enabled)
adminServer := dash.NewAdminServer(*options.master, nil, dataDir, icebergPort)