package main import ( "errors" "fmt" "path/filepath" "sort" "strings" "time" "unsafe" "github.com/ying32/govcl/vcl" "go.etcd.io/bbolt" "go.etcd.io/bbolt/version" ) const ( boltFilter = "Bolt database|*.db|All files|*.*" ) 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) error { // 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 { return err } // Valid vcl_stringgrid_columnwidths(f.contentBox) f.contentBox.SetEnabled(true) return nil } 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 }) } 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) error { bucketName := "" if !vcl.InputQuery(APPNAME, "Enter a name for the new bucket:", &bucketName) { return nil // 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 { return fmt.Errorf("Error adding bucket: %w", err) } return nil } func (ld *boltLoadedDatabase) DeleteBucket(ndata *navData) error { 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 { return fmt.Errorf("Error deleting bucket %q: %w", strings.Join(ndata.bucketPath, `/`), err) } return nil } func (ld *boltLoadedDatabase) ExecQuery(query string, resultArea *vcl.TStringGrid) error { return ErrNotSupported } 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 }