safing-portbase/database/registry.go
2022-10-11 12:27:30 +02:00

168 lines
3.3 KiB
Go

package database
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path"
"regexp"
"sync"
"time"
"github.com/tevino/abool"
)
const (
registryFileName = "databases.json"
)
var (
registryPersistence = abool.NewBool(false)
writeRegistrySoon = abool.NewBool(false)
registry = make(map[string]*Database)
registryLock sync.Mutex
nameConstraint = regexp.MustCompile("^[A-Za-z0-9_-]{3,}$")
)
// Register registers a new database.
// If the database is already registered, only
// the description and the primary API will be
// updated and the effective object will be returned.
func Register(db *Database) (*Database, error) {
if !initialized.IsSet() {
return nil, errors.New("database not initialized")
}
registryLock.Lock()
defer registryLock.Unlock()
registeredDB, ok := registry[db.Name]
save := false
if ok {
// update database
if registeredDB.Description != db.Description {
registeredDB.Description = db.Description
save = true
}
if registeredDB.ShadowDelete != db.ShadowDelete {
registeredDB.ShadowDelete = db.ShadowDelete
save = true
}
} else {
// register new database
if !nameConstraint.MatchString(db.Name) {
return nil, errors.New("database name must only contain alphanumeric and `_-` characters and must be at least 3 characters long")
}
now := time.Now().Round(time.Second)
db.Registered = now
db.LastUpdated = now
db.LastLoaded = time.Time{}
registry[db.Name] = db
save = true
}
if save && registryPersistence.IsSet() {
if ok {
registeredDB.Updated()
}
err := saveRegistry(false)
if err != nil {
return nil, err
}
}
if ok {
return registeredDB, nil
}
return nil, nil
}
func getDatabase(name string) (*Database, error) {
registryLock.Lock()
defer registryLock.Unlock()
registeredDB, ok := registry[name]
if !ok {
return nil, fmt.Errorf(`database "%s" not registered`, name)
}
if time.Now().Add(-24 * time.Hour).After(registeredDB.LastLoaded) {
writeRegistrySoon.Set()
}
registeredDB.Loaded()
return registeredDB, nil
}
// EnableRegistryPersistence enables persistence of the database registry.
func EnableRegistryPersistence() {
if registryPersistence.SetToIf(false, true) {
// start registry writer
go registryWriter()
// TODO: make an initial write if database system is already initialized
}
}
func loadRegistry() error {
registryLock.Lock()
defer registryLock.Unlock()
// read file
filePath := path.Join(rootStructure.Path, registryFileName)
data, err := os.ReadFile(filePath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil
}
return err
}
// parse
databases := make(map[string]*Database)
err = json.Unmarshal(data, &databases)
if err != nil {
return err
}
// set
registry = databases
return nil
}
func saveRegistry(lock bool) error {
if lock {
registryLock.Lock()
defer registryLock.Unlock()
}
// marshal
data, err := json.MarshalIndent(registry, "", "\t")
if err != nil {
return err
}
// write file
// TODO: write atomically (best effort)
filePath := path.Join(rootStructure.Path, registryFileName)
return os.WriteFile(filePath, data, 0o0600)
}
func registryWriter() {
for {
select {
case <-time.After(1 * time.Hour):
if writeRegistrySoon.SetToIf(true, false) {
_ = saveRegistry(true)
}
case <-shutdownSignal:
_ = saveRegistry(true)
return
}
}
}