164 lines
3.9 KiB
Go
164 lines
3.9 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
|
||
|
|
)
|
||
|
|
|
||
|
|
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 {
|
||
|
|
log.Printf("NewSettingsWithContext %s error from initial update: %v\n", filepath.Base(filename), err)
|
||
|
|
}
|
||
|
|
go settings.maintenanceRoutine(ctx)
|
||
|
|
return settings
|
||
|
|
}
|