16 Commits

16 changed files with 141 additions and 70 deletions

1
.hgtags Normal file
View File

@@ -0,0 +1 @@
74cacbbe8f6cfb20a63902fb76e48c02d5cf2963 release-1.0.0

View File

@@ -1,7 +1,7 @@
export PATH := /usr/lib/mxe/usr/bin:$(PATH)
GOFLAGS := -ldflags='-s -w' -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)'
VERSION := 1.0.0
VERSION := 1.0.1
.PHONY: all libs dist clean

View File

@@ -9,8 +9,9 @@ Written in C++ (Qt), Golang (CGo)
=FEATURES=
- Open existing database or create new database
- Supports nested buckets
- Create and edit keys
- Option to open database as readonly for concurrent use
- Create, list, edit and delete keys and buckets (including nested buckets)
- Safe for use with arbitrary binary key/bucket names (new ones created in UTF-8)
- View database and bucket statistics
- 100% Bolt compatibility via the real codebase
- Tested working on both Windows and Linux
@@ -27,5 +28,11 @@ The Windows binary is released under LGPL-3+ owing to the static copy of Qt.
=CHANGELOG=
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
2017-05-21 1.0.0
- Initial public release

View File

@@ -1,9 +1,9 @@
- Extra option to open read-only (allows concurrent access)
- Reduce unnecessary memory copies between QString/QByteArray
- Convert from item-based to model-based Qt widgets - needs deep integration with Bolt cursors...
- Remove dependence on UTF-8-encoded keys
- Rename buckets
- Delete multiple buckets

