settings/settings.go

177 lines
4.3 KiB
Go

package settings
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
)
var (
MaintenanceRoutinePace = time.Second
WarnIfFileNotFound = false
TheSettings = NewSettings("settings.conf", true)
)
type Settings struct {
mutex sync.RWMutex
contents map[string]string
filename string
modtime time.Time
LogUpdates atomic.Bool
}
func (s *Settings) GetFilename() string {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.filename
}
func (s *Settings) SetFilename(filename string) (updated bool) {
s.mutex.Lock()
defer s.mutex.Unlock()
if filename == s.filename {
return false
}
if s.LogUpdates.Load() {
log.Printf("Settings::SetFilename changing from '%s' to '%s'\n", s.filename, filename)
}
s.filename = strings.Clone(filename)
s.modtime = time.Time{}
err := s.lockedUpdate()
if err != nil {
if !errors.Is(err, os.ErrNotExist) || WarnIfFileNotFound {
log.Printf("Settings::SetFilename %s: error from lockedUpdate: %v\n", filepath.Base(s.filename), err)
}
}
return true
}
func (s *Settings) lockedSetKeyValue(key, value string) bool {
logUpdates := s.LogUpdates.Load()
baseFilename := filepath.Base(s.filename)
if stored, found := s.contents[key]; found && stored == value {
return false
} else if found && logUpdates {
log.Printf("Settings::lockedKeyValue %s: updating key '%s' from value '%s' to '%s'\n", baseFilename, key, stored, value)
} else if !found && logUpdates {
log.Printf("Settings::lockedKeyValue %s: storing key '%s' with value '%s'\n", baseFilename, key, value)
}
s.contents[strings.Clone(key)] = strings.Clone(value)
return true
}
func (s *Settings) SetKeyValue(key, value string) (updated bool) {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.lockedSetKeyValue(key, value)
}
func (s *Settings) lockedContainsKey(key string) bool {
_, found := s.contents[key]
return found
}
func (s *Settings) ContainsKey(key string) bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.lockedContainsKey(key)
}
func (s *Settings) lockedGetKeyValue(key string) string {
return s.contents[key]
}
func (s *Settings) GetKeyValue(key string) string {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.lockedGetKeyValue(key)
}
func (s *Settings) lockedUpdate() error {
stat, err := os.Stat(s.filename)
if err != nil {
return fmt.Errorf("error getting os.FileInfo for %s: %w", s.filename, err)
} else if !stat.ModTime().After(s.modtime) {
return nil
}
contents, err := os.ReadFile(s.filename)
if err != nil {
return fmt.Errorf("error reading %s: %w", s.filename, err)
}
scanner := NewScanner(bytes.NewReader(contents))
loop:
for {
key, value, err := scanner.ReadNextLine()
if err != nil {
if !errors.Is(err, io.EOF) {
log.Printf("error from scanner.ReadNextLine: %v\n", err)
} else {
break loop
}
}
s.lockedSetKeyValue(key, value)
}
s.modtime = stat.ModTime()
return nil
}
func (s *Settings) Update() error {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.lockedUpdate()
}
func (s *Settings) maintenanceRoutine(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case <-time.After(MaintenanceRoutinePace):
err := s.Update()
if err != nil {
if !errors.Is(err, os.ErrNotExist) || WarnIfFileNotFound {
log.Printf("Settings::maintenanceRoutine %s error from Update: %v\n", filepath.Base(s.GetFilename()), err)
}
}
}
}
}
func NewSettings(filename string, logUpdates bool) *Settings {
return NewSettingsWithContext(context.Background(), filename, logUpdates)
}
func NewSettingsWithContext(ctx context.Context, filename string, logUpdates bool) *Settings {
settings := &Settings{
contents: make(map[string]string),
filename: strings.Clone(filename),
}
settings.LogUpdates.Store(logUpdates)
if err := settings.lockedUpdate(); err != nil && (!errors.Is(err, os.ErrNotExist) || WarnIfFileNotFound) {
log.Printf("NewSettingsWithContext %s error from initial update: %v\n", filepath.Base(filename), err)
}
go settings.maintenanceRoutine(ctx)
return settings
}
func ParseBool(value string) (bool, error) {
switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "true", "t", "yes", "y", "on":
return true, nil
case "0", "false", "f", "no", "n", "off":
return false, nil
default:
return false, fmt.Errorf("cannot parse '%s' as bool", value)
}
}