164 lines
4.4 KiB
Go
164 lines
4.4 KiB
Go
package bbolt
|
|
|
|
import (
|
|
bolt "go.etcd.io/bbolt"
|
|
|
|
"github.com/philippgille/gokv/encoding"
|
|
"github.com/philippgille/gokv/util"
|
|
)
|
|
|
|
// Store is a gokv.Store implementation for bbolt (formerly known as Bolt / Bolt DB).
|
|
type Store struct {
|
|
db *bolt.DB
|
|
bucketName string
|
|
codec encoding.Codec
|
|
}
|
|
|
|
// Set stores the given value for the given key.
|
|
// Values are automatically marshalled to JSON or gob (depending on the configuration).
|
|
// The key must not be "" and the value must not be nil.
|
|
func (s Store) Set(k string, v interface{}) error {
|
|
if err := util.CheckKeyAndValue(k, v); err != nil {
|
|
return err
|
|
}
|
|
|
|
// First turn the passed object into something that bbolt can handle
|
|
data, err := s.codec.Marshal(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = s.db.Update(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket([]byte(s.bucketName))
|
|
return b.Put([]byte(k), data)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Get retrieves the stored value for the given key.
|
|
// You need to pass a pointer to the value, so in case of a struct
|
|
// the automatic unmarshalling can populate the fields of the object
|
|
// that v points to with the values of the retrieved object's values.
|
|
// If no value is found it returns (false, nil).
|
|
// The key must not be "" and the pointer must not be nil.
|
|
func (s Store) Get(k string, v interface{}) (found bool, err error) {
|
|
if err := util.CheckKeyAndValue(k, v); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var data []byte
|
|
err = s.db.View(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket([]byte(s.bucketName))
|
|
txData := b.Get([]byte(k))
|
|
// txData is only valid during the transaction.
|
|
// Its value must be copied to make it valid outside of the tx.
|
|
// TODO: Benchmark if it's faster to copy + close tx,
|
|
// or to keep the tx open until unmarshalling is done.
|
|
if txData != nil {
|
|
// `data = append([]byte{}, txData...)` would also work, but the following is more explicit
|
|
data = make([]byte, len(txData))
|
|
copy(data, txData)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return false, nil
|
|
}
|
|
|
|
// If no value was found return false
|
|
if data == nil {
|
|
return false, nil
|
|
}
|
|
|
|
return true, s.codec.Unmarshal(data, v)
|
|
}
|
|
|
|
// Delete deletes the stored value for the given key.
|
|
// Deleting a non-existing key-value pair does NOT lead to an error.
|
|
// The key must not be "".
|
|
func (s Store) Delete(k string) error {
|
|
if err := util.CheckKey(k); err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.db.Update(func(tx *bolt.Tx) error {
|
|
b := tx.Bucket([]byte(s.bucketName))
|
|
return b.Delete([]byte(k))
|
|
})
|
|
}
|
|
|
|
// Close closes the store.
|
|
// It must be called to make sure that all open transactions finish and to release all DB resources.
|
|
func (s Store) Close() error {
|
|
return s.db.Close()
|
|
}
|
|
|
|
// Options are the options for the bbolt store.
|
|
type Options struct {
|
|
// Bucket name for storing the key-value pairs.
|
|
// Optional ("default" by default).
|
|
BucketName string
|
|
// Path of the DB file.
|
|
// Optional ("bbolt.db" by default).
|
|
Path string
|
|
// Encoding format.
|
|
// Optional (encoding.JSON by default).
|
|
Codec encoding.Codec
|
|
}
|
|
|
|
// DefaultOptions is an Options object with default values.
|
|
// BucketName: "default", Path: "bbolt.db", Codec: encoding.JSON
|
|
var DefaultOptions = Options{
|
|
BucketName: "default",
|
|
Path: "bbolt.db",
|
|
Codec: encoding.JSON,
|
|
}
|
|
|
|
// NewStore creates a new bbolt store.
|
|
// Note: bbolt uses an exclusive write lock on the database file so it cannot be shared by multiple processes.
|
|
// So when creating multiple clients you should always use a new database file (by setting a different Path in the options).
|
|
//
|
|
// You must call the Close() method on the store when you're done working with it.
|
|
func NewStore(options Options) (Store, error) {
|
|
result := Store{}
|
|
|
|
// Set default values
|
|
if options.BucketName == "" {
|
|
options.BucketName = DefaultOptions.BucketName
|
|
}
|
|
if options.Path == "" {
|
|
options.Path = DefaultOptions.Path
|
|
}
|
|
if options.Codec == nil {
|
|
options.Codec = DefaultOptions.Codec
|
|
}
|
|
|
|
// Open DB
|
|
db, err := bolt.Open(options.Path, 0600, nil)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
// Create a bucket if it doesn't exist yet.
|
|
// In bbolt key/value pairs are stored to and read from buckets.
|
|
err = db.Update(func(tx *bolt.Tx) error {
|
|
_, err := tx.CreateBucketIfNotExists([]byte(options.BucketName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
result.db = db
|
|
result.bucketName = options.BucketName
|
|
result.codec = options.Codec
|
|
|
|
return result, nil
|
|
}
|