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 {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