79 Commits

Author SHA1 Message Date
99096b2360 doc/README: add v1.0.2 changelog 2021-04-12 18:01:46 +12:00
0e92459779 no-op merge with legacy-codesite branch 2021-04-12 17:52:00 +12:00
a13eaaf523 make: upx doesn't support current mxe mingw/qt linked binaries 2021-04-12 17:44:38 +12:00
dca8d277d3 make: option to build windows binary in docker 2021-04-12 17:44:17 +12:00
15c739c0b9 squash 2021-04-12 17:43:59 +12:00
fc2da972ef dummy-data: fix build for renamed bbolt package 2021-04-12 17:07:16 +12:00
e45eea7111 makefile: remove 'hg export' function 2021-04-12 17:04:40 +12:00
1e62d79c07 doc: delete TODO.txt (issues now being tracked in Gitea)
The new issue tracker is at https://git.ivysaur.me/code.ivysaur.me/qbolt/issues
2021-04-12 17:04:27 +12:00
c74c5ae5c0 doc: move README/TODO to top-level directory 2021-04-12 17:00:49 +12:00
b0092c4a6e vendor: switch bolt implementation to etcd-io/bbolt v1.3.5 2021-04-12 17:00:40 +12:00
5ce6368c4a qt: fix missing -lpthread on Debian Bullseye 2021-04-12 16:54:06 +12:00
3e7e54da4b go: add go.mod file for bolt dependency 2021-04-12 16:53:57 +12:00
9f80d687b2 hg2git: replace hgignore/hgtags files 2021-04-12 16:53:43 +12:00
96411e877f bump all versions to 1.0.2 2017-06-19 21:04:40 +12:00
ac7e078c02 Added tag release-1.0.1 for changeset 0528a0ab20b6 2017-06-19 21:04:23 +12:00
e13314f5dc 1.0.1 meta 2017-06-19 21:04:17 +12:00
b50c3e738a doc: update features list in readme 2017-06-19 21:02:42 +12:00
54ad6015b7 more binary correctness 2017-06-19 20:53:50 +12:00
40e84ac230 dummy-data: change to generate binary names 2017-06-19 20:45:26 +12:00
767eaa0a47 add fallback display for non-printable characters 2017-06-19 20:42:51 +12:00
bb594e768c doc: update readme 2017-06-19 20:42:42 +12:00
516bd99c4d doc: update TODO 2017-06-19 20:28:33 +12:00
571bfcf4b6 one more preservation for previous 2017-06-19 20:27:52 +12:00
26f7a11d80 preserve the binary content of keys and bucket names during edit operations 2017-06-19 20:27:05 +12:00
21588021d3 doc: update TODO 2017-05-25 20:00:10 +12:00
7441e0c15b option to open database as read-only 2017-05-25 19:59:53 +12:00
142f3f6bf4 track screenshot of qbolt on windows 2017-05-25 19:54:35 +12:00
19ddb1c956 select parent when deleting bucket (maybe fixes a crash?) 2017-05-25 19:53:35 +12:00
97467eae4d add icon for win32 binary 2017-05-25 19:50:11 +12:00
17c37b6568 bump dist version to 1.0.1 2017-05-21 18:23:41 +12:00
57237ef2e8 Added tag release-1.0.0 for changeset 74cacbbe8f6c 2017-05-21 18:23:29 +12:00
164f5a071d doc: update README 2017-05-21 18:22:23 +12:00
ac46b16d0e hgignore: simplify 2017-05-21 18:19:47 +12:00
b9d114a2d8 remove extra .db file from dist archives 2017-05-21 18:18:45 +12:00
b368457247 add some screenshots 2017-05-21 18:18:14 +12:00
fb940d5f60 doc: TODO.txt 2017-05-21 18:18:10 +12:00
6d1e671e44 doc: README 2017-05-21 18:18:04 +12:00
b5149c4efa bolt: strict 10 second timeout for opening databases 2017-05-21 18:05:20 +12:00
f0f642b6b0 add many more icons 2017-05-21 18:05:11 +12:00
7c62abb352 working add items 2017-05-21 17:59:27 +12:00
4f1c48b55d working edit/delete items 2017-05-21 17:53:40 +12:00
37f9307db1 wip set/delete items 2017-05-21 17:44:07 +12:00
14e7ebb59c go: simplify out a wrapper function 2017-05-21 17:26:09 +12:00
14510545d4 fix right-hand default pane when a bucket is selected 2017-05-21 17:14:32 +12:00
906cff2e57 working bucket deletions 2017-05-21 17:14:20 +12:00
bf28495b1f add our application icon to system popups 2017-05-21 17:12:02 +12:00
98b78ab1e6 option to create new buckets / sub-buckets 2017-05-21 17:08:15 +12:00
4394c8a50a option to create new databases 2017-05-21 16:50:17 +12:00
9fb5bdad78 always open databases read/write 2017-05-21 16:47:28 +12:00
d2ec12798e popup editor to view record content 2017-05-21 16:44:44 +12:00
5270dd00bc display data keys, data item length 2017-05-21 16:15:49 +12:00
35f28fa5ed makefile: fix qbolt.a target 2017-05-21 15:59:00 +12:00
5ea969e10c pretty-print json display 2017-05-21 15:49:47 +12:00
d0becd0c3c clean up qt project, always refer to makefile-produced qbolt.a files 2017-05-21 15:00:53 +12:00
1c81444645 makefile: distribution targets 2017-05-21 14:51:22 +12:00
d7c3bfd1f5 unify makefile, make win32 builds 2017-05-21 14:33:46 +12:00
0f1cc014d7 working 'refresh buckets' action 2017-05-21 13:49:41 +12:00
6ac8c3e67b remove dead code 2017-05-21 13:49:36 +12:00
546681a0ef working cgo slices, working nested-bucket operations 2017-05-21 13:47:46 +12:00
679d1140cc recursive bucket scan - we're crashing when passing a slice to go code 2017-05-21 13:31:28 +12:00
60d71104e8 retrieve bucket properties 2017-05-21 12:39:55 +12:00
95a1bfeea5 retrieve database properties 2017-05-21 11:59:38 +12:00
8597f270f6 context menus, disconnection, re-refresh, selection behaviour, ^O shortcut 2017-05-21 11:47:16 +12:00
d32ab82d73 don't pass go strings to c - pass char*+len, callee must call free() 2017-05-20 23:02:58 +12:00
b6d1cafe54 cgo: minor cleanups, remove unused export 2017-05-20 18:15:39 +12:00
3911d7d66c convert cgo function pointer call to repeated iteration 2017-05-20 18:01:55 +12:00
be594cb1a5 call bucket enumeration - function pointers are misbehaving 2017-05-20 17:28:27 +12:00
8a93f163c4 c++ wrappers for bucket enumeration function 2017-05-20 15:47:18 +12:00
e82d0a5dd2 add top-level db items to ui 2017-05-20 15:47:05 +12:00
4b3fb783c4 wire up other menu items, add app logo icon 2017-05-20 15:20:30 +12:00
e8030a9579 cgo: change interface to not return already-collected error interfaces 2017-05-20 15:13:16 +12:00
a94a330345 add GetMagic() check, move c/go interop into separate classes 2017-05-20 14:57:51 +12:00
8ce03cbed6 go: build .a instead of .so 2017-05-20 14:57:19 +12:00
b76fad9512 hgignore: pro.user file 2017-05-18 20:11:19 +12:00
a9158c3a0c gms: fix missing initialisation 2017-05-16 19:47:50 +12:00
dc31787526 track sample.db file 2017-05-16 19:47:41 +12:00
ff7484079e track dummy-data generator 2017-05-16 19:47:26 +12:00
6e1f9ca1e1 hgignore 2017-05-16 19:47:17 +12:00
ac56f5e6c8 initial commit 2017-05-16 19:34:54 +12:00
50 changed files with 2188 additions and 20 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
build-qbolt-*
dummy-data/dummy-data*
*.pro.user$
build/

