github.com/mdaxf/iac@v0.0.0-20240519030858-58a061660378/framework/cache/file.go (about) 1 // The package is migrated from beego, you can get from following link: 2 // import( 3 // "github.com/beego/beego/v2/client/cache" 4 // ) 5 // Copyright 2023. All Rights Reserved. 6 // 7 // Licensed under the Apache License, Version 2.0 (the "License"); 8 // you may not use this file except in compliance with the License. 9 // You may obtain a copy of the License at 10 // 11 // http://www.apache.org/licenses/LICENSE-2.0 12 // 13 // Unless required by applicable law or agreed to in writing, software 14 // distributed under the License is distributed on an "AS IS" BASIS, 15 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 // See the License for the specific language governing permissions and 17 // limitations under the License. 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 "os" 31 "path/filepath" 32 "strconv" 33 "strings" 34 "time" 35 36 "github.com/mdaxf/iac/framework/berror" 37 ) 38 39 // FileCacheItem is basic unit of file cache adapter which 40 // contains data and expire time. 41 type FileCacheItem struct { 42 Data interface{} 43 Lastaccess time.Time 44 Expired time.Time 45 } 46 47 // FileCache Config 48 var ( 49 FileCachePath = "cache" // cache directory 50 FileCacheFileSuffix = ".bin" // cache file suffix 51 FileCacheDirectoryLevel = 2 // cache file deep level if auto generated cache files. 52 FileCacheEmbedExpiry time.Duration // cache expire time, default is no expire forever. 53 ) 54 55 // FileCache is cache adapter for file storage. 56 type FileCache struct { 57 CachePath string 58 FileSuffix string 59 DirectoryLevel int 60 EmbedExpiry int 61 } 62 63 // NewFileCache creates a new file cache with no config. 64 // The level and expiry need to be set in the method StartAndGC as config string. 65 func NewFileCache() Cache { 66 // return &FileCache{CachePath:FileCachePath, FileSuffix:FileCacheFileSuffix} 67 return &FileCache{} 68 } 69 70 // StartAndGC starts gc for file cache. 71 // config must be in the format {CachePath:"/cache","FileSuffix":".bin","DirectoryLevel":"2","EmbedExpiry":"0"} 72 func (fc *FileCache) StartAndGC(config string) error { 73 // fmt.Println("file cache config:", config) 74 cfg := make(map[string]string) 75 err := json.Unmarshal([]byte(config), &cfg) 76 if err != nil { 77 fmt.Println("file cache config error:", err) 78 return err 79 } 80 // fmt.Println("file cache config:", cfg) 81 const cpKey = "CachePath" 82 const fsKey = "FileSuffix" 83 const dlKey = "DirectoryLevel" 84 const eeKey = "EmbedExpiry" 85 86 if _, ok := cfg[cpKey]; !ok { 87 cfg[cpKey] = FileCachePath 88 } 89 90 if _, ok := cfg[fsKey]; !ok { 91 cfg[fsKey] = FileCacheFileSuffix 92 } 93 94 if _, ok := cfg[dlKey]; !ok { 95 cfg[dlKey] = strconv.Itoa(FileCacheDirectoryLevel) 96 } 97 98 if _, ok := cfg[eeKey]; !ok { 99 cfg[eeKey] = strconv.FormatInt(int64(FileCacheEmbedExpiry.Seconds()), 10) 100 } 101 fc.CachePath = cfg[cpKey] 102 fc.FileSuffix = cfg[fsKey] 103 fc.DirectoryLevel, err = strconv.Atoi(cfg[dlKey]) 104 if err != nil { 105 return berror.Wrapf(err, InvalidFileCacheDirectoryLevelCfg, 106 "invalid directory level config, please check your input, it must be integer: %s", cfg[dlKey]) 107 } 108 fc.EmbedExpiry, err = strconv.Atoi(cfg[eeKey]) 109 if err != nil { 110 return berror.Wrapf(err, InvalidFileCacheEmbedExpiryCfg, 111 "invalid embed expiry config, please check your input, it must be integer: %s", cfg[eeKey]) 112 } 113 return fc.Init() 114 } 115 116 // Init makes new a dir for file cache if it does not already exist 117 func (fc *FileCache) Init() error { 118 ok, err := exists(fc.CachePath) 119 // fmt.Println("file cache path:", fc.CachePath, ok, err) 120 if err != nil || ok { 121 return err 122 } 123 err = os.MkdirAll(fc.CachePath, os.ModePerm) 124 // fmt.Println("file cache path:", fc.CachePath, err) 125 if err != nil { 126 return berror.Wrapf(err, CreateFileCacheDirFailed, 127 "could not create directory, please check the config [%s] and file mode.", fc.CachePath) 128 } 129 // fmt.Println("file cache path:", fc.CachePath, err) 130 return nil 131 } 132 133 // getCacheFileName returns a md5 encoded file name. 134 func (fc *FileCache) getCacheFileName(key string) (string, error) { 135 // fmt.Println("file cache path-getCacheFileName:", fc.CachePath, key) 136 m := md5.New() 137 _, _ = io.WriteString(m, key) 138 keyMd5 := hex.EncodeToString(m.Sum(nil)) 139 cachePath := fc.CachePath 140 switch fc.DirectoryLevel { 141 case 2: 142 cachePath = filepath.Join(cachePath, keyMd5[0:2], keyMd5[2:4]) 143 case 1: 144 cachePath = filepath.Join(cachePath, keyMd5[0:2]) 145 } 146 ok, err := exists(cachePath) 147 if err != nil { 148 return "", err 149 } 150 if !ok { 151 err = os.MkdirAll(cachePath, os.ModePerm) 152 if err != nil { 153 return "", berror.Wrapf(err, CreateFileCacheDirFailed, 154 "could not create the directory: %s", cachePath) 155 } 156 } 157 158 return filepath.Join(cachePath, fmt.Sprintf("%s%s", keyMd5, fc.FileSuffix)), nil 159 } 160 161 // Get value from file cache. 162 // if nonexistent or expired return an empty string. 163 func (fc *FileCache) Get(ctx context.Context, key string) (interface{}, error) { 164 fn, err := fc.getCacheFileName(key) 165 if err != nil { 166 return nil, err 167 } 168 fileData, err := FileGetContents(fn) 169 if err != nil { 170 return nil, err 171 } 172 173 var to FileCacheItem 174 err = GobDecode(fileData, &to) 175 if err != nil { 176 return nil, err 177 } 178 179 if to.Expired.Before(time.Now()) { 180 return nil, ErrKeyExpired 181 } 182 return to.Data, nil 183 } 184 185 // GetMulti gets values from file cache. 186 // if nonexistent or expired return an empty string. 187 func (fc *FileCache) GetMulti(ctx context.Context, keys []string) ([]interface{}, error) { 188 rc := make([]interface{}, len(keys)) 189 keysErr := make([]string, 0) 190 191 for i, ki := range keys { 192 val, err := fc.Get(context.Background(), ki) 193 if err != nil { 194 keysErr = append(keysErr, fmt.Sprintf("key [%s] error: %s", ki, err.Error())) 195 continue 196 } 197 rc[i] = val 198 } 199 200 if len(keysErr) == 0 { 201 return rc, nil 202 } 203 return rc, berror.Error(MultiGetFailed, strings.Join(keysErr, "; ")) 204 } 205 206 // Put value into file cache. 207 // timeout: how long this file should be kept in ms 208 // if timeout equals fc.EmbedExpiry(default is 0), cache this item forever. 209 func (fc *FileCache) Put(ctx context.Context, key string, val interface{}, timeout time.Duration) error { 210 gob.Register(val) 211 212 item := FileCacheItem{Data: val} 213 if timeout == time.Duration(fc.EmbedExpiry) { 214 item.Expired = time.Now().Add((86400 * 365 * 10) * time.Second) // ten years 215 } else { 216 item.Expired = time.Now().Add(timeout) 217 } 218 item.Lastaccess = time.Now() 219 data, err := GobEncode(item) 220 if err != nil { 221 return err 222 } 223 224 fn, err := fc.getCacheFileName(key) 225 if err != nil { 226 return err 227 } 228 return FilePutContents(fn, data) 229 } 230 231 // Delete file cache value. 232 func (fc *FileCache) Delete(ctx context.Context, key string) error { 233 filename, err := fc.getCacheFileName(key) 234 if err != nil { 235 return err 236 } 237 if ok, _ := exists(filename); ok { 238 err = os.Remove(filename) 239 if err != nil { 240 return berror.Wrapf(err, DeleteFileCacheItemFailed, 241 "can not delete this file cache key-value, key is %s and file name is %s", key, filename) 242 } 243 } 244 return nil 245 } 246 247 // Incr increases cached int value. 248 // fc value is saved forever unless deleted. 249 func (fc *FileCache) Incr(ctx context.Context, key string) error { 250 data, err := fc.Get(context.Background(), key) 251 if err != nil { 252 return err 253 } 254 255 val, err := incr(data) 256 if err != nil { 257 return err 258 } 259 260 return fc.Put(context.Background(), key, val, time.Duration(fc.EmbedExpiry)) 261 } 262 263 // Decr decreases cached int value. 264 func (fc *FileCache) Decr(ctx context.Context, key string) error { 265 data, err := fc.Get(context.Background(), key) 266 if err != nil { 267 return err 268 } 269 270 val, err := decr(data) 271 if err != nil { 272 return err 273 } 274 275 return fc.Put(context.Background(), key, val, time.Duration(fc.EmbedExpiry)) 276 } 277 278 // IsExist checks if value exists. 279 func (fc *FileCache) IsExist(ctx context.Context, key string) (bool, error) { 280 fn, err := fc.getCacheFileName(key) 281 if err != nil { 282 return false, err 283 } 284 return exists(fn) 285 } 286 287 // ClearAll cleans cached files (not implemented) 288 func (fc *FileCache) ClearAll(context.Context) error { 289 return nil 290 } 291 292 // Check if a file exists 293 func exists(path string) (bool, error) { 294 _, err := os.Stat(path) 295 if err == nil { 296 return true, nil 297 } 298 if os.IsNotExist(err) { 299 return false, nil 300 } 301 return false, berror.Wrapf(err, InvalidFileCachePath, "file cache path is invalid: %s", path) 302 } 303 304 // FileGetContents Reads bytes from a file. 305 // if non-existent, create this file. 306 func FileGetContents(filename string) ([]byte, error) { 307 data, err := os.ReadFile(filename) 308 if err != nil { 309 return nil, berror.Wrapf(err, ReadFileCacheContentFailed, 310 "could not read the data from the file: %s, "+ 311 "please confirm that file exist and Beego has the permission to read the content.", filename) 312 } 313 return data, nil 314 } 315 316 // FilePutContents puts bytes into a file. 317 // if non-existent, create this file. 318 func FilePutContents(filename string, content []byte) error { 319 return os.WriteFile(filename, content, os.ModePerm) 320 } 321 322 // GobEncode Gob encodes a file cache item. 323 func GobEncode(data interface{}) ([]byte, error) { 324 buf := bytes.NewBuffer(nil) 325 enc := gob.NewEncoder(buf) 326 err := enc.Encode(data) 327 if err != nil { 328 return nil, berror.Wrap(err, GobEncodeDataFailed, "could not encode this data") 329 } 330 return buf.Bytes(), nil 331 } 332 333 // GobDecode Gob decodes a file cache item. 334 func GobDecode(data []byte, to *FileCacheItem) error { 335 buf := bytes.NewBuffer(data) 336 dec := gob.NewDecoder(buf) 337 err := dec.Decode(&to) 338 if err != nil { 339 return berror.Wrap(err, InvalidGobEncodedData, 340 "could not decode this data to FileCacheItem. Make sure that the data is encoded by GOB.") 341 } 342 return nil 343 } 344 345 func init() { 346 Register("file", NewFileCache) 347 }