initial commit

This commit is contained in:
ston1th 2022-07-10 01:10:31 +02:00
commit f48fa210bb
49 changed files with 4058 additions and 0 deletions

140
pkg/store/boltstore.go Normal file
View file

@ -0,0 +1,140 @@
// Copyright (C) 2022 Marius Schellenberger
package store
import (
"encoding/json"
"io"
bolt "go.etcd.io/bbolt"
)
const (
defaultBoltBucket = "default"
fileMode = 0o600
)
type BoltStore struct {
Marshaler
db *bolt.DB
}
func NewBoltStore(file string, m Marshaler) (Store, error) {
if m == nil {
m = NewGOB()
}
db, err := bolt.Open(file, fileMode, nil)
if err != nil {
return nil, err
}
if err := db.Update(func(tx *bolt.Tx) (err error) {
_, err = tx.CreateBucketIfNotExists([]byte(defaultBoltBucket))
return
}); err != nil {
return nil, err
}
return &BoltStore{m, db}, nil
}
type dump struct {
K string `json:"k"`
V []byte `json:"v"`
}
func (bs *BoltStore) Dump(w io.Writer) (err error) {
if w == nil {
return ErrWriterIsNil
}
var d []dump
err = bs.ForEach(func(k string, v []byte) error {
b := make([]byte, len(v))
copy(b, v)
d = append(d, dump{k, b})
return nil
})
if err != nil {
return
}
return json.NewEncoder(w).Encode(d)
}
func (bs *BoltStore) Restore(r io.Reader) (err error) {
if r == nil {
return ErrReaderIsNil
}
var d []dump
err = json.NewDecoder(r).Decode(&d)
if err != nil {
return
}
if err = bs.db.Update(func(tx *bolt.Tx) (err error) {
if err = tx.DeleteBucket([]byte(defaultBoltBucket)); err != nil {
return
}
_, err = tx.CreateBucket([]byte(defaultBoltBucket))
return
}); err != nil {
return
}
return bs.db.Update(func(tx *bolt.Tx) (err error) {
b := tx.Bucket([]byte(defaultBoltBucket))
for _, v := range d {
b.Put([]byte(v.K), v.V)
}
return nil
})
}
func (bs *BoltStore) Get(key string, v interface{}) (err error) {
err = bs.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(defaultBoltBucket)).Get([]byte(key))
if b == nil {
return ErrKeyNotFound
}
if v != nil {
return bs.Unmarshal(b, v)
}
return nil
})
return
}
func (bs *BoltStore) Set(key string, v interface{}) error {
return bs.db.Update(func(tx *bolt.Tx) (err error) {
var b []byte
if v != nil {
b, err = bs.Marshal(v)
if err != nil {
return
}
}
return tx.Bucket([]byte(defaultBoltBucket)).Put([]byte(key), b)
})
}
func (bs *BoltStore) ForEach(f func(string, []byte) error) error {
return bs.db.View(func(tx *bolt.Tx) error {
return tx.Bucket([]byte(defaultBoltBucket)).ForEach(func(k, v []byte) error {
return f(string(k), v)
})
})
}
func (bs *BoltStore) ForEachPrefix(prefix string, f func(string, []byte) error) error {
return bs.ForEach(func(k string, v []byte) error {
if trim, ok := hasTrimPrefix(k, prefix); ok {
return f(trim, v)
}
return nil
})
}
func (bs *BoltStore) Delete(key string) error {
return bs.db.Update(func(tx *bolt.Tx) error {
return tx.Bucket([]byte(defaultBoltBucket)).Delete([]byte(key))
})
}
func (bs *BoltStore) Close() error {
return bs.db.Close()
}

View file

