diff --git a/boltease.go b/boltease.go index 428c900..38b657a 100644 --- a/boltease.go +++ b/boltease.go @@ -35,6 +35,92 @@ func Create(fn string, m os.FileMode, opts *bolt.Options) (*DB, error) { return &b, nil } +// SetStruct takes a database, a path, a struct, and a function that takes a +// path and an element and processes it. +func SetStruct[T any](b *DB, path []string, value T, fn func([]string, T) error) error { + var err error + if !b.dbIsOpen { + if err = b.OpenDB(); err != nil { + return err + } + defer b.CloseDB() + } + + err = b.MkBucketPath(path) + if err != nil { + return err + } + err = b.boltDB.Update(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(path[0])) + if bkt == nil { + return fmt.Errorf("couldn't find bucket " + path[0]) + } + for idx := 1; idx < len(path); idx++ { + bkt, err = bkt.CreateBucketIfNotExists([]byte(path[idx])) + if err != nil { + return err + } + } + // bkt should have the last bucket in the path + return fn(path, value) + }) + return err +} + +// SetStructList takes a path, a slice of structs, and a function that takes +// a bolt bucket and one element of the slice and processes it. +func SetStructList[T any](b *DB, path []string, values []T, fn func(*bolt.Bucket, T) error) error { + var err error + if !b.dbIsOpen { + if err = b.OpenDB(); err != nil { + return err + } + defer b.CloseDB() + } + + err = b.MkBucketPath(path) + if err != nil { + return err + } + err = b.boltDB.Update(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(path[0])) + if bkt == nil { + return fmt.Errorf("couldn't find bucket " + path[0]) + } + for idx := 1; idx < len(path); idx++ { + bkt, err = bkt.CreateBucketIfNotExists([]byte(path[idx])) + if err != nil { + return err + } + } + // bkt should have the last bucket in the path + // We need to get the sequence number, if it's greater than the length of + // 'values', we're going to have to delete values. + seq := bkt.Sequence() + bkt.SetSequence(0) + for _, v := range values { + id, _ := bkt.NextSequence() + bId := make([]byte, 8) + binary.BigEndian.PutUint64(bId, id) + structBkt, err := bkt.CreateBucketIfNotExists(bId) + if err = fn(structBkt, v); err != nil { + return err + } + } + currSeq := bkt.Sequence() + for ; seq < currSeq; seq++ { + bId := make([]byte, 8) + binary.BigEndian.PutUint64(bId, seq) + err = bkt.Delete(bId) + if err != nil { + return err + } + } + return nil + }) + return err +} + func (b *DB) OpenDB() error { if b.dbIsOpen { // DB is already open, that's fine. @@ -112,7 +198,7 @@ func (b *DB) Set(path []string, key string, val interface{}) error { case []byte: return b.SetBytes(path, key, v) } - return errors.New("Unknown Data Type") + return errors.New("unknown data type") } func (b *DB) GetBytes(path []string, key string) ([]byte, error) { @@ -127,18 +213,18 @@ func (b *DB) GetBytes(path []string, key string) ([]byte, error) { err = b.boltDB.View(func(tx *bolt.Tx) error { bkt := tx.Bucket([]byte(path[0])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + path[0]) + return fmt.Errorf("couldn't find bucket " + path[0]) } for idx := 1; idx < len(path); idx++ { bkt = bkt.Bucket([]byte(path[idx])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + strings.Join(path[:idx+1], "/")) + return fmt.Errorf("couldn't find bucket " + strings.Join(path[:idx+1], "/")) } } // newBkt should have the last bucket in the path val := bkt.Get([]byte(key)) if val == nil { - return fmt.Errorf("Couldn't find value") + return fmt.Errorf("couldn't find value") } ret = make([]byte, len(val)) copy(ret, val) @@ -166,7 +252,7 @@ func (b *DB) SetBytes(path []string, key string, val []byte) error { err = b.boltDB.Update(func(tx *bolt.Tx) error { bkt := tx.Bucket([]byte(path[0])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + path[0]) + return fmt.Errorf("couldn't find bucket " + path[0]) } for idx := 1; idx < len(path); idx++ { bkt, err = bkt.CreateBucketIfNotExists([]byte(path[idx])) @@ -195,15 +281,15 @@ func (b *DB) GetString(path []string, key string) (string, error) { err = b.boltDB.View(func(tx *bolt.Tx) error { bkt := tx.Bucket([]byte(path[0])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + path[0]) + return fmt.Errorf("couldn't find bucket " + path[0]) } for idx := 1; idx < len(path); idx++ { bkt = bkt.Bucket([]byte(path[idx])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + strings.Join(path[:idx+1], "/")) + return fmt.Errorf("couldn't find bucket " + strings.Join(path[:idx+1], "/")) } } - // newBkt should have the last bucket in the path + // bkt should have the last bucket in the path ret = string(bkt.Get([]byte(key))) return nil }) @@ -228,7 +314,7 @@ func (b *DB) SetString(path []string, key, val string) error { err = b.boltDB.Update(func(tx *bolt.Tx) error { bkt := tx.Bucket([]byte(path[0])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + path[0]) + return fmt.Errorf("couldn't find bucket " + path[0]) } for idx := 1; idx < len(path); idx++ { bkt, err = bkt.CreateBucketIfNotExists([]byte(path[idx])) @@ -237,7 +323,7 @@ func (b *DB) SetString(path []string, key, val string) error { } } // bkt should have the last bucket in the path - return bkt.Put([]byte(key), []byte(val)) + return SetString(bkt, key, val) }) return err } @@ -268,7 +354,7 @@ func (b *DB) GetBool(path []string, key string) (bool, error) { if r == "true" || r == "1" { ret = true } else if r != "false" && r != "0" { - err = fmt.Errorf("Cannot parse as a boolean") + err = fmt.Errorf("cannot parse as a boolean") } } return ret, err @@ -311,14 +397,14 @@ func (b *DB) GetBucketList(path []string) ([]string, error) { err = b.boltDB.Update(func(tx *bolt.Tx) error { bkt := tx.Bucket([]byte(path[0])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + path[0]) + return fmt.Errorf("couldn't find bucket " + path[0]) } var berr error if len(path) > 1 { for idx := 1; idx < len(path); idx++ { bkt = bkt.Bucket([]byte(path[idx])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + strings.Join(path[:idx+1], " / ")) + return fmt.Errorf("couldn't find bucket " + strings.Join(path[:idx+1], " / ")) } } } @@ -350,14 +436,14 @@ func (b *DB) GetKeyList(path []string) ([]string, error) { err = b.boltDB.Update(func(tx *bolt.Tx) error { bkt := tx.Bucket([]byte(path[0])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + path[0]) + return fmt.Errorf("couldn't find bucket " + path[0]) } var berr error if len(path) > 1 { for idx := 1; idx < len(path); idx++ { bkt = bkt.Bucket([]byte(path[idx])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + strings.Join(path[:idx], " / ")) + return fmt.Errorf("couldn't find bucket " + strings.Join(path[:idx], " / ")) } } } @@ -388,14 +474,14 @@ func (b *DB) DeletePair(path []string, key string) error { err = b.boltDB.Update(func(tx *bolt.Tx) error { bkt := tx.Bucket([]byte(path[0])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + path[0]) + return fmt.Errorf("couldn't find bucket " + path[0]) } if len(path) > 1 { var newBkt *bolt.Bucket for idx := 1; idx < len(path); idx++ { newBkt = bkt.Bucket([]byte(path[idx])) if newBkt == nil { - return fmt.Errorf("Couldn't find bucket " + strings.Join(path[:idx], "/")) + return fmt.Errorf("couldn't find bucket " + strings.Join(path[:idx], "/")) } } bkt = newBkt @@ -423,12 +509,12 @@ func (b *DB) DeleteBucket(path []string, key string) error { err = b.boltDB.Update(func(tx *bolt.Tx) error { bkt := tx.Bucket([]byte(path[0])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + path[0]) + return fmt.Errorf("couldn't find bucket " + path[0]) } for idx := 1; idx < len(path); idx++ { bkt = bkt.Bucket([]byte(path[idx])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + strings.Join(path[:idx], "/")) + return fmt.Errorf("couldn't find bucket " + strings.Join(path[:idx], "/")) } } // bkt should have the last bucket in the path @@ -455,14 +541,14 @@ func (b *DB) GetStringList(path []string) ([]string, error) { err = b.boltDB.Update(func(tx *bolt.Tx) error { bkt := tx.Bucket([]byte(path[0])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + path[0]) + return fmt.Errorf("couldn't find bucket " + path[0]) } var berr error if len(path) > 1 { for idx := 1; idx < len(path); idx++ { bkt = bkt.Bucket([]byte(path[idx])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + strings.Join(path[:idx+1], " / ")) + return fmt.Errorf("couldn't find bucket " + strings.Join(path[:idx+1], " / ")) } } } @@ -497,7 +583,61 @@ func (b *DB) SetStringList(path, values []string) error { err = b.boltDB.Update(func(tx *bolt.Tx) error { bkt := tx.Bucket([]byte(path[0])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + path[0]) + return fmt.Errorf("couldn't find bucket " + path[0]) + } + for idx := 1; idx < len(path); idx++ { + bkt, err = bkt.CreateBucketIfNotExists([]byte(path[idx])) + if err != nil { + return err + } + } + + // bkt should have the last bucket in the path + // We need to get the sequence number, if it's greater than the length + // of 'values', we're going to have to delete values. + seq := bkt.Sequence() + bkt.SetSequence(0) + for _, v := range values { + id, _ := bkt.NextSequence() + bId := make([]byte, 8) + binary.BigEndian.PutUint64(bId, id) + err = bkt.Put(bId, []byte(v)) + if err != nil { + return err + } + } + currSeq := bkt.Sequence() + for ; seq < currSeq; seq++ { + bId := make([]byte, 8) + binary.BigEndian.PutUint64(bId, seq) + err = bkt.Delete(bId) + if err != nil { + return err + } + } + return nil + }) + return err +} + +// AddToStringList adds strings to a bucket of string slices +func (b *DB) AddToStringList(path []string, values ...string) error { + var err error + if !b.dbIsOpen { + if err = b.OpenDB(); err != nil { + return err + } + defer b.CloseDB() + } + + err = b.MkBucketPath(path) + if err != nil { + return err + } + err = b.boltDB.Update(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(path[0])) + if bkt == nil { + return fmt.Errorf("couldn't find bucket " + path[0]) } for idx := 1; idx < len(path); idx++ { bkt, err = bkt.CreateBucketIfNotExists([]byte(path[idx])) @@ -534,14 +674,14 @@ func (b *DB) GetKeyValueMap(path []string) (map[string][]byte, error) { err = b.boltDB.Update(func(tx *bolt.Tx) error { bkt := tx.Bucket([]byte(path[0])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + path[0]) + return fmt.Errorf("couldn't find bucket " + path[0]) } var berr error if len(path) > 1 { for idx := 1; idx < len(path); idx++ { bkt = bkt.Bucket([]byte(path[idx])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + strings.Join(path[:idx], " / ")) + return fmt.Errorf("couldn't find bucket " + strings.Join(path[:idx], " / ")) } } } @@ -589,7 +729,7 @@ func (b *DB) SetKeyValueMap(path []string, kvMap map[string][]byte) error { err = b.boltDB.Update(func(tx *bolt.Tx) error { bkt := tx.Bucket([]byte(path[0])) if bkt == nil { - return fmt.Errorf("Couldn't find bucket " + path[0]) + return fmt.Errorf("couldn't find bucket " + path[0]) } for idx := 1; idx < len(path); idx++ { bkt, err = bkt.CreateBucketIfNotExists([]byte(path[idx])) diff --git a/bucket_funcs.go b/bucket_funcs.go new file mode 100644 index 0000000..83a1e0c --- /dev/null +++ b/bucket_funcs.go @@ -0,0 +1,323 @@ +package boltease + +import ( + "encoding/binary" + "fmt" + "strconv" + "strings" + "time" + + "github.com/boltdb/bolt" +) + +func DoUpdate(b *DB, path []string, fn func(bkt *bolt.Bucket) error) error { + var err error + if !b.dbIsOpen { + if err = b.OpenDB(); err != nil { + return err + } + defer b.CloseDB() + } + + err = b.MkBucketPath(path) + if err != nil { + return err + } + err = b.boltDB.Update(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(path[0])) + if bkt == nil { + return fmt.Errorf("couldn't find bucket " + path[0]) + } + for idx := 1; idx < len(path); idx++ { + bkt, err = bkt.CreateBucketIfNotExists([]byte(path[idx])) + if err != nil { + return err + } + } + // bkt should have the last bucket in the path + return fn(bkt) + }) + return err +} + +func DoRead(b *DB, path []string, fn func(bkt *bolt.Bucket) error) error { + var err error + if !b.dbIsOpen { + if err = b.OpenDB(); err != nil { + return err + } + defer b.CloseDB() + } + err = b.boltDB.View(func(tx *bolt.Tx) error { + bkt := tx.Bucket([]byte(path[0])) + if bkt == nil { + return fmt.Errorf("couldn't find bucket " + path[0]) + } + for idx := 1; idx < len(path); idx++ { + bkt = bkt.Bucket([]byte(path[idx])) + if bkt == nil { + return fmt.Errorf("couldn't find bucket " + strings.Join(path[:idx+1], "/")) + } + } + // bkt should have the last bucket in the path + return fn(bkt) + }) + return err +} + +// These bucket functions are meant to be run inside of a boltdb +// transaction (Update/View) +// GetBucket must be run inside of an Update transaction +// Otherwise, any Get* function can be in either. +// And any Set* function needs to be in an Update. +func GetBucket(bkt *bolt.Bucket, path ...string) (*bolt.Bucket, error) { + if bkt == nil { + return nil, fmt.Errorf("bucket must not be nil") + } + var ret *bolt.Bucket + var err error + ret, err = bkt.CreateBucketIfNotExists([]byte(path[0])) + if len(path) > 1 { + for idx := 1; idx < len(path); idx++ { + ret, err = bkt.CreateBucketIfNotExists([]byte(path[idx])) + if err != nil { + return nil, err + } + } + } + return ret, err +} + +func GetString(bkt *bolt.Bucket, key string) (string, error) { + if bkt == nil { + return "", fmt.Errorf("bucket must not be nil") + } + return string(bkt.Get([]byte(key))), nil +} + +func SetString(bkt *bolt.Bucket, key, val string) error { + if bkt == nil { + return fmt.Errorf("bucket must not be nil") + } + return bkt.Put([]byte(key), []byte(val)) +} + +func GetInt(bkt *bolt.Bucket, key string) (int, error) { + if bkt == nil { + return 0, fmt.Errorf("bucket must not be nil") + } + r, err := GetString(bkt, key) + if err != nil { + return 0, err + } + return strconv.Atoi(r) +} + +func SetInt(bkt *bolt.Bucket, key string, val int) error { + if bkt == nil { + return fmt.Errorf("bucket must not be nil") + } + return SetString(bkt, key, strconv.Itoa(val)) +} + +func GetBool(bkt *bolt.Bucket, key string) (bool, error) { + r, err := GetString(bkt, key) + if err != nil { + return false, err + } + if r == "true" || r == "1" { + return true, nil + } else if r == "false" || r == "0" { + return false, nil + } else { + return false, fmt.Errorf("cannot parse as a boolean") + } +} + +func SetBool(bkt *bolt.Bucket, key string, val bool) error { + if bkt == nil { + return fmt.Errorf("bucket must not be nil") + } + if val { + return SetString(bkt, key, "true") + } + return SetString(bkt, key, "false") +} + +func GetTimestamp(bkt *bolt.Bucket, key string) (time.Time, error) { + r, err := GetString(bkt, key) + if err != nil { + return time.Unix(0, 0), err + } + return time.Parse(time.RFC3339, r) +} + +func SetTimestamp(bkt *bolt.Bucket, key string, val time.Time) error { + return SetString(bkt, key, val.Format(time.RFC3339)) +} + +func GetBucketList(bkt *bolt.Bucket) ([]string, error) { + var ret []string + if bkt == nil { + return ret, fmt.Errorf("bucket must not be nil") + } + bkt.ForEach(func(k, v []byte) error { + if v == nil { + // Must be a bucket + ret = append(ret, string(k)) + } + return nil + }) + return ret, nil +} + +func GetKeyList(bkt *bolt.Bucket) ([]string, error) { + var ret []string + if bkt == nil { + return ret, fmt.Errorf("bucket must not be nil") + } + bkt.ForEach(func(k, v []byte) error { + if v != nil { + ret = append(ret, string(k)) + } + return nil + }) + return ret, nil +} + +func DeletePair(bkt *bolt.Bucket, key string) error { + if bkt == nil { + return fmt.Errorf("bucket must not be nil") + } + // Make sure that the key belongs to a pair + if tst := bkt.Bucket([]byte(key)); tst != nil { + return fmt.Errorf("key is a bucket") + } + return bkt.Delete([]byte(key)) +} + +func DeleteBucket(bkt *bolt.Bucket, key string) error { + if bkt == nil { + return fmt.Errorf("bucket must not be nil") + } + // Make sure that the key belongs to a pair + if tst := bkt.Bucket([]byte(key)); tst == nil { + return fmt.Errorf("key is not a bucket") + } + return bkt.DeleteBucket([]byte(key)) +} + +func GetStringList(bkt *bolt.Bucket) ([]string, error) { + var ret []string + if bkt == nil { + return ret, fmt.Errorf("bucket must not be nil") + } + bkt.ForEach(func(k, v []byte) error { + if v != nil { + // Must be a key + ret = append(ret, string(v)) + } + return nil + }) + return ret, nil +} + +func SetStringList(bkt *bolt.Bucket, val []string) error { + if bkt == nil { + return fmt.Errorf("bucket must not be nil") + } + genseq := func(id uint64) []byte { + bId := make([]byte, 8) + binary.BigEndian.PutUint64(bId, id) + return bId + } + var err error + // We need to get the sequence number, if it's greater than the length of + // val, we're going to have to delete values + seq := bkt.Sequence() + bkt.SetSequence(0) + for _, v := range val { + id, _ := bkt.NextSequence() + if err = bkt.Put(genseq(id), []byte(v)); err != nil { + return err + } + } + currSeq := bkt.Sequence() + for ; seq < currSeq; seq++ { + err = bkt.Delete(genseq(seq)) + if err != nil { + return err + } + } + return nil +} + +func AddToStringList(bkt *bolt.Bucket, key string, values ...string) error { + if bkt == nil { + return fmt.Errorf("bucket must not be nil") + } + genseq := func(id uint64) []byte { + bId := make([]byte, 8) + binary.BigEndian.PutUint64(bId, id) + return bId + } + var err error + for _, v := range values { + id, _ := bkt.NextSequence() + if err = bkt.Put(genseq(id), []byte(v)); err != nil { + return err + } + } + return nil +} + +func GetKeyValueMap(bkt *bolt.Bucket) (map[string][]byte, error) { + ret := make(map[string][]byte) + if bkt == nil { + return ret, fmt.Errorf("bucket must not be nil") + } + bkt.ForEach(func(k, v []byte) error { + if v != nil { + ret[string(k)] = v + } + return nil + }) + return ret, nil +} + +func SetKeyValueMap(bkt *bolt.Bucket, kvMap map[string][]byte) error { + if bkt == nil { + return fmt.Errorf("bucket must not be nil") + } + var err error + for k, v := range kvMap { + err = bkt.Put([]byte(k), v) + if err != nil { + return err + } + } + return nil +} + +func GetKeyValueStringMap(bkt *bolt.Bucket) (map[string]string, error) { + ret := make(map[string]string) + if bkt == nil { + return ret, fmt.Errorf("bucket must not be nil") + } + btMap, err := GetKeyValueMap(bkt) + if err != nil { + return ret, err + } + for k, v := range btMap { + ret[k] = string(v) + } + return ret, nil +} + +func SetKeyValueStringMap(bkt *bolt.Bucket, kvMap map[string]string) error { + chain := make(map[string][]byte) + for k, v := range kvMap { + chain[k] = []byte(v) + } + return SetKeyValueMap(bkt, chain) +} diff --git a/go.mod b/go.mod index 3e4bd93..00dd5a9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module git.bullercodeworks.com/brian/boltease -go 1.16 +go 1.23.0 require ( github.com/boltdb/bolt v1.3.1