On this page
Add your own storage
Context
The storage repository defines an interface called Storer
in the core/core.go file that should be implemented by your storage.
Your struct
must implement it to be a valid storer and be registered in the storage pool.
By convention we declare a Factory
function that respects this signature:
func(providerConfiguration core.CacheProvider, logger core.Logger, stale time.Duration) (core.Storer, error)
And the Storer
interface is the following:
type Storer interface {
MapKeys(prefix string) map[string]string
ListKeys() []string
Get(key string) []byte
Set(key string, value []byte, duration time.Duration) error
Delete(key string)
DeleteMany(key string)
Init() error
Name() string
Uuid() string
Reset() error
// Multi level storer to handle fresh/stale at once
GetMultiLevel(key string, req *http.Request, validator *Revalidator) (fresh *http.Response, stale *http.Response)
SetMultiLevel(baseKey, variedKey string, value []byte, variedHeaders http.Header, etag string, duration time.Duration, realKey string) error
}
Example
Let’s define our simple in-memory storage
// your_custom_storage.go
package your_package
import (
"sync"
"time"
"github.com/darkweak/souin/pkg/storage/types"
"github.com/darkweak/storages/core"
)
// custom storage provider type
type customStorage struct {
m *sync.Map
stale time.Duration
logger core.Logger
}
// Factory function create new custom storage instance
func Factory(_ core.CacheProvider, logger core.Logger, stale time.Duration) (types.Storer, error) {
return &customStorage{m: &sync.Map{}, logger: logger, stale: stale}, nil
}
We have to implement the Storer
interface now.
// your_custom_storage.go
...
// Name returns the storer name
func (provider *customStorage) Name() string {
return "YOUR_CUSTOM_STORAGE"
}
// Uuid returns an unique identifier
func (provider *customStorage) Uuid() string {
return "THE_UUID"
}
// MapKeys method returns a map with the key and value
func (provider *customStorage) MapKeys(prefix string) map[string]string {
now := time.Now()
keys := map[string]string{}
provider.m.Range(func(key, value any) bool {
if strings.HasPrefix(key.(string), prefix) {
k, _ := strings.CutPrefix(key.(string), prefix)
if v, ok := value.(core.StorageMapper); ok {
for _, v := range v.Mapping {
if v.StaleTime.After(now) {
keys[v.RealKey] = string(provider.Get(v.RealKey))
}
}
return true
}
keys[k] = string(value.([]byte))
}
return true
})
return keys
}
// ListKeys method returns the list of existing keys
func (provider *customStorage) ListKeys() []string {
now := time.Now()
keys := []string{}
provider.m.Range(func(key, value any) bool {
if strings.HasPrefix(key.(string), core.MappingKeyPrefix) {
mapping, err := core.DecodeMapping(value.([]byte))
if err == nil {
for _, v := range mapping.Mapping {
if v.StaleTime.After(now) {
keys = append(keys, v.RealKey)
} else {
provider.m.Delete(v.RealKey)
}
}
}
}
return true
})
return keys
}
// Get method returns the populated response if exists, empty response then
func (provider *customStorage) Get(key string) []byte {
result, ok := provider.m.Load(key)
if !ok || result == nil {
return nil
}
res, ok := result.([]byte)
if !ok {
return nil
}
return res
}
// GetMultiLevel tries to load the key and check if one of linked keys is a fresh/stale candidate.
func (provider *customStorage) GetMultiLevel(key string, req *http.Request, validator *core.Revalidator) (fresh *http.Response, stale *http.Response) {
result, found := provider.m.Load(core.MappingKeyPrefix + key)
if !found {
return
}
fresh, stale, _ = core.MappingElection(provider, result.([]byte), req, validator, provider.logger)
return
}
// SetMultiLevel tries to store the key with the given value and update the mapping key to store metadata.
func (provider *customStorage) SetMultiLevel(baseKey, variedKey string, value []byte, variedHeaders http.Header, etag string, duration time.Duration, realKey string) error {
now := time.Now()
var e error
compressed := new(bytes.Buffer)
if _, e = lz4.NewWriter(compressed).ReadFrom(bytes.NewReader(value)); e != nil {
provider.logger.Errorf("Impossible to compress the key %s into Badger, %v", variedKey, e)
return e
}
provider.m.Store(variedKey, compressed.Bytes())
mappingKey := core.MappingKeyPrefix + baseKey
item, ok := provider.m.Load(mappingKey)
var val []byte
if ok {
val = item.([]byte)
}
val, e = core.MappingUpdater(variedKey, val, provider.logger, now, now.Add(duration), now.Add(duration+provider.stale), variedHeaders, etag, realKey)
if e != nil {
return e
}
provider.logger.Debugf("Store the new mapping for the key %s in customStorage", variedKey)
provider.m.Store(mappingKey, val)
return nil
}
// Set method will store the response in Badger provider
func (provider *customStorage) Set(key string, value []byte, duration time.Duration) error {
provider.m.Store(key, value)
return nil
}
// Delete method will delete the response in Badger provider if exists corresponding to key param
func (provider *customStorage) Delete(key string) {
provider.m.Delete(key)
}
// DeleteMany method will delete the responses in Badger provider if exists corresponding to the regex key param
func (provider *customStorage) DeleteMany(key string) {
re, e := regexp.Compile(key)
if e != nil {
return
}
provider.m.Range(func(key, _ any) bool {
if re.MatchString(key.(string)) {
provider.m.Delete(key)
}
return true
})
}
// Init method will
func (provider *customStorage) Init() error {
return nil
}
// Reset method will reset or close provider
func (provider *customStorage) Reset() error {
provider.m = &sync.Map{}
return nil
}
After that you’ll be able to register your storage using
// anywhere.go
...
logger, _ := zap.NewProduction()
customStorage, _ := your_package.Factory(core.CacheProvider{}, logger.Sugar(), time.Hour)
// It will register as `YOUR_CUSTOM_STORAGE-THE_UUID`.
core.RegisterStorage(customStorage)
// In your code
if st := core.GetRegisteredStorer("YOUR_CUSTOM_STORAGE-THE_UUID"); st != nil {
customStorage = st.(types.Storer)
}