Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Override users.json and repeaters.json trough enviroment variables. #1283

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
135 changes: 110 additions & 25 deletions internal/repeaterdb/repeaterdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
Expand All @@ -22,12 +22,12 @@ package repeaterdb
import (
"bytes"
"context"
// Embed the repeaters.json.xz file into the binary.
_ "embed"
_ "embed" // Embed the repeaters.json.xz file into the binary.
"encoding/json"
"errors"
"io"
"net/http"
"os"
"strings"
"sync/atomic"
"time"
Expand All @@ -40,8 +40,6 @@ import (
//go:embed repeaterdb-date.txt
var builtInDateStr string

// https://www.radioid.net/static/rptrs.json
//
//go:embed repeaters.json.xz
var comressedDMRRepeatersDB []byte

Expand All @@ -58,7 +56,48 @@ var (
ErrDecodingDB = errors.New("error decoding DMR repeaters database")
)

const waitTime = 100 * time.Millisecond
const (
waitTime = 100 * time.Millisecond
defaultRepeatersDBURL = "https://www.radioid.net/static/rptrs.json"
envOverrideRepeatersDBURL = "OVERRIDE_REPEATERS_DB_URL"
updateTimeout = 10 * time.Minute
)

// --- New additions below:

// forceRepeaterListCheck indicates whether we *only* check the network-updated
// list (ignoring the embedded or previously loaded local data).
var forceRepeaterListCheck = os.Getenv("FORCE_REPEATER_LIST_CHECK") != ""

// networkCacheDuration is how long (in minutes) we rely on the cached
// network data before fetching again.
const networkCacheDuration = 5 * time.Minute

// lastNetworkCheckTime is the last time we successfully updated from the network.
var lastNetworkCheckTime atomic.Int64 // store UnixNano time

func getLastNetworkCheck() time.Time {
unixNano := lastNetworkCheckTime.Load()
if unixNano == 0 {
return time.Time{}
}
return time.Unix(0, unixNano)
}

func setLastNetworkCheck(t time.Time) {
lastNetworkCheckTime.Store(t.UnixNano())
}

// --- End new additions ---

// getRepeatersDBURL checks if an override is provided via environment
// variable OVERRIDE_REPEATERS_DB_URL. If not, returns the default URL.
func getRepeatersDBURL() string {
if override := os.Getenv(envOverrideRepeatersDBURL); override != "" {
return override
}
return defaultRepeatersDBURL
}

type RepeaterDB struct {
uncompressedJSON []byte
Expand Down Expand Up @@ -96,10 +135,7 @@ type DMRRepeater struct {

func IsValidRepeaterID(dmrID uint) bool {
// Check that the repeater id is 6 digits
if dmrID < 100000 || dmrID > 999999 {
return false
}
return true
return dmrID >= 100000 && dmrID <= 999999
}

func ValidRepeaterCallsign(dmrID uint, callsign string) bool {
Expand All @@ -116,11 +152,7 @@ func ValidRepeaterCallsign(dmrID uint, callsign string) bool {
return false
}

if !strings.EqualFold(repeater.Trustee, callsign) {
return false
}

return true
return strings.EqualFold(repeater.Trustee, callsign)
}

func (e *dmrRepeaterDB) Unmarshal(b []byte) error {
Expand All @@ -131,11 +163,57 @@ func (e *dmrRepeaterDB) Unmarshal(b []byte) error {
return nil
}

// UnpackDB is responsible for ensuring the internal repeaterDB is initialized.
//
// If FORCE_REPEATER_LIST_CHECK is set:
// - We ignore the built-in data and rely solely on the network-updated data.
// - We only perform an HTTP fetch if 5 minutes (networkCacheDuration) have
// passed since the last successful update. Otherwise, we use the in-RAM
// cached data from the last network update.
//
// If FORCE_REPEATER_LIST_CHECK is not set:
// - We do the usual embedded data unpack and fallback, and only call Update()
// when you explicitly do so or at first initialization.
func UnpackDB() error {
// 1. If forceRepeaterListCheck is set, skip the embedded/built-in data
// and only rely on the network database (cached for 5 minutes).
if forceRepeaterListCheck {
lastCheck := getLastNetworkCheck()
if time.Since(lastCheck) > networkCacheDuration {
logging.Log("FORCE_REPEATER_LIST_CHECK is set; checking network for repeater DB update")
err := Update()
if err != nil {
// If we fail here, we do NOT fallback to local data. We simply return the error.
logging.Errorf("Forced network update failed: %v", err)
return err
}
setLastNetworkCheck(time.Now())
}

// Mark as initialized/done if not set yet.
if !repeaterDB.isInited.Load() {
repeaterDB.isInited.Store(true)
}
if !repeaterDB.isDone.Load() {
repeaterDB.isDone.Store(true)
}

// Verify we have data
rptdb, ok := repeaterDB.dmrRepeaters.Load().(dmrRepeaterDB)
if !ok || len(rptdb.Repeaters) == 0 {
return ErrNoRepeaters
}
return nil
}

// 2. If we are not forcing network check, do the original built-in
// data unpack logic (the normal flow).
lastInit := repeaterDB.isInited.Swap(true)
if !lastInit {
// First-time init from embedded data
repeaterDB.dmrRepeaterMap = xsync.NewMapOf[uint, DMRRepeater]()
repeaterDB.dmrRepeaterMapUpdating = xsync.NewMapOf[uint, DMRRepeater]()

var err error
repeaterDB.builtInDate, err = time.Parse(time.RFC3339, builtInDateStr)
if err != nil {
Expand Down Expand Up @@ -165,6 +243,7 @@ func UnpackDB() error {
repeaterDB.isDone.Store(true)
}

// Wait for any in-progress initialization to complete
for !repeaterDB.isDone.Load() {
time.Sleep(waitTime)
}
Expand Down Expand Up @@ -209,6 +288,11 @@ func Get(id uint) (DMRRepeater, bool) {
return repeater, true
}

// Update explicitly fetches the repeater data from the network.
//
// If FORCE_REPEATER_LIST_CHECK is set, UnpackDB() will call Update() automatically
// every 5 minutes, ignoring the built-in data. If it's *not* set, you can call
// Update() manually to refresh.
func Update() error {
if !repeaterDB.isDone.Load() {
err := UnpackDB()
Expand All @@ -217,11 +301,15 @@ func Update() error {
return ErrUpdateFailed
}
}
const updateTimeout = 10 * time.Minute

// Use the helper to get the URL from an environment variable,
// falling back to the default if not set.
url := getRepeatersDBURL()

ctx, cancel := context.WithTimeout(context.Background(), updateTimeout)
defer cancel()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.radioid.net/static/rptrs.json", nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return ErrUpdateFailed
}
Expand All @@ -230,6 +318,9 @@ func Update() error {
if err != nil {
return ErrUpdateFailed
}
defer func() {
_ = resp.Body.Close()
}()

if resp.StatusCode != http.StatusOK {
return ErrUpdateFailed
Expand All @@ -239,12 +330,7 @@ func Update() error {
if err != nil {
return ErrUpdateFailed
}
defer func() {
err := resp.Body.Close()
if err != nil {
logging.Errorf("Error closing response body: %v", err)
}
}()

var tmpDB dmrRepeaterDB
if err := json.Unmarshal(repeaterDB.uncompressedJSON, &tmpDB); err != nil {
logging.Errorf("Error decoding DMR repeaters database: %v", err)
Expand All @@ -267,8 +353,7 @@ func Update() error {
repeaterDB.dmrRepeaterMap = repeaterDB.dmrRepeaterMapUpdating
repeaterDB.dmrRepeaterMapUpdating = xsync.NewMapOf[uint, DMRRepeater]()

logging.Errorf("Update complete. Loaded %d DMR repeaters", Len())

logging.Log("Update complete")
return nil
}

Expand Down
Loading