2024-06-08 01:34:33 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2024-07-05 23:45:41 +00:00
|
|
|
"errors"
|
2024-06-08 01:34:33 +00:00
|
|
|
"fmt"
|
|
|
|
"path/filepath"
|
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
"unsafe"
|
|
|
|
|
|
|
|
"github.com/ying32/govcl/vcl"
|
|
|
|
"go.etcd.io/bbolt"
|
2024-06-14 23:43:24 +00:00
|
|
|
"go.etcd.io/bbolt/version"
|
2024-06-08 01:34:33 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type boltLoadedDatabase struct {
|
|
|
|
displayName string
|
|
|
|
path string
|
|
|
|
db *bbolt.DB
|
|
|
|
nav *vcl.TTreeNode
|
|
|
|
|
|
|
|
arena []*navData // keepalive
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ld *boltLoadedDatabase) DisplayName() string {
|
|
|
|
return ld.displayName
|
|
|
|
}
|
|
|
|
|
2024-06-14 23:43:24 +00:00
|
|
|
func (ld *boltLoadedDatabase) DriverName() string {
|
|
|
|
return "Bolt " + version.Version
|
|
|
|
}
|
|
|
|
|
2024-06-08 01:34:33 +00:00
|
|
|
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
|
2024-06-08 02:49:57 +00:00
|
|
|
// Bolt always uses Key + Value as the columns
|
|
|
|
colKey := f.contentBox.Columns().Add()
|
2024-07-05 07:21:08 +00:00
|
|
|
colKey.Title().SetCaption("Key")
|
2024-06-08 02:49:57 +00:00
|
|
|
colVal := f.contentBox.Columns().Add()
|
2024-07-05 07:21:08 +00:00
|
|
|
colVal.Title().SetCaption("Value")
|
2024-06-08 02:49:57 +00:00
|
|
|
|
2024-06-08 01:34:33 +00:00
|
|
|
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() {
|
2024-07-05 07:21:08 +00:00
|
|
|
rpos := f.contentBox.RowCount()
|
|
|
|
f.contentBox.SetRowCount(rpos + 1)
|
|
|
|
f.contentBox.SetCells(0, rpos, formatUtf8(k))
|
|
|
|
f.contentBox.SetCells(1, rpos, formatUtf8(v))
|
2024-06-08 01:34:33 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
vcl.ShowMessage(fmt.Sprintf("Failed to load data for bucket %q: %s", bucketDisplayName, err.Error()))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Valid
|
2024-07-05 07:35:24 +00:00
|
|
|
vcl_stringgrid_columnwidths(f.contentBox)
|
2024-06-08 01:34:33 +00:00
|
|
|
f.contentBox.SetEnabled(true)
|
|
|
|
}
|
|
|
|
|
2024-07-05 23:45:41 +00:00
|
|
|
func (n *boltLoadedDatabase) ApplyChanges(f *TMainForm, ndata *navData) error {
|
|
|
|
if n.db.IsReadOnly() {
|
|
|
|
return errors.New("Database was opened read-only")
|
|
|
|
}
|
|
|
|
|
|
|
|
// We have rendered row IDs, need to convert back to a bolt primary key
|
|
|
|
// TODO stash the real key inside f.contentBox.Objects()
|
|
|
|
// FIXME breaks if you try and edit the primary key(!)
|
|
|
|
primaryKeyForRendered := func(rowid int32) []byte {
|
|
|
|
return []byte(f.contentBox.Cells(0, rowid))
|
|
|
|
}
|
|
|
|
|
|
|
|
return n.db.Update(func(tx *bbolt.Tx) error {
|
|
|
|
|
|
|
|
// Get current bucket handle
|
|
|
|
b := boltTargetBucket(tx, ndata.bucketPath)
|
|
|
|
|
|
|
|
// Edit
|
|
|
|
for rowid, _ /*editcells*/ := range f.updateRows {
|
|
|
|
k := primaryKeyForRendered(rowid)
|
|
|
|
v := f.contentBox.Cells(1, rowid) // There's only one value cell
|
|
|
|
err := b.Put(k, []byte(v))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Updating cell %q: %w", formatUtf8(k), err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete by key (affects rowids after re-render)
|
|
|
|
for rowid, _ := range f.deleteRows {
|
|
|
|
k := primaryKeyForRendered(rowid)
|
|
|
|
err := b.Delete(k)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Deleting cell %q: %w", formatUtf8(k), err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Insert all new entries
|
|
|
|
for rowid, _ := range f.insertRows {
|
|
|
|
k := primaryKeyForRendered(rowid)
|
|
|
|
v := f.contentBox.Cells(1, rowid) // There's only one value cell
|
|
|
|
err := b.Put(k, []byte(v))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Inserting cell %q: %w", formatUtf8(k), err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Done
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-06-08 01:34:33 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2024-06-27 23:53:07 +00:00
|
|
|
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)
|
|
|
|
}
|
2024-06-27 23:34:00 +00:00
|
|
|
}
|
|
|
|
|
2024-07-05 23:59:55 +00:00
|
|
|
func (ld *boltLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error {
|
|
|
|
return errors.New("Bolt doesn't support querying")
|
2024-06-15 00:14:11 +00:00
|
|
|
}
|
|
|
|
|
2024-06-23 03:28:15 +00:00
|
|
|
func (ld *boltLoadedDatabase) Close() {
|
|
|
|
_ = ld.db.Close()
|
|
|
|
ld.arena = nil
|
|
|
|
}
|
|
|
|
|
2024-06-08 01:34:33 +00:00
|
|
|
var _ loadedDatabase = &boltLoadedDatabase{} // interface assertion
|
|
|
|
|
|
|
|
//
|
|
|
|
|
2024-06-27 23:33:43 +00:00
|
|
|
func (f *TMainForm) boltAddDatabaseFromFile(path string, readonly bool) {
|
2024-06-08 01:34:33 +00:00
|
|
|
// TODO load in background thread to stop blocking the UI
|
2024-06-27 23:33:43 +00:00
|
|
|
|
|
|
|
opts := bbolt.Options{
|
|
|
|
Timeout: 1 * time.Second,
|
|
|
|
ReadOnly: readonly,
|
|
|
|
}
|
|
|
|
|
|
|
|
db, err := bbolt.Open(path, 0644, &opts)
|
2024-06-08 01:34:33 +00:00
|
|
|
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,
|
|
|
|
}
|
2024-06-27 23:52:50 +00:00
|
|
|
if readonly {
|
|
|
|
ld.displayName += " (read-only)"
|
|
|
|
}
|
2024-06-08 01:34:33 +00:00
|
|
|
|
|
|
|
ld.nav = f.Buckets.Items().Add(nil, ld.displayName)
|
|
|
|
ld.nav.SetHasChildren(true) // dynamically populate in OnNavExpanding
|
2024-06-08 02:24:44 +00:00
|
|
|
ld.nav.SetImageIndex(imgDatabase)
|
2024-06-14 23:43:36 +00:00
|
|
|
ld.nav.SetSelectedIndex(imgDatabase)
|
2024-06-08 01:34:33 +00:00
|
|
|
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)
|
2024-06-23 02:54:49 +00:00
|
|
|
f.Buckets.SetSelected(ld.nav) // Select new element
|
2024-06-08 01:34:33 +00:00
|
|
|
|
|
|
|
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 {
|
2024-06-28 00:29:44 +00:00
|
|
|
return fmt.Errorf("Root bucket %q: %w", path[0], ErrNavNotExist)
|
2024-06-08 01:34:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for i := 1; i < len(path); i += 1 {
|
|
|
|
b = b.Bucket([]byte(path[i]))
|
|
|
|
if b == nil {
|
2024-06-28 00:29:44 +00:00
|
|
|
return fmt.Errorf("Bucket %q: %w", strings.Join(path[0:i], `/`), ErrNavNotExist)
|
2024-06-08 01:34:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|