package main import ( "fmt" "path/filepath" "sort" "strings" "time" "unsafe" "github.com/ying32/govcl/vcl" "github.com/ying32/govcl/vcl/types" "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 f.contentBox.SetEnabled(false) f.contentBox.Clear() // Bolt always uses Key + Value as the columns f.contentBox.Columns().Clear() colKey := f.contentBox.Columns().Add() colKey.SetCaption("Key") colKey.SetWidth(MY_WIDTH) colKey.SetAlignment(types.TaLeftJustify) colVal := f.contentBox.Columns().Add() colVal.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 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) } func (ld *boltLoadedDatabase) NavContext(ndata *navData) ([]contextAction, error) { return nil, nil // No special actions are supported } func (ld *boltLoadedDatabase) ExecQuery(query string, resultArea *vcl.TListView) { 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("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 }