BIN
_dist/image0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -1,7 +1,7 @@
package main
import (
"fmt"
//"fmt"
"math/rand"
"os"
@@ -9,7 +9,10 @@ import (
)
func random_name() string {
return fmt.Sprintf("%08x-%08x-%08x", rand.Int63(), rand.Int63(), rand.Int63())
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 {

View File

@@ -26,9 +26,11 @@ func GetMagic() int64 {
}
//export Bolt_Open
func Bolt_Open(path string) (ObjectReference, *C.char, int) {
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()

View File

@@ -7,12 +7,12 @@ BoltDB::BoltDB()
}
BoltDB* BoltDB::createFrom(QString filePath, QString &errorOut)
BoltDB* BoltDB::createFrom(QString filePath, bool readOnly, QString &errorOut)
{
QByteArray filePathBytes(filePath.toUtf8());
GoString filePathGS = Interop::toGoString_WeakRef(&filePathBytes);
auto open_ret = ::Bolt_Open(filePathGS);
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);
@@ -45,27 +45,27 @@ static bool handleTriple(int r0, char* r1, int64_t r2, QString& errorOut) {
}
}
bool BoltDB::addBucket(QStringList bucketPath, QByteArray bucketName, QString& errorOut)
bool BoltDB::addBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut)
{
GoSliceManagedWrapper browse(&bucketPath);
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(QStringList bucketPath, QByteArray bucketName, QString& errorOut)
bool BoltDB::deleteBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut)
{
GoSliceManagedWrapper browse(&bucketPath);
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(QStringList bucketPath, QByteArray keyName, QByteArray value, QString& errorOut)
bool BoltDB::setItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QByteArray value, QString& errorOut)
{
GoSliceManagedWrapper browse(&bucketPath);
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);
@@ -73,9 +73,9 @@ bool BoltDB::setItem(QStringList bucketPath, QByteArray keyName, QByteArray valu
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
}
bool BoltDB::deleteItem(QStringList bucketPath, QByteArray keyName, QString& errorOut)
bool BoltDB::deleteItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QString& errorOut)
{
GoSliceManagedWrapper browse(&bucketPath);
GoSliceManagedWrapper browse(bucketPath);
GoString keyNameGS = Interop::toGoString_WeakRef(&keyName);
auto resp = ::Bolt_DeleteItem(this->gmsDbRef, browse.slice, keyNameGS);
@@ -89,20 +89,20 @@ bool BoltDB::listBucketsAtRoot(QString& errorOut, NameReciever cb)
return pumpNext(listJob, errorOut, cb);
}
bool BoltDB::listBuckets(QStringList bucketPath, QString& errorOut, NameReciever cb)
bool BoltDB::listBuckets(const QList<QByteArray>& bucketPath, QString& errorOut, NameReciever cb)
{
if (bucketPath.size() == 0) {
return listBucketsAtRoot(errorOut, cb);
}
GoSliceManagedWrapper browse(&bucketPath);
GoSliceManagedWrapper browse(bucketPath);
auto listJob = ::Bolt_ListBuckets(this->gmsDbRef, browse.slice);
return pumpNext(listJob, errorOut, cb);
}
bool BoltDB::listKeys(QStringList bucketPath, QString& errorOut, std::function<void(QByteArray, int64_t)> cb)
bool BoltDB::listKeys(const QList<QByteArray>& bucketPath, QString& errorOut, std::function<void(QByteArray, int64_t)> cb)
{
GoSliceManagedWrapper browse(&bucketPath);
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
@@ -111,9 +111,9 @@ bool BoltDB::listKeys(QStringList bucketPath, QString& errorOut, std::function<v
});
}
bool BoltDB::getData(QStringList bucketPath, QByteArray key, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
bool BoltDB::getData(const QList<QByteArray>& bucketPath, QByteArray key, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
{
GoSliceManagedWrapper browse(&bucketPath);
GoSliceManagedWrapper browse(bucketPath);
GoString keyGS = Interop::toGoString_WeakRef(&key);
auto resp = ::Bolt_GetItem(this->gmsDbRef, browse.slice, keyGS);
@@ -185,9 +185,9 @@ bool BoltDB::getStatsJSON(std::function<void(QByteArray)> onSuccess, std::functi
}
}
bool BoltDB::getBucketStatsJSON(QStringList bucketPath, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
bool BoltDB::getBucketStatsJSON(const QList<QByteArray>& bucketPath, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
{
GoSliceManagedWrapper sliceWrapper(&bucketPath);
GoSliceManagedWrapper sliceWrapper(bucketPath);
auto statresp = Bolt_BucketStats(this->gmsDbRef, sliceWrapper.slice);
if (statresp.r0 == ERROR_AND_STOP_CALLING) {

View File

@@ -14,27 +14,27 @@ protected:
GoInt64 gmsDbRef;
public:
static BoltDB* createFrom(QString filePath, QString &errorOut);
static BoltDB* createFrom(QString filePath, bool readOnly, QString &errorOut);
bool listBucketsAtRoot(QString& errorOut, NameReciever cb);
bool listBuckets(QStringList bucketPath, QString& errorOut, NameReciever cb);
bool listBuckets(const QList<QByteArray>& bucketPath, QString& errorOut, NameReciever cb);
bool addBucket(QStringList bucketPath, QByteArray bucketName, QString& errorOut);
bool addBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut);
bool deleteBucket(QStringList bucketPath, QByteArray bucketName, QString& errorOut);
bool deleteBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut);
bool setItem(QStringList bucketPath, QByteArray keyName, QByteArray value, QString& errorOut);
bool setItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QByteArray value, QString& errorOut);
bool deleteItem(QStringList bucketPath, QByteArray keyName, QString& errorOut);
bool deleteItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QString& errorOut);
bool listKeys(QStringList bucketPath, QString& errorOut, std::function<void(QByteArray, int64_t)> cb);
bool listKeys(const QList<QByteArray>& bucketPath, QString& errorOut, std::function<void(QByteArray, int64_t)> cb);
bool getData(QStringList bucketPath, QByteArray key, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError);
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(QStringList bucketPath, 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();

View File

@@ -17,22 +17,22 @@ int64_t Interop::GetMagic() {
//
GoSliceManagedWrapper::GoSliceManagedWrapper(QStringList *qsl) :
GoSliceManagedWrapper::GoSliceManagedWrapper(const QList<QByteArray>& qsl) :
rawStrings(),
slice(),
strings(nullptr)
{
rawStrings.reserve(qsl->size());
strings = new GoString[qsl->size()];
rawStrings.reserve(qsl.size());
strings = new GoString[qsl.size()];
for (int i = 0; i < qsl->size(); ++i) {
rawStrings.push_back( qsl->at(i).toUtf8() );
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.len = qsl.size(); // * sizeof(GoString);
slice.cap = slice.len;
}

View File

@@ -9,7 +9,7 @@ class GoSliceManagedWrapper {
Q_DISABLE_COPY(GoSliceManagedWrapper)
public:
GoSliceManagedWrapper(QStringList *qsl);
GoSliceManagedWrapper(const QList<QByteArray>& qsl);
~GoSliceManagedWrapper();
protected:
QList<QByteArray> rawStrings;

View File

@@ -35,6 +35,8 @@ MainWindow::~MainWindow()
}
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*>() )
@@ -42,7 +44,7 @@ void MainWindow::on_actionNew_database_triggered()
{
QString file = QFileDialog::getSaveFileName(this, tr("Save new bolt database as..."));
if (file.length()) {
openDatabase(file);
openDatabase(file, false);
}
}
@@ -50,15 +52,23 @@ void MainWindow::on_actionOpen_database_triggered()
{
QString file = QFileDialog::getOpenFileName(this, tr("Select bolt database..."));
if (file.length()) {
openDatabase(file);
openDatabase(file, false);
}
}
void MainWindow::openDatabase(QString file)
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, error);
auto *bdb = BoltDB::createFrom(file, readOnly, error);
if (bdb == nullptr) {
QMessageBox qmb;
qmb.setText(tr("Error opening database: %1").arg(error));
@@ -78,12 +88,38 @@ void MainWindow::openDatabase(QString file)
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;
QStringList browsePath;
QList<QByteArray> browsePath;
while(top->parent() != nullptr) {
browsePath.push_front(top->text(0));
browsePath.push_front(top->data(0, BinaryDataRole).toByteArray());
top = top->parent();
}
@@ -100,7 +136,8 @@ void MainWindow::refreshBucketTree(QTreeWidgetItem* itm)
error,
[=](QByteArray qba){
QTreeWidgetItem *child = new QTreeWidgetItem();
child->setText(0, QString::fromUtf8(qba));
child->setText(0, getDisplayName(qba));
child->setData(0, BinaryDataRole, qba);
child->setIcon(0, QIcon(":/rsrc/table.png"));
itm->addChild(child);
@@ -215,10 +252,10 @@ void MainWindow::on_bucketTree_currentItemChanged(QTreeWidgetItem *current, QTre
ui->stackedWidget->setCurrentWidget(ui->bucketPage);
ui->bucketPropertiesArea->clear();
QStringList browse;
QList<QByteArray> browse;
QTreeWidgetItem *top = current;
while (top->parent() != nullptr) {
browse.push_front(top->text(0));
browse.push_front(top->data(0, BinaryDataRole).toByteArray());
top = top->parent();
}
auto *bdb = GET_BDB(top);
@@ -242,14 +279,15 @@ void MainWindow::on_bucketTree_currentItemChanged(QTreeWidgetItem *current, QTre
}
}
void MainWindow::refreshData(BoltDB *bdb, QStringList browse)
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, QString::fromUtf8(name));
itm->setText(0, getDisplayName(name));
itm->setData(0, BinaryDataRole, name);
itm->setText(1, QString("%1").arg(dataLen));
ui->bucketData->addTopLevelItem(itm);
});
@@ -275,14 +313,14 @@ void MainWindow::on_actionClear_selection_triggered()
return; \
} \
QTreeWidgetItem* top = itm; \
QStringList browse; \
QList<QByteArray> browse; \
while(top->parent() != nullptr) { \
browse.push_front(top->text(0)); \
browse.push_front(top->data(0, BinaryDataRole).toByteArray()); \
top = top->parent(); \
} \
auto *bdb = GET_BDB(top);
void MainWindow::openEditor(BoltDB *bdb, QStringList saveAs, QByteArray saveAsKey, QByteArray currentContent)
void MainWindow::openEditor(BoltDB *bdb, const QList<QByteArray>& saveAs, QByteArray saveAsKey, QByteArray currentContent)
{
auto iw = new ItemWindow();
iw->ContentArea()->setPlainText(QString::fromUtf8(currentContent));
@@ -312,15 +350,15 @@ void MainWindow::on_bucketData_doubleClicked(const QModelIndex &index)
// Get item key
auto model = index.model();
QString key = model->data(model->index(index.row(), 0), 0).toString();
const QByteArray& key = model->data(model->index(index.row(), 0), BinaryDataRole).toByteArray();
// DB lookup
bdb->getData(
browse,
key.toUtf8(),
key,
[=](QByteArray content) {
openEditor(bdb, browse, key.toUtf8(), content);
openEditor(bdb, browse, key, content);
},
[=](QString error) {
QMessageBox qmb;
@@ -361,17 +399,27 @@ void MainWindow::on_actionDelete_bucket_triggered()
GET_ITM_TOP_BROWSE_BDB;
// Prompt for confirmation
QString bucketToDelete = itm->text(0);
if (QMessageBox::question(this, tr("Delete bucket"), tr("Are you sure you want to remove the bucket '%1'?").arg(bucketToDelete), QMessageBox::Yes, QMessageBox::Cancel) != QMessageBox::Yes) {
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.toUtf8(), err)) {
if (! bdb->deleteBucket(browse, bucketToDelete, err)) {
QMessageBox qmb;
qmb.setText(tr("Error removing bucket: %1").arg(err));
qmb.exec();
@@ -379,9 +427,9 @@ void MainWindow::on_actionDelete_bucket_triggered()
}
// Refresh bucket list
refreshBucketTree(itm->parent()); // sub-tree only
ui->bucketTree->expandItem(itm->parent());
refreshBucketTree(parent); // sub-tree only
ui->bucketTree->expandItem(parent);
ui->bucketTree->setCurrentItem(parent);
}
void MainWindow::on_AddDataButton_clicked()
@@ -414,7 +462,7 @@ void MainWindow::on_DeleteDataButton_clicked()
QString err;
for (int i = selection.length(); i-->0;) {
if (! bdb->deleteItem(browse, selection[i]->text(0).toUtf8(), err)) {
if (! bdb->deleteItem(browse, selection[i]->data(0, BinaryDataRole).toByteArray(), err)) {
QMessageBox qmb;
qmb.setText(tr("Error removing item: %1").arg(err));
qmb.exec();

View File

@@ -51,11 +51,13 @@ private slots:
void on_bucketData_itemSelectionChanged();
void on_actionOpen_database_as_read_only_triggered();
protected:
void openDatabase(QString file);
void openDatabase(QString file, bool readOnly);
void refreshBucketTree(QTreeWidgetItem* top);
void refreshData(BoltDB *bdb, QStringList browse);
void openEditor(BoltDB *bdb, QStringList saveAs, QByteArray saveAsKey, QByteArray currentContent);
void refreshData(BoltDB *bdb, const QList<QByteArray>& browse);
void openEditor(BoltDB *bdb, const QList<QByteArray>& saveAs, QByteArray saveAsKey, QByteArray currentContent);
private:
Ui::MainWindow *ui;

View File

@@ -278,6 +278,7 @@
</property>
<addaction name="actionNew_database"/>
<addaction name="actionOpen_database"/>
<addaction name="actionOpen_database_as_read_only"/>
<addaction name="separator"/>
<addaction name="actionExit"/>
</widget>
@@ -395,6 +396,11 @@
<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>

View File

@@ -20,6 +20,8 @@ win32: {
DEFINES += CGO_WINDOWS
QMAKE_LIBS += $$_PRO_FILE_PWD_/../build/win32/qbolt.a
QMAKE_LIBS += -lntdll
RC_ICONS = rsrc/qbolt.ico
}
linux: {

BIN
qbolt/rsrc/qbolt.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB