Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// adminAssets carries the interface sheets, the self-hosted fonts and the
|
||||
// glyph sprite of the admin UI, packaged next to the templates so one
|
||||
// binary serves the whole product.
|
||||
//
|
||||
//go:embed static/*.css static/graphis.svg static/fonts/*.woff2
|
||||
var adminAssets embed.FS
|
||||
|
||||
// asset is one packaged file with its content type and an ETag derived
|
||||
// from its bytes.
|
||||
type asset struct {
|
||||
body []byte
|
||||
contentType string
|
||||
etag string
|
||||
}
|
||||
|
||||
// assets indexes the packaged files by their URL tail under
|
||||
// /admin/assets/. The index doubles as the allow-list: a path the binary
|
||||
// does not hold is a 404, so no traversal can reach the disk.
|
||||
var assets = func() map[string]*asset {
|
||||
entries, err := fs.ReadDir(adminAssets, "static")
|
||||
if err != nil {
|
||||
panic("web: embedded assets unreadable: " + err.Error())
|
||||
}
|
||||
out := map[string]*asset{}
|
||||
add := func(name, tail, contentType string) {
|
||||
body, err := adminAssets.ReadFile(name)
|
||||
if err != nil {
|
||||
panic("web: embedded asset unreadable: " + name)
|
||||
}
|
||||
sum := sha256.Sum256(body)
|
||||
out[tail] = &asset{
|
||||
body: body,
|
||||
contentType: contentType,
|
||||
etag: `"` + hex.EncodeToString(sum[:])[:16] + `"`,
|
||||
}
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
switch {
|
||||
case strings.HasSuffix(name, ".css"):
|
||||
add("static/"+name, name, "text/css; charset=utf-8")
|
||||
case strings.HasSuffix(name, ".svg"):
|
||||
add("static/"+name, name, "image/svg+xml; charset=utf-8")
|
||||
}
|
||||
}
|
||||
fonts, err := fs.ReadDir(adminAssets, "static/fonts")
|
||||
if err != nil {
|
||||
panic("web: embedded fonts unreadable: " + err.Error())
|
||||
}
|
||||
for _, font := range fonts {
|
||||
add("static/fonts/"+font.Name(), "fonts/"+font.Name(), "font/woff2")
|
||||
}
|
||||
return out
|
||||
}()
|
||||
|
||||
// AssetHandler serves one embedded asset by its tail under
|
||||
// /admin/assets/. Responses are revalidatable: the ETag is the content,
|
||||
// so a deploy refreshes every page while a visit fetches nothing new.
|
||||
func AssetHandler(w http.ResponseWriter, r *http.Request) {
|
||||
tail := path.Clean(strings.TrimPrefix(r.URL.Path, "/admin/assets/"))
|
||||
found, ok := assets[strings.TrimPrefix(tail, "./")]
|
||||
if !ok || tail == "." {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", found.contentType)
|
||||
w.Header().Set("ETag", found.etag)
|
||||
w.Header().Set("Cache-Control", "public, max-age=0, must-revalidate")
|
||||
http.ServeContent(w, r, tail, time.Time{}, bytes.NewReader(found.body))
|
||||
}
|
||||
Reference in New Issue
Block a user