Add filestore store option
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
bin/
|
||||
config.yml
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
/store
|
||||
@@ -63,6 +63,9 @@ routes:
|
||||
default_download: false
|
||||
default_format: png
|
||||
cache:
|
||||
store:
|
||||
type: filestore
|
||||
dir: store
|
||||
skin_cache_duration: 12h # 12 hours
|
||||
render_cache_duration: 12h # 12 hours
|
||||
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 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.
|
||||
@@ -42,19 +44,19 @@ func SetCachedRenderResult(renderType, uuid string, opts *QueryParams, data []by
|
||||
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.
|
||||
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 {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if ok {
|
||||
slim, err := r.Exists(fmt.Sprintf("slim:%s", uuid))
|
||||
slim, err := s.Exists(fmt.Sprintf("slim:%s", uuid))
|
||||
|
||||
if err != nil {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,9 +126,10 @@ type RouteConfig struct {
|
||||
|
||||
// CacheConfig is the configuration data used to set TTL values for Redis keys.
|
||||
type CacheConfig struct {
|
||||
SkinCacheDuration *time.Duration `yaml:"skin_cache_duration"`
|
||||
RenderCacheDuration *time.Duration `yaml:"render_cache_duration"`
|
||||
EnableLocks bool `yaml:"enable_locks"`
|
||||
Store map[string]interface{} `yaml:"store"`
|
||||
SkinCacheDuration *time.Duration `yaml:"skin_cache_duration"`
|
||||
RenderCacheDuration *time.Duration `yaml:"render_cache_duration"`
|
||||
EnableLocks bool `yaml:"enable_locks"`
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/mineatar-io/api-server/src/store"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,9 +25,10 @@ var (
|
||||
return ctx.SendStatus(http.StatusInternalServerError)
|
||||
},
|
||||
})
|
||||
r *Redis = &Redis{}
|
||||
config *Config = &Config{}
|
||||
instanceID uint16 = 0
|
||||
r *Redis = &Redis{}
|
||||
s store.Store = nil
|
||||
config *Config = &Config{}
|
||||
instanceID uint16 = 0
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -42,13 +44,29 @@ func init() {
|
||||
|
||||
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 {
|
||||
panic(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
defer r.Close()
|
||||
defer s.Close()
|
||||
|
||||
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