// // fs_adapter.go // Copyright (C) 2023 tiglog // // 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) }