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  }