mirror of https://github.com/go-gitea/gitea
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 {POST} from '../../modules/fetch.js'; |
||||
|
||||
const {appSubUrl, csrfToken, pageData} = window.config; |
||||
const {appSubUrl} = window.config; |
||||
|
||||
export function initAdminConfigs() { |
||||
const isAdminConfigPage = pageData?.adminConfigPage; |
||||
if (!isAdminConfigPage) return; |
||||
const elAdminConfig = document.querySelector('.page-content.admin.config'); |
||||
if (!elAdminConfig) return; |
||||
|
||||
$("input[type='checkbox']").on('change', (e) => { |
||||
const $this = $(e.currentTarget); |
||||
$.ajax({ |
||||
url: `${appSubUrl}/admin/config`, |
||||
type: 'POST', |
||||
data: { |
||||
_csrf: csrfToken, |
||||
key: $this.attr('name'), |
||||
value: $this.is(':checked'), |
||||
version: $this.attr('version'), |
||||
} |
||||
}).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')); |
||||
} |
||||
} |
||||
for (const el of elAdminConfig.querySelectorAll('input[type="checkbox"][data-config-dyn-key]')) { |
||||
el.addEventListener('change', async () => { |
||||
try { |
||||
const resp = await POST(`${appSubUrl}/admin/config`, { |
||||
data: new URLSearchParams({key: el.getAttribute('data-config-dyn-key'), value: el.checked}), |
||||
}); |
||||
|
||||
e.preventDefault(); |
||||
return false; |
||||
const json = await resp.json(); |
||||
if (json.errorMessage) throw new Error(json.errorMessage); |
||||
} catch (ex) { |
||||
showTemporaryTooltip(el, ex.toString()); |
||||
el.checked = !el.checked; |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
Loading…
Reference in new issue