diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3584adc7..6944b34b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -110,3 +110,21 @@ updates: automerged_updates: - match: dependency_name: "gofiber/fiber/*" + - package-ecosystem: "gomod" + directory: "/s3/" # Location of package manifests + default_labels: + - "🤖 Dependencies" + schedule: + interval: "daily" + automerged_updates: + - match: + dependency_name: "gofiber/fiber/*" + - package-ecosystem: "gomod" + directory: "/bbolt/" # Location of package manifests + default_labels: + - "🤖 Dependencies" + schedule: + interval: "daily" + automerged_updates: + - match: + dependency_name: "gofiber/fiber/*" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c7a089b9..94f5cfd4 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -13,10 +13,10 @@ jobs: - name: Install Gosec run: | export PATH=${PATH}:`go env GOPATH`/bin - go get -u github.com/securego/gosec/v2/cmd/gosec + go install github.com/securego/gosec/v2/cmd/gosec@latest - name: Run Gosec (root) working-directory: . - run: "`go env GOPATH`/bin/gosec -exclude-dir=internal -exclude-dir=arangodb -exclude-dir=badger -exclude-dir=dynamodb -exclude-dir=memcache -exclude-dir=memory -exclude-dir=mongodb -exclude-dir=mysql -exclude-dir=postgres -exclude-dir=redis -exclude-dir=ristretto -exclude-dir=sqlite3 ./..." + run: "`go env GOPATH`/bin/gosec -exclude-dir=internal -exclude-dir=arangodb -exclude-dir=badger -exclude-dir=dynamodb -exclude-dir=memcache -exclude-dir=memory -exclude-dir=mongodb -exclude-dir=mysql -exclude-dir=postgres -exclude-dir=redis -exclude-dir=ristretto -exclude-dir=sqlite3 -exclude-dir=s3 -exclude-dir=bbolt ./..." # ----- - name: Run Gosec (arangodb) working-directory: ./arangodb @@ -58,7 +58,15 @@ jobs: working-directory: ./sqlite3 run: "`go env GOPATH`/bin/gosec -exclude-dir=internal ./..." # ----- + - name: Run Gosec (s3) + working-directory: ./s3 + run: "`go env GOPATH`/bin/gosec -exclude-dir=internal ./..." + # ----- - name: Run Gosec (ristretto) working-directory: ./ristretto run: "`go env GOPATH`/bin/gosec -exclude-dir=internal ./..." # ----- + - name: Run Gosec (bbolt) + working-directory: ./bbolt + run: "`go env GOPATH`/bin/gosec -exclude-dir=internal ./..." + # ----- diff --git a/.github/workflows/test-bbolt.yml b/.github/workflows/test-bbolt.yml new file mode 100644 index 00000000..65cc04aa --- /dev/null +++ b/.github/workflows/test-bbolt.yml @@ -0,0 +1,24 @@ +'on': + - push + - pull_request +name: Bbolt +jobs: + Tests: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: + - 1.16.x + - 1.17.x + platform: + - ubuntu-latest + - windows-latest + steps: + - name: Install Go + uses: actions/setup-go@v1 + with: + go-version: '${{ matrix.go-version }}' + - name: Fetch Repository + uses: actions/checkout@v2 + - name: Run Test + run: cd ./bbolt && go mod tidy && go test ./... -v -race diff --git a/.github/workflows/test-dynamodb.yml b/.github/workflows/test-dynamodb.yml new file mode 100644 index 00000000..1dc2398c --- /dev/null +++ b/.github/workflows/test-dynamodb.yml @@ -0,0 +1,31 @@ +'on': + - push + - pull_request +name: DynamoDB +jobs: + Tests: + runs-on: ubuntu-latest + services: + mongo: + image: 'amazon/dynamodb-local:latest' + ports: + - '8000:8000' + strategy: + matrix: + go-version: + - 1.14.x + - 1.15.x + - 1.16.x + - 1.17.x + platform: + - ubuntu-latest + - windows-latest + steps: + - name: Install Go + uses: actions/setup-go@v1 + with: + go-version: '${{ matrix.go-version }}' + - name: Fetch Repository + uses: actions/checkout@v2 + - name: Run Test + run: cd ./dynamodb && go test ./... -v -race diff --git a/.github/workflows/test-s3.yml b/.github/workflows/test-s3.yml new file mode 100644 index 00000000..06a6cc05 --- /dev/null +++ b/.github/workflows/test-s3.yml @@ -0,0 +1,35 @@ +'on': + - push + - pull_request +name: S3 +jobs: + Tests: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: + - 1.14.x + - 1.15.x + - 1.16.x + - 1.17.x + platform: + - ubuntu-latest + - windows-latest + steps: + - name: Install MinIO + run: | + docker run -d -p 9000:9000 --name minio minio/minio server /data + + export AWS_ACCESS_KEY_ID=minioadmin + export AWS_SECRET_ACCESS_KEY=minioadmin + export AWS_EC2_METADATA_DISABLED=true + + aws --endpoint-url http://127.0.0.1:9000/ s3 mb s3://testbucket + - name: Install Go + uses: actions/setup-go@v1 + with: + go-version: '${{ matrix.go-version }}' + - name: Fetch Repository + uses: actions/checkout@v2 + - name: Run Test + run: cd ./s3 && go test ./... -v -race diff --git a/README.md b/README.md index ff69ad59..4291cf5d 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,9 @@ type Storage interface { * [SQLite3](/sqlite3) +* [S3](/s3) + + +* [Bbolt](/bbolt) + + \ No newline at end of file diff --git a/arangodb/go.mod b/arangodb/go.mod index 0e932cbd..0b084a0a 100644 --- a/arangodb/go.mod +++ b/arangodb/go.mod @@ -3,6 +3,6 @@ module github.com/gofiber/storage/arangodb go 1.14 require ( - github.com/arangodb/go-driver v1.2.1 + github.com/arangodb/go-driver v1.3.1 github.com/gofiber/utils v0.1.2 ) diff --git a/arangodb/go.sum b/arangodb/go.sum index 26d98a39..5fd285fb 100644 --- a/arangodb/go.sum +++ b/arangodb/go.sum @@ -1,5 +1,5 @@ -github.com/arangodb/go-driver v1.2.1 h1:HREDHhDmzdIWxHmfkfTESbYUnRjESjPh4WUuXq7FZa8= -github.com/arangodb/go-driver v1.2.1/go.mod h1:zdDkJJnCj8DAkfbtIjIXnsTrWIiy6VhP3Vy14p+uQeY= +github.com/arangodb/go-driver v1.3.1 h1:ypwg9uwahiUekuwdDOttoLR7F5DmK5BzpSXt92poCyQ= +github.com/arangodb/go-driver v1.3.1/go.mod h1:5GAx3XvK72DJPhJgyjZOtYAGc4SpY7rZDb3LyhCvLcQ= github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e h1:Xg+hGrY2LcQBbxd0ZFdbGSyRKTYMZCfBbw/pMJFOk1g= github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e/go.mod h1:mq7Shfa/CaixoDxiyAAc5jZ6CVBAyPaNQCGS7mkj4Ho= github.com/coreos/go-iptables v0.4.3/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= diff --git a/bbolt/README.md b/bbolt/README.md new file mode 100644 index 00000000..b00a548f --- /dev/null +++ b/bbolt/README.md @@ -0,0 +1,92 @@ +# Bbolt +A Bbolt storage driver using [etcd-io/bbolt](https://github.com/etcd-io/bbolt). Bolt is a pure Go key/value store inspired by [Howard Chu's](https://twitter.com/hyc_symas) [LMDB project](https://www.symas.com/symas-embedded-database-lmdb). The goal of the project is to provide a simple, fast, and reliable database for projects that don't require a full database server such as Postgres or MySQL. + + +### Table of Contents +- [Signatures](#signatures) +- [Installation](#installation) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + +### Signatures +```go +func New(config ...Config) Storage +func (s *Storage) Get(key string) ([]byte, error) +func (s *Storage) Set(key string, val []byte, exp time.Duration) error +func (s *Storage) Delete(key string) error +func (s *Storage) Reset() error +func (s *Storage) Close() error +``` +### Installation +Bbolt is tested on the 2 last [Go versions](https://golang.org/dl/) with support for modules. So make sure to initialize one first if you didn't do that yet: +```bash +go mod init github.com// +``` +And then install the s3 implementation: +```bash +go get github.com/gofiber/storage/bbolt +``` + +### Examples +Import the storage package. +```go +import "github.com/gofiber/storage/bbolt" +``` + +You can use the following possibilities to create a storage: +```go +// Initialize default config +store := bbolt.New() + +// Initialize custom config +store := bbolt.New(bbolt.Config{ + Database: "my_database.db", + Bucket: "my-bucket", + Reset: false, +}) +``` + +### Config +```go +// Config defines the config for storage. +type Config struct { + // Database path + // + // Optional. Default is "fiber.db" + Database string + + // Bbolt bucket name + // + // Optional. Default is "fiber_storage" + Bucket string + + // Timeout is the amount of time to wait to obtain a file lock. + // Only available on Darwin and Linux. + // + // Optional. Default is 0 (no timeout) + Timeout time.Duration + + // Open database in read-only mode. + // + // Optional. Default is false + ReadOnly bool + + // Reset clears any existing keys in existing Bucket + // + // Optional. Default is false + Reset bool +} +``` + +### Default Config +```go +// ConfigDefault is the default config +var ConfigDefault = Config{ + Database: "fiber.db", + Bucket: "fiber_storage", + Timeout: 0, + ReadOnly: false, + Reset: false, +} +``` diff --git a/bbolt/bbolt.go b/bbolt/bbolt.go new file mode 100644 index 00000000..b93253eb --- /dev/null +++ b/bbolt/bbolt.go @@ -0,0 +1,106 @@ +package bbolt + +import ( + "time" + + "github.com/gofiber/utils" + "go.etcd.io/bbolt" +) + +// Storage interface that is implemented by storage providers +type Storage struct { + conn *bbolt.DB + bucket string +} + +// New creates a new storage +func New(config ...Config) *Storage { + // Set default config + cfg := configDefault(config...) + + conn, err := bbolt.Open(cfg.Database, 0666, &bbolt.Options{ + Timeout: cfg.Timeout, + ReadOnly: cfg.ReadOnly, + }) + if err != nil { + panic(err) + } + + // Reset bucket if field selected + if cfg.Reset { + if err := removeBucket(cfg, conn); err != nil { + panic(err) + } + } + + // Create bucket if not exists + if err := createBucket(cfg, conn); err != nil { + panic(err) + } + + return &Storage{ + conn: conn, + bucket: cfg.Bucket, + } + +} + +// Get value by key +func (s *Storage) Get(key string) ([]byte, error) { + if len(key) <= 0 { + return nil, nil + } + + var value []byte + + err := s.conn.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(utils.UnsafeBytes(s.bucket)) + value = b.Get(utils.UnsafeBytes(key)) + + return nil + }) + + return value, err +} + +// Set key with value +func (s *Storage) Set(key string, value []byte, exp time.Duration) error { + if len(key) <= 0 || len(value) <= 0 { + return nil + } + + return s.conn.Update(func(tx *bbolt.Tx) error { + b := tx.Bucket(utils.UnsafeBytes(s.bucket)) + + return b.Put(utils.UnsafeBytes(key), value) + }) +} + +// Delete entry by key +func (s *Storage) Delete(key string) error { + if len(key) <= 0 { + return nil + } + + return s.conn.Update(func(tx *bbolt.Tx) error { + b := tx.Bucket(utils.UnsafeBytes(s.bucket)) + + return b.Delete(utils.UnsafeBytes(key)) + }) +} + +// Reset all entries +func (s *Storage) Reset() error { + return s.conn.Update(func(tx *bbolt.Tx) error { + b := tx.Bucket(utils.UnsafeBytes(s.bucket)) + + return b.ForEach(func(k, _ []byte) error { + return b.Delete(k) + }) + }) +} + +// Close the database +func (s *Storage) Close() error { + return s.conn.Close() +} diff --git a/bbolt/bbolt_test.go b/bbolt/bbolt_test.go new file mode 100644 index 00000000..f703a93a --- /dev/null +++ b/bbolt/bbolt_test.go @@ -0,0 +1,97 @@ +package bbolt + +import ( + "testing" + + "github.com/gofiber/utils" +) + +var testStore = New() + +func Test_Bbolt_Set(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) +} + +func Test_Bbolt_Set_Override(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) +} + +func Test_Bbolt_Get(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, val, result) +} + +func Test_Bbolt_Get_NotExist(t *testing.T) { + + result, err := testStore.Get("notexist") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_Bbolt_Delete(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Delete(key) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_Bbolt_Reset(t *testing.T) { + var ( + val = []byte("doe") + ) + + err := testStore.Set("john1", val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Set("john2", val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Reset() + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get("john1") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) + + result, err = testStore.Get("john2") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_Bbolt_Close(t *testing.T) { + utils.AssertEqual(t, nil, testStore.Close()) +} diff --git a/bbolt/config.go b/bbolt/config.go new file mode 100644 index 00000000..5cf21ddb --- /dev/null +++ b/bbolt/config.go @@ -0,0 +1,63 @@ +package bbolt + +import "time" + +// Config defines the config for storage. +type Config struct { + // Database path + // + // Optional. Default is "fiber.db" + Database string + + // Bbolt bucket name + // + // Optional. Default is "fiber_storage" + Bucket string + + // Timeout is the amount of time to wait to obtain a file lock. + // Only available on Darwin and Linux. + // + // Optional. Default is 0 (no timeout) + Timeout time.Duration + + // Open database in read-only mode. + // + // Optional. Default is false + ReadOnly bool + + // Reset clears any existing keys in existing Bucket + // + // Optional. Default is false + Reset bool +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Database: "fiber.db", + Bucket: "fiber_storage", + Timeout: 0, + ReadOnly: false, + Reset: false, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Database == "" { + cfg.Database = ConfigDefault.Database + } + + if cfg.Bucket == "" { + cfg.Bucket = ConfigDefault.Bucket + } + + return cfg +} diff --git a/bbolt/go.mod b/bbolt/go.mod new file mode 100644 index 00000000..65153a36 --- /dev/null +++ b/bbolt/go.mod @@ -0,0 +1,10 @@ +module github.com/gofiber/storage/bbolt + +go 1.16 + +require ( + github.com/gofiber/utils v0.1.2 + go.etcd.io/bbolt v1.3.6 +) + +require golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a // indirect diff --git a/bbolt/go.sum b/bbolt/go.sum new file mode 100644 index 00000000..a4e257fa --- /dev/null +++ b/bbolt/go.sum @@ -0,0 +1,7 @@ +github.com/gofiber/utils v0.1.2 h1:1SH2YEz4RlNS0tJlMJ0bGwO0JkqPqvq6TbHK9tXZKtk= +github.com/gofiber/utils v0.1.2/go.mod h1:pacRFtghAE3UoknMOUiXh2Io/nLWSUHtQCi/3QASsOc= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a h1:ppl5mZgokTT8uPkmYOyEUmPTr3ypaKkg5eFOGrAmxxE= +golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/bbolt/utils.go b/bbolt/utils.go new file mode 100644 index 00000000..f920dd05 --- /dev/null +++ b/bbolt/utils.go @@ -0,0 +1,20 @@ +package bbolt + +import ( + "github.com/gofiber/utils" + "go.etcd.io/bbolt" +) + +func createBucket(cfg Config, conn *bbolt.DB) error { + return conn.Update(func(tx *bbolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(utils.UnsafeBytes(cfg.Bucket)) + + return err + }) +} + +func removeBucket(cfg Config, conn *bbolt.DB) error { + return conn.Update(func(tx *bbolt.Tx) error { + return tx.DeleteBucket(utils.UnsafeBytes(cfg.Bucket)) + }) +} diff --git a/dynamodb/README.md b/dynamodb/README.md index d905ed73..a7ee9b75 100644 --- a/dynamodb/README.md +++ b/dynamodb/README.md @@ -1,4 +1,7 @@ -# ⚠ DynamoDB is still in development, do not use in production! +# DynamoDB +A DynamoDB storage driver using [aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2). + +**Note:** If config fields of credentials not given, credentials are using from the environment variables, ~/.aws/credentials, or EC2 instance role. If config fields of credentials given, credentials are using from config. Look at: [specifying credentials](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials) .... @@ -60,28 +63,70 @@ type Config struct { // Optional ("fiber_storage" by default). Table string - // AWS access key ID (part of the credentials). - // Optional (read from shared credentials file or environment variable if not set). - // Environment variable: "AWS_ACCESS_KEY_ID". - AWSaccessKeyID string - - // AWS secret access key (part of the credentials). - // Optional (read from shared credentials file or environment variable if not set). - // Environment variable: "AWS_SECRET_ACCESS_KEY". - AWSsecretAccessKey string - // CustomEndpoint allows you to set a custom DynamoDB service endpoint. // This is especially useful if you're running a "DynamoDB local" Docker container for local testing. // Typical value for the Docker container: "http://localhost:8000". // See https://hub.docker.com/r/amazon/dynamodb-local/. // Optional ("" by default) - CustomEndpoint string + Endpoint string + + // Credentials overrides AWS access key and AWS secret access key. Not recommended. + // + // Optional. Default is Credentials{} + Credentials Credentials + + // The maximum number of times requests that encounter retryable failures should be attempted. + // + // Optional. Default is 3 + MaxAttempts int + + // Reset clears any existing keys in existing Bucket + // + // Optional. Default is false + Reset bool + + // ReadCapacityUnits of the table. + // Only required when the table doesn't exist yet and is created by gokv. + // Optional (5 by default, which is the same default value as when creating a table in the web console) + // 25 RCUs are included in the free tier (across all tables). + // For example calculations, see https://github.com/awsdocs/amazon-dynamodb-developer-guide/blob/c420420a59040c5b3dd44a6e59f7c9e55fc922ef/doc_source/HowItWorks.ProvisionedThroughput. + // For limits, see https://github.com/awsdocs/amazon-dynamodb-developer-guide/blob/c420420a59040c5b3dd44a6e59f7c9e55fc922ef/doc_source/Limits.md#capacity-units-and-provisioned-throughput.md#provisioned-throughput. + ReadCapacityUnits int64 + + // ReadCapacityUnits of the table. + // Only required when the table doesn't exist yet and is created by gokv. + // Optional (5 by default, which is the same default value as when creating a table in the web console) + // 25 RCUs are included in the free tier (across all tables). + // For example calculations, see https://github.com/awsdocs/amazon-dynamodb-developer-guide/blob/c420420a59040c5b3dd44a6e59f7c9e55fc922ef/doc_source/HowItWorks.ProvisionedThroughput. + // For limits, see https://github.com/awsdocs/amazon-dynamodb-developer-guide/blob/c420420a59040c5b3dd44a6e59f7c9e55fc922ef/doc_source/Limits.md#capacity-units-and-provisioned-throughput.md#provisioned-throughput. + WriteCapacityUnits int64 + + // If the table doesn't exist yet, gokv creates it. + // If WaitForTableCreation is true, gokv will block until the table is created, with a timeout of 15 seconds. + // If the table still doesn't exist after 15 seconds, an error is returned. + // If WaitForTableCreation is false, gokv returns the client immediately. + // In the latter case you need to make sure that you don't read from or write to the table before it's created, + // because otherwise you will get ResourceNotFoundException errors. + // Optional (true by default). + WaitForTableCreation *bool } + +type Credentials struct { + AccessKey string + SecretAccessKey string +} + ``` ### Default Config ```go var ConfigDefault = Config{ - Table: "fiber_storage", + Table: "fiber_storage", + Credentials: Credentials{}, + MaxAttempts: 3, + Reset: false, + ReadCapacityUnits: 5, + WriteCapacityUnits: 5, + WaitForTableCreation: aws.Bool(true), } ``` diff --git a/dynamodb/config.go b/dynamodb/config.go index e28f656a..07375cae 100644 --- a/dynamodb/config.go +++ b/dynamodb/config.go @@ -1,6 +1,6 @@ package dynamodb -import "github.com/aws/aws-sdk-go/aws" +import "github.com/aws/aws-sdk-go-v2/aws" // Config defines the config for storage. type Config struct { @@ -15,22 +15,27 @@ type Config struct { // Optional ("fiber_storage" by default). Table string - // AWS access key ID (part of the credentials). - // Optional (read from shared credentials file or environment variable if not set). - // Environment variable: "AWS_ACCESS_KEY_ID". - AWSaccessKeyID string - - // AWS secret access key (part of the credentials). - // Optional (read from shared credentials file or environment variable if not set). - // Environment variable: "AWS_SECRET_ACCESS_KEY". - AWSsecretAccessKey string - // CustomEndpoint allows you to set a custom DynamoDB service endpoint. // This is especially useful if you're running a "DynamoDB local" Docker container for local testing. // Typical value for the Docker container: "http://localhost:8000". // See https://hub.docker.com/r/amazon/dynamodb-local/. // Optional ("" by default) - CustomEndpoint string + Endpoint string + + // Credentials overrides AWS access key and AWS secret access key. Not recommended. + // + // Optional. Default is Credentials{} + Credentials Credentials + + // The maximum number of times requests that encounter retryable failures should be attempted. + // + // Optional. Default is 3 + MaxAttempts int + + // Reset clears any existing keys in existing Bucket + // + // Optional. Default is false + Reset bool // ReadCapacityUnits of the table. // Only required when the table doesn't exist yet and is created by gokv. @@ -38,14 +43,16 @@ type Config struct { // 25 RCUs are included in the free tier (across all tables). // For example calculations, see https://github.com/awsdocs/amazon-dynamodb-developer-guide/blob/c420420a59040c5b3dd44a6e59f7c9e55fc922ef/doc_source/HowItWorks.ProvisionedThroughput. // For limits, see https://github.com/awsdocs/amazon-dynamodb-developer-guide/blob/c420420a59040c5b3dd44a6e59f7c9e55fc922ef/doc_source/Limits.md#capacity-units-and-provisioned-throughput.md#provisioned-throughput. - readCapacityUnits int64 + ReadCapacityUnits int64 + // ReadCapacityUnits of the table. // Only required when the table doesn't exist yet and is created by gokv. // Optional (5 by default, which is the same default value as when creating a table in the web console) // 25 RCUs are included in the free tier (across all tables). // For example calculations, see https://github.com/awsdocs/amazon-dynamodb-developer-guide/blob/c420420a59040c5b3dd44a6e59f7c9e55fc922ef/doc_source/HowItWorks.ProvisionedThroughput. // For limits, see https://github.com/awsdocs/amazon-dynamodb-developer-guide/blob/c420420a59040c5b3dd44a6e59f7c9e55fc922ef/doc_source/Limits.md#capacity-units-and-provisioned-throughput.md#provisioned-throughput. - writeCapacityUnits int64 + WriteCapacityUnits int64 + // If the table doesn't exist yet, gokv creates it. // If WaitForTableCreation is true, gokv will block until the table is created, with a timeout of 15 seconds. // If the table still doesn't exist after 15 seconds, an error is returned. @@ -53,15 +60,23 @@ type Config struct { // In the latter case you need to make sure that you don't read from or write to the table before it's created, // because otherwise you will get ResourceNotFoundException errors. // Optional (true by default). - waitForTableCreation *bool + WaitForTableCreation *bool +} + +type Credentials struct { + AccessKey string + SecretAccessKey string } // ConfigDefault is the default config var ConfigDefault = Config{ Table: "fiber_storage", - readCapacityUnits: 5, - writeCapacityUnits: 5, - waitForTableCreation: aws.Bool(true), + Credentials: Credentials{}, + MaxAttempts: 3, + Reset: false, + ReadCapacityUnits: 5, + WriteCapacityUnits: 5, + WaitForTableCreation: aws.Bool(true), } // configDefault is a helper function to set default values @@ -78,5 +93,18 @@ func configDefault(config ...Config) Config { if cfg.Table == "" { cfg.Table = ConfigDefault.Table } + if cfg.MaxAttempts == 0 { + cfg.MaxAttempts = ConfigDefault.MaxAttempts + } + if cfg.ReadCapacityUnits == 0 { + cfg.ReadCapacityUnits = ConfigDefault.ReadCapacityUnits + } + if cfg.WriteCapacityUnits == 0 { + cfg.WriteCapacityUnits = ConfigDefault.WriteCapacityUnits + } + if cfg.WaitForTableCreation == nil { + cfg.WaitForTableCreation = ConfigDefault.WaitForTableCreation + } + return cfg } diff --git a/dynamodb/dynamodb.go b/dynamodb/dynamodb.go index e4ec9229..fa25bb71 100644 --- a/dynamodb/dynamodb.go +++ b/dynamodb/dynamodb.go @@ -3,90 +3,23 @@ package dynamodb import ( "context" "errors" + "fmt" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - awsdynamodb "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/retry" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" + awsdynamodb "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" ) // Storage interface that is implemented by storage providers type Storage struct { - db *awsdynamodb.DynamoDB - table string -} - -// New creates a new storage -func New(config Config) *Storage { - // Set default config - cfg := configDefault(config) - - // Create db - var creds *credentials.Credentials - if (cfg.AWSaccessKeyID != "" && cfg.AWSsecretAccessKey == "") || (cfg.AWSaccessKeyID == "" && cfg.AWSsecretAccessKey != "") { - panic("[DynamoDB] You need to set BOTH AWSaccessKeyID AND AWSsecretAccessKey") - } else if cfg.AWSaccessKeyID != "" { - // Due to the previous check we can be sure that in this case AWSsecretAccessKey is not empty as well. - creds = credentials.NewStaticCredentials(cfg.AWSaccessKeyID, cfg.AWSsecretAccessKey, "") - } - - // Set database options - opt := aws.NewConfig() - if cfg.Region != "" { - opt = opt.WithRegion(cfg.Region) - } - if creds != nil { - opt = opt.WithCredentials(creds) - } - if cfg.CustomEndpoint != "" { - opt = opt.WithEndpoint(cfg.CustomEndpoint) - } - - sessionOpt := session.Options{ - SharedConfigState: session.SharedConfigEnable, - } - - // ...but allow overwrite of region and credentials if they are set in the options. - sessionOpt.Config.MergeIn(opt) - session, err := session.NewSessionWithOptions(sessionOpt) - if err != nil { - panic(err) - } - svc := awsdynamodb.New(session) - - timeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - describeTableInput := awsdynamodb.DescribeTableInput{ - TableName: &cfg.Table, - } - - _, err = svc.DescribeTableWithContext(timeoutCtx, &describeTableInput) - if err != nil { - awsErr, ok := err.(awserr.Error) - if !ok { - panic(err) - } else if awsErr.Code() == awsdynamodb.ErrCodeResourceNotFoundException { - err = createTable(cfg.Table, cfg.readCapacityUnits, cfg.writeCapacityUnits, *cfg.waitForTableCreation, describeTableInput, svc) - if err != nil { - panic(err) - } - } else { - panic(err) - } - } - - // Create storage - store := &Storage{ - db: svc, - table: cfg.Table, - } - - // Start garbage collector - //go store.gc() - - return store + db *awsdynamodb.Client + table string + requestTimeout time.Duration } // "k" is used as table column name for the key. @@ -95,126 +28,182 @@ var keyAttrName = "k" // "v" is used as table column name for the value. var valAttrName = "v" +type table struct { + K string + V []byte +} + +// New creates a new storage +func New(config Config) *Storage { + // Set default config + cfg := configDefault(config) + + awscfg, err := returnAWSConfig(cfg) + if err != nil { + panic(fmt.Sprintf("unable to load SDK config, %v", err)) + } + + // Create db + sess := awsdynamodb.NewFromConfig(awscfg) + + timeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + describeTableInput := awsdynamodb.DescribeTableInput{ + TableName: &cfg.Table, + } + + // Create storage + store := &Storage{ + db: sess, + table: cfg.Table, + } + + // Create table + _, err = sess.DescribeTable(timeoutCtx, &describeTableInput) + if err != nil { + var rnfe *types.ResourceNotFoundException + if errors.As(err, &rnfe) { + err := store.createTable(cfg, describeTableInput) + if err != nil { + panic(err) + } + } else { + panic(err) + } + } + + return store +} + // Get value by key func (s *Storage) Get(key string) ([]byte, error) { - k := make(map[string]*awsdynamodb.AttributeValue) - k[keyAttrName] = &awsdynamodb.AttributeValue{ - S: &key, + ctx, cancel := s.requestContext() + defer cancel() + + k := make(map[string]types.AttributeValue) + k[keyAttrName] = &types.AttributeValueMemberS{ + Value: key, } getItemInput := awsdynamodb.GetItemInput{ TableName: &s.table, Key: k, } - getItemOutput, err := s.db.GetItem(&getItemInput) + getItemOutput, err := s.db.GetItem(ctx, &getItemInput) if err != nil { + var rnfe *types.ResourceNotFoundException + if errors.As(err, &rnfe) { + return nil, nil + } + return nil, err } else if getItemOutput.Item == nil { return nil, nil } - attributeVal := getItemOutput.Item[valAttrName] - if attributeVal == nil { - return nil, nil - } - return attributeVal.B, nil + + item := &table{} + err = attributevalue.UnmarshalMap(getItemOutput.Item, &item) + + return item.V, err } -// Set key with value // Set key with value func (s *Storage) Set(key string, val []byte, exp time.Duration) error { + ctx, cancel := s.requestContext() + defer cancel() + // Ain't Nobody Got Time For That if len(key) <= 0 || len(val) <= 0 { return nil } - item := make(map[string]*awsdynamodb.AttributeValue) - item[keyAttrName] = &awsdynamodb.AttributeValue{ - S: &key, + + item := make(map[string]types.AttributeValue) + item[keyAttrName] = &types.AttributeValueMemberS{ + Value: key, } - item[valAttrName] = &awsdynamodb.AttributeValue{ - B: val, + item[valAttrName] = &types.AttributeValueMemberB{ + Value: val, } putItemInput := awsdynamodb.PutItemInput{ TableName: &s.table, Item: item, } - _, err := s.db.PutItem(&putItemInput) + + _, err := s.db.PutItem(ctx, &putItemInput) return err } // Delete entry by key func (s *Storage) Delete(key string) error { + ctx, cancel := s.requestContext() + defer cancel() + // Ain't Nobody Got Time For That if len(key) <= 0 { return nil } - k := make(map[string]*awsdynamodb.AttributeValue) - k[keyAttrName] = &awsdynamodb.AttributeValue{ - S: &key, + + k := make(map[string]types.AttributeValue) + k[keyAttrName] = &types.AttributeValueMemberS{ + Value: key, } deleteItemInput := awsdynamodb.DeleteItemInput{ TableName: &s.table, Key: k, } - _, err := s.db.DeleteItem(&deleteItemInput) + + _, err := s.db.DeleteItem(ctx, &deleteItemInput) return err } // Reset all entries, including unexpired func (s *Storage) Reset() error { + ctx, cancel := s.requestContext() + defer cancel() + deleteTableInput := awsdynamodb.DeleteTableInput{ TableName: &s.table, } - _, err := s.db.DeleteTable(&deleteTableInput) + _, err := s.db.DeleteTable(ctx, &deleteTableInput) return err } // Close the database func (s *Storage) Close() error { - // In the DynamoDB implementation this doesn't have any effect. return nil } -// GC deletes all expired entries -// func (s *Storage) gc() { -// ticker := time.NewTicker(s.gcInterval) -// defer ticker.Stop() -// for { -// select { -// case <-s.done: -// return -// case t := <-ticker.C: -// _, _ = s.db.Exec(s.sqlGC, t.Unix()) -// } -// } -// } +func (s *Storage) createTable(cfg Config, describeTableInput awsdynamodb.DescribeTableInput) error { + ctx, cancel := s.requestContext() + defer cancel() -func createTable(tableName string, readCapacityUnits, writeCapacityUnits int64, waitForTableCreation bool, describeTableInput awsdynamodb.DescribeTableInput, svc *awsdynamodb.DynamoDB) error { keyAttrType := "S" // For "string" keyType := "HASH" // As opposed to "RANGE" + createTableInput := awsdynamodb.CreateTableInput{ - TableName: &tableName, - AttributeDefinitions: []*awsdynamodb.AttributeDefinition{{ + TableName: &s.table, + AttributeDefinitions: []types.AttributeDefinition{{ AttributeName: &keyAttrName, - AttributeType: &keyAttrType, + AttributeType: types.ScalarAttributeType(keyAttrType), }}, - KeySchema: []*awsdynamodb.KeySchemaElement{{ + KeySchema: []types.KeySchemaElement{{ AttributeName: &keyAttrName, - KeyType: &keyType, + KeyType: types.KeyType(keyType), }}, - ProvisionedThroughput: &awsdynamodb.ProvisionedThroughput{ - ReadCapacityUnits: &readCapacityUnits, - WriteCapacityUnits: &writeCapacityUnits, + ProvisionedThroughput: &types.ProvisionedThroughput{ + ReadCapacityUnits: &cfg.ReadCapacityUnits, + WriteCapacityUnits: &cfg.WriteCapacityUnits, }, } - _, err := svc.CreateTable(&createTableInput) + _, err := s.db.CreateTable(ctx, &createTableInput) if err != nil { return err } // If configured (true by default), block until the table is created. // Typical table creation duration is 10 seconds. - if waitForTableCreation { + if *cfg.WaitForTableCreation { for try := 1; try < 16; try++ { - describeTableOutput, err := svc.DescribeTable(&describeTableInput) - if err != nil || *describeTableOutput.Table.TableStatus == "CREATING" { + describeTableOutput, err := s.db.DescribeTable(ctx, &describeTableInput) + if err != nil || describeTableOutput.Table.TableStatus == "CREATING" { time.Sleep(1 * time.Second) } else { break @@ -222,14 +211,56 @@ func createTable(tableName string, readCapacityUnits, writeCapacityUnits int64, } // Last try (16th) after 15 seconds of waiting. // Now handle error as such. - describeTableOutput, err := svc.DescribeTable(&describeTableInput) + describeTableOutput, err := s.db.DescribeTable(ctx, &describeTableInput) if err != nil { - return errors.New("The DynamoDB table couldn't be created") + return errors.New("dynamodb: the table couldn't be created") } - if *describeTableOutput.Table.TableStatus == "CREATING" { - return errors.New("The DynamoDB table took too long to be created") + if describeTableOutput.Table.TableStatus == "CREATING" { + return errors.New("dynamodb: the table took too long to be created") } } return nil } + +// Context for making requests will timeout if a non-zero timeout is configured +func (s *Storage) requestContext() (context.Context, context.CancelFunc) { + if s.requestTimeout > 0 { + return context.WithTimeout(context.Background(), s.requestTimeout) + } + return context.Background(), func() {} +} + +func returnAWSConfig(cfg Config) (aws.Config, error) { + endpoint := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { + if cfg.Endpoint != "" { + return aws.Endpoint{ + PartitionID: "aws", + URL: cfg.Endpoint, + SigningRegion: cfg.Region, + HostnameImmutable: true, + }, nil + } + return aws.Endpoint{}, &aws.EndpointNotFoundError{} + }) + + if cfg.Credentials != (Credentials{}) { + credentials := credentials.NewStaticCredentialsProvider(cfg.Credentials.AccessKey, cfg.Credentials.SecretAccessKey, "") + return awsconfig.LoadDefaultConfig(context.TODO(), + awsconfig.WithRegion(cfg.Region), + awsconfig.WithEndpointResolverWithOptions(endpoint), + awsconfig.WithCredentialsProvider(credentials), + awsconfig.WithRetryer(func() aws.Retryer { + return retry.AddWithMaxAttempts(retry.NewStandard(), cfg.MaxAttempts) + }), + ) + } + + return awsconfig.LoadDefaultConfig(context.TODO(), + awsconfig.WithRegion(cfg.Region), + awsconfig.WithEndpointResolverWithOptions(endpoint), + awsconfig.WithRetryer(func() aws.Retryer { + return retry.AddWithMaxAttempts(retry.NewStandard(), cfg.MaxAttempts) + }), + ) +} diff --git a/dynamodb/dynamodb_test.go b/dynamodb/dynamodb_test.go index eeacc196..d70278a2 100644 --- a/dynamodb/dynamodb_test.go +++ b/dynamodb/dynamodb_test.go @@ -1 +1,107 @@ package dynamodb + +import ( + "testing" + + "github.com/gofiber/utils" +) + +var testStore = New( + Config{ + Table: "fiber_storage", + Endpoint: "http://localhost:8000/", + Region: "us-east-1", + Credentials: Credentials{ + AccessKey: "dummy", + SecretAccessKey: "dummy", + }, + }, +) + +func Test_DynamoDB_Set(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) +} + +func Test_DynamoDB_Set_Override(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) +} + +func Test_DynamoDB_Get(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, val, result) +} + +func Test_DynamoDB_Get_NotExist(t *testing.T) { + + result, err := testStore.Get("notexist") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_DynamoDB_Delete(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Delete(key) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_DynamoDB_Reset(t *testing.T) { + var ( + val = []byte("doe") + ) + + err := testStore.Set("john1", val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Set("john2", val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Reset() + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get("john1") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) + + result, err = testStore.Get("john2") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_DynamoDB_Close(t *testing.T) { + utils.AssertEqual(t, nil, testStore.Close()) +} diff --git a/dynamodb/go.mod b/dynamodb/go.mod index 801166b2..f711eba6 100644 --- a/dynamodb/go.mod +++ b/dynamodb/go.mod @@ -2,4 +2,11 @@ module github.com/gofiber/storage/dynamodb go 1.14 -require github.com/aws/aws-sdk-go v1.42.46 +require ( + github.com/aws/aws-sdk-go-v2 v1.16.3 + github.com/aws/aws-sdk-go-v2/config v1.15.4 + github.com/aws/aws-sdk-go-v2/credentials v1.12.0 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.9.1 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.4 + github.com/gofiber/utils v0.1.2 +) diff --git a/dynamodb/go.sum b/dynamodb/go.sum index 3ee04b05..a9e87c44 100644 --- a/dynamodb/go.sum +++ b/dynamodb/go.sum @@ -1,23 +1,50 @@ -github.com/aws/aws-sdk-go v1.42.46 h1:Uehqm39VwQ+t0T7PeoFfsT1SjYRmazuTd9LMdN1JszE= -github.com/aws/aws-sdk-go v1.42.46/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= +github.com/aws/aws-sdk-go-v2 v1.16.3 h1:0W1TSJ7O6OzwuEvIXAtJGvOeQ0SGAhcpxPN2/NK5EhM= +github.com/aws/aws-sdk-go-v2 v1.16.3/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= +github.com/aws/aws-sdk-go-v2/config v1.15.4 h1:P4mesY1hYUxru4f9SU0XxNKXmzfxsD0FtMIPRBjkH7Q= +github.com/aws/aws-sdk-go-v2/config v1.15.4/go.mod h1:ZijHHh0xd/A+ZY53az0qzC5tT46kt4JVCePf2NX9Lk4= +github.com/aws/aws-sdk-go-v2/credentials v1.12.0 h1:4R/NqlcRFSkR0wxOhgHi+agGpbEr5qMCjn7VqUIJY+E= +github.com/aws/aws-sdk-go-v2/credentials v1.12.0/go.mod h1:9YWk7VW+eyKsoIL6/CljkTrNVWBSK9pkqOPUuijid4A= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.9.1 h1:W5OvMA6XTRXs/voHKPOCSVyzhV07GzHKn5GKTDzjKx0= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.9.1/go.mod h1:47xITY/Q+OIf25Z5Z3EbJkG2WxCllBjKxreRmJECDMI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.4 h1:FP8gquGeGHHdfY6G5llaMQDF+HAf20VKc8opRwmjf04= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.4/go.mod h1:u/s5/Z+ohUQOPXl00m2yJVyioWDECsbpXTQlaqSlufc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.10 h1:uFWgo6mGJI1n17nbcvSc6fxVuR3xLNqvXt12JCnEcT8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.10/go.mod h1:F+EZtuIwjlv35kRJPyBGcsA4f7bnSoz15zOQ2lJq1Z4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.4 h1:cnsvEKSoHN4oAN7spMMr0zhEW2MHnhAVpmqQg8E6UcM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.4/go.mod h1:8glyUqVIM4AmeenIsPo0oVh3+NUwnsQml2OFupfQW+0= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.11 h1:6cZRymlLEIlDTEB0+5+An6Zj1CKt6rSE69tOmFeu1nk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.11/go.mod h1:0MR+sS1b/yxsfAPvAESrw8NfwUoxMinDyw6EYR9BS2U= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.4 h1:M65DLU8yF7OT8h66B5ULgCdqDx3aq6KZTB2viHozSyM= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.4/go.mod h1:lBz+dFsiLZcTCnIdWKUmNQLGX4CidaQqb706AIJ652M= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.4 h1:q8C+UWoUI/PWVy/qaA8anr8rNeqdQKmVKN6x8zpj+6o= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.13.4/go.mod h1:Ldxp5sLfT8Is7fZOIqTJ8oaVoDo+Rxu0xAYhZqnN6y8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1 h1:T4pFel53bkHjL2mMo+4DKE6r6AuoZnM0fg7k1/ratr4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.4 h1:kkIspXTzCx1Mo8sF/UrzGkb5FmUsAnRy09DCjOKO03g= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.4/go.mod h1:EjdPGnmBHOi9ieyuR9ck5Nguyb32/fdjoxDPVrYWYAA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.4 h1:b16QW0XWl0jWjLABFc1A+uh145Oqv+xDcObNk0iQgUk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.4/go.mod h1:uKkN7qmSIsNJVyMtxNQoCEYMvFEXbOg9fwCJPdfp2u8= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.4 h1:Uw5wBybFQ1UeA9ts0Y07gbv0ncZnIAyw858tDW0NP2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.11.4/go.mod h1:cPDwJwsP4Kff9mldCXAmddjJL6JGQqtA3Mzer2zyr88= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.4 h1:+xtV90n3abQmgzk1pS++FdxZTrPEDgQng6e4/56WR2A= +github.com/aws/aws-sdk-go-v2/service/sts v1.16.4/go.mod h1:lfSYenAXtavyX2A1LsViglqlG9eEFYxNryTZS5rn3QE= +github.com/aws/smithy-go v1.11.2 h1:eG/N+CcUMAvsdffgMvjMKwfyDzIkjM6pfxMJ8Mzc6mE= +github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gofiber/utils v0.1.2 h1:1SH2YEz4RlNS0tJlMJ0bGwO0JkqPqvq6TbHK9tXZKtk= +github.com/gofiber/utils v0.1.2/go.mod h1:pacRFtghAE3UoknMOUiXh2Io/nLWSUHtQCi/3QASsOc= +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= diff --git a/mongodb/go.mod b/mongodb/go.mod index e994783e..9baf543f 100644 --- a/mongodb/go.mod +++ b/mongodb/go.mod @@ -7,7 +7,7 @@ require ( github.com/gofiber/utils v0.1.2 github.com/golang/snappy v0.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect - go.mongodb.org/mongo-driver v1.8.3 + go.mongodb.org/mongo-driver v1.9.1 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/text v0.3.7 // indirect diff --git a/mongodb/go.sum b/mongodb/go.sum index 364da9cc..b3a6a259 100644 --- a/mongodb/go.sum +++ b/mongodb/go.sum @@ -37,8 +37,8 @@ github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= -go.mongodb.org/mongo-driver v1.8.3 h1:TDKlTkGDKm9kkJVUOAXDK5/fkqKHJVwYQSpoRfB43R4= -go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= +go.mongodb.org/mongo-driver v1.9.1 h1:m078y9v7sBItkt1aaoe2YlvWEXcD263e1a4E1fBrJ1c= +go.mongodb.org/mongo-driver v1.9.1/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= diff --git a/postgres/go.mod b/postgres/go.mod index 0e804cdd..b7575326 100644 --- a/postgres/go.mod +++ b/postgres/go.mod @@ -14,4 +14,5 @@ require ( golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect + github.com/lib/pq v1.10.5 ) diff --git a/postgres/go.sum b/postgres/go.sum index f63e2577..bd4ad919 100644 --- a/postgres/go.sum +++ b/postgres/go.sum @@ -200,3 +200,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= +github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= \ No newline at end of file diff --git a/postgres/postgres.go b/postgres/postgres.go index ac0dc6b9..37f96117 100644 --- a/postgres/postgres.go +++ b/postgres/postgres.go @@ -56,12 +56,16 @@ func New(config ...Config) *Storage { if cfg.Username != "" || cfg.Password != "" { dsn += "@" } - dsn += fmt.Sprintf("%s:%d", url.QueryEscape(cfg.Host), cfg.Port) + // unix socket host path + if strings.HasPrefix(cfg.Host, "/") { + dsn += fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) + } else { + dsn += fmt.Sprintf("%s:%d", url.QueryEscape(cfg.Host), cfg.Port) + } dsn += fmt.Sprintf("/%s?connect_timeout=%d&sslmode=%s", url.QueryEscape(cfg.Database), int64(cfg.timeout.Seconds()), - cfg.SslMode, - ) + cfg.SslMode) // Create db db, err := sql.Open("postgres", dsn) diff --git a/redis/README.md b/redis/README.md index 4bedeca8..16f12bbc 100644 --- a/redis/README.md +++ b/redis/README.md @@ -68,7 +68,7 @@ type Config struct { // Port where the DB is listening on // - // Optional. Default is 3306 + // Optional. Default is 6379 Port int // Server username diff --git a/redis/config.go b/redis/config.go index 4734e22b..1a0876d6 100644 --- a/redis/config.go +++ b/redis/config.go @@ -11,7 +11,7 @@ type Config struct { // Port where the DB is listening on // - // Optional. Default is 3306 + // Optional. Default is 6379 Port int // Server username diff --git a/redis/go.mod b/redis/go.mod index ce4dae19..60d30a4f 100644 --- a/redis/go.mod +++ b/redis/go.mod @@ -3,6 +3,7 @@ module github.com/gofiber/storage/redis go 1.14 require ( - github.com/go-redis/redis/v8 v8.11.4 + github.com/go-redis/redis/v8 v8.11.5 github.com/gofiber/utils v0.1.2 + github.com/google/go-cmp v0.5.6 // indirect ) diff --git a/redis/go.sum b/redis/go.sum index 00141442..56028444 100644 --- a/redis/go.sum +++ b/redis/go.sum @@ -1,5 +1,8 @@ github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -7,8 +10,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= -github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gofiber/utils v0.1.2 h1:1SH2YEz4RlNS0tJlMJ0bGwO0JkqPqvq6TbHK9tXZKtk= github.com/gofiber/utils v0.1.2/go.mod h1:pacRFtghAE3UoknMOUiXh2Io/nLWSUHtQCi/3QASsOc= @@ -28,18 +31,24 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= +github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= -github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -64,12 +73,14 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/ristretto/ristretto_test.go b/ristretto/ristretto_test.go index 807ba067..b424273b 100644 --- a/ristretto/ristretto_test.go +++ b/ristretto/ristretto_test.go @@ -42,6 +42,9 @@ func Test_Ristretto_Get(t *testing.T) { err := testStore.Set(key, val, 0) utils.AssertEqual(t, nil, err) + // stabilize with some delay in between -> bug already communicated + time.Sleep(10000) + result, err := testStore.Get(key) utils.AssertEqual(t, nil, err) utils.AssertEqual(t, val, result) @@ -86,6 +89,9 @@ func Test_Ristretto_Delete(t *testing.T) { err := testStore.Set(key, val, 0) utils.AssertEqual(t, nil, err) + // stabilize with some delay in between -> bug already communicated + time.Sleep(10000) + err = testStore.Delete(key) utils.AssertEqual(t, nil, err) diff --git a/s3/README.md b/s3/README.md new file mode 100644 index 00000000..78bce204 --- /dev/null +++ b/s3/README.md @@ -0,0 +1,108 @@ +# S3 + +A S3 storage driver using [aws/aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2). + +**Note:** If config fields of credentials not given, credentials are using from the environment variables, ~/.aws/credentials, or EC2 instance role. If config fields of credentials given, credentials are using from config. Look at: [specifying credentials](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials) + + +### Table of Contents +- [Signatures](#signatures) +- [Installation](#installation) +- [Examples](#examples) +- [Config](#config) +- [Default Config](#default-config) + +### Signatures +```go +func New(config ...Config) Storage +func (s *Storage) Get(key string) ([]byte, error) +func (s *Storage) Set(key string, val []byte, exp time.Duration) error +func (s *Storage) Delete(key string) error +func (s *Storage) Reset() error +func (s *Storage) Close() error +``` +### Installation +S3 is tested on the 2 last [Go versions](https://golang.org/dl/) with support for modules. So make sure to initialize one first if you didn't do that yet: +```bash +go mod init github.com// +``` +And then install the s3 implementation: +```bash +go get github.com/gofiber/storage/s3 +``` + +### Examples +Import the storage package. +```go +import "github.com/gofiber/storage/s3" +``` + +You can use the following possibilities to create a storage: +```go +// Initialize default config +store := s3.New() + +// Initialize custom config +store := s3.New(s3.Config{ + Bucket: "my-bucket-url", + Endpoint: "my-endpoint", + Region: "my-region", + Reset: false, +}) +``` + +### Config +```go +// Config defines the config for storage. +type Config struct { + // S3 bucket name + Bucket string + + // AWS endpoint + Endpoint string + + // AWS region + Region string + + // Request timeout + // + // Optional. Default is 0 (no timeout) + RequestTimeout time.Duration + + // Reset clears any existing keys in existing Bucket + // + // Optional. Default is false + Reset bool + + // Credentials overrides AWS access key and AWS secret access key. Not recommended. + // + // Optional. Default is Credentials{} + Credentials Credentials + + // The maximum number of times requests that encounter retryable failures should be attempted. + // + // Optional. Default is 3 + MaxAttempts int + +} + +type Credentials struct { + AccessKey string + SecretAccessKey string +} +``` + +### Default Config +The default configuration lacks Bucket, Region, and Endpoint which are all required and must be overwritten: +```go +// ConfigDefault is the default config +var ConfigDefault = Config{ + Bucket: "", + Region: "", + Endpoint: "", + Credentials: Credentials{}, + MaxAttempts: 3, + RequestTimeout: 0, + Reset: false, +} +``` diff --git a/s3/config.go b/s3/config.go new file mode 100644 index 00000000..fadb1777 --- /dev/null +++ b/s3/config.go @@ -0,0 +1,69 @@ +package s3 + +import "time" + +// Config defines the config for storage. +type Config struct { + // S3 bucket name + Bucket string + + // AWS endpoint + Endpoint string + + // AWS region + Region string + + // Request timeout + // + // Optional. Default is 0 (no timeout) + RequestTimeout time.Duration + + // Reset clears any existing keys in existing Bucket + // + // Optional. Default is false + Reset bool + + // Credentials overrides AWS access key and AWS secret access key. Not recommended. + // + // Optional. Default is Credentials{} + Credentials Credentials + + // The maximum number of times requests that encounter retryable failures should be attempted. + // + // Optional. Default is 3 + MaxAttempts int +} + +type Credentials struct { + AccessKey string + SecretAccessKey string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Bucket: "", + Region: "", + Endpoint: "", + Credentials: Credentials{}, + MaxAttempts: 3, + RequestTimeout: 0, + Reset: false, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Bucket == "" { + cfg.Bucket = ConfigDefault.Bucket + } + + return cfg +} diff --git a/s3/go.mod b/s3/go.mod new file mode 100644 index 00000000..ab274bf9 --- /dev/null +++ b/s3/go.mod @@ -0,0 +1,12 @@ +module github.com/gofiber/storage/s3 + +go 1.16 + +require ( + github.com/aws/aws-sdk-go-v2 v1.13.0 + github.com/aws/aws-sdk-go-v2/config v1.13.1 + github.com/aws/aws-sdk-go-v2/credentials v1.8.0 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.9.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.24.1 + github.com/gofiber/utils v0.1.2 +) diff --git a/s3/go.sum b/s3/go.sum new file mode 100644 index 00000000..3803e843 --- /dev/null +++ b/s3/go.sum @@ -0,0 +1,51 @@ +github.com/aws/aws-sdk-go-v2 v1.13.0 h1:1XIXAfxsEmbhbj5ry3D3vX+6ZcUYvIqSm4CWWEuGZCA= +github.com/aws/aws-sdk-go-v2 v1.13.0/go.mod h1:L6+ZpqHaLbAaxsqV0L4cvxZY7QupWJB4fhkf8LXvC7w= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.2.0 h1:scBthy70MB3m4LCMFaBcmYCyR2XWOz6MxSfdSu/+fQo= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.2.0/go.mod h1:oZHzg1OVbuCiRTY0oRPM+c2HQvwnFCGJwKeSqqAJ/yM= +github.com/aws/aws-sdk-go-v2/config v1.13.1 h1:yLv8bfNoT4r+UvUKQKqRtdnvuWGMK5a82l4ru9Jvnuo= +github.com/aws/aws-sdk-go-v2/config v1.13.1/go.mod h1:Ba5Z4yL/UGbjQUzsiaN378YobhFo0MLfueXGiOsYtEs= +github.com/aws/aws-sdk-go-v2/credentials v1.8.0 h1:8Ow0WcyDesGNL0No11jcgb1JAtE+WtubqXjgxau+S0o= +github.com/aws/aws-sdk-go-v2/credentials v1.8.0/go.mod h1:gnMo58Vwx3Mu7hj1wpcG8DI0s57c9o42UQ6wgTQT5to= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 h1:NITDuUZO34mqtOwFWZiXo7yAHj7kf+XPE+EiKuCBNUI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0/go.mod h1:I6/fHT/fH460v09eg2gVrd8B/IqskhNdpcLH0WNO3QI= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.9.1 h1:oUCLhAKNaXyTqdJyw+KEjDVVBs1V5mCy8YDLMi08LL8= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.9.1/go.mod h1:pB38jI+AdaPoLAgaL9bwxDdy6rjwO6LIArBZDLjq6zs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4 h1:CRiQJ4E2RhfDdqbie1ZYDo8QtIo75Mk7oTdJSfwJTMQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.4/go.mod h1:XHgQ7Hz2WY2GAn//UXHofLfPXWh+s62MbMOijrg12Lw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0 h1:3ADoioDMOtF4uiK59vCpplpCwugEU+v4ZFD29jDL3RQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.2.0/go.mod h1:BsCSJHx5DnDXIrOcqB8KN1/B+hXLG/bi4Y6Vjcx/x9E= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5 h1:ixotxbfTCFpqbuwFv/RcZwyzhkxPSYDYEMcj4niB5Uk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.5/go.mod h1:R3sWUqPcfXSiF/LSFJhjyJmpg9uV6yP2yv3YZZjldVI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.7.0 h1:F1diQIOkNn8jcez4173r+PLPdkWK7chy74r3fKpDrLI= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.7.0/go.mod h1:8ctElVINyp+SjhoZZceUAZw78glZH6R8ox5MVNu5j2s= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0 h1:4QAOB3KrvI1ApJK14sliGr3Ie2pjyvNypn/lfzDHfUw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.7.0/go.mod h1:K/qPe6AP2TGYv4l6n7c88zh9jWBDf6nHhvg1fx/EWfU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.11.0 h1:XAe+PDnaBELHr25qaJKfB415V4CKFWE8H+prUreql8k= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.11.0/go.mod h1:RMlgnt1LbOT2BxJ3cdw+qVz7KL84714LFkWtF6sLI7A= +github.com/aws/aws-sdk-go-v2/service/s3 v1.24.1 h1:zAU2P99CLTz8kUGl+IptU2ycAXuMaLAvgIv+UH4U8pY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.24.1/go.mod h1:oIUXg/5F0x0gy6nkwEnlxZboueddwPEKO6Xl+U6/3a0= +github.com/aws/aws-sdk-go-v2/service/sso v1.9.0 h1:1qLJeQGBmNQW3mBNzK2CFmrQNmoXWrscPqsrAaU1aTA= +github.com/aws/aws-sdk-go-v2/service/sso v1.9.0/go.mod h1:vCV4glupK3tR7pw7ks7Y4jYRL86VvxS+g5qk04YeWrU= +github.com/aws/aws-sdk-go-v2/service/sts v1.14.0 h1:ksiDXhvNYg0D2/UFkLejsaz3LqpW5yjNQ8Nx9Sn2c0E= +github.com/aws/aws-sdk-go-v2/service/sts v1.14.0/go.mod h1:u0xMJKDvvfocRjiozsoZglVNXRG19043xzp3r2ivLIk= +github.com/aws/smithy-go v1.10.0 h1:gsoZQMNHnX+PaghNw4ynPsyGP7aUCqx5sY2dlPQsZ0w= +github.com/aws/smithy-go v1.10.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gofiber/utils v0.1.2 h1:1SH2YEz4RlNS0tJlMJ0bGwO0JkqPqvq6TbHK9tXZKtk= +github.com/gofiber/utils v0.1.2/go.mod h1:pacRFtghAE3UoknMOUiXh2Io/nLWSUHtQCi/3QASsOc= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/s3/s3.go b/s3/s3.go new file mode 100644 index 00000000..da328a51 --- /dev/null +++ b/s3/s3.go @@ -0,0 +1,202 @@ +package s3 + +import ( + "bytes" + "context" + "errors" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/retry" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" +) + +// Storage interface that is implemented by storage providers +type Storage struct { + svc *s3.Client + downloader *manager.Downloader + uploader *manager.Uploader + requestTimeout time.Duration + bucket string +} + +// New creates a new storage +func New(config ...Config) *Storage { + // Set default config + cfg := configDefault(config...) + + // Create s3 session + // If config fields of credentials not given, credentials are using from the environment variables, ~/.aws/credentials, or EC2 instance role. + // If config fields of credentials given, credentials are using from config. + // + // Look at: https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials + awscfg, err := returnAWSConfig(cfg) + if err != nil { + panic(fmt.Sprintf("unable to load SDK config, %v", err)) + } + + sess := s3.NewFromConfig(awscfg) + storage := &Storage{ + svc: sess, + downloader: manager.NewDownloader(sess), + uploader: manager.NewUploader(sess), + requestTimeout: cfg.RequestTimeout, + bucket: cfg.Bucket, + } + + // Reset all entries if set to true + if cfg.Reset { + if err := storage.Reset(); err != nil { + panic(err) + } + } + + return storage +} + +// Get value by key +func (s *Storage) Get(key string) ([]byte, error) { + var nsk *types.NoSuchKey + + if len(key) <= 0 { + return nil, nil + } + + ctx, cancel := s.requestContext() + defer cancel() + + buf := manager.NewWriteAtBuffer([]byte{}) + + _, err := s.downloader.Download(ctx, buf, &s3.GetObjectInput{ + Bucket: &s.bucket, + Key: aws.String(key), + }) + if errors.As(err, &nsk) { + return nil, nil + } + + return buf.Bytes(), err +} + +// Set key with value +func (s *Storage) Set(key string, val []byte, exp time.Duration) error { + if len(key) <= 0 { + return nil + } + + ctx, cancel := s.requestContext() + defer cancel() + + _, err := s.uploader.Upload(ctx, &s3.PutObjectInput{ + Bucket: &s.bucket, + Key: aws.String(key), + Body: bytes.NewReader(val), + }) + + return err +} + +// Delete entry by key +func (s *Storage) Delete(key string) error { + if len(key) <= 0 { + return nil + } + + ctx, cancel := s.requestContext() + defer cancel() + + _, err := s.svc.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &s.bucket, + Key: aws.String(key), + }) + + return err +} + +// Reset all entries, including unexpired +func (s *Storage) Reset() error { + ctx, cancel := s.requestContext() + defer cancel() + + paginator := s3.NewListObjectsV2Paginator(s.svc, &s3.ListObjectsV2Input{ + Bucket: &s.bucket, + }) + + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + return err + } + + var objects []types.ObjectIdentifier + for _, object := range page.Contents { + objects = append(objects, types.ObjectIdentifier{ + Key: object.Key, + }) + } + + _, err = s.svc.DeleteObjects(ctx, &s3.DeleteObjectsInput{ + Bucket: &s.bucket, + Delete: &types.Delete{ + Objects: objects, + }, + }) + if err != nil { + return err + } + } + + return nil +} + +// Close the database +func (s *Storage) Close() error { + return nil +} + +// Context for making requests will timeout if a non-zero timeout is configured +func (s *Storage) requestContext() (context.Context, context.CancelFunc) { + if s.requestTimeout > 0 { + return context.WithTimeout(context.Background(), s.requestTimeout) + } + return context.Background(), func() {} +} + +func returnAWSConfig(cfg Config) (aws.Config, error) { + endpoint := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { + if cfg.Endpoint != "" { + return aws.Endpoint{ + PartitionID: "aws", + URL: cfg.Endpoint, + SigningRegion: cfg.Region, + HostnameImmutable: true, + }, nil + } + return aws.Endpoint{}, &aws.EndpointNotFoundError{} + }) + + if cfg.Credentials != (Credentials{}) { + credentials := credentials.NewStaticCredentialsProvider(cfg.Credentials.AccessKey, cfg.Credentials.SecretAccessKey, "") + return awsconfig.LoadDefaultConfig(context.TODO(), + awsconfig.WithRegion(cfg.Region), + awsconfig.WithEndpointResolverWithOptions(endpoint), + awsconfig.WithCredentialsProvider(credentials), + awsconfig.WithRetryer(func() aws.Retryer { + return retry.AddWithMaxAttempts(retry.NewStandard(), cfg.MaxAttempts) + }), + ) + } + + return awsconfig.LoadDefaultConfig(context.TODO(), + awsconfig.WithRegion(cfg.Region), + awsconfig.WithEndpointResolverWithOptions(endpoint), + awsconfig.WithRetryer(func() aws.Retryer { + return retry.AddWithMaxAttempts(retry.NewStandard(), cfg.MaxAttempts) + }), + ) +} diff --git a/s3/s3_test.go b/s3/s3_test.go new file mode 100644 index 00000000..7b3f27fe --- /dev/null +++ b/s3/s3_test.go @@ -0,0 +1,107 @@ +package s3 + +import ( + "testing" + + "github.com/gofiber/utils" +) + +var testStore = New( + Config{ + Bucket: "testbucket", + Endpoint: "http://127.0.0.1:9000/", + Region: "us-east-1", + Credentials: Credentials{ + AccessKey: "minioadmin", + SecretAccessKey: "minioadmin", + }, + }, +) + +func Test_S3_Set(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) +} + +func Test_S3_Set_Override(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) +} + +func Test_S3_Get(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, val, result) +} + +func Test_S3_Get_NotExist(t *testing.T) { + + result, err := testStore.Get("notexist") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_S3_Delete(t *testing.T) { + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Delete(key) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_S3_Reset(t *testing.T) { + var ( + val = []byte("doe") + ) + + err := testStore.Set("john1", val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Set("john2", val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Reset() + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get("john1") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) + + result, err = testStore.Get("john2") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_S3_Close(t *testing.T) { + utils.AssertEqual(t, nil, testStore.Close()) +} diff --git a/sqlite3/go.mod b/sqlite3/go.mod index a09d283e..842364e9 100644 --- a/sqlite3/go.mod +++ b/sqlite3/go.mod @@ -4,5 +4,5 @@ go 1.14 require ( github.com/gofiber/utils v0.1.2 - github.com/mattn/go-sqlite3 v1.14.11 + github.com/mattn/go-sqlite3 v1.14.13 ) diff --git a/sqlite3/go.sum b/sqlite3/go.sum index 2c69d84e..7b24a1d3 100644 --- a/sqlite3/go.sum +++ b/sqlite3/go.sum @@ -1,4 +1,4 @@ github.com/gofiber/utils v0.1.2 h1:1SH2YEz4RlNS0tJlMJ0bGwO0JkqPqvq6TbHK9tXZKtk= github.com/gofiber/utils v0.1.2/go.mod h1:pacRFtghAE3UoknMOUiXh2Io/nLWSUHtQCi/3QASsOc= -github.com/mattn/go-sqlite3 v1.14.11 h1:gt+cp9c0XGqe9S/wAHTL3n/7MqY+siPWgWJgqdsFrzQ= -github.com/mattn/go-sqlite3 v1.14.11/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= +github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=