Add filestore store option
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
bin/
|
bin/
|
||||||
config.yml
|
config.yml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/store
|
||||||
@@ -63,6 +63,9 @@ routes:
|
|||||||
default_download: false
|
default_download: false
|
||||||
default_format: png
|
default_format: png
|
||||||
cache:
|
cache:
|
||||||
|
store:
|
||||||
|
type: filestore
|
||||||
|
dir: store
|
||||||
skin_cache_duration: 12h # 12 hours
|
skin_cache_duration: 12h # 12 hours
|
||||||
render_cache_duration: 12h # 12 hours
|
render_cache_duration: 12h # 12 hours
|
||||||
enable_locks: true
|
enable_locks: true
|
||||||
16
src/cache.go
16
src/cache.go
@@ -33,7 +33,9 @@ func GetCachedRenderResult(renderType, uuid string, opts *QueryParams) ([]byte,
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.GetBytes(fmt.Sprintf("result:%s", GetResultCacheKey(uuid, renderType, opts)))
|
data, _, err := s.GetBytes(fmt.Sprintf("result:%s", GetResultCacheKey(uuid, renderType, opts)))
|
||||||
|
|
||||||
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCachedRenderResult puts the render result into cache, or does nothing is cache is disabled.
|
// SetCachedRenderResult puts the render result into cache, or does nothing is cache is disabled.
|
||||||
@@ -42,19 +44,19 @@ func SetCachedRenderResult(renderType, uuid string, opts *QueryParams, data []by
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.Set(fmt.Sprintf("result:%s", GetResultCacheKey(uuid, renderType, opts)), data, *config.Cache.RenderCacheDuration)
|
return s.SetBytes(fmt.Sprintf("result:%s", GetResultCacheKey(uuid, renderType, opts)), data, *config.Cache.RenderCacheDuration)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCachedSkin returns the raw skin of a player by UUID from the cache, also returning if the player has a slim player model.
|
// GetCachedSkin returns the raw skin of a player by UUID from the cache, also returning if the player has a slim player model.
|
||||||
func GetCachedSkin(uuid string) (*image.NRGBA, bool, error) {
|
func GetCachedSkin(uuid string) (*image.NRGBA, bool, error) {
|
||||||
cache, ok, err := r.GetNRGBA(fmt.Sprintf("skin:%s", uuid))
|
cache, ok, err := s.GetNRGBA(fmt.Sprintf("skin:%s", uuid))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
slim, err := r.Exists(fmt.Sprintf("slim:%s", uuid))
|
slim, err := s.Exists(fmt.Sprintf("slim:%s", uuid))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
@@ -67,16 +69,16 @@ func GetCachedSkin(uuid string) (*image.NRGBA, bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SetCachedSkin(uuid string, value []byte, isSlim bool) error {
|
func SetCachedSkin(uuid string, value []byte, isSlim bool) error {
|
||||||
if err := r.Set(fmt.Sprintf("skin:%s", uuid), value, *config.Cache.SkinCacheDuration); err != nil {
|
if err := s.SetBytes(fmt.Sprintf("skin:%s", uuid), value, *config.Cache.SkinCacheDuration); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSlim {
|
if isSlim {
|
||||||
if err := r.Set(fmt.Sprintf("slim:%s", uuid), "true", *config.Cache.SkinCacheDuration); err != nil {
|
if err := s.SetBytes(fmt.Sprintf("slim:%s", uuid), []byte("true"), *config.Cache.SkinCacheDuration); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := r.Delete(fmt.Sprintf("slim:%s", uuid)); err != nil {
|
if err := s.Delete(fmt.Sprintf("slim:%s", uuid)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,9 +126,10 @@ type RouteConfig struct {
|
|||||||
|
|
||||||
// CacheConfig is the configuration data used to set TTL values for Redis keys.
|
// CacheConfig is the configuration data used to set TTL values for Redis keys.
|
||||||
type CacheConfig struct {
|
type CacheConfig struct {
|
||||||
SkinCacheDuration *time.Duration `yaml:"skin_cache_duration"`
|
Store map[string]interface{} `yaml:"store"`
|
||||||
RenderCacheDuration *time.Duration `yaml:"render_cache_duration"`
|
SkinCacheDuration *time.Duration `yaml:"skin_cache_duration"`
|
||||||
EnableLocks bool `yaml:"enable_locks"`
|
RenderCacheDuration *time.Duration `yaml:"render_cache_duration"`
|
||||||
|
EnableLocks bool `yaml:"enable_locks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadFile reads the configuration from the file and parses it as YAML.
|
// ReadFile reads the configuration from the file and parses it as YAML.
|
||||||
|
|||||||
28
src/main.go
28
src/main.go
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/mineatar-io/api-server/src/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -24,9 +25,10 @@ var (
|
|||||||
return ctx.SendStatus(http.StatusInternalServerError)
|
return ctx.SendStatus(http.StatusInternalServerError)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
r *Redis = &Redis{}
|
r *Redis = &Redis{}
|
||||||
config *Config = &Config{}
|
s store.Store = nil
|
||||||
instanceID uint16 = 0
|
config *Config = &Config{}
|
||||||
|
instanceID uint16 = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -42,13 +44,29 @@ func init() {
|
|||||||
|
|
||||||
log.Println("Successfully connected to Redis")
|
log.Println("Successfully connected to Redis")
|
||||||
|
|
||||||
|
storeType, ok := config.Cache.Store["type"].(string)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("config: invalid cache.store.type type: %T", config.Cache.Store["type"])
|
||||||
|
}
|
||||||
|
|
||||||
|
s, ok = store.StoreTypes[storeType]
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("config: unknown store type: %s", storeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Initialize(config.Cache.Store); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
if instanceID, err = GetInstanceID(); err != nil {
|
if instanceID, err = GetInstanceID(); err != nil {
|
||||||
panic(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
defer r.Close()
|
defer s.Close()
|
||||||
|
|
||||||
log.Printf("Listening on %s:%d\n", config.Host, config.Port+instanceID)
|
log.Printf("Listening on %s:%d\n", config.Host, config.Port+instanceID)
|
||||||
|
|
||||||
|
|||||||
140
src/store/filestore.go
Normal file
140
src/store/filestore.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/draw"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileStore struct {
|
||||||
|
BaseDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileStore) Initialize(config map[string]interface{}) error {
|
||||||
|
baseDir, ok := config["dir"].(string)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("filestore: invalid base directory value: %s", config["dir"])
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(path.Clean(baseDir), 0777); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.BaseDir = path.Clean(baseDir)
|
||||||
|
log.Printf("%s\n", s.BaseDir)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileStore) GetBytes(key string) ([]byte, bool, error) {
|
||||||
|
expiration, err := os.ReadFile(path.Join(s.BaseDir, fmt.Sprintf("%s.expiration.txt", key)))
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
expirationDate, err := time.Parse(time.RFC3339, string(expiration))
|
||||||
|
|
||||||
|
if err == nil && time.Now().After(expirationDate) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path.Join(s.BaseDir, fmt.Sprintf("%s.bin", key)))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileStore) GetNRGBA(key string) (*image.NRGBA, bool, error) {
|
||||||
|
data, exists, err := s.GetBytes(key)
|
||||||
|
|
||||||
|
if !exists || err != nil {
|
||||||
|
return nil, exists, err
|
||||||
|
}
|
||||||
|
|
||||||
|
img, format, err := image.Decode(bytes.NewReader(data))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if format != "NRGBA" {
|
||||||
|
outputImg := image.NewNRGBA(img.Bounds())
|
||||||
|
|
||||||
|
draw.Draw(outputImg, img.Bounds(), img, image.Pt(0, 0), draw.Src)
|
||||||
|
|
||||||
|
return outputImg, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return img.(*image.NRGBA), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileStore) Exists(key string) (bool, error) {
|
||||||
|
expiration, err := os.ReadFile(path.Join(s.BaseDir, fmt.Sprintf("%s.expiration.txt", key)))
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
expirationDate, err := time.Parse(time.RFC3339, string(expiration))
|
||||||
|
|
||||||
|
if err == nil && time.Now().After(expirationDate) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = os.Stat(path.Join(s.BaseDir, fmt.Sprintf("%s.bin", key))); err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileStore) SetBytes(key string, data []byte, ttl time.Duration) error {
|
||||||
|
if err := os.WriteFile(path.Join(s.BaseDir, fmt.Sprintf("%s.bin", key)), data, 0777); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ttl > 0 {
|
||||||
|
if err := os.WriteFile(path.Join(s.BaseDir, fmt.Sprintf("%s.expiration.txt", key)), []byte(time.Now().Add(ttl).Format(time.RFC3339)), 0777); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := os.RemoveAll(path.Join(s.BaseDir, fmt.Sprintf("%s.expiration.txt", key))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileStore) Delete(key string) error {
|
||||||
|
if err := os.RemoveAll(path.Join(s.BaseDir, fmt.Sprintf("%s.bin", key))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.RemoveAll(path.Join(s.BaseDir, fmt.Sprintf("%s.expiration.txt", key))); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FileStore) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Store = &FileStore{}
|
||||||
22
src/store/store.go
Normal file
22
src/store/store.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
StoreTypes map[string]Store = map[string]Store{
|
||||||
|
"filestore": &FileStore{},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
Initialize(config map[string]interface{}) error
|
||||||
|
GetBytes(id string) ([]byte, bool, error)
|
||||||
|
GetNRGBA(id string) (*image.NRGBA, bool, error)
|
||||||
|
Exists(id string) (bool, error)
|
||||||
|
SetBytes(id string, data []byte, ttl time.Duration) error
|
||||||
|
Delete(id string) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user