feat: complete 2.0.7.12 platform overhaul
This commit is contained in:
@@ -3,8 +3,7 @@ package web
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"html"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -63,18 +62,10 @@ func (r *router) serveAdminAsset(w http.ResponseWriter, req *http.Request, asset
|
||||
return
|
||||
}
|
||||
setStaticCacheHeaders(w, assetPath)
|
||||
if err := validateAdminDiskBuild(r.cfg.AdminWebDir); err == nil {
|
||||
if tryServeDiskFile(w, req, r.cfg.AdminWebDir, assetPath) {
|
||||
return
|
||||
}
|
||||
log.Printf("admin web disk build is missing requested asset: %s", assetPath)
|
||||
} else {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
log.Printf("admin web disk build is incomplete; serving embedded assets: %v", err)
|
||||
}
|
||||
if serveEmbeddedFile(w, req, "admin/dist/"+filepath.ToSlash(assetPath)) {
|
||||
return
|
||||
}
|
||||
data, err := r.adminAssets.ReadFile(filepath.ToSlash(assetPath))
|
||||
if err == nil {
|
||||
serveAssetContent(w, req, assetPath, data)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
http.NotFound(w, req)
|
||||
@@ -136,52 +127,29 @@ func (r *router) servePortal(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
func (r *router) serveAdmin(w http.ResponseWriter, req *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store, must-revalidate")
|
||||
index := filepath.Join(r.cfg.AdminWebDir, "index.html")
|
||||
if err := validateAdminDiskBuild(r.cfg.AdminWebDir); err == nil {
|
||||
http.ServeFile(w, req, index)
|
||||
return
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
log.Printf("admin web disk build is incomplete: %v", err)
|
||||
}
|
||||
if serveEmbeddedFile(w, req, "admin/dist/index.html") {
|
||||
data, err := r.adminAssets.ReadFile("index.html")
|
||||
if err == nil {
|
||||
serveAssetContent(w, req, "index.html", data)
|
||||
return
|
||||
}
|
||||
status := r.adminAssets.Diagnostics()
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write([]byte(`<!doctype html><html><head><meta charset="utf-8"><title>YMhut Admin</title></head><body><main><h1>YMhut Admin</h1><p>Build web/admin to enable the Vue console.</p></main></body></html>`))
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
_, _ = w.Write([]byte(`<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>管理后台资源不可用</title><style>body{margin:0;background:#f5f7f7;color:#253131;font:14px/1.6 "Segoe UI",sans-serif}main{max-width:680px;margin:12vh auto;padding:28px;background:#fff;border:1px solid #d9e0df;border-radius:8px}h1{font-size:22px;margin:0 0 12px}code{display:block;margin-top:16px;padding:12px;background:#f1f4f3;border-radius:6px;overflow-wrap:anywhere}</style></head><body><main><h1>管理后台资源不可用</h1><p>服务拒绝混用不完整或版本不一致的后台资源。请重新发布同一构建生成的服务二进制。</p><code>` + html.EscapeString(status.Mode+" / "+status.BuildID+" / "+status.ValidationError) + `</code></main></body></html>`))
|
||||
}
|
||||
|
||||
var adminAssetReferencePattern = regexp.MustCompile(`(?:src|href)=["'](/admin/assets/[^"'?#]+)`)
|
||||
|
||||
func validateAdminDiskBuild(root string) error {
|
||||
index := filepath.Join(root, "index.html")
|
||||
data, err := os.ReadFile(index)
|
||||
if err != nil {
|
||||
return err
|
||||
func serveAssetContent(w http.ResponseWriter, req *http.Request, name string, data []byte) {
|
||||
if contentType := mime.TypeByExtension(filepath.Ext(name)); contentType != "" {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
}
|
||||
matches := adminAssetReferencePattern.FindAllSubmatch(data, -1)
|
||||
if len(matches) == 0 {
|
||||
return errors.New("index.html does not reference any admin assets")
|
||||
}
|
||||
for _, match := range matches {
|
||||
assetPath := strings.TrimPrefix(string(match[1]), "/admin/")
|
||||
if strings.Contains(assetPath, "..") || strings.ContainsAny(assetPath, `\`) {
|
||||
return fmt.Errorf("invalid admin asset reference %s", assetPath)
|
||||
}
|
||||
path := filepath.Join(root, filepath.FromSlash(assetPath))
|
||||
info, statErr := os.Stat(path)
|
||||
if statErr != nil {
|
||||
return fmt.Errorf("missing %s: %w", assetPath, statErr)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return fmt.Errorf("%s is not a file", assetPath)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
http.ServeContent(w, req, filepath.Base(name), time.Time{}, bytes.NewReader(data))
|
||||
}
|
||||
|
||||
var hashedAssetPattern = regexp.MustCompile(`-[A-Za-z0-9_-]{8,}\.[A-Za-z0-9]+$`)
|
||||
|
||||
func setStaticCacheHeaders(w http.ResponseWriter, assetPath string) {
|
||||
extension := strings.ToLower(filepath.Ext(assetPath))
|
||||
if strings.HasPrefix(filepath.ToSlash(assetPath), "assets/") && extension != ".ico" {
|
||||
if strings.HasPrefix(filepath.ToSlash(assetPath), "assets/") && extension != ".ico" && hashedAssetPattern.MatchString(filepath.Base(assetPath)) {
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user