]> git.lizzy.rs Git - go-anidb.git/blob - cache.go
anidb: New caching mechanism
[go-anidb.git] / cache.go
1 package anidb
2
3 import (
4         "bytes"
5         "compress/gzip"
6         "encoding/gob"
7         "errors"
8         "fmt"
9         "io"
10         "log"
11         "os"
12         "path"
13         "reflect"
14         "regexp"
15         "sync"
16         "time"
17 )
18
19 var _ log.Logger
20
21 type Cacheable interface {
22         // Updates the last modified time
23         Touch()
24         // Returns true if the Cacheable is nil, or if the last modified time is too old.
25         IsStale() bool
26 }
27
28 func init() {
29         gob.RegisterName("*github.com/Kovensky/go-anidb.invalidKeyCache", &invalidKeyCache{})
30 }
31
32 type invalidKeyCache struct{ time.Time }
33
34 func (c *invalidKeyCache) Touch() {
35         c.Time = time.Now()
36 }
37 func (c *invalidKeyCache) IsStale() bool {
38         return time.Now().Sub(c.Time) > InvalidKeyCacheDuration
39 }
40
41 type cacheDir struct {
42         *sync.RWMutex
43
44         CacheDir string
45 }
46
47 func init() {
48         SetCacheDir(path.Join(os.TempDir(), "anidb", "cache"))
49 }
50
51 var cache cacheDir
52
53 func SetCacheDir(path string) (err error) {
54         m := cache.RWMutex
55         if m == nil {
56                 m = &sync.RWMutex{}
57                 cache.RWMutex = m
58         }
59         cache.Lock()
60
61         if err = os.MkdirAll(path, 0755|os.ModeDir); err != nil {
62                 cache.Unlock()
63                 return err
64         }
65
66         cache = cacheDir{
67                 RWMutex:  m,
68                 CacheDir: path,
69         }
70
71         cache.Unlock()
72         RefreshTitles()
73         return nil
74 }
75
76 func GetCacheDir() (path string) {
77         cache.RLock()
78         defer cache.RUnlock()
79
80         return cache.CacheDir
81 }
82
83 type cacheKey interface{}
84
85 // All "bad characters" that can't go in Windows paths.
86 // It's a superset of the "bad characters" on other OSes, so this works.
87 var badPath = regexp.MustCompile(`[\\/:\*\?\"<>\|]`)
88
89 func stringify(stuff ...cacheKey) []string {
90         ret := make([]string, len(stuff))
91         for i := range stuff {
92                 s := fmt.Sprint(stuff[i])
93                 ret[i] = badPath.ReplaceAllLiteralString(s, "_")
94         }
95         return ret
96 }
97
98 // Each key but the last is treated as a directory.
99 // The last key is treated as a regular file.
100 //
101 // This also means that cache keys that are file-backed
102 // cannot have subkeys.
103 func cachePath(keys ...cacheKey) string {
104         parts := append([]string{GetCacheDir()}, stringify(keys...)...)
105         p := path.Join(parts...)
106         return p
107 }
108
109 // Opens the file that backs the specified keys.
110 func (c *cacheDir) Open(keys ...cacheKey) (fh *os.File, err error) {
111         subItem := cachePath(keys...)
112         return os.Open(subItem)
113 }
114
115 // Creates a new file to back the specified keys.
116 func (c *cacheDir) Create(keys ...cacheKey) (fh *os.File, err error) {
117         subItem := cachePath(keys...)
118         subDir := path.Dir(subItem)
119
120         if err = os.MkdirAll(subDir, 0755|os.ModeDir); err != nil {
121                 return nil, err
122         }
123         return os.Create(subItem)
124 }
125
126 // Deletes the file that backs the specified keys.
127 func (c *cacheDir) Delete(keys ...cacheKey) (err error) {
128         return os.Remove(cachePath(keys...))
129 }
130
131 // Deletes the specified key and all subkeys.
132 func (c *cacheDir) DeleteAll(keys ...cacheKey) (err error) {
133         return os.RemoveAll(cachePath(keys...))
134 }
135
136 func (c *cacheDir) Get(v Cacheable, keys ...cacheKey) (err error) {
137         defer func() {
138                 log.Println("Got entry", keys, "(error", err, ")")
139         }()
140         flock := lockFile(cachePath(keys...))
141         flock.Lock()
142         defer flock.Unlock()
143
144         fh, err := c.Open(keys...)
145         if err != nil {
146                 return err
147         }
148         defer func() {
149                 if e := fh.Close(); err == nil {
150                         err = e
151                 }
152         }()
153
154         val := reflect.ValueOf(v)
155         if k := val.Kind(); k == reflect.Ptr || k == reflect.Interface {
156                 val = val.Elem()
157         }
158         if !val.CanSet() {
159                 // panic because this is an internal coding mistake
160                 panic("(*cacheDir).Get(): given Cacheable is not setable")
161         }
162         gz, err := gzip.NewReader(fh)
163         if err != nil {
164                 return err
165         }
166         defer func() {
167                 if e := gz.Close(); err == nil {
168                         err = e
169                 }
170         }()
171
172         // defer func() {
173         //      if err == io.EOF {
174         //              err = nil
175         //      }
176         // }()
177
178         switch f := gz.Header.Comment; f {
179         case "encoding/gob":
180                 dec := gob.NewDecoder(gz)
181                 err = dec.Decode(v)
182         default:
183                 return errors.New(fmt.Sprintf("Cached data (format %q) is not in a known format", f))
184         }
185
186         return
187 }
188
189 func (c *cacheDir) Set(v Cacheable, keys ...cacheKey) (n int64, err error) {
190         if v := reflect.ValueOf(v); !v.IsValid() {
191                 panic("reflect.ValueOf() returned invaled value")
192         } else if k := v.Kind(); k == reflect.Ptr || k == reflect.Interface {
193                 if v.IsNil() {
194                         return // no point in saving nil
195                 }
196         }
197         defer func() {
198                 log.Println("Set entry", keys, "(error", err, ")")
199         }()
200
201         // First we encode to memory -- we don't want to create/truncate a file and put bad data in it.
202         buf := bytes.Buffer{}
203         gz, err := gzip.NewWriterLevel(&buf, gzip.BestCompression)
204         if err != nil {
205                 return 0, err
206         }
207         gz.Header.Comment = "encoding/gob"
208
209         // it doesn't matter if the caller doesn't see this,
210         // the important part is that the cache does.
211         v.Touch()
212
213         enc := gob.NewEncoder(gz)
214         err = enc.Encode(v)
215
216         if e := gz.Close(); err == nil {
217                 err = e
218         }
219
220         if err != nil {
221                 return 0, err
222         }
223
224         // We have good data, time to actually put it in the cache
225         flock := lockFile(cachePath(keys...))
226         flock.Lock()
227         defer flock.Unlock()
228
229         fh, err := c.Create(keys...)
230         if err != nil {
231                 return 0, err
232         }
233         defer func() {
234                 if e := fh.Close(); err == nil {
235                         err = e
236                 }
237         }()
238         n, err = io.Copy(fh, &buf)
239         return
240 }
241
242 // Checks if the given keys are not marked as invalid.
243 //
244 // If the key was marked as invalid but is no longer considered
245 // so, deletes the invalid marker.
246 func (c *cacheDir) CheckValid(keys ...cacheKey) bool {
247         invKeys := append([]cacheKey{"invalid"}, keys...)
248         inv := invalidKeyCache{}
249
250         if cache.Get(&inv, invKeys...) == nil {
251                 if inv.IsStale() {
252                         cache.Delete(invKeys...)
253                 } else {
254                         return false
255                 }
256         }
257         return true
258 }
259
260 // Deletes the given keys and marks them as invalid.
261 //
262 // They are considered invalid for InvalidKeyCacheDuration.
263 func (c *cacheDir) MarkInvalid(keys ...cacheKey) error {
264         invKeys := append([]cacheKey{"invalid"}, keys...)
265
266         cache.Delete(keys...)
267         _, err := cache.Set(&invalidKeyCache{}, invKeys...)
268         return err
269 }