-
Notifications
You must be signed in to change notification settings - Fork 72
/
Copy pathbbolt.go
163 lines (142 loc) · 4.36 KB
/
bbolt.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
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 any) 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 any) (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
}