diff --git a/cointop/cache.go b/cointop/cache.go deleted file mode 100644 index 52bd44c..0000000 --- a/cointop/cache.go +++ /dev/null @@ -1,54 +0,0 @@ -package cointop - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "os" -) - -func (ct *Cointop) writeHardCache(data interface{}, filename string) error { - b, err := json.Marshal(data) - if err != nil { - return err - } - path := fmt.Sprintf("%s/.%s", ct.hardCachePath(), filename) - of, err := os.Create(path) - defer of.Close() - if err != nil { - return err - } - _, err = of.Write(b) - if err != nil { - return err - } - return nil -} - -func (ct *Cointop) readHardCache(i interface{}, filename string) (interface{}, bool, error) { - path := fmt.Sprintf("%s/.%s", ct.hardCachePath(), filename) - b, err := ioutil.ReadFile(path) - if err != nil { - return nil, false, err - } - err = json.Unmarshal(b, &i) - if err != nil { - return nil, false, err - } - return i, true, nil -} - -func (ct *Cointop) hardCachePath() string { - return fmt.Sprintf("%v%v", ct.configDirPath(), "/.cache") -} - -func (ct *Cointop) createCacheDir() error { - path := ct.hardCachePath() - if _, err := os.Stat(path); os.IsNotExist(err) { - err := os.MkdirAll(path, os.ModePerm) - if err != nil { - return err - } - } - return nil -} diff --git a/cointop/chart.go b/cointop/chart.go index a1aba3e..1e85347 100644 --- a/cointop/chart.go +++ b/cointop/chart.go @@ -6,6 +6,7 @@ import ( "time" "github.com/miguelmota/cointop/pkg/color" + "github.com/miguelmota/cointop/pkg/fcache" "github.com/miguelmota/cointop/pkg/termui" ) @@ -46,32 +47,42 @@ func (ct *Cointop) chartPoints(maxX int, coin string) error { end := secs var data []float64 - filename := strings.ToLower(coin) - if filename == "" { - filename = "globaldata" + cachekey := strings.ToLower(coin) + if cachekey == "" { + cachekey = "globaldata" } - if coin == "" { - graphData, err := ct.api.GetGlobalMarketGraphData(start, end) - if err != nil { - return nil - } - for i := range graphData.MarketCapByAvailableSupply { - data = append(data, graphData.MarketCapByAvailableSupply[i][1]/1E9) - } - } else { - graphData, err := ct.api.GetCoinGraphData(coin, start, end) - if err != nil { - return nil - } - for i := range graphData.PriceUSD { - data = append(data, graphData.PriceUSD[i][1]) - } + cached, found := ct.cache.Get(cachekey) + if found { + // cache hit + data, _ = cached.([]float64) + ct.debuglog("soft cache hit") } - go func() { - _ = ct.writeHardCache(data, filename) - }() + if len(data) == 0 { + if coin == "" { + graphData, err := ct.api.GetGlobalMarketGraphData(start, end) + if err != nil { + return nil + } + for i := range graphData.MarketCapByAvailableSupply { + data = append(data, graphData.MarketCapByAvailableSupply[i][1]/1E9) + } + } else { + graphData, err := ct.api.GetCoinGraphData(coin, start, end) + if err != nil { + return nil + } + for i := range graphData.PriceUSD { + data = append(data, graphData.PriceUSD[i][1]) + } + } + + ct.cache.Set(cachekey, data, 10*time.Second) + go func() { + _ = fcache.Set(cachekey, data, 24*time.Hour) + }() + } chart.Data = data termui.Body = termui.NewGrid() diff --git a/cointop/cointop.go b/cointop/cointop.go index 18a68a1..39458b3 100644 --- a/cointop/cointop.go +++ b/cointop/cointop.go @@ -72,10 +72,6 @@ func Run() { if err != nil { log.Fatal(err) } - err = ct.createCacheDir() - if err != nil { - log.Fatal(err) - } g, err := gocui.NewGui(gocui.Output256) if err != nil { log.Fatalf("new gocui: %v", err) diff --git a/cointop/list.go b/cointop/list.go index 15199a1..878f8cb 100644 --- a/cointop/list.go +++ b/cointop/list.go @@ -4,6 +4,7 @@ import ( "time" types "github.com/miguelmota/cointop/pkg/api/types" + "github.com/miguelmota/cointop/pkg/fcache" ) func (ct *Cointop) updateCoins() error { @@ -28,7 +29,7 @@ func (ct *Cointop) updateCoins() error { } ct.cache.Set(cachekey, allcoinsmap, 10*time.Second) go func() { - _ = ct.writeHardCache(allcoinsmap, "allcoinsmap") + _ = fcache.Set(cachekey, allcoinsmap, 24*time.Hour) }() } diff --git a/cointop/marketbar.go b/cointop/marketbar.go index b205cf1..088d9b5 100644 --- a/cointop/marketbar.go +++ b/cointop/marketbar.go @@ -2,21 +2,41 @@ package cointop import ( "fmt" + "time" + types "github.com/miguelmota/cointop/pkg/api/types" "github.com/miguelmota/cointop/pkg/color" + "github.com/miguelmota/cointop/pkg/fcache" "github.com/miguelmota/cointop/pkg/humanize" "github.com/miguelmota/cointop/pkg/pad" ) func (ct *Cointop) updateMarketbar() error { maxX := ct.width() - market, err := ct.api.GetGlobalMarketData() - if err != nil { - return err + + var market types.GlobalMarketData + var err error + cachekey := "market" + cached, found := ct.cache.Get(cachekey) + if found { + // cache hit + var ok bool + market, ok = cached.(types.GlobalMarketData) + if ok { + ct.debuglog("soft cache hit") + } + } else { + market, err = ct.api.GetGlobalMarketData() + if err != nil { + return err + } + + ct.cache.Set(cachekey, market, 10*time.Second) + go func() { + _ = fcache.Set(cachekey, market, 24*time.Hour) + }() } - go func() { - _ = ct.writeHardCache(market, "market") - }() + timeframe := "7 Day" chartname := ct.selectedCoinName() if chartname == "" { diff --git a/pkg/fcache/fcache.go b/pkg/fcache/fcache.go new file mode 100644 index 0000000..e027d16 --- /dev/null +++ b/pkg/fcache/fcache.go @@ -0,0 +1,130 @@ +package fcache + +import ( + "bytes" + "encoding/gob" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "time" +) + +const cachedir = "/tmp" + +// Set writes item to cache +func Set(key string, data interface{}, expire time.Duration) error { + key = regexp.MustCompile("[^a-zA-Z0-9_-]").ReplaceAllLiteralString(key, "") + file := fmt.Sprintf("fcache.%s.%v", key, strconv.FormatInt(time.Now().Add(expire).Unix(), 10)) + fpath := filepath.Join(cachedir, file) + + clean(key) + + serialized, err := serialize(data) + if err != nil { + return err + } + + var fmutex sync.RWMutex + fmutex.Lock() + defer fmutex.Unlock() + fp, err := os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return err + } + + defer fp.Close() + if _, err = fp.Write(serialized); err != nil { + return err + } + + return nil +} + +// Get reads item from cache +func Get(key string, dst interface{}) error { + key = regexp.MustCompile("[^a-zA-Z0-9_-]").ReplaceAllLiteralString(key, "") + pattern := filepath.Join(cachedir, fmt.Sprintf("fcache.%s.*", key)) + files, err := filepath.Glob(pattern) + if len(files) < 1 || err != nil { + return errors.New("fcache: no cache file found") + } + + if _, err = os.Stat(files[0]); err != nil { + return err + } + + fp, err := os.OpenFile(files[0], os.O_RDONLY, 0400) + if err != nil { + return err + } + defer fp.Close() + + var serialized []byte + buf := make([]byte, 1024) + for { + var n int + n, err = fp.Read(buf) + serialized = append(serialized, buf[0:n]...) + if err != nil || err == io.EOF { + break + } + } + + if err = deserialize(serialized, dst); err != nil { + return err + } + + for _, file := range files { + exptime, err := strconv.ParseInt(strings.Split(file, ".")[2], 10, 64) + if err != nil { + return err + } + + if exptime < time.Now().Unix() { + if _, err = os.Stat(file); err == nil { + os.Remove(file) + } + } + } + + return nil +} + +// clean removes item from cache +func clean(key string) error { + pattern := filepath.Join(cachedir, fmt.Sprintf("fcache.%s.*", key)) + files, _ := filepath.Glob(pattern) + for _, file := range files { + if _, err := os.Stat(file); err == nil { + os.Remove(file) + } + } + + return nil +} + +// serialize encodes a value using binary +func serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + if err := gob.NewEncoder(buf).Encode(src); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +// deserialize decodes a value using binary +func deserialize(src []byte, dst interface{}) error { + buf := bytes.NewReader(src) + if err := gob.NewDecoder(buf).Decode(dst); err != nil { + return err + } + + return nil +}