diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee265dc0d..de70541b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: file: ./Dockerfile-prod tags: | darkweak/souin:latest-full - darkweak/souin:${{ env.RELEASE_VERSION }}-full + darkweak/souin:${{ env.RELEASE_VERSION }} generate-artifacts: name: Generate cross-platform builds runs-on: ubuntu-latest diff --git a/README.md b/README.md index e92567d9c..ae3a8c4e7 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ As it's written in go, it can be deployed on any server and thanks to the docker It's RFC compatible, supporting Vary, request coalescing and other specifications related to the [RFC-7234](https://tools.ietf.org/html/rfc7234) ## Disclaimer -If you need redis or other custom cache providers, you have to use the full-featured version. You can read the documentation, on [the full-featured branch](https://github.com/Darkweak/Souin/tree/full-version) to discover the specific parts. +If you don't need redis or other custom cache providers, you can use the minimal version. You can read the documentation, on [the minimal branch](https://github.com/Darkweak/Souin) to discover the specific parts. ## Configuration The configuration file is stored at `/anywhere/configuration.yml`. You can edit it provided you fill at least the required parameters as shown below. diff --git a/api/main.go b/api/main.go index 626374222..dec053198 100644 --- a/api/main.go +++ b/api/main.go @@ -7,7 +7,7 @@ import ( ) // Initialize contains all apis that should be enabled -func Initialize(provider types.AbstractProviderInterface, c configurationtypes.AbstractConfigurationInterface) []EndpointInterface { +func Initialize(providers map[string]types.AbstractProviderInterface, c configurationtypes.AbstractConfigurationInterface) []EndpointInterface { security := auth.InitializeSecurity(c) - return []EndpointInterface{security, initializeSouin(provider, c, security)} + return []EndpointInterface{security, initializeSouin(providers, c, security)} } diff --git a/api/souin.go b/api/souin.go index 4c4316707..259a76d01 100644 --- a/api/souin.go +++ b/api/souin.go @@ -14,11 +14,11 @@ import ( type SouinAPI struct { basePath string enabled bool - provider types.AbstractProviderInterface + providers map[string]types.AbstractProviderInterface security *auth.SecurityAPI } -func initializeSouin(provider types.AbstractProviderInterface, configuration configurationtypes.AbstractConfigurationInterface, api *auth.SecurityAPI) *SouinAPI { +func initializeSouin(providers map[string]types.AbstractProviderInterface, configuration configurationtypes.AbstractConfigurationInterface, api *auth.SecurityAPI) *SouinAPI { basePath := configuration.GetAPI().Souin.BasePath enabled := configuration.GetAPI().Souin.Enable var security *auth.SecurityAPI @@ -31,28 +31,37 @@ func initializeSouin(provider types.AbstractProviderInterface, configuration con return &SouinAPI{ basePath, enabled, - provider, + providers, security, } } // BulkDelete allow user to delete multiple items with regexp func (s *SouinAPI) BulkDelete(rg *regexp.Regexp) { - for _, key := range s.GetAll() { - if rg.Match([]byte(key)) { - s.Delete(key) + for _, v := range s.GetAll() { + for _, key := range v { + if rg.Match([]byte(key)) { + s.Delete(key) + } } } } // Delete will delete a record into the provider cache system and will update the Souin API if enabled func (s *SouinAPI) Delete(key string) { - s.provider.Delete(key) + for _, p := range s.providers { + p.Delete(key) + } } // GetAll will retrieve all stored keys in the provider -func (s *SouinAPI) GetAll() []string { - return s.provider.ListKeys() +func (s *SouinAPI) GetAll() map[string][]string { + list := map[string][]string{} + for pName, p := range s.providers { + list[pName] = p.ListKeys() + } + + return list } // GetBasePath will return the basepath for this resource diff --git a/api/souin_test.go b/api/souin_test.go index 03d918184..53cd5bb53 100644 --- a/api/souin_test.go +++ b/api/souin_test.go @@ -24,47 +24,68 @@ func mockSouinAPI() *SouinAPI { func TestSouinAPI_BulkDelete(t *testing.T) { souinMock := mockSouinAPI() - souinMock.provider.Set("key", []byte("value"), tests.GetMatchedURL("key"), 20 * time.Second) - souinMock.provider.Set("key2", []byte("value"), tests.GetMatchedURL("key"), 20 * time.Second) + for _, provider := range souinMock.providers { + provider.Set("key", []byte("value"), tests.GetMatchedURL("key"), 20 * time.Second) + provider.Set("key2", []byte("value"), tests.GetMatchedURL("key"), 20 * time.Second) + } time.Sleep(3 * time.Second) - if len(souinMock.GetAll()) != 2 { - errors.GenerateError(t, "Souin API should have a record") + for _, v := range souinMock.GetAll() { + if len(v) != 2 { + errors.GenerateError(t, "Souin API should have a record") + } } souinMock.BulkDelete(regexp.MustCompile(".+")) time.Sleep(5 * time.Second) - if len(souinMock.GetAll()) != 0 { - errors.GenerateError(t, "Souin API shouldn't have a record") + for _, v := range souinMock.GetAll() { + if len(v) != 0 { + errors.GenerateError(t, "Souin API should have a record") + } } } func TestSouinAPI_Delete(t *testing.T) { souinMock := mockSouinAPI() - souinMock.provider.Set("key", []byte("value"), tests.GetMatchedURL("key"), 20 * time.Second) + for _, provider := range souinMock.providers { + provider.Set("key", []byte("value"), tests.GetMatchedURL("key"), 20 * time.Second) + } time.Sleep(3 * time.Second) - if len(souinMock.GetAll()) != 1 { - errors.GenerateError(t, "Souin API should have a record") + for _, v := range souinMock.GetAll() { + if len(v) != 1 { + errors.GenerateError(t, "Souin API should have a record") + } } souinMock.Delete("key") time.Sleep(3 * time.Second) - if len(souinMock.GetAll()) == 1 { - errors.GenerateError(t, "Souin API shouldn't have a record") + for _, v := range souinMock.GetAll() { + if len(v) == 1 { + errors.GenerateError(t, "Souin API shouldn't have a record") + } } } func TestSouinAPI_GetAll(t *testing.T) { souinMock := mockSouinAPI() - if len(souinMock.GetAll()) > 0 { - errors.GenerateError(t, "Souin API don't have any record yet") + for _, v := range souinMock.GetAll() { + if len(v) > 0 { + errors.GenerateError(t, "Souin API shouldn't have a record") + } } - souinMock.provider.Set("key", []byte("value"), tests.GetMatchedURL("key"), 6 * time.Second) + for _, provider := range souinMock.providers { + provider.Set("key", []byte("value"), tests.GetMatchedURL("key"), 6 * time.Second) + } time.Sleep(3 * time.Second) - if len(souinMock.GetAll()) != 1 { - errors.GenerateError(t, "Souin API should have a record") + for _, v := range souinMock.GetAll() { + if len(v) != 1 { + errors.GenerateError(t, "Souin API should have a record") + } } + souinMock.providers["redis"].Delete("key") time.Sleep(10 * time.Second) - if len(souinMock.GetAll()) == 1 { - errors.GenerateError(t, "Souin API shouldn't have a record") + for _, v := range souinMock.GetAll() { + if len(v) == 1 { + errors.GenerateError(t, "Souin API shouldn't have a record") + } } } diff --git a/cache/providers/redisProvider.go b/cache/providers/redisProvider.go index 756801dd9..0f634eba4 100644 --- a/cache/providers/redisProvider.go +++ b/cache/providers/redisProvider.go @@ -1,6 +1,7 @@ package providers import ( + "github.com/darkweak/souin/cache/keysaver" t "github.com/darkweak/souin/configurationtypes" redis "github.com/go-redis/redis/v8" "strconv" @@ -10,21 +11,35 @@ import ( // Redis provider type type Redis struct { *redis.Client - t.AbstractConfigurationInterface + keySaver *keysaver.ClearKey } // RedisConnectionFactory function create new Redis instance func RedisConnectionFactory(configuration t.AbstractConfigurationInterface) (*Redis, error) { + var keySaver *keysaver.ClearKey + if configuration.GetAPI().Souin.Enable { + keySaver = keysaver.NewClearKey() + //TODO handle eviction on redis + } + return &Redis{ redis.NewClient(&redis.Options{ Addr: configuration.GetDefaultCache().Redis.URL, DB: 0, Password: "", }), - configuration, + keySaver, }, nil } +// ListKeys method returns the list of existing keys +func (provider *Redis) ListKeys() []string { + if nil != provider.keySaver { + return provider.keySaver.ListKeys() + } + return []string{} +} + // Get method returns the populated response if exists, empty response then func (provider *Redis) Get(key string) []byte { val2, err := provider.Client.Get(provider.Context(), key).Result() @@ -46,12 +61,21 @@ func (provider *Redis) Set(key string, value []byte, url t.URL, duration time.Du err := provider.Client.Set(provider.Context(), key, string(value), duration).Err() if err != nil { panic(err) + } else { + go func() { + if nil != provider.keySaver { + provider.keySaver.AddKey(key) + } + }() } } // Delete method will delete the response in Redis provider if exists corresponding to key param func (provider *Redis) Delete(key string) { - provider.Do(provider.Context(), "del", key) + go func() { + provider.Do(provider.Context(), "del", key) + provider.keySaver.DelKey(key, 0) + }() } // Init method will diff --git a/cache/souin.go b/cache/souin.go index 0257cfdc7..d3b9fb082 100644 --- a/cache/souin.go +++ b/cache/souin.go @@ -125,7 +125,7 @@ func Start() { if basePathAPIS == "" { basePathAPIS = "/souin-api" } - for _, endpoint := range api.Initialize(provider, c) { + for _, endpoint := range api.Initialize(cacheProviders, c) { if endpoint.IsEnabled() { http.HandleFunc(fmt.Sprintf("%s%s", basePathAPIS, endpoint.GetBasePath()), endpoint.HandleRequest) http.HandleFunc(fmt.Sprintf("%s%s/", basePathAPIS, endpoint.GetBasePath()), endpoint.HandleRequest)