github.com/clysto/awgo@v0.15.0/cache.go (about) 1 // 2 // Copyright (c) 2017 Dean Jackson <deanishe@deanishe.net> 3 // 4 // MIT Licence. See http://opensource.org/licenses/MIT 5 // 6 // Created on 2017-08-08 7 // 8 9 package aw 10 11 import ( 12 "encoding/json" 13 "fmt" 14 "io/ioutil" 15 "log" 16 "math/rand" 17 "os" 18 "path/filepath" 19 "strings" 20 "time" 21 22 "github.com/deanishe/awgo/util" 23 ) 24 25 var ( 26 // Filenames of session cache files are prefixed with this string 27 sessionPrefix = "_aw_session" 28 sidLength = 24 29 letters = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 30 ) 31 32 func init() { 33 rand.Seed(time.Now().UnixNano()) 34 } 35 36 // Cache implements a simple store/load API, saving data to specified directory. 37 // 38 // There are two APIs, one for storing/loading bytes and one for 39 // marshalling and storing/loading and unmarshalling JSON. 40 // 41 // Each API has basic Store/Load functions plus a LoadOrStore function which 42 // loads cached data if these exist and aren't too old, or retrieves new data 43 // via the provided function, then caches and returns these. 44 type Cache struct { 45 Dir string // Directory to save data in 46 } 47 48 // NewCache creates a new Cache using given directory. 49 // Directory is created if it doesn't exist. Panics if directory can't be created. 50 func NewCache(dir string) *Cache { 51 util.MustExist(dir) 52 return &Cache{dir} 53 } 54 55 // Store saves data under the given name. If data is nil, the file is deleted. 56 func (c Cache) Store(name string, data []byte) error { 57 p := c.path(name) 58 if data == nil { 59 if util.PathExists(p) { 60 return os.Remove(p) 61 } 62 return nil 63 } 64 return ioutil.WriteFile(p, data, 0600) 65 } 66 67 // StoreJSON serialises v to JSON and saves it to the cache. If v is nil, 68 // the cache is deleted. 69 func (c Cache) StoreJSON(name string, v interface{}) error { 70 p := c.path(name) 71 if v == nil { 72 if util.PathExists(p) { 73 return os.Remove(p) 74 } 75 return nil 76 } 77 data, err := json.MarshalIndent(v, "", " ") 78 if err != nil { 79 return fmt.Errorf("couldn't marshal JSON: %v", err) 80 } 81 return c.Store(name, data) 82 } 83 84 // Load reads data saved under given name. 85 func (c Cache) Load(name string) ([]byte, error) { 86 p := c.path(name) 87 if _, err := os.Stat(p); err != nil { 88 return nil, err 89 } 90 return ioutil.ReadFile(p) 91 } 92 93 // LoadJSON unmarshals a cache into v. 94 func (c Cache) LoadJSON(name string, v interface{}) error { 95 p := c.path(name) 96 data, err := ioutil.ReadFile(p) 97 if err != nil { 98 return err 99 } 100 return json.Unmarshal(data, v) 101 } 102 103 // LoadOrStore loads data from cache if they exist and are newer than maxAge. If 104 // data do not exist or are older than maxAge, reload is called, and the returned 105 // data are cached & returned. 106 // 107 // If maxAge is 0, any cached data are always returned. 108 func (c Cache) LoadOrStore(name string, maxAge time.Duration, reload func() ([]byte, error)) ([]byte, error) { 109 var load bool 110 age, err := c.Age(name) 111 if err != nil { 112 load = true 113 } else if maxAge > 0 && age > maxAge { 114 load = true 115 } 116 // log.Printf("age=%v, maxAge=%v, load=%v", age, maxAge, load) 117 if load { 118 data, err := reload() 119 if err != nil { 120 return nil, fmt.Errorf("couldn't reload data: %v", err) 121 } 122 if err := c.Store(name, data); err != nil { 123 return nil, err 124 } 125 return data, nil 126 } 127 return c.Load(name) 128 } 129 130 // LoadOrStoreJSON loads JSON-serialised data from cache if they exist and are 131 // newer than maxAge. If the data do not exist or are older than maxAge, reload 132 // is called, and the returned data are marshalled to JSON and cached, and 133 // unmarshalled into v. 134 // 135 // If maxAge is 0, any cached data are loaded regardless of age. 136 func (c Cache) LoadOrStoreJSON(name string, maxAge time.Duration, reload func() (interface{}, error), v interface{}) error { 137 var ( 138 load bool 139 data []byte 140 err error 141 ) 142 age, err := c.Age(name) 143 if err != nil { 144 load = true 145 } else if maxAge > 0 && age > maxAge { 146 load = true 147 } 148 149 if load { 150 i, err := reload() 151 if err != nil { 152 return fmt.Errorf("couldn't reload data: %v", err) 153 } 154 data, err = json.MarshalIndent(i, "", " ") 155 if err != nil { 156 return fmt.Errorf("couldn't marshal data to JSON: %v", err) 157 } 158 if err := c.Store(name, data); err != nil { 159 return err 160 } 161 } else { 162 data, err = c.Load(name) 163 if err != nil { 164 return fmt.Errorf("couldn't load cached data: %v", err) 165 } 166 } 167 // TODO: Is there any way to directly return i without marshalling and unmarshalling it? 168 return json.Unmarshal(data, v) 169 } 170 171 // Exists returns true if the named cache exists. 172 func (c Cache) Exists(name string) bool { return util.PathExists(c.path(name)) } 173 174 // Expired returns true if the named cache does not exist or is older than maxAge. 175 func (c Cache) Expired(name string, maxAge time.Duration) bool { 176 age, err := c.Age(name) 177 if err != nil { 178 return true 179 } 180 return age > maxAge 181 } 182 183 // Age returns the age of the data cached at name. 184 func (c Cache) Age(name string) (time.Duration, error) { 185 p := c.path(name) 186 fi, err := os.Stat(p) 187 if err != nil { 188 return time.Duration(0), err 189 } 190 return time.Now().Sub(fi.ModTime()), nil 191 } 192 193 // path returns the path to a named file within cache directory. 194 func (c Cache) path(name string) string { return filepath.Join(c.Dir, name) } 195 196 // Session is a Cache that is tied to the `sessionID` value passed to NewSession(). 197 // 198 // All cached data are stored under the sessionID. NewSessionID() creates 199 // a pseudo-random string based on the current UNIX time (in nanoseconds). 200 // The Workflow struct persists this value as a session ID as long as the 201 // user is using the current workflow via the `AW_SESSION_ID` top-level 202 // workflow variable. 203 // 204 // As soon as Alfred closes or the user calls another workflow, this variable 205 // is lost and the data are "hidden". Session.Clear(false) must be called to 206 // actually remove the data from the cache directory, which Workflow.Run() does. 207 // 208 // In contrast to the Cache API, Session methods lack an explicit `maxAge` 209 // parameter. It is always `0`, i.e. cached data are always loaded regardless 210 // of age as long as the session is valid. 211 // 212 // TODO: Embed Cache rather than wrapping it. 213 type Session struct { 214 SessionID string 215 cache *Cache 216 } 217 218 // NewSession creates and initialises a Session. 219 func NewSession(dir, sessionID string) *Session { 220 s := &Session{sessionID, NewCache(dir)} 221 return s 222 } 223 224 // NewSessionID returns a pseudo-random string based on the current UNIX time 225 // in nanoseconds. 226 func NewSessionID() string { 227 b := make([]rune, sidLength) 228 for i := range b { 229 b[i] = letters[rand.Intn(len(letters))] 230 } 231 return string(b) 232 } 233 234 // Clear removes session-scoped cache data. If current is true, it also removes 235 // data cached for the current session. 236 func (s Session) Clear(current bool) error { 237 prefix := sessionPrefix + "." 238 curPrefix := fmt.Sprintf("%s.%s.", sessionPrefix, s.SessionID) 239 240 files, err := ioutil.ReadDir(s.cache.Dir) 241 if err != nil { 242 return fmt.Errorf("couldn't read directory (%s): %v", s.cache.Dir, err) 243 } 244 for _, fi := range files { 245 if !strings.HasPrefix(fi.Name(), prefix) { 246 continue 247 } 248 if !current && strings.HasPrefix(fi.Name(), curPrefix) { 249 continue 250 } 251 p := filepath.Join(s.cache.Dir, fi.Name()) 252 os.RemoveAll(p) 253 log.Printf("deleted %s", p) 254 } 255 return nil 256 } 257 258 // Store saves data under the given name. If len(data) is 0, the file is 259 // deleted. 260 func (s Session) Store(name string, data []byte) error { 261 return s.cache.Store(s.name(name), data) 262 } 263 264 // StoreJSON serialises v to JSON and saves it to the cache. If v is nil, 265 // the cache is deleted. 266 func (s Session) StoreJSON(name string, v interface{}) error { 267 return s.cache.StoreJSON(s.name(name), v) 268 } 269 270 // Load reads data saved under given name. 271 func (s Session) Load(name string) ([]byte, error) { 272 return s.cache.Load(s.name(name)) 273 } 274 275 // LoadJSON unmarshals a cache into v. 276 func (s Session) LoadJSON(name string, v interface{}) error { 277 return s.cache.LoadJSON(s.name(name), v) 278 } 279 280 // LoadOrStore loads data from cache if they exist. If data do not exist, 281 // reload is called, and the resulting data are cached & returned. 282 // 283 // If maxAge is 0, any cached data are always returned. 284 func (s Session) LoadOrStore(name string, reload func() ([]byte, error)) ([]byte, error) { 285 return s.cache.LoadOrStore(s.name(name), 0, reload) 286 } 287 288 // LoadOrStoreJSON loads JSON-serialised data from cache if they exist. 289 // If the data do not exist, reload is called, and the resulting interface{} 290 // is cached and returned. 291 func (s Session) LoadOrStoreJSON(name string, reload func() (interface{}, error), v interface{}) error { 292 return s.cache.LoadOrStoreJSON(s.name(name), 0, reload, v) 293 } 294 295 // Exists returns true if the named cache exists. 296 func (s Session) Exists(name string) bool { 297 return s.cache.Exists(s.name(name)) 298 } 299 300 // name prefixes name with session prefix and session ID. 301 func (s Session) name(name string) string { 302 return fmt.Sprintf("%s.%s.%s", sessionPrefix, s.SessionID, name) 303 }