From 63c604d4d168812808febef7cd657648b7d6f263 Mon Sep 17 00:00:00 2001 From: Jacob Gunther Date: Sat, 10 Aug 2024 23:21:36 -0500 Subject: [PATCH] Add filestore store option --- .gitignore | 3 +- config.example.yml | 3 + src/cache.go | 16 ++--- src/config.go | 7 ++- src/main.go | 28 +++++++-- src/store/filestore.go | 140 +++++++++++++++++++++++++++++++++++++++++ src/store/store.go | 22 +++++++ 7 files changed, 203 insertions(+), 16 deletions(-) create mode 100644 src/store/filestore.go create mode 100644 src/store/store.go diff --git a/.gitignore b/.gitignore index 85cb51e..cf723fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ bin/ config.yml -.DS_Store \ No newline at end of file +.DS_Store +/store \ No newline at end of file diff --git a/config.example.yml b/config.example.yml index b41c8e2..17e5b1a 100644 --- a/config.example.yml +++ b/config.example.yml @@ -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 \ No newline at end of file diff --git a/src/cache.go b/src/cache.go index a11d5dc..33fc01d 100644 --- a/src/cache.go +++ b/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 } } diff --git a/src/config.go b/src/config.go index 463395f..5ee0bb5 100644 --- a/src/config.go +++ b/src/config.go @@ -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. diff --git a/src/main.go b/src/main.go index 4139cfc..442e0b1 100644 --- a/src/main.go +++ b/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) diff --git a/src/store/filestore.go b/src/store/filestore.go new file mode 100644 index 0000000..3fa7036 --- /dev/null +++ b/src/store/filestore.go @@ -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{} diff --git a/src/store/store.go b/src/store/store.go new file mode 100644 index 0000000..2e032ce --- /dev/null +++ b/src/store/store.go @@ -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 +}