新增图源修改功能
This commit is contained in:
@@ -0,0 +1,320 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMapTileSourceName = "OpenStreetMap Japan"
|
||||
defaultMapTileSourceURLTemplate = "https://tile.openstreetmap.jp/{z}/{x}/{y}.png"
|
||||
defaultMapTileSourceAttribution = "© OpenStreetMap contributors"
|
||||
defaultMapTileSourceMaxZoom = 19
|
||||
maxMapTileSourceURLLength = 2048
|
||||
)
|
||||
|
||||
var (
|
||||
errMapTileSourceAlreadyExists = errors.New("map source already exists")
|
||||
errMapTileSourceCannotDeleteDefault = errors.New("default map source cannot be deleted")
|
||||
errMapTileSourceCannotDisableDefault = errors.New("default map source cannot be disabled")
|
||||
errMapTileSourceDefaultMustBeEnabled = errors.New("default map source must be enabled")
|
||||
)
|
||||
|
||||
type mapTileSourceInput struct {
|
||||
Name string
|
||||
URLTemplate string
|
||||
Attribution string
|
||||
MaxZoom int
|
||||
Enabled bool
|
||||
IsDefault bool
|
||||
}
|
||||
|
||||
func (s *store) ListMapTileSources(opts listOptions) ([]mapTileSourceRecord, error) {
|
||||
opts = normalizeListOptions(opts)
|
||||
var rows []mapTileSourceRecord
|
||||
q := s.db.Model(&mapTileSourceRecord{}).
|
||||
Order("is_default DESC").
|
||||
Order("updated_at DESC").
|
||||
Order("id DESC").
|
||||
Limit(opts.Limit).
|
||||
Offset(opts.Offset)
|
||||
return rows, q.Find(&rows).Error
|
||||
}
|
||||
|
||||
func (s *store) CountMapTileSources(opts listOptions) (int64, error) {
|
||||
var total int64
|
||||
return total, s.db.Model(&mapTileSourceRecord{}).Count(&total).Error
|
||||
}
|
||||
|
||||
func (s *store) GetDefaultMapTileSource() (*mapTileSourceRecord, error) {
|
||||
var row mapTileSourceRecord
|
||||
err := s.db.Where("enabled = ? AND is_default = ?", true, true).Order("id ASC").Take(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
fallback := defaultMapTileSourceRecord()
|
||||
return &fallback, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
func (s *store) CreateMapTileSource(input mapTileSourceInput) (*mapTileSourceRecord, error) {
|
||||
row, err := mapTileSourceFromInput(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if row.IsDefault && !row.Enabled {
|
||||
return nil, errMapTileSourceDefaultMustBeEnabled
|
||||
}
|
||||
if err := s.ensureMapTileSourceUnique(0, row.Name, row.URLTemplate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
if row.IsDefault {
|
||||
if err := tx.Model(&mapTileSourceRecord{}).Where("is_default = ?", true).Update("is_default", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Create(row).Error
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
func (s *store) UpdateMapTileSource(id uint64, input mapTileSourceInput) (*mapTileSourceRecord, error) {
|
||||
if id == 0 {
|
||||
return nil, fmt.Errorf("map source id is required")
|
||||
}
|
||||
row, err := mapTileSourceFromInput(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var updated mapTileSourceRecord
|
||||
if err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var existing mapTileSourceRecord
|
||||
if err := tx.Where("id = ?", id).Take(&existing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if existing.IsDefault && !row.Enabled {
|
||||
return errMapTileSourceCannotDisableDefault
|
||||
}
|
||||
if row.IsDefault && !row.Enabled {
|
||||
return errMapTileSourceDefaultMustBeEnabled
|
||||
}
|
||||
if !row.IsDefault && existing.IsDefault {
|
||||
row.IsDefault = true
|
||||
}
|
||||
if err := ensureMapTileSourceUniqueTx(tx, id, row.Name, row.URLTemplate); err != nil {
|
||||
return err
|
||||
}
|
||||
if row.IsDefault {
|
||||
if err := tx.Model(&mapTileSourceRecord{}).Where("id <> ? AND is_default = ?", id, true).Update("is_default", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
updates := map[string]any{
|
||||
"name": row.Name,
|
||||
"url_template": row.URLTemplate,
|
||||
"attribution": row.Attribution,
|
||||
"max_zoom": row.MaxZoom,
|
||||
"enabled": row.Enabled,
|
||||
"is_default": row.IsDefault,
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
if err := tx.Model(&mapTileSourceRecord{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Where("id = ?", id).Take(&updated).Error
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &updated, nil
|
||||
}
|
||||
|
||||
func (s *store) DeleteMapTileSource(id uint64) error {
|
||||
if id == 0 {
|
||||
return fmt.Errorf("map source id is required")
|
||||
}
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var row mapTileSourceRecord
|
||||
if err := tx.Where("id = ?", id).Take(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if row.IsDefault {
|
||||
return errMapTileSourceCannotDeleteDefault
|
||||
}
|
||||
result := tx.Where("id = ?", id).Delete(&mapTileSourceRecord{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *store) SetDefaultMapTileSource(id uint64) (*mapTileSourceRecord, error) {
|
||||
if id == 0 {
|
||||
return nil, fmt.Errorf("map source id is required")
|
||||
}
|
||||
var row mapTileSourceRecord
|
||||
if err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("id = ?", id).Take(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if !row.Enabled {
|
||||
return errMapTileSourceDefaultMustBeEnabled
|
||||
}
|
||||
if err := tx.Model(&mapTileSourceRecord{}).Where("is_default = ?", true).Update("is_default", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&mapTileSourceRecord{}).Where("id = ?", id).Updates(map[string]any{"is_default": true, "updated_at": time.Now()}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Where("id = ?", id).Take(&row).Error
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
func (s *store) EnsureDefaultMapTileSource() error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var count int64
|
||||
if err := tx.Model(&mapTileSourceRecord{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
row := defaultMapTileSourceRecord()
|
||||
return tx.Create(&row).Error
|
||||
}
|
||||
|
||||
var defaults []mapTileSourceRecord
|
||||
if err := tx.Where("enabled = ? AND is_default = ?", true, true).Order("id ASC").Find(&defaults).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(defaults) > 0 {
|
||||
return tx.Model(&mapTileSourceRecord{}).Where("id <> ? AND is_default = ?", defaults[0].ID, true).Update("is_default", false).Error
|
||||
}
|
||||
|
||||
var enabled mapTileSourceRecord
|
||||
err := tx.Where("enabled = ?", true).Order("id ASC").Take(&enabled).Error
|
||||
if err == nil {
|
||||
return tx.Model(&mapTileSourceRecord{}).Where("id = ?", enabled.ID).Updates(map[string]any{"is_default": true, "updated_at": time.Now()}).Error
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
row := defaultMapTileSourceRecord()
|
||||
var existing mapTileSourceRecord
|
||||
err = tx.Where("name = ? OR url_template = ?", row.Name, row.URLTemplate).Order("id ASC").Take(&existing).Error
|
||||
if err == nil {
|
||||
return tx.Model(&mapTileSourceRecord{}).Where("id = ?", existing.ID).Updates(map[string]any{"enabled": true, "is_default": true, "updated_at": time.Now()}).Error
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
return tx.Create(&row).Error
|
||||
})
|
||||
}
|
||||
|
||||
func defaultMapTileSourceRecord() mapTileSourceRecord {
|
||||
return mapTileSourceRecord{
|
||||
Name: defaultMapTileSourceName,
|
||||
URLTemplate: defaultMapTileSourceURLTemplate,
|
||||
Attribution: defaultMapTileSourceAttribution,
|
||||
MaxZoom: defaultMapTileSourceMaxZoom,
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
}
|
||||
}
|
||||
|
||||
func mapTileSourceFromInput(input mapTileSourceInput) (*mapTileSourceRecord, error) {
|
||||
name := strings.TrimSpace(input.Name)
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("map source name is required")
|
||||
}
|
||||
urlTemplate, err := normalizeMapTileSourceURLTemplate(input.URLTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxZoom := input.MaxZoom
|
||||
if maxZoom == 0 {
|
||||
maxZoom = defaultMapTileSourceMaxZoom
|
||||
}
|
||||
if maxZoom < 1 || maxZoom > 30 {
|
||||
return nil, fmt.Errorf("max zoom must be between 1 and 30")
|
||||
}
|
||||
return &mapTileSourceRecord{
|
||||
Name: name,
|
||||
URLTemplate: urlTemplate,
|
||||
Attribution: strings.TrimSpace(input.Attribution),
|
||||
MaxZoom: maxZoom,
|
||||
Enabled: input.Enabled,
|
||||
IsDefault: input.IsDefault,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeMapTileSourceURLTemplate(value string) (string, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return "", fmt.Errorf("map source url template is required")
|
||||
}
|
||||
if len(value) > maxMapTileSourceURLLength {
|
||||
return "", fmt.Errorf("map source url template is too long")
|
||||
}
|
||||
for _, r := range value {
|
||||
if unicode.IsControl(r) || unicode.IsSpace(r) {
|
||||
return "", fmt.Errorf("map source url template must not contain whitespace or control characters")
|
||||
}
|
||||
}
|
||||
for _, placeholder := range []string{"{z}", "{x}", "{y}"} {
|
||||
if strings.Count(value, placeholder) != 1 {
|
||||
return "", fmt.Errorf("map source url template must contain %s exactly once", placeholder)
|
||||
}
|
||||
}
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("map source url template is invalid")
|
||||
}
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return "", fmt.Errorf("map source url template must use http or https")
|
||||
}
|
||||
if parsed.Host == "" {
|
||||
return "", fmt.Errorf("map source url template host is required")
|
||||
}
|
||||
if parsed.User != nil {
|
||||
return "", fmt.Errorf("map source url template must not contain credentials")
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (s *store) ensureMapTileSourceUnique(id uint64, name, urlTemplate string) error {
|
||||
return ensureMapTileSourceUniqueTx(s.db, id, name, urlTemplate)
|
||||
}
|
||||
|
||||
func ensureMapTileSourceUniqueTx(tx *gorm.DB, id uint64, name, urlTemplate string) error {
|
||||
var existing mapTileSourceRecord
|
||||
q := tx.Where("name = ? OR url_template = ?", name, urlTemplate)
|
||||
if id != 0 {
|
||||
q = q.Where("id <> ?", id)
|
||||
}
|
||||
err := q.Take(&existing).Error
|
||||
if err == nil {
|
||||
return errMapTileSourceAlreadyExists
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user