3x-ui/web/asset_manifest.go
2026-04-07 12:24:33 +08:00

102 lines
2 KiB
Go

package web
import (
"encoding/json"
"fmt"
"io/fs"
"path"
"strings"
)
type assetManifest map[string]string
type assetResolver struct {
basePath string
debug bool
manifest assetManifest
}
func newAssetResolver(basePath string, debug bool, manifest assetManifest) assetResolver {
return assetResolver{
basePath: basePath,
debug: debug,
manifest: manifest,
}
}
func (r assetResolver) URL(logical string) string {
target := logical
if !r.debug {
hashed, ok := r.manifest[logical]
if !ok {
panic(fmt.Sprintf("missing asset manifest entry for %q", logical))
}
target = hashed
}
return path.Join(r.basePath, "assets", target)
}
func loadAssetManifest(raw []byte) (assetManifest, error) {
if len(raw) == 0 {
return nil, fmt.Errorf("asset manifest is empty")
}
var manifest assetManifest
if err := json.Unmarshal(raw, &manifest); err != nil {
return nil, err
}
if len(manifest) == 0 {
return nil, fmt.Errorf("asset manifest has no entries")
}
return manifest, nil
}
func assetCacheControl(requestPath string) string {
if hasFingerprint(requestPath) {
return "public, max-age=31536000, immutable"
}
return "public, max-age=300"
}
func assetRequestCacheControl(requestPath string, exists bool) string {
if exists {
return assetCacheControl(requestPath)
}
return "public, max-age=300"
}
func assetExists(assetsFS fs.FS, assetPath string) bool {
if assetPath == "" {
return false
}
if _, err := fs.Stat(assetsFS, assetPath); err != nil {
return false
}
return true
}
func hasFingerprint(requestPath string) bool {
base := path.Base(requestPath)
parts := strings.Split(base, ".")
if len(parts) < 2 {
return false
}
if isFingerprintHash(parts[len(parts)-1]) {
return true
}
if len(parts) >= 3 && isFingerprintHash(parts[len(parts)-2]) {
return true
}
return false
}
func isFingerprintHash(hash string) bool {
if len(hash) < 6 || len(hash) > 64 {
return false
}
for _, ch := range hash {
if !strings.ContainsRune("0123456789abcdef", ch) {
return false
}
}
return true
}