服务器代理地图数据
This commit is contained in:
@@ -155,5 +155,9 @@ func mapTileSourceDTO(row mapTileSourceRecord) gin.H {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func publicMapTileSourceDTO(row mapTileSourceRecord) gin.H {
|
func publicMapTileSourceDTO(row mapTileSourceRecord) gin.H {
|
||||||
return gin.H{"id": row.ID, "name": row.Name, "url_template": row.URLTemplate, "attribution": row.Attribution, "max_zoom": row.MaxZoom}
|
hash := row.URLTemplateHash
|
||||||
|
if hash == "" {
|
||||||
|
hash = mapTileSourceHash(row.URLTemplate)
|
||||||
|
}
|
||||||
|
return gin.H{"id": row.ID, "name": row.Name, "url_template": "/api/map/" + hash + "?x={x}&y={y}&z={z}", "attribution": row.Attribution, "max_zoom": row.MaxZoom}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ type webConfig struct {
|
|||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
SocketPath string `yaml:"socket_path"`
|
SocketPath string `yaml:"socket_path"`
|
||||||
StaticDir string `yaml:"static_dir"`
|
StaticDir string `yaml:"static_dir"`
|
||||||
|
MapTileCacheDir string `yaml:"map_tile_cache_dir"`
|
||||||
Admin webAdminConfig `yaml:"admin"`
|
Admin webAdminConfig `yaml:"admin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +110,7 @@ type rawWebConfig struct {
|
|||||||
Port *int `yaml:"port"`
|
Port *int `yaml:"port"`
|
||||||
SocketPath *string `yaml:"socket_path"`
|
SocketPath *string `yaml:"socket_path"`
|
||||||
StaticDir *string `yaml:"static_dir"`
|
StaticDir *string `yaml:"static_dir"`
|
||||||
|
MapTileCacheDir *string `yaml:"map_tile_cache_dir"`
|
||||||
Admin *rawWebAdminConfig `yaml:"admin"`
|
Admin *rawWebAdminConfig `yaml:"admin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +147,7 @@ func defaultConfig() *config {
|
|||||||
Port: 8080,
|
Port: 8080,
|
||||||
SocketPath: defaultWebSocketPath(),
|
SocketPath: defaultWebSocketPath(),
|
||||||
StaticDir: "./dist",
|
StaticDir: "./dist",
|
||||||
|
MapTileCacheDir: defaultMapTileCacheDir(),
|
||||||
Admin: webAdminConfig{
|
Admin: webAdminConfig{
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
Password: "admin",
|
Password: "admin",
|
||||||
@@ -176,6 +179,17 @@ func defaultWebSocketPath() string {
|
|||||||
return defaultWebSocketPathForGOOS(runtime.GOOS)
|
return defaultWebSocketPathForGOOS(runtime.GOOS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func defaultMapTileCacheDir() string {
|
||||||
|
return defaultMapTileCacheDirForGOOS(runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultMapTileCacheDirForGOOS(goos string) string {
|
||||||
|
if goos == "windows" {
|
||||||
|
return filepath.Join(".", "win", "srv", "mesh_mqtt_go")
|
||||||
|
}
|
||||||
|
return filepath.Join(string(filepath.Separator), "srv", "mesh_mqtt_go")
|
||||||
|
}
|
||||||
|
|
||||||
func defaultWebSocketPathForGOOS(goos string) string {
|
func defaultWebSocketPathForGOOS(goos string) string {
|
||||||
if goos == "windows" {
|
if goos == "windows" {
|
||||||
return ""
|
return ""
|
||||||
@@ -342,6 +356,11 @@ func normalizeConfig(raw rawConfig) (*config, bool) {
|
|||||||
} else {
|
} else {
|
||||||
cfg.Web.StaticDir = *raw.Web.StaticDir
|
cfg.Web.StaticDir = *raw.Web.StaticDir
|
||||||
}
|
}
|
||||||
|
if raw.Web.MapTileCacheDir == nil {
|
||||||
|
changed = true
|
||||||
|
} else {
|
||||||
|
cfg.Web.MapTileCacheDir = *raw.Web.MapTileCacheDir
|
||||||
|
}
|
||||||
if raw.Web.Admin == nil {
|
if raw.Web.Admin == nil {
|
||||||
changed = true
|
changed = true
|
||||||
} else {
|
} else {
|
||||||
@@ -394,6 +413,9 @@ func validateConfig(cfg *config) error {
|
|||||||
if cfg.Web.StaticDir == "" {
|
if cfg.Web.StaticDir == "" {
|
||||||
return fmt.Errorf("web.static_dir is required when web is enabled")
|
return fmt.Errorf("web.static_dir is required when web is enabled")
|
||||||
}
|
}
|
||||||
|
if cfg.Web.MapTileCacheDir == "" {
|
||||||
|
return fmt.Errorf("web.map_tile_cache_dir is required when web is enabled")
|
||||||
|
}
|
||||||
if cfg.Web.Admin.Username == "" {
|
if cfg.Web.Admin.Username == "" {
|
||||||
return fmt.Errorf("web.admin.username is required when web is enabled")
|
return fmt.Errorf("web.admin.username is required when web is enabled")
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-1
@@ -44,6 +44,9 @@ func TestLoadConfigCreatesDefaultFile(t *testing.T) {
|
|||||||
if cfg.Web.StaticDir != "./dist" {
|
if cfg.Web.StaticDir != "./dist" {
|
||||||
t.Fatalf("web static dir = %q, want ./dist", cfg.Web.StaticDir)
|
t.Fatalf("web static dir = %q, want ./dist", cfg.Web.StaticDir)
|
||||||
}
|
}
|
||||||
|
if cfg.Web.MapTileCacheDir != defaultMapTileCacheDir() {
|
||||||
|
t.Fatalf("web map tile cache dir = %q, want %q", cfg.Web.MapTileCacheDir, defaultMapTileCacheDir())
|
||||||
|
}
|
||||||
if _, err := os.Stat(path); err != nil {
|
if _, err := os.Stat(path); err != nil {
|
||||||
t.Fatalf("default config was not written: %v", err)
|
t.Fatalf("default config was not written: %v", err)
|
||||||
}
|
}
|
||||||
@@ -80,7 +83,7 @@ func TestLoadConfigFillsMissingFields(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
text := string(data)
|
text := string(data)
|
||||||
for _, want := range []string{"host:", "tls:", "enabled:", "cert_file:", "key_file:", "meshtastic:", "psk:", "database:", "driver:", "sqlite:", "mysql:", "dsn:", "web:", "port:", "socket_path:", "static_dir:"} {
|
for _, want := range []string{"host:", "tls:", "enabled:", "cert_file:", "key_file:", "meshtastic:", "psk:", "database:", "driver:", "sqlite:", "mysql:", "dsn:", "web:", "port:", "socket_path:", "static_dir:", "map_tile_cache_dir:"} {
|
||||||
if !strings.Contains(text, want) {
|
if !strings.Contains(text, want) {
|
||||||
t.Fatalf("completed config missing %q in:\n%s", want, text)
|
t.Fatalf("completed config missing %q in:\n%s", want, text)
|
||||||
}
|
}
|
||||||
@@ -154,6 +157,20 @@ func TestLoadConfigMalformedYAMLDoesNotOverwrite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultMapTileCacheDirForGOOS(t *testing.T) {
|
||||||
|
windowsPath := defaultMapTileCacheDirForGOOS("windows")
|
||||||
|
wantWindows := filepath.Join(".", "win", "srv", "mesh_mqtt_go")
|
||||||
|
if windowsPath != wantWindows {
|
||||||
|
t.Fatalf("windows map tile cache dir = %q, want %q", windowsPath, wantWindows)
|
||||||
|
}
|
||||||
|
|
||||||
|
linuxPath := defaultMapTileCacheDirForGOOS("linux")
|
||||||
|
wantLinux := filepath.Join(string(filepath.Separator), "srv", "mesh_mqtt_go")
|
||||||
|
if linuxPath != wantLinux {
|
||||||
|
t.Fatalf("linux map tile cache dir = %q, want %q", linuxPath, wantLinux)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDefaultWebSocketPathForGOOS(t *testing.T) {
|
func TestDefaultWebSocketPathForGOOS(t *testing.T) {
|
||||||
if windowsPath := defaultWebSocketPathForGOOS("windows"); windowsPath != "" {
|
if windowsPath := defaultWebSocketPathForGOOS("windows"); windowsPath != "" {
|
||||||
t.Fatalf("windows web socket path = %q, want empty", windowsPath)
|
t.Fatalf("windows web socket path = %q, want empty", windowsPath)
|
||||||
@@ -228,6 +245,7 @@ func TestValidateConfigWeb(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg = defaultConfig()
|
cfg = defaultConfig()
|
||||||
|
cfg.Web.SocketPath = filepath.Join(string(filepath.Separator), "tmp", "mesh_mqtt_go.sock")
|
||||||
cfg.Web.Port = 0
|
cfg.Web.Port = 0
|
||||||
if err := validateConfig(cfg); err != nil {
|
if err := validateConfig(cfg); err != nil {
|
||||||
t.Fatalf("web socket with invalid port error = %v, want nil", err)
|
t.Fatalf("web socket with invalid port error = %v, want nil", err)
|
||||||
@@ -246,6 +264,12 @@ func TestValidateConfigWeb(t *testing.T) {
|
|||||||
t.Fatalf("missing web static dir error = %v, want web.static_dir error", err)
|
t.Fatalf("missing web static dir error = %v, want web.static_dir error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg = defaultConfig()
|
||||||
|
cfg.Web.MapTileCacheDir = ""
|
||||||
|
if err := validateConfig(cfg); err == nil || !strings.Contains(err.Error(), "web.map_tile_cache_dir") {
|
||||||
|
t.Fatalf("missing map tile cache dir error = %v, want web.map_tile_cache_dir error", err)
|
||||||
|
}
|
||||||
|
|
||||||
cfg = defaultConfig()
|
cfg = defaultConfig()
|
||||||
cfg.Web.Enabled = false
|
cfg.Web.Enabled = false
|
||||||
cfg.Web.Port = 0
|
cfg.Web.Port = 0
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ type mapTileSourceRecord struct {
|
|||||||
ID uint64 `gorm:"column:id;primaryKey;autoIncrement"`
|
ID uint64 `gorm:"column:id;primaryKey;autoIncrement"`
|
||||||
Name string `gorm:"column:name;not null;uniqueIndex"`
|
Name string `gorm:"column:name;not null;uniqueIndex"`
|
||||||
URLTemplate string `gorm:"column:url_template;not null;uniqueIndex"`
|
URLTemplate string `gorm:"column:url_template;not null;uniqueIndex"`
|
||||||
|
URLTemplateHash string `gorm:"column:url_template_hash;size:64;not null;uniqueIndex"`
|
||||||
Attribution string `gorm:"column:attribution"`
|
Attribution string `gorm:"column:attribution"`
|
||||||
MaxZoom int `gorm:"column:max_zoom;not null"`
|
MaxZoom int `gorm:"column:max_zoom;not null"`
|
||||||
Enabled bool `gorm:"column:enabled;not null;index"`
|
Enabled bool `gorm:"column:enabled;not null;index"`
|
||||||
@@ -463,10 +464,41 @@ func (s *store) migrate() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := migrateMapTileSourceHash(tx, migrator, s.driver); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return (&store{db: tx, driver: s.driver}).EnsureDefaultMapTileSource()
|
return (&store{db: tx, driver: s.driver}).EnsureDefaultMapTileSource()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func migrateMapTileSourceHash(tx *gorm.DB, migrator gorm.Migrator, driver string) error {
|
||||||
|
if !migrator.HasColumn(&mapTileSourceRecord{}, "URLTemplateHash") {
|
||||||
|
if driver == databaseDriverSQLite {
|
||||||
|
if err := tx.Exec("ALTER TABLE map_tile_sources ADD COLUMN url_template_hash TEXT NOT NULL DEFAULT ''").Error; err != nil {
|
||||||
|
return fmt.Errorf("migrate map_tile_sources url_template_hash column: %w", err)
|
||||||
|
}
|
||||||
|
} else if err := migrator.AddColumn(&mapTileSourceRecord{}, "URLTemplateHash"); err != nil {
|
||||||
|
return fmt.Errorf("migrate map_tile_sources url_template_hash column: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []mapTileSourceRecord
|
||||||
|
if err := tx.Model(&mapTileSourceRecord{}).Where("url_template_hash = '' OR url_template_hash IS NULL").Find(&rows).Error; err != nil {
|
||||||
|
return fmt.Errorf("list map_tile_sources missing url_template_hash: %w", err)
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
if err := tx.Model(&mapTileSourceRecord{}).Where("id = ?", row.ID).Update("url_template_hash", mapTileSourceHash(row.URLTemplate)).Error; err != nil {
|
||||||
|
return fmt.Errorf("backfill map_tile_sources url_template_hash: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !migrator.HasIndex(&mapTileSourceRecord{}, "idx_map_tile_sources_url_template_hash") {
|
||||||
|
if err := migrator.CreateIndex(&mapTileSourceRecord{}, "idx_map_tile_sources_url_template_hash"); err != nil {
|
||||||
|
return fmt.Errorf("migrate map_tile_sources index idx_map_tile_sources_url_template_hash: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func createMissingIndexes(migrator gorm.Migrator, model any, label string, indexNames []string) error {
|
func createMissingIndexes(migrator gorm.Migrator, model any, label string, indexNames []string) error {
|
||||||
for _, indexName := range indexNames {
|
for _, indexName := range indexNames {
|
||||||
if !migrator.HasIndex(model, indexName) {
|
if !migrator.HasIndex(model, indexName) {
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ func parseArgs() (*config, error) {
|
|||||||
flag.IntVar(&cfg.Web.Port, "web-port", cfg.Web.Port, "Web server listen port")
|
flag.IntVar(&cfg.Web.Port, "web-port", cfg.Web.Port, "Web server listen port")
|
||||||
flag.StringVar(&cfg.Web.SocketPath, "web-socket-path", cfg.Web.SocketPath, "Web server Unix socket path; empty uses host and port; unsupported on Windows")
|
flag.StringVar(&cfg.Web.SocketPath, "web-socket-path", cfg.Web.SocketPath, "Web server Unix socket path; empty uses host and port; unsupported on Windows")
|
||||||
flag.StringVar(&cfg.Web.StaticDir, "web-static-dir", cfg.Web.StaticDir, "Web frontend static files directory")
|
flag.StringVar(&cfg.Web.StaticDir, "web-static-dir", cfg.Web.StaticDir, "Web frontend static files directory")
|
||||||
|
flag.StringVar(&cfg.Web.MapTileCacheDir, "web-map-tile-cache-dir", cfg.Web.MapTileCacheDir, "Map tile disk cache root directory")
|
||||||
flag.StringVar(&cfg.Web.Admin.Username, "admin-username", cfg.Web.Admin.Username, "Web admin username")
|
flag.StringVar(&cfg.Web.Admin.Username, "admin-username", cfg.Web.Admin.Username, "Web admin username")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -81,6 +83,14 @@ func (s *store) GetDefaultMapTileSource() (*mapTileSourceRecord, error) {
|
|||||||
return &row, nil
|
return &row, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *store) GetEnabledMapTileSourceByHash(hash string) (*mapTileSourceRecord, error) {
|
||||||
|
var row mapTileSourceRecord
|
||||||
|
if err := s.db.Where("enabled = ? AND url_template_hash = ?", true, hash).Take(&row).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &row, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *store) CreateMapTileSource(input mapTileSourceInput) (*mapTileSourceRecord, error) {
|
func (s *store) CreateMapTileSource(input mapTileSourceInput) (*mapTileSourceRecord, error) {
|
||||||
row, err := mapTileSourceFromInput(input)
|
row, err := mapTileSourceFromInput(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -139,6 +149,7 @@ func (s *store) UpdateMapTileSource(id uint64, input mapTileSourceInput) (*mapTi
|
|||||||
updates := map[string]any{
|
updates := map[string]any{
|
||||||
"name": row.Name,
|
"name": row.Name,
|
||||||
"url_template": row.URLTemplate,
|
"url_template": row.URLTemplate,
|
||||||
|
"url_template_hash": row.URLTemplateHash,
|
||||||
"attribution": row.Attribution,
|
"attribution": row.Attribution,
|
||||||
"max_zoom": row.MaxZoom,
|
"max_zoom": row.MaxZoom,
|
||||||
"enabled": row.Enabled,
|
"enabled": row.Enabled,
|
||||||
@@ -244,10 +255,16 @@ func (s *store) EnsureDefaultMapTileSource() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mapTileSourceHash(urlTemplate string) string {
|
||||||
|
h := sha256.Sum256([]byte(urlTemplate))
|
||||||
|
return hex.EncodeToString(h[:])
|
||||||
|
}
|
||||||
|
|
||||||
func defaultMapTileSourceRecord() mapTileSourceRecord {
|
func defaultMapTileSourceRecord() mapTileSourceRecord {
|
||||||
return mapTileSourceRecord{
|
return mapTileSourceRecord{
|
||||||
Name: defaultMapTileSourceName,
|
Name: defaultMapTileSourceName,
|
||||||
URLTemplate: defaultMapTileSourceURLTemplate,
|
URLTemplate: defaultMapTileSourceURLTemplate,
|
||||||
|
URLTemplateHash: mapTileSourceHash(defaultMapTileSourceURLTemplate),
|
||||||
Attribution: defaultMapTileSourceAttribution,
|
Attribution: defaultMapTileSourceAttribution,
|
||||||
MaxZoom: defaultMapTileSourceMaxZoom,
|
MaxZoom: defaultMapTileSourceMaxZoom,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
@@ -274,6 +291,7 @@ func mapTileSourceFromInput(input mapTileSourceInput) (*mapTileSourceRecord, err
|
|||||||
return &mapTileSourceRecord{
|
return &mapTileSourceRecord{
|
||||||
Name: name,
|
Name: name,
|
||||||
URLTemplate: urlTemplate,
|
URLTemplate: urlTemplate,
|
||||||
|
URLTemplateHash: mapTileSourceHash(urlTemplate),
|
||||||
Attribution: strings.TrimSpace(input.Attribution),
|
Attribution: strings.TrimSpace(input.Attribution),
|
||||||
MaxZoom: maxZoom,
|
MaxZoom: maxZoom,
|
||||||
Enabled: input.Enabled,
|
Enabled: input.Enabled,
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMapTileSourceDefaultSeeded(t *testing.T) {
|
func TestMapTileSourceDefaultSeeded(t *testing.T) {
|
||||||
@@ -106,3 +109,113 @@ func TestMapTileSourceDuplicateAndDefaultRules(t *testing.T) {
|
|||||||
t.Fatalf("delete default error = %v, want errMapTileSourceCannotDeleteDefault", err)
|
t.Fatalf("delete default error = %v, want errMapTileSourceCannotDeleteDefault", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMapTileSourceHashIsSetOnCreate(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
row, err := st.CreateMapTileSource(mapTileSourceInput{Name: "Hashed", URLTemplate: "https://test.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMapTileSource() error = %v", err)
|
||||||
|
}
|
||||||
|
want := mapTileSourceHash("https://test.example.com/{z}/{x}/{y}.png")
|
||||||
|
if row.URLTemplateHash != want {
|
||||||
|
t.Fatalf("URLTemplateHash = %q, want %q", row.URLTemplateHash, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapTileSourceDefaultHasHash(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
row, err := st.GetDefaultMapTileSource()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetDefaultMapTileSource() error = %v", err)
|
||||||
|
}
|
||||||
|
want := mapTileSourceHash(defaultMapTileSourceURLTemplate)
|
||||||
|
if row.URLTemplateHash != want {
|
||||||
|
t.Fatalf("default URLTemplateHash = %q, want %q", row.URLTemplateHash, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetEnabledMapTileSourceByHash(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
row, err := st.CreateMapTileSource(mapTileSourceInput{Name: "HashLookup", URLTemplate: "https://lookup.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMapTileSource() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
found, err := st.GetEnabledMapTileSourceByHash(row.URLTemplateHash)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetEnabledMapTileSourceByHash() error = %v", err)
|
||||||
|
}
|
||||||
|
if found.ID != row.ID {
|
||||||
|
t.Fatalf("found ID = %d, want %d", found.ID, row.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetEnabledMapTileSourceByHashDisabled(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
row, err := st.CreateMapTileSource(mapTileSourceInput{Name: "DisabledHash", URLTemplate: "https://disabled-hash.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: false})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMapTileSource() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = st.GetEnabledMapTileSourceByHash(row.URLTemplateHash)
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
t.Fatalf("GetEnabledMapTileSourceByHash(disabled) = %v, want gorm.ErrRecordNotFound", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetEnabledMapTileSourceByHashUnknown(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
_, err := st.GetEnabledMapTileSourceByHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
t.Fatalf("GetEnabledMapTileSourceByHash(unknown) = %v, want gorm.ErrRecordNotFound", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublicMapTileSourceDTOProxyURL(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
row, err := st.CreateMapTileSource(mapTileSourceInput{Name: "ProxyTest", URLTemplate: "https://proxy.example.com/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMapTileSource() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dto := publicMapTileSourceDTO(*row)
|
||||||
|
urlTemplate, ok := dto["url_template"].(string)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("url_template is not a string")
|
||||||
|
}
|
||||||
|
wantPrefix := "/api/map/" + row.URLTemplateHash + "?x={x}&y={y}&z={z}"
|
||||||
|
if urlTemplate != wantPrefix {
|
||||||
|
t.Fatalf("url_template = %q, want %q", urlTemplate, wantPrefix)
|
||||||
|
}
|
||||||
|
if strings.Contains(urlTemplate, "proxy.example.com") {
|
||||||
|
t.Fatal("url_template should not contain upstream hostname")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapTileSourceHashFunction(t *testing.T) {
|
||||||
|
hash1 := mapTileSourceHash("https://tile.openstreetmap.jp/{z}/{x}/{y}.png")
|
||||||
|
hash2 := mapTileSourceHash("https://tile.openstreetmap.jp/{z}/{x}/{y}.png")
|
||||||
|
hash3 := mapTileSourceHash("https://other.example.com/{z}/{x}/{y}.png")
|
||||||
|
|
||||||
|
if hash1 != hash2 {
|
||||||
|
t.Fatal("hash should be deterministic")
|
||||||
|
}
|
||||||
|
if len(hash1) != 64 {
|
||||||
|
t.Fatalf("hash length = %d, want 64", len(hash1))
|
||||||
|
}
|
||||||
|
if hash1 == hash3 {
|
||||||
|
t.Fatal("different URLs should produce different hashes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mapTileCacheControl = "public, max-age=86400"
|
||||||
|
maxMapTileBytes = 10 << 20
|
||||||
|
)
|
||||||
|
|
||||||
|
type mapTileProxy struct {
|
||||||
|
store *store
|
||||||
|
cacheDir string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerMapTileProxyRoutes(r gin.IRouter, store *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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMapTileProxyFetchesAndCaches(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
requests := 0
|
||||||
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requests++
|
||||||
|
if r.URL.Path != "/3/1/2.png" {
|
||||||
|
t.Fatalf("upstream path = %q, want /3/1/2.png", r.URL.Path)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "image/png")
|
||||||
|
_, _ = w.Write([]byte("tile-data"))
|
||||||
|
}))
|
||||||
|
defer upstream.Close()
|
||||||
|
|
||||||
|
row, err := st.CreateMapTileSource(mapTileSourceInput{Name: "Tiles", URLTemplate: upstream.URL + "/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMapTileSource() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := t.TempDir()
|
||||||
|
router := newRouter(webConfig{StaticDir: t.TempDir(), MapTileCacheDir: cacheDir}, st, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
url := "/api/map/" + row.URLTemplateHash + "?x=1&y=2&z=3"
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, url, nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("request %d status = %d, body = %s", i+1, recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if recorder.Body.String() != "tile-data" {
|
||||||
|
t.Fatalf("request %d body = %q, want tile-data", i+1, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if requests != 1 {
|
||||||
|
t.Fatalf("upstream requests = %d, want 1", requests)
|
||||||
|
}
|
||||||
|
|
||||||
|
cachePath := filepath.Join(cacheDir, row.URLTemplateHash, "3", "1", "2.tile")
|
||||||
|
data, err := os.ReadFile(cachePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read cache file %s: %v", cachePath, err)
|
||||||
|
}
|
||||||
|
if string(data) != "tile-data" {
|
||||||
|
t.Fatalf("cache file = %q, want tile-data", string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapTileProxyRejectsInvalidCoordinates(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
row, err := st.CreateMapTileSource(mapTileSourceInput{Name: "Tiles", URLTemplate: "https://tiles.example.com/{z}/{x}/{y}.png", MaxZoom: 3, Enabled: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMapTileSource() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := newRouter(webConfig{StaticDir: t.TempDir(), MapTileCacheDir: t.TempDir()}, st, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
cases := []string{
|
||||||
|
"/api/map/" + row.URLTemplateHash + "?y=0&z=0",
|
||||||
|
"/api/map/" + row.URLTemplateHash + "?x=-1&y=0&z=0",
|
||||||
|
"/api/map/" + row.URLTemplateHash + "?x=0&y=0&z=4",
|
||||||
|
"/api/map/" + row.URLTemplateHash + "?x=2&y=0&z=1",
|
||||||
|
}
|
||||||
|
for _, url := range cases {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, url, nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
if recorder.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("%s status = %d, want 400; body = %s", url, recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapTileProxyUnknownAndDisabledSource(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
disabled, err := st.CreateMapTileSource(mapTileSourceInput{Name: "Disabled", URLTemplate: "https://disabled.example.com/{z}/{x}/{y}.png", MaxZoom: 3, Enabled: false})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMapTileSource() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := newRouter(webConfig{StaticDir: t.TempDir(), MapTileCacheDir: t.TempDir()}, st, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
cases := []string{
|
||||||
|
"/api/map/not-a-hash?x=0&y=0&z=0",
|
||||||
|
"/api/map/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa?x=0&y=0&z=0",
|
||||||
|
"/api/map/" + disabled.URLTemplateHash + "?x=0&y=0&z=0",
|
||||||
|
}
|
||||||
|
wantStatus := []int{http.StatusBadRequest, http.StatusNotFound, http.StatusNotFound}
|
||||||
|
for i, url := range cases {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, url, nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
if recorder.Code != wantStatus[i] {
|
||||||
|
t.Fatalf("%s status = %d, want %d; body = %s", url, recorder.Code, wantStatus[i], recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapTileProxyUpstreamStatus(t *testing.T) {
|
||||||
|
st := openTestStore(t)
|
||||||
|
defer st.Close()
|
||||||
|
|
||||||
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.Contains(r.URL.Path, "/404/") {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.Error(w, "upstream error", http.StatusInternalServerError)
|
||||||
|
}))
|
||||||
|
defer upstream.Close()
|
||||||
|
|
||||||
|
row404, err := st.CreateMapTileSource(mapTileSourceInput{Name: "NotFoundTiles", URLTemplate: upstream.URL + "/404/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMapTileSource(404) error = %v", err)
|
||||||
|
}
|
||||||
|
row500, err := st.CreateMapTileSource(mapTileSourceInput{Name: "StatusTiles", URLTemplate: upstream.URL + "/{z}/{x}/{y}.png", MaxZoom: 18, Enabled: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateMapTileSource(500) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := newRouter(webConfig{StaticDir: t.TempDir(), MapTileCacheDir: t.TempDir()}, st, nil, nil, nil, nil, nil)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
url string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{url: "/api/map/" + row404.URLTemplateHash + "?x=0&y=0&z=0", want: http.StatusNotFound},
|
||||||
|
{url: "/api/map/" + row500.URLTemplateHash + "?x=0&y=0&z=0", want: http.StatusBadGateway},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodGet, tc.url, nil)
|
||||||
|
router.ServeHTTP(recorder, req)
|
||||||
|
if recorder.Code != tc.want {
|
||||||
|
t.Fatalf("%s status = %d, want %d; body = %s", tc.url, recorder.Code, tc.want, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,13 +51,13 @@ func newRouter(cfg webConfig, store *store, sessions *sessionManager, mqttStatus
|
|||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(gin.Logger(), gin.Recovery())
|
r.Use(gin.Logger(), gin.Recovery())
|
||||||
api := r.Group("/api")
|
api := r.Group("/api")
|
||||||
registerAPIRoutes(api, store)
|
registerAPIRoutes(api, store, cfg.MapTileCacheDir)
|
||||||
registerAdminRoutes(api.Group("/admin"), store, sessions, mqttStatus, blocking, forwarder, settings)
|
registerAdminRoutes(api.Group("/admin"), store, sessions, mqttStatus, blocking, forwarder, settings)
|
||||||
registerStaticRoutes(r, cfg.StaticDir)
|
registerStaticRoutes(r, cfg.StaticDir)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerAPIRoutes(r gin.IRouter, store *store) {
|
func registerAPIRoutes(r gin.IRouter, store *store, mapTileCacheDir string) {
|
||||||
r.GET("/health", func(c *gin.Context) {
|
r.GET("/health", func(c *gin.Context) {
|
||||||
status := gin.H{"status": "ok", "database": "ok"}
|
status := gin.H{"status": "ok", "database": "ok"}
|
||||||
if err := store.Ping(); err != nil {
|
if err := store.Ping(); err != nil {
|
||||||
@@ -73,6 +73,7 @@ func registerAPIRoutes(r gin.IRouter, store *store) {
|
|||||||
registerNodeInfoRoutes(r, store, "/nodes")
|
registerNodeInfoRoutes(r, store, "/nodes")
|
||||||
registerMapReportRoutes(r, store)
|
registerMapReportRoutes(r, store)
|
||||||
registerMapSourceRoutes(r, store)
|
registerMapSourceRoutes(r, store)
|
||||||
|
registerMapTileProxyRoutes(r, store, mapTileCacheDir)
|
||||||
registerHelpRoutes(r, store)
|
registerHelpRoutes(r, store)
|
||||||
r.GET("/text-messages", func(c *gin.Context) {
|
r.GET("/text-messages", func(c *gin.Context) {
|
||||||
opts, ok := parseListOptions(c)
|
opts, ok := parseListOptions(c)
|
||||||
|
|||||||
Reference in New Issue
Block a user