forgejo/modules/setting/config_provider.go
Earl Warren 20f022de72
[GITEA] add GetFile to config provider
(cherry picked from commit 88d1b53eeaa0d5ad0ed54c191236db928aadedf0)
(cherry picked from commit f63f71afad2aa8272772dba919485c5e858d2fae)
(cherry picked from commit 1f774145fc731e2d93cf8a6708dbf4c15722fa5e)
(cherry picked from commit 53e637693bcbb9fe9daca9a296c9d2c700fc915b)
(cherry picked from commit 4974cbf10a8b51d51591fa6f6281f4cd7a415c91)
(cherry picked from commit 554bca7fae7fa6f0235ab631598115875711d80e)
(cherry picked from commit 61b8cf83b988a2d8547b81cfb7aaeefaf2e17fba)
(cherry picked from commit d8bcc6f68c6dc972e8c99eb7b46a4f096f85cd61)
(cherry picked from commit ee04c340724cccada2c71db45ec510d19e92e099)
(cherry picked from commit 713153a6d63205984f2a85ee07f08269df3b0a85)
(cherry picked from commit 4e18c4f8bb35b02494d75311986fadb87328d477)
(cherry picked from commit 32be9db12df3dea9c12bf803a5226b6ae8cd7b32)
(cherry picked from commit fd50e9b9b4d7cd804e57418056126ed2df9bec6a)
(cherry picked from commit 11717b864bf4d820305511cf22ec50f61604a770)
(cherry picked from commit b70f3e0fa5db9cd90c961138c1ab748aa5c6a4d8)
(cherry picked from commit 2d802b2963d18b28a85a82e01f7a7f18ab8c3192)
(cherry picked from commit f61e2f71e2d629cc4e480f9e61c9b383675da0ef)
(cherry picked from commit 31723651c96d3f20811540aad94878815795da15)
(cherry picked from commit 38f6f703a0dfb04a9d436c8c806f1be513c7ea3a)
(cherry picked from commit 75f6716905755cbe3d7e49d965ab81a47f625bb7)
(cherry picked from commit fed2b2daf5d44348a0096572e91708ac3db5e9c8)
(cherry picked from commit a9bafa50213a3a385ba3901b2730fb84149855cc)
(cherry picked from commit 8ce897b3fe918c5702d5c86d97f1703a1f266697)
(cherry picked from commit c806addcd07c6afb20d9aca833cfa9a33ac680a7)
(cherry picked from commit e70d51bd26b49de93b2fe012a4f03efbe6c97607)
2024-02-05 14:44:33 +01:00

