code.gitea.io/gitea@v1.22.3/modules/setting/config_provider.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package setting 5 6 import ( 7 "errors" 8 "fmt" 9 "os" 10 "path/filepath" 11 "strconv" 12 "strings" 13 "time" 14 15 "code.gitea.io/gitea/modules/log" 16 "code.gitea.io/gitea/modules/util" 17 18 "gopkg.in/ini.v1" //nolint:depguard 19 ) 20 21 type ConfigKey interface { 22 Name() string 23 Value() string 24 SetValue(v string) 25 26 In(defaultVal string, candidates []string) string 27 String() string 28 Strings(delim string) []string 29 30 MustString(defaultVal string) string 31 MustBool(defaultVal ...bool) bool 32 MustInt(defaultVal ...int) int 33 MustInt64(defaultVal ...int64) int64 34 MustDuration(defaultVal ...time.Duration) time.Duration 35 } 36 37 type ConfigSection interface { 38 Name() string 39 MapTo(any) error 40 HasKey(key string) bool 41 NewKey(name, value string) (ConfigKey, error) 42 Key(key string) ConfigKey 43 Keys() []ConfigKey 44 ChildSections() []ConfigSection 45 } 46 47 // ConfigProvider represents a config provider 48 type ConfigProvider interface { 49 Section(section string) ConfigSection 50 Sections() []ConfigSection 51 NewSection(name string) (ConfigSection, error) 52 GetSection(name string) (ConfigSection, error) 53 Save() error 54 SaveTo(filename string) error 55 56 DisableSaving() 57 PrepareSaving() (ConfigProvider, error) 58 IsLoadedFromEmpty() bool 59 } 60 61 type iniConfigProvider struct { 62 file string 63 ini *ini.File 64 65 disableSaving bool // disable the "Save" method because the config options could be polluted 66 loadedFromEmpty bool // whether the file has not existed previously 67 } 68 69 type iniConfigSection struct { 70 sec *ini.Section 71 } 72 73 var ( 74 _ ConfigProvider = (*iniConfigProvider)(nil) 75 _ ConfigSection = (*iniConfigSection)(nil) 76 _ ConfigKey = (*ini.Key)(nil) 77 ) 78 79 // ConfigSectionKey only searches the keys in the given section, but it is O(n). 80 // ini package has a special behavior: with "[sec] a=1" and an empty "[sec.sub]", 81 // then in "[sec.sub]", Key()/HasKey() can always see "a=1" because it always tries parent sections. 82 // It returns nil if the key doesn't exist. 83 func ConfigSectionKey(sec ConfigSection, key string) ConfigKey { 84 if sec == nil { 85 return nil 86 } 87 for _, k := range sec.Keys() { 88 if k.Name() == key { 89 return k 90 } 91 } 92 return nil 93 } 94 95 func ConfigSectionKeyString(sec ConfigSection, key string, def ...string) string { 96 k := ConfigSectionKey(sec, key) 97 if k != nil && k.String() != "" { 98 return k.String() 99 } 100 if len(def) > 0 { 101 return def[0] 102 } 103 return "" 104 } 105 106 func ConfigSectionKeyBool(sec ConfigSection, key string, def ...bool) bool { 107 k := ConfigSectionKey(sec, key) 108 if k != nil && k.String() != "" { 109 b, _ := strconv.ParseBool(k.String()) 110 return b 111 } 112 if len(def) > 0 { 113 return def[0] 114 } 115 return false 116 } 117 118 // ConfigInheritedKey works like ini.Section.Key(), but it always returns a new key instance, it is O(n) because NewKey is O(n) 119 // and the returned key is safe to be used with "MustXxx", it doesn't change the parent's values. 120 // Otherwise, ini.Section.Key().MustXxx would pollute the parent section's keys. 121 // It never returns nil. 122 func ConfigInheritedKey(sec ConfigSection, key string) ConfigKey { 123 k := sec.Key(key) 124 if k != nil && k.String() != "" { 125 newKey, _ := sec.NewKey(k.Name(), k.String()) 126 return newKey 127 } 128 newKey, _ := sec.NewKey(key, "") 129 return newKey 130 } 131 132 func ConfigInheritedKeyString(sec ConfigSection, key string, def ...string) string { 133 k := sec.Key(key) 134 if k != nil && k.String() != "" { 135 return k.String() 136 } 137 if len(def) > 0 { 138 return def[0] 139 } 140 return "" 141 } 142 143 func (s *iniConfigSection) Name() string { 144 return s.sec.Name() 145 } 146 147 func (s *iniConfigSection) MapTo(v any) error { 148 return s.sec.MapTo(v) 149 } 150 151 func (s *iniConfigSection) HasKey(key string) bool { 152 return s.sec.HasKey(key) 153 } 154 155 func (s *iniConfigSection) NewKey(name, value string) (ConfigKey, error) { 156 return s.sec.NewKey(name, value) 157 } 158 159 func (s *iniConfigSection) Key(key string) ConfigKey { 160 return s.sec.Key(key) 161 } 162 163 func (s *iniConfigSection) Keys() (keys []ConfigKey) { 164 for _, k := range s.sec.Keys() { 165 keys = append(keys, k) 166 } 167 return keys 168 } 169 170 func (s *iniConfigSection) ChildSections() (sections []ConfigSection) { 171 for _, s := range s.sec.ChildSections() { 172 sections = append(sections, &iniConfigSection{s}) 173 } 174 return sections 175 } 176 177 func configProviderLoadOptions() ini.LoadOptions { 178 return ini.LoadOptions{ 179 KeyValueDelimiterOnWrite: " = ", 180 IgnoreContinuation: true, 181 } 182 } 183 184 // NewConfigProviderFromData this function is mainly for testing purpose 185 func NewConfigProviderFromData(configContent string) (ConfigProvider, error) { 186 cfg, err := ini.LoadSources(configProviderLoadOptions(), strings.NewReader(configContent)) 187 if err != nil { 188 return nil, err 189 } 190 cfg.NameMapper = ini.SnackCase 191 return &iniConfigProvider{ 192 ini: cfg, 193 loadedFromEmpty: true, 194 }, nil 195 } 196 197 // NewConfigProviderFromFile load configuration from file. 198 // NOTE: do not print any log except error. 199 func NewConfigProviderFromFile(file string) (ConfigProvider, error) { 200 cfg := ini.Empty(configProviderLoadOptions()) 201 loadedFromEmpty := true 202 203 if file != "" { 204 isFile, err := util.IsFile(file) 205 if err != nil { 206 return nil, fmt.Errorf("unable to check if %q is a file. Error: %v", file, err) 207 } 208 if isFile { 209 if err = cfg.Append(file); err != nil { 210 return nil, fmt.Errorf("failed to load config file %q: %v", file, err) 211 } 212 loadedFromEmpty = false 213 } 214 } 215 216 cfg.NameMapper = ini.SnackCase 217 return &iniConfigProvider{ 218 file: file, 219 ini: cfg, 220 loadedFromEmpty: loadedFromEmpty, 221 }, nil 222 } 223 224 func (p *iniConfigProvider) Section(section string) ConfigSection { 225 return &iniConfigSection{sec: p.ini.Section(section)} 226 } 227 228 func (p *iniConfigProvider) Sections() (sections []ConfigSection) { 229 for _, s := range p.ini.Sections() { 230 sections = append(sections, &iniConfigSection{s}) 231 } 232 return sections 233 } 234 235 func (p *iniConfigProvider) NewSection(name string) (ConfigSection, error) { 236 sec, err := p.ini.NewSection(name) 237 if err != nil { 238 return nil, err 239 } 240 return &iniConfigSection{sec: sec}, nil 241 } 242 243 func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) { 244 sec, err := p.ini.GetSection(name) 245 if err != nil { 246 return nil, err 247 } 248 return &iniConfigSection{sec: sec}, nil 249 } 250 251 var errDisableSaving = errors.New("this config can't be saved, developers should prepare a new config to save") 252 253 // Save saves the content into file 254 func (p *iniConfigProvider) Save() error { 255 if p.disableSaving { 256 return errDisableSaving 257 } 258 filename := p.file 259 if filename == "" { 260 return fmt.Errorf("config file path must not be empty") 261 } 262 if p.loadedFromEmpty { 263 if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil { 264 return fmt.Errorf("failed to create %q: %v", filename, err) 265 } 266 } 267 if err := p.ini.SaveTo(filename); err != nil { 268 return fmt.Errorf("failed to save %q: %v", filename, err) 269 } 270 271 // Change permissions to be more restrictive 272 fi, err := os.Stat(filename) 273 if err != nil { 274 return fmt.Errorf("failed to determine current conf file permissions: %v", err) 275 } 276 277 if fi.Mode().Perm() > 0o600 { 278 if err = os.Chmod(filename, 0o600); err != nil { 279 log.Warn("Failed changing conf file permissions to -rw-------. Consider changing them manually.") 280 } 281 } 282 return nil 283 } 284 285 func (p *iniConfigProvider) SaveTo(filename string) error { 286 if p.disableSaving { 287 return errDisableSaving 288 } 289 return p.ini.SaveTo(filename) 290 } 291 292 // DisableSaving disables the saving function, use PrepareSaving to get clear config options. 293 func (p *iniConfigProvider) DisableSaving() { 294 p.disableSaving = true 295 } 296 297 // PrepareSaving loads the ini from file again to get clear config options. 298 // Otherwise, the "MustXxx" calls would have polluted the current config provider, 299 // it makes the "Save" outputs a lot of garbage options 300 // After the INI package gets refactored, no "MustXxx" pollution, this workaround can be dropped. 301 func (p *iniConfigProvider) PrepareSaving() (ConfigProvider, error) { 302 if p.file == "" { 303 return nil, errors.New("no config file to save") 304 } 305 return NewConfigProviderFromFile(p.file) 306 } 307 308 func (p *iniConfigProvider) IsLoadedFromEmpty() bool { 309 return p.loadedFromEmpty 310 } 311 312 func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) { 313 if err := rootCfg.Section(sectionName).MapTo(setting); err != nil { 314 log.Fatal("Failed to map %s settings: %v", sectionName, err) 315 } 316 } 317 318 // StartupProblems contains the messages for various startup problems, including: setting option, file/folder, etc 319 var StartupProblems []string 320 321 func LogStartupProblem(skip int, level log.Level, format string, args ...any) { 322 msg := fmt.Sprintf(format, args...) 323 log.Log(skip+1, level, "%s", msg) 324 StartupProblems = append(StartupProblems, msg) 325 } 326 327 func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) { 328 if rootCfg.Section(oldSection).HasKey(oldKey) { 329 LogStartupProblem(1, log.ERROR, "Deprecated config option `[%s].%s` is present, please use `[%s].%s` instead. This fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version) 330 } 331 } 332 333 // deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini 334 func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) { 335 if rootCfg.Section(oldSection).HasKey(oldKey) { 336 LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey) 337 } 338 } 339 340 // NewConfigProviderForLocale loads locale configuration from source and others. "string" if for a local file path, "[]byte" is for INI content 341 func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, error) { 342 iniFile, err := ini.LoadSources(ini.LoadOptions{ 343 IgnoreInlineComment: true, 344 UnescapeValueCommentSymbols: true, 345 IgnoreContinuation: true, 346 }, source, others...) 347 if err != nil { 348 return nil, fmt.Errorf("unable to load locale ini: %w", err) 349 } 350 iniFile.BlockMode = false 351 return &iniConfigProvider{ 352 ini: iniFile, 353 loadedFromEmpty: true, 354 }, nil 355 } 356 357 func init() { 358 ini.PrettyFormat = false 359 }