Split internal/web into subpackages before domain-admin growth.
test / test (push) Has been cancelled
test / test (push) Has been cancelled
Lay out view, auth, validate, and handlers under internal/web while keeping the cmd/panel API unchanged; update roadmap and changelog for web-split closure. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
)
|
||||
|
||||
// staticETags maps each /static/ URL path to a strong ETag over the asset's
|
||||
// bytes, computed once at startup from the embedded FS.
|
||||
//
|
||||
// The assets are baked into the binary, so their FS modification times are the
|
||||
// zero value and http.FileServer sends no Last-Modified. Without an ETag either,
|
||||
// a response carries no validator at all and the browser is free to guess how
|
||||
// long to keep it — which is how a replaced favicon keeps showing the old mark
|
||||
// long after a deploy. Hashing the content gives every asset a validator that
|
||||
// changes exactly when the asset does.
|
||||
var staticETags = buildStaticETags()
|
||||
|
||||
func buildStaticETags() map[string]string {
|
||||
etags := make(map[string]string)
|
||||
// An error here would mean the embed directive and this walk disagree, which
|
||||
// is a build-time defect rather than a runtime condition; the assets still
|
||||
// serve correctly without a validator, so skip what can't be read.
|
||||
_ = fs.WalkDir(assetsFS, "static", func(name string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
b, err := fs.ReadFile(assetsFS, name)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
sum := sha256.Sum256(b)
|
||||
etags["/"+name] = `"` + hex.EncodeToString(sum[:16]) + `"`
|
||||
return nil
|
||||
})
|
||||
return etags
|
||||
}
|
||||
|
||||
// StaticHandler serves the embedded assets under /static/ with a content ETag.
|
||||
//
|
||||
// Cache-Control is no-cache rather than a max-age: it lets the browser keep the
|
||||
// copy but requires it to revalidate, so an asset that changed is picked up on
|
||||
// the next page load while an unchanged one costs a 304 with no body. For a
|
||||
// handful of small files on a single-operator panel that trade is the right way
|
||||
// round — correctness after a deploy matters more than saving the round trip.
|
||||
// http.ServeContent answers the conditional request from the ETag we set here.
|
||||
func StaticHandler() http.Handler {
|
||||
files := http.FileServer(http.FS(assetsFS))
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if etag, ok := staticETags[path.Clean(r.URL.Path)]; ok {
|
||||
w.Header().Set("ETag", etag)
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
}
|
||||
files.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user