367 lines
9.9 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
"gopkg.in/ini.v1" //nolint:depguard
)
type ConfigKey interface {
Name() string
Value() string
SetValue(v string)
In(defaultVal string, candidates []string) string
String() string
Strings(delim string) []string
MustString(defaultVal string) string
MustBool(defaultVal ...bool) bool
MustInt(defaultVal ...int) int
MustInt64(defaultVal ...int64) int64
MustDuration(defaultVal ...time.Duration) time.Duration
}
type ConfigSection interface {
Name() string
MapTo(any) error
HasKey(key string) bool
NewKey(name, value string) (ConfigKey, error)
Key(key string) ConfigKey
Keys() []ConfigKey
ChildSections() []ConfigSection
}
// ConfigProvider represents a config provider
type ConfigProvider interface {
Section(section string) ConfigSection
Sections() []ConfigSection
NewSection(name string) (ConfigSection, error)
GetSection(name string) (ConfigSection, error)
Save() error
SaveTo(filename string) error
GetFile() string
DisableSaving()
PrepareSaving() (ConfigProvider, error)
IsLoadedFromEmpty() bool
}
type iniConfigProvider struct {
file string
ini *ini.File
disableSaving bool // disable the "Save" method because the config options could be polluted
loadedFromEmpty bool // whether the file has not existed previously
}
type iniConfigSection struct {
sec *ini.Section
}
var (
_ ConfigProvider = (*iniConfigProvider)(nil)
_ ConfigSection = (*iniConfigSection)(nil)
_ ConfigKey = (*ini.Key)(nil)
)
// ConfigSectionKey only searches the keys in the given section, but it is O(n).
// ini package has a special behavior: with "[sec] a=1" and an empty "[sec.sub]",
// then in "[sec.sub]", Key()/HasKey() can always see "a=1" because it always tries parent sections.
// It returns nil if the key doesn't exist.
func ConfigSectionKey(sec ConfigSection, key string) ConfigKey {
if sec == nil {
return nil
}
for _, k := range sec.Keys() {
if k.Name() == key {
return k
}
}
return nil
}
func ConfigSectionKeyString(sec ConfigSection, key string, def ...string) string {
k := ConfigSectionKey(sec, key)
if k != nil && k.String() != "" {
return k.String()
}
if len(def) > 0 {
return def[0]
}
return ""
}
func ConfigSectionKeyBool(sec ConfigSection, key string, def ...bool) bool {
k := ConfigSectionKey(sec, key)
if k != nil && k.String() != "" {
b, _ := strconv.ParseBool(k.String())
return b
}
if len(def) > 0 {
return def[0]
}
return false
}
// ConfigInheritedKey works like ini.Section.Key(), but it always returns a new key instance, it is O(n) because NewKey is O(n)
// and the returned key is safe to be used with "MustXxx", it doesn't change the parent's values.
// Otherwise, ini.Section.Key().MustXxx would pollute the parent section's keys.
// It never returns nil.
func ConfigInheritedKey(sec ConfigSection, key string) ConfigKey {
k := sec.Key(key)
if k != nil && k.String() != "" {
newKey, _ := sec.NewKey(k.Name(), k.String())
return newKey
}
newKey, _ := sec.NewKey(key, "")
return newKey
}
func ConfigInheritedKeyString(sec ConfigSection, key string, def ...string) string {
k := sec.Key(key)
if k != nil && k.String() != "" {
return k.String()
}
if len(def) > 0 {
return def[0]
}
return ""
}
func (s *iniConfigSection) Name() string {
return s.sec.Name()
}
func (s *iniConfigSection) MapTo(v any) error {
return s.sec.MapTo(v)
}
func (s *iniConfigSection) HasKey(key string) bool {
return s.sec.HasKey(key)
}
func (s *iniConfigSection) NewKey(name, value string) (ConfigKey, error) {
return s.sec.NewKey(name, value)
}
func (s *iniConfigSection) Key(key string) ConfigKey {
return s.sec.Key(key)
}
func (s *iniConfigSection) Keys() (keys []ConfigKey) {
for _, k := range s.sec.Keys() {
keys = append(keys, k)
}
return keys
}
func (s *iniConfigSection) ChildSections() (sections []ConfigSection) {
for _, s := range s.sec.ChildSections() {
sections = append(sections, &iniConfigSection{s})
}
return sections
}
func configProviderLoadOptions() ini.LoadOptions {
return ini.LoadOptions{
KeyValueDelimiterOnWrite: " = ",
IgnoreContinuation: true,
}
}
// NewConfigProviderFromData this function is mainly for testing purpose
func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
cfg, err := ini.LoadSources(configProviderLoadOptions(), strings.NewReader(configContent))
if err != nil {
return nil, err
}
cfg.NameMapper = ini.SnackCase
return &iniConfigProvider{
ini: cfg,
loadedFromEmpty: true,
}, nil
}
// NewConfigProviderFromFile load configuration from file.
// NOTE: do not print any log except error.
func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) {
cfg := ini.Empty(configProviderLoadOptions())
loadedFromEmpty := true
if file != "" {
isFile, err := util.IsFile(file)
if err != nil {
return nil, fmt.Errorf("unable to check if %q is a file. Error: %v", file, err)
}
if isFile {
if err = cfg.Append(file); err != nil {
return nil, fmt.Errorf("failed to load config file %q: %v", file, err)
}
loadedFromEmpty = false
}
}
for _, s := range extraConfigs {
if err := cfg.Append([]byte(s)); err != nil {
return nil, fmt.Errorf("unable to append more config: %v", err)
}
}
cfg.NameMapper = ini.SnackCase
return &iniConfigProvider{
file: file,
ini: cfg,
loadedFromEmpty: loadedFromEmpty,
}, nil
}
func (p *iniConfigProvider) Section(section string) ConfigSection {
return &iniConfigSection{sec: p.ini.Section(section)}
}
func (p *iniConfigProvider) Sections() (sections []ConfigSection) {
for _, s := range p.ini.Sections() {
sections = append(sections, &iniConfigSection{s})
}
return sections
}
func (p *iniConfigProvider) NewSection(name string) (ConfigSection, error) {
sec, err := p.ini.NewSection(name)
if err != nil {
return nil, err
}
return &iniConfigSection{sec: sec}, nil
}
func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) {
sec, err := p.ini.GetSection(name)
if err != nil {
return nil, err
}
return &iniConfigSection{sec: sec}, nil
}
var errDisableSaving = errors.New("this config can't be saved, developers should prepare a new config to save")
func (p *iniConfigProvider) GetFile() string {
return p.file
}
// Save saves the content into file
func (p *iniConfigProvider) Save() error {
if p.disableSaving {
return errDisableSaving
}
filename := p.file
if filename == "" {
return fmt.Errorf("config file path must not be empty")
}
if p.loadedFromEmpty {
if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
return fmt.Errorf("failed to create %q: %v", filename, err)
}
}
if err := p.ini.SaveTo(filename); err != nil {
return fmt.Errorf("failed to save %q: %v", filename, err)
}
// Change permissions to be more restrictive
fi, err := os.Stat(filename)
if err != nil {
return fmt.Errorf("failed to determine current conf file permissions: %v", err)
}
if fi.Mode().Perm() > 0o600 {
if err = os.Chmod(filename, 0o600); err != nil {
log.Warn("Failed changing conf file permissions to -rw-------. Consider changing them manually.")
}
}
return nil
}
func (p *iniConfigProvider) SaveTo(filename string) error {
if p.disableSaving {
return errDisableSaving
}
return p.ini.SaveTo(filename)
}
// DisableSaving disables the saving function, use PrepareSaving to get clear config options.
func (p *iniConfigProvider) DisableSaving() {
p.disableSaving = true
}
// PrepareSaving loads the ini from file again to get clear config options.
// Otherwise, the "MustXxx" calls would have polluted the current config provider,
// it makes the "Save" outputs a lot of garbage options
// After the INI package gets refactored, no "MustXxx" pollution, this workaround can be dropped.
func (p *iniConfigProvider) PrepareSaving() (ConfigProvider, error) {
if p.file == "" {
return nil, errors.New("no config file to save")
}
return NewConfigProviderFromFile(p.file)
}
func (p *iniConfigProvider) IsLoadedFromEmpty() bool {
return p.loadedFromEmpty
}
func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) {
if err := rootCfg.Section(sectionName).MapTo(setting); err != nil {
log.Fatal("Failed to map %s settings: %v", sectionName, err)
}
}
// DeprecatedWarnings contains the warning message for various deprecations, including: setting option, file/folder, etc
var DeprecatedWarnings []string
func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
if rootCfg.Section(oldSection).HasKey(oldKey) {
msg := fmt.Sprintf("Deprecated config option `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
log.Error("%v", msg)
DeprecatedWarnings = append(DeprecatedWarnings, msg)
}
}
// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
if rootCfg.Section(oldSection).HasKey(oldKey) {
log.Error("Deprecated `[%s]` `%s` present which has been copied to database table sys_setting", oldSection, oldKey)
}
}
// NewConfigProviderForLocale loads locale configuration from source and others. "string" if for a local file path, "[]byte" is for INI content
func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, error) {
iniFile, err := ini.LoadSources(ini.LoadOptions{
IgnoreInlineComment: true,
UnescapeValueCommentSymbols: true,
IgnoreContinuation: true,
}, source, others...)
if err != nil {
return nil, fmt.Errorf("unable to load locale ini: %w", err)
}
iniFile.BlockMode = false
return &iniConfigProvider{
ini: iniFile,
loadedFromEmpty: true,
}, nil
}
func init() {
ini.PrettyFormat = false
}