diff --git a/config.yml.sample b/config.yml.sample
index e86b344b86..6f69f81bef 100644
--- a/config.yml.sample
+++ b/config.yml.sample
@@ -61,8 +61,10 @@ database:
cache:
# If cache is enabled or not
enabled: false
- # Cache type. Possible values are memory or redis, you'll need to enable redis below when using redis
- type: memory
+ # Cache type. Possible values are "keyvalue", "memory" or "redis".
+ # When choosing "keyvalue" this setting follows the one configured in the "keyvalue" section.
+ # When choosing "redis" you will need to configure the redis connection seperately.
+ type: keyvalue
# When using memory this defines the maximum size an element can take
maxelementsize: 1000
@@ -136,8 +138,10 @@ ratelimit:
period: 60
# The max number of requests a user is allowed to do in the configured time period
limit: 100
- # The store where the limit counter for each user is stored. Possible values are "memory" or "redis"
- store: memory
+ # The store where the limit counter for each user is stored.
+ # Possible values are "keyvalue", "memory" or "redis".
+ # When choosing "keyvalue" this setting follows the one configured in the "keyvalue" section.
+ store: keyvalue
files:
# The path where files are stored
@@ -205,3 +209,9 @@ backgrounds:
legal:
imprinturl:
privacyurl:
+
+# Key Value Storage settings
+# The Key Value Storage is used for different kinds of things like metrics and a few cache systems.
+keyvalue:
+ # The type of the storage backend. Can be either "memory" or "redis". If "redis" is chosen it needs to be configured seperately.
+ type: "memory"
diff --git a/docs/content/doc/setup/config.md b/docs/content/doc/setup/config.md
index 283e8c82b4..5a645cc116 100644
--- a/docs/content/doc/setup/config.md
+++ b/docs/content/doc/setup/config.md
@@ -104,8 +104,10 @@ database:
cache:
# If cache is enabled or not
enabled: false
- # Cache type. Possible values are memory or redis, you'll need to enable redis below when using redis
- type: memory
+ # Cache type. Possible values are "keyvalue", "memory" or "redis".
+ # When choosing "keyvalue" this setting follows the one configured in the "keyvalue" section.
+ # When choosing "redis" you will need to configure the redis connection seperately.
+ type: keyvalue
# When using memory this defines the maximum size an element can take
maxelementsize: 1000
@@ -169,7 +171,7 @@ log:
http: "stdout"
# Echo has its own logging which usually is unnessecary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
echo: "off"
-
+
ratelimit:
# whether or not to enable the rate limit
enabled: false
@@ -179,8 +181,10 @@ ratelimit:
period: 60
# The max number of requests a user is allowed to do in the configured time period
limit: 100
- # The store where the limit counter for each user is stored. Possible values are "memory" or "redis"
- store: memory
+ # The store where the limit counter for each user is stored.
+ # Possible values are "keyvalue", "memory" or "redis".
+ # When choosing "keyvalue" this setting follows the one configured in the "keyvalue" section.
+ store: keyvalue
files:
# The path where files are stored
@@ -234,7 +238,7 @@ backgrounds:
unsplash:
# Whether to enable setting backgrounds from unsplash as list backgrounds
enabled: false
- # You need to create an application for your installation at https://unsplash.com/oauth/applications/new
+ # You need to create an application for your installation at https://unsplash.com/oauth/applications/new
# and set the access token below.
accesstoken:
# The unsplash application id is only used for pingback and required as per their api guidelines.
@@ -246,6 +250,12 @@ backgrounds:
# Legal urls
# Will be shown in the frontend if configured here
legal:
- imprinturl:
- privacyurl:
+ imprinturl:
+ privacyurl:
+
+# Key Value Storage settings
+# The Key Value Storage is used for different kinds of things like metrics and a few cache systems.
+keyvalue:
+ # The type of the storage backend. Can be either "memory" or "redis". If "redis" is chosen it needs to be configured seperately.
+ type: "memory"
{{< /highlight >}}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index aa861f073c..f17f7b26de 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -124,6 +124,8 @@ const (
BackgroundsUnsplashEnabled Key = `backgrounds.providers.unsplash.enabled`
BackgroundsUnsplashAccessToken Key = `backgrounds.providers.unsplash.accesstoken`
BackgroundsUnsplashApplicationID Key = `backgrounds.providers.unsplash.applicationid`
+
+ KeyvalueType Key = `keyvalue.type`
)
// GetString returns a string config value
@@ -277,6 +279,8 @@ func InitDefaultConfig() {
BackgroundsEnabled.setDefault(true)
BackgroundsUploadEnabled.setDefault(true)
BackgroundsUnsplashEnabled.setDefault(false)
+ // Key Value
+ KeyvalueType.setDefault("memory")
}
// InitConfig initializes the config, sets defaults etc.
@@ -310,6 +314,14 @@ func InitConfig() {
return
}
+ if CacheType.GetString() == "keyvalue" {
+ CacheType.Set(KeyvalueType.GetString())
+ }
+
+ if RateLimitStore.GetString() == "keyvalue" {
+ RateLimitStore.Set(KeyvalueType.GetString())
+ }
+
log.Printf("Using config file: %s", viper.ConfigFileUsed())
}
diff --git a/pkg/db/db.go b/pkg/db/db.go
index 73251251f0..5224232a6f 100644
--- a/pkg/db/db.go
+++ b/pkg/db/db.go
@@ -90,7 +90,7 @@ func CreateDBEngine() (engine *xorm.Engine, err error) {
cacher := caches.NewLRUCacher(caches.NewMemoryStore(), config.CacheMaxElementSize.GetInt())
engine.SetDefaultCacher(cacher)
case "redis":
- cacher := xrc.NewRedisCacher(config.RedisEnabled.GetString(), config.RedisPassword.GetString(), xrc.DEFAULT_EXPIRATION, engine.Logger())
+ cacher := xrc.NewRedisCacher(config.RedisHost.GetString(), config.RedisPassword.GetString(), xrc.DEFAULT_EXPIRATION, engine.Logger())
engine.SetDefaultCacher(cacher)
default:
log.Info("Did not find a valid cache type. Caching disabled. Please refer to the docs for poosible cache types.")
diff --git a/pkg/initialize/init.go b/pkg/initialize/init.go
index dc9f89e822..e08a9fd98b 100644
--- a/pkg/initialize/init.go
+++ b/pkg/initialize/init.go
@@ -23,6 +23,7 @@ import (
"code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/migration"
"code.vikunja.io/api/pkg/models"
+ "code.vikunja.io/api/pkg/modules/keyvalue"
migrator "code.vikunja.io/api/pkg/modules/migration"
"code.vikunja.io/api/pkg/red"
"code.vikunja.io/api/pkg/user"
@@ -36,6 +37,9 @@ func LightInit() {
// Init redis
red.InitRedis()
+ // Init keyvalue store
+ keyvalue.InitStorage()
+
// Set logger
log.InitLogger()
}
diff --git a/pkg/metrics/active_users.go b/pkg/metrics/active_users.go
index af3c72821d..328be356df 100644
--- a/pkg/metrics/active_users.go
+++ b/pkg/metrics/active_users.go
@@ -17,10 +17,9 @@
package metrics
import (
- "bytes"
"code.vikunja.io/api/pkg/log"
+ "code.vikunja.io/api/pkg/modules/keyvalue"
"code.vikunja.io/web"
- "encoding/gob"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"sync"
@@ -91,36 +90,19 @@ func SetUserActive(a web.Auth) (err error) {
// getActiveUsers returns the active users from redis
func getActiveUsers() (users activeUsersMap, err error) {
-
- activeUsersR, err := r.Get(ActiveUsersKey).Bytes()
- if err != nil {
- if err.Error() == "redis: nil" {
- return users, nil
- }
- return
- }
-
- var b bytes.Buffer
- _, err = b.Write(activeUsersR)
+ u, err := keyvalue.Get(ActiveUsersKey)
if err != nil {
return nil, err
}
- d := gob.NewDecoder(&b)
- if err := d.Decode(&users); err != nil {
- return nil, err
- }
+
+ users = u.(activeUsersMap)
return
}
// PushActiveUsers pushed the content of the activeUsers map to redis
func PushActiveUsers() (err error) {
- var b bytes.Buffer
- e := gob.NewEncoder(&b)
activeUsers.mutex.Lock()
defer activeUsers.mutex.Unlock()
- if err := e.Encode(activeUsers.users); err != nil {
- return err
- }
- return r.Set(ActiveUsersKey, b.Bytes(), 0).Err()
+ return keyvalue.Put(ActiveUsersKey, activeUsers.users)
}
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index 715543ef87..0d1a953d96 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -19,14 +19,12 @@ package metrics
import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
- "code.vikunja.io/api/pkg/red"
- "github.com/go-redis/redis/v7"
+ "code.vikunja.io/api/pkg/modules/keyvalue"
+ e "code.vikunja.io/api/pkg/modules/keyvalue/error"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
-var r *redis.Client
-
const (
// ListCountKey is the name of the key in which we save the list count
ListCountKey = `listcount`
@@ -46,8 +44,6 @@ const (
// InitMetrics Initializes the metrics
func InitMetrics() {
- r = red.GetRedis()
-
// init active users, sometimes we'll have garbage from previous runs in redis instead
if err := PushActiveUsers(); err != nil {
log.Fatalf("Could not set initial count for active users, error was %s", err)
@@ -101,18 +97,21 @@ func InitMetrics() {
// GetCount returns the current count from redis
func GetCount(key string) (count int64, err error) {
- count, err = r.Get(key).Int64()
- if err != nil && err.Error() != "redis: nil" {
- return
+ cnt, err := keyvalue.Get(key)
+ if err != nil {
+ if e.IsErrValueNotFoundForKey(err) {
+ return 0, nil
+ }
+ return 0, err
}
- err = nil
+ count = cnt.(int64)
return
}
// SetCount sets the list count to a given value
func SetCount(count int64, key string) error {
- return r.Set(key, count, 0).Err()
+ return keyvalue.Put(key, count)
}
// UpdateCount updates a count with a given amount
@@ -121,13 +120,13 @@ func UpdateCount(update int64, key string) {
return
}
if update > 0 {
- err := r.IncrBy(key, update).Err()
+ err := keyvalue.IncrBy(key, update)
if err != nil {
log.Error(err.Error())
}
}
if update < 0 {
- err := r.DecrBy(key, update).Err()
+ err := keyvalue.DecrBy(key, update)
if err != nil {
log.Error(err.Error())
}
diff --git a/pkg/modules/avatar/initials/initials.go b/pkg/modules/avatar/initials/initials.go
index 727c8b0dd5..0defdc66aa 100644
--- a/pkg/modules/avatar/initials/initials.go
+++ b/pkg/modules/avatar/initials/initials.go
@@ -17,14 +17,12 @@
package initials
import (
+ "bytes"
"code.vikunja.io/api/pkg/log"
+ "code.vikunja.io/api/pkg/modules/keyvalue"
+ e "code.vikunja.io/api/pkg/modules/keyvalue/error"
"code.vikunja.io/api/pkg/user"
"github.com/disintegration/imaging"
- "strconv"
- "strings"
- "sync"
-
- "bytes"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/font/gofont/goregular"
@@ -33,6 +31,8 @@ import (
"image/color"
"image/draw"
"image/png"
+ "strconv"
+ "strings"
)
// Provider represents the provider implementation of the initials provider
@@ -51,19 +51,8 @@ var (
{121, 134, 203, 255},
{241, 185, 29, 255},
}
-
- // Contain the created avatars with a size of defaultSize
- cache = map[int64]*image.RGBA64{}
- cacheLock = sync.Mutex{}
- cacheResized = map[string][]byte{}
- cacheResizedLock = sync.Mutex{}
)
-func init() {
- cache = make(map[int64]*image.RGBA64)
- cacheResized = make(map[string][]byte)
-}
-
const (
dpi = 72
defaultSize = 1024
@@ -124,10 +113,26 @@ func drawImage(text rune, bg *color.RGBA) (img *image.RGBA64, err error) {
return img, err
}
+func getCacheKey(prefix string, keys ...int64) string {
+ result := "avatar_initials_" + prefix
+ for i, key := range keys {
+ result += strconv.Itoa(int(key))
+ if i < len(keys) {
+ result += "_"
+ }
+ }
+ return result
+}
+
func getAvatarForUser(u *user.User) (fullSizeAvatar *image.RGBA64, err error) {
- var cached bool
- fullSizeAvatar, cached = cache[u.ID]
- if !cached {
+ cacheKey := getCacheKey("full", u.ID)
+
+ a, err := keyvalue.Get(cacheKey)
+ if err != nil && !e.IsErrValueNotFoundForKey(err) {
+ return nil, err
+ }
+
+ if err != nil && e.IsErrValueNotFoundForKey(err) {
log.Debugf("Initials avatar for user %d not cached, creating...", u.ID)
firstRune := []rune(strings.ToUpper(u.Username))[0]
bg := avatarBgColors[int(u.ID)%len(avatarBgColors)] // Random color based on the user id
@@ -136,21 +141,27 @@ func getAvatarForUser(u *user.User) (fullSizeAvatar *image.RGBA64, err error) {
if err != nil {
return nil, err
}
- cacheLock.Lock()
- cache[u.ID] = fullSizeAvatar
- cacheLock.Unlock()
+ err = keyvalue.Put(cacheKey, fullSizeAvatar)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ fullSizeAvatar = a.(*image.RGBA64)
}
- return fullSizeAvatar, err
+ return fullSizeAvatar, nil
}
// GetAvatar returns an initials avatar for a user
func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType string, err error) {
+ cacheKey := getCacheKey("resized", u.ID, size)
- var cached bool
- cacheKey := strconv.Itoa(int(u.ID)) + "_" + strconv.Itoa(int(size))
- avatar, cached = cacheResized[cacheKey]
- if !cached {
+ a, err := keyvalue.Get(cacheKey)
+ if err != nil && !e.IsErrValueNotFoundForKey(err) {
+ return nil, "", err
+ }
+
+ if err != nil && e.IsErrValueNotFoundForKey(err) {
log.Debugf("Initials avatar for user %d and size %d not cached, creating...", u.ID, size)
fullAvatar, err := getAvatarForUser(u)
if err != nil {
@@ -164,12 +175,14 @@ func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType
return nil, "", err
}
avatar = buf.Bytes()
- cacheResizedLock.Lock()
- cacheResized[cacheKey] = avatar
- cacheResizedLock.Unlock()
+ err = keyvalue.Put(cacheKey, avatar)
+ if err != nil {
+ return nil, "", err
+ }
} else {
+ avatar = a.([]byte)
log.Debugf("Serving initials avatar for user %d and size %d from cache", u.ID, size)
}
- return avatar, "image/png", err
+ return avatar, "image/png", nil
}
diff --git a/pkg/modules/avatar/upload/upload.go b/pkg/modules/avatar/upload/upload.go
index b2dd5deca6..357bc287d7 100644
--- a/pkg/modules/avatar/upload/upload.go
+++ b/pkg/modules/avatar/upload/upload.go
@@ -20,25 +20,16 @@ import (
"bytes"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
+ "code.vikunja.io/api/pkg/modules/keyvalue"
+ e "code.vikunja.io/api/pkg/modules/keyvalue/error"
"code.vikunja.io/api/pkg/user"
"github.com/disintegration/imaging"
"image"
"image/png"
"io/ioutil"
- "sync"
+ "strconv"
)
-var (
- // This is a map with a map so we're able to clear all cached avatar (in all sizes) for one user at once
- // The first map has as key the user id, the second one has the size as key
- resizedCache = map[int64]map[int64][]byte{}
- resizedCacheLock = sync.Mutex{}
-)
-
-func init() {
- resizedCache = make(map[int64]map[int64][]byte)
-}
-
// Provider represents the upload avatar provider
type Provider struct {
}
@@ -46,19 +37,32 @@ type Provider struct {
// GetAvatar returns an uploaded user avatar
func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType string, err error) {
- a, cached := resizedCache[u.ID]
- if cached {
+ cacheKey := "avatar_upload_" + strconv.Itoa(int(u.ID))
+
+ ai, err := keyvalue.Get(cacheKey)
+ if err != nil && !e.IsErrValueNotFoundForKey(err) {
+ return nil, "", err
+ }
+
+ var cached map[int64][]byte
+
+ if ai != nil {
+ cached = ai.(map[int64][]byte)
+ }
+
+ if err != nil && e.IsErrValueNotFoundForKey(err) {
+ // Nothing ever cached for this user so we need to create the size map to avoid panics
+ cached = make(map[int64][]byte)
+ } else {
+ a := ai.(map[int64][]byte)
if a != nil && a[size] != nil {
log.Debugf("Serving uploaded avatar for user %d and size %d from cache.", u.ID, size)
return a[size], "", nil
}
// This means we have a map for the user, but nothing in it.
if a == nil {
- resizedCache[u.ID] = make(map[int64][]byte)
+ cached = make(map[int64][]byte)
}
- } else {
- // Nothing ever cached for this user so we need to create the size map to avoid panics
- resizedCache[u.ID] = make(map[int64][]byte)
}
log.Debugf("Uploaded avatar for user %d and size %d not cached, resizing and caching.", u.ID, size)
@@ -84,15 +88,17 @@ func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType
}
avatar, err = ioutil.ReadAll(buf)
- resizedCacheLock.Lock()
- resizedCache[u.ID][size] = avatar
- resizedCacheLock.Unlock()
+ if err != nil {
+ return nil, "", err
+ }
+ cached[size] = avatar
+ err = keyvalue.Put(cacheKey, cached)
return avatar, f.Mime, err
}
// InvalidateCache invalidates the avatar cache for a user
func InvalidateCache(u *user.User) {
- resizedCacheLock.Lock()
- delete(resizedCache, u.ID)
- resizedCacheLock.Unlock()
+ if err := keyvalue.Del("avatar_upload_" + strconv.Itoa(int(u.ID))); err != nil {
+ log.Errorf("Could not invalidate upload avatar cache for user %d, error was %s", u.ID, err)
+ }
}
diff --git a/pkg/modules/background/unsplash/unsplash.go b/pkg/modules/background/unsplash/unsplash.go
index f2c6db42e2..cacd404b26 100644
--- a/pkg/modules/background/unsplash/unsplash.go
+++ b/pkg/modules/background/unsplash/unsplash.go
@@ -23,6 +23,8 @@ import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/background"
+ "code.vikunja.io/api/pkg/modules/keyvalue"
+ e "code.vikunja.io/api/pkg/modules/keyvalue/error"
"code.vikunja.io/web"
"encoding/json"
"net/http"
@@ -32,7 +34,10 @@ import (
"time"
)
-const unsplashAPIURL = `https://api.unsplash.com/`
+const (
+ unsplashAPIURL = `https://api.unsplash.com/`
+ cachePrefix = `unsplash_photo_`
+)
// Provider represents an unsplash image provider
type Provider struct {
@@ -72,10 +77,6 @@ type Photo struct {
} `json:"links"`
}
-// Very simple caching method - pretty much only used to retain information when saving an image
-// FIXME: Should use a proper cache
-var photos map[string]*Photo
-
// We're caching the initial collection to save a few api requests as this is retrieved every time a
// user opens the settings page.
type initialCollection struct {
@@ -87,10 +88,6 @@ type initialCollection struct {
var emptySearchResult *initialCollection
-func init() {
- photos = make(map[string]*Photo)
-}
-
func doGet(url string, result ...interface{}) (err error) {
req, err := http.NewRequest("GET", unsplashAPIURL+url, nil)
if err != nil {
@@ -120,15 +117,21 @@ func getImageID(fullURL string) string {
// Gets an unsplash photo either from cache or directly from the unsplash api
func getUnsplashPhotoInfoByID(photoID string) (photo *Photo, err error) {
- var exists bool
- photo, exists = photos[photoID]
- if !exists {
+
+ p, err := keyvalue.Get(cachePrefix + photoID)
+ if err != nil && !e.IsErrValueNotFoundForKey(err) {
+ return nil, err
+ }
+
+ if err != nil && e.IsErrValueNotFoundForKey(err) {
log.Debugf("Image information for unsplash photo %s not cached, requesting from unsplash...", photoID)
photo = &Photo{}
err = doGet("photos/"+photoID, photo)
if err != nil {
return
}
+ } else {
+ photo = p.(*Photo)
}
return
}
@@ -180,7 +183,9 @@ func (p *Provider) Search(search string, page int64) (result []*background.Image
AuthorName: p.User.Name,
},
})
- photos[p.ID] = p
+ if err := keyvalue.Put(cachePrefix+p.ID, p); err != nil {
+ return nil, err
+ }
}
// Put the collection in cache
@@ -213,7 +218,9 @@ func (p *Provider) Search(search string, page int64) (result []*background.Image
AuthorName: p.User.Name,
},
})
- photos[p.ID] = p
+ if err := keyvalue.Put(cachePrefix+p.ID, p); err != nil {
+ return nil, err
+ }
}
return
diff --git a/pkg/modules/keyvalue/error/error.go b/pkg/modules/keyvalue/error/error.go
new file mode 100644
index 0000000000..fbaac3ab63
--- /dev/null
+++ b/pkg/modules/keyvalue/error/error.go
@@ -0,0 +1,52 @@
+// Vikunja is a to-do list application to facilitate your life.
+// Copyright 2018-2020 Vikunja and contributors. All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package error
+
+import "fmt"
+
+// ErrValueNotFoundForKey represents an error where a key could not be found
+type ErrValueNotFoundForKey struct {
+ Key string
+}
+
+// Error is the error implementation
+func (e *ErrValueNotFoundForKey) Error() string {
+ return fmt.Sprintf("could not find value for key %s", e.Key)
+}
+
+// IsErrValueNotFoundForKey checks if an error is ErrValueNotFoundForKey
+func IsErrValueNotFoundForKey(err error) bool {
+ _, is := err.(*ErrValueNotFoundForKey)
+ return is
+}
+
+// ErrValueHasWrongType represents an error where a value saved at key has the wrong value
+type ErrValueHasWrongType struct {
+ Key string
+ ExpectedValue string
+}
+
+// Error is the error implementation
+func (e *ErrValueHasWrongType) Error() string {
+ return fmt.Sprintf("value at key %s has the wrong value, expexted was %s", e.Key, e.ExpectedValue)
+}
+
+// IsErrValueHasWrongType checks if an error is ErrValueHasWrongType
+func IsErrValueHasWrongType(err error) bool {
+ _, is := err.(*ErrValueHasWrongType)
+ return is
+}
diff --git a/pkg/modules/keyvalue/keyvalue.go b/pkg/modules/keyvalue/keyvalue.go
new file mode 100644
index 0000000000..df798640d8
--- /dev/null
+++ b/pkg/modules/keyvalue/keyvalue.go
@@ -0,0 +1,72 @@
+// Copyright 2020 Vikunja and contriubtors. All rights reserved.
+//
+// This file is part of Vikunja.
+//
+// Vikunja is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Vikunja is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Vikunja. If not, see .
+
+package keyvalue
+
+import (
+ "code.vikunja.io/api/pkg/config"
+ "code.vikunja.io/api/pkg/modules/keyvalue/memory"
+ "code.vikunja.io/api/pkg/modules/keyvalue/redis"
+)
+
+// Storage defines an interface for saving key-value pairs
+type Storage interface {
+ Put(key string, value interface{}) (err error)
+ Get(key string) (value interface{}, err error)
+ Del(key string) (err error)
+ IncrBy(key string, update int64) (err error)
+ DecrBy(key string, update int64) (err error)
+}
+
+var store Storage
+
+// InitStorage initializes the configured storage backend
+func InitStorage() {
+ switch config.KeyvalueType.GetString() {
+ case "redis":
+ store = redis.NewStorage()
+ case "memory":
+ fallthrough
+ default:
+ store = memory.NewStorage()
+ }
+}
+
+// Put puts a value in the storage backend
+func Put(key string, value interface{}) error {
+ return store.Put(key, value)
+}
+
+// Get returns a value from a storage backend
+func Get(key string) (value interface{}, err error) {
+ return store.Get(key)
+}
+
+// Del removes a save value from a storage backend
+func Del(key string) (err error) {
+ return store.Del(key)
+}
+
+// IncrBy increases a value at key by the amount in update
+func IncrBy(key string, update int64) (err error) {
+ return store.IncrBy(key, update)
+}
+
+// DecrBy increases a value at key by the amount in update
+func DecrBy(key string, update int64) (err error) {
+ return store.DecrBy(key, update)
+}
diff --git a/pkg/modules/keyvalue/memory/memory.go b/pkg/modules/keyvalue/memory/memory.go
new file mode 100644
index 0000000000..043573f7d2
--- /dev/null
+++ b/pkg/modules/keyvalue/memory/memory.go
@@ -0,0 +1,102 @@
+// Copyright 2020 Vikunja and contriubtors. All rights reserved.
+//
+// This file is part of Vikunja.
+//
+// Vikunja is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Vikunja is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Vikunja. If not, see .
+
+package memory
+
+import (
+ e "code.vikunja.io/api/pkg/modules/keyvalue/error"
+ "sync"
+)
+
+// Storage is the memory implementation of a storage backend
+type Storage struct {
+ store map[string]interface{}
+ mutex sync.Mutex
+}
+
+// NewStorage creates a new memory storage
+func NewStorage() *Storage {
+ s := &Storage{}
+ s.store = make(map[string]interface{})
+ return s
+}
+
+// Put puts a value into the memory storage
+func (s *Storage) Put(key string, value interface{}) (err error) {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+ s.store[key] = value
+ return nil
+}
+
+// Get retrieves a saved value from memory storage
+func (s *Storage) Get(key string) (value interface{}, err error) {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ var exists bool
+ value, exists = s.store[key]
+ if !exists {
+ return nil, &e.ErrValueNotFoundForKey{Key: key}
+ }
+
+ return
+}
+
+// Del removes a saved value from a memory storage
+func (s *Storage) Del(key string) (err error) {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+ delete(s.store, key)
+ return nil
+}
+
+// IncrBy increases the value saved at key by the amount provided through update
+// It assumes the value saved for the key either does not exist or has a type of int64
+func (s *Storage) IncrBy(key string, update int64) (err error) {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ v, err := s.Get(key)
+ if err != nil && !e.IsErrValueNotFoundForKey(err) {
+ return err
+ }
+ val, is := v.(int64)
+ if !is {
+ return &e.ErrValueHasWrongType{Key: key, ExpectedValue: "int64"}
+ }
+ s.store[key] = val + update
+ return nil
+}
+
+// DecrBy decreases the value saved at key by the amount provided through update
+// It assumes the value saved for the key either does not exist or has a type of int64
+func (s *Storage) DecrBy(key string, update int64) (err error) {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ v, err := s.Get(key)
+ if err != nil && !e.IsErrValueNotFoundForKey(err) {
+ return err
+ }
+ val, is := v.(int64)
+ if !is {
+ return &e.ErrValueHasWrongType{Key: key, ExpectedValue: "int64"}
+ }
+ s.store[key] = val - update
+ return nil
+}
diff --git a/pkg/modules/keyvalue/redis/redis.go b/pkg/modules/keyvalue/redis/redis.go
new file mode 100644
index 0000000000..1422ceb153
--- /dev/null
+++ b/pkg/modules/keyvalue/redis/redis.go
@@ -0,0 +1,78 @@
+// Copyright 2020 Vikunja and contriubtors. All rights reserved.
+//
+// This file is part of Vikunja.
+//
+// Vikunja is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Vikunja is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Vikunja. If not, see .
+
+package redis
+
+import (
+ e "code.vikunja.io/api/pkg/modules/keyvalue/error"
+ "code.vikunja.io/api/pkg/red"
+ "encoding/json"
+ "github.com/go-redis/redis/v7"
+)
+
+// Storage is a redis implementation of a keyvalue storage
+type Storage struct {
+ client *redis.Client
+}
+
+// NewStorage creates a new redis key value storage
+func NewStorage() *Storage {
+ red.InitRedis()
+
+ return &Storage{
+ client: red.GetRedis(),
+ }
+}
+
+// Put puts a value into redis
+func (s *Storage) Put(key string, value interface{}) (err error) {
+ v, err := json.Marshal(value)
+ if err != nil {
+ return err
+ }
+
+ return s.client.Set(key, v, 0).Err()
+}
+
+// Get retrieves a saved value from redis
+func (s *Storage) Get(key string) (value interface{}, err error) {
+ b, err := s.client.Get(key).Bytes()
+ if err != nil {
+ if err == redis.Nil {
+ return nil, &e.ErrValueNotFoundForKey{Key: key}
+ }
+ return nil, err
+ }
+
+ err = json.Unmarshal(b, value)
+ return
+}
+
+// Del removed a value from redis
+func (s *Storage) Del(key string) (err error) {
+ return s.client.Del(key).Err()
+}
+
+// IncrBy increases the value saved at key by the amount provided through update
+func (s *Storage) IncrBy(key string, update int64) (err error) {
+ return s.client.IncrBy(key, update).Err()
+}
+
+// DecrBy decreases the value saved at key by the amount provided through update
+func (s *Storage) DecrBy(key string, update int64) (err error) {
+ return s.client.DecrBy(key, update).Err()
+}
diff --git a/pkg/red/redis.go b/pkg/red/redis.go
index 016fb58731..4ae7e9a846 100644
--- a/pkg/red/redis.go
+++ b/pkg/red/redis.go
@@ -26,6 +26,10 @@ var r *redis.Client
// InitRedis initializes a redis connection
func InitRedis() {
+ if r != nil {
+ return
+ }
+
if !config.RedisEnabled.GetBool() {
return
}
diff --git a/pkg/routes/metrics.go b/pkg/routes/metrics.go
index a0a9031c79..61722f0699 100644
--- a/pkg/routes/metrics.go
+++ b/pkg/routes/metrics.go
@@ -33,10 +33,6 @@ func setupMetrics(a *echo.Group) {
return
}
- if !config.RedisEnabled.GetBool() {
- log.Fatal("You have to enable redis in order to use metrics")
- }
-
metrics.InitMetrics()
type countable struct {