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  }