mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-07-31 09:38:40 -04:00
149 lines
3.6 KiB
Go
149 lines
3.6 KiB
Go
package json
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/qdm12/ddns-updater/internal/models"
|
|
)
|
|
|
|
type Database struct {
|
|
data dataModel
|
|
filepath string
|
|
sync.RWMutex
|
|
}
|
|
|
|
func (db *Database) Close() error {
|
|
db.Lock() // ensure a write operation finishes
|
|
defer db.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// NewDatabase opens or creates the JSON file database.
|
|
func NewDatabase(dataDir string) (*Database, error) {
|
|
filePath := filepath.Join(dataDir, "updates.json")
|
|
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
return nil, fmt.Errorf("reading file: %w", err)
|
|
}
|
|
const perm fs.FileMode = 0700
|
|
err = os.MkdirAll(filepath.Dir(filePath), perm)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating data directory: %w", err)
|
|
}
|
|
return &Database{filepath: filePath}, nil
|
|
}
|
|
|
|
stat, err := file.Stat()
|
|
if err != nil {
|
|
_ = file.Close()
|
|
return nil, fmt.Errorf("stating file: %w", err)
|
|
} else if stat.Size() == 0 { // empty file
|
|
_ = file.Close()
|
|
return &Database{filepath: filePath}, nil
|
|
}
|
|
|
|
decoder := json.NewDecoder(file)
|
|
var data dataModel
|
|
err = decoder.Decode(&data)
|
|
if err != nil {
|
|
_ = file.Close()
|
|
return nil, fmt.Errorf("decoding data from file: %w", err)
|
|
}
|
|
|
|
err = file.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("closing database file: %w", err)
|
|
}
|
|
|
|
err = checkData(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s validation error: %w", filePath, err)
|
|
}
|
|
|
|
return &Database{
|
|
data: data,
|
|
filepath: filePath,
|
|
}, nil
|
|
}
|
|
|
|
var (
|
|
ErrDomainEmpty = errors.New("domain is empty")
|
|
ErrHostIsEmpty = errors.New("host is empty")
|
|
ErrIPRecordsMisordered = errors.New("IP records are not ordered correctly by time")
|
|
ErrIPEmpty = errors.New("IP is empty")
|
|
ErrIPTimeEmpty = errors.New("time of IP is empty")
|
|
)
|
|
|
|
func checkData(data dataModel) (err error) {
|
|
for i, record := range data.Records {
|
|
switch {
|
|
case record.Domain == "":
|
|
return fmt.Errorf("%w: for record %d of %d", ErrDomainEmpty,
|
|
i+1, len(data.Records))
|
|
case record.Host == "":
|
|
return fmt.Errorf("%w: for record %d of %d with domain %s",
|
|
ErrHostIsEmpty, i+1, len(data.Records), record.Domain)
|
|
}
|
|
|
|
err = checkHistoryEvents(record.Events)
|
|
if err != nil {
|
|
return fmt.Errorf("for record %d of %d with domain %s and host %s: "+
|
|
"history events: %w", i+1, len(data.Records),
|
|
record.Domain, record.Host, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkHistoryEvents(events []models.HistoryEvent) (err error) {
|
|
var previousEventTime time.Time
|
|
for i, event := range events {
|
|
switch {
|
|
case event.Time.IsZero():
|
|
return fmt.Errorf("%w: for event %d of %d (IP %s)",
|
|
ErrIPTimeEmpty, i+1, len(events), event.IP)
|
|
case event.Time.Before(previousEventTime):
|
|
return fmt.Errorf("%w: event %d of %d (IP %s and time %s) "+
|
|
" is before event %d of %d (IP %s and time %s)",
|
|
ErrIPRecordsMisordered, i+1, len(events), event.IP, event.Time,
|
|
i, len(events), events[i-1].IP, events[i-1].Time)
|
|
case !event.IP.IsValid():
|
|
return fmt.Errorf("%w: for event %d of %d",
|
|
ErrIPEmpty, i+1, len(events))
|
|
}
|
|
previousEventTime = event.Time
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (db *Database) write() error {
|
|
const createPerms fs.FileMode = 0600
|
|
file, err := os.OpenFile(db.filepath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, createPerms)
|
|
if err != nil {
|
|
return fmt.Errorf("opening file: %w", err)
|
|
}
|
|
|
|
encoder := json.NewEncoder(file)
|
|
encoder.SetIndent("", " ")
|
|
err = encoder.Encode(db.data)
|
|
if err != nil {
|
|
_ = file.Close()
|
|
return fmt.Errorf("encoding data to file: %w", err)
|
|
}
|
|
|
|
err = file.Close()
|
|
if err != nil {
|
|
return fmt.Errorf("closing database file: %w", err)
|
|
}
|
|
return nil
|
|
}
|