github.com/safing/portbase@v0.19.5/config/persistence.go (about) 1 package config 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "path" 8 "strings" 9 "sync" 10 11 "github.com/safing/portbase/log" 12 ) 13 14 var ( 15 configFilePath string 16 17 loadedConfigValidationErrors []*ValidationError 18 loadedConfigValidationErrorsLock sync.Mutex 19 ) 20 21 // GetLoadedConfigValidationErrors returns the encountered validation errors 22 // from the last time loading config from disk. 23 func GetLoadedConfigValidationErrors() []*ValidationError { 24 loadedConfigValidationErrorsLock.Lock() 25 defer loadedConfigValidationErrorsLock.Unlock() 26 27 return loadedConfigValidationErrors 28 } 29 30 func loadConfig(requireValidConfig bool) error { 31 // check if persistence is configured 32 if configFilePath == "" { 33 return nil 34 } 35 36 // read config file 37 data, err := os.ReadFile(configFilePath) 38 if err != nil { 39 return err 40 } 41 42 // convert to map 43 newValues, err := JSONToMap(data) 44 if err != nil { 45 return err 46 } 47 48 validationErrors, _ := ReplaceConfig(newValues) 49 if requireValidConfig && len(validationErrors) > 0 { 50 return fmt.Errorf("encountered %d validation errors during config loading", len(validationErrors)) 51 } 52 53 // Save validation errors. 54 loadedConfigValidationErrorsLock.Lock() 55 defer loadedConfigValidationErrorsLock.Unlock() 56 loadedConfigValidationErrors = validationErrors 57 58 return nil 59 } 60 61 // SaveConfig saves the current configuration to file. 62 // It will acquire a read-lock on the global options registry 63 // lock and must lock each option! 64 func SaveConfig() error { 65 optionsLock.RLock() 66 defer optionsLock.RUnlock() 67 68 // check if persistence is configured 69 if configFilePath == "" { 70 return nil 71 } 72 73 // extract values 74 activeValues := make(map[string]interface{}) 75 for key, option := range options { 76 // we cannot immedately unlock the option afger 77 // getData() because someone could lock and change it 78 // while we are marshaling the value (i.e. for string slices). 79 // We NEED to keep the option locks until we finsihed. 80 option.Lock() 81 defer option.Unlock() 82 83 if option.activeValue != nil { 84 activeValues[key] = option.activeValue.getData(option) 85 } 86 } 87 88 // convert to JSON 89 data, err := MapToJSON(activeValues) 90 if err != nil { 91 log.Errorf("config: failed to save config: %s", err) 92 return err 93 } 94 95 // write file 96 return os.WriteFile(configFilePath, data, 0o0600) 97 } 98 99 // JSONToMap parses and flattens a hierarchical json object. 100 func JSONToMap(jsonData []byte) (map[string]interface{}, error) { 101 loaded := make(map[string]interface{}) 102 err := json.Unmarshal(jsonData, &loaded) 103 if err != nil { 104 return nil, err 105 } 106 107 return Flatten(loaded), nil 108 } 109 110 // Flatten returns a flattened copy of the given hierarchical config. 111 func Flatten(config map[string]interface{}) (flattenedConfig map[string]interface{}) { 112 flattenedConfig = make(map[string]interface{}) 113 flattenMap(flattenedConfig, config, "") 114 return flattenedConfig 115 } 116 117 func flattenMap(rootMap, subMap map[string]interface{}, subKey string) { 118 for key, entry := range subMap { 119 120 // get next level key 121 subbedKey := path.Join(subKey, key) 122 123 // check for next subMap 124 nextSub, ok := entry.(map[string]interface{}) 125 if ok { 126 flattenMap(rootMap, nextSub, subbedKey) 127 } else { 128 // only set if not on root level 129 rootMap[subbedKey] = entry 130 } 131 } 132 } 133 134 // MapToJSON expands a flattened map and returns it as json. 135 func MapToJSON(config map[string]interface{}) ([]byte, error) { 136 return json.MarshalIndent(Expand(config), "", " ") 137 } 138 139 // Expand returns a hierarchical copy of the given flattened config. 140 func Expand(flattenedConfig map[string]interface{}) (config map[string]interface{}) { 141 config = make(map[string]interface{}) 142 for key, entry := range flattenedConfig { 143 PutValueIntoHierarchicalConfig(config, key, entry) 144 } 145 return config 146 } 147 148 // PutValueIntoHierarchicalConfig injects a configuration entry into an hierarchical config map. Conflicting entries will be replaced. 149 func PutValueIntoHierarchicalConfig(config map[string]interface{}, key string, value interface{}) { 150 parts := strings.Split(key, "/") 151 152 // create/check maps for all parts except the last one 153 subMap := config 154 for i, part := range parts { 155 if i == len(parts)-1 { 156 // do not process the last part, 157 // which is not a map, but the value key itself 158 break 159 } 160 161 var nextSubMap map[string]interface{} 162 // get value 163 value, ok := subMap[part] 164 if !ok { 165 // create new map and assign it 166 nextSubMap = make(map[string]interface{}) 167 subMap[part] = nextSubMap 168 } else { 169 nextSubMap, ok = value.(map[string]interface{}) 170 if !ok { 171 // create new map and assign it 172 nextSubMap = make(map[string]interface{}) 173 subMap[part] = nextSubMap 174 } 175 } 176 177 // assign for next parts loop 178 subMap = nextSubMap 179 } 180 181 // assign value to last submap 182 subMap[parts[len(parts)-1]] = value 183 } 184 185 // CleanFlattenedConfig removes all inexistent configuration options from the given flattened config map. 186 func CleanFlattenedConfig(flattenedConfig map[string]interface{}) { 187 optionsLock.RLock() 188 defer optionsLock.RUnlock() 189 190 for key := range flattenedConfig { 191 _, ok := options[key] 192 if !ok { 193 delete(flattenedConfig, key) 194 } 195 } 196 } 197 198 // CleanHierarchicalConfig removes all inexistent configuration options from the given hierarchical config map. 199 func CleanHierarchicalConfig(config map[string]interface{}) { 200 optionsLock.RLock() 201 defer optionsLock.RUnlock() 202 203 cleanSubMap(config, "") 204 } 205 206 func cleanSubMap(subMap map[string]interface{}, subKey string) (empty bool) { 207 var foundValid int 208 for key, value := range subMap { 209 value, ok := value.(map[string]interface{}) 210 if ok { 211 // we found another section 212 isEmpty := cleanSubMap(value, path.Join(subKey, key)) 213 if isEmpty { 214 delete(subMap, key) 215 } else { 216 foundValid++ 217 } 218 continue 219 } 220 221 // we found an option value 222 if strings.Contains(key, "/") { 223 delete(subMap, key) 224 } else { 225 _, ok := options[path.Join(subKey, key)] 226 if ok { 227 foundValid++ 228 } else { 229 delete(subMap, key) 230 } 231 } 232 } 233 return foundValid == 0 234 }