重构:拆出 bot / sign / mapsource / llmadmin / web 包
第三批(最终批):把 root 中剩下所有领域文件按功能搬到 internal/ 下。
完成后根目录从 46 个 .go 文件降到 14 个,其中 11 个是 bridge(type alias
+ thin wrapper),仅供尚未改造的 main.go 引用。
新增包
- internal/bot/ Service / TextSender / NewPKIKeyResolver /
RegisterRoutes,含 PKI 直连发送、节点信息广播、
outbound DM 持久化等业务逻辑。
- internal/sign/ SignDTO / SignDayCountDTO / RegisterAdminRoutes,
把原来分散在 admin_sign_routes.go 与 web.go 中的
sign DTO/路由收拢到一处。
- internal/mapsource/ AdminDTO / PublicDTO / RegisterAdminRoutes /
RegisterPublicRoutes。
- internal/llmadmin/ LLM 消息队列、Provider、ToolRouter、PrimaryConfig 的
admin 路由。
- internal/web/ 路由总入口(NewRouter/NewHTTPServer/ServeUnixSocket)、
各资源的 GET API、admin 用户/登录/MQTT 状态、所有
DTO 函数。把 auth.go 的 sessionClaims 升级为
auth.SessionClaims;mqtt_status.go 重写成
MQTTRuntimeStatus / AdminMQTTStatus 结构体并把
client info 解析在 web 包内自带,不再依赖 main 包。
map_tile_proxy_routes 与测试一起搬入。
修改
- web.go 中 parseListOptions / writeListResponse / ptrString 等本地 helper
改为对 internal/webutil 的 thin wrapper,避免重复实现。
- internal/auth 在 step 4 已创建,本批中 web 包正式开始引用其 Manager /
RequireAdmin / SessionClaims。
根目录新增 bridge:bot_bridge.go / sign_bridge.go / mapsource_bridge.go /
llmadmin_bridge.go / web_bridge.go。后者把 mqttRuntimeStatus 包成
webpkg.MQTTStatusProvider 适配器,使 main.go 中旧字段名保持可用。
go build ./... / go test ./... 全部通过;测试数量未变。
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
storepkg "meshtastic_mqtt_server/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
mapTileCacheControl = "public, max-age=86400"
|
||||
maxMapTileBytes = 10 << 20
|
||||
)
|
||||
|
||||
type mapTileProxy struct {
|
||||
store *storepkg.Store
|
||||
cacheDir string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func registerMapTileProxyRoutes(r gin.IRouter, store *storepkg.Store, cacheDir string) {
|
||||
proxy := &mapTileProxy{
|
||||
store: store,
|
||||
cacheDir: cacheDir,
|
||||
client: &http.Client{Timeout: 15 * time.Second},
|
||||
}
|
||||
r.GET("/map/:sourceHash", proxy.handle)
|
||||
}
|
||||
|
||||
func (p *mapTileProxy) handle(c *gin.Context) {
|
||||
sourceHash := strings.ToLower(c.Param("sourceHash"))
|
||||
if !isMapTileSourceHash(sourceHash) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid map source hash"})
|
||||
return
|
||||
}
|
||||
|
||||
row, err := p.store.GetEnabledMapTileSourceByHash(sourceHash)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "map source not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
tile, ok := parseMapTileCoordinates(c, row.MaxZoom)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
cachePath := mapTileCachePath(p.cacheDir, sourceHash, tile)
|
||||
if data, err := os.ReadFile(cachePath); err == nil {
|
||||
writeMapTile(c, data)
|
||||
return
|
||||
} else if !os.IsNotExist(err) {
|
||||
// Fall through to upstream fetch. A broken cache file should not prevent map rendering.
|
||||
}
|
||||
|
||||
data, status, err := p.fetchRemoteTile(c.Request, row.URLTemplate, tile)
|
||||
if err != nil {
|
||||
c.JSON(status, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_ = writeMapTileCacheFile(cachePath, data)
|
||||
writeMapTile(c, data)
|
||||
}
|
||||
|
||||
func (p *mapTileProxy) fetchRemoteTile(req *http.Request, template string, tile mapTileCoordinates) ([]byte, int, error) {
|
||||
remoteURL := expandMapTileURLTemplate(template, tile)
|
||||
upstreamReq, err := http.NewRequestWithContext(req.Context(), http.MethodGet, remoteURL, nil)
|
||||
if err != nil {
|
||||
return nil, http.StatusBadGateway, fmt.Errorf("build upstream map tile request: %w", err)
|
||||
}
|
||||
upstreamReq.Header.Set("User-Agent", "mesh_mqtt_go map tile cache")
|
||||
|
||||
resp, err := p.client.Do(upstreamReq)
|
||||
if err != nil {
|
||||
return nil, http.StatusBadGateway, fmt.Errorf("fetch upstream map tile: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, http.StatusNotFound, fmt.Errorf("upstream map tile not found")
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, http.StatusBadGateway, fmt.Errorf("upstream map tile returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, maxMapTileBytes+1))
|
||||
if err != nil {
|
||||
return nil, http.StatusBadGateway, fmt.Errorf("read upstream map tile: %w", err)
|
||||
}
|
||||
if len(data) > maxMapTileBytes {
|
||||
return nil, http.StatusBadGateway, fmt.Errorf("upstream map tile is too large")
|
||||
}
|
||||
return data, http.StatusOK, nil
|
||||
}
|
||||
|
||||
type mapTileCoordinates struct {
|
||||
x int64
|
||||
y int64
|
||||
z int64
|
||||
}
|
||||
|
||||
func parseMapTileCoordinates(c *gin.Context, maxZoom int) (mapTileCoordinates, bool) {
|
||||
x, ok := parseMapTileCoordinate(c, "x")
|
||||
if !ok {
|
||||
return mapTileCoordinates{}, false
|
||||
}
|
||||
y, ok := parseMapTileCoordinate(c, "y")
|
||||
if !ok {
|
||||
return mapTileCoordinates{}, false
|
||||
}
|
||||
z, ok := parseMapTileCoordinate(c, "z")
|
||||
if !ok {
|
||||
return mapTileCoordinates{}, false
|
||||
}
|
||||
if z > int64(maxZoom) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "map tile z exceeds max zoom"})
|
||||
return mapTileCoordinates{}, false
|
||||
}
|
||||
limit := int64(1) << z
|
||||
if x >= limit || y >= limit {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "map tile coordinates out of range"})
|
||||
return mapTileCoordinates{}, false
|
||||
}
|
||||
return mapTileCoordinates{x: x, y: y, z: z}, true
|
||||
}
|
||||
|
||||
func parseMapTileCoordinate(c *gin.Context, name string) (int64, bool) {
|
||||
value := c.Query(name)
|
||||
if value == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing map tile " + name})
|
||||
return 0, false
|
||||
}
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || parsed < 0 || parsed > 30_000_000_000 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid map tile " + name})
|
||||
return 0, false
|
||||
}
|
||||
return parsed, true
|
||||
}
|
||||
|
||||
func isMapTileSourceHash(value string) bool {
|
||||
if len(value) != 64 {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if (r < '0' || r > '9') && (r < 'a' || r > 'f') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func expandMapTileURLTemplate(template string, tile mapTileCoordinates) string {
|
||||
result := strings.ReplaceAll(template, "{x}", strconv.FormatInt(tile.x, 10))
|
||||
result = strings.ReplaceAll(result, "{y}", strconv.FormatInt(tile.y, 10))
|
||||
result = strings.ReplaceAll(result, "{z}", strconv.FormatInt(tile.z, 10))
|
||||
return result
|
||||
}
|
||||
|
||||
func mapTileCachePath(cacheDir, sourceHash string, tile mapTileCoordinates) string {
|
||||
return filepath.Join(cacheDir, sourceHash, strconv.FormatInt(tile.z, 10), strconv.FormatInt(tile.x, 10), strconv.FormatInt(tile.y, 10)+".tile")
|
||||
}
|
||||
|
||||
func writeMapTileCacheFile(path string, data []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path)+".*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpPath := tmp.Name()
|
||||
defer os.Remove(tmpPath)
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
tmp.Close()
|
||||
return err
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmpPath, path)
|
||||
}
|
||||
|
||||
func writeMapTile(c *gin.Context, data []byte) {
|
||||
contentType := http.DetectContentType(data)
|
||||
c.Header("Cache-Control", mapTileCacheControl)
|
||||
c.Data(http.StatusOK, contentType, data)
|
||||
}
|
||||
Reference in New Issue
Block a user