@ -0,0 +1,98 @@
// Copyright (C) 2022 Marius Schellenberger
package store
import (
"bytes"
"errors"
"os"
"path/filepath"
"testing"
)
type testData struct {
Data string
}
func TestBoltStore(t *testing.T) {
data := []testData{
{"123"},
{"hello"},
}
dir, err := os.MkdirTemp("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
bs, err := NewBoltStore(filepath.Join(dir, "bolt.db"), nil)
if err != nil {
t.Fatal(err)
}
err = bs.Set("key0", data[0])
if err != nil {
t.Fatal(err)
}
err = bs.Set("key1", data[1])
if err != nil {
t.Fatal(err)
}
t.Run("GetKey0", func(t *testing.T) {
var d testData
err := bs.Get("key0", &d)
if err != nil {
t.Fatal(err)
}
if d != data[0] {
t.Error("d is not data[0]")
}
})
t.Run("ForEach", func(t *testing.T) {
err := bs.ForEach(func(k string, v []byte) error {
if k == "key0" || k == "key1" {
return nil
}
return errors.New("key0 or key1 not found")
})
if err != nil {
t.Fatal(err)
}
})
t.Run("DumpRestore", func(t *testing.T) {
buf := new(bytes.Buffer)
err := bs.Dump(buf)
if err != nil {
t.Fatal(err)
}
if buf.Len() == 0 {
t.Error("no dump written")
}
err = bs.Restore(buf)
if err != nil {
t.Fatal(err)
}
})
t.Run("DeleteKey1", func(t *testing.T) {
err := bs.Delete("key1")
if err != nil {
t.Fatal(err)
}
})
t.Run("GetKey1", func(t *testing.T) {
var d testData
err := bs.Get("key1", &d)
if err == nil {
t.Error("key1 should be deleted")
}
if d == data[1] {
t.Error("d is not data[1]")
}
})
t.Run("Close", func(t *testing.T) {
err := bs.Close()
if err != nil {
t.Fatal(err)
}
})
}

13
pkg/store/dump.go Normal file
View file

@ -0,0 +1,13 @@
// Copyright (C) 2022 Marius Schellenberger
package store
import "io"
type Dumper interface {
Dump(io.Writer) error
}
type Restorer interface {
Restore(io.Reader) error
}

25
pkg/store/gob.go Normal file
View file

@ -0,0 +1,25 @@
// Copyright (C) 2022 Marius Schellenberger
package store
import (
"bytes"
"encoding/gob"
)
type gobMarshaler struct{}
func NewGOB() Marshaler {
return gobMarshaler{}
}
func (gobMarshaler) Marshal(v interface{}) (b []byte, err error) {
buf := new(bytes.Buffer)
err = gob.NewEncoder(buf).Encode(v)
b = buf.Bytes()
return
}
func (gobMarshaler) Unmarshal(data []byte, v interface{}) error {
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v)
}

11
pkg/store/helper.go Normal file
View file

@ -0,0 +1,11 @@
// Copyright (C) 2022 Marius Schellenberger
package store
// Parts taken from strings.TrimPrefix
func hasTrimPrefix(s, prefix string) (string, bool) {
if len(s) >= len(prefix) && s[0:len(prefix)] == prefix {
return s[len(prefix):], true
}
return s, false
}

8
pkg/store/marshal.go Normal file
View file

@ -0,0 +1,8 @@
// Copyright (C) 2022 Marius Schellenberger
package store
type Marshaler interface {
Marshal(v interface{}) ([]byte, error)
Unmarshal(data []byte, v interface{}) error
}

23
pkg/store/store.go Normal file
View file

@ -0,0 +1,23 @@
// Copyright (C) 2022 Marius Schellenberger
package store
import "errors"
var (
ErrKeyNotFound = errors.New("store: key not found")
ErrWriterIsNil = errors.New("store: writer is nil")
ErrReaderIsNil = errors.New("store: reader is nil")
)
type Store interface {
Marshaler
Dumper
Restorer
Get(string, interface{}) error
Set(string, interface{}) error
ForEach(func(string, []byte) error) error
ForEachPrefix(string, func(string, []byte) error) error
Delete(string) error
Close() error
}