| 
									
										
										
										
											2020-07-27 17:01:02 +00:00
										 |  |  | package diskcache | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"crypto/sha1" //nolint:gosec
 | 
					
						
							|  |  |  | 	"encoding/hex" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"io/ioutil" | 
					
						
							|  |  |  | 	"os" | 
					
						
							|  |  |  | 	"path/filepath" | 
					
						
							|  |  |  | 	"sync" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/spf13/afero" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type FileCache struct { | 
					
						
							|  |  |  | 	fs afero.Fs | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// granular locks
 | 
					
						
							|  |  |  | 	scopedLocks struct { | 
					
						
							|  |  |  | 		sync.Mutex | 
					
						
							|  |  |  | 		sync.Once | 
					
						
							|  |  |  | 		locks map[string]sync.Locker | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func New(fs afero.Fs, root string) *FileCache { | 
					
						
							|  |  |  | 	return &FileCache{ | 
					
						
							|  |  |  | 		fs: afero.NewBasePathFs(fs, root), | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (f *FileCache) Store(ctx context.Context, key string, value []byte) error { | 
					
						
							|  |  |  | 	mu := f.getScopedLocks(key) | 
					
						
							|  |  |  | 	mu.Lock() | 
					
						
							|  |  |  | 	defer mu.Unlock() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	fileName := f.getFileName(key) | 
					
						
							|  |  |  | 	if err := f.fs.MkdirAll(filepath.Dir(fileName), 0700); err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if err := afero.WriteFile(f.fs, fileName, value, 0700); err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (f *FileCache) Load(ctx context.Context, key string) (value []byte, exist bool, err error) { | 
					
						
							|  |  |  | 	r, ok, err := f.open(key) | 
					
						
							|  |  |  | 	if err != nil || !ok { | 
					
						
							|  |  |  | 		return nil, ok, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	defer r.Close() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	value, err = ioutil.ReadAll(r) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, false, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return value, true, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (f *FileCache) Delete(ctx context.Context, key string) error { | 
					
						
							|  |  |  | 	mu := f.getScopedLocks(key) | 
					
						
							|  |  |  | 	mu.Lock() | 
					
						
							|  |  |  | 	defer mu.Unlock() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	fileName := f.getFileName(key) | 
					
						
							| 
									
										
										
										
											2020-07-28 09:57:26 +00:00
										 |  |  | 	if err := f.fs.Remove(fileName); err != nil && !errors.Is(err, os.ErrNotExist) { | 
					
						
							| 
									
										
										
										
											2020-07-27 17:01:02 +00:00
										 |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (f *FileCache) open(key string) (afero.File, bool, error) { | 
					
						
							|  |  |  | 	fileName := f.getFileName(key) | 
					
						
							|  |  |  | 	file, err := f.fs.Open(fileName) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		if errors.Is(err, os.ErrNotExist) { | 
					
						
							|  |  |  | 			return nil, false, nil | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return nil, false, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return file, true, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // getScopedLocks pull lock from the map if found or create a new one
 | 
					
						
							|  |  |  | func (f *FileCache) getScopedLocks(key string) (lock sync.Locker) { | 
					
						
							|  |  |  | 	f.scopedLocks.Do(func() { f.scopedLocks.locks = map[string]sync.Locker{} }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	f.scopedLocks.Lock() | 
					
						
							|  |  |  | 	lock, ok := f.scopedLocks.locks[key] | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		lock = &sync.Mutex{} | 
					
						
							|  |  |  | 		f.scopedLocks.locks[key] = lock | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	f.scopedLocks.Unlock() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return lock | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (f *FileCache) getFileName(key string) string { | 
					
						
							|  |  |  | 	hasher := sha1.New() //nolint:gosec
 | 
					
						
							|  |  |  | 	_, _ = hasher.Write([]byte(key)) | 
					
						
							|  |  |  | 	hash := hex.EncodeToString(hasher.Sum(nil)) | 
					
						
							|  |  |  | 	return fmt.Sprintf("%s/%s/%s", hash[:1], hash[1:3], hash) | 
					
						
							|  |  |  | } |