Repository: wasmvision/wasmvision
Pull request #105: feature: add BoltDB Datastore
Author: login: diegohce
Created: 2025-04-15T14:26:32Z | Merged: 2025-04-15T16:07:52Z

This PR adds BoltDB as a Datastore backend


Diff:
diff --git a/.gitignore b/.gitignore
index 567609b..01f9cb9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 build/
+.vscode/
\ No newline at end of file
diff --git a/cv/context.go b/cv/context.go
index 859a648..5fd3836 100644
--- a/cv/context.go
+++ b/cv/context.go
@@ -22,6 +22,10 @@ func NewContext(modelsDir string, conf *config.Store, datastorage string, enable
 	switch datastorage {
 	case "memory":
 		store = storage.NewMemStorage[string]()
+
+	case "boltdb":
+		store = storage.NewBoltDBStorage()
+
 	default:
 		store = storage.NewMemStorage[string]()
 	}
diff --git a/datastore/processors.go b/datastore/processors.go
index aa60235..9921461 100644
--- a/datastore/processors.go
+++ b/datastore/processors.go
@@ -41,3 +41,13 @@ func (s *Processors) DeleteAll(processor string) {
 func (s *Processors) Exists(processor string) bool {
 	return s.store.Exists(processor)
 }
+
+// Error returns last operational error if any. nil otherwise.
+func (s *Processors) Error() error {
+	return nil
+}
+
+// Close closes the underlying storage.
+func (s *Processors) Close() error {
+	return nil
+}
diff --git a/datastore/processors_test.go b/datastore/processors_test.go
index dd503b8..bb818ee 100644
--- a/datastore/processors_test.go
+++ b/datastore/processors_test.go
@@ -1,6 +1,8 @@
 package datastore
 
 import (
+	"os"
+	"path"
 	"testing"
 
 	"github.com/wasmvision/wasmvision/datastore/storage"
@@ -25,3 +27,36 @@ func TestProcessors(t *testing.T) {
 		}
 	})
 }
+
+func TestProcessorsBoltDB(t *testing.T) {
+	t.Run("set/get", func(t *testing.T) {
+
+		dbFilename := path.Join(os.TempDir(), "test-bolt.db")
+
+		os.Setenv("WASMVISION_STORAGE_BOLTDB_FILENAME", dbFilename)
+		defer func() {
+			os.Unsetenv("WASMVISION_STORAGE_BOLTDB_FILENAME")
+			os.Remove(dbFilename)
+		}()
+
+		s := NewProcessors(storage.NewBoltDBStorage())
+
+		if s.Error() != nil {
+			t.Fatal(s.Error())
+		}
+
+		err := s.Set("proc", "key", "value")
+		if err != nil {
+			t.Errorf("unexpected error: %v", err)
+		}
+
+		val, ok := s.Get("proc", "key")
+		if !ok {
+			t.Errorf("key not found")
+		}
+
+		if string(val) != "value" {
+			t.Errorf("unexpected value: %s", val)
+		}
+	})
+}
diff --git a/datastore/storage.go b/datastore/storage.go
index 9ae2e0e..a7926ef 100644
--- a/datastore/storage.go
+++ b/datastore/storage.go
@@ -11,4 +11,6 @@ type DataStorage interface {
 	Delete(string, string)
 	DeleteAll(string)
 	Exists(string) bool
+	Error() error
+	Close() error
 }