View File

@@ -1,9 +0,0 @@
# Converted with codesite2git
project_name="qbolt"
short_description="A graphical database manager for BoltDB."
written_in_lang="C++ (Qt), Golang (CGo)"
topics=[]
ctime=1495324800
mtime=1497830400

72
Makefile Normal file
View File

@@ -0,0 +1,72 @@
export PATH := /usr/lib/mxe/usr/bin:$(PATH)
GOFLAGS := -ldflags='-s -w' -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)'
VERSION := 1.0.2
.PHONY: all libs dist clean
all: \
build/linux/qbolt \
build/win32/release/qbolt.exe
libs: \
build/linux/qbolt.a \
build/win32/qbolt.a
dist: \
build/dist/qbolt-${VERSION}-win32.zip \
build/dist/qbolt-${VERSION}-linux_amd64.tar.xz
clean:
if [ -f qbolt/qbolt.a ] ; then rm qbolt/qbolt.a ; fi
if [ -f qbolt ] ; then rm qbolt ; fi
if [ -d build ] ; then rm -r build ; fi
# Build core golang shared library (linux)
build/linux/qbolt.a: *.go
mkdir -p build/linux
go build ${GOFLAGS} -buildmode=c-archive -o build/linux/qbolt.a
# Build core golang shared library (win32)
build/win32/qbolt.a: *.go
mkdir -p build/win32
CC=/usr/lib/mxe/usr/bin/i686-w64-mingw32.static-gcc CGO_ENABLED=1 GOARCH=386 GOOS=windows \
go build ${GOFLAGS} -buildmode=c-archive -o build/win32/qbolt.a
# Linux binaries
build/linux/qbolt: build/linux/qbolt.a qbolt/*
cd build/linux && qmake ../../qbolt/qbolt.pro && make
# Linux distribution
build/dist/qbolt-${VERSION}-linux_amd64.tar.xz: build/linux/qbolt
XZ_OPTS=-9 tar caf build/dist/qbolt-${VERSION}-linux_amd64.tar.xz -C build/linux qbolt --owner=0 --group=0
# Windows binaries
build/win32/release/qbolt.exe: build/win32/qbolt.a qbolt/*
cd build/win32 && i686-w64-mingw32.static-qmake-qt5 ../../qbolt/qbolt.pro && make
# Dockerized Windows build
.PHONY: build-docker-build-environment
build-docker-build-environment:
cd docker && docker build -t win32-cross-qt-mxe:latest -f win32-cross-qt-mxe.Dockerfile
.PHONY: build-windows-in-docker
build-windows-in-docker:
docker run --rm -v $(CURDIR):/qbolt win32-cross-qt-mxe:latest /bin/sh -c 'cd /qbolt && make build/win32/release/qbolt.exe'
# Windows distribution
build/win32/dist/qbolt.exe: build/win32/release/qbolt.exe
mkdir -p build/win32/dist
cp build/win32/release/qbolt.exe build/win32/dist/qbolt.exe
# upx --lzma build/win32/dist/qbolt.exe
build/dist/qbolt-${VERSION}-win32.zip: build/win32/dist/qbolt.exe
mkdir -p build/dist
zip -0 -j build/dist/qbolt-${VERSION}-win32.zip build/win32/dist/qbolt.exe

56
MemoryStore.go Normal file
View File

@@ -0,0 +1,56 @@
package main
import "C"
import (
"errors"
"sync"
)
type ObjectReference int64
var NullObjectReference error = errors.New("Null object reference")
// GoMemoryStore is a int->interface storage structure so that Go pointers are
// never exposed to C code.
type GoMemoryStore struct {
mtx sync.RWMutex
items map[int64]interface{}
next int64
}
func NewGoMemoryStore() *GoMemoryStore {
ret := GoMemoryStore{}
ret.items = make(map[int64]interface{})
return &ret
}
func (this *GoMemoryStore) Put(itm interface{}) ObjectReference {
this.mtx.Lock()
defer this.mtx.Unlock()
key := this.next
this.items[key] = itm
this.next++
return ObjectReference(key)
}
func (this *GoMemoryStore) Get(i ObjectReference) (interface{}, bool) {
this.mtx.RLock()
defer this.mtx.RUnlock()
ret, ok := this.items[int64(i)]
return ret, ok
}
func (this *GoMemoryStore) Delete(i ObjectReference) {
this.mtx.Lock()
defer this.mtx.Unlock()
delete(this.items, int64(i))
}
var gms *GoMemoryStore = nil
func init() {
gms = NewGoMemoryStore()
}

View File

@@ -1,13 +1,13 @@
# qbolt
![](https://img.shields.io/badge/written%20in-C%2B%2B%20%28Qt%29%2C%20Golang%20%28CGo%29-blue)
A graphical database manager for BoltDB.
QBolt allows you to graphically view and edit the content of Bolt databases.
The project consists of two parts; a C binding (CGo) for the embeddable Bolt database engine, and a graphical interface built in C++/Qt that links to it.
Written in C++ (Qt), Golang (CGo)
## Features
- Open existing database or create new database
@@ -24,25 +24,26 @@ Source code content of `qbolt-x.x.x-src.tar.gz` is released under the ISC licens
BoltDB is released under the MIT license.
The Windows binary is released under LGPL-3+ owing to the static copy of Qt.
## See Also
## See also
- BoltDB https://github.com/boltdb/bolt
## Changelog
2020-04-12 1.0.2
- Rebuild artefacts with etcd-io/bbolt v1.3.5, go 1.15, Qt 5.15, and new GCC versions
- Switch from hg to Git
- Use Go modules
- Add support for building Windows binary in Docker
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.2)
2017-06-19 1.0.1
- Feature: Option to open database as read-only
- Fix an issue with support for bucket names and keys not surviving UTF-8 roundtrips (now binary-clean)
- Fix an issue with crashing when deleting a bucket other than the selected one
- Fix a cosmetic issue with application icon on Windows
- [ qbolt-1.0.1-win32.zip](dist-archive/qbolt-1.0.1-win32.zip) *(5.07 MiB)*
- [⬇️ qbolt-1.0.1-src.tar.gz](dist-archive/qbolt-1.0.1-src.tar.gz) *(265.88 KiB)*
- [⬇️ qbolt-1.0.1-linux_amd64.tar.xz](dist-archive/qbolt-1.0.1-linux_amd64.tar.xz) *(584.88 KiB)*
- [ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.1)
2017-05-21 1.0.0
- Initial public release
- [ qbolt-1.0.0-win32.zip](dist-archive/qbolt-1.0.0-win32.zip) *(5.07 MiB)*
- [⬇️ qbolt-1.0.0-src.tar.gz](dist-archive/qbolt-1.0.0-src.tar.gz) *(221.91 KiB)*
- [⬇️ qbolt-1.0.0-linux_amd64.tar.xz](dist-archive/qbolt-1.0.0-linux_amd64.tar.xz) *(584.62 KiB)*
- [ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.0)

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,13 @@
FROM debian:bullseye
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
apt-get install -qyy gnupg2 golang-go ca-certificates
RUN DEBIAN_FRONTEND=noninteractive \
echo "deb https://pkg.mxe.cc/repos/apt buster main" >/etc/apt/sources.list.d/mxeapt.list && \
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 86B72ED9 && \
apt-get update && \
apt-get install -qyy mxe-i686-w64-mingw32.static-qt5 && \
apt-get clean
ENV PATH=/usr/lib/mxe/usr/bin:$PATH

83
dummy-data/main.go Normal file
View File

@@ -0,0 +1,83 @@
package main
import (
//"fmt"
"math/rand"
"os"
bolt "github.com/boltdb/bolt"
)
func random_name() string {
ret := make([]byte, 12)
rand.Read(ret)
return string(ret)
//return fmt.Sprintf("%08x-%08x-%08x", rand.Int63(), rand.Int63(), rand.Int63())
}
func fill_bucket(tx *bolt.Tx, bucket *bolt.Bucket) error {
// fill with some basic items
for i := 0; i < 30; i += 1 {
err := bucket.Put([]byte(random_name()), []byte("SAMPLE CONTENT "+random_name()))
if err != nil {
return err
}
}
// 1/20 (5%) chance of recursion
if rand.Intn(100) >= 95 {
for i := 0; i < 5; i += 1 {
child, err := bucket.CreateBucket([]byte(random_name()))
if err != nil {
return err
}
err = fill_bucket(tx, child)
if err != nil {
return err
}
}
}
return nil
}
func main() {
db, err := bolt.Open("sample.db", 0644, bolt.DefaultOptions)
if err != nil {
panic(err)
}
err = db.Update(func(tx *bolt.Tx) error {
// top-level buckets
for i := 0; i < 50; i += 1 {
bucketName := random_name()
bucket, err := tx.CreateBucket([]byte(bucketName))
if err != nil {
return err
}
err = fill_bucket(tx, bucket)
if err != nil {
return err
}
}
return nil
})
if err != nil {
panic(err)
}
err = db.Close()
if err != nil {
panic(err)
}
os.Exit(0)
}

7
go.mod Normal file
View File

@@ -0,0 +1,7 @@
module code.ivysaur.me/qbolt
go 1.15
require github.com/boltdb/bolt v1.3.1
replace github.com/boltdb/bolt => go.etcd.io/bbolt v1.3.5

5
go.sum Normal file
View File

@@ -0,0 +1,5 @@
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

398
main.go Normal file
View File

@@ -0,0 +1,398 @@
package main
import "C"
import (
"encoding/binary"
"encoding/json"
"errors"
"os"
"time"
bolt "github.com/boltdb/bolt"
)
const (
ERROR_AND_STOP_CALLING int64 = 100
ERROR_AND_KEEP_CALLING = 101
FINISHED_OK = 102
REAL_MESSAGE = 103
)
const Magic int64 = 0x10203040
//export GetMagic
func GetMagic() int64 {
return Magic
}
//export Bolt_Open
func Bolt_Open(readOnly bool, path string) (ObjectReference, *C.char, int) {
opts := *bolt.DefaultOptions
opts.Timeout = 10 * time.Second
opts.ReadOnly = readOnly
ptrDB, err := bolt.Open(path, os.FileMode(0644), &opts)
if err != nil {
errMsg := err.Error()
return 0, C.CString(errMsg), len(errMsg)
}
dbRef := gms.Put(ptrDB)
return dbRef, nil, 0
}
func withBoltDBReference(b ObjectReference, fn func(db *bolt.DB) error) error {
dbIFC, ok := gms.Get(b)
if !ok {
return NullObjectReference
}
ptrDB, ok := dbIFC.(*bolt.DB)
if !ok {
return NullObjectReference
}
return fn(ptrDB)
}
func walkBuckets(tx *bolt.Tx, browse []string) (*bolt.Bucket, error) {
bucket := tx.Bucket([]byte(browse[0]))
if bucket == nil {
return nil, errors.New("Unknown bucket")
}
for i := 1; i < len(browse); i += 1 {
bucket = bucket.Bucket([]byte(browse[i]))
if bucket == nil {
return nil, errors.New("Unknown bucket")
}
}
return bucket, nil
}
func withBrowse_ReadOnly(b_ref ObjectReference, browse []string, fn func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error) error {
if len(browse) == 0 {
// not a bucket
return errors.New("No bucket selected")
}
return withBoltDBReference(b_ref, func(db *bolt.DB) error {
return db.View(func(tx *bolt.Tx) error {
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
// Walked the bucket chain, now run the user callback
return fn(db, tx, bucket)
})
})
}
func err2triple(err error) (int64, *C.char, int) {
if err != nil {
msg := err.Error()
return ERROR_AND_STOP_CALLING, C.CString(msg), len(msg)
}
return FINISHED_OK, nil, 0
}
//export Bolt_CreateBucket
func Bolt_CreateBucket(b_ref ObjectReference, browse []string, newBucket string) (int64, *C.char, int) {
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
return db.Update(func(tx *bolt.Tx) error {
if len(browse) == 0 {
// Top-level bucket
_, err := tx.CreateBucket([]byte(newBucket))
return err
} else {
// Deeper bucket
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
// Walked the bucket chain, now create the new bucket
_, err = bucket.CreateBucket([]byte(newBucket))
return err
}
})
})
return err2triple(err)
}
//export Bolt_DeleteBucket
func Bolt_DeleteBucket(b_ref ObjectReference, browse []string, delBucket string) (int64, *C.char, int) {
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
return db.Update(func(tx *bolt.Tx) error {
if len(browse) == 0 {
// Top-level bucket
return tx.DeleteBucket([]byte(delBucket))
} else {
// Deeper bucket
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
// Walked the bucket chain, now delete the selected bucket
return bucket.DeleteBucket([]byte(delBucket))
}
})
})
return err2triple(err)
}
//export Bolt_SetItem
func Bolt_SetItem(b_ref ObjectReference, browse []string, key, val string) (int64, *C.char, int) {
if len(browse) == 0 {
return err2triple(errors.New("Can't create top-level items"))
}
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
return db.Update(func(tx *bolt.Tx) error {
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
return bucket.Put([]byte(key), []byte(val))
})
})
return err2triple(err)
}
//export Bolt_DeleteItem
func Bolt_DeleteItem(b_ref ObjectReference, browse []string, key string) (int64, *C.char, int) {
if len(browse) == 0 {
return err2triple(errors.New("Can't create top-level items"))
}
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
return db.Update(func(tx *bolt.Tx) error {
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
return bucket.Delete([]byte(key))
})
})
return err2triple(err)
}
type CallResponse struct {
s string
e error
}
//export Bolt_DBStats
func Bolt_DBStats(b ObjectReference) (int64, *C.char, int) {
var stats bolt.Stats
err := withBoltDBReference(b, func(db *bolt.DB) error {
stats = db.Stats()
return nil
})
jBytes, err := json.Marshal(stats)
if err != nil {
return err2triple(err)
}
return REAL_MESSAGE, C.CString(string(jBytes)), len(jBytes)
}
//export Bolt_BucketStats
func Bolt_BucketStats(b ObjectReference, browse []string) (int64, *C.char, int) {
var stats bolt.BucketStats
err := withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
stats = bucket.Stats()
return nil
})
if err != nil {
return err2triple(err)
}
jBytes, err := json.Marshal(stats)
if err != nil {
return err2triple(err)
}
return REAL_MESSAGE, C.CString(string(jBytes)), len(jBytes)
}
type NextCall struct {
content chan CallResponse
}
//export Bolt_ListBuckets
func Bolt_ListBuckets(b ObjectReference, browse []string) ObjectReference {
pNC := &NextCall{
content: make(chan CallResponse, 0),
}
pNC_Ref := gms.Put(pNC)
go func() {
var err error
if len(browse) == 0 {
// root mode
err = withBoltDBReference(b, func(db *bolt.DB) error {
return db.View(func(tx *bolt.Tx) error {
return tx.ForEach(func(k []byte, _ *bolt.Bucket) error {
pNC.content <- CallResponse{s: string(k)}
return nil
})
})
})
} else {
// Nested-mode
err = withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
return bucket.ForEach(func(k, v []byte) error {
// non-nil v means it's a data item
if v == nil {
pNC.content <- CallResponse{s: string(k)}
}
return nil
})
})
}
if err != nil {
pNC.content <- CallResponse{e: err}
}
close(pNC.content)
}()
return pNC_Ref
}
//export Bolt_ListItems
func Bolt_ListItems(b ObjectReference, browse []string) ObjectReference {
pNC := &NextCall{
content: make(chan CallResponse, 0),
}
pNC_Ref := gms.Put(pNC)
go func() {
var err error
if len(browse) == 0 {
err = errors.New("No bucket specified")
} else {
// Nested-mode
err = withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
return bucket.ForEach(func(k, v []byte) error {
if v == nil {
return nil // nil v means it's a bucket, skip
}
itemLength := make([]byte, 8)
binary.LittleEndian.PutUint64(itemLength, uint64(len(v)))
pNC.content <- CallResponse{s: string(itemLength) + string(k)}
return nil
})
})
}
if err != nil {
pNC.content <- CallResponse{e: err}
}
close(pNC.content)
}()
return pNC_Ref
}
//export Bolt_GetItem
func Bolt_GetItem(b ObjectReference, browse []string, key string) (int64, *C.char, int) {
var ret *C.char = nil
var ret_len = 0
err := withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
d := bucket.Get([]byte(key))
ret = C.CString(string(d))
ret_len = len(d)
return nil
})
if err != nil {
return err2triple(err)
}
return REAL_MESSAGE, ret, ret_len
}
//export GetNext
func GetNext(oRef ObjectReference) (int64, *C.char, int) {
pNC_Iface, ok := gms.Get(oRef)
if !ok {
return err2triple(NullObjectReference)
}
pNC, ok := pNC_Iface.(*NextCall)
if !ok {
return err2triple(NullObjectReference)
}
cr, ok := <-pNC.content
if !ok {
gms.Delete(oRef)
return err2triple(nil)
}
if cr.e != nil {
msg := cr.e.Error()
return ERROR_AND_KEEP_CALLING, C.CString(msg), len(msg)
}
return REAL_MESSAGE, C.CString(cr.s), len(cr.s)
}
//export Bolt_ListBucketsAtRoot
func Bolt_ListBucketsAtRoot(b ObjectReference) ObjectReference {
return Bolt_ListBuckets(b, nil)
}
//export Bolt_Close
func Bolt_Close(b ObjectReference) (*C.char, int) {
err := withBoltDBReference(b, func(db *bolt.DB) error {
return db.Close()
})
if err != nil {
msg := err.Error()
return C.CString(msg), len(msg)
}
gms.Delete(b)
return nil, 0
}
func main() {
// virtual
}

219
qbolt/boltdb.cpp Normal file
View File

@@ -0,0 +1,219 @@
#include "boltdb.h"
#include <QtEndian>
BoltDB::BoltDB()
{
}
BoltDB* BoltDB::createFrom(QString filePath, bool readOnly, QString &errorOut)
{
QByteArray filePathBytes(filePath.toUtf8());
GoString filePathGS = Interop::toGoString_WeakRef(&filePathBytes);
auto open_ret = ::Bolt_Open(readOnly, filePathGS);
if (open_ret.r2 != 0) {
errorOut = QString::fromUtf8(open_ret.r1, open_ret.r2);
free(open_ret.r1);
return nullptr;
}
BoltDB *ret = new BoltDB();
ret->gmsDbRef = open_ret.r0;
return ret;
}
static const int ERROR_AND_STOP_CALLING = 100;
static const int ERROR_AND_KEEP_CALLING = 101;
static const int FINISHED_OK = 102;
static const int REAL_MESSAGE = 103;
static bool handleTriple(int r0, char* r1, int64_t r2, QString& errorOut) {
if (r0 == ERROR_AND_STOP_CALLING) {
errorOut = QString::fromUtf8(r1, r2);
free(r1);
return false;
} else if (r0 == FINISHED_OK) {
return true;
} else {
// ?? unreachable
return false;
}
}
bool BoltDB::addBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut)
{
GoSliceManagedWrapper browse(bucketPath);
GoString bucketNameGS = Interop::toGoString_WeakRef(&bucketName);
auto resp = ::Bolt_CreateBucket(this->gmsDbRef, browse.slice, bucketNameGS);
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
}
bool BoltDB::deleteBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut)
{
GoSliceManagedWrapper browse(bucketPath);
GoString bucketNameGS = Interop::toGoString_WeakRef(&bucketName);
auto resp = ::Bolt_DeleteBucket(this->gmsDbRef, browse.slice, bucketNameGS);
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
}
bool BoltDB::setItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QByteArray value, QString& errorOut)
{
GoSliceManagedWrapper browse(bucketPath);
GoString keyNameGS = Interop::toGoString_WeakRef(&keyName);
GoString valueGS = Interop::toGoString_WeakRef(&value);
auto resp = ::Bolt_SetItem(this->gmsDbRef, browse.slice, keyNameGS, valueGS);
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
}
bool BoltDB::deleteItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QString& errorOut)
{
GoSliceManagedWrapper browse(bucketPath);
GoString keyNameGS = Interop::toGoString_WeakRef(&keyName);
auto resp = ::Bolt_DeleteItem(this->gmsDbRef, browse.slice, keyNameGS);
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
}
bool BoltDB::listBucketsAtRoot(QString& errorOut, NameReciever cb)
{
auto listJob = ::Bolt_ListBucketsAtRoot(this->gmsDbRef);
return pumpNext(listJob, errorOut, cb);
}
bool BoltDB::listBuckets(const QList<QByteArray>& bucketPath, QString& errorOut, NameReciever cb)
{
if (bucketPath.size() == 0) {
return listBucketsAtRoot(errorOut, cb);
}
GoSliceManagedWrapper browse(bucketPath);
auto listJob = ::Bolt_ListBuckets(this->gmsDbRef, browse.slice);
return pumpNext(listJob, errorOut, cb);
}
bool BoltDB::listKeys(const QList<QByteArray>& bucketPath, QString& errorOut, std::function<void(QByteArray, int64_t)> cb)
{
GoSliceManagedWrapper browse(bucketPath);
auto listJob = ::Bolt_ListItems(this->gmsDbRef, browse.slice);
return pumpNext(listJob, errorOut, [=](QByteArray b) {
// First 8 bytes are little-endian uint64 len
int64_t dataLen = qFromLittleEndian<qint64>(b.mid(0, 8));
cb(b.mid(8), dataLen);
});
}
bool BoltDB::getData(const QList<QByteArray>& bucketPath, QByteArray key, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
{
GoSliceManagedWrapper browse(bucketPath);
GoString keyGS = Interop::toGoString_WeakRef(&key);
auto resp = ::Bolt_GetItem(this->gmsDbRef, browse.slice, keyGS);
if (resp.r0 == ERROR_AND_STOP_CALLING) {
onError(QString::fromUtf8(resp.r1, resp.r2));
free(resp.r1);
return false;
} else if (resp.r0 == REAL_MESSAGE) {
onSuccess(QByteArray(resp.r1, resp.r2));
free(resp.r1);
return true;
} else {
// ?? unreachable
return false;
}
}
bool BoltDB::pumpNext(GoInt64 jobRef, QString& errorOut, NameReciever cb)
{
errorOut.clear();
for(;;) {
auto gnr = ::GetNext(jobRef);
if (gnr.r0 == ERROR_AND_STOP_CALLING) {
errorOut.append(QString::fromUtf8(gnr.r1, gnr.r2)); // log error
free(gnr.r1);
break; // done
} else if (gnr.r0 == ERROR_AND_KEEP_CALLING) {
errorOut.append(QString::fromUtf8(gnr.r1, gnr.r2)); // log error
free(gnr.r1);
continue;
} else if (gnr.r0 == FINISHED_OK) {
// Once we hit this, the go-side will clean up the channel / associated goroutines
break;
} else if (gnr.r0 == REAL_MESSAGE) {
cb(QByteArray(gnr.r1, gnr.r2));
free(gnr.r1);
continue;
}
}
return (errorOut.length() == 0);
}
bool BoltDB::getStatsJSON(std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
{
auto statresp = Bolt_DBStats(this->gmsDbRef);
if (statresp.r0 == ERROR_AND_STOP_CALLING) {
onError(QString::fromUtf8(statresp.r1, statresp.r2));
free(statresp.r1);
return false;
} else if (statresp.r0 == REAL_MESSAGE) {
onSuccess(QByteArray(statresp.r1, statresp.r2));
free(statresp.r1);
return true;
} else {
// ?? shouldn't be reachable
return false;
}
}
bool BoltDB::getBucketStatsJSON(const QList<QByteArray>& bucketPath, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
{
GoSliceManagedWrapper sliceWrapper(bucketPath);
auto statresp = Bolt_BucketStats(this->gmsDbRef, sliceWrapper.slice);
if (statresp.r0 == ERROR_AND_STOP_CALLING) {
QString err = QString::fromUtf8(statresp.r1, statresp.r2);
free(statresp.r1);
onError(err);
return false;
} else if (statresp.r0 == REAL_MESSAGE) {
onSuccess(QByteArray(statresp.r1, statresp.r2));
free(statresp.r1);
return true;
} else {
// ?? shouldn't be reachable
return false;
}
}
BoltDB::~BoltDB()
{
auto err = ::Bolt_Close(this->gmsDbRef);
if (err.r1 != 0) {
// Error closing database!
// Need to display an alert... somewhere
free(err.r0);
}
}

46
qbolt/boltdb.h Normal file
View File

@@ -0,0 +1,46 @@
#ifndef BOLTDB_H
#define BOLTDB_H
#include "interop.h"
#include <functional>
typedef std::function<void(QByteArray)> NameReciever;
class BoltDB
{
protected:
BoltDB();
GoInt64 gmsDbRef;
public:
static BoltDB* createFrom(QString filePath, bool readOnly, QString &errorOut);
bool listBucketsAtRoot(QString& errorOut, NameReciever cb);
bool listBuckets(const QList<QByteArray>& bucketPath, QString& errorOut, NameReciever cb);
bool addBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut);
bool deleteBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut);
bool setItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QByteArray value, QString& errorOut);
bool deleteItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QString& errorOut);
bool listKeys(const QList<QByteArray>& bucketPath, QString& errorOut, std::function<void(QByteArray, int64_t)> cb);
bool getData(const QList<QByteArray>& bucketPath, QByteArray key, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError);
bool getStatsJSON(std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError);
bool getBucketStatsJSON(const QList<QByteArray>& bucketPath, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError);
~BoltDB();
protected:
bool pumpNext(GoInt64 jobRef, QString& errorOut, NameReciever cb);
};
#endif // BOLTDB_H

42
qbolt/interop.cpp Normal file
View File

@@ -0,0 +1,42 @@
#include "interop.h"
#include <QStringList>
Interop::Interop()
{
}
GoString Interop::toGoString_WeakRef(QByteArray *qba) {
return GoString{qba->data(), qba->length()};
}
int64_t Interop::GetMagic() {
return ::GetMagic();
}
//
GoSliceManagedWrapper::GoSliceManagedWrapper(const QList<QByteArray>& qsl) :
rawStrings(),
slice(),
strings(nullptr)
{
rawStrings.reserve(qsl.size());
strings = new GoString[qsl.size()];
for (int i = 0; i < qsl.size(); ++i) {
rawStrings.push_back( qsl.at(i) );
strings[i].p = rawStrings[i].data();
strings[i].n = rawStrings[i].size();
}
slice.data = static_cast<void*>(strings);
slice.len = qsl.size(); // * sizeof(GoString);
slice.cap = slice.len;
}
GoSliceManagedWrapper::~GoSliceManagedWrapper()
{
delete[] strings;
}

31
qbolt/interop.h Normal file
View File

@@ -0,0 +1,31 @@
#ifndef INTEROP_H
#define INTEROP_H
#include "qbolt_cgo.h"
#include <QString>
#include <QList>
class GoSliceManagedWrapper {
Q_DISABLE_COPY(GoSliceManagedWrapper)
public:
GoSliceManagedWrapper(const QList<QByteArray>& qsl);
~GoSliceManagedWrapper();
protected:
QList<QByteArray> rawStrings;
public:
GoSlice slice;
GoString *strings;
};
class Interop
{
public:
Interop();
static GoString toGoString_WeakRef(QByteArray *qba);
static int64_t GetMagic();
};
#endif // INTEROP_H

18
qbolt/itemwindow.cpp Normal file
View File

@@ -0,0 +1,18 @@
#include "itemwindow.h"
#include "ui_itemwindow.h"
ItemWindow::ItemWindow(QWidget *parent) :
QDialog(parent),
ui(new Ui::ItemWindow)
{
ui->setupUi(this);
}
ItemWindow::~ItemWindow()
{
delete ui;
}
QPlainTextEdit* ItemWindow::ContentArea() const {
return ui->contentArea;
}

25
qbolt/itemwindow.h Normal file
View File

@@ -0,0 +1,25 @@
#ifndef ITEMWINDOW_H
#define ITEMWINDOW_H
#include <QDialog>
#include <QPlainTextEdit>
namespace Ui {
class ItemWindow;
}
class ItemWindow : public QDialog
{
Q_OBJECT
public:
explicit ItemWindow(QWidget *parent = 0);
~ItemWindow();
QPlainTextEdit* ContentArea() const;
private:
Ui::ItemWindow *ui;
};
#endif // ITEMWINDOW_H

101
qbolt/itemwindow.ui Normal file
View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ItemWindow</class>
<widget class="QDialog" name="ItemWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>370</width>
<height>353</height>
</rect>
</property>
<property name="windowTitle">
<string/>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/database_lightning.png</normaloff>:/rsrc/database_lightning.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="verticalSpacing">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QPlainTextEdit" name="contentArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="resources.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ItemWindow</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>184</x>
<y>330</y>
</hint>
<hint type="destinationlabel">
<x>184</x>
<y>176</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ItemWindow</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>184</x>
<y>330</y>
</hint>
<hint type="destinationlabel">
<x>184</x>
<y>176</y>
</hint>
</hints>
</connection>
</connections>
</ui>

23
qbolt/main.cpp Normal file
View File

@@ -0,0 +1,23 @@
#include "mainwindow.h"
#include <QApplication>
#include "interop.h"
#include <QDebug>
int main(int argc, char *argv[])
{
int64_t magic = Interop::GetMagic();
if (magic != 0x10203040) {
qDebug() << "bad magic " << magic;
return 1;
}
QApplication a(argc, argv);
QApplication::setApplicationDisplayName("QBolt");
QApplication::setWindowIcon(QIcon(":/rsrc/database_lightning.png"));
MainWindow w;
w.show();
return a.exec();
}

478
qbolt/mainwindow.cpp Normal file
View File

@@ -0,0 +1,478 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "itemwindow.h"
#include "boltdb.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QJsonDocument>
#include <QInputDialog>
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
on_bucketTree_currentItemChanged(nullptr, nullptr);
databaseContext = new QMenu();
databaseContext->addAction(ui->actionRefresh_buckets);
databaseContext->addAction(ui->actionAdd_bucket);
databaseContext->addSeparator();
databaseContext->addAction(ui->actionDisconnect);
bucketContext = new QMenu();
bucketContext->addAction(ui->actionRefresh_buckets);
bucketContext->addAction(ui->actionAdd_bucket);
bucketContext->addSeparator();
bucketContext->addAction(ui->actionDelete_bucket);
}
MainWindow::~MainWindow()
{
delete ui;
}
static const int BdbPointerRole = Qt::UserRole + 1;
static const int BinaryDataRole = Qt::UserRole + 2;
#define SET_BDB(top, bdb) top->setData(0, BdbPointerRole, QVariant::fromValue<void*>(static_cast<void*>(bdb)))
#define GET_BDB(top) static_cast<BoltDB*>( top->data(0, BdbPointerRole).value<void*>() )
void MainWindow::on_actionNew_database_triggered()
{
QString file = QFileDialog::getSaveFileName(this, tr("Save new bolt database as..."));
if (file.length()) {
openDatabase(file, false);
}
}
void MainWindow::on_actionOpen_database_triggered()
{
QString file = QFileDialog::getOpenFileName(this, tr("Select bolt database..."));
if (file.length()) {
openDatabase(file, false);
}
}
void MainWindow::on_actionOpen_database_as_read_only_triggered()
{
QString file = QFileDialog::getOpenFileName(this, tr("Select bolt database..."));
if (file.length()) {
openDatabase(file, true);
}
}
void MainWindow::openDatabase(QString file, bool readOnly)
{
// Open
QString error;
auto *bdb = BoltDB::createFrom(file, readOnly, error);
if (bdb == nullptr) {
QMessageBox qmb;
qmb.setText(tr("Error opening database: %1").arg(error));
qmb.exec();
return;
}
QTreeWidgetItem *top = new QTreeWidgetItem();
top->setText(0, QFileInfo(file).fileName());
top->setIcon(0, QIcon(":/rsrc/database.png"));
SET_BDB(top, bdb);
ui->bucketTree->addTopLevelItem(top);
refreshBucketTree(top);
ui->bucketTree->setCurrentItem(top);
ui->bucketTree->expandItem(top);
}
static const QString getDisplayName(const QByteArray &qba) {
// FIXME the formatting isn't so great when control characters, etc. are used
// A C-style escape display, or the unicode-replacement-character would be preferable
QString ret(QString::fromUtf8(qba));
bool allPrintable = true;
for (auto i = ret.begin(), e = ret.end(); i != e; ++i) {
if (! i->isPrint()) {
allPrintable = false;
break;
}
}
if (allPrintable) {
return ret; // fine
}
// Some of the characters weren't printable.
// Build up a replacement string
QString replacement;
for (auto i = ret.begin(), e = ret.end(); i != e; ++i) {
replacement += i->isPrint() ? *i : QStringLiteral("\\u{%1}").arg(i->unicode());
}
return replacement;
}
void MainWindow::refreshBucketTree(QTreeWidgetItem* itm)
{
QTreeWidgetItem *top = itm;
QList<QByteArray> browsePath;
while(top->parent() != nullptr) {
browsePath.push_front(top->data(0, BinaryDataRole).toByteArray());
top = top->parent();
}
// Remove existing children
for (int i = itm->childCount(); i --> 0;) {
delete itm->takeChild(i);
}
auto *bdb = GET_BDB(top);
QString error;
bool ok = bdb->listBuckets(
browsePath,
error,
[=](QByteArray qba){
QTreeWidgetItem *child = new QTreeWidgetItem();
child->setText(0, getDisplayName(qba));
child->setData(0, BinaryDataRole, qba);
child->setIcon(0, QIcon(":/rsrc/table.png"));
itm->addChild(child);
refreshBucketTree(child);
}
);
if (!ok) {
QMessageBox qmb;
qmb.setText(tr("Error listing buckets: %1").arg(error));
qmb.exec();
// (continue)
}
}
void MainWindow::on_actionExit_triggered()
{
close();
}
void MainWindow::on_actionAbout_Qt_triggered()
{
QApplication::aboutQt();
}
void MainWindow::on_actionAbout_qbolt_triggered()
{
QMessageBox::about(
this,
QApplication::applicationDisplayName(),
"<b>QBolt</b><br>Graphical interface for managing Bolt databases<br><br>"
"- <a href='https://github.com/boltdb/bolt'>About BoltDB</a><br>"
"- <a href='http://www.famfamfam.com/lab/icons/silk/'>FamFamFam &quot;Silk&quot; icon set</a><br>"
"- <a href='https://code.ivysaur.me/qbolt'>QBolt homepage</a><br>"
);
}
void MainWindow::on_actionDisconnect_triggered()
{
QTreeWidgetItem *top = lastContextSelection;
if (top->parent()) {
return; // somehow we didn't select a top-level item
}
auto *bdb = GET_BDB(top);
// Remove UI
ui->bucketTree->clearSelection();
delete top;
// Disconnect from DB
delete bdb;
}
void MainWindow::on_bucketTree_customContextMenuRequested(const QPoint &pos)
{
auto *itm = ui->bucketTree->itemAt(pos);
if (itm == nullptr) {
return;
}
lastContextSelection = itm;
if (itm->parent() != nullptr) {
// Child item, show the bucket menu
bucketContext->popup(ui->bucketTree->mapToGlobal(pos));
} else {
// Top-level item, show the database menu
databaseContext->popup(ui->bucketTree->mapToGlobal(pos));
}
}
void MainWindow::on_actionRefresh_buckets_triggered()
{
refreshBucketTree(lastContextSelection);
}
void MainWindow::on_bucketTree_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous)
{
Q_UNUSED(previous);
if (current == nullptr) {
ui->stackedWidget->setVisible(false);
return;
}
ui->stackedWidget->setVisible(true);
if (current->parent() == nullptr) {
// Selected a database
ui->stackedWidget->setCurrentWidget(ui->databasePage);
ui->databasePropertiesArea->clear();
auto *bdb = GET_BDB(current);
bdb->getStatsJSON(
[=](QByteArray j) {
auto doc = QJsonDocument::fromJson(j);
ui->databasePropertiesArea->setPlainText(QString::fromUtf8(doc.toJson(QJsonDocument::Indented)));
},
[=](QString error) {
ui->databasePropertiesArea->setPlainText(tr("Error retrieving database statistics: %1").arg(error));
}
);
// Clean up foreign areas
ui->bucketPropertiesArea->clear();
ui->bucketData->clear();
} else {
// Selected a bucket
ui->stackedWidget->setCurrentWidget(ui->bucketPage);
ui->bucketPropertiesArea->clear();
QList<QByteArray> browse;
QTreeWidgetItem *top = current;
while (top->parent() != nullptr) {
browse.push_front(top->data(0, BinaryDataRole).toByteArray());
top = top->parent();
}
auto *bdb = GET_BDB(top);
bdb->getBucketStatsJSON(
browse,
[=](QByteArray j) {
auto doc = QJsonDocument::fromJson(j);
ui->bucketPropertiesArea->setPlainText(QString::fromUtf8(doc.toJson(QJsonDocument::Indented)));
},
[=](QString error) {
ui->bucketPropertiesArea->setPlainText(tr("Error retrieving bucket statistics: %1").arg(error));
}
);
// Load the data tab
refreshData(bdb, browse);
// Clean up foreign areas
ui->databasePropertiesArea->clear();
}
}
void MainWindow::refreshData(BoltDB *bdb, const QList<QByteArray>& browse)
{
// Load the data tab
ui->bucketData->clear();
QString err;
bool ok = bdb->listKeys(browse, err, [=](QByteArray name, int64_t dataLen) {
auto *itm = new QTreeWidgetItem();
itm->setText(0, getDisplayName(name));
itm->setData(0, BinaryDataRole, name);
itm->setText(1, QString("%1").arg(dataLen));
ui->bucketData->addTopLevelItem(itm);
});
if (! ok) {
QMessageBox qmb;
qmb.setText(tr("Error listing bucket content: %1").arg(err));
qmb.exec();
}
ui->bucketData->resizeColumnToContents(0);
on_bucketData_itemSelectionChanged();
}
void MainWindow::on_actionClear_selection_triggered()
{
ui->bucketTree->setCurrentItem(nullptr);
}
#define GET_ITM_TOP_BROWSE_BDB \
QTreeWidgetItem* itm = ui->bucketTree->currentItem(); \
if (itm == nullptr) { \
return; \
} \
QTreeWidgetItem* top = itm; \
QList<QByteArray> browse; \
while(top->parent() != nullptr) { \
browse.push_front(top->data(0, BinaryDataRole).toByteArray()); \
top = top->parent(); \
} \
auto *bdb = GET_BDB(top);
void MainWindow::openEditor(BoltDB *bdb, const QList<QByteArray>& saveAs, QByteArray saveAsKey, QByteArray currentContent)
{
auto iw = new ItemWindow();
iw->ContentArea()->setPlainText(QString::fromUtf8(currentContent));
iw->setWindowTitle(QString::fromUtf8(saveAsKey));
iw->setWindowModality(Qt::ApplicationModal); // we need this - otherwise we'll refresh a possibly-changed area after saving
connect(iw, &ItemWindow::finished, iw, [=](int exitCode){
if (exitCode == ItemWindow::Accepted) {
QString err;
if (! bdb->setItem(saveAs, saveAsKey, iw->ContentArea()->toPlainText().toUtf8(), err)) {
QMessageBox qmb;
qmb.setText(tr("Error saving item content: %1").arg(err));
qmb.exec();
}
refreshData(bdb, saveAs);
}
iw->deleteLater();
});
iw->show();
}
void MainWindow::on_bucketData_doubleClicked(const QModelIndex &index)
{
GET_ITM_TOP_BROWSE_BDB;
// Get item key
auto model = index.model();
const QByteArray& key = model->data(model->index(index.row(), 0), BinaryDataRole).toByteArray();
// DB lookup
bdb->getData(
browse,
key,
[=](QByteArray content) {
openEditor(bdb, browse, key, content);
},
[=](QString error) {
QMessageBox qmb;
qmb.setText(tr("Error loading item content: %1").arg(error));
qmb.exec();
}
);
}
void MainWindow::on_actionAdd_bucket_triggered()
{
GET_ITM_TOP_BROWSE_BDB;
// Prompt for bucket name
QString name = QInputDialog::getText(this, tr("New bucket"), tr("Enter a key for the new bucket:"));
if (name.length() == 0) {
return;
}
// Create
QString err;
if (! bdb->addBucket(browse, name.toUtf8(), err)) {
QMessageBox qmb;
qmb.setText(tr("Error creating bucket: %1").arg(err));
qmb.exec();
return;
}
// Refresh bucket list
refreshBucketTree(itm); // sub-tree only
ui->bucketTree->expandItem(itm);
}
void MainWindow::on_actionDelete_bucket_triggered()
{
GET_ITM_TOP_BROWSE_BDB;
// Prompt for confirmation
const QByteArray& bucketToDelete = itm->data(0, BinaryDataRole).toByteArray();
if (
QMessageBox::question(
this,
tr("Delete bucket"),
tr("Are you sure you want to remove the bucket '%1'?").arg(getDisplayName(bucketToDelete)),
QMessageBox::Yes,
QMessageBox::Cancel
) != QMessageBox::Yes
) {
return;
}
QTreeWidgetItem* parent = itm->parent();
// One level down
browse.pop_back();
QString err;
if (! bdb->deleteBucket(browse, bucketToDelete, err)) {
QMessageBox qmb;
qmb.setText(tr("Error removing bucket: %1").arg(err));
qmb.exec();
return;
}
// Refresh bucket list
refreshBucketTree(parent); // sub-tree only
ui->bucketTree->expandItem(parent);
ui->bucketTree->setCurrentItem(parent);
}
void MainWindow::on_AddDataButton_clicked()
{
GET_ITM_TOP_BROWSE_BDB;
// Prompt for bucket name
QString name = QInputDialog::getText(this, tr("New item"), tr("Enter a key for the new item:"));
if (name.length() == 0) {
return;
}
openEditor(bdb, browse, name.toUtf8(), QByteArray());
}
void MainWindow::on_DeleteDataButton_clicked()
{
GET_ITM_TOP_BROWSE_BDB;
auto selection = ui->bucketData->selectedItems();
if (selection.length() == 0) {
return; // nothing to do
}
// Prompt for confirmation
if (QMessageBox::question(this, tr("Delete items"), tr("Are you sure you want to remove %1 item(s)?").arg(selection.length()), QMessageBox::Yes, QMessageBox::Cancel) != QMessageBox::Yes) {
return;
}
QString err;
for (int i = selection.length(); i-->0;) {
if (! bdb->deleteItem(browse, selection[i]->data(0, BinaryDataRole).toByteArray(), err)) {
QMessageBox qmb;
qmb.setText(tr("Error removing item: %1").arg(err));
qmb.exec();
}
}
refreshData(bdb, browse);
}
void MainWindow::on_bucketData_itemSelectionChanged()
{
ui->DeleteDataButton->setEnabled( (ui->bucketData->selectedItems().size() > 0) );
}

70
qbolt/mainwindow.h Normal file
View File

@@ -0,0 +1,70 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include "boltdb.h"
#include <QMainWindow>
#include <QMenu>
#include <QTreeWidgetItem>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
private slots:
void on_actionOpen_database_triggered();
void on_actionExit_triggered();
void on_actionAbout_Qt_triggered();
void on_actionAbout_qbolt_triggered();
void on_actionDisconnect_triggered();
void on_bucketTree_customContextMenuRequested(const QPoint &pos);
void on_actionRefresh_buckets_triggered();
void on_bucketTree_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous);
void on_actionClear_selection_triggered();
void on_bucketData_doubleClicked(const QModelIndex &index);
void on_actionNew_database_triggered();
void on_actionAdd_bucket_triggered();
void on_actionDelete_bucket_triggered();
void on_AddDataButton_clicked();
void on_DeleteDataButton_clicked();
void on_bucketData_itemSelectionChanged();
void on_actionOpen_database_as_read_only_triggered();
protected:
void openDatabase(QString file, bool readOnly);
void refreshBucketTree(QTreeWidgetItem* top);
void refreshData(BoltDB *bdb, const QList<QByteArray>& browse);
void openEditor(BoltDB *bdb, const QList<QByteArray>& saveAs, QByteArray saveAsKey, QByteArray currentContent);
private:
Ui::MainWindow *ui;
QMenu *databaseContext;
QMenu *bucketContext;
QTreeWidgetItem* lastContextSelection;
};
#endif // MAINWINDOW_H

410
qbolt/mainwindow.ui Normal file
View File

@@ -0,0 +1,410 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>668</width>
<height>405</height>
</rect>
</property>
<property name="windowTitle">
<string>QBolt</string>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/database_lightning.png</normaloff>:/rsrc/database_lightning.png</iconset>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
</property>
<widget class="QTreeWidget" name="bucketTree">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<column>
<property name="text">
<string>Bucket</string>
</property>
</column>
</widget>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="databasePage">
<layout class="QGridLayout" name="gridLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QTabWidget" name="databaseTabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="databasePropertiesTab">
<attribute name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/chart_bar.png</normaloff>:/rsrc/chart_bar.png</iconset>
</attribute>
<attribute name="title">
<string>Database</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item row="0" column="0">
<widget class="QPlainTextEdit" name="databasePropertiesArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="plainText">
<string>No selection</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="bucketPage">
<layout class="QGridLayout" name="gridLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QTabWidget" name="bucketTabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="bucketPropertiesTab">
<attribute name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/chart_bar.png</normaloff>:/rsrc/chart_bar.png</iconset>
</attribute>
<attribute name="title">
<string>Bucket</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_5">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item row="0" column="0">
<widget class="QPlainTextEdit" name="bucketPropertiesArea">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="bucketDataTab">
<attribute name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/table.png</normaloff>:/rsrc/table.png</iconset>
</attribute>
<attribute name="title">
<string>Data</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_6">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item row="0" column="0" colspan="3">
<widget class="QTreeWidget" name="bucketData">
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="indentation">
<number>0</number>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>Key</string>
</property>
</column>
<column>
<property name="text">
<string>Data length</string>
</property>
</column>
</widget>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="AddDataButton">
<property name="text">
<string>Add...</string>
</property>
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/add.png</normaloff>:/rsrc/add.png</iconset>
</property>
</widget>
</item>
<item row="1" column="2">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="DeleteDataButton">
<property name="text">
<string>Delete...</string>
</property>
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/delete.png</normaloff>:/rsrc/delete.png</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menuBar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>668</width>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>Fi&amp;le</string>
</property>
<addaction name="actionNew_database"/>
<addaction name="actionOpen_database"/>
<addaction name="actionOpen_database_as_read_only"/>
<addaction name="separator"/>
<addaction name="actionExit"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="actionAbout_qbolt"/>
<addaction name="actionAbout_Qt"/>
</widget>
<widget class="QMenu" name="menuView">
<property name="title">
<string>&amp;View</string>
</property>
<addaction name="actionClear_selection"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuView"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QToolBar" name="mainToolBar">
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionNew_database"/>
<addaction name="actionOpen_database"/>
<addaction name="separator"/>
</widget>
<widget class="QStatusBar" name="statusBar"/>
<action name="actionAbout_qbolt">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/information.png</normaloff>:/rsrc/information.png</iconset>
</property>
<property name="text">
<string>&amp;About QBolt</string>
</property>
</action>
<action name="actionAbout_Qt">
<property name="text">
<string>About &amp;Qt</string>
</property>
</action>
<action name="actionOpen_database">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/database.png</normaloff>:/rsrc/database.png</iconset>
</property>
<property name="text">
<string>&amp;Open database...</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionExit">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/door_out.png</normaloff>:/rsrc/door_out.png</iconset>
</property>
<property name="text">
<string>&amp;Exit</string>
</property>
</action>
<action name="actionDisconnect">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/disconnect.png</normaloff>:/rsrc/disconnect.png</iconset>
</property>
<property name="text">
<string>Disconnect</string>
</property>
</action>
<action name="actionDelete_bucket">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/table_delete.png</normaloff>:/rsrc/table_delete.png</iconset>
</property>
<property name="text">
<string>Delete bucket</string>
</property>
</action>
<action name="actionRefresh_buckets">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/arrow_refresh.png</normaloff>:/rsrc/arrow_refresh.png</iconset>
</property>
<property name="text">
<string>Refresh buckets</string>
</property>
</action>
<action name="actionClear_selection">
<property name="text">
<string>&amp;Clear selection</string>
</property>
</action>
<action name="actionNew_database">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/database_add.png</normaloff>:/rsrc/database_add.png</iconset>
</property>
<property name="text">
<string>&amp;New database...</string>
</property>
</action>
<action name="actionAdd_bucket">
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/rsrc/table_add.png</normaloff>:/rsrc/table_add.png</iconset>
</property>
<property name="text">
<string>Add bucket...</string>
</property>
</action>
<action name="actionOpen_database_as_read_only">
<property name="text">
<string>Open database as read-only...</string>
</property>
</action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources>
<include location="resources.qrc"/>
</resources>
<connections/>
</ui>

48
qbolt/qbolt.pro Normal file
View File

@@ -0,0 +1,48 @@
#-------------------------------------------------
#
# Project created by QtCreator 2017-05-15T19:38:33
#
#-------------------------------------------------
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = qbolt
TEMPLATE = app
# Enforce Qt deprecations
DEFINES += QT_DEPRECATED_WARNINGS
DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000
win32: {
# for some reason, qbolt_cgo.h never realises that Q_OS_WIN is defined for win32 builds... weird
DEFINES += CGO_WINDOWS
QMAKE_LIBS += $$_PRO_FILE_PWD_/../build/win32/qbolt.a
QMAKE_LIBS += -lntdll
RC_ICONS = rsrc/qbolt.ico
}
linux: {
QMAKE_LIBS += $$_PRO_FILE_PWD_/../build/linux/qbolt.a
QMAKE_LIBS += -lpthread
}
SOURCES += main.cpp\
mainwindow.cpp \
interop.cpp \
boltdb.cpp \
itemwindow.cpp
HEADERS += mainwindow.h \
interop.h \
boltdb.h \
qbolt_cgo.h \
itemwindow.h
FORMS += mainwindow.ui \
itemwindow.ui
RESOURCES += \
resources.qrc

10
qbolt/qbolt_cgo.h Normal file
View File

@@ -0,0 +1,10 @@
#ifndef QBOLT_CGO_H
#define QBOLT_CGO_H
#if defined(Q_OS_WIN) || defined(CGO_WINDOWS)
#include "../build/win32/qbolt.h"
#else
#include "../build/linux/qbolt.h"
#endif
#endif // QBOLT_CGO_H

17
qbolt/resources.qrc Normal file
View File

@@ -0,0 +1,17 @@
<RCC>
<qresource prefix="/">
<file>rsrc/database_add.png</file>
<file>rsrc/table.png</file>
<file>rsrc/information.png</file>
<file>rsrc/database_lightning.png</file>
<file>rsrc/database.png</file>
<file>rsrc/table_add.png</file>
<file>rsrc/table_delete.png</file>
<file>rsrc/add.png</file>
<file>rsrc/delete.png</file>
<file>rsrc/chart_bar.png</file>
<file>rsrc/arrow_refresh.png</file>
<file>rsrc/disconnect.png</file>
<file>rsrc/door_out.png</file>
</qresource>
</RCC>

BIN
qbolt/rsrc/add.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

BIN
qbolt/rsrc/arrow_refresh.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

BIN
qbolt/rsrc/chart_bar.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

BIN
qbolt/rsrc/database.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

BIN
qbolt/rsrc/database_add.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

BIN
qbolt/rsrc/database_connect.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

BIN
qbolt/rsrc/database_lightning.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

BIN
qbolt/rsrc/delete.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

BIN
qbolt/rsrc/disconnect.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 796 B

BIN
qbolt/rsrc/door_out.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 B

BIN
qbolt/rsrc/information.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

BIN
qbolt/rsrc/page.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

BIN
qbolt/rsrc/qbolt.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
qbolt/rsrc/table.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

BIN
qbolt/rsrc/table_add.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

BIN
qbolt/rsrc/table_delete.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B