Compare commits
79 Commits
archive/co
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 99096b2360 | |||
| 0e92459779 | |||
| a13eaaf523 | |||
| dca8d277d3 | |||
| 15c739c0b9 | |||
| fc2da972ef | |||
| e45eea7111 | |||
| 1e62d79c07 | |||
| c74c5ae5c0 | |||
| b0092c4a6e | |||
| 5ce6368c4a | |||
| 3e7e54da4b | |||
| 9f80d687b2 | |||
| 96411e877f | |||
| ac7e078c02 | |||
| e13314f5dc | |||
| b50c3e738a | |||
| 54ad6015b7 | |||
| 40e84ac230 | |||
| 767eaa0a47 | |||
| bb594e768c | |||
| 516bd99c4d | |||
| 571bfcf4b6 | |||
| 26f7a11d80 | |||
| 21588021d3 | |||
| 7441e0c15b | |||
| 142f3f6bf4 | |||
| 19ddb1c956 | |||
| 97467eae4d | |||
| 17c37b6568 | |||
| 57237ef2e8 | |||
| 164f5a071d | |||
| ac46b16d0e | |||
| b9d114a2d8 | |||
| b368457247 | |||
| fb940d5f60 | |||
| 6d1e671e44 | |||
| b5149c4efa | |||
| f0f642b6b0 | |||
| 7c62abb352 | |||
| 4f1c48b55d | |||
| 37f9307db1 | |||
| 14e7ebb59c | |||
| 14510545d4 | |||
| 906cff2e57 | |||
| bf28495b1f | |||
| 98b78ab1e6 | |||
| 4394c8a50a | |||
| 9fb5bdad78 | |||
| d2ec12798e | |||
| 5270dd00bc | |||
| 35f28fa5ed | |||
| 5ea969e10c | |||
| d0becd0c3c | |||
| 1c81444645 | |||
| d7c3bfd1f5 | |||
| 0f1cc014d7 | |||
| 6ac8c3e67b | |||
| 546681a0ef | |||
| 679d1140cc | |||
| 60d71104e8 | |||
| 95a1bfeea5 | |||
| 8597f270f6 | |||
| d32ab82d73 | |||
| b6d1cafe54 | |||
| 3911d7d66c | |||
| be594cb1a5 | |||
| 8a93f163c4 | |||
| e82d0a5dd2 | |||
| 4b3fb783c4 | |||
| e8030a9579 | |||
| a94a330345 | |||
| 8ce03cbed6 | |||
| b76fad9512 | |||
| a9158c3a0c | |||
| dc31787526 | |||
| ff7484079e | |||
| 6e1f9ca1e1 | |||
| ac56f5e6c8 |
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
build-qbolt-*
|
||||
dummy-data/dummy-data*
|
||||
*.pro.user$
|
||||
build/
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
23
README.md
@@ -1,13 +1,13 @@
|
||||
# qbolt
|
||||
|
||||

|
||||
|
||||
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)
|
||||
|
||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
13
docker/win32-cross-qt-mxe.Dockerfile
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 "Silk" 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
@@ -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
@@ -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&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>&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>&About QBolt</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAbout_Qt">
|
||||
<property name="text">
|
||||
<string>About &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>&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>&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>&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>&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
@@ -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
@@ -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
@@ -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
|
After Width: | Height: | Size: 733 B |
BIN
qbolt/rsrc/arrow_refresh.png
Executable file
|
After Width: | Height: | Size: 685 B |
BIN
qbolt/rsrc/chart_bar.png
Executable file
|
After Width: | Height: | Size: 541 B |
BIN
qbolt/rsrc/database.png
Executable file
|
After Width: | Height: | Size: 390 B |
BIN
qbolt/rsrc/database_add.png
Executable file
|
After Width: | Height: | Size: 658 B |
BIN
qbolt/rsrc/database_connect.png
Executable file
|
After Width: | Height: | Size: 763 B |
BIN
qbolt/rsrc/database_lightning.png
Executable file
|
After Width: | Height: | Size: 775 B |
BIN
qbolt/rsrc/delete.png
Executable file
|
After Width: | Height: | Size: 715 B |
BIN
qbolt/rsrc/disconnect.png
Executable file
|
After Width: | Height: | Size: 796 B |
BIN
qbolt/rsrc/door_out.png
Executable file
|
After Width: | Height: | Size: 688 B |
BIN
qbolt/rsrc/information.png
Executable file
|
After Width: | Height: | Size: 778 B |
BIN
qbolt/rsrc/page.png
Executable file
|
After Width: | Height: | Size: 635 B |
BIN
qbolt/rsrc/qbolt.ico
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
qbolt/rsrc/table.png
Executable file
|
After Width: | Height: | Size: 566 B |
BIN
qbolt/rsrc/table_add.png
Executable file
|
After Width: | Height: | Size: 663 B |
BIN
qbolt/rsrc/table_delete.png
Executable file
|
After Width: | Height: | Size: 660 B |