diff --git a/datastore/storage/boltdb.go b/datastore/storage/boltdb.go
new file mode 100644
index 0000000..916fe1f
--- /dev/null
+++ b/datastore/storage/boltdb.go
@@ -0,0 +1,138 @@
+package storage
+
+import (
+	"errors"
+	"os"
+	"strconv"
+
+	bolt "go.etcd.io/bbolt"
+)
+
+type BoltDBStorage struct {
+	db      *bolt.DB
+	lastErr error
+}
+
+// NewBoltDBStorage Creates a new BoltDB storage
+func NewBoltDBStorage() *BoltDBStorage {
+
+	ds := &BoltDBStorage{}
+
+	fname := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILENAME")
+	if fname == "" {
+		ds.lastErr = errors.New("WASMVISION_STORAGE_BOLTDB_FILENAME env var not defined")
+		return ds
+	}
+
+	fmodeInt := 0644
+	fmode := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILEMODE")
+	if fmode != "" {
+		i, err := strconv.ParseInt(fmode, 8, 32)
+		if err == nil {
+			fmodeInt = int(i)
+		}
+	}
+
+	db, err := bolt.Open(fname, os.FileMode(fmodeInt), nil)
+	if err != nil {
+		ds.lastErr = err
+		return ds
+	}
+
+	return &BoltDBStorage{
+		db: db,
+	}
+}
+
+// Error returns last operational error
+func (ds *BoltDBStorage) Error() error {
+	return ds.lastErr
+}
+
+// Get returns a data value from the store.
+func (ds *BoltDBStorage) Get(root string, key string) (string, bool) {
+
+	value := ""
+
+	ds.lastErr = ds.db.View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(root))
+		if b == nil {
+			return errors.New("invalid bucket")
+		}
+		value = string(b.Get([]byte(key)))
+		return nil
+	})
+
+	return value, value != ""
+}
+
+// GetKeys returns all the keys for a specific id from the store.
+func (ds *BoltDBStorage) GetKeys(root string) ([]string, bool) {
+
+	var keys []string
+
+	ds.lastErr = ds.db.View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(root))
+		if b == nil {
+			return errors.New("invalid bucket")
+		}
+		err := b.ForEach(func(k, v []byte) error {
+			keys = append(keys, string(k))
+			return nil
+		})
+		return err
+	})
+
+	return keys, len(keys) > 0
+}
+
+// Set sets a config value in the store.
+func (ds *BoltDBStorage) Set(root string, key string, val string) error {
+
+	ds.lastErr = ds.db.Update(func(tx *bolt.Tx) error {
+		b, err := tx.CreateBucketIfNotExists([]byte(root))
+		if err != nil {
+			return err
+		}
+		return b.Put([]byte(key), []byte(val))
+	})
+
+	return ds.lastErr
+}
+
+// Delete deletes data from the store.
+func (ds *BoltDBStorage) Delete(root string, key string) {
+
+	ds.lastErr = ds.db.Update(func(tx *bolt.Tx) error {
+		b, err := tx.CreateBucketIfNotExists([]byte(root))
+		if err != nil {
+			return err
+		}
+		return b.Delete([]byte(key))
+	})
+}
+
+// DeleteAll deletes all data for a specific id from the store.
+func (ds *BoltDBStorage) DeleteAll(root string) {
+
+	ds.lastErr = ds.db.Update(func(tx *bolt.Tx) error {
+		return tx.DeleteBucket([]byte(root))
+	})
+}
+
+// Exists returns true if there is any data for a specific id in the store.
+func (ds *BoltDBStorage) Exists(root string) bool {
+	exists := false
+
+	ds.lastErr = ds.db.View(func(tx *bolt.Tx) error {
+		exists = tx.Bucket([]byte(root)) != nil
+		return nil
+	})
+
+	return exists
+}
+
+// Close closes BoltDB Storage
+func (ds *BoltDBStorage) Close() error {
+	return ds.db.Close()
+}
diff --git a/datastore/storage/memstorage.go b/datastore/storage/memstorage.go
index d2e1fcf..091bdbb 100644
--- a/datastore/storage/memstorage.go
+++ b/datastore/storage/memstorage.go
@@ -71,3 +71,13 @@ func (s *MemStorage[T]) Exists(key T) bool {
 	_, ok := s.storeMap[key]
 	return ok
 }
