- Updated docker-compose.tools.yml to mount source code at /src and set working directory for backend tools, ensuring proper Go module caching. - Modified Dockerfile.tools to install the latest golangci-lint version compatible with Go 1.24 and adjusted working directory for build-time operations. - Enhanced Makefile to build backend tools before running tests and linting, ensuring dependencies are up-to-date and improving overall workflow efficiency. - Refactored test and handler files to include error handling for database operations, enhancing reliability and debugging capabilities.
163 lines
4.4 KiB
Go
163 lines
4.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/andyleap/hnh-map/internal/app"
|
|
"github.com/andyleap/hnh-map/internal/app/services"
|
|
)
|
|
|
|
var transparentPNG = []byte{
|
|
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
|
0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
|
|
0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41,
|
|
0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00,
|
|
0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00,
|
|
0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
|
|
0x42, 0x60, 0x82,
|
|
}
|
|
|
|
var tileRegex = regexp.MustCompile(`([0-9]+)/([0-9]+)/([-0-9]+)_([-0-9]+)\.png`)
|
|
|
|
// WatchGridUpdates is the SSE endpoint for tile updates.
|
|
func (h *Handlers) WatchGridUpdates(rw http.ResponseWriter, req *http.Request) {
|
|
ctx := req.Context()
|
|
s := h.Auth.GetSession(ctx, req)
|
|
if !h.canAccessMap(s) {
|
|
rw.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
rw.Header().Set("Content-Type", "text/event-stream")
|
|
rw.Header().Set("Access-Control-Allow-Origin", "*")
|
|
rw.Header().Set("X-Accel-Buffering", "no")
|
|
|
|
flusher, ok := rw.(http.Flusher)
|
|
if !ok {
|
|
JSONError(rw, http.StatusInternalServerError, "streaming unsupported", "INTERNAL_ERROR")
|
|
return
|
|
}
|
|
|
|
c := h.Map.WatchTiles()
|
|
mc := h.Map.WatchMerges()
|
|
|
|
// Option 1A: do not send full cache on connect; client requests tiles with cache=0 when missing.
|
|
// This avoids a huge JSON dump and slow parse on connect when the DB has many tiles.
|
|
tileCache := []services.TileCache{}
|
|
raw, _ := json.Marshal(tileCache)
|
|
fmt.Fprint(rw, "data: ")
|
|
_, _ = rw.Write(raw)
|
|
fmt.Fprint(rw, "\n\n")
|
|
flusher.Flush()
|
|
|
|
ticker := time.NewTicker(app.SSETickInterval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case e, ok := <-c:
|
|
if !ok {
|
|
return
|
|
}
|
|
found := false
|
|
for i := range tileCache {
|
|
if tileCache[i].M == e.MapID && tileCache[i].X == e.Coord.X && tileCache[i].Y == e.Coord.Y && tileCache[i].Z == e.Zoom {
|
|
tileCache[i].T = int(e.Cache)
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
tileCache = append(tileCache, services.TileCache{
|
|
M: e.MapID,
|
|
X: e.Coord.X,
|
|
Y: e.Coord.Y,
|
|
Z: e.Zoom,
|
|
T: int(e.Cache),
|
|
})
|
|
}
|
|
case e, ok := <-mc:
|
|
if !ok {
|
|
return
|
|
}
|
|
raw, err := json.Marshal(e)
|
|
if err != nil {
|
|
slog.Error("failed to marshal merge event", "error", err)
|
|
}
|
|
fmt.Fprint(rw, "event: merge\n")
|
|
fmt.Fprint(rw, "data: ")
|
|
_, _ = rw.Write(raw)
|
|
fmt.Fprint(rw, "\n\n")
|
|
flusher.Flush()
|
|
case <-ticker.C:
|
|
raw, _ := json.Marshal(tileCache)
|
|
fmt.Fprint(rw, "data: ")
|
|
_, _ = rw.Write(raw)
|
|
fmt.Fprint(rw, "\n\n")
|
|
tileCache = tileCache[:0]
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
}
|
|
|
|
// GridTile serves tile images.
|
|
func (h *Handlers) GridTile(rw http.ResponseWriter, req *http.Request) {
|
|
ctx := req.Context()
|
|
s := h.Auth.GetSession(ctx, req)
|
|
if !h.canAccessMap(s) {
|
|
rw.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
tile := tileRegex.FindStringSubmatch(req.URL.Path)
|
|
if tile == nil || len(tile) < 5 {
|
|
JSONError(rw, http.StatusBadRequest, "invalid path", "BAD_REQUEST")
|
|
return
|
|
}
|
|
mapid, err := strconv.Atoi(tile[1])
|
|
if err != nil {
|
|
JSONError(rw, http.StatusBadRequest, "request parsing error", "BAD_REQUEST")
|
|
return
|
|
}
|
|
z, err := strconv.Atoi(tile[2])
|
|
if err != nil {
|
|
JSONError(rw, http.StatusBadRequest, "request parsing error", "BAD_REQUEST")
|
|
return
|
|
}
|
|
x, err := strconv.Atoi(tile[3])
|
|
if err != nil {
|
|
JSONError(rw, http.StatusBadRequest, "request parsing error", "BAD_REQUEST")
|
|
return
|
|
}
|
|
y, err := strconv.Atoi(tile[4])
|
|
if err != nil {
|
|
JSONError(rw, http.StatusBadRequest, "request parsing error", "BAD_REQUEST")
|
|
return
|
|
}
|
|
storageZ := z
|
|
if storageZ == 6 {
|
|
storageZ = 0
|
|
}
|
|
if storageZ < 0 || storageZ > app.MaxZoomLevel {
|
|
storageZ = 0
|
|
}
|
|
td := h.Map.GetTile(ctx, mapid, app.Coord{X: x, Y: y}, storageZ)
|
|
if td == nil {
|
|
rw.Header().Set("Content-Type", "image/png")
|
|
rw.Header().Set("Cache-Control", "private, max-age=3600")
|
|
rw.WriteHeader(http.StatusOK)
|
|
_, _ = rw.Write(transparentPNG)
|
|
return
|
|
}
|
|
|
|
rw.Header().Set("Content-Type", "image/png")
|
|
rw.Header().Set("Cache-Control", "private immutable")
|
|
http.ServeFile(rw, req, filepath.Join(h.Map.GridStorage(), td.File))
|
|
}
|