github.com/ChicK00o/awgo@v0.29.4/cache.go (about) 1 // Copyright (c) 2018 Dean Jackson <deanishe@deanishe.net> 2 // MIT Licence - http://opensource.org/licenses/MIT 3 4 package aw 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io/ioutil" 10 "log" 11 "math/rand" 12 "os" 13 "path/filepath" 14 "strings" 15 "time" 16 17 "github.com/ChicK00o/awgo/util" 18 ) 19 20 var ( 21 // Filenames of session cache files are prefixed with this string 22 sessionPrefix = "_aw_session" 23 sidLength = 24 24 letters = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 25 ) 26 27 func init() { 28 rand.Seed(time.Now().UnixNano()) 29 } 30 31 // Cache implements a simple store/load API, saving data to specified directory. 32 // 33 // There are two APIs, one for storing/loading bytes and one for 34 // marshalling and storing/loading and unmarshalling JSON. 35 // 36 // Each API has basic Store/Load functions plus a LoadOrStore function which 37 // loads cached data if these exist and aren't too old, or retrieves new data 38 // via the provided function, then caches and returns these. 39 // 40 // The `name` parameter passed to Load*/Store* methods is used as the filename 41 // for the on-disk cache, so make sure it's filesystem-safe, and consider 42 // adding an appropriate extension to the name, e.g. use "name.txt" (or 43 // "name.json" with LoadOrStoreJSON). 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 cache 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 util.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("marshal JSON: %w", 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 named 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 fmt.Errorf("read file: %w", err) 99 } 100 return json.Unmarshal(data, v) 101 } 102 103 // LoadOrStore loads data from cache if they exist and are newer than maxAge. 104 // If data do not exist or are older than maxAge, the reload function is 105 // called, and the returned data are saved to the cache and also 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("reload data: %w", 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, the 132 // reload function is called, and the data it returns are marshalled to JSON & 133 // cached, and also 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("reload data: %w", err) 153 } 154 data, err = json.MarshalIndent(i, "", " ") 155 if err != nil { 156 return fmt.Errorf("marshal data to JSON: %w", 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("load cached data: %w", 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 0, err 189 } 190 return time.Since(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("read directory (%s): %w", 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 func (s Session) LoadOrStore(name string, reload func() ([]byte, error)) ([]byte, error) { 283 return s.cache.LoadOrStore(s.name(name), 0, reload) 284 } 285 286 // LoadOrStoreJSON loads JSON-serialised data from cache if they exist. 287 // If the data do not exist, reload is called, and the resulting interface{} 288 // is cached and returned. 289 func (s Session) LoadOrStoreJSON(name string, reload func() (interface{}, error), v interface{}) error { 290 return s.cache.LoadOrStoreJSON(s.name(name), 0, reload, v) 291 } 292 293 // Exists returns true if the named cache exists. 294 func (s Session) Exists(name string) bool { 295 return s.cache.Exists(s.name(name)) 296 } 297 298 // name prefixes name with session prefix and session ID. 299 func (s Session) name(name string) string { 300 return fmt.Sprintf("%s.%s.%s", sessionPrefix, s.SessionID, name) 301 }