+
+// Error returns last operational error if any. nil otherwise.
+func (s *MemStorage[T]) Error() error {
+	return nil
+}
+
+// Close closes the underlying storage.
+func (s *MemStorage[T]) Close() error {
+	return nil
+}
diff --git a/go.mod b/go.mod
index 6c3546a..348fb1b 100644
--- a/go.mod
+++ b/go.mod
@@ -16,6 +16,7 @@ require (
 	github.com/orsinium-labs/tinytest v1.0.0
 	github.com/urfave/cli-altsrc/v3 v3.0.1
 	github.com/urfave/cli/v3 v3.1.1
+	go.etcd.io/bbolt v1.4.0
 	gopkg.in/yaml.v3 v3.0.1
 )
 
@@ -33,4 +34,5 @@ require (
 	github.com/mitchellh/go-testing-interface v1.14.1 // indirect
 	github.com/ulikunitz/xz v0.5.10 // indirect
 	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
+	golang.org/x/sys v0.32.0 // indirect
 )
diff --git a/go.sum b/go.sum
index 6c44176..fe7d448 100644
--- a/go.sum
+++ b/go.sum
@@ -50,8 +50,14 @@ github.com/urfave/cli/v3 v3.1.1 h1:bNnl8pFI5dxPOjeONvFCDFoECLQsceDG4ejahs4Jtxk=
 github.com/urfave/cli/v3 v3.1.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
 github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
 github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
+go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
+go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
 gocv.io/x/gocv v0.41.0 h1:KM+zRXUP28b6dHfhy+4JxDODbCNQNtLg8kio+YE7TqA=
 gocv.io/x/gocv v0.41.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=


Reviews:
user: login: deadprogram
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:32:59Z
user: login: deadprogram
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:34:26Z
user: login: deadprogram
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:36:45Z
user: login: deadprogram
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:38:41Z
user: login: diegohce
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T14:50:04Z
user: login: diegohce
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T14:50:27Z
user: login: deadprogram
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:53:10Z
user: login: deadprogram
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:53:21Z
user: login: deadprogram
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:53:32Z
user: login: deadprogram
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:53:47Z
user: login: diegohce
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-15T14:54:30Z
user: login: deadprogram
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:54:49Z
user: login: deadprogram
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T14:55:15Z

Review comments:
diff_hunk: @@ -25,3 +26,34 @@ func TestProcessors(t *testing.T) {
 		}
 	})
 }
