From 02f8f05538630e2ce8679cf75374a583909f6e80 Mon Sep 17 00:00:00 2001 From: Felipe Martin Garcia Date: Sun, 30 Jan 2022 19:25:16 +0100 Subject: [PATCH] feat: cache support (#19) - Added file and memory cache handlers - Steam provider now uses a cache to store the app list for 24h Closes #12 --- internal/cli/cli.go | 5 +- internal/models/cache.go | 15 +++++ internal/models/provider.go | 2 +- pkg/cache/file.go | 78 +++++++++++++++++++++++ pkg/cache/memory.go | 54 ++++++++++++++++ pkg/providers/minecraft/provider.go | 2 +- pkg/providers/nintendo_switch/provider.go | 2 +- pkg/providers/playstation4/provider.go | 2 +- pkg/providers/retroarch/provider.go | 2 +- pkg/providers/steam/helpers.go | 44 ++++++++++--- pkg/providers/steam/provider.go | 6 +- pkg/registry/registry.go | 9 ++- 12 files changed, 200 insertions(+), 21 deletions(-) create mode 100644 internal/models/cache.go create mode 100644 pkg/cache/file.go create mode 100644 pkg/cache/memory.go diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 2ee976a..5406d56 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -6,6 +6,7 @@ import ( "os" "github.com/fmartingr/games-screenshot-manager/internal/models" + "github.com/fmartingr/games-screenshot-manager/pkg/cache" "github.com/fmartingr/games-screenshot-manager/pkg/processor" "github.com/fmartingr/games-screenshot-manager/pkg/providers/minecraft" "github.com/fmartingr/games-screenshot-manager/pkg/providers/nintendo_switch" @@ -27,7 +28,9 @@ func Start() { logger := logrus.New() flagSet := flag.NewFlagSet("gsm", flag.ExitOnError) - registry := registry.NewProviderRegistry(logger) + cache := cache.NewFileCache(logger) + + registry := registry.NewProviderRegistry(logger, cache) registry.Register(minecraft.Name, minecraft.NewMinecraftProvider) registry.Register(nintendo_switch.Name, nintendo_switch.NewNintendoSwitchProvider) registry.Register(playstation4.Name, playstation4.NewPlaystation4Provider) diff --git a/internal/models/cache.go b/internal/models/cache.go new file mode 100644 index 0000000..68b8227 --- /dev/null +++ b/internal/models/cache.go @@ -0,0 +1,15 @@ +package models + +import ( + "errors" + "time" +) + +var ErrCacheKeyDontExist = errors.New("cache key don't exist") + +type Cache interface { + Delete(key string) error + Get(key string) (string, error) + GetExpiry(key string, expiration time.Duration) (string, error) + Put(key, value string) error +} diff --git a/internal/models/provider.go b/internal/models/provider.go index a555416..1a35a58 100644 --- a/internal/models/provider.go +++ b/internal/models/provider.go @@ -10,4 +10,4 @@ type Provider interface { FindGames(options ProviderOptions) ([]*Game, error) } -type ProviderFactory func(logger *logrus.Logger) Provider +type ProviderFactory func(logger *logrus.Logger, cache Cache) Provider diff --git a/pkg/cache/file.go b/pkg/cache/file.go new file mode 100644 index 0000000..f1d9fa9 --- /dev/null +++ b/pkg/cache/file.go @@ -0,0 +1,78 @@ +package cache + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/fmartingr/games-screenshot-manager/internal/models" + "github.com/sirupsen/logrus" +) + +type FileCache struct { + logger *logrus.Entry + path string +} + +func (c *FileCache) Get(key string) (result string, err error) { + path := filepath.Join(c.path, key) + + contents, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) { + return result, models.ErrCacheKeyDontExist + } + return + } + + return string(contents), nil +} + +func (c *FileCache) GetExpiry(key string, expiration time.Duration) (result string, err error) { + path := filepath.Join(c.path, key) + info, err := os.Stat(path) + if err != nil { + return result, models.ErrCacheKeyDontExist + } + + if info.ModTime().Add(expiration).Before(time.Now()) { + c.Delete(key) + return result, models.ErrCacheKeyDontExist + } + + return c.Get(key) +} + +func (c *FileCache) Put(key, value string) error { + path := filepath.Join(c.path, key) + + if err := os.WriteFile(path, []byte(value), 0766); err != nil { + return fmt.Errorf("error writting cache file: %s", err) + } + + return nil +} + +func (c *FileCache) Delete(key string) error { + path := filepath.Join(c.path, key) + return os.Remove(path) +} + +func NewFileCache(logger *logrus.Logger) *FileCache { + userCacheDir, err := os.UserCacheDir() + if err != nil { + logger.Fatalf("error getting cache directory: %s", err) + } + path := filepath.Join(userCacheDir, "games-screenshot-manager") + + if err := os.MkdirAll(path, 0755); err != nil { + logger.Error(err) + } + + return &FileCache{ + logger: logger.WithField("from", "cache.file"), + path: path, + } +} diff --git a/pkg/cache/memory.go b/pkg/cache/memory.go new file mode 100644 index 0000000..9ade606 --- /dev/null +++ b/pkg/cache/memory.go @@ -0,0 +1,54 @@ +package cache + +import ( + "sync" + "time" + + "github.com/fmartingr/games-screenshot-manager/internal/models" + "github.com/sirupsen/logrus" +) + +type MemoryCache struct { + logger *logrus.Entry + data map[string]string + dataMu sync.RWMutex +} + +func (c *MemoryCache) Get(key string) (result string, err error) { + c.dataMu.RLock() + defer c.dataMu.RUnlock() + + result, exists := c.data[key] + if !exists { + return result, models.ErrCacheKeyDontExist + } + + return +} + +func (c *MemoryCache) Put(key, value string) error { + c.dataMu.Lock() + c.data[key] = value + c.dataMu.Unlock() + + return nil +} + +func (c *MemoryCache) GetExpiry(key string, expiration time.Duration) (string, error) { + // Since this is a in-memory storage, expiration is not required as of now. + return c.Get(key) +} + +func (c *MemoryCache) Delete(key string) error { + c.dataMu.Lock() + delete(c.data, key) + c.dataMu.Unlock() + return nil +} + +func NewMemoryCache(logger *logrus.Logger) *MemoryCache { + return &MemoryCache{ + logger: logger.WithField("from", "cache.file"), + data: make(map[string]string), + } +} diff --git a/pkg/providers/minecraft/provider.go b/pkg/providers/minecraft/provider.go index 496daaa..c312109 100644 --- a/pkg/providers/minecraft/provider.go +++ b/pkg/providers/minecraft/provider.go @@ -48,7 +48,7 @@ func (p *MinecraftProvider) FindGames(options models.ProviderOptions) ([]*models return result, nil } -func NewMinecraftProvider(logger *logrus.Logger) models.Provider { +func NewMinecraftProvider(logger *logrus.Logger, cache models.Cache) models.Provider { return &MinecraftProvider{ logger: logger.WithField("from", "provider."+Name), } diff --git a/pkg/providers/nintendo_switch/provider.go b/pkg/providers/nintendo_switch/provider.go index afa598b..5003e67 100644 --- a/pkg/providers/nintendo_switch/provider.go +++ b/pkg/providers/nintendo_switch/provider.go @@ -57,7 +57,7 @@ func (p *NintendoSwitchProvider) FindGames(options models.ProviderOptions) ([]*m return userGames, nil } -func NewNintendoSwitchProvider(logger *logrus.Logger) models.Provider { +func NewNintendoSwitchProvider(logger *logrus.Logger, cache models.Cache) models.Provider { return &NintendoSwitchProvider{ logger: logger.WithField("from", "provider."+Name), } diff --git a/pkg/providers/playstation4/provider.go b/pkg/providers/playstation4/provider.go index e249bed..d808fba 100644 --- a/pkg/providers/playstation4/provider.go +++ b/pkg/providers/playstation4/provider.go @@ -77,7 +77,7 @@ func (p *Playstation4Provider) FindGames(options models.ProviderOptions) ([]*mod return userGames, nil } -func NewPlaystation4Provider(logger *logrus.Logger) models.Provider { +func NewPlaystation4Provider(logger *logrus.Logger, cache models.Cache) models.Provider { return &Playstation4Provider{ logger: logger.WithField("from", "provider."+Name), } diff --git a/pkg/providers/retroarch/provider.go b/pkg/providers/retroarch/provider.go index 3c8db5b..336ee2a 100644 --- a/pkg/providers/retroarch/provider.go +++ b/pkg/providers/retroarch/provider.go @@ -55,7 +55,7 @@ func (p *RetroArchProvider) FindGames(options models.ProviderOptions) ([]*models return userGames, nil } -func NewRetroArchProvider(logger *logrus.Logger) models.Provider { +func NewRetroArchProvider(logger *logrus.Logger, cache models.Cache) models.Provider { return &RetroArchProvider{ logger: logger.WithField("from", "provider."+Name), } diff --git a/pkg/providers/steam/helpers.go b/pkg/providers/steam/helpers.go index bae9784..e5ce6b9 100644 --- a/pkg/providers/steam/helpers.go +++ b/pkg/providers/steam/helpers.go @@ -2,6 +2,7 @@ package steam import ( "encoding/json" + "errors" "fmt" "io/ioutil" "os" @@ -9,6 +10,7 @@ import ( "runtime" "strconv" "strings" + "time" "github.com/fmartingr/games-screenshot-manager/internal/models" "github.com/fmartingr/games-screenshot-manager/pkg/helpers" @@ -30,25 +32,47 @@ func getBasePathForOS() (string, error) { return path, nil } -func getSteamAppList(logger *logrus.Entry, c chan SteamAppList) { +func getSteamAppList(logger *logrus.Entry, cache models.Cache, c chan SteamAppList) { defer close(c) + cacheKey := "steam-applist" + download := true + var payload []byte - response, err := helpers.DoRequest("GET", gameListURL) - if err != nil { - logger.Errorf("Error making request for Steam APP List: %s", err) + result, err := cache.GetExpiry(cacheKey, 24*time.Hour) + if err != nil && !errors.Is(err, models.ErrCacheKeyDontExist) { + logger.Errorf("error retrieving cache: %s", err) + return } - if response.Body != nil { - defer response.Body.Close() + if len(result) > 0 { + download = false + payload = []byte(result) } - body, err := ioutil.ReadAll(response.Body) - if err != nil { - logger.Errorf("Error reading steam response: %s", err) + if download { + response, err := helpers.DoRequest("GET", gameListURL) + if err != nil { + logger.Errorf("Error making request for Steam APP List: %s", err) + } + + if response.Body != nil { + defer response.Body.Close() + } + + payload, err = ioutil.ReadAll(response.Body) + if err != nil { + logger.Errorf("Error reading steam response: %s", err) + } + } + + if download { + if err := cache.Put(cacheKey, string(payload)); err != nil { + logger.Error(err) + } } steamListResponse := SteamAppListResponse{} - jsonErr := json.Unmarshal(body, &steamListResponse) + jsonErr := json.Unmarshal(payload, &steamListResponse) if jsonErr != nil { logger.Errorf("Error unmarshalling steam's response: %s", jsonErr) } diff --git a/pkg/providers/steam/provider.go b/pkg/providers/steam/provider.go index 9183885..351234b 100644 --- a/pkg/providers/steam/provider.go +++ b/pkg/providers/steam/provider.go @@ -43,6 +43,7 @@ type SteamAppListResponse struct { type SteamProvider struct { logger *logrus.Entry + cache models.Cache } func (p *SteamProvider) FindGames(options models.ProviderOptions) ([]*models.Game, error) { @@ -53,7 +54,7 @@ func (p *SteamProvider) FindGames(options models.ProviderOptions) ([]*models.Gam var localGames []*models.Game c := make(chan SteamAppList) - go getSteamAppList(p.logger, c) + go getSteamAppList(p.logger, p.cache, c) users, err := guessUsers(basePath) if err != nil { @@ -93,8 +94,9 @@ func (p *SteamProvider) FindGames(options models.ProviderOptions) ([]*models.Gam return localGames, nil } -func NewSteamProvider(logger *logrus.Logger) models.Provider { +func NewSteamProvider(logger *logrus.Logger, cache models.Cache) models.Provider { return &SteamProvider{ + cache: cache, logger: logger.WithField("from", "provider."+Name), } } diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 1af32c4..d5d5e80 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -11,7 +11,9 @@ var ErrProviderAlreadyRegistered = errors.New("provider already registered") var ErrProviderNotRegistered = errors.New("provider not registered") type ProviderRegistry struct { - logger *logrus.Entry + logger *logrus.Entry + cache models.Cache + providers map[string]*models.Provider } @@ -21,7 +23,7 @@ func (r *ProviderRegistry) Register(name string, providerFactory models.Provider return ErrProviderAlreadyRegistered } - provider := providerFactory(r.logger.Logger) + provider := providerFactory(r.logger.Logger, r.cache) r.providers[name] = &provider return nil @@ -35,9 +37,10 @@ func (r *ProviderRegistry) Get(providerName string) (models.Provider, error) { return *provider, nil } -func NewProviderRegistry(logger *logrus.Logger) *ProviderRegistry { +func NewProviderRegistry(logger *logrus.Logger, cache models.Cache) *ProviderRegistry { return &ProviderRegistry{ logger: logger.WithField("from", "registry"), + cache: cache, providers: make(map[string]*models.Provider), } }