golib/gcache/fs_adapter.go

215 lines
3.7 KiB
Go
Raw Normal View History

//
// fs_adapter.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import (
"bytes"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"time"
)
// Option is an optional type
type FCOption func(fs *FSStore)
func BaseDirectory(base string) FCOption {
return func(fs *FSStore) {
fs.baseDir = base
}
}
func FCSerializer(serializer Serializer) FCOption {
return func(fs *FSStore) {
fs.b = serializer
}
}
func FCCacheKeyGenerator(fn KeyFunc) FCOption {
return func(fs *FSStore) {
fs.keyFn = fn
}
}
const (
defaultFilePerm os.FileMode = 0644
defaultDirectoryFilePerm = 0755
)
func FilePathKeyFunc(s string) string {
hashSum := md5.Sum([]byte(s))
hashSumAsString := hex.EncodeToString(hashSum[:])
return filepath.Join(string(hashSumAsString[0:2]),
string(hashSumAsString[2:4]),
string(hashSumAsString[4:6]), hashSumAsString)
}
func createDirectory(dir string) error {
return os.MkdirAll(dir, defaultDirectoryFilePerm)
}
type FSStore struct {
baseDir string
b Serializer
keyFn KeyFunc
}
func NewFSStore(baseDir string, opts ...FCOption) (*FSStore, error) {
_, err := os.Stat(baseDir)
if err != nil { //Directory does not exist..Let's create it
if err := createDirectory(baseDir); err != nil {
panic(fmt.Errorf("Base directory could not be created : %s", err))
}
}
opts = append(opts, BaseDirectory(baseDir))
store := &FSStore{}
for _, opt := range opts {
opt(store)
}
if store.b == nil {
store.b = NewCacheSerializer()
}
if len(strings.TrimSpace(store.baseDir)) == 0 {
return nil, errors.New("onecache : base directory not provided")
}
if store.keyFn == nil {
store.keyFn = FilePathKeyFunc
}
return store, nil
}
func (fs *FSStore) Set(key string, data []byte, expiresAt time.Duration) error {
path := fs.filePathFor(key)
if err := createDirectory(filepath.Dir(path)); err != nil {
return err
}
i := &Item{ExpiresAt: time.Now().Add(expiresAt), Data: data}
b, err := fs.b.Serialize(i)
if err != nil {
return err
}
return writeFile(path, b)
}
func (fs *FSStore) Get(key string) ([]byte, error) {
var b = new(bytes.Buffer)
f, err := os.OpenFile(fs.filePathFor(key), os.O_RDONLY, 0644)
if err != nil {
pe, ok := err.(*os.PathError)
if !ok {
return nil, err
}
if pe.Err == syscall.ENOENT && pe.Op == "open" {
return nil, ErrCacheMiss
}
return nil, err
}
if _, err := io.Copy(b, f); err != nil {
f.Close()
return nil, err
}
f.Close()
i := new(Item)
if err := fs.b.DeSerialize(b.Bytes(), i); err != nil {
return nil, err
}
if i.IsExpired() {
fs.Delete(key)
return nil, ErrCacheMiss
}
return i.Data, nil
}
func (fs *FSStore) Delete(key string) error {
return os.RemoveAll(fs.filePathFor(key))
}
func (fs *FSStore) Flush() error {
return os.RemoveAll(fs.baseDir)
}
func (fs *FSStore) GC() {
filepath.Walk(
fs.baseDir,
func(path string, finfo os.FileInfo, err error) error {
if err != nil {
return err
}
if finfo.IsDir() {
return nil
}
currentItem := new(Item)
byt, err := os.ReadFile(path)
if err != nil {
return err
}
if err = fs.b.DeSerialize(byt, currentItem); err != nil {
return err
}
if currentItem.IsExpired() {
if err := os.Remove(path); !os.IsExist(err) {
return err
}
}
return nil
})
}
func (fs *FSStore) Has(key string) bool {
_, err := os.Stat(fs.filePathFor(key))
return !os.IsNotExist(err)
}
func (fs *FSStore) filePathFor(key string) string {
return filepath.Join(fs.baseDir, fs.keyFn(key))
}
func writeFile(path string, b []byte) error {
return os.WriteFile(path, b, defaultFilePerm)
}