254 lines
6.7 KiB
Go
254 lines
6.7 KiB
Go
package nuts
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jrapoport/chestnut/log"
|
|
"github.com/jrapoport/chestnut/storage"
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/xujiajun/nutsdb"
|
|
)
|
|
|
|
const logName = "nutsdb"
|
|
|
|
// nutsDBStore is an implementation the Storage interface for nutsdb
|
|
// https://github.com/xujiajun/nutsdb.
|
|
type nutsDBStore struct {
|
|
opts storage.StoreOptions
|
|
path string
|
|
db *nutsdb.DB
|
|
log log.Logger
|
|
}
|
|
|
|
var _ storage.Storage = (*nutsDBStore)(nil)
|
|
|
|
// NewStore is used to instantiate a datastore backed by nutsdb.
|
|
func NewStore(path string, opt ...storage.StoreOption) storage.Storage {
|
|
opts := storage.ApplyOptions(storage.DefaultStoreOptions, opt...)
|
|
logger := log.Named(opts.Logger(), logName)
|
|
if path == "" {
|
|
logger.Fatal("store path required")
|
|
}
|
|
return &nutsDBStore{path: path, opts: opts, log: logger}
|
|
}
|
|
|
|
// Options returns the configuration options for the store.
|
|
func (s *nutsDBStore) Options() storage.StoreOptions {
|
|
return s.opts
|
|
}
|
|
|
|
// Open opens the store.
|
|
func (s *nutsDBStore) Open() (err error) {
|
|
s.log.Debugf("opening store at path: %s", s.path)
|
|
opt := nutsdb.DefaultOptions
|
|
opt.Dir = s.path
|
|
if s.db, err = nutsdb.Open(opt); err != nil {
|
|
err = s.logError("open", err)
|
|
return
|
|
}
|
|
if s.db == nil {
|
|
err = errors.New("unable to open backing store")
|
|
err = s.logError("open", err)
|
|
return
|
|
}
|
|
s.log.Infof("opened store at path: %s", s.path)
|
|
return
|
|
}
|
|
|
|
// Put an entry in the store.
|
|
func (s *nutsDBStore) Put(name string, key []byte, value []byte) error {
|
|
s.log.Debugf("put: %d value bytes to key: %s", len(value), key)
|
|
if err := storage.ValidKey(name, key); err != nil {
|
|
return s.logError("put", err)
|
|
} else if len(value) <= 0 {
|
|
err = errors.New("value cannot be empty")
|
|
return s.logError("put", err)
|
|
}
|
|
putValue := func(tx *nutsdb.Tx) error {
|
|
s.log.Debugf("put: tx %d bytes to key: %s.%s",
|
|
len(value), name, string(key))
|
|
return tx.Put(name, key, value, 0)
|
|
}
|
|
return s.logError("put", s.db.Update(putValue))
|
|
}
|
|
|
|
// Get a value from the store.
|
|
func (s *nutsDBStore) Get(name string, key []byte) ([]byte, error) {
|
|
s.log.Debugf("get: value at key: %s", key)
|
|
if err := storage.ValidKey(name, key); err != nil {
|
|
return nil, s.logError("get", err)
|
|
}
|
|
var value []byte
|
|
getValue := func(tx *nutsdb.Tx) error {
|
|
s.log.Debugf("get: tx key: %s.%s", name, key)
|
|
e, err := tx.Get(name, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value = e.Value
|
|
s.log.Debugf("get: tx key: %s.%s value (%d bytes)",
|
|
name, string(key), len(value))
|
|
return nil
|
|
}
|
|
if err := s.db.View(getValue); err != nil {
|
|
return nil, s.logError("get", err)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
// Save the value in v and store the result at key.
|
|
func (s *nutsDBStore) Save(name string, key []byte, v interface{}) error {
|
|
b, err := jsoniter.Marshal(v)
|
|
if err != nil {
|
|
return s.logError("save", err)
|
|
}
|
|
return s.Put(name, key, b)
|
|
}
|
|
|
|
// Load the value at key and stores the result in v.
|
|
func (s *nutsDBStore) Load(name string, key []byte, v interface{}) error {
|
|
b, err := s.Get(name, key)
|
|
if err != nil {
|
|
return s.logError("load", err)
|
|
}
|
|
return s.logError("load", jsoniter.Unmarshal(b, v))
|
|
}
|
|
|
|
// Has checks for a key in the store.
|
|
func (s *nutsDBStore) Has(name string, key []byte) (bool, error) {
|
|
s.log.Debugf("has: key: %s", key)
|
|
if err := storage.ValidKey(name, key); err != nil {
|
|
return false, s.logError("has", err)
|
|
}
|
|
var has bool
|
|
hasKey := func(tx *nutsdb.Tx) error {
|
|
s.log.Debugf("has: tx get namespace: %s", name)
|
|
entries, err := tx.GetAll(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.log.Debugf("has: tx found %d keys in: %s", len(entries), name)
|
|
for _, entry := range entries {
|
|
has = bytes.Equal(key, entry.Key)
|
|
if has {
|
|
s.log.Debugf("has: tx key found: %s.%s", name, string(key))
|
|
break
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := s.db.View(hasKey); err != nil {
|
|
return false, s.logError("has", err)
|
|
}
|
|
s.log.Debugf("has: found key %s: %t", key, has)
|
|
return has, nil
|
|
}
|
|
|
|
// Delete removes a key from the store.
|
|
func (s *nutsDBStore) Delete(name string, key []byte) error {
|
|
s.log.Debugf("delete: key: %s", key)
|
|
if err := storage.ValidKey(name, key); err != nil {
|
|
return s.logError("delete", err)
|
|
}
|
|
del := func(tx *nutsdb.Tx) error {
|
|
s.log.Debugf("delete: tx key: %s.%s", name, string(key))
|
|
return tx.Delete(name, key)
|
|
}
|
|
return s.logError("delete", s.db.Update(del))
|
|
}
|
|
|
|
// List returns a list of all keys in the namespace.
|
|
func (s *nutsDBStore) List(name string) (keys [][]byte, err error) {
|
|
s.log.Debugf("list: keys in namespace: %s", name)
|
|
listKeys := func(tx *nutsdb.Tx) error {
|
|
keys, err = s.listKeys(name, tx)
|
|
return err
|
|
}
|
|
if err = s.db.View(listKeys); err != nil {
|
|
return nil, s.logError("list", err)
|
|
}
|
|
s.log.Debugf("list: found %d keys: %s", len(keys), keys)
|
|
return
|
|
}
|
|
|
|
func (s *nutsDBStore) listKeys(name string, tx *nutsdb.Tx) ([][]byte, error) {
|
|
var keys [][]byte
|
|
s.log.Debugf("list: tx scan namespace: %s", name)
|
|
entries, err := tx.GetAll(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keys = make([][]byte, len(entries))
|
|
s.log.Debugf("list: tx found %d keys in: %s", len(entries), name)
|
|
for i, entry := range entries {
|
|
s.log.Debugf("list: tx found key: %s.%s", name, string(entry.Key))
|
|
keys[i] = entry.Key
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// ListAll returns a mapped list of all keys in the store.
|
|
func (s *nutsDBStore) ListAll() (map[string][][]byte, error) {
|
|
s.log.Debugf("list: all keys")
|
|
var total int
|
|
allKeys := map[string][][]byte{}
|
|
listKeys := func(tx *nutsdb.Tx) error {
|
|
for name := range s.db.BPTreeIdx {
|
|
keys, err := s.listKeys(name, tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(keys) <= 0 {
|
|
continue
|
|
}
|
|
allKeys[name] = keys
|
|
total += len(keys)
|
|
}
|
|
return nil
|
|
}
|
|
if err := s.db.View(listKeys); err != nil {
|
|
return nil, s.logError("list", err)
|
|
}
|
|
s.log.Debugf("list: found %d keys: %s", total, allKeys)
|
|
return allKeys, nil
|
|
}
|
|
|
|
// Export copies the datastore to directory at path.
|
|
func (s *nutsDBStore) Export(path string) error {
|
|
s.log.Debugf("export: to path: %s", path)
|
|
if path == "" {
|
|
err := fmt.Errorf("invalid path: %s", path)
|
|
return s.logError("export", err)
|
|
} else if s.path == path {
|
|
err := fmt.Errorf("path cannot be store path: %s", path)
|
|
return s.logError("export", err)
|
|
}
|
|
if err := s.db.Backup(path); err != nil {
|
|
return s.logError("export", err)
|
|
}
|
|
s.log.Debugf("export: to path complete: %s", path)
|
|
return nil
|
|
}
|
|
|
|
// Close closes the datastore and releases all db resources.
|
|
func (s *nutsDBStore) Close() error {
|
|
s.log.Debugf("closing store at path: %s", s.path)
|
|
err := s.db.Close()
|
|
s.db = nil
|
|
s.log.Info("store closed")
|
|
return s.logError("close", err)
|
|
}
|
|
|
|
func (s *nutsDBStore) logError(name string, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if name != "" {
|
|
err = fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
s.log.Error(err)
|
|
return err
|
|
}
|