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
|
# qbolt
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
A graphical database manager for BoltDB.
|
A graphical database manager for BoltDB.
|
||||||
|
|
||||||
QBolt allows you to graphically view and edit the content of Bolt databases.
|
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.
|
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
|
## Features
|
||||||
|
|
||||||
- Open existing database or create new database
|
- 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.
|
BoltDB is released under the MIT license.
|
||||||
The Windows binary is released under LGPL-3+ owing to the static copy of Qt.
|
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
|
- BoltDB https://github.com/boltdb/bolt
|
||||||
|
|
||||||
## Changelog
|
## 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
|
2017-06-19 1.0.1
|
||||||
- Feature: Option to open database as read-only
|
- 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 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 an issue with crashing when deleting a bucket other than the selected one
|
||||||
- Fix a cosmetic issue with application icon on Windows
|
- 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)*
|
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.1)
|
||||||
- [⬇️ 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)*
|
|
||||||
|
|
||||||
|
|
||||||
2017-05-21 1.0.0
|
2017-05-21 1.0.0
|
||||||
- Initial public release
|
- Initial public release
|
||||||
- [⬇️ qbolt-1.0.0-win32.zip](dist-archive/qbolt-1.0.0-win32.zip) *(5.07 MiB)*
|
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.0)
|
||||||
- [⬇️ 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)*
|
|
||||||
|
|
||||||
|
|||||||
|
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 |