summaryrefslogtreecommitdiffstats
path: root/models/user/setting.go
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
commitdd136858f1ea40ad3c94191d647487fa4f31926c (patch)
tree58fec94a7b2a12510c9664b21793f1ed560c6518 /models/user/setting.go
parentInitial commit. (diff)
downloadforgejo-upstream.tar.xz
forgejo-upstream.zip
Adding upstream version 9.0.0.HEADupstream/9.0.0upstreamdebian
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r--models/user/setting.go212
1 files changed, 212 insertions, 0 deletions
diff --git a/models/user/setting.go b/models/user/setting.go
new file mode 100644
index 0000000..b4af0e5
--- /dev/null
+++ b/models/user/setting.go
@@ -0,0 +1,212 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/cache"
+ setting_module "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
+
+ "xorm.io/builder"
+)
+
+// Setting is a key value store of user settings
+type Setting struct {
+ ID int64 `xorm:"pk autoincr"`
+ UserID int64 `xorm:"index unique(key_userid)"` // to load all of someone's settings
+ SettingKey string `xorm:"varchar(255) index unique(key_userid)"` // ensure key is always lowercase
+ SettingValue string `xorm:"text"`
+}
+
+// TableName sets the table name for the settings struct
+func (s *Setting) TableName() string {
+ return "user_setting"
+}
+
+func init() {
+ db.RegisterModel(new(Setting))
+}
+
+// ErrUserSettingIsNotExist represents an error that a setting is not exist with special key
+type ErrUserSettingIsNotExist struct {
+ Key string
+}
+
+// Error implements error
+func (err ErrUserSettingIsNotExist) Error() string {
+ return fmt.Sprintf("Setting[%s] is not exist", err.Key)
+}
+
+func (err ErrUserSettingIsNotExist) Unwrap() error {
+ return util.ErrNotExist
+}
+
+// IsErrUserSettingIsNotExist return true if err is ErrSettingIsNotExist
+func IsErrUserSettingIsNotExist(err error) bool {
+ _, ok := err.(ErrUserSettingIsNotExist)
+ return ok
+}
+
+// genSettingCacheKey returns the cache key for some configuration
+func genSettingCacheKey(userID int64, key string) string {
+ return fmt.Sprintf("user_%d.setting.%s", userID, key)
+}
+
+// GetSetting returns the setting value via the key
+func GetSetting(ctx context.Context, uid int64, key string) (string, error) {
+ return cache.GetString(genSettingCacheKey(uid, key), func() (string, error) {
+ res, err := GetSettingNoCache(ctx, uid, key)
+ if err != nil {
+ return "", err
+ }
+ return res.SettingValue, nil
+ })
+}
+
+// GetSettingNoCache returns specific setting without using the cache
+func GetSettingNoCache(ctx context.Context, uid int64, key string) (*Setting, error) {
+ v, err := GetSettings(ctx, uid, []string{key})
+ if err != nil {
+ return nil, err
+ }
+ if len(v) == 0 {
+ return nil, ErrUserSettingIsNotExist{key}
+ }
+ return v[key], nil
+}
+
+// GetSettings returns specific settings from user
+func GetSettings(ctx context.Context, uid int64, keys []string) (map[string]*Setting, error) {
+ settings := make([]*Setting, 0, len(keys))
+ if err := db.GetEngine(ctx).
+ Where("user_id=?", uid).
+ And(builder.In("setting_key", keys)).
+ Find(&settings); err != nil {
+ return nil, err
+ }
+ settingsMap := make(map[string]*Setting)
+ for _, s := range settings {
+ settingsMap[s.SettingKey] = s
+ }
+ return settingsMap, nil
+}
+
+// GetUserAllSettings returns all settings from user
+func GetUserAllSettings(ctx context.Context, uid int64) (map[string]*Setting, error) {
+ settings := make([]*Setting, 0, 5)
+ if err := db.GetEngine(ctx).
+ Where("user_id=?", uid).
+ Find(&settings); err != nil {
+ return nil, err
+ }
+ settingsMap := make(map[string]*Setting)
+ for _, s := range settings {
+ settingsMap[s.SettingKey] = s
+ }
+ return settingsMap, nil
+}
+
+func validateUserSettingKey(key string) error {
+ if len(key) == 0 {
+ return fmt.Errorf("setting key must be set")
+ }
+ if strings.ToLower(key) != key {
+ return fmt.Errorf("setting key should be lowercase")
+ }
+ return nil
+}
+
+// GetUserSetting gets a specific setting for a user
+func GetUserSetting(ctx context.Context, userID int64, key string, def ...string) (string, error) {
+ if err := validateUserSettingKey(key); err != nil {
+ return "", err
+ }
+
+ setting := &Setting{UserID: userID, SettingKey: key}
+ has, err := db.GetEngine(ctx).Get(setting)
+ if err != nil {
+ return "", err
+ }
+ if !has {
+ if len(def) == 1 {
+ return def[0], nil
+ }
+ return "", nil
+ }
+ return setting.SettingValue, nil
+}
+
+// DeleteUserSetting deletes a specific setting for a user
+func DeleteUserSetting(ctx context.Context, userID int64, key string) error {
+ if err := validateUserSettingKey(key); err != nil {
+ return err
+ }
+
+ cache.Remove(genSettingCacheKey(userID, key))
+ _, err := db.GetEngine(ctx).Delete(&Setting{UserID: userID, SettingKey: key})
+
+ return err
+}
+
+// SetUserSetting updates a users' setting for a specific key
+func SetUserSetting(ctx context.Context, userID int64, key, value string) error {
+ if err := validateUserSettingKey(key); err != nil {
+ return err
+ }
+
+ if err := upsertUserSettingValue(ctx, userID, key, value); err != nil {
+ return err
+ }
+
+ cc := cache.GetCache()
+ if cc != nil {
+ return cc.Put(genSettingCacheKey(userID, key), value, setting_module.CacheService.TTLSeconds())
+ }
+
+ return nil
+}
+
+func upsertUserSettingValue(ctx context.Context, userID int64, key, value string) error {
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ e := db.GetEngine(ctx)
+
+ // here we use a general method to do a safe upsert for different databases (and most transaction levels)
+ // 1. try to UPDATE the record and acquire the transaction write lock
+ // if UPDATE returns non-zero rows are changed, OK, the setting is saved correctly
+ // if UPDATE returns "0 rows changed", two possibilities: (a) record doesn't exist (b) value is not changed
+ // 2. do a SELECT to check if the row exists or not (we already have the transaction lock)
+ // 3. if the row doesn't exist, do an INSERT (we are still protected by the transaction lock, so it's safe)
+ //
+ // to optimize the SELECT in step 2, we can use an extra column like `revision=revision+1`
+ // to make sure the UPDATE always returns a non-zero value for existing (unchanged) records.
+
+ res, err := e.Exec("UPDATE user_setting SET setting_value=? WHERE setting_key=? AND user_id=?", value, key, userID)
+ if err != nil {
+ return err
+ }
+ rows, _ := res.RowsAffected()
+ if rows > 0 {
+ // the existing row is updated, so we can return
+ return nil
+ }
+
+ // in case the value isn't changed, update would return 0 rows changed, so we need this check
+ has, err := e.Exist(&Setting{UserID: userID, SettingKey: key})
+ if err != nil {
+ return err
+ }
+ if has {
+ return nil
+ }
+
+ // if no existing row, insert a new row
+ _, err = e.Insert(&Setting{UserID: userID, SettingKey: key, SettingValue: value})
+ return err
+ })
+}