bolt: refactor extract to separate interface
This commit is contained in:
parent
d97c8872de
commit
f913b63c58
178
bolt.go
Normal file
178
bolt.go
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/ying32/govcl/vcl"
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type boltLoadedDatabase struct {
|
||||||
|
displayName string
|
||||||
|
path string
|
||||||
|
db *bbolt.DB
|
||||||
|
nav *vcl.TTreeNode
|
||||||
|
|
||||||
|
arena []*navData // keepalive
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *boltLoadedDatabase) DisplayName() string {
|
||||||
|
return ld.displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *boltLoadedDatabase) RootElement() *vcl.TTreeNode {
|
||||||
|
return ld.nav
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *boltLoadedDatabase) Keepalive(ndata *navData) {
|
||||||
|
ld.arena = append(ld.arena, ndata)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *boltLoadedDatabase) RenderForNav(f *TMainForm, ndata *navData) {
|
||||||
|
|
||||||
|
// Load properties
|
||||||
|
|
||||||
|
bucketDisplayName := strings.Join(ndata.bucketPath, `/`)
|
||||||
|
content := fmt.Sprintf("Selected database: %#v\n\n\nSelected bucket: %q\n", ld.db.Stats(), bucketDisplayName)
|
||||||
|
f.propertiesBox.SetText(content)
|
||||||
|
|
||||||
|
// Load data
|
||||||
|
|
||||||
|
f.contentBox.SetEnabled(false)
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
|
err := ld.db.View(func(tx *bbolt.Tx) error {
|
||||||
|
b := boltTargetBucket(tx, ndata.bucketPath)
|
||||||
|
if b == nil {
|
||||||
|
// no such bucket
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid
|
||||||
|
f.contentBox.Clear()
|
||||||
|
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
|
dataEntry := f.contentBox.Items().Add()
|
||||||
|
dataEntry.SetCaption(formatUtf8(k))
|
||||||
|
dataEntry.SubItems().Add(formatUtf8(v))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage(fmt.Sprintf("Failed to load data for bucket %q: %s", bucketDisplayName, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid
|
||||||
|
f.contentBox.SetEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ld *boltLoadedDatabase) NavChildren(ndata *navData) ([]string, error) {
|
||||||
|
// In the bolt implementation, the nav is a recursive tree of child buckets
|
||||||
|
return boltChildBucketNames(ld.db, ndata.bucketPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ loadedDatabase = &boltLoadedDatabase{} // interface assertion
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
func (f *TMainForm) addDatabaseFromFile(path string) {
|
||||||
|
// TODO load in background thread to stop blocking the UI
|
||||||
|
db, err := bbolt.Open(path, 0644, &bbolt.Options{Timeout: 1 * time.Second})
|
||||||
|
if err != nil {
|
||||||
|
vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ld := &boltLoadedDatabase{
|
||||||
|
path: path,
|
||||||
|
displayName: filepath.Base(path),
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
|
||||||
|
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
|
||||||
|
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
|
||||||
|
navData := &navData{
|
||||||
|
ld: ld,
|
||||||
|
childrenLoaded: false, // will be loaded dynamically
|
||||||
|
bucketPath: []string{}, // empty = root
|
||||||
|
}
|
||||||
|
ld.nav.SetData(unsafe.Pointer(navData))
|
||||||
|
|
||||||
|
f.dbs = append(f.dbs, ld)
|
||||||
|
|
||||||
|
ld.Keepalive(navData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func boltTargetBucket(tx *bbolt.Tx, path []string) *bbolt.Bucket {
|
||||||
|
|
||||||
|
// If we are already deep in buckets, go directly there to find children
|
||||||
|
if len(path) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b := tx.Bucket([]byte(path[0]))
|
||||||
|
if b == nil {
|
||||||
|
return nil // unexpectedly missing
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < len(path); i += 1 {
|
||||||
|
b = b.Bucket([]byte(path[i]))
|
||||||
|
if b == nil {
|
||||||
|
return nil // unexpectedly missing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b // OK
|
||||||
|
}
|
||||||
|
|
||||||
|
func boltChildBucketNames(db *bbolt.DB, path []string) ([]string, error) {
|
||||||
|
var nextBucketNames []string
|
||||||
|
|
||||||
|
err := db.View(func(tx *bbolt.Tx) error {
|
||||||
|
|
||||||
|
// If we are already deep in buckets, go directly there to find children
|
||||||
|
if len(path) > 0 {
|
||||||
|
b := tx.Bucket([]byte(path[0]))
|
||||||
|
if b == nil {
|
||||||
|
return fmt.Errorf("Unexpected missing root bucket %q", path[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i < len(path); i += 1 {
|
||||||
|
b = b.Bucket([]byte(path[i]))
|
||||||
|
if b == nil {
|
||||||
|
return fmt.Errorf("Unexpected missing bucket %q", strings.Join(path[0:i], `/`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find child buckets of this bucket
|
||||||
|
b.ForEachBucket(func(bucketName []byte) error {
|
||||||
|
nextBucketNames = append(nextBucketNames, string(bucketName))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Find root bucket names
|
||||||
|
return tx.ForEach(func(bucketName []byte, _ *bbolt.Bucket) error {
|
||||||
|
nextBucketNames = append(nextBucketNames, string(bucketName))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// OK
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(nextBucketNames)
|
||||||
|
|
||||||
|
return nextBucketNames, nil
|
||||||
|
}
|
21
loadedDatabase.go
Normal file
21
loadedDatabase.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ying32/govcl/vcl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// loadedDatabase is a DB-agnostic interface for each loaded database.
|
||||||
|
type loadedDatabase interface {
|
||||||
|
DisplayName() string
|
||||||
|
RootElement() *vcl.TTreeNode
|
||||||
|
RenderForNav(f *TMainForm, ndata *navData)
|
||||||
|
NavChildren(ndata *navData) ([]string, error)
|
||||||
|
Keepalive(ndata *navData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// navData is the .Data() pointer for each TTreeNode in the left-hand tree.
|
||||||
|
type navData struct {
|
||||||
|
ld loadedDatabase
|
||||||
|
childrenLoaded bool
|
||||||
|
bucketPath []string
|
||||||
|
}
|
156
main.go
156
main.go
@ -2,10 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"os"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
@ -13,22 +11,8 @@ import (
|
|||||||
"github.com/ying32/govcl/vcl"
|
"github.com/ying32/govcl/vcl"
|
||||||
"github.com/ying32/govcl/vcl/types"
|
"github.com/ying32/govcl/vcl/types"
|
||||||
|
|
||||||
"go.etcd.io/bbolt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type loadedDatabase struct {
|
|
||||||
displayName string
|
|
||||||
path string
|
|
||||||
db *bbolt.DB
|
|
||||||
nav *vcl.TTreeNode
|
|
||||||
}
|
|
||||||
|
|
||||||
type navData struct {
|
|
||||||
ld *loadedDatabase
|
|
||||||
childrenLoaded bool
|
|
||||||
bucketPath []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TMainForm struct {
|
type TMainForm struct {
|
||||||
*vcl.TForm
|
*vcl.TForm
|
||||||
Menu *vcl.TMainMenu
|
Menu *vcl.TMainMenu
|
||||||
@ -38,7 +22,6 @@ type TMainForm struct {
|
|||||||
contentBox *vcl.TListView
|
contentBox *vcl.TListView
|
||||||
|
|
||||||
dbs []loadedDatabase
|
dbs []loadedDatabase
|
||||||
arena []*navData // keepalive
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -135,43 +118,7 @@ func (f *TMainForm) OnNavChange(sender vcl.IObject, node *vcl.TTreeNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ndata := (*navData)(node.Data())
|
ndata := (*navData)(node.Data())
|
||||||
|
ndata.ld.RenderForNav(f, ndata) // Handover to the database type's own renderer function
|
||||||
// Load properties
|
|
||||||
|
|
||||||
bucketDisplayName := strings.Join(ndata.bucketPath, `/`)
|
|
||||||
content := fmt.Sprintf("Selected database: %#v\n\n\nSelected bucket: %q\n", ndata.ld.db.Stats(), bucketDisplayName)
|
|
||||||
f.propertiesBox.SetText(content)
|
|
||||||
|
|
||||||
// Load data
|
|
||||||
|
|
||||||
f.contentBox.SetEnabled(false)
|
|
||||||
f.contentBox.Clear()
|
|
||||||
|
|
||||||
err := ndata.ld.db.View(func(tx *bbolt.Tx) error {
|
|
||||||
b := boltTargetBucket(tx, ndata.bucketPath)
|
|
||||||
if b == nil {
|
|
||||||
// no such bucket
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid
|
|
||||||
f.contentBox.Clear()
|
|
||||||
|
|
||||||
c := b.Cursor()
|
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
|
||||||
dataEntry := f.contentBox.Items().Add()
|
|
||||||
dataEntry.SetCaption(formatUtf8(k))
|
|
||||||
dataEntry.SubItems().Add(formatUtf8(v))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
vcl.ShowMessage(fmt.Sprintf("Failed to load data for bucket %q: %s", bucketDisplayName, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Valid
|
|
||||||
f.contentBox.SetEnabled(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatUtf8(in []byte) string {
|
func formatUtf8(in []byte) string {
|
||||||
@ -182,73 +129,6 @@ func formatUtf8(in []byte) string {
|
|||||||
return string(in)
|
return string(in)
|
||||||
}
|
}
|
||||||
|
|
||||||
func boltTargetBucket(tx *bbolt.Tx, path []string) *bbolt.Bucket {
|
|
||||||
|
|
||||||
// If we are already deep in buckets, go directly there to find children
|
|
||||||
if len(path) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
b := tx.Bucket([]byte(path[0]))
|
|
||||||
if b == nil {
|
|
||||||
return nil // unexpectedly missing
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 1; i < len(path); i += 1 {
|
|
||||||
b = b.Bucket([]byte(path[i]))
|
|
||||||
if b == nil {
|
|
||||||
return nil // unexpectedly missing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return b // OK
|
|
||||||
}
|
|
||||||
|
|
||||||
func boltChildBucketNames(db *bbolt.DB, path []string) ([]string, error) {
|
|
||||||
var nextBucketNames []string
|
|
||||||
|
|
||||||
err := db.View(func(tx *bbolt.Tx) error {
|
|
||||||
|
|
||||||
// If we are already deep in buckets, go directly there to find children
|
|
||||||
if len(path) > 0 {
|
|
||||||
b := tx.Bucket([]byte(path[0]))
|
|
||||||
if b == nil {
|
|
||||||
return fmt.Errorf("Unexpected missing root bucket %q", path[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 1; i < len(path); i += 1 {
|
|
||||||
b = b.Bucket([]byte(path[i]))
|
|
||||||
if b == nil {
|
|
||||||
return fmt.Errorf("Unexpected missing bucket %q", strings.Join(path[0:i], `/`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find child buckets of this bucket
|
|
||||||
b.ForEachBucket(func(bucketName []byte) error {
|
|
||||||
nextBucketNames = append(nextBucketNames, string(bucketName))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Find root bucket names
|
|
||||||
return tx.ForEach(func(bucketName []byte, _ *bbolt.Bucket) error {
|
|
||||||
nextBucketNames = append(nextBucketNames, string(bucketName))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// OK
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Strings(nextBucketNames)
|
|
||||||
|
|
||||||
return nextBucketNames, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allowExpansion *bool) {
|
func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allowExpansion *bool) {
|
||||||
|
|
||||||
if node.Data() == nil {
|
if node.Data() == nil {
|
||||||
@ -264,7 +144,7 @@ func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the child buckets from this point under the element
|
// Find the child buckets from this point under the element
|
||||||
nextBucketNames, err := boltChildBucketNames(ndata.ld.db, ndata.bucketPath)
|
nextBucketNames, err := ndata.ld.NavChildren(ndata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vcl.ShowMessage(fmt.Sprintf("Failed to find child buckets under %q: %s", strings.Join(ndata.bucketPath, `/`), err.Error()))
|
vcl.ShowMessage(fmt.Sprintf("Failed to find child buckets under %q: %s", strings.Join(ndata.bucketPath, `/`), err.Error()))
|
||||||
*allowExpansion = false
|
*allowExpansion = false
|
||||||
@ -292,36 +172,10 @@ func (f *TMainForm) OnNavExpanding(sender vcl.IObject, node *vcl.TTreeNode, allo
|
|||||||
navData.bucketPath = append(navData.bucketPath, ndata.bucketPath...)
|
navData.bucketPath = append(navData.bucketPath, ndata.bucketPath...)
|
||||||
navData.bucketPath = append(navData.bucketPath, bucketName)
|
navData.bucketPath = append(navData.bucketPath, bucketName)
|
||||||
node.SetData(unsafe.Pointer(navData))
|
node.SetData(unsafe.Pointer(navData))
|
||||||
f.arena = append(f.arena, navData) // keepalive
|
|
||||||
|
ndata.ld.Keepalive(navData)
|
||||||
}
|
}
|
||||||
|
|
||||||
*allowExpansion = true
|
*allowExpansion = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *TMainForm) addDatabaseFromFile(path string) {
|
|
||||||
// TODO load in background thread to stop blocking the UI
|
|
||||||
db, err := bbolt.Open(path, 0644, &bbolt.Options{Timeout: 1 * time.Second})
|
|
||||||
if err != nil {
|
|
||||||
vcl.ShowMessage(fmt.Sprintf("Failed to load database '%s': %s", path, err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := loadedDatabase{
|
|
||||||
path: path,
|
|
||||||
displayName: filepath.Base(path),
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.nav = f.Buckets.Items().Add(nil, entry.displayName)
|
|
||||||
entry.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
|
|
||||||
navData := &navData{
|
|
||||||
ld: &entry,
|
|
||||||
childrenLoaded: false, // will be loaded dynamically
|
|
||||||
bucketPath: []string{}, // empty = root
|
|
||||||
}
|
|
||||||
entry.nav.SetData(unsafe.Pointer(navData))
|
|
||||||
|
|
||||||
f.dbs = append(f.dbs, entry)
|
|
||||||
f.arena = append(f.arena, navData) // keepalive
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user