mirror of
https://github.com/gofiber/storage.git
synced 2025-10-05 08:37:10 +08:00
Merge pull request #44 from antoniodipinto/main
Added ArangoDB support for storage
This commit is contained in:
107
arangodb/README.md
Normal file
107
arangodb/README.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# ArangoDB
|
||||
A ArangoDB storage driver using `arangodb/go-driver` and [arangodb/go-driver](https://github.com/arangodb/go-driver).
|
||||
|
||||
### 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
|
||||
ArangoDB is tested on the 2 last (1.14/1.15) [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/<user>/<repo>
|
||||
```
|
||||
And then install the mysql implementation:
|
||||
```bash
|
||||
go get github.com/gofiber/storage/arangodb
|
||||
```
|
||||
|
||||
### Examples
|
||||
Import the storage package.
|
||||
```go
|
||||
import "github.com/gofiber/storage/arangodb"
|
||||
```
|
||||
|
||||
You can use the following possibilities to create a storage:
|
||||
```go
|
||||
// Initialize custom config
|
||||
// *http* is mandatory
|
||||
store := arangodb.New(arangodb.Config{
|
||||
Host: "http://127.0.0.1",
|
||||
Port: "8529",
|
||||
Username: "username",
|
||||
Password: "password"
|
||||
Database: "fiber",
|
||||
Collection: "fiber_storage",
|
||||
Reset: false,
|
||||
GCInterval: 10 * time.Second,
|
||||
})
|
||||
```
|
||||
|
||||
### Config
|
||||
```go
|
||||
type Config struct {
|
||||
// Host name where the DB is hosted
|
||||
//
|
||||
// Optional. Default is "http://127.0.0.1"
|
||||
Host string
|
||||
|
||||
// Port where the DB is listening on
|
||||
//
|
||||
// Optional. Default is 8529
|
||||
Port string
|
||||
|
||||
// Server username
|
||||
//
|
||||
// Mandatory
|
||||
Username string
|
||||
|
||||
// Server password
|
||||
//
|
||||
// Mandatory
|
||||
Password string
|
||||
|
||||
// Database name
|
||||
//
|
||||
// Optional. Default is "fiber"
|
||||
Database string
|
||||
|
||||
// Collection name
|
||||
//
|
||||
// Optional. Default is "fiber_storage"
|
||||
Collection string
|
||||
|
||||
// Reset clears any existing keys in existing collection
|
||||
//
|
||||
// Optional. Default is false
|
||||
Reset bool
|
||||
// Time before deleting expired keys
|
||||
//
|
||||
// Optional. Default is 10 * time.Second
|
||||
GCInterval time.Duration
|
||||
}
|
||||
```
|
||||
|
||||
### Default Config
|
||||
Used only for optional fields
|
||||
```go
|
||||
var ConfigDefault = Config{
|
||||
Host: "http://127.0.0.1",
|
||||
Port: "8529",
|
||||
Database: "fiber",
|
||||
Collection: "fiber_storage",
|
||||
Reset: false,
|
||||
GCInterval: 10 * time.Second,
|
||||
}
|
||||
```
|
247
arangodb/arangodb.go
Normal file
247
arangodb/arangodb.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package arangodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/arangodb/go-driver"
|
||||
"github.com/arangodb/go-driver/http"
|
||||
"github.com/gofiber/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Storage interface that is implemented by storage providers
|
||||
type Storage struct {
|
||||
db driver.Database
|
||||
gcInterval time.Duration
|
||||
done chan struct{}
|
||||
|
||||
// Arango mandatory fields
|
||||
connection driver.Connection
|
||||
client driver.Client
|
||||
collection driver.Collection
|
||||
bindingParams map[string]interface{}
|
||||
config Config
|
||||
// AQL query used to remove expired keys
|
||||
aqlRemoveGC string
|
||||
}
|
||||
|
||||
type model struct {
|
||||
// respect key format field name for ArangoDB
|
||||
Key string `json:"_key"`
|
||||
Val string `json:"val"`
|
||||
Exp int64 `json:"exp"`
|
||||
}
|
||||
|
||||
// New creates a new storage
|
||||
func New(config Config) *Storage {
|
||||
// Set default config
|
||||
cfg := configDefault(config)
|
||||
|
||||
// create connection object to arango
|
||||
conn, err := http.NewConnection(http.ConnectionConfig{
|
||||
Endpoints: []string{cfg.hostComposed()},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// instantiate client after the connection is started
|
||||
client, err := driver.NewClient(driver.ClientConfig{
|
||||
Connection: conn,
|
||||
Authentication: driver.BasicAuthentication(cfg.Username, cfg.Password),
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// check if the database exists
|
||||
// if not create it
|
||||
// (it works only with admin privilege user)
|
||||
exists, err := client.DatabaseExists(context.Background(), cfg.Database)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !exists {
|
||||
_, err = client.CreateDatabase(context.Background(), cfg.Database, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
database, err := client.Database(context.Background(), cfg.Database)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
found, _ := database.CollectionExists(context.Background(), cfg.Collection)
|
||||
|
||||
// Create the collection if not exists
|
||||
var collection driver.Collection
|
||||
if !found {
|
||||
// Create
|
||||
collection, err = database.CreateCollection(context.Background(), cfg.Collection, &driver.CreateCollectionOptions{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
// Get the collection
|
||||
collection, err = database.Collection(context.Background(), cfg.Collection)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate collection if Reset set to true
|
||||
if cfg.Reset {
|
||||
err = collection.Truncate(context.Background())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create storage
|
||||
store := &Storage{
|
||||
gcInterval: cfg.GCInterval,
|
||||
db: database,
|
||||
collection: collection,
|
||||
client: client,
|
||||
connection: conn,
|
||||
config: cfg,
|
||||
done: make(chan struct{}),
|
||||
aqlRemoveGC: fmt.Sprintf("FOR doc IN %s\n FILTER doc.exp <= @exp \n REMOVE { _key: doc._key } IN %s", collection.Name(), collection.Name()),
|
||||
}
|
||||
|
||||
// Start garbage collector
|
||||
go store.gc()
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
// Get value by key
|
||||
func (s *Storage) Get(key string) ([]byte, error) {
|
||||
if len(key) <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Check if the document exists
|
||||
// to avoid errors later
|
||||
exists, err := s.collection.DocumentExists(ctx, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// instead of returning an error if not exists
|
||||
// return nil
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// result model
|
||||
var model model
|
||||
_, err = s.collection.ReadDocument(ctx, key, &model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If the expiration time has already passed, then return nil
|
||||
if model.Exp != 0 && model.Exp <= time.Now().Unix() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return utils.UnsafeBytes(model.Val), nil
|
||||
}
|
||||
|
||||
// Set key with value
|
||||
func (s *Storage) Set(key string, val []byte, exp time.Duration) error {
|
||||
// Ain't Nobody Got Time For That
|
||||
if len(key) <= 0 || len(val) <= 0 {
|
||||
return nil
|
||||
}
|
||||
var expireAt int64
|
||||
if exp != 0 {
|
||||
expireAt = time.Now().Add(exp).Unix()
|
||||
}
|
||||
valStr := utils.UnsafeString(val)
|
||||
|
||||
// create the structure for the storage
|
||||
data := model{
|
||||
Key: key,
|
||||
Val: valStr,
|
||||
Exp: expireAt,
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
// Arango does not support documents with the same key
|
||||
// So we need to check if the document exists
|
||||
exists, err := s.collection.DocumentExists(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Update the document if exists
|
||||
if exists {
|
||||
_, err = s.collection.UpdateDocument(ctx, key, data)
|
||||
return err
|
||||
}
|
||||
// Otherwise create it
|
||||
_, err = s.collection.CreateDocument(ctx, data)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete value by key
|
||||
func (s *Storage) Delete(key string) error {
|
||||
// Ain't Nobody Got Time For That
|
||||
if len(key) <= 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := s.collection.RemoveDocument(context.Background(), key)
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset all keys
|
||||
// truncate the collection
|
||||
func (s *Storage) Reset() error {
|
||||
return s.collection.Truncate(context.Background())
|
||||
}
|
||||
|
||||
// Close the database
|
||||
// Arango does not provide a method to close the connection
|
||||
// more info @https://github.com/arangodb/go-driver/issues/43
|
||||
func (s *Storage) Close() error {
|
||||
// Stop gc
|
||||
s.done <- struct{}{}
|
||||
// reset connection params
|
||||
s.db = nil
|
||||
s.collection = nil
|
||||
s.connection = nil
|
||||
s.bindingParams = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// execute query
|
||||
func (s *Storage) exec(query string) error {
|
||||
// execute query
|
||||
_, err := s.db.Query(context.Background(), query, s.bindingParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// reset binding params
|
||||
s.bindingParams = map[string]interface{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Garbage collector to delete expired keys
|
||||
func (s *Storage) gc() {
|
||||
ticker := time.NewTicker(s.gcInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
case t := <-ticker.C:
|
||||
// set the expiration
|
||||
s.bindingParams["exp"] = t.Unix()
|
||||
_ = s.exec(s.aqlRemoveGC)
|
||||
}
|
||||
}
|
||||
}
|
126
arangodb/arangodb_test.go
Normal file
126
arangodb/arangodb_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package arangodb
|
||||
|
||||
import (
|
||||
"github.com/gofiber/utils"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var testStore = New(Config{
|
||||
Host: os.Getenv("ARANGODB_HOST"),
|
||||
Username: os.Getenv("ARANGODB_USERNAME"),
|
||||
Password: os.Getenv("ARANGODB_PASSWORD"),
|
||||
Reset: true,
|
||||
})
|
||||
|
||||
func Test_ARANGODB_Set(t *testing.T) {
|
||||
var (
|
||||
key = "john"
|
||||
val = []byte("doe")
|
||||
)
|
||||
|
||||
err := testStore.Set(key, val, 0)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
}
|
||||
|
||||
func Test_ARANGODB_Upsert(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_ARANGODB_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_ARANGODB_Set_Expiration(t *testing.T) {
|
||||
var (
|
||||
key = "john"
|
||||
val = []byte("doe")
|
||||
exp = 1 * time.Second
|
||||
)
|
||||
|
||||
err := testStore.Set(key, val, exp)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
|
||||
time.Sleep(1100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func Test_ARANGODB_Get_Expired(t *testing.T) {
|
||||
var (
|
||||
key = "john"
|
||||
)
|
||||
|
||||
result, err := testStore.Get(key)
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, true, len(result) == 0)
|
||||
}
|
||||
|
||||
func Test_ARANGODB_Get_NotExist(t *testing.T) {
|
||||
|
||||
result, err := testStore.Get("notexist")
|
||||
utils.AssertEqual(t, nil, err)
|
||||
utils.AssertEqual(t, true, len(result) == 0)
|
||||
}
|
||||
|
||||
func Test_ARANGODB_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_ARANGODB_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_ARANGODB_Close(t *testing.T) {
|
||||
utils.AssertEqual(t, nil, testStore.Close())
|
||||
}
|
92
arangodb/config.go
Normal file
92
arangodb/config.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package arangodb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config defines the config for storage.
|
||||
type Config struct {
|
||||
// Host name where the DB is hosted
|
||||
//
|
||||
// Optional. Default is "http://127.0.0.1"
|
||||
Host string
|
||||
|
||||
// Port where the DB is listening on
|
||||
//
|
||||
// Optional. Default is 8529
|
||||
Port string
|
||||
|
||||
// Server username
|
||||
//
|
||||
// Mandatory
|
||||
Username string
|
||||
|
||||
// Server password
|
||||
//
|
||||
// Mandatory
|
||||
Password string
|
||||
|
||||
// Database name
|
||||
//
|
||||
// Optional. Default is "fiber"
|
||||
Database string
|
||||
|
||||
// Collection name
|
||||
//
|
||||
// Optional. Default is "fiber_storage"
|
||||
Collection string
|
||||
|
||||
// Reset clears any existing keys in existing collection
|
||||
//
|
||||
// Optional. Default is false
|
||||
Reset bool
|
||||
// Time before deleting expired keys
|
||||
//
|
||||
// Optional. Default is 10 * time.Second
|
||||
GCInterval time.Duration
|
||||
}
|
||||
|
||||
// ConfigDefault is the default config
|
||||
var ConfigDefault = Config{
|
||||
Host: "http://127.0.0.1",
|
||||
Port: "8529",
|
||||
Database: "fiber",
|
||||
Collection: "fiber_storage",
|
||||
Reset: false,
|
||||
GCInterval: 10 * time.Second,
|
||||
}
|
||||
|
||||
func (c Config) hostComposed() string {
|
||||
return fmt.Sprintf("%s:%s", c.Host, c.Port)
|
||||
}
|
||||
|
||||
// Helper function to set default values
|
||||
func configDefault(cfg Config) Config {
|
||||
if cfg.Username == "" || cfg.Password == "" {
|
||||
panic("username and password are mandatory")
|
||||
}
|
||||
// Set default values
|
||||
if cfg.Host == "" {
|
||||
cfg.Host = ConfigDefault.Host
|
||||
} else {
|
||||
if !strings.HasPrefix(cfg.Host, "http") {
|
||||
panic("the host should start with http:// or https://")
|
||||
}
|
||||
}
|
||||
if len(cfg.Port) <= 0 {
|
||||
cfg.Port = ConfigDefault.Port
|
||||
}
|
||||
if cfg.Database == "" {
|
||||
cfg.Database = ConfigDefault.Database
|
||||
}
|
||||
if cfg.Collection == "" {
|
||||
cfg.Collection = ConfigDefault.Collection
|
||||
}
|
||||
|
||||
if int(cfg.GCInterval.Seconds()) <= 0 {
|
||||
cfg.GCInterval = ConfigDefault.GCInterval
|
||||
}
|
||||
return cfg
|
||||
}
|
8
arangodb/go.mod
Normal file
8
arangodb/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module github.com/gofiber/storage/arangodb
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/arangodb/go-driver v0.0.0-20201106193344-56ae8fd24510
|
||||
github.com/gofiber/utils v0.1.2
|
||||
)
|
Reference in New Issue
Block a user