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 }