github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/system/cache/file.go (about) 1 // This file is part of the Smart Home 2 // Program complex distribution https://github.com/e154/smart-home 3 // Copyright (C) 2016-2023, Filippov Alex 4 // 5 // This library is free software: you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 3 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Library General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library. If not, see 17 // <https://www.gnu.org/licenses/>. 18 19 package cache 20 21 import ( 22 "bytes" 23 "context" 24 "crypto/md5" 25 "encoding/gob" 26 "encoding/hex" 27 "encoding/json" 28 "fmt" 29 "io" 30 "io/ioutil" 31 "os" 32 "path/filepath" 33 "strconv" 34 "strings" 35 "time" 36 37 "github.com/pkg/errors" 38 ) 39 40 // FileCacheItem is basic unit of file cache adapter which 41 // contains data and expire time. 42 type FileCacheItem struct { 43 Data interface{} 44 Lastaccess time.Time 45 Expired time.Time 46 } 47 48 // FileCache Config 49 var ( 50 FileCachePath = "cache" // cache directory 51 FileCacheFileSuffix = ".bin" // cache file suffix 52 FileCacheDirectoryLevel = 2 // cache file deep level if auto generated cache files. 53 FileCacheEmbedExpiry time.Duration // cache expire time, default is no expire forever. 54 ) 55 56 // FileCache is cache adapter for file storage. 57 type FileCache struct { 58 CachePath string 59 FileSuffix string 60 DirectoryLevel int 61 EmbedExpiry int 62 } 63 64 // NewFileCache creates a new file cache with no config. 65 // The level and expiry need to be set in the method StartAndGC as config string. 66 func NewFileCache() Cache { 67 // return &FileCache{CachePath:FileCachePath, FileSuffix:FileCacheFileSuffix} 68 return &FileCache{} 69 } 70 71 // StartAndGC starts gc for file cache. 72 // config must be in the format {CachePath:"/cache","FileSuffix":".bin","DirectoryLevel":"2","EmbedExpiry":"0"} 73 func (fc *FileCache) StartAndGC(config string) error { 74 75 cfg := make(map[string]string) 76 err := json.Unmarshal([]byte(config), &cfg) 77 if err != nil { 78 return err 79 } 80 if _, ok := cfg["CachePath"]; !ok { 81 cfg["CachePath"] = FileCachePath 82 } 83 if _, ok := cfg["FileSuffix"]; !ok { 84 cfg["FileSuffix"] = FileCacheFileSuffix 85 } 86 if _, ok := cfg["DirectoryLevel"]; !ok { 87 cfg["DirectoryLevel"] = strconv.Itoa(FileCacheDirectoryLevel) 88 } 89 if _, ok := cfg["EmbedExpiry"]; !ok { 90 cfg["EmbedExpiry"] = strconv.FormatInt(int64(FileCacheEmbedExpiry.Seconds()), 10) 91 } 92 fc.CachePath = cfg["CachePath"] 93 fc.FileSuffix = cfg["FileSuffix"] 94 fc.DirectoryLevel, _ = strconv.Atoi(cfg["DirectoryLevel"]) 95 fc.EmbedExpiry, _ = strconv.Atoi(cfg["EmbedExpiry"]) 96 97 fc.Init() 98 return nil 99 } 100 101 // Init makes new a dir for file cache if it does not already exist 102 func (fc *FileCache) Init() { 103 if ok, _ := exists(fc.CachePath); !ok { // todo : error handle 104 _ = os.MkdirAll(fc.CachePath, os.ModePerm) // todo : error handle 105 } 106 } 107 108 // getCachedFilename returns an md5 encoded file name. 109 func (fc *FileCache) getCacheFileName(key string) string { 110 m := md5.New() 111 _, _ = io.WriteString(m, key) 112 keyMd5 := hex.EncodeToString(m.Sum(nil)) 113 cachePath := fc.CachePath 114 switch fc.DirectoryLevel { 115 case 2: 116 cachePath = filepath.Join(cachePath, keyMd5[0:2], keyMd5[2:4]) 117 case 1: 118 cachePath = filepath.Join(cachePath, keyMd5[0:2]) 119 } 120 121 if ok, _ := exists(cachePath); !ok { // todo : error handle 122 _ = os.MkdirAll(cachePath, os.ModePerm) // todo : error handle 123 } 124 125 return filepath.Join(cachePath, fmt.Sprintf("%s%s", keyMd5, fc.FileSuffix)) 126 } 127 128 // Get value from file cache. 129 // if nonexistent or expired return an empty string. 130 func (fc *FileCache) Get(ctx context.Context, key string) (interface{}, error) { 131 fileData, err := FileGetContents(fc.getCacheFileName(key)) 132 if err != nil { 133 return nil, err 134 } 135 136 var to FileCacheItem 137 err = GobDecode(fileData, &to) 138 if err != nil { 139 return nil, err 140 } 141 142 if to.Expired.Before(time.Now()) { 143 return nil, errors.New("The key is expired") 144 } 145 return to.Data, nil 146 } 147 148 // GetMulti gets values from file cache. 149 // if nonexistent or expired return an empty string. 150 func (fc *FileCache) GetMulti(ctx context.Context, keys []string) ([]interface{}, error) { 151 rc := make([]interface{}, len(keys)) 152 keysErr := make([]string, 0) 153 154 for i, ki := range keys { 155 val, err := fc.Get(context.Background(), ki) 156 if err != nil { 157 keysErr = append(keysErr, fmt.Sprintf("key [%s] error: %s", ki, err.Error())) 158 continue 159 } 160 rc[i] = val 161 } 162 163 if len(keysErr) == 0 { 164 return rc, nil 165 } 166 return rc, errors.New(strings.Join(keysErr, "; ")) 167 } 168 169 // Put value into file cache. 170 // timeout: how long this file should be kept in ms 171 // if timeout equals fc.EmbedExpiry(default is 0), cache this item forever. 172 func (fc *FileCache) Put(ctx context.Context, key string, val interface{}, timeout time.Duration) error { 173 gob.Register(val) 174 175 item := FileCacheItem{Data: val} 176 if timeout == time.Duration(fc.EmbedExpiry) { 177 item.Expired = time.Now().Add((86400 * 365 * 10) * time.Second) // ten years 178 } else { 179 item.Expired = time.Now().Add(timeout) 180 } 181 item.Lastaccess = time.Now() 182 data, err := GobEncode(item) 183 if err != nil { 184 return err 185 } 186 return FilePutContents(fc.getCacheFileName(key), data) 187 } 188 189 // Delete file cache value. 190 func (fc *FileCache) Delete(ctx context.Context, key string) error { 191 filename := fc.getCacheFileName(key) 192 if ok, _ := exists(filename); ok { 193 return os.Remove(filename) 194 } 195 return nil 196 } 197 198 // Incr increases cached int value. 199 // fc value is saved forever unless deleted. 200 func (fc *FileCache) Incr(ctx context.Context, key string) error { 201 data, err := fc.Get(context.Background(), key) 202 if err != nil { 203 return err 204 } 205 206 var res interface{} 207 switch val := data.(type) { 208 case int: 209 res = val + 1 210 case int32: 211 res = val + 1 212 case int64: 213 res = val + 1 214 case uint: 215 res = val + 1 216 case uint32: 217 res = val + 1 218 case uint64: 219 res = val + 1 220 default: 221 return errors.Errorf("data is not (u)int (u)int32 (u)int64") 222 } 223 224 return fc.Put(context.Background(), key, res, time.Duration(fc.EmbedExpiry)) 225 } 226 227 // Decr decreases cached int value. 228 func (fc *FileCache) Decr(ctx context.Context, key string) error { 229 data, err := fc.Get(context.Background(), key) 230 if err != nil { 231 return err 232 } 233 234 var res interface{} 235 switch val := data.(type) { 236 case int: 237 res = val - 1 238 case int32: 239 res = val - 1 240 case int64: 241 res = val - 1 242 case uint: 243 if val > 0 { 244 res = val - 1 245 } else { 246 return errors.New("data val is less than 0") 247 } 248 case uint32: 249 if val > 0 { 250 res = val - 1 251 } else { 252 return errors.New("data val is less than 0") 253 } 254 case uint64: 255 if val > 0 { 256 res = val - 1 257 } else { 258 return errors.New("data val is less than 0") 259 } 260 default: 261 return errors.Errorf("data is not (u)int (u)int32 (u)int64") 262 } 263 264 return fc.Put(context.Background(), key, res, time.Duration(fc.EmbedExpiry)) 265 } 266 267 // IsExist checks if value exists. 268 func (fc *FileCache) IsExist(ctx context.Context, key string) (bool, error) { 269 ret, _ := exists(fc.getCacheFileName(key)) 270 return ret, nil 271 } 272 273 // ClearAll cleans cached files (not implemented) 274 func (fc *FileCache) ClearAll(context.Context) error { 275 return nil 276 } 277 278 // Check if a file exists 279 func exists(path string) (bool, error) { 280 _, err := os.Stat(path) 281 if err == nil { 282 return true, nil 283 } 284 if os.IsNotExist(err) { 285 return false, nil 286 } 287 return false, err 288 } 289 290 // FileGetContents Reads bytes from a file. 291 // if non-existent, create this file. 292 func FileGetContents(filename string) (data []byte, e error) { 293 return ioutil.ReadFile(filename) 294 } 295 296 // FilePutContents puts bytes into a file. 297 // if non-existent, create this file. 298 func FilePutContents(filename string, content []byte) error { 299 return ioutil.WriteFile(filename, content, os.ModePerm) 300 } 301 302 // GobEncode Gob encodes a file cache item. 303 func GobEncode(data interface{}) ([]byte, error) { 304 buf := bytes.NewBuffer(nil) 305 enc := gob.NewEncoder(buf) 306 err := enc.Encode(data) 307 if err != nil { 308 return nil, err 309 } 310 return buf.Bytes(), err 311 } 312 313 // GobDecode Gob decodes a file cache item. 314 func GobDecode(data []byte, to *FileCacheItem) error { 315 buf := bytes.NewBuffer(data) 316 dec := gob.NewDecoder(buf) 317 return dec.Decode(&to) 318 } 319 320 func init() { 321 Register("file", NewFileCache) 322 }