github.com/safing/portbase@v0.19.5/database/registry.go (about)

     1  package database
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io/fs"
     8  	"os"
     9  	"path"
    10  	"regexp"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/tevino/abool"
    15  )
    16  
    17  const (
    18  	registryFileName = "databases.json"
    19  )
    20  
    21  var (
    22  	registryPersistence = abool.NewBool(false)
    23  	writeRegistrySoon   = abool.NewBool(false)
    24  
    25  	registry     = make(map[string]*Database)
    26  	registryLock sync.Mutex
    27  
    28  	nameConstraint = regexp.MustCompile("^[A-Za-z0-9_-]{3,}$")
    29  )
    30  
    31  // Register registers a new database.
    32  // If the database is already registered, only
    33  // the description and the primary API will be
    34  // updated and the effective object will be returned.
    35  func Register(db *Database) (*Database, error) {
    36  	if !initialized.IsSet() {
    37  		return nil, errors.New("database not initialized")
    38  	}
    39  
    40  	registryLock.Lock()
    41  	defer registryLock.Unlock()
    42  
    43  	registeredDB, ok := registry[db.Name]
    44  	save := false
    45  
    46  	if ok {
    47  		// update database
    48  		if registeredDB.Description != db.Description {
    49  			registeredDB.Description = db.Description
    50  			save = true
    51  		}
    52  		if registeredDB.ShadowDelete != db.ShadowDelete {
    53  			registeredDB.ShadowDelete = db.ShadowDelete
    54  			save = true
    55  		}
    56  	} else {
    57  		// register new database
    58  		if !nameConstraint.MatchString(db.Name) {
    59  			return nil, errors.New("database name must only contain alphanumeric and `_-` characters and must be at least 3 characters long")
    60  		}
    61  
    62  		now := time.Now().Round(time.Second)
    63  		db.Registered = now
    64  		db.LastUpdated = now
    65  		db.LastLoaded = time.Time{}
    66  
    67  		registry[db.Name] = db
    68  		save = true
    69  	}
    70  
    71  	if save && registryPersistence.IsSet() {
    72  		if ok {
    73  			registeredDB.Updated()
    74  		}
    75  		err := saveRegistry(false)
    76  		if err != nil {
    77  			return nil, err
    78  		}
    79  	}
    80  
    81  	if ok {
    82  		return registeredDB, nil
    83  	}
    84  	return nil, nil
    85  }
    86  
    87  func getDatabase(name string) (*Database, error) {
    88  	registryLock.Lock()
    89  	defer registryLock.Unlock()
    90  
    91  	registeredDB, ok := registry[name]
    92  	if !ok {
    93  		return nil, fmt.Errorf(`database "%s" not registered`, name)
    94  	}
    95  	if time.Now().Add(-24 * time.Hour).After(registeredDB.LastLoaded) {
    96  		writeRegistrySoon.Set()
    97  	}
    98  	registeredDB.Loaded()
    99  
   100  	return registeredDB, nil
   101  }
   102  
   103  // EnableRegistryPersistence enables persistence of the database registry.
   104  func EnableRegistryPersistence() {
   105  	if registryPersistence.SetToIf(false, true) {
   106  		// start registry writer
   107  		go registryWriter()
   108  		// TODO: make an initial write if database system is already initialized
   109  	}
   110  }
   111  
   112  func loadRegistry() error {
   113  	registryLock.Lock()
   114  	defer registryLock.Unlock()
   115  
   116  	// read file
   117  	filePath := path.Join(rootStructure.Path, registryFileName)
   118  	data, err := os.ReadFile(filePath)
   119  	if err != nil {
   120  		if errors.Is(err, fs.ErrNotExist) {
   121  			return nil
   122  		}
   123  		return err
   124  	}
   125  
   126  	// parse
   127  	databases := make(map[string]*Database)
   128  	err = json.Unmarshal(data, &databases)
   129  	if err != nil {
   130  		return err
   131  	}
   132  
   133  	// set
   134  	registry = databases
   135  	return nil
   136  }
   137  
   138  func saveRegistry(lock bool) error {
   139  	if lock {
   140  		registryLock.Lock()
   141  		defer registryLock.Unlock()
   142  	}
   143  
   144  	// marshal
   145  	data, err := json.MarshalIndent(registry, "", "\t")
   146  	if err != nil {
   147  		return err
   148  	}
   149  
   150  	// write file
   151  	// TODO: write atomically (best effort)
   152  	filePath := path.Join(rootStructure.Path, registryFileName)
   153  	return os.WriteFile(filePath, data, 0o0600)
   154  }
   155  
   156  func registryWriter() {
   157  	for {
   158  		select {
   159  		case <-time.After(1 * time.Hour):
   160  			if writeRegistrySoon.SetToIf(true, false) {
   161  				_ = saveRegistry(true)
   162  			}
   163  		case <-shutdownSignal:
   164  			_ = saveRegistry(true)
   165  			return
   166  		}
   167  	}
   168  }