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
This commit is contained in:
Felipe Martin Garcia 2022-01-30 19:25:16 +01:00
parent 3d56f84762
commit af72c45833
12 changed files with 200 additions and 21 deletions

View File

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

15
internal/models/cache.go Normal file
View File

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

View File

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

78
pkg/cache/file.go vendored Normal file
View File

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

54
pkg/cache/memory.go vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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