255 lines
6.1 KiB
Go
255 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/ying32/govcl/vcl"
|
|
"go.etcd.io/bbolt"
|
|
"go.etcd.io/bbolt/version"
|
|
)
|
|
|
|
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) DriverName() string {
|
|
return "Bolt " + version.Version
|
|
}
|
|
|
|
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
|
|
// Bolt always uses Key + Value as the columns
|
|
colKey := f.contentBox.Columns().Add()
|
|
colKey.Title().SetCaption("Key")
|
|
colVal := f.contentBox.Columns().Add()
|
|
colVal.Title().SetCaption("Value")
|
|
|
|
err := ld.db.View(func(tx *bbolt.Tx) error {
|
|
b := boltTargetBucket(tx, ndata.bucketPath)
|
|
if b == nil {
|
|
// no such bucket
|
|
return nil
|
|
}
|
|
|
|
// Valid
|
|
c := b.Cursor()
|
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
|
rpos := f.contentBox.RowCount()
|
|
f.contentBox.SetRowCount(rpos + 1)
|
|
f.contentBox.SetCells(0, rpos, formatUtf8(k))
|
|
f.contentBox.SetCells(1, rpos, formatUtf8(v))
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
vcl.ShowMessage(fmt.Sprintf("Failed to load data for bucket %q: %s", bucketDisplayName, err.Error()))
|
|
return
|
|
}
|
|
|
|
// Valid
|
|
vcl_stringgrid_columnwidths(f.contentBox)
|
|
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)
|
|
}
|
|
|
|
func (ld *boltLoadedDatabase) NavContext(ndata *navData) (ret []contextAction, err error) {
|
|
ret = append(ret, contextAction{"Add bucket...", ld.AddChildBucket})
|
|
|
|
if len(ndata.bucketPath) > 0 {
|
|
ret = append(ret, contextAction{"Delete bucket", ld.DeleteBucket})
|
|
}
|
|
return
|
|
}
|
|
|
|
func (ld *boltLoadedDatabase) AddChildBucket(ndata *navData) {
|
|
bucketName := ""
|
|
if !vcl.InputQuery(APPNAME, "Enter a name for the new bucket:", &bucketName) {
|
|
return // cancel
|
|
}
|
|
|
|
err := ld.db.Update(func(tx *bbolt.Tx) error {
|
|
parent := boltTargetBucket(tx, ndata.bucketPath)
|
|
if parent != nil {
|
|
_, err := parent.CreateBucket([]byte(bucketName))
|
|
return err
|
|
}
|
|
|
|
// Top-level
|
|
_, err := tx.CreateBucket([]byte(bucketName))
|
|
return err
|
|
})
|
|
if err != nil {
|
|
vcl.ShowMessageFmt("Error adding bucket: %v", err)
|
|
}
|
|
}
|
|
|
|
func (ld *boltLoadedDatabase) DeleteBucket(ndata *navData) {
|
|
err := ld.db.Update(func(tx *bbolt.Tx) error {
|
|
// Find parent of this bucket.
|
|
if len(ndata.bucketPath) >= 2 {
|
|
// child bucket
|
|
parent := boltTargetBucket(tx, ndata.bucketPath[0:len(ndata.bucketPath)-1])
|
|
return parent.DeleteBucket([]byte(ndata.bucketPath[len(ndata.bucketPath)-1]))
|
|
} else {
|
|
// top-level bucket
|
|
return tx.DeleteBucket([]byte(ndata.bucketPath[0]))
|
|
}
|
|
})
|
|
if err != nil {
|
|
vcl.ShowMessageFmt("Error deleting bucket %q: %v", strings.Join(ndata.bucketPath, `/`), err)
|
|
}
|
|
}
|
|
|
|
func (ld *boltLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) {
|
|
vcl.ShowMessage("Bolt doesn't support querying")
|
|
}
|
|
|
|
func (ld *boltLoadedDatabase) Close() {
|
|
_ = ld.db.Close()
|
|
ld.arena = nil
|
|
}
|
|
|
|
var _ loadedDatabase = &boltLoadedDatabase{} // interface assertion
|
|
|
|
//
|
|
|
|
func (f *TMainForm) boltAddDatabaseFromFile(path string, readonly bool) {
|
|
// TODO load in background thread to stop blocking the UI
|
|
|
|
opts := bbolt.Options{
|
|
Timeout: 1 * time.Second,
|
|
ReadOnly: readonly,
|
|
}
|
|
|
|
db, err := bbolt.Open(path, 0644, &opts)
|
|
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,
|
|
}
|
|
if readonly {
|
|
ld.displayName += " (read-only)"
|
|
}
|
|
|
|
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
|
|
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
|
|
ld.nav.SetImageIndex(imgDatabase)
|
|
ld.nav.SetSelectedIndex(imgDatabase)
|
|
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)
|
|
f.Buckets.SetSelected(ld.nav) // Select new element
|
|
|
|
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("Root bucket %q: %w", path[0], ErrNavNotExist)
|
|
}
|
|
|
|
for i := 1; i < len(path); i += 1 {
|
|
b = b.Bucket([]byte(path[i]))
|
|
if b == nil {
|
|
return fmt.Errorf("Bucket %q: %w", strings.Join(path[0:i], `/`), ErrNavNotExist)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|