chore: 初始化 idsync 项目
飞书通讯录 ↔ LDAP 同步工具的首个提交。 - internal/feishu: 飞书通讯录客户端、快照与重试 - internal/ldaplib: LDAP 读写与批量操作 - internal/mapping: 飞书部门/用户到 LDAP 条目的映射 - internal/syncer: 同步计划生成、执行与调度 - internal/store: SQLite 台账 - internal/server + internal/webui: 管理后台与内嵌前端产物 - internal/password: SSHA 口令处理 - config.example.yml / Dockerfile / Makefile / web 前端源码 真实凭据(config.yml)、运行时数据(data/、backup/)与构建产物(bin/)不入库。
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
// Package webui 通过 go:embed 把前端资源打进二进制。
|
||||
//
|
||||
// 前端用 Tailwind v4 + FlyonUI 构建:FlyonUI 官方不支持 CDN 方式
|
||||
// (样式在构建期生成),所以这里使用构建产物,用户无需 Node 环境。
|
||||
// 构建命令见 scripts/build-webui.sh,产物在 internal/webui/dist 下。
|
||||
package webui
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// dist 是前端构建产物。
|
||||
//
|
||||
//go:embed all:dist
|
||||
var dist embed.FS
|
||||
|
||||
// assets 保存资源名 → 内容;assetETags 保存内容指纹用于 ETag。
|
||||
var (
|
||||
assets map[string][]byte
|
||||
assetETags map[string]string
|
||||
)
|
||||
|
||||
func init() {
|
||||
fsys, err := fs.Sub(dist, "dist")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
assets = make(map[string][]byte, 8)
|
||||
assetETags = make(map[string]string, 8)
|
||||
|
||||
_ = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := fs.ReadFile(fsys, path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
assets[path] = data
|
||||
sum := sha256.Sum256(data)
|
||||
assetETags[path] = `"` + hex.EncodeToString(sum[:8]) + `"`
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// contentTypes 是各静态资源的 MIME 类型。
|
||||
var contentTypes = map[string]string{
|
||||
".css": "text/css; charset=utf-8",
|
||||
".js": "application/javascript; charset=utf-8",
|
||||
".html": "text/html; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
".ico": "image/x-icon",
|
||||
".png": "image/png",
|
||||
".woff2": "font/woff2",
|
||||
}
|
||||
|
||||
// IndexHandler 输出内嵌的 index.html。
|
||||
//
|
||||
// 刻意不缓存 HTML:它引用 app.js,一旦被缓存,升级二进制后浏览器仍会
|
||||
// 加载旧脚本,表现为界面数据莫名其妙地缺失或显示 undefined。
|
||||
func IndexHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
data, ok := assets["index.html"]
|
||||
if !ok {
|
||||
http.Error(w, "前端资源缺失,请先执行 make css", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
// AssetHandler 提供 /static/ 下的静态资源。
|
||||
//
|
||||
// 用内容指纹做 ETag 并允许缓存,但每次回源校验(no-cache)。
|
||||
// 这样既有 304 的高效,又不会在升级二进制后继续使用旧资源——
|
||||
// 仅设 max-age 而不带指纹的策略在内嵌场景下会长期给出过期界面。
|
||||
func AssetHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
name := strings.TrimPrefix(r.URL.Path, "/static/")
|
||||
if name == "" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
data, ok := assets[name]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
etag := assetETags[name]
|
||||
w.Header().Set("ETag", etag)
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
|
||||
if match := r.Header.Get("If-None-Match"); match != "" && strings.Contains(match, etag) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
if ct, ok := contentTypes[extOf(name)]; ok {
|
||||
w.Header().Set("Content-Type", ct)
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprint(len(data)))
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
func extOf(name string) string {
|
||||
if i := strings.LastIndex(name, "."); i >= 0 {
|
||||
return name[i:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// AssetNames 返回已嵌入的资源名,供测试与健康检查使用。
|
||||
func AssetNames() []string {
|
||||
out := make([]string, 0, len(assets))
|
||||
for name := range assets {
|
||||
out = append(out, name)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user