mirror of https://github.com/go-gitea/gitea.git
Refactor system setting (#27000)
This PR reduces the complexity of the system setting system. It only needs one line to introduce a new option, and the option can be used anywhere out-of-box. It is still high-performant (and more performant) because the config values are cached in the config system.pull/27433/head^2
parent
976d1760ac
commit
9f8d59858a
@ -1,15 +0,0 @@
|
|||||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package system
|
|
||||||
|
|
||||||
// enumerate all system setting keys
|
|
||||||
const (
|
|
||||||
KeyPictureDisableGravatar = "picture.disable_gravatar"
|
|
||||||
KeyPictureEnableFederatedAvatar = "picture.enable_federated_avatar"
|
|
||||||
)
|
|
||||||
|
|
||||||
// genSettingCacheKey returns the cache key for some configuration
|
|
||||||
func genSettingCacheKey(key string) string {
|
|
||||||
return "system.setting." + key
|
|
||||||
}
|
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PictureStruct struct {
|
||||||
|
DisableGravatar *config.Value[bool]
|
||||||
|
EnableFederatedAvatar *config.Value[bool]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigStruct struct {
|
||||||
|
Picture *PictureStruct
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultConfig *ConfigStruct
|
||||||
|
defaultConfigOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func initDefaultConfig() {
|
||||||
|
config.SetCfgSecKeyGetter(&cfgSecKeyGetter{})
|
||||||
|
defaultConfig = &ConfigStruct{
|
||||||
|
Picture: &PictureStruct{
|
||||||
|
DisableGravatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}, "picture.disable_gravatar"),
|
||||||
|
EnableFederatedAvatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}, "picture.enable_federated_avatar"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Config() *ConfigStruct {
|
||||||
|
defaultConfigOnce.Do(initDefaultConfig)
|
||||||
|
return defaultConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type cfgSecKeyGetter struct{}
|
||||||
|
|
||||||
|
func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) {
|
||||||
|
cfgSec, err := CfgProvider.GetSection(sec)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to get config section: %q", sec)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
cfgKey := ConfigSectionKey(cfgSec, key)
|
||||||
|
if cfgKey == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return cfgKey.Value(), true
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var getterMu sync.RWMutex
|
||||||
|
|
||||||
|
type CfgSecKeyGetter interface {
|
||||||
|
GetValue(sec, key string) (v string, has bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfgSecKeyGetterInternal CfgSecKeyGetter
|
||||||
|
|
||||||
|
func SetCfgSecKeyGetter(p CfgSecKeyGetter) {
|
||||||
|
getterMu.Lock()
|
||||||
|
cfgSecKeyGetterInternal = p
|
||||||
|
getterMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCfgSecKeyGetter() CfgSecKeyGetter {
|
||||||
|
getterMu.RLock()
|
||||||
|
defer getterMu.RUnlock()
|
||||||
|
return cfgSecKeyGetterInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
type DynKeyGetter interface {
|
||||||
|
GetValue(ctx context.Context, key string) (v string, has bool)
|
||||||
|
GetRevision(ctx context.Context) int
|
||||||
|
InvalidateCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
var dynKeyGetterInternal DynKeyGetter
|
||||||
|
|
||||||
|
func SetDynGetter(p DynKeyGetter) {
|
||||||
|
getterMu.Lock()
|
||||||
|
dynKeyGetterInternal = p
|
||||||
|
getterMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDynGetter() DynKeyGetter {
|
||||||
|
getterMu.RLock()
|
||||||
|
defer getterMu.RUnlock()
|
||||||
|
return dynKeyGetterInternal
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CfgSecKey struct {
|
||||||
|
Sec, Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Value[T any] struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
cfgSecKey CfgSecKey
|
||||||
|
dynKey string
|
||||||
|
|
||||||
|
def, value T
|
||||||
|
revision int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (value *Value[T]) parse(s string) (v T) {
|
||||||
|
switch any(v).(type) {
|
||||||
|
case bool:
|
||||||
|
b, _ := strconv.ParseBool(s)
|
||||||
|
return any(b).(T)
|
||||||
|
default:
|
||||||
|
panic("unsupported config type, please complete the code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (value *Value[T]) Value(ctx context.Context) (v T) {
|
||||||
|
dg := GetDynGetter()
|
||||||
|
if dg == nil {
|
||||||
|
// this is an edge case: the database is not initialized but the system setting is going to be used
|
||||||
|
// it should panic to avoid inconsistent config values (from config / system setting) and fix the code
|
||||||
|
panic("no config dyn value getter")
|
||||||
|
}
|
||||||
|
|
||||||
|
rev := dg.GetRevision(ctx)
|
||||||
|
|
||||||
|
// if the revision in database doesn't change, use the last value
|
||||||
|
value.mu.RLock()
|
||||||
|
if rev == value.revision {
|
||||||
|
v = value.value
|
||||||
|
value.mu.RUnlock()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
value.mu.RUnlock()
|
||||||
|
|
||||||
|
// try to parse the config and cache it
|
||||||
|
var valStr *string
|
||||||
|
if dynVal, has := dg.GetValue(ctx, value.dynKey); has {
|
||||||
|
valStr = &dynVal
|
||||||
|
} else if cfgVal, has := GetCfgSecKeyGetter().GetValue(value.cfgSecKey.Sec, value.cfgSecKey.Key); has {
|
||||||
|
valStr = &cfgVal
|
||||||
|
}
|
||||||
|
if valStr == nil {
|
||||||
|
v = value.def
|
||||||
|
} else {
|
||||||
|
v = value.parse(*valStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
value.mu.Lock()
|
||||||
|
value.value = v
|
||||||
|
value.revision = rev
|
||||||
|
value.mu.Unlock()
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (value *Value[T]) DynKey() string {
|
||||||
|
return value.dynKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func Bool(def bool, cfgSecKey CfgSecKey, dynKey string) *Value[bool] {
|
||||||
|
return &Value[bool]{def: def, cfgSecKey: cfgSecKey, dynKey: dynKey}
|
||||||
|
}
|
||||||
@ -1,37 +1,24 @@
|
|||||||
import $ from 'jquery';
|
|
||||||
import {showTemporaryTooltip} from '../../modules/tippy.js';
|
import {showTemporaryTooltip} from '../../modules/tippy.js';
|
||||||
|
import {POST} from '../../modules/fetch.js';
|
||||||
|
|
||||||
const {appSubUrl, csrfToken, pageData} = window.config;
|
const {appSubUrl} = window.config;
|
||||||
|
|
||||||
export function initAdminConfigs() {
|
export function initAdminConfigs() {
|
||||||
const isAdminConfigPage = pageData?.adminConfigPage;
|
const elAdminConfig = document.querySelector('.page-content.admin.config');
|
||||||
if (!isAdminConfigPage) return;
|
if (!elAdminConfig) return;
|
||||||
|
|
||||||
$("input[type='checkbox']").on('change', (e) => {
|
for (const el of elAdminConfig.querySelectorAll('input[type="checkbox"][data-config-dyn-key]')) {
|
||||||
const $this = $(e.currentTarget);
|
el.addEventListener('change', async () => {
|
||||||
$.ajax({
|
try {
|
||||||
url: `${appSubUrl}/admin/config`,
|
const resp = await POST(`${appSubUrl}/admin/config`, {
|
||||||
type: 'POST',
|
data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key'), value: el.checked}),
|
||||||
data: {
|
});
|
||||||
_csrf: csrfToken,
|
const json = await resp.json();
|
||||||
key: $this.attr('name'),
|
if (json.errorMessage) throw new Error(json.errorMessage);
|
||||||
value: $this.is(':checked'),
|
} catch (ex) {
|
||||||
version: $this.attr('version'),
|
showTemporaryTooltip(el, ex.toString());
|
||||||
}
|
el.checked = !el.checked;
|
||||||
}).done((resp) => {
|
|
||||||
if (resp) {
|
|
||||||
if (resp.redirect) {
|
|
||||||
window.location.href = resp.redirect;
|
|
||||||
} else if (resp.version) {
|
|
||||||
$this.attr('version', resp.version);
|
|
||||||
} else if (resp.err) {
|
|
||||||
showTemporaryTooltip(e.currentTarget, resp.err);
|
|
||||||
$this.prop('checked', !$this.is(':checked'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue