aboutsummaryrefslogtreecommitdiff
path: root/internal/feed/cache.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--internal/feed/cache.go230
1 files changed, 230 insertions, 0 deletions
diff --git a/internal/feed/cache.go b/internal/feed/cache.go
new file mode 100644
index 0000000..4f27144
--- /dev/null
+++ b/internal/feed/cache.go
@@ -0,0 +1,230 @@
+package feed
+
+import (
+ "bufio"
+ "crypto/sha256"
+ "encoding/gob"
+ "errors"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/Necoro/feed2imap-go/internal/log"
+)
+
+const (
+ currentVersion byte = 1
+ startFeedId uint64 = 1
+)
+
+type Cache interface {
+ findItem(*Feed) CachedFeed
+ Version() byte
+ transformToCurrent() (Cache, error)
+}
+
+type feedId uint64
+
+type feedDescriptor struct {
+ Name string
+ Url string
+}
+
+type CachedFeed interface {
+ Checked(withFailure bool)
+ Failures() uint
+}
+
+type cachedFeed struct {
+ LastCheck time.Time
+ NumFailures uint // can't be named `Failures` b/c it'll collide with the interface
+ Items []cachedItem
+}
+
+func (cf *cachedFeed) Checked(withFailure bool) {
+ cf.LastCheck = time.Now()
+ if withFailure {
+ cf.NumFailures++
+ } else {
+ cf.NumFailures = 0
+ }
+}
+
+func (cf *cachedFeed) Failures() uint {
+ return cf.NumFailures
+}
+
+type itemHash [sha256.Size]byte
+
+type cachedItem struct {
+ Uid string
+ Title string
+ Link string
+ Date time.Time
+ Updated time.Time
+ Creator string
+ Hash itemHash
+}
+
+type v1Cache struct {
+ version byte
+ Ids map[feedDescriptor]feedId
+ NextId uint64
+ Feeds map[feedId]*cachedFeed
+}
+
+func (cache *v1Cache) Version() byte {
+ return cache.version
+}
+
+func New() Cache {
+ cache := v1Cache{
+ Ids: map[feedDescriptor]feedId{},
+ Feeds: map[feedId]*cachedFeed{},
+ NextId: startFeedId,
+ }
+ cache.version = currentVersion
+ return &cache
+}
+
+func cacheForVersion(version byte) (Cache, error) {
+ switch version {
+ case 1:
+ return New(), nil
+ default:
+ return nil, fmt.Errorf("unknown cache version '%d'", version)
+ }
+}
+
+func (cache *v1Cache) transformToCurrent() (Cache, error) {
+ return cache, nil
+}
+
+func (cache *v1Cache) getItem(id feedId) CachedFeed {
+ feed, ok := cache.Feeds[id]
+ if !ok {
+ feed = &cachedFeed{}
+ cache.Feeds[id] = feed
+ }
+ return feed
+}
+
+func (cache *v1Cache) findItem(feed *Feed) CachedFeed {
+ if feed.cached != nil {
+ return feed.cached.(*cachedFeed)
+ }
+
+ fId := feedDescriptor{Name: feed.Name, Url: feed.Url}
+ id, ok := cache.Ids[fId]
+ if !ok {
+ var otherId feedDescriptor
+ changed := false
+ for otherId, id = range cache.Ids {
+ if otherId.Name == fId.Name {
+ log.Warnf("Feed %s seems to have changed URLs: New '%s', old '%s'. Updating.",
+ fId.Name, fId.Url, otherId.Url)
+ changed = true
+ break
+ } else if otherId.Url == fId.Url {
+ log.Warnf("Feed with URL '%s' seems to have changed its name: New '%s', old '%s'. Updating",
+ fId.Url, fId.Name, otherId.Name)
+ changed = true
+ break
+ }
+ }
+ if changed {
+ delete(cache.Ids, otherId)
+ } else {
+ id = feedId(cache.NextId)
+ cache.NextId++
+ }
+
+ cache.Ids[fId] = id
+ }
+
+ item := cache.getItem(id)
+ feed.cached = item
+ return item
+}
+
+func (feeds *Feeds) StoreCache(fileName string) error {
+ cache := feeds.cache
+ if cache == nil {
+ return fmt.Errorf("trying to store nil cache")
+ }
+ if cache.Version() != currentVersion {
+ return fmt.Errorf("trying to store cache with unsupported version '%d' (current: '%d')", cache.Version(), currentVersion)
+ }
+
+ f, err := os.Create(fileName)
+ if err != nil {
+ return fmt.Errorf("trying to store cache to '%s': %w", fileName, err)
+ }
+ defer f.Close()
+
+ writer := bufio.NewWriter(f)
+ if err = writer.WriteByte(currentVersion); err != nil {
+ return fmt.Errorf("writing to '%s': %w", fileName, err)
+ }
+
+ encoder := gob.NewEncoder(writer)
+ if err = encoder.Encode(cache); err != nil {
+ return fmt.Errorf("encoding cache: %w", err)
+ }
+
+ writer.Flush()
+ log.Printf("Stored cache to '%s'.", fileName)
+
+ return nil
+}
+
+func (feeds *Feeds) LoadCache(fileName string) error {
+ cache, err := loadCache(fileName)
+ if err != nil {
+ return err
+ }
+ feeds.cache = cache
+
+ for _, feed := range feeds.feeds {
+ feed.cached = cache.findItem(feed)
+ }
+ return nil
+}
+
+func loadCache(fileName string) (Cache, error) {
+ f, err := os.Open(fileName)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ // no cache there yet -- make new
+ return New(), nil
+ }
+ return nil, fmt.Errorf("opening cache at '%s': %w", fileName, err)
+ }
+ defer f.Close()
+
+ log.Printf("Loading cache from '%s'", fileName)
+
+ reader := bufio.NewReader(f)
+ version, err := reader.ReadByte()
+ if err != nil {
+ return nil, fmt.Errorf("reading from '%s': %w", fileName, err)
+ }
+
+ cache, err := cacheForVersion(version)
+ if err != nil {
+ return nil, err
+ }
+
+ decoder := gob.NewDecoder(reader)
+ if err = decoder.Decode(cache); err != nil {
+ return nil, fmt.Errorf("decoding for version '%d' from '%s': %w", version, fileName, err)
+ }
+
+ if cache, err = cache.transformToCurrent(); err != nil {
+ return nil, fmt.Errorf("cannot transform from version %d to %d: %w", version, currentVersion, err)
+ }
+
+ log.Printf("Loaded cache (version %d), transformed to version %d.", version, currentVersion)
+
+ return cache, nil
+}