+
+func TestProcessorsBoltDB(t *testing.T) {
+	t.Run("set/get", func(t *testing.T) {
+
+		os.Setenv("WASMVISION_STORAGE_BOLTDB_FILENAME", "test-bolt.db")
path: datastore/processors_test.go
user: login: deadprogram
body: You could perhaps use `os.CreateTemp()` to create the DB file without having to manually delete it after the test.
created_at: 2025-04-15T14:32:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,151 @@
+package storage
+
+import (
+	"errors"
+	"os"
+	"strconv"
+
+	"github.com/boltdb/bolt"
+)
+
+type DatastoreBoltDB struct {
path: datastore/storage/boltdb.go
user: login: deadprogram
body: Perhaps name this `BoltDBStorage` to match the naming of `MemStorage` type?
created_at: 2025-04-15T14:34:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,6 +10,7 @@ require (
 
 require (
 	github.com/BurntSushi/toml v1.5.0
+	github.com/boltdb/bolt v1.3.1
path: go.mod
user: login: deadprogram
body: Should we be using the https://github.com/etcd-io/bbolt now?
created_at: 2025-04-15T14:36:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,151 @@
+package storage
+
+import (
+	"errors"
+	"os"
+	"strconv"
+
+	"github.com/boltdb/bolt"
+)
+
+type DatastoreBoltDB struct {
+	db      *bolt.DB
+	lastErr error
+}
+
+// NewBoltDBStorage Creates a new BoltDB storage
+func NewBoltDBStorage() *DatastoreBoltDB {
+
+	ds := &DatastoreBoltDB{}
+
+	fname := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILENAME")
+	if fname == "" {
+		ds.lastErr = errors.New("WASMVISION_STORAGE_BOLTDB_FILENAME env var not defined")
+		return ds
+	}
+
+	fmodeInt := 0644
+	fmode := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILEMODE")
+	if fmode != "" {
+		i, err := strconv.ParseInt(fmode, 8, 32)
+		if err == nil {
+			fmodeInt = int(i)
+		}
+	}
+
+	db, err := bolt.Open(fname, os.FileMode(fmodeInt), nil)
+	if err != nil {
+		ds.lastErr = err
+		return ds
+	}
+
+	return &DatastoreBoltDB{
+		db: db,
+	}
+}
+
+// Error returns last operational error
+func (ds *DatastoreBoltDB) Error() error {
+	return ds.lastErr
+}
+
+// Get returns a data value from the store.
+func (ds *DatastoreBoltDB) Get(root string, key string) (string, bool) {
+
+	value := ""
+
+	err := ds.db.View(func(tx *bolt.Tx) error {
path: datastore/storage/boltdb.go
user: login: deadprogram
body: ```suggestion
	ds.lastErr = ds.db.View(func(tx *bolt.Tx) error {
```

Could do this and remove the assignment in line 66?
created_at: 2025-04-15T14:38:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,6 +10,7 @@ require (
 
 require (
 	github.com/BurntSushi/toml v1.5.0
+	github.com/boltdb/bolt v1.3.1
path: go.mod
user: login: diegohce
body: shoot.... don't know
created_at: 2025-04-15T14:50:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,151 @@
+package storage
+
+import (
+	"errors"
+	"os"
+	"strconv"
+
+	"github.com/boltdb/bolt"
+)
+
+type DatastoreBoltDB struct {
+	db      *bolt.DB
+	lastErr error
+}
+
+// NewBoltDBStorage Creates a new BoltDB storage
+func NewBoltDBStorage() *DatastoreBoltDB {
+
+	ds := &DatastoreBoltDB{}
+
+	fname := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILENAME")
+	if fname == "" {
+		ds.lastErr = errors.New("WASMVISION_STORAGE_BOLTDB_FILENAME env var not defined")
+		return ds
+	}
+
+	fmodeInt := 0644
+	fmode := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILEMODE")
+	if fmode != "" {
+		i, err := strconv.ParseInt(fmode, 8, 32)
+		if err == nil {
+			fmodeInt = int(i)
+		}
+	}
+
+	db, err := bolt.Open(fname, os.FileMode(fmodeInt), nil)
+	if err != nil {
+		ds.lastErr = err
+		return ds
+	}
+
+	return &DatastoreBoltDB{
+		db: db,
+	}
+}
+
+// Error returns last operational error
+func (ds *DatastoreBoltDB) Error() error {
+	return ds.lastErr
+}
+
+// Get returns a data value from the store.
+func (ds *DatastoreBoltDB) Get(root string, key string) (string, bool) {
+
+	value := ""
+
+	err := ds.db.View(func(tx *bolt.Tx) error {
path: datastore/storage/boltdb.go
user: login: diegohce
body: done
created_at: 2025-04-15T14:50:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,151 @@
+package storage
+
+import (
+	"errors"
+	"os"
+	"strconv"
+
+	"github.com/boltdb/bolt"
+)
+
+type DatastoreBoltDB struct {
+	db      *bolt.DB
+	lastErr error
+}
+
+// NewBoltDBStorage Creates a new BoltDB storage
+func NewBoltDBStorage() *DatastoreBoltDB {
+
+	ds := &DatastoreBoltDB{}
+
+	fname := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILENAME")
+	if fname == "" {
+		ds.lastErr = errors.New("WASMVISION_STORAGE_BOLTDB_FILENAME env var not defined")
+		return ds
+	}
+
+	fmodeInt := 0644
+	fmode := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILEMODE")
+	if fmode != "" {
+		i, err := strconv.ParseInt(fmode, 8, 32)
+		if err == nil {
+			fmodeInt = int(i)
+		}
+	}
+
+	db, err := bolt.Open(fname, os.FileMode(fmodeInt), nil)
+	if err != nil {
+		ds.lastErr = err
+		return ds
+	}
+
+	return &DatastoreBoltDB{
+		db: db,
+	}
+}
+
+// Error returns last operational error
+func (ds *DatastoreBoltDB) Error() error {
+	return ds.lastErr
+}
+
+// Get returns a data value from the store.
+func (ds *DatastoreBoltDB) Get(root string, key string) (string, bool) {
+
+	value := ""
+
+	err := ds.db.View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(root))
+		if b == nil {
+			return errors.New("invalid bucket")
+		}
+		value = string(b.Get([]byte(key)))
+		return nil
+	})
+
+	ds.lastErr = err
+
+	return value, value != ""
+}
+
+// GetKeys returns all the keys for a specific id from the store.
+func (ds *DatastoreBoltDB) GetKeys(root string) ([]string, bool) {
+
+	var keys []string
+
+	err := ds.db.View(func(tx *bolt.Tx) error {
path: datastore/storage/boltdb.go
user: login: deadprogram
body: Same as above.
created_at: 2025-04-15T14:53:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,151 @@
+package storage
+
+import (
+	"errors"
+	"os"
+	"strconv"
+
+	"github.com/boltdb/bolt"
+)
+
+type DatastoreBoltDB struct {
+	db      *bolt.DB
+	lastErr error
+}
+
+// NewBoltDBStorage Creates a new BoltDB storage
+func NewBoltDBStorage() *DatastoreBoltDB {
+
+	ds := &DatastoreBoltDB{}
+
+	fname := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILENAME")
+	if fname == "" {
+		ds.lastErr = errors.New("WASMVISION_STORAGE_BOLTDB_FILENAME env var not defined")
+		return ds
+	}
+
+	fmodeInt := 0644
+	fmode := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILEMODE")
+	if fmode != "" {
+		i, err := strconv.ParseInt(fmode, 8, 32)
+		if err == nil {
+			fmodeInt = int(i)
+		}
+	}
+
+	db, err := bolt.Open(fname, os.FileMode(fmodeInt), nil)
+	if err != nil {
+		ds.lastErr = err
+		return ds
+	}
+
+	return &DatastoreBoltDB{
+		db: db,
+	}
+}
+
+// Error returns last operational error
+func (ds *DatastoreBoltDB) Error() error {
+	return ds.lastErr
+}
+
+// Get returns a data value from the store.
+func (ds *DatastoreBoltDB) Get(root string, key string) (string, bool) {
+
+	value := ""
+
+	err := ds.db.View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(root))
+		if b == nil {
+			return errors.New("invalid bucket")
+		}
+		value = string(b.Get([]byte(key)))
+		return nil
+	})
+
+	ds.lastErr = err
+
+	return value, value != ""
+}
+
+// GetKeys returns all the keys for a specific id from the store.
+func (ds *DatastoreBoltDB) GetKeys(root string) ([]string, bool) {
+
+	var keys []string
+
+	err := ds.db.View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(root))
+		if b == nil {
+			return errors.New("invalid bucket")
+		}
+		err := b.ForEach(func(k, v []byte) error {
+			keys = append(keys, string(k))
+			return nil
+		})
+		return err
+	})
+
+	ds.lastErr = err
+
+	return keys, len(keys) > 0
+}
+
+// Set sets a config value in the store.
+func (ds *DatastoreBoltDB) Set(root string, key string, val string) error {
+
+	err := ds.db.Update(func(tx *bolt.Tx) error {
path: datastore/storage/boltdb.go
user: login: deadprogram
body: Same as above.
created_at: 2025-04-15T14:53:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,151 @@
+package storage
+
+import (
+	"errors"
+	"os"
+	"strconv"
+
+	"github.com/boltdb/bolt"
+)
+
+type DatastoreBoltDB struct {
+	db      *bolt.DB
+	lastErr error
+}
+
+// NewBoltDBStorage Creates a new BoltDB storage
+func NewBoltDBStorage() *DatastoreBoltDB {
+
+	ds := &DatastoreBoltDB{}
+
+	fname := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILENAME")
+	if fname == "" {
+		ds.lastErr = errors.New("WASMVISION_STORAGE_BOLTDB_FILENAME env var not defined")
+		return ds
+	}
+
+	fmodeInt := 0644
+	fmode := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILEMODE")
+	if fmode != "" {
+		i, err := strconv.ParseInt(fmode, 8, 32)
+		if err == nil {
+			fmodeInt = int(i)
+		}
+	}
+
+	db, err := bolt.Open(fname, os.FileMode(fmodeInt), nil)
+	if err != nil {
+		ds.lastErr = err
+		return ds
+	}
+
+	return &DatastoreBoltDB{
+		db: db,
+	}
+}
+
+// Error returns last operational error
+func (ds *DatastoreBoltDB) Error() error {
+	return ds.lastErr
+}
+
+// Get returns a data value from the store.
+func (ds *DatastoreBoltDB) Get(root string, key string) (string, bool) {
+
+	value := ""
+
+	err := ds.db.View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(root))
+		if b == nil {
+			return errors.New("invalid bucket")
+		}
+		value = string(b.Get([]byte(key)))
+		return nil
+	})
+
+	ds.lastErr = err
+
+	return value, value != ""
+}
+
+// GetKeys returns all the keys for a specific id from the store.
+func (ds *DatastoreBoltDB) GetKeys(root string) ([]string, bool) {
+
+	var keys []string
+
+	err := ds.db.View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(root))
+		if b == nil {
+			return errors.New("invalid bucket")
+		}
+		err := b.ForEach(func(k, v []byte) error {
+			keys = append(keys, string(k))
+			return nil
+		})
+		return err
+	})
+
+	ds.lastErr = err
+
+	return keys, len(keys) > 0
+}
+
+// Set sets a config value in the store.
+func (ds *DatastoreBoltDB) Set(root string, key string, val string) error {
+
+	err := ds.db.Update(func(tx *bolt.Tx) error {
+		b, err := tx.CreateBucketIfNotExists([]byte(root))
+		if err != nil {
+			return err
+		}
+		return b.Put([]byte(key), []byte(val))
+	})
+
+	ds.lastErr = err
+
+	return err
+}
+
+// Delete deletes data from the store.
+func (ds *DatastoreBoltDB) Delete(root string, key string) {
+
+	err := ds.db.Update(func(tx *bolt.Tx) error {
path: datastore/storage/boltdb.go
user: login: deadprogram
body: Same.
created_at: 2025-04-15T14:53:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,151 @@
+package storage
+
+import (
+	"errors"
+	"os"
+	"strconv"
+
+	"github.com/boltdb/bolt"
+)
+
+type DatastoreBoltDB struct {
+	db      *bolt.DB
+	lastErr error
+}
+
+// NewBoltDBStorage Creates a new BoltDB storage
+func NewBoltDBStorage() *DatastoreBoltDB {
+
+	ds := &DatastoreBoltDB{}
+
+	fname := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILENAME")
+	if fname == "" {
+		ds.lastErr = errors.New("WASMVISION_STORAGE_BOLTDB_FILENAME env var not defined")
+		return ds
+	}
+
+	fmodeInt := 0644
+	fmode := os.Getenv("WASMVISION_STORAGE_BOLTDB_FILEMODE")
+	if fmode != "" {
+		i, err := strconv.ParseInt(fmode, 8, 32)
+		if err == nil {
+			fmodeInt = int(i)
+		}
+	}
+
+	db, err := bolt.Open(fname, os.FileMode(fmodeInt), nil)
+	if err != nil {
+		ds.lastErr = err
+		return ds
+	}
+
+	return &DatastoreBoltDB{
+		db: db,
+	}
+}
+
+// Error returns last operational error
+func (ds *DatastoreBoltDB) Error() error {
+	return ds.lastErr
+}
+
+// Get returns a data value from the store.
+func (ds *DatastoreBoltDB) Get(root string, key string) (string, bool) {
+
+	value := ""
+
+	err := ds.db.View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(root))
+		if b == nil {
+			return errors.New("invalid bucket")
+		}
+		value = string(b.Get([]byte(key)))
+		return nil
+	})
+
+	ds.lastErr = err
+
+	return value, value != ""
+}
+
+// GetKeys returns all the keys for a specific id from the store.
+func (ds *DatastoreBoltDB) GetKeys(root string) ([]string, bool) {
+
+	var keys []string
+
+	err := ds.db.View(func(tx *bolt.Tx) error {
+		b := tx.Bucket([]byte(root))
+		if b == nil {
+			return errors.New("invalid bucket")
+		}
+		err := b.ForEach(func(k, v []byte) error {
+			keys = append(keys, string(k))
+			return nil
+		})
+		return err
+	})
+
+	ds.lastErr = err
+
+	return keys, len(keys) > 0
+}
+
+// Set sets a config value in the store.
+func (ds *DatastoreBoltDB) Set(root string, key string, val string) error {
+
+	err := ds.db.Update(func(tx *bolt.Tx) error {
+		b, err := tx.CreateBucketIfNotExists([]byte(root))
+		if err != nil {
+			return err
+		}
+		return b.Put([]byte(key), []byte(val))
+	})
+
+	ds.lastErr = err
+
+	return err
+}
+
+// Delete deletes data from the store.
+func (ds *DatastoreBoltDB) Delete(root string, key string) {
+
+	err := ds.db.Update(func(tx *bolt.Tx) error {
+		b, err := tx.CreateBucketIfNotExists([]byte(root))
+		if err != nil {
+			return err
+		}
+		return b.Delete([]byte(key))
+	})
+
+	ds.lastErr = err
+}
+
+// DeleteAll deletes all data for a specific id from the store.
+func (ds *DatastoreBoltDB) DeleteAll(root string) {
+
+	err := ds.db.Update(func(tx *bolt.Tx) error {
path: datastore/storage/boltdb.go
user: login: deadprogram
body: same here.
created_at: 2025-04-15T14:53:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,151 @@
+package storage
+
+import (
+	"errors"
+	"os"
+	"strconv"
+
+	"github.com/boltdb/bolt"
+)
+
+type DatastoreBoltDB struct {
path: datastore/storage/boltdb.go
user: login: diegohce
body: done
created_at: 2025-04-15T14:54:30Z
author_association: COLLABORATOR
diff_hunk: @@ -10,6 +10,7 @@ require (
 
 require (
 	github.com/BurntSushi/toml v1.5.0
+	github.com/boltdb/bolt v1.3.1
path: go.mod
user: login: deadprogram
body: The `boltdb/bolt` repo says

> This repository was archived by the owner on Mar 9, 2019. It is now read-only.
created_at: 2025-04-15T14:54:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,151 @@
+package storage
+
+import (
+	"errors"
+	"os"
+	"strconv"
+
+	"github.com/boltdb/bolt"
+)
+
+type DatastoreBoltDB struct {
path: datastore/storage/boltdb.go
user: login: deadprogram
body: Note comment about naming.
created_at: 2025-04-15T14:55:15Z
author_association: CONTRIBUTOR

PR comments:
user: login: deadprogram
created_at: 2025-04-15T15:00:46Z
body: I think you still need to connect it here: https://github.com/wasmvision/wasmvision/blob/dev/cv/context.go#L22-L27
author_association: CONTRIBUTOR
user: login: deadprogram
created_at: 2025-04-15T16:07:37Z
body: Great work on this @diegohce thank you very much! Now squash/merging.
author_association: CONTRIBUTOR