github.com/quantumghost/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  }