Add filestore store option

This commit is contained in:
Jacob Gunther
2024-08-10 23:21:36 -05:00
parent e123b0e337
commit 63c604d4d1
7 changed files with 203 additions and 16 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
bin/
config.yml
.DS_Store
.DS_Store
/store

View File

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

View File

@@ -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
}
}

View File

@@ -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.

View File

@@ -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
View 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
View 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
}