├── .gitignore ├── LICENSE ├── README.md ├── cache.go ├── inmemory.go ├── inmemory_test.go └── levedb.go /.gitignore: -------------------------------------------------------------------------------- 1 | src/ 2 | pkg/ 3 | bin/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Gin-cache tiny and simple cache middleware for gin framework. 2 | Copyright (C) 2015 Oleg Lebedev 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 20 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gin-cache 2 | Tiny and simple cache middleware for gin framework 3 | 4 | ## Usage 5 | 6 | ```go 7 | package main 8 | 9 | import ( 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/olebedev/gin-cache" 14 | ) 15 | 16 | func main() { 17 | r := gin.New() 18 | 19 | r.Use(cache.New(cache.Options{ 20 | // set expire duration 21 | // by default zero, it means that cached content won't drop 22 | Expire: 5 * time.Minute, 23 | 24 | // store interface, see cache.go 25 | // by default it uses cache.InMemory 26 | Store: func() *cache.LevelDB { 27 | store, err := cache.NewLevelDB("cache") 28 | panicIf(err) 29 | return store 30 | }(), 31 | 32 | // it uses slice listed below as default to calculate 33 | // key, if `Header` slice is not specified 34 | Header: []string{ 35 | "User-Agent", 36 | "Accept", 37 | "Accept-Encoding", 38 | "Accept-Language", 39 | "Cookie", 40 | "User-Agent", 41 | }, 42 | 43 | // *gin.Context.Abort() will be invoked immediately after cache has been served 44 | // so, you can change this, but you should manage c.Writer.Written() flag by self 45 | // example: 46 | // func config(c *gin.Context) { 47 | // if c.Writer.Written() { 48 | // return 49 | // } 50 | // // else serve content 51 | // ... 52 | // } 53 | DoNotUseAbort: false, 54 | })) 55 | 56 | r.Run(":3000") 57 | } 58 | ``` 59 | 60 | 61 | ### TODO 62 | - [x] inmemory store 63 | - [x] leveldb store 64 | - [ ] cache_test.go 65 | - [ ] leveldb_test.go 66 | - [ ] redis store 67 | - [ ] memcache store 68 | - [ ] add CI tool 69 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/gob" 7 | "encoding/hex" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "strings" 13 | "time" 14 | 15 | "github.com/gin-gonic/gin" 16 | ) 17 | 18 | const KEY_PREFIX = "gin:cache:" 19 | 20 | var ( 21 | ErrNotFound = errors.New("not found") 22 | ErrAlreadyExists = errors.New("already exists") 23 | ) 24 | 25 | type Cached struct { 26 | Status int 27 | Body []byte 28 | Header http.Header 29 | ExpireAt time.Time 30 | } 31 | 32 | type Store interface { 33 | Get(string) ([]byte, error) 34 | Set(string, []byte) error 35 | Remove(string) error 36 | Update(string, []byte) error 37 | Keys() []string 38 | } 39 | 40 | type Options struct { 41 | Store Store 42 | Expire time.Duration 43 | Headers []string 44 | DoNotUseAbort bool 45 | } 46 | 47 | func (o *Options) init() { 48 | if o.Headers == nil { 49 | o.Headers = []string{ 50 | "User-Agent", 51 | "Accept", 52 | "Accept-Encoding", 53 | "Accept-Language", 54 | "Cookie", 55 | "User-Agent", 56 | } 57 | } 58 | } 59 | 60 | type Cache struct { 61 | Store 62 | options Options 63 | expires map[string]time.Time 64 | } 65 | 66 | func (c *Cache) Get(key string) (*Cached, error) { 67 | 68 | if data, err := c.Store.Get(key); err == nil { 69 | var cch *Cached 70 | dec := gob.NewDecoder(bytes.NewBuffer(data)) 71 | dec.Decode(&cch) 72 | 73 | if cch.ExpireAt.Nanosecond() != 0 && cch.ExpireAt.Before(time.Now()) { 74 | c.Store.Remove(key) 75 | return nil, nil 76 | } 77 | 78 | return cch, nil 79 | } else { 80 | return nil, err 81 | } 82 | 83 | return nil, ErrNotFound 84 | } 85 | 86 | func (c *Cache) Set(key string, cch *Cached) error { 87 | var b bytes.Buffer 88 | enc := gob.NewEncoder(&b) 89 | 90 | panicIf(enc.Encode(*cch)) 91 | return c.Store.Set(key, b.Bytes()) 92 | } 93 | 94 | func (c *Cache) Update(key string, cch *Cached) error { 95 | var b bytes.Buffer 96 | enc := gob.NewEncoder(&b) 97 | 98 | panicIf(enc.Encode(*cch)) 99 | 100 | return c.Store.Update(key, b.Bytes()) 101 | } 102 | 103 | type wrappedWriter struct { 104 | gin.ResponseWriter 105 | body bytes.Buffer 106 | } 107 | 108 | func (rw *wrappedWriter) Write(body []byte) (int, error) { 109 | n, err := rw.ResponseWriter.Write(body) 110 | if err == nil { 111 | rw.body.Write(body) 112 | } 113 | return n, err 114 | } 115 | 116 | func New(o ...Options) gin.HandlerFunc { 117 | opts := Options{ 118 | Store: NewInMemory(), 119 | Expire: 0, 120 | } 121 | 122 | for _, i := range o { 123 | opts = i 124 | break 125 | } 126 | opts.init() 127 | 128 | cache := Cache{ 129 | Store: opts.Store, 130 | options: opts, 131 | expires: make(map[string]time.Time), 132 | } 133 | 134 | return func(c *gin.Context) { 135 | 136 | // only GET method available for caching 137 | if c.Request.Method != "GET" { 138 | c.Next() 139 | return 140 | } 141 | 142 | tohash := c.Request.URL.RequestURI() 143 | for _, k := range cache.options.Headers { 144 | if v, ok := c.Request.Header[k]; ok { 145 | tohash += k 146 | tohash += strings.Join(v, "") 147 | } 148 | } 149 | 150 | key := KEY_PREFIX + md5String(tohash) 151 | 152 | if cch, _ := cache.Get(key); cch == nil { 153 | // cache miss 154 | writer := c.Writer 155 | rw := wrappedWriter{ResponseWriter: c.Writer} 156 | c.Writer = &rw 157 | c.Next() 158 | c.Writer = writer 159 | 160 | cache.Set(key, &Cached{ 161 | Status: rw.Status(), 162 | Body: rw.body.Bytes(), 163 | Header: http.Header(rw.Header()), 164 | ExpireAt: func() time.Time { 165 | if cache.options.Expire == 0 { 166 | return time.Time{} 167 | } else { 168 | return time.Now().Add(cache.options.Expire) 169 | } 170 | }(), 171 | }) 172 | 173 | } else { 174 | // cache found 175 | start := time.Now() 176 | c.Writer.WriteHeader(cch.Status) 177 | for k, val := range cch.Header { 178 | for _, v := range val { 179 | c.Writer.Header().Add(k, v) 180 | } 181 | } 182 | c.Writer.Header().Add("X-Gin-Cache", fmt.Sprintf("%f ms", time.Now().Sub(start).Seconds()*1000)) 183 | c.Writer.Write(cch.Body) 184 | 185 | if !cache.options.DoNotUseAbort { 186 | c.Abort() 187 | } 188 | } 189 | } 190 | } 191 | 192 | func md5String(url string) string { 193 | h := md5.New() 194 | io.WriteString(h, url) 195 | return hex.EncodeToString(h.Sum(nil)) 196 | } 197 | 198 | func init() { 199 | gob.Register(Cached{}) 200 | } 201 | 202 | func panicIf(err error) { 203 | if err != nil { 204 | panic(err) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /inmemory.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "sync" 4 | 5 | type InMemory struct { 6 | hash map[string][]byte 7 | mu sync.RWMutex 8 | } 9 | 10 | func NewInMemory() *InMemory { 11 | return &InMemory{hash: make(map[string][]byte)} 12 | } 13 | 14 | func (im *InMemory) Get(key string) ([]byte, error) { 15 | im.mu.RLock() 16 | defer im.mu.RUnlock() 17 | 18 | if v, found := im.hash[key]; found { 19 | return v, nil 20 | } 21 | 22 | return []byte{}, ErrNotFound 23 | } 24 | 25 | func (im *InMemory) Set(key string, value []byte) error { 26 | im.mu.Lock() 27 | defer im.mu.Unlock() 28 | 29 | if _, found := im.hash[key]; found { 30 | return ErrAlreadyExists 31 | } 32 | 33 | im.hash[key] = value 34 | return nil 35 | } 36 | 37 | func (im *InMemory) Remove(key string) error { 38 | im.mu.Lock() 39 | defer im.mu.Unlock() 40 | 41 | if _, found := im.hash[key]; found { 42 | delete(im.hash, key) 43 | return nil 44 | } 45 | 46 | return ErrNotFound 47 | } 48 | 49 | func (im *InMemory) Update(key string, value []byte) error { 50 | im.mu.Lock() 51 | defer im.mu.Unlock() 52 | 53 | if _, found := im.hash[key]; found { 54 | im.hash[key] = value 55 | return nil 56 | } 57 | 58 | return ErrNotFound 59 | } 60 | 61 | func (im *InMemory) Keys() []string { 62 | im.mu.RLock() 63 | defer im.mu.RUnlock() 64 | 65 | cumul := []string{} 66 | for k, _ := range im.hash { 67 | cumul = append(cumul, k) 68 | } 69 | return cumul 70 | } 71 | -------------------------------------------------------------------------------- /inmemory_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestGet(t *testing.T) { 9 | im := InMemory{ 10 | hash: map[string][]byte{ 11 | "key": []byte{1, 2, 3}, 12 | }, 13 | } 14 | // im.Set("key", []byte{1, 2, 3}) 15 | res, err := im.Get("key") 16 | expect(t, err, nil) 17 | expect(t, len(res), 3) 18 | 19 | _, err = im.Get("__key__") 20 | expect(t, err, ErrNotFound) 21 | } 22 | 23 | func TestIMSet(t *testing.T) { 24 | im := InMemory{hash: map[string][]byte{}} 25 | 26 | err := im.Set("key", []byte{1, 2, 3}) 27 | expect(t, err, nil) 28 | 29 | res, ok := im.hash["key"] 30 | expect(t, ok, true) 31 | expect(t, len(res), 3) 32 | 33 | err = im.Set("key", []byte{1, 2, 3}) 34 | expect(t, err, ErrAlreadyExists) 35 | } 36 | 37 | func TestIMRemove(t *testing.T) { 38 | im := InMemory{ 39 | hash: map[string][]byte{ 40 | "key": []byte{1, 2, 3}, 41 | }, 42 | } 43 | 44 | err := im.Remove("key") 45 | expect(t, err, nil) 46 | 47 | err = im.Remove("__key__") 48 | expect(t, err, ErrNotFound) 49 | 50 | expect(t, len(im.hash), 0) 51 | } 52 | 53 | // update 54 | func TestIMUpdate(t *testing.T) { 55 | im := InMemory{ 56 | hash: map[string][]byte{ 57 | "key": []byte{1, 2, 3}, 58 | }, 59 | } 60 | 61 | err := im.Update("key", []byte{1}) 62 | expect(t, err, nil) 63 | expect(t, len(im.hash["key"]), 1) 64 | 65 | err = im.Update("__key__", []byte{1}) 66 | expect(t, err, ErrNotFound) 67 | } 68 | 69 | // keys 70 | func TestIMKeys(t *testing.T) { 71 | im := InMemory{ 72 | hash: map[string][]byte{ 73 | "key": []byte{1, 2, 3}, 74 | }, 75 | } 76 | 77 | keys := im.Keys() 78 | expect(t, len(keys), 1) 79 | expect(t, keys[0], "key") 80 | 81 | im.hash["__key__"] = []byte{} 82 | 83 | keys = im.Keys() 84 | expect(t, len(keys), 2) 85 | expect(t, keys[0], "key") 86 | expect(t, keys[1], "__key__") 87 | } 88 | 89 | func expect(t *testing.T, a interface{}, b interface{}) { 90 | if a != b { 91 | t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /levedb.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "github.com/syndtr/goleveldb/leveldb" 5 | "github.com/syndtr/goleveldb/leveldb/util" 6 | ) 7 | 8 | type LevelDB struct { 9 | db *leveldb.DB 10 | } 11 | 12 | func NewLevelDB(path string) (*LevelDB, error) { 13 | db, err := leveldb.OpenFile(path, nil) 14 | ldb := LevelDB{ 15 | db: db, 16 | } 17 | return &ldb, err 18 | } 19 | 20 | func (ldb *LevelDB) Get(key string) ([]byte, error) { 21 | return ldb.db.Get([]byte(key), nil) 22 | } 23 | 24 | func (ldb *LevelDB) Set(key string, value []byte) error { 25 | return ldb.db.Put([]byte(key), value, nil) 26 | } 27 | 28 | func (ldb *LevelDB) Remove(key string) error { 29 | return ldb.db.Delete([]byte(key), nil) 30 | } 31 | 32 | func (ldb *LevelDB) Update(key string, value []byte) error { 33 | batch := new(leveldb.Batch) 34 | batch.Delete([]byte(key)) 35 | batch.Put([]byte(key), value) 36 | return ldb.db.Write(batch, nil) 37 | } 38 | 39 | func (ldb *LevelDB) Keys() []string { 40 | cumul := []string{} 41 | iter := ldb.db.NewIterator(util.BytesPrefix([]byte(KEY_PREFIX)), nil) 42 | for iter.Next() { 43 | cumul = append(cumul, string(iter.Key())) 44 | } 45 | iter.Release() 46 | return cumul 47 | } 48 | --------------------------------------------------------------------------------