package anidb
import (
- "encoding/gob"
"fmt"
"github.com/Kovensky/go-anidb/http"
"github.com/Kovensky/go-anidb/misc"
"github.com/Kovensky/go-anidb/udp"
+ "github.com/Kovensky/go-fscache"
"log"
"sort"
"strconv"
"time"
)
-func init() {
- gob.RegisterName("*github.com/Kovensky/go-anidb.Anime", &Anime{})
- gob.RegisterName("github.com/Kovensky/go-anidb.AID", AID(0))
-}
+var _ cacheable = &Anime{}
-func (a *Anime) Touch() {
- a.Cached = time.Now()
+func (a *Anime) setCachedTS(ts time.Time) {
+ a.Cached = ts
}
func (a *Anime) IsStale() bool {
// Unique Anime IDentifier.
type AID int
-// make AID Cacheable
-
-func (e AID) Touch() {}
-func (e AID) IsStale() bool { return false }
-
// Returns a cached Anime. Returns nil if there is no cached Anime with this AID.
func (aid AID) Anime() *Anime {
var a Anime
- if cache.Get(&a, "aid", aid) == nil {
+ if CacheGet(&a, "aid", aid) == nil {
return &a
}
return nil
// Retrieves an Anime by its AID. Uses both the HTTP and UDP APIs,
// but can work without the UDP API.
func (adb *AniDB) AnimeByID(aid AID) <-chan *Anime {
- keys := []cacheKey{"aid", aid}
+ key := []fscache.CacheKey{"aid", aid}
ch := make(chan *Anime, 1)
if aid < 1 {
close(ch)
}
- ic := make(chan Cacheable, 1)
+ ic := make(chan notification, 1)
go func() { ch <- (<-ic).(*Anime); close(ch) }()
- if intentMap.Intent(ic, keys...) {
+ if intentMap.Intent(ic, key...) {
return ch
}
- if !cache.CheckValid(keys...) {
- intentMap.NotifyClose((*Anime)(nil), keys...)
+ if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
+ intentMap.NotifyClose((*Anime)(nil), key...)
return ch
}
anime := aid.Anime()
if !anime.IsStale() {
- intentMap.NotifyClose(anime, keys...)
+ intentMap.NotifyClose(anime, key...)
return ch
}
} else {
// HTTP ok but parsing not ok
if anime.PrimaryTitle == "" {
- cache.MarkInvalid(keys...)
+ Cache.SetInvalid(key...)
}
switch resp.anime.Error {
case "Anime not found", "aid Missing or Invalid":
// deleted AID?
- cache.Delete(keys...)
+ Cache.Delete(key...)
}
ok = false
httpChan = nil
case reply := <-udpChan:
if reply.Code() == 330 {
- cache.MarkInvalid(keys...)
+ Cache.SetInvalid(key...)
// deleted AID?
- cache.Delete(keys...)
+ Cache.Delete(key...)
ok = false
break Loop
}
if anime.PrimaryTitle != "" {
if ok {
- cache.Set(anime, keys...)
+ CacheSet(anime, key...)
}
- intentMap.NotifyClose(anime, keys...)
+ intentMap.NotifyClose(anime, key...)
} else {
- intentMap.NotifyClose((*Anime)(nil), keys...)
+ intentMap.NotifyClose((*Anime)(nil), key...)
}
}()
return ch
package anidb
import (
- "bytes"
- "compress/gzip"
- "encoding/gob"
- "errors"
- "fmt"
- "io"
+ "github.com/Kovensky/go-fscache"
"os"
"path"
- "reflect"
- "regexp"
- "sync"
"time"
)
-type Cacheable interface {
- // Updates the last modified time
- Touch()
- // Returns true if the Cacheable is nil, or if the last modified time is too old.
- IsStale() bool
-}
-
-func init() {
- gob.RegisterName("*github.com/Kovensky/go-anidb.invalidKeyCache", &invalidKeyCache{})
-}
-
-type invalidKeyCache struct{ time.Time }
-
-func (c *invalidKeyCache) Touch() {
- c.Time = time.Now()
-}
-func (c *invalidKeyCache) IsStale() bool {
- return time.Now().Sub(c.Time) > InvalidKeyCacheDuration
-}
-
-type cacheDir struct {
- *sync.RWMutex
-
- CacheDir string
-}
-
func init() {
- if err := SetCacheDir(path.Join(os.TempDir(), "anidb", "cache")); err != nil {
+ c, err := fscache.NewCacheDir(path.Join(os.TempDir(), "anidb", "cache"))
+ if err != nil {
panic(err)
}
-}
-
-var cache cacheDir
-
-// Sets the cache directory to the given path.
-//
-// go-anidb needs a valid cache directory to function, so, during module
-// initialization, it uses os.TempDir() to set a default cache dir.
-// go-anidb panics if it's unable to set the default cache dir.
-func SetCacheDir(path string) (err error) {
- m := cache.RWMutex
- if m == nil {
- m = &sync.RWMutex{}
- cache.RWMutex = m
- }
- cache.Lock()
-
- if err = os.MkdirAll(path, 0755|os.ModeDir); err != nil {
- cache.Unlock()
- return err
- }
+ Cache = *c
- cache = cacheDir{
- RWMutex: m,
- CacheDir: path,
- }
-
- cache.Unlock()
RefreshTitles()
- return nil
-}
-
-// Returns the current cache dir.
-func GetCacheDir() (path string) {
- cache.RLock()
- defer cache.RUnlock()
-
- return cache.CacheDir
-}
-
-type cacheKey interface{}
-
-// All "bad characters" that can't go in Windows paths.
-// It's a superset of the "bad characters" on other OSes, so this works.
-var badPath = regexp.MustCompile(`[\\/:\*\?\"<>\|]`)
-
-func stringify(stuff ...cacheKey) []string {
- ret := make([]string, len(stuff))
- for i := range stuff {
- s := fmt.Sprint(stuff[i])
- ret[i] = badPath.ReplaceAllLiteralString(s, "_")
- }
- return ret
}
-// Each key but the last is treated as a directory.
-// The last key is treated as a regular file.
-//
-// This also means that cache keys that are file-backed
-// cannot have subkeys.
-func cachePath(keys ...cacheKey) string {
- parts := append([]string{GetCacheDir()}, stringify(keys...)...)
- p := path.Join(parts...)
- return p
-}
-
-// Opens the file that backs the specified keys.
-func (c *cacheDir) Open(keys ...cacheKey) (fh *os.File, err error) {
- subItem := cachePath(keys...)
- return os.Open(subItem)
-}
-
-// Creates a new file to back the specified keys.
-func (c *cacheDir) Create(keys ...cacheKey) (fh *os.File, err error) {
- subItem := cachePath(keys...)
- subDir := path.Dir(subItem)
-
- if err = os.MkdirAll(subDir, 0755|os.ModeDir); err != nil {
- return nil, err
- }
- return os.Create(subItem)
-}
+var Cache fscache.CacheDir
-// Deletes the file that backs the specified keys.
-func (c *cacheDir) Delete(keys ...cacheKey) (err error) {
- return os.Remove(cachePath(keys...))
+type cacheable interface {
+ setCachedTS(time.Time)
}
-// Deletes the specified key and all subkeys.
-func (c *cacheDir) DeleteAll(keys ...cacheKey) (err error) {
- return os.RemoveAll(cachePath(keys...))
-}
-
-func (c *cacheDir) Get(v Cacheable, keys ...cacheKey) (err error) {
-
- val := reflect.ValueOf(v)
- if k := val.Kind(); k == reflect.Ptr || k == reflect.Interface {
- val = val.Elem()
- }
- if !val.CanSet() {
- // panic because this is an internal coding mistake
- panic("(*cacheDir).Get(): given Cacheable is not setable")
- }
-
- flock := lockFile(cachePath(keys...))
- if flock != nil {
- flock.Lock()
- }
- defer func() {
- if flock != nil {
- flock.Unlock()
- }
- }()
-
- fh, err := c.Open(keys...)
- if err != nil {
- return err
- }
-
- buf := bytes.Buffer{}
- if _, err = io.Copy(&buf, fh); err != nil {
- fh.Close()
- return err
- }
- if err = fh.Close(); err != nil {
- return err
- }
-
- if flock != nil {
- flock.Unlock()
- flock = nil
- }
-
- gz, err := gzip.NewReader(&buf)
+func CacheSet(v interface{}, key ...fscache.CacheKey) (err error) {
+ now := time.Now()
+ _, err = Cache.Set(v, key...)
if err != nil {
return err
}
- defer func() {
- if e := gz.Close(); err == nil {
- err = e
- }
- }()
-
- switch f := gz.Header.Comment; f {
- case "encoding/gob":
- dec := gob.NewDecoder(gz)
- err = dec.Decode(v)
- default:
- return errors.New(fmt.Sprintf("Cached data (format %q) is not in a known format", f))
+ switch t := v.(type) {
+ case cacheable:
+ t.setCachedTS(now)
}
-
return
}
-func (c *cacheDir) Set(v Cacheable, keys ...cacheKey) (n int64, err error) {
- if v := reflect.ValueOf(v); !v.IsValid() {
- panic("reflect.ValueOf() returned invaled value")
- } else if k := v.Kind(); k == reflect.Ptr || k == reflect.Interface {
- if v.IsNil() {
- return // no point in saving nil
- }
- }
-
- // First we encode to memory -- we don't want to create/truncate a file and put bad data in it.
- buf := bytes.Buffer{}
- gz, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
+func CacheGet(v interface{}, key ...fscache.CacheKey) (err error) {
+ ts, err := Cache.Get(v, key...)
if err != nil {
- return 0, err
- }
- gz.Header.Comment = "encoding/gob"
-
- // it doesn't matter if the caller doesn't see this,
- // the important part is that the cache does.
- v.Touch()
-
- enc := gob.NewEncoder(gz)
- err = enc.Encode(v)
-
- if e := gz.Close(); err == nil {
- err = e
- }
-
- if err != nil {
- return 0, err
+ return err
}
-
- // We have good data, time to actually put it in the cache
- if flock := lockFile(cachePath(keys...)); flock != nil {
- flock.Lock()
- defer flock.Unlock()
+ switch t := v.(type) {
+ case cacheable:
+ t.setCachedTS(ts)
}
-
- fh, err := c.Create(keys...)
- if err != nil {
- return 0, err
- }
- defer func() {
- if e := fh.Close(); err == nil {
- err = e
- }
- }()
- n, err = io.Copy(fh, &buf)
return
}
-
-// Checks if the given keys are not marked as invalid.
-//
-// If the key was marked as invalid but is no longer considered
-// so, deletes the invalid marker.
-func (c *cacheDir) CheckValid(keys ...cacheKey) bool {
- invKeys := append([]cacheKey{"invalid"}, keys...)
- inv := invalidKeyCache{}
-
- if cache.Get(&inv, invKeys...) == nil {
- if inv.IsStale() {
- cache.Delete(invKeys...)
- } else {
- return false
- }
- }
- return true
-}
-
-// Deletes the given keys and marks them as invalid.
-//
-// They are considered invalid for InvalidKeyCacheDuration.
-func (c *cacheDir) MarkInvalid(keys ...cacheKey) error {
- invKeys := append([]cacheKey{"invalid"}, keys...)
-
- cache.Delete(keys...)
- _, err := cache.Set(&invalidKeyCache{}, invKeys...)
- return err
-}
+++ /dev/null
-package anidb
-
-import (
- "encoding/gob"
- "os"
- "path"
- "reflect"
- "testing"
-)
-
-type stringifyVec struct {
- result []string
- data []cacheKey
-}
-
-func TestStringify(T *testing.T) {
- T.Parallel()
-
- vec := []stringifyVec{
- stringifyVec{[]string{"a"}, []cacheKey{"a"}},
- }
- for i, v := range vec {
- str := stringify(v.data...)
- if !reflect.DeepEqual(v.result, str) {
- T.Errorf("Vector #%d: Expected %v, got %v", i+1, v.result, str)
- }
- }
-}
-
-type cachePathVec struct {
- path string
- data []cacheKey
-}
-
-var testDir = path.Join(os.TempDir(), "testing", "anidb")
-
-func init() { SetCacheDir(testDir) }
-
-func TestCachePath(T *testing.T) {
- T.Parallel()
-
- vec := []cachePathVec{
- cachePathVec{path.Join(testDir, "a"), []cacheKey{"a"}},
- cachePathVec{path.Join(testDir, "b", "c", "d"), []cacheKey{"b", "c", "d"}},
- }
- for i, v := range vec {
- str := cachePath(v.data...)
-
- if v.path != str {
- T.Errorf("Vector #%d: Expected %v, got %v", i+1, v.path, str)
- }
- }
-}
-
-type testString string
-
-func (_ testString) Touch() {}
-func (_ testString) IsStale() bool { return false }
-
-func init() {
- gob.Register(testString(""))
-}
-
-func TestCacheRoundtrip(T *testing.T) {
- T.Parallel()
-
- test := testString("some string")
- _, err := cache.Set(test, "test", "string")
- if err != nil {
- T.Fatalf("Error storing: %v", err)
- }
-
- var t2 testString
- err = cache.Get(&t2, "test", "string")
- if err != nil {
- T.Errorf("Error reading: %v", err)
- }
-
- if test != t2 {
- T.Errorf("Expected %q, got %q", test, t2)
- }
-}
package anidb
import (
- "encoding/gob"
+ "github.com/Kovensky/go-fscache"
"strconv"
"strings"
"time"
)
-func init() {
- gob.RegisterName("*github.com/Kovensky/go-anidb.Episode", &Episode{})
-}
+var _ cacheable = &Episode{}
-func (e *Episode) Touch() {
- e.Cached = time.Now()
+func (e *Episode) setCachedTS(ts time.Time) {
+ e.Cached = ts
}
func (e *Episode) IsStale() bool {
// Retrieves the Episode corresponding to this EID from the cache.
func (eid EID) Episode() *Episode {
var e Episode
- if cache.Get(&e, "eid", eid) == nil {
+ if CacheGet(&e, "eid", eid) == nil {
return &e
}
return nil
}
func cacheEpisode(ep *Episode) {
- cache.Set(ep.AID, "aid", "by-eid", ep.EID)
- cache.Set(ep, "eid", ep.EID)
+ CacheSet(ep.AID, "aid", "by-eid", ep.EID)
+ CacheSet(ep, "eid", ep.EID)
}
// Retrieves an Episode by its EID.
// to an Anime query. Otherwise, uses both the HTTP and UDP
// APIs to retrieve it.
func (adb *AniDB) EpisodeByID(eid EID) <-chan *Episode {
- keys := []cacheKey{"eid", eid}
+ key := []fscache.CacheKey{"eid", eid}
ch := make(chan *Episode, 1)
if eid < 1 {
return ch
}
- ic := make(chan Cacheable, 1)
+ ic := make(chan notification, 1)
go func() { ch <- (<-ic).(*Episode); close(ch) }()
- if intentMap.Intent(ic, keys...) {
+ if intentMap.Intent(ic, key...) {
return ch
}
- if !cache.CheckValid(keys...) {
- intentMap.NotifyClose((*Episode)(nil), keys...)
+ if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
+ intentMap.NotifyClose((*Episode)(nil), key...)
return ch
}
e := eid.Episode()
if !e.IsStale() {
- intentMap.NotifyClose(e, keys...)
+ intentMap.NotifyClose(e, key...)
return ch
}
// API episode list.
aid := AID(0)
- ok := cache.Get(&aid, "aid", "by-eid", eid) == nil
+ _, err := Cache.Get(&aid, "aid", "by-eid", eid)
+ ok := err == nil
udpDone := false
break
}
} else if reply.Code() == 340 {
- cache.MarkInvalid(keys...)
- cache.Delete(keys...) // deleted EID?
+ Cache.SetInvalid(key...)
break
} else {
break
} else {
// the EID<->AID map broke
ok = false
- cache.Delete("aid", "by-eid", eid)
+ Cache.Delete("aid", "by-eid", eid)
}
}
- intentMap.NotifyClose(e, keys...)
+ intentMap.NotifyClose(e, key...)
}()
return ch
}
package anidb
import (
- "encoding/gob"
"fmt"
"github.com/Kovensky/go-anidb/misc"
"github.com/Kovensky/go-anidb/udp"
+ "github.com/Kovensky/go-fscache"
"image"
"log"
"regexp"
"time"
)
-func init() {
- gob.RegisterName("*github.com/Kovensky/go-anidb.File", &File{})
- gob.RegisterName("*github.com/Kovensky/go-anidb.fidCache", &fidCache{})
- gob.RegisterName("github.com/Kovensky/go-anidb.FID", FID(0))
-}
+var _ cacheable = &File{}
-func (f *File) Touch() {
- f.Cached = time.Now()
+func (f *File) setCachedTS(ts time.Time) {
+ f.Cached = ts
}
func (f *File) IsStale() bool {
return time.Now().Sub(f.Cached) > FileCacheDuration
}
-type FID int
-
-// make FID Cacheable
+func cacheFile(f *File) {
+ CacheSet(f.FID, "fid", "by-ed2k", f.Ed2kHash, f.Filesize)
+ CacheSet(f, "fid", f.FID)
+}
-func (e FID) Touch() {}
-func (e FID) IsStale() bool { return false }
+type FID int
func (fid FID) File() *File {
var f File
- if cache.Get(&f, "fid", fid) == nil {
+ if CacheGet(&f, "fid", fid) == nil {
return &f
}
return nil
}
-type fidCache struct {
- FID
- Time time.Time
-}
-
-func (c *fidCache) Touch() {
- c.Time = time.Now()
-}
-
-func (c *fidCache) IsStale() bool {
- return time.Now().Sub(c.Time) > FileCacheDuration
-}
-
// Prefetches the Anime, Episode and Group that this
// file is linked to using the given AniDB instance.
//
// Retrieves a File by its FID. Uses the UDP API.
func (adb *AniDB) FileByID(fid FID) <-chan *File {
- keys := []cacheKey{"fid", fid}
+ key := []fscache.CacheKey{"fid", fid}
ch := make(chan *File, 1)
return ch
}
- ic := make(chan Cacheable, 1)
+ ic := make(chan notification, 1)
go func() { ch <- (<-ic).(*File); close(ch) }()
- if intentMap.Intent(ic, keys...) {
+ if intentMap.Intent(ic, key...) {
return ch
}
- if !cache.CheckValid(keys...) {
- intentMap.NotifyClose((*File)(nil), keys...)
+ if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
+ intentMap.NotifyClose((*File)(nil), key...)
return ch
}
f := fid.File()
if !f.IsStale() {
- intentMap.NotifyClose(f, keys...)
+ intentMap.NotifyClose(f, key...)
return ch
}
if reply.Error() == nil {
f = adb.parseFileResponse(reply, false)
- cache.Set(&fidCache{FID: f.FID}, "fid", "by-ed2k", f.Ed2kHash, f.Filesize)
- cache.Set(f, keys...)
+ cacheFile(f)
} else if reply.Code() == 320 {
- cache.MarkInvalid(keys...)
+ Cache.SetInvalid(key...)
}
- intentMap.NotifyClose(f, keys...)
+ intentMap.NotifyClose(f, key...)
}()
return ch
}
// Retrieves a File by its Ed2kHash + Filesize combination. Uses the UDP API.
func (adb *AniDB) FileByEd2kSize(ed2k string, size int64) <-chan *File {
- keys := []cacheKey{"fid", "by-ed2k", ed2k, size}
+ key := []fscache.CacheKey{"fid", "by-ed2k", ed2k, size}
ch := make(chan *File, 1)
// AniDB always uses lower case hashes
ed2k = strings.ToLower(ed2k)
- ic := make(chan Cacheable, 1)
+ ic := make(chan notification, 1)
go func() {
fid := (<-ic).(FID)
if fid > 0 {
}
close(ch)
}()
- if intentMap.Intent(ic, keys...) {
+ if intentMap.Intent(ic, key...) {
return ch
}
- if !cache.CheckValid(keys...) {
- intentMap.NotifyClose(FID(0), keys...)
+ if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
+ intentMap.NotifyClose(FID(0), key...)
return ch
}
fid := FID(0)
- var ec fidCache
- if cache.Get(&ec, keys...) == nil && !ec.IsStale() {
- intentMap.NotifyClose(ec.FID, keys...)
+ switch ts, err := Cache.Get(&fid, key...); {
+ case err != nil && time.Now().Sub(ts) < FileCacheDuration:
+ intentMap.NotifyClose(fid, key...)
return ch
}
- fid = ec.FID
go func() {
reply := <-adb.udp.SendRecv("FILE",
fid = f.FID
- cache.Set(&fidCache{FID: fid}, keys...)
- cache.Set(f, "fid", fid)
+ cacheFile(f)
} else if reply.Code() == 320 { // file not found
- cache.MarkInvalid(keys...)
+ Cache.SetInvalid(key...)
} else if reply.Code() == 322 { // multiple files found
panic("Don't know what to do with " + strings.Join(reply.Lines(), "\n"))
}
- intentMap.NotifyClose(fid, keys...)
+ intentMap.NotifyClose(fid, key...)
}()
return ch
}
package anidb
import (
+ "github.com/Kovensky/go-fscache"
"strconv"
"strings"
"time"
)
-type fidList struct {
- FIDs []FID
- Time time.Time
-}
-
-func (l *fidList) Touch() { l.Time = time.Now() }
-func (l *fidList) IsStale() bool { return time.Now().Sub(l.Time) > FileCacheDuration }
-
// Gets the Files that the given Group has released for the given
// Episode. Convenience method that calls FilesByGID.
func (adb *AniDB) FilesByGroup(ep *Episode, g *Group) <-chan *File {
// On API error (offline, etc), the first *File returned is nil,
// followed by cached files (which may also be nil).
func (adb *AniDB) FIDsByGID(ep *Episode, gid GID) <-chan FID {
- keys := []cacheKey{"fid", "by-ep-gid", ep.EID, gid}
+ key := []fscache.CacheKey{"fid", "by-eid-gid", ep.EID, gid}
ch := make(chan FID, 10)
return ch
}
- ic := make(chan Cacheable, 1)
+ ic := make(chan notification, 1)
go func() {
for c := range ic {
ch <- c.(FID)
}
close(ch)
}()
- if intentMap.Intent(ic, keys...) {
+ if intentMap.Intent(ic, key...) {
return ch
}
- if !cache.CheckValid(keys...) {
- intentMap.Close(keys...)
+ if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
+ intentMap.Close(key...)
return ch
}
- var fids fidList
- if cache.Get(&fids, keys...) == nil {
- is := intentMap.LockIntent(keys...)
+ var fids []FID
+ switch ts, err := Cache.Get(&fids, key...); {
+ case err == nil && time.Now().Sub(ts) < FileCacheDuration:
+ is := intentMap.LockIntent(key...)
go func() {
- defer intentMap.Free(is, keys...)
+ defer intentMap.Free(is, key...)
defer is.Close()
- for _, fid := range fids.FIDs {
+ for _, fid := range fids {
is.Notify(fid)
}
}()
"amask": fileAmask,
})
- is := intentMap.LockIntent(keys...)
- defer intentMap.Free(is, keys...)
+ is := intentMap.LockIntent(key...)
+ defer intentMap.Free(is, key...)
switch reply.Code() {
case 220:
f := adb.parseFileResponse(reply, true)
- fids.FIDs = []FID{f.FID}
- cache.Set(&fids, keys...)
+ fids = []FID{f.FID}
+ CacheSet(&fids, key...)
- cache.Set(&fidCache{FID: f.FID}, "fid", "by-ed2k", f.Ed2kHash, f.Filesize)
- cache.Set(f, "fid", f.FID)
+ cacheFile(f)
is.NotifyClose(f.FID)
return
case 322:
parts := strings.Split(reply.Lines()[1], "|")
- fids.FIDs = make([]FID, len(parts))
+ fids = make([]FID, len(parts))
for i := range parts {
id, _ := strconv.ParseInt(parts[i], 10, 32)
- fids.FIDs[i] = FID(id)
+ fids[i] = FID(id)
}
- cache.Set(&fids, keys...)
+ CacheSet(&fids, key...)
case 320:
- cache.MarkInvalid(keys...)
- cache.Delete(keys...)
+ Cache.SetInvalid(key...)
is.Close()
return
default:
}
defer is.Close()
- for _, fid := range fids.FIDs {
+ for _, fid := range fids {
is.Notify(fid)
}
}()
+++ /dev/null
-package anidb
-
-type fileLock interface {
- Lock() error
- Unlock() error
-}
-
-// func lockFile(p path) fileLock
+++ /dev/null
-// +build !windows
-
-package anidb
-
-import "github.com/tgulacsi/go-locking"
-
-type flockLock struct {
- locking.FLock
-}
-
-func lockFile(p string) fileLock {
- flock, err := locking.NewFLock(p)
- if err == nil {
- return &flockLock{FLock: flock}
- }
- return nil
-}
-
-func (fl *flockLock) Lock() error {
- if fl != nil {
- return fl.FLock.Lock()
- }
- return nil
-}
-
-func (fl *flockLock) Unlock() error {
- if fl != nil {
- return fl.FLock.Unlock()
- }
- return nil
-}
+++ /dev/null
-package anidb
-
-type winFileLock struct{}
-
-func lockFile(p string) fileLock {
- return &winFileLock{}
-}
-
-// empty implementations -- go-locking doesn't support windows
-// windows also does file locking on its own
-func (_ *winFileLock) Lock() error { return nil }
-func (_ *winFileLock) Unlock() error { return nil }
package anidb
import (
- "encoding/gob"
"github.com/Kovensky/go-anidb/http"
"github.com/Kovensky/go-anidb/udp"
+ "github.com/Kovensky/go-fscache"
"strconv"
"strings"
"time"
)
-func init() {
- gob.RegisterName("*github.com/Kovensky/go-anidb.Group", &Group{})
- gob.RegisterName("github.com/Kovensky/go-anidb.GID", GID(0))
- gob.RegisterName("*github.com/Kovensky/go-anidb.gidCache", &gidCache{})
-}
+var _ cacheable = &Group{}
-func (g *Group) Touch() {
- g.Cached = time.Now()
+func (g *Group) setCachedTS(ts time.Time) {
+ g.Cached = ts
}
func (g *Group) IsStale() bool {
// Unique Group IDentifier
type GID int
-// make GID cacheable
-
-func (e GID) Touch() {}
-func (e GID) IsStale() bool { return false }
+func cacheGroup(g *Group) {
+ CacheSet(g.GID, "gid", "by-name", g.Name)
+ CacheSet(g.GID, "gid", "by-shortname", g.ShortName)
+ CacheSet(g, "gid", g.GID)
+}
// Retrieves the Group from the cache.
func (gid GID) Group() *Group {
var g Group
- if cache.Get(&g, "gid", gid) == nil {
+ if CacheGet(&g, "gid", gid) == nil {
return &g
}
return nil
}
-type gidCache struct {
- GID
- Time time.Time
-}
-
-func (c *gidCache) Touch() { c.Time = time.Now() }
-func (c *gidCache) IsStale() bool {
- if c != nil && time.Now().Sub(c.Time) < GroupCacheDuration {
- return false
- }
- return true
-}
-
// Retrieves a Group by its GID. Uses the UDP API.
func (adb *AniDB) GroupByID(gid GID) <-chan *Group {
- keys := []cacheKey{"gid", gid}
+ key := []fscache.CacheKey{"gid", gid}
ch := make(chan *Group, 1)
if gid < 1 {
return ch
}
- ic := make(chan Cacheable, 1)
+ ic := make(chan notification, 1)
go func() { ch <- (<-ic).(*Group); close(ch) }()
- if intentMap.Intent(ic, keys...) {
+ if intentMap.Intent(ic, key...) {
return ch
}
- if !cache.CheckValid(keys...) {
- intentMap.NotifyClose((*Group)(nil), keys...)
+ if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
+ intentMap.NotifyClose((*Group)(nil), key...)
return ch
}
g := gid.Group()
if !g.IsStale() {
- intentMap.NotifyClose(g, keys...)
+ intentMap.NotifyClose(g, key...)
return ch
}
if reply.Error() == nil {
g = parseGroupReply(reply)
- cache.Set(&gidCache{GID: g.GID}, "gid", "by-name", g.Name)
- cache.Set(&gidCache{GID: g.GID}, "gid", "by-shortname", g.ShortName)
- cache.Set(g, keys...)
+ cacheGroup(g)
} else if reply.Code() == 350 {
- cache.MarkInvalid(keys...)
- cache.Delete(keys...) // deleted group?
+ Cache.SetInvalid(key...)
}
- intentMap.NotifyClose(g, keys...)
+ intentMap.NotifyClose(g, key...)
}()
return ch
}
// Retrieves a Group by its name. Either full or short names are matched.
// Uses the UDP API.
func (adb *AniDB) GroupByName(gname string) <-chan *Group {
- keys := []cacheKey{"gid", "by-name", gname}
- altKeys := []cacheKey{"gid", "by-shortname", gname}
+ key := []fscache.CacheKey{"gid", "by-name", gname}
+ altKey := []fscache.CacheKey{"gid", "by-shortname", gname}
ch := make(chan *Group, 1)
if gname == "" {
return ch
}
- ic := make(chan Cacheable, 1)
+ ic := make(chan notification, 1)
go func() {
gid := (<-ic).(GID)
if gid > 0 {
}
close(ch)
}()
- if intentMap.Intent(ic, keys...) {
+ if intentMap.Intent(ic, key...) {
return ch
}
- if !cache.CheckValid(keys...) {
- intentMap.NotifyClose(GID(0), keys...)
+ if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
+ intentMap.NotifyClose(GID(0), key...)
return ch
}
gid := GID(0)
- var gc gidCache
- if cache.Get(&gc, keys...) == nil && !gc.IsStale() {
- intentMap.NotifyClose(gc.GID, keys...)
+ switch ts, err := Cache.Get(&gid, key...); {
+ case err == nil && time.Now().Sub(ts) < GroupCacheDuration:
+ intentMap.NotifyClose(gid, key...)
return ch
- }
- gid = gc.GID
-
- if gid == 0 {
- if cache.Get(&gc, altKeys...) == nil && !gc.IsStale() {
- intentMap.NotifyClose(gc.GID, keys...)
+ default:
+ switch ts, err = Cache.Get(&gid, altKey...); {
+ case err == nil && time.Now().Sub(ts) < GroupCacheDuration:
+ intentMap.NotifyClose(gid, key...)
return ch
}
- gid = gc.GID
}
go func() {
gid = g.GID
- cache.Set(&gidCache{GID: gid}, keys...)
- cache.Set(&gidCache{GID: gid}, altKeys...)
- cache.Set(g, "gid", gid)
+ cacheGroup(g)
} else if reply.Code() == 350 {
- cache.MarkInvalid(keys...)
- cache.Delete(keys...) // renamed group?
- cache.Delete(altKeys...)
+ Cache.SetInvalid(key...)
+ Cache.SetInvalid(altKey...)
}
- intentMap.NotifyClose(gid, keys...)
+ intentMap.NotifyClose(gid, key...)
}()
return ch
}
package anidb
-import "sync"
+import (
+ "github.com/Kovensky/go-fscache"
+ "strings"
+ "sync"
+)
+
+type notification interface{}
type intentStruct struct {
sync.Mutex
- chs []chan Cacheable
+ chs []chan notification
}
type intentMapStruct struct {
m: map[string]*intentStruct{},
}
+func intentKey(key ...fscache.CacheKey) string {
+ return strings.Join(fscache.Stringify(key...), "-")
+}
+
// Register a channel to be notified when the specified keys are notified.
// Returns whether the caller was the first to register intent for the given
// keys.
// Cache checks should be done after registering intent, since it's possible to
// register Intent while a Notify is running, and the Notify is done after
// setting the cache.
-func (m *intentMapStruct) Intent(ch chan Cacheable, keys ...cacheKey) bool {
- key := cachePath(keys...)
+func (m *intentMapStruct) Intent(ch chan notification, keys ...fscache.CacheKey) bool {
+ key := intentKey(keys...)
m.intentLock.Lock()
defer m.intentLock.Unlock()
//
// The intentStruct can be directly unlocked, or given to Free to also
// remove it from the intent map.
-func (m *intentMapStruct) LockIntent(keys ...cacheKey) *intentStruct {
+func (m *intentMapStruct) LockIntent(keys ...fscache.CacheKey) *intentStruct {
m.Lock()
defer m.Unlock()
return m._lockIntent(keys...)
}
-func (m *intentMapStruct) _lockIntent(keys ...cacheKey) *intentStruct {
- s, ok := m.m[cachePath(keys...)]
+func (m *intentMapStruct) _lockIntent(keys ...fscache.CacheKey) *intentStruct {
+ s, ok := m.m[intentKey(keys...)]
if !ok {
return nil
}
}
// Removes the given intent from the intent map and unlocks the intentStruct.
-func (m *intentMapStruct) Free(is *intentStruct, keys ...cacheKey) {
+func (m *intentMapStruct) Free(is *intentStruct, keys ...fscache.CacheKey) {
m.Lock()
defer m.Unlock()
m._free(is, keys...)
}
-func (m *intentMapStruct) _free(is *intentStruct, keys ...cacheKey) {
+func (m *intentMapStruct) _free(is *intentStruct, keys ...fscache.CacheKey) {
// deletes the key before unlocking, Intent needs to recheck key status
- delete(m.m, cachePath(keys...))
+ delete(m.m, intentKey(keys...))
// better than unlocking then deleting -- could delete a "brand new" entry
is.Unlock()
}
// also removes them from the intent map.
//
// Should be called after setting the cache.
-func (m *intentMapStruct) NotifyClose(v Cacheable, keys ...cacheKey) {
+func (m *intentMapStruct) NotifyClose(v notification, keys ...fscache.CacheKey) {
m.Lock()
defer m.Unlock()
// Closes all channels that are listening for the specified keys
// and removes them from the intent map.
-func (m *intentMapStruct) Close(keys ...cacheKey) {
+func (m *intentMapStruct) Close(keys ...fscache.CacheKey) {
m.Lock()
defer m.Unlock()
// Notifies all channels that are listening for the specified keys,
// but doesn't close or remove them from the intent map.
-func (m *intentMapStruct) Notify(v Cacheable, keys ...cacheKey) {
+func (m *intentMapStruct) Notify(v notification, keys ...fscache.CacheKey) {
m.Lock()
defer m.Unlock()
}
// NOTE: does not lock the stuct
-func (s *intentStruct) Notify(v Cacheable) {
+func (s *intentStruct) Notify(v notification) {
for _, ch := range s.chs {
ch <- v
}
}
// NOTE: does not lock the struct
-func (s *intentStruct) NotifyClose(v Cacheable) {
+func (s *intentStruct) NotifyClose(v notification) {
for _, ch := range s.chs {
ch <- v
close(ch)
"io"
"log"
"net/http"
+ "os"
"time"
)
// Loads the database from anime-titles.dat.gz in the cache dir.
func RefreshTitles() error {
- if flock := lockFile(cachePath("anime-titles.dat.gz")); flock != nil {
- flock.Lock()
- defer flock.Unlock()
+ if lock, err := Cache.Lock("anime-titles.dat.gz"); err != nil {
+ return err
+ } else {
+ defer lock.Unlock()
}
- fh, err := cache.Open("anime-titles.dat.gz")
+ fh, err := Cache.Open("anime-titles.dat.gz")
if err != nil {
return err
}
return nil
}
- if flock := lockFile(cachePath("anime-titles.dat.gz")); flock != nil {
- flock.Lock()
- defer flock.Unlock()
+ switch lock, err := Cache.Lock("anime-titles.dat.gz"); {
+ case os.IsNotExist(err):
+ // we're creating it now
+ case err == nil:
+ defer lock.Unlock()
+ default:
+ return err
}
c := &http.Client{Transport: &http.Transport{DisableCompression: true}}
return err
}
- fh, err := cache.Create("anime-titles.dat.gz")
+ fh, err := Cache.Create("anime-titles.dat.gz")
if err != nil {
return err
}
package anidb
import (
- "encoding/gob"
"github.com/Kovensky/go-anidb/udp"
"log"
"sync"
"time"
)
-func init() {
- gob.RegisterName("*github.com/Kovensky/go-anidb.banCache", &banCache{})
-}
-
const banDuration = 30*time.Minute + 1*time.Second
-type banCache struct{ time.Time }
-
-func (c *banCache) Touch() {
- c.Time = time.Now()
-}
-func (c *banCache) IsStale() bool {
- return time.Now().Sub(c.Time) > banDuration
-}
-
// Returns whether the last UDP API access returned a 555 BANNED message.
func Banned() bool {
- var banTime banCache
- cache.Get(&banTime, "banned")
+ stat, err := Cache.Stat("banned")
+ if err != nil {
+ return false
+ }
- stale := banTime.IsStale()
- if stale {
- cache.Delete("banned")
+ switch ts := stat.ModTime(); {
+ case ts.IsZero():
+ return false
+ case time.Now().Sub(ts) > banDuration:
+ return false
+ default:
+ return true
}
- return !stale
}
func setBanned() {
- cache.Set(&banCache{}, "banned")
+ Cache.Touch("banned")
}
type paramSet struct {