393 Commits

Author SHA1 Message Date
2a1d0fa036 etcd: support v2 as well as v3 2026-01-27 08:04:46 +13:00
5b9963eb77 rename kvstore_ApplyChanges -> ApplyChanges_binColumn 2026-01-27 08:03:59 +13:00
ca7c827e75 etcd: initial support (v3 only) 2026-01-24 18:52:16 +13:00
fd913fa1eb qbolt: use full installed list of qstyles 2026-01-24 17:47:07 +13:00
8e5f003e29 doc: update TODO, README, CHANGELOG 2026-01-24 17:06:06 +13:00
28afd05199 ui: add appearance changer (qstyle, density, row backgrounds) 2026-01-24 16:51:40 +13:00
99e138aa94 tooltips: show badger/bolt full paths, show in connmgr + main area 2026-01-24 16:16:11 +13:00
518b66b270 config: refactor connector.String() to separate function [...]
- lmdb: adds (readonly) to default names
- sqlite/ssh: focus on DB name
2026-01-24 16:05:15 +13:00
ee066ec7e7 ui: fix 'failed to save' error if we fail to load config dialog 2026-01-24 15:33:20 +13:00
638d2e69eb ui/mainwindow: reduce main padding, reduce data tab padding 2026-01-24 15:29:58 +13:00
894f730706 ui/connection: better dialog size for instant connection 2026-01-24 15:29:47 +13:00
4498542e71 config: use non-conflicting names when saving new entries 2026-01-24 15:23:23 +13:00
a1345f1c24 table: hovering integers show converted timestamp in tooltip 2026-01-22 22:22:42 +13:00
73bc1c050b table: add context menu actions, support setting cell null 2026-01-22 22:22:23 +13:00
6a5605e067 table: show data type when hovering column header 2026-01-22 22:21:01 +13:00
f345860468 doc: update TODO 2026-01-22 18:27:04 +13:00
965a257ebb badger: fix extra data appearing in k/v pairs 2026-01-22 17:00:51 +13:00
fc9d1b54de doc: update TODO 2026-01-17 10:29:02 +13:00
0d04effe90 buntdb: support advanced options 2026-01-17 10:11:51 +13:00
ee1ff5582f bolt: support advanced options 2026-01-17 10:02:18 +13:00
029f79d800 badger: allow resetting advanced options struct 2026-01-16 19:29:28 +13:00
ea0ee78f2e autoconfig: upgrade v0.5->v0.6 (fix int sizes, scroll dialogs, autoupgrade field types, re-reset buttons) 2026-01-16 19:21:18 +13:00
9774a8690b doc: update TODO 2026-01-13 19:19:08 +13:00
3a66197c2c sqlite/sqliteclidriver: fix crash if zero rows in table 2026-01-13 19:19:06 +13:00
a2badc6964 sqlite: clidriver doesn't like "!=", use "<>" instead 2026-01-13 19:10:21 +13:00
aee1a7ede1 badger: support fully advanced configuration 2026-01-13 19:07:33 +13:00
1b5c97abca main: add --config-export, --config-import CLI flags 2026-01-13 19:07:19 +13:00
1c37b71414 sshtunnel: fix warning in test output 2026-01-13 19:07:04 +13:00
e9bdbb8066 doc: update TODO 2026-01-11 14:21:24 +13:00
2a9f06588b voiddb: only supported on linux 2026-01-04 11:24:40 +13:00
8ee74e16c2 doc: changelog for v2.1.0 2026-01-04 11:21:03 +13:00
96630dc940 makefile: add test-redis command 2026-01-04 11:05:50 +13:00
85e245658e autoconfig: update v0.5.0 (nicer string editors/labels) 2026-01-04 11:05:42 +13:00
1de694cb88 table/binary: hide extra APPNAME label from autoconfig editor 2026-01-02 19:14:14 +13:00
312898ab3b doc: update README and TODO 2026-01-02 19:14:05 +13:00
19e36ca615 pogreb: initial support + editing + compact 2026-01-02 17:50:18 +13:00
e1403f8e7d doc: update CHANGELOG and TODO 2026-01-02 17:36:02 +13:00
eecdc2b5f2 voiddb: initial support 2026-01-02 17:35:48 +13:00
beff8bc323 config: show multiple connection errors
Not clear why this fixed it
2026-01-02 16:35:49 +13:00
06d939b06e ui: enforce smaller toolbar icon size 2025-12-29 18:24:32 +13:00
addabd85f8 app: add 'set cell to null' button (no-op), separate toolbars into 3 2025-12-29 18:24:26 +13:00
85659885ab doc: update TODO 2025-12-28 19:30:28 +13:00
f34f2f84dc table: show nullable cells as grey 2025-12-28 19:30:23 +13:00
cbc14b261c sqlite: always use extra rowid column as primary key 2025-12-28 19:24:04 +13:00
4f29d531c1 sqlite: use GetCell for preserving typed data through update/insert 2025-12-28 19:21:54 +13:00
b1634e92d4 table: add getCell for dynamic values 2025-12-28 19:21:22 +13:00
4ce9b753e9 sqlite: hide sqlite_sequence and _stat1 tables 2025-12-28 19:04:48 +13:00
1c2567dc3c sqlite: remove builtin LIMIT for now 2025-12-28 19:04:40 +13:00
f2def1371b ui: fix text in context menu when toggling toolbar 2025-12-28 18:44:55 +13:00
4a3e37cdc4 sqliteclidriver: declare all columns are nullable BLOBs 2025-12-28 18:38:22 +13:00
f165853c6b sqlite: always rely on column information from query cursor 2025-12-28 18:28:08 +13:00
93ce3d4a90 ui: non-queryable databases should disable entire tab 2025-12-28 18:27:50 +13:00
88346852b3 ui: when adding new db, always start on properties tab 2025-12-28 18:27:41 +13:00
d15d42c37e sqlite: refactor to use strongly typed column data 2025-12-28 18:02:56 +13:00
76581b9454 table_binary: improve performance by eliding large data values for display 2025-12-28 18:02:28 +13:00
5c9f165aa7 table_binary, _string: show more detailed type errors 2025-12-28 18:02:18 +13:00
a576138428 table_dynamic: use correct format specifier for type information in error 2025-12-28 18:02:00 +13:00
f7f3bfb035 table: add dynamicColumn{}, use for sqlite 2025-12-23 16:46:12 +13:00
53c90bf0dc table: no need for ds_normal, setting delegate to nil is sufficient 2025-12-23 16:45:56 +13:00
2f3c956549 fix execquery being called twice 2025-12-23 15:56:31 +13:00
7c441ecc50 embeddedversions: clickable links to module dependencies 2025-12-23 15:53:24 +13:00
f75a161acd table: remove columnType abstraction 2025-12-23 15:38:02 +13:00
0002c82594 table: reuse delegate, move customization into interface method 2025-12-23 15:28:29 +13:00
13fedfa2f6 doc: update README and TODO 2025-12-22 19:48:36 +13:00
8b06ccef48 buntdb: add support, including editing+shrink 2025-12-22 18:43:48 +13:00
17d6b5172d ui: use compress icon for bolt-zip tool 2025-12-22 18:23:13 +13:00
708e8072ff sqlite: support ssh connections 2025-12-22 18:17:41 +13:00
c8125d2c84 sqliteclidriver: add ssh backend 2025-12-22 18:17:35 +13:00
a5138a51f3 sqliteclidriver: refactor cmd handling to support other implementations 2025-12-22 18:17:29 +13:00
02b5a8fd48 sqliteclidriver: add more comments, expose DriverName const 2025-12-22 18:17:06 +13:00
b2118c9196 config: add omitempty for many db types 2025-12-22 18:16:35 +13:00
5aa76a465c sshtunnel: add icons for connection modes, add omitempty 2025-12-22 18:16:11 +13:00
f3e729b023 assets: add compress, key, page_key icons 2025-12-22 18:15:55 +13:00
5c7a99d16b makefile: autogenerate embed.qrc from asset files 2025-12-22 18:15:49 +13:00
4d5ca19b47 doc/changelog: changelog for v2.0.0 2025-12-20 19:19:46 +13:00
a0e70636a1 doc/LICENSE: merge qbolt/yvbolt license headers 2025-12-20 19:19:46 +13:00
13e139e023 app: show version in main window titlebar 2025-12-20 19:19:46 +13:00
1f7d03e67a doc: update screenshots 2025-12-20 19:19:46 +13:00
0b1f662e99 doc/README: add other features from qbolt's readme 2025-12-20 19:19:46 +13:00
e5f36b0f66 doc/README: remove yvbolt/qbolt cross reference link 2025-12-20 19:19:46 +13:00
0eccb12744 doc/README: add direct links to changelog/downloads 2025-12-20 19:19:46 +13:00
f2d3240153 git: merge yvbolt/qbolt changelogs as separate file 2025-12-20 19:19:46 +13:00
9fb4302000 git: merge all yvbolt/qbolt names 2025-12-20 19:19:40 +13:00
e3f94f1eba git: synthetic merge of qbolt and yvbolt branches 2025-12-20 18:32:58 +13:00
2259b3f455 doc: update changelog for v0.11.0 2025-12-20 18:24:24 +13:00
b4e8733798 doc: update TODO 2025-12-20 18:24:17 +13:00
ace5e3e65e bolt: support import/export as zip 2025-12-20 18:10:55 +13:00
e132500fd8 util: add slice_and, slice_apply 2025-12-20 18:10:33 +13:00
a6bb412a34 bolt: add comments on boltTargetBucket 2025-12-20 18:10:24 +13:00
51f2a69ed2 badger, sqlite: fix *.* spec for 'all files' in export file path 2025-12-20 17:46:07 +13:00
1db2d9781d ssh-agent: support lock/unlock 2025-12-20 17:21:35 +13:00
9f662a7fa2 badger: move encryption options to sub-struct [BREAKING] 2025-12-20 17:21:28 +13:00
034bd8114a ssh-agent: add initial support 2025-12-20 17:02:23 +13:00
7242e8644b ssh-tunnel: support ssh-agents 2025-12-20 16:43:07 +13:00
820285066b doc: update TODO 2025-12-19 19:17:46 +13:00
94c517a324 secretsvc: treat data column as binary 2025-12-19 19:11:11 +13:00
9fbc7b4ee6 config: fix saving new entries causing order to be lost 2025-12-19 19:06:51 +13:00
421cabb7a5 lmdb: support truncating existing childdb 2025-12-19 19:05:39 +13:00
910ef0dd9a makefile: add helper for running test mongodb server 2025-12-19 18:49:01 +13:00
a9c6b135c7 mongo: show index details in properties pane 2025-12-19 18:48:52 +13:00
02d4b918d6 ui: scroll to top when changing tables 2025-12-19 18:43:02 +13:00
0798547b39 ssh known_hosts support 2025-12-19 18:23:07 +13:00
3d114ec1a4 app: prevent editing if table did not load data 2025-12-19 17:55:41 +13:00
4913e36ad6 doc: changelog for v0.10.1 2025-12-17 18:17:51 +13:00
bafabdf690 makefile: build release binaries with go1.26rc1 2025-12-17 17:28:06 +13:00
4f2381ee33 go mod tidy 2025-12-17 17:27:58 +13:00
d876013ade main: fix mixing qt5 + qt6 libraries(!) 2025-12-17 17:27:54 +13:00
e42eb6ace7 doc: changelog for v0.10.0 2025-12-16 20:32:10 +13:00
4789c8c706 doc: update README and TODO 2025-12-16 20:31:06 +13:00
cad3e9d496 table: refactor typed-column handling 2025-12-16 19:55:25 +13:00
fc63b992ca driver-versions: fix missing space 2025-12-16 19:55:11 +13:00
1877417327 mongo: replace fake-edit bson with real readonly bson support 2025-12-16 17:29:31 +13:00
13b5878fe5 ui: 'driver versions' show full detail in new table 2025-12-16 17:08:08 +13:00
fe5d218291 pebble: remove driver-version display in status bar 2025-12-16 17:07:51 +13:00
2abca95d72 hexview: tighter column layout 2025-12-16 16:57:31 +13:00
7228bc5ba7 table: for []byte columns, dynamically switch between textedit + hexview 2025-12-16 16:57:23 +13:00
3087ba498d table: use hexview widget for []byte-type data 2025-12-15 19:18:36 +13:00
847521541f hexview: initial commit 2025-12-15 19:18:24 +13:00
1379e912e5 app: move monospace() to helper function 2025-12-15 19:18:06 +13:00
69e17868a4 mongodb: show BSON editor in an autoconfig popup 2025-12-14 12:11:17 +13:00
45650ead8c mongo: add bson_find_id helper function 2025-12-14 11:28:11 +13:00
e1f8c40143 bolt, lmdb, mongo: add confirmation warning for destructive actions 2025-12-14 11:27:38 +13:00
f3496db2c7 doc: update README and TODO 2025-12-12 18:57:52 +13:00
13511a389a rosedb: initial support, including editing 2025-12-12 18:54:52 +13:00
2c1faf98c7 bitcask: initial support, including editing + backup 2025-12-12 18:42:18 +13:00
9a42a4021d lotusdb: optimize png logo 2025-12-12 18:42:00 +13:00
d2587949d6 lotusdb: initial support, including editing 2025-12-12 18:18:52 +13:00
7c24b1f24a doc: update README and TODO 2025-12-11 20:02:35 +13:00
882517dffc mongodb: support querying 2025-12-11 19:52:33 +13:00
f98db57a97 queryable: pass in bucketPath 2025-12-11 19:52:25 +13:00
7a68362149 ui: fix pasting causing rich text in query editor 2025-12-11 19:52:09 +13:00
f716afee74 mongodb: ssh tunnel support 2025-12-11 19:13:55 +13:00
aecab00e70 mongodb: add/remove databases and collections 2025-12-11 18:55:28 +13:00
65d921ddde mongodb: initial support 2025-12-11 18:48:09 +13:00
5d39d90044 redis: minor bump in driver version 2025-12-11 18:08:26 +13:00
6a685a2562 ui: trigger 'unsaved changes' warning when changing nav 2025-12-10 19:50:58 +13:00
d4ee91ed10 doc: update README and TODO 2025-12-10 19:33:32 +13:00
9fc11d8c61 sqlite: fix primary key handling when editing 2025-12-10 19:06:56 +13:00
cb09e0eb84 Merge branch 'tableview'
# Conflicts:
#	db_badger.go
2025-12-10 18:56:31 +13:00
12a60f57e3 doc/TODO: update 2025-12-10 18:54:43 +13:00
a3e32178ac starskey: fix k/v handling, support editing 2025-12-10 18:54:09 +13:00
9e76cb32fe pebble: support editing 2025-12-10 18:38:12 +13:00
63e47ae505 leveldb: support editing 2025-12-10 18:29:26 +13:00
319da7c13c badger: support editing 2025-12-10 18:26:00 +13:00
e03c635e7b bolt, lmdb: use common code for applying changes to k/v store 2025-12-10 18:21:53 +13:00
b2b5f8ba54 sqlite: new editing support 2025-12-10 18:19:20 +13:00
dcff85cfe5 lmdb: new editing support 2025-12-10 18:04:58 +13:00
65cbfec7af tableview: editing support 2025-12-10 18:00:23 +13:00
c2c997e53f tableview: fix crashes 2025-12-09 16:53:42 +13:00
9d2afaf57c connection-manager: auto save with preferred displayName 2025-12-09 16:53:30 +13:00
8b410bca89 badger: fix wrong value column 2025-12-09 16:39:23 +13:00
e11b5b2100 ui: port to qtableview 2025-12-09 16:38:50 +13:00
f292780972 doc/TODO: log bug in connection manager 2025-12-09 16:38:17 +13:00
89bd3ed27a makefile: embed version in binary
Avoiding using a git tag for this because miqt-docker already sets
the ldflags argument
2025-12-03 20:03:16 +13:00
2a22e92be4 makefile: add Windows icon and properties 2025-12-03 20:03:16 +13:00
5d829befca makefile: SOURCES needs 'shell' 2025-12-03 20:03:16 +13:00
2eb7385516 makefile: use conditional variables for helper tools 2025-12-03 20:03:16 +13:00
543f573c7f autoconfig: move defaults to Reset(), choose Bolt by default 2025-12-03 19:28:33 +13:00
463daba2cf autoconfig: fix labels for new autoconfig camelcase detection 2025-12-03 19:28:33 +13:00
9588e5189e autoconfig: update v0.4.1 for windows flicker issue 2025-12-03 19:28:25 +13:00
30716df112 doc/README: changelog for v0.9.0 2025-12-02 19:08:08 +13:00
ad8af93545 makefile: the linux release artefact should work on deb12 2025-12-02 19:00:39 +13:00
c306a6d1a5 doc: update README and TODO files 2025-12-02 17:09:33 +13:00
6a90605bd1 secret-service: initial commit 2025-12-02 17:01:49 +13:00
c69089841a ui: fix recursive triggering of on-edit signal, reinstate explicit-edit colours 2025-12-02 13:58:34 +13:00
ddbc30ed01 connection-manager: support import/export of saved connections 2025-12-01 23:17:56 +13:00
292e13a3e0 ui: fix panic in setting cell background colour (not fully understood) 2025-12-01 23:06:31 +13:00
7aa6703ee0 app: fix row-deletion from triggering the edit callback 2025-12-01 23:06:01 +13:00
f90f76c097 doc: update README and TODO files 2025-12-01 19:39:00 +13:00
134d4cf290 lmdb: edit support (minimal) 2025-12-01 19:38:01 +13:00
773470b30c lmdb: size key buffer based on database's maximum key size 2025-12-01 19:25:11 +13:00
f55ab455be ui: adjust recursive thresholds for preloading child nav 2025-12-01 18:56:15 +13:00
b77ea21378 connection-manager: load using saved displayname 2025-12-01 18:56:04 +13:00
002298b4ff lmdb: add multi-db mode, add create/delete child databases 2025-12-01 18:55:55 +13:00
293627ab17 lmdb: initial commit 2025-12-01 18:20:20 +13:00
14219d6d49 doc/TODO: update status 2025-11-30 19:34:43 +13:00
37a6f479b8 starskey: initial read-only implementation 2025-11-30 19:34:09 +13:00
176c51549c app: add defensive panics/error handling 2025-11-30 18:33:06 +13:00
17c0fb3332 app: use gob instead of json for stashing bucketPaths 2025-11-30 18:32:51 +13:00
4a4e8a76a3 config: fix test cleanliness in error wrappers 2025-11-30 18:31:59 +13:00
af77b83e27 doc: update README and TODO files 2025-11-30 17:53:25 +13:00
6b187c4142 debconf: group applications by first slash 2025-11-30 17:46:27 +13:00
da56321624 assets: re-optimize all embed images, strip colour-profile data 2025-11-30 17:13:37 +13:00
4447e149b9 bolt: add default names for autosaved connections 2025-11-30 17:04:38 +13:00
5850ba8836 connection-manager: refactor move config save/load to separate file 2025-11-30 17:04:31 +13:00
81206ae1f0 ssh: do not save H1 header field in json blob 2025-11-30 17:01:48 +13:00
5be200c672 ui: add comment re hidpi scaling 2025-11-30 17:01:39 +13:00
25d8af1043 ui: accurate transparency for properties text area 2025-11-30 17:01:34 +13:00
6abb5b6159 connection-manager: full implementation 2025-11-30 17:01:20 +13:00
1a20ca1a9c connection-manager: prompt to save on successful connection 2025-11-29 16:37:35 +13:00
38c5b88055 ssh: replace auth fields with OneOf 2025-11-29 16:11:35 +13:00
fb20955bf1 config: replace DB type registration with autoconfig.OneOf 2025-11-29 16:11:26 +13:00
8f76b5858d autoconfig: upgrade to v0.3.0 2025-11-29 16:11:10 +13:00
d8cad1e59e connection-manager: WIP 2025-11-25 18:18:12 +13:00
6bb1d0ab9f ui: add 'about qt' handling 2025-11-25 17:27:43 +13:00
35e06396e1 gui: redesign layout to use global toolbar, toggle editable 2025-11-25 17:27:36 +13:00
649cff7178 filter: fix all files *.* to use plain * 2025-11-25 17:27:08 +13:00
8236078ace doc/README: changelog for v0.8.0 2025-11-23 12:32:50 +13:00
4e084b914f doc/README: update latest status, database support in header 2025-11-23 12:32:07 +13:00
fc9965d757 makefile: use miqt-docker to produce release artefacts 2025-11-23 12:31:43 +13:00
ff315a8e1c doc/TODO: update latest status 2025-11-23 12:31:31 +13:00
f4863923a5 deps: update miqt to support qt6.5-static branch 2025-11-23 12:31:23 +13:00
7fe5fa02f6 autoconfig: bump v0.2.0 2025-11-23 11:53:46 +13:00
18139ee11b ui: confirm refresh if there were unsaved changes 2025-11-22 11:28:43 +13:00
daa79bf0d6 ui: f5 to refresh, f9 to execute 2025-11-22 11:28:31 +13:00
541fe5b0a8 sqlite: allow editing the primary key column 2025-11-22 11:11:19 +13:00
6cc8213490 util: add slice_find helper 2025-11-22 11:11:12 +13:00
5bf36d70c5 autoconfig: replace local package with github import 2025-11-15 15:01:11 +13:00
593df7abba autoconfig: update README 2025-11-15 14:37:56 +13:00
5776226130 autoconfig: move all type handling into interface method 2025-11-15 14:37:48 +13:00
fab96d4602 redis: add ssh tunnel connection option 2025-11-14 20:45:30 +13:00
abfa27d20e ssh: support ssh tunnels 2025-11-14 20:45:19 +13:00
104456049d autoconfig: support MultiLineString 2025-11-14 20:45:08 +13:00
9ea060fda7 autoconfig: the *.* filter hides files with no period in name 2025-11-14 20:45:03 +13:00
d568b75530 autoconfig: clean up dead code, use clearer typedefs 2025-11-14 20:14:51 +13:00
c0a11d936a autoconfig: support InitDefaults() 2025-11-14 20:14:22 +13:00
994cef8357 sqlite: fix test error in fmt specifier 2025-11-14 20:13:51 +13:00
82ddab1431 autoconfig: mutate existing struct 2025-11-14 20:01:40 +13:00
d1b0b4986e autoconfig: saving nested struct content 2025-11-14 19:27:19 +13:00
25d1609220 autoconfig: nicer icons for 'browse' 2025-11-14 12:26:52 +13:00
2609223ea6 autoconfig: extract types to registry 2025-11-14 12:23:06 +13:00
8be4c79556 autoconfig: initial work on child structs 2025-11-14 11:50:02 +13:00
5006ac6e91 autoconfig: add child dialog support + test 2025-11-14 10:36:28 +13:00
e2eb81da77 autoconfig: split to separate package 2025-11-14 10:36:16 +13:00
0f5bf963ba doc/TODO: update 2025-11-13 17:23:31 +13:00
8612ad630e leveldb: initial support 2025-11-13 17:23:17 +13:00
0dc90a546b assets/pencil: optipng all assets 2025-11-13 17:17:37 +13:00
5c9ad3bd9d badger: add backup, restore, and compact actions 2025-11-12 19:39:48 +13:00
d6c6dd594c doc/TODO: update 2025-11-12 19:15:13 +13:00
12fb79bb8b pebble: option for readonly 2025-11-12 19:15:10 +13:00
9003982da8 badger: allow setting encryption key, allow readonly 2025-11-10 19:27:57 +13:00
548f3dc68c config: add support for enums 2025-11-10 19:27:44 +13:00
43f334331b gui/miqt: initial work on edit capability (2) 2025-11-10 19:27:34 +13:00
cccb06caf0 gui/miqt: initial work on edit capability 2025-11-09 16:34:20 +13:00
f257901965 debconf: no need for child navigation 2025-11-09 16:34:07 +13:00
8e36e00460 gui/connect: hold dialog open until we successfully connect 2025-11-09 16:34:02 +13:00
07a749bb03 loadedDatabase: remove DisplayName(), pass it manually 2025-11-09 16:04:15 +13:00
fb069ed3b2 pebble: fix missing driver version display 2025-11-09 16:03:58 +13:00
f5d2e69007 gui: ensure we gc a database on close 2025-11-09 15:49:12 +13:00
35c2b01843 gui: add ctrl+o shortcut to open connection dialog 2025-11-09 15:46:28 +13:00
858af50136 gui: properties box should blend in with tab 2025-11-09 15:46:20 +13:00
13647edc0d gui: fix connection dialog not being modal 2025-11-09 15:46:04 +13:00
8b2c09d859 gui: context menu fixes, remove allowExpansion tracking 2025-11-09 15:45:56 +13:00
03c3a142df bolt: remove leftover printf specifier 2025-11-09 15:45:10 +13:00
318e634d9b gui/nav: reimplement first-level child preload 2025-11-09 15:06:43 +13:00
a51d568a87 doc/TODO: update 2025-11-08 13:20:13 +13:00
99c20e76de makefile(minor): add helper to launch qt6 designer 2025-11-08 13:20:09 +13:00
bc09478278 connect: new dialog, autogenerate fields from struct tags 2025-11-08 13:20:02 +13:00
18d782e7e5 badger, pebble: remove one-level 'Data' 2025-11-05 19:09:03 +13:00
4e6a359b10 miqt port: wip (4) 2025-11-05 18:55:19 +13:00
f42bdf219b bolt: hide child buckets from appearing in data tables 2025-11-05 18:54:47 +13:00
2f2972f97e vendor: deps update 2025-11-04 19:55:42 +13:00
5cf6a838ae miqt port: wip (3) 2025-11-04 19:52:55 +13:00
8e5e80af79 miqt port: wip (2) 2025-11-03 20:10:31 +13:00
49890599ea miqt port: wip (1) 2025-11-02 19:19:23 +13:00
c541e8b941 doc/README: add v0.7.0 download links 2024-07-18 18:15:10 +12:00
877f291a1f makefile: set dist as default target 2024-07-18 18:02:37 +12:00
6145320858 doc: v0.7.0 changelog, update TODO 2024-07-18 18:00:56 +12:00
8296a2fec9 gui: hardcode better windows colours 2024-07-18 17:51:27 +12:00
223d13be58 gui: toggle edit buttons as well 2024-07-18 17:49:16 +12:00
eca27dcd4f sqlite: basic editing support 2024-07-18 17:46:13 +12:00
0f2a3e021a gui: toggle the Query form fields if selected db is not queryable 2024-07-18 17:10:02 +12:00
90259fb2b9 gui: prevent submitting blank queries to db (seems to hang sqlite) 2024-07-18 17:09:48 +12:00
7573cf0453 app: upcast loadedDatabase to more specific interfaces 2024-07-18 17:09:32 +12:00
6dd0635c9e doc/TODO: update 2024-07-14 15:35:56 +12:00
ce3d08740f sqlite: add context actions for compact, export, drop table 2024-07-14 15:34:17 +12:00
8f5e1054fb db: return error from contextAction.Callback (2) 2024-07-14 15:28:18 +12:00
1ac96eb133 move filter consts to each db file 2024-07-14 15:27:54 +12:00
53e9b6555e doc/TODO: more ideas 2024-07-13 18:03:47 +12:00
e1a9f187cb db: return error from RenderForNav, contextAction.Callback 2024-07-13 18:03:42 +12:00
ee3110162b doc/TODO: update 2024-07-06 12:44:37 +12:00
35a83eb483 gui: basic syntax highlighting implementation (disabled) 2024-07-06 12:41:57 +12:00
60add3be86 db: add common errunsupported 2024-07-06 12:02:58 +12:00
2f65ffdd70 db: lift execQuery error handling to parent 2024-07-06 11:59:55 +12:00
aad92d27e9 gui: use icons for toolbar 2024-07-06 11:54:48 +12:00
21151be8a3 gui/images: load more image assets 2024-07-06 11:54:36 +12:00
f78eec1872 bolt: support editing 2024-07-06 11:45:41 +12:00
8af27f8834 gui: change tracking for insert, edit, delete actions 2024-07-06 11:04:19 +12:00
0d3b90b879 gui: prep work for inserting rows 2024-07-05 20:07:19 +12:00
2b59efc410 gui: add refresh button on data tab 2024-07-05 19:46:04 +12:00
7fbf2ef1ed gui: common column handling, set widths automatically 2024-07-05 19:35:24 +12:00
d7e3363173 gui: convert data tables from TListView to TStringGrid 2024-07-05 19:21:08 +12:00
cecfc338d4 gui: bigger default window size 2024-07-05 18:43:02 +12:00
35f09fc072 doc/README: add v0.6.0 download links 2024-06-30 14:17:59 +12:00
2163b46907 doc/README: changelog for v0.6.0 2024-06-30 14:16:06 +12:00
81b6b08e7b doc/TODO: status update 2024-06-30 14:15:53 +12:00
f31724a110 gui: add extra space in help menu driver list 2024-06-30 14:14:50 +12:00
063a8ca837 debconf: add as database option 2024-06-30 14:14:41 +12:00
1cfc94a42b debconf: fix extra spaces, Name column ordering 2024-06-30 14:14:26 +12:00
053e07c319 debconf: implement dat file parser 2024-06-30 13:47:29 +12:00
0b91c379b8 doc/README: update partial changelog 2024-06-30 13:28:17 +12:00
7b4cc885f5 gui: use Consolas as monospace font on Windows 2024-06-30 13:16:27 +12:00
3b17ddd8a4 sqliteclidriver: always get columns in the right order 2024-06-30 13:12:11 +12:00
8d051a14e5 orderedkv: initial commit 2024-06-30 13:11:54 +12:00
4735c391bd doc/TODO: update status 2024-06-30 12:51:40 +12:00
0866e5edac sqliteclidriver: clear content from ret if sentinel was found 2024-06-30 12:51:05 +12:00
5c44dc5f54 sqliteclidriver: indicate driver in status bar 2024-06-30 12:48:22 +12:00
a7dd1ca340 makefile: compile cgo with -O2 2024-06-30 12:45:40 +12:00
abcf7dbfe5 sqliteclidriver: better bubble up stderr errors 2024-06-30 12:45:23 +12:00
d359f42b24 sqlite: support tables named using special characters 2024-06-30 12:45:03 +12:00
7cec5cee4c sqliteclidriver: use channel events, handle no results via sentinel 2024-06-30 12:33:47 +12:00
be91cd54c6 eventcmd: initial commit of channel-based process wrapper 2024-06-30 12:33:01 +12:00
b141aaaa6c lexer: separate tokens for top-level special characters 2024-06-30 11:26:00 +12:00
493ab846b9 gui: adjust styles for query frame 2024-06-30 11:10:19 +12:00
d3ebcb4666 gui: fix popup position for redis connection dialog 2024-06-30 11:05:25 +12:00
50cf207eae gui: fix properties tab background colour for windows build 2024-06-30 11:05:16 +12:00
e5cbbb6822 makefile/linux: don't upx, but xz harder, for faster startup 2024-06-30 10:45:19 +12:00
18674568dd makefile/windows: upx harder 2024-06-30 10:45:08 +12:00
748dd96267 makefile/windows: set windowsgui flag 2024-06-30 10:44:59 +12:00
c5578daa9f makefile: add cleanups before targets 2024-06-30 10:44:13 +12:00
3bc7f539ad doc/README: add v0.5.0 download links 2024-06-29 13:03:55 +12:00
52677224c1 doc/README: changelog for v0.5.0 2024-06-29 12:59:54 +12:00
0a31eab9f2 doc: update screenshot 2024-06-29 12:59:41 +12:00
011063597d makefile: improvements for dist archives 2024-06-29 12:54:57 +12:00
71c182692a makefile: use cgo for windows build 2024-06-29 12:54:47 +12:00
e15af5a544 makefile: add targets for release dist builds 2024-06-29 12:41:05 +12:00
ba7228ad44 doc/TODO: update 2024-06-29 12:15:26 +12:00
650c9e7183 sqlite: basic integration for the cli driver 2024-06-29 12:13:34 +12:00
471737f421 gui: add helper function for menu separators 2024-06-29 12:13:18 +12:00
d2b9618da0 sqliteclidriver: initial commit 2024-06-29 11:56:45 +12:00
f0f0ff7904 lexer: add re-quote helper 2024-06-29 11:56:26 +12:00
639da11ab3 add custom lexer, use for redis string splitting 2024-06-29 11:21:30 +12:00
fc084d7190 gui: seamless refresh after bucket action, support nav removal 2024-06-28 12:29:44 +12:00
e2111017eb gui: implement nav refresh 2024-06-28 12:04:39 +12:00
ef70e5825a gui: add nav context images for refresh + close 2024-06-28 12:00:18 +12:00
15b29b32ce bolt: support adding/removing child buckets 2024-06-28 11:53:07 +12:00
975e120530 main: move appname to constant 2024-06-28 11:53:01 +12:00
5b8883d31a bolt: add nav suffix for readonly connections 2024-06-28 11:52:50 +12:00
ce43f5765c bolt: support 'create new database' 2024-06-28 11:40:03 +12:00
feffa67677 main: infra for custom context actions 2024-06-28 11:34:00 +12:00
a14e58297a bolt: support open as readonly 2024-06-28 11:33:43 +12:00
2b309fbda7 gui: add help option to show driver versions 2024-06-28 11:21:14 +12:00
70db402cdf gui: use helper function for setting up menus 2024-06-28 11:11:34 +12:00
a82d5e6b26 gui: run optipng on some images 2024-06-28 11:05:57 +12:00
28570b0b96 versions: replace embed go.mod with stdlib debug api 2024-06-28 11:05:49 +12:00
9bf84fa140 pebble: initial support 2024-06-28 11:01:34 +12:00
eb34221620 doc/README: changelog for v0.4.0 2024-06-23 16:35:25 +12:00
cc3ba4d9f0 doc/TODO: update current progress 2024-06-23 16:29:53 +12:00
f79d17afed badger: support temporary in-memory databases 2024-06-23 16:29:53 +12:00
d7c2282335 badger: add dgraph vendor icon 2024-06-23 16:29:53 +12:00
43002a9fde main: restructure nav menu into alphabetical per-DB options 2024-06-23 16:29:53 +12:00
bc33d26cfd images: add vendor logos 2024-06-23 16:29:47 +12:00
38d9e6238f main: add help menu with website link 2024-06-23 15:58:12 +12:00
a653ef8ca4 main: get accurate cross-platform background colour for properties tab 2024-06-23 15:58:03 +12:00
cc336366c9 redis: show results in query tab 2024-06-23 15:57:33 +12:00
8f105183eb redis: handle data browsing for different typed keys 2024-06-23 15:57:20 +12:00
b45faa2e73 main: add nav context menu, support closing open connections 2024-06-23 15:28:15 +12:00
7e5d17100d main: preload recursive navigation only one layer at a time 2024-06-23 15:21:41 +12:00
a817e5fa21 main: recursively load all nav state at connection time 2024-06-23 15:12:26 +12:00
3d185033f3 main: allow scrolling long content on properties tab 2024-06-23 14:56:00 +12:00
924957d00d main: abstract the nil selection into a virtual database type 2024-06-23 14:55:46 +12:00
d674078071 main: auto switch to newly opened database 2024-06-23 14:54:49 +12:00
5992d19906 redis: complete basic multidatabase browse support 2024-06-23 14:54:33 +12:00
8cac46e9f2 doc: update README + TODO 2024-06-23 13:13:43 +12:00
db5f6816c5 main: allow running partial query by selection 2024-06-23 13:07:52 +12:00
3a4bdbde94 redis: improve dialog style, open connection, enumerate databases 2024-06-23 13:07:42 +12:00
0363bc65f4 platform_windows: move extra syso inclusion out of main 2024-06-23 13:06:41 +12:00
a47898e099 format: move to util_ file 2024-06-23 13:06:25 +12:00
1487b18a3a doc/TODO: add notes re virtual list rendering 2024-06-22 17:36:34 +12:00
04ef53720f redis: initial work on connection dialog 2024-06-22 17:36:25 +12:00
bc82aacd57 redis: add library dependency 2024-06-22 17:36:19 +12:00
6dcc1afd6b rename files to db_ prefixes 2024-06-22 16:22:33 +12:00
645ab29cdd doc/README: changelog for v0.3.0 2024-06-15 13:22:02 +12:00
4202b9b970 doc/README: update current features 2024-06-15 13:21:55 +12:00
42bbe3957a gitignore: exclude test data folder 2024-06-15 13:17:59 +12:00
5e2aae9032 sqlite: use cgo-free driver if necessary 2024-06-15 13:16:48 +12:00
f54577f93f doc/TODO: initial commit 2024-06-15 13:08:14 +12:00
f4d2d2ec39 gui: improvements for execute toolbar 2024-06-15 12:57:35 +12:00
5cd3f6c765 badger: initial support 2024-06-15 12:37:44 +12:00
5d268d22af gui: strip extra quote marks from string cells 2024-06-15 12:15:29 +12:00
5e0422e10f gui: add query shortcut, or switch tab if not focused 2024-06-15 12:15:29 +12:00
ef30a0d210 add new custom query feature 2024-06-15 12:15:21 +12:00
38847b3a7e sqlite: refactor separate populateRows, populateColumns 2024-06-15 12:13:33 +12:00
f432b52652 gui: use soft close from menuitem instead of hard pkill 2024-06-15 11:44:19 +12:00
f64522dfa1 gui: more icons on tables, tabs 2024-06-15 11:43:58 +12:00
79bce40581 gui: fix icons going missing when selecting in nav tree 2024-06-15 11:43:36 +12:00
3731ee1781 show current/selected driver name in status bar 2024-06-15 11:43:24 +12:00
2481a1da20 gui: set main app icon 2024-06-15 11:42:23 +12:00
1035086ed4 images: add more images 2024-06-15 11:42:13 +12:00
038eb44f48 bolt: update v1.3.10 to v1.4.0-alpha.1 2024-06-15 11:42:02 +12:00
841575700e doc/README: fill in current statuses 2024-06-08 15:09:26 +12:00
617393b627 sqlite: load table data 2024-06-08 15:02:02 +12:00
91f9c5fc30 sqlite: load column headers 2024-06-08 14:49:57 +12:00
6234f02ea6 gui: attach some icons to menu and nav tree 2024-06-08 14:24:44 +12:00
8b1e7064e7 gui: create a TImageList with famfamfam/silk resources 2024-06-08 14:24:35 +12:00
00a96bfe84 sqlite: pull table schema 2024-06-08 14:23:38 +12:00
232a1dd0e8 sqlite: initial support 2024-06-08 13:44:18 +12:00
cb4b35b059 gui: add exit menu item 2024-06-08 13:34:49 +12:00
f913b63c58 bolt: refactor extract to separate interface 2024-06-08 13:34:33 +12:00
d97c8872de doc: add v0.1.0 changelog 2024-06-08 13:33:54 +12:00
78d4b90189 gitignore: also exclude windows binary 2024-06-03 17:00:51 +12:00
e30e3e6138 doc: update README, add screenshot 2024-06-03 17:00:33 +12:00
f22d149a66 initial commit 2024-06-03 16:49:04 +12:00
153 changed files with 11079 additions and 2127 deletions

19
.gitignore vendored
View File

@@ -1,16 +1,5 @@
# development
testdata/
qbolt
dummy-data/dummy-data*
# temporary build files
rsrc_windows_amd64.syso
windows-manifest.json
# release build files
build/qbolt
build/qbolt.exe
build/*.xz
build/*.zip
# local makefile definition scripts
make-*
qbolt.exe
qbolt.linux64.tar.xz
qbolt.win64.zip

228
CHANGELOG.md Normal file
View File

@@ -0,0 +1,228 @@
# Changelog
Unreleased vNext
- Badger, Bolt, BuntDB: Support full advanced configuration options
- Badger, Bolt: Show full paths to database file on hover
- SQLite CLI/SSH driver: Fix a syntax issue browsing tables, fix an issue if a table has zero rows
- Allow exporting configuration via new `--config-export` and `--config-import` command-line arguments
- UI: Show column's datatype in hover tooltip
- UI: Add context menu actions to data view
- UI: Allow setting cells to NULL
- UI: Detect integer Unix timestamps (seconds or milliseconds) and show local+UTC time in hover tooltip
- UI: Allow configuring the appearance - Qt Style, Density/Padding, and alternating row backgrounds
- UI: Reduce default padding in the main window
- UI: Update autoconfig library to v0.6.0: large configuration dialogs now scrollable, adds "reset to default" buttons
- UI: Show basic database properties when hovering connection in Connection Manager and in main tree view
- Connection Manager: Add `(2)` prefixes when saving new connections
- Fix an issue with uninitialized slice data appearing in binary data columns, affecting at least Badger
- Fix a cosmetic issue Connection Manager saying "fail to save" error messages if we fail to load
2026-01-04 v2.1.0
- SQLite: Support SSH tunnel
- SQLite: Strict type handling, support nullable columns, always rely on `rowid` as PK
- SQLite: Remove hardcoded LIMIT
- SQLite: Hide internal sqlite_sequence and sqlite_stat1 tables
- BuntDB: Initial support, including editing
- VoidDB: Initial support, including editing and multiple keyspace management
- Pogreb: Initial support, including compaction
- Fix an issue with SQL queries running twice
- Fix an issue with not showing multiple connection errors from the connection manager dialog
- Truncate large binary column display in grid
- UI: Add icons for SSH/Password connection options, for Bolt/Zip conversion tool
- UI: Show 'null' as grey in tables
- UI: Clickable links to Go.mod dependencies in 'Driver versions' data table
- UI: When connecting to a new DB, always start on the Properties tab
- UI: Allow toggling separate toolbar sections in context menu
- UI: Disable Query tab for non-queryable database types
- UI: Fix extra labels appearing when editing large binary columns
2025-12-20 v2.0.0
- Merge yvbolt and QBolt together
- App: Show application version in main window titlebar
2025-12-20 v0.11.0 (yvbolt)
- Badger: Change encryption configuration options (**BREAKING**)
- SSH Agent: Initial support, including lock/unlock
- Badger, SQLite: Fix "all files" spec when choosing an export file path
- Bolt: Support import/export as zip archive
- Support SSH agents when using SSH tunnels (e.g. for Redis or Mongo)
2025-12-17 v0.10.1 (yvbolt)
- Fix mixed Qt 5 / Qt 6 syntax
- Build release binaries with Go1.26
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.10.1)
2025-12-16 v0.10.0 (yvbolt)
- MongoDB: Initial support, including SSH tunnel, managing collections, traversing BSON documents, and querying
- LotusDB: Initial support, including editing
- Bitcask: Initial support, including editing and backup
- RoseDB: Initial support, including editing
- Badger: Fix k/v column display, support editing
- LMDB: Support editing, add warning for destructive actions
- LevelDB: Support editing
- Pebble: Support editing
- Starskey: Fix k/v column display, support editing
- Redis: Bump driver version v9.16.0 -> v9.17.2
- Bolt: Add warning for destructive actions
- Connection manager: When saving, use the database's preferred name
- App: Add hex viewer for binary data if it is not valid UTF-8
- App: Use virtualized table renderer
- App: Use multiple plug-in typed column stores in table backend
- App: Fix graphical flicker when editing connections on Windows
- App: Set up icon and exe properties on Windows
- App: Embed version number in release builds
- App: Show all driver versions as a virtual data table
- App: Show unsaved-changes warning also when changing database
- App: Fix rich text formatting appearing when pasting into the query window
2025-12-02 v0.9.0 (yvbolt)
- LMDB: Initial support, including multi-database mode, data editing, and managing child databases
- Starskey: Initial support
- Freedesktop.org Secret Service: Initial support, including unlocking collections and creating child collections
- App: Use global toolbar style
- App: Add connection manager, encrypting credentials with AEAD AES256-GCM using OS keychain
- App: Offer to save valid quick-connection to the connection manager
- App: Add 'About Qt' menu option
- App: Fixed file extension filter for database files with no extension
- App: Fixed background colour for Properties area on different OSes
- App: Fixed libpng warnings about greyscale image data for embedded logo images
- App: Fixed issue with non-UTF8 child database names
- Debconf: Improve navigation speed by splitting applications into virtual tables
- SSH: Redesign options to pick only one of the available auth methods
- Fixed running release binary on Debian 12
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.9.0)
2025-11-23 v0.8.0 (yvbolt)
- Port from [liblcl](https://github.com/ying32/liblcl) to [MIQT Qt 6](https://github.com/mappu/miqt)
- Badger: Upgrade v4.2.0 -> v4.8.0
- Pebble: Upgrade v1.0.0 -> v1.1.5
- SQLite: Upgrade v1.14.22 -> v1.14.32
- Redis: Upgrade v9.5.3 -> v9.16.0
- Bolt: Upgrade v1.4.0-alpha.1 -> v1.4.3
- Bolt: Fix child buckets appearing in data area
- Badger, Pebble, Debconf: Remove redundant "Data" navigation layer
- Badger: Support encrypted databases
- Badger: Support readonly databases
- Badger: Add context-menu actions for backup, restore, and compact
- Pebble: Support readonly databases
- LevelDB: Add LevelDB database integration
- Redis: Support SSH tunnel
- SQLite: Allow editing the primary key column
- App: New style connection dialog
- App: Updated keyboard shortcuts (Ctrl+O to open new connection, F5 to refresh, F9 to execute query)
- App: Add confirmation when refreshing the data table if there are uncommitted changes
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.8.0)
2025-05-04 1.1.0 (qbolt)
- New feature to import/export database as zip archive
- Upgrade to Qt 6
- Add keyboard shortcuts for refresh
- Improve High DPI support
- Rebuild artefacts with miqt v0.10.0, etcd-io/bbolt v1.4.0, go 1.23, Qt 6.8 (win64)
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.1.0)
2024-10-05 1.0.3 (qbolt)
- Port from hybrid Go/C++ to now using [MIQT](https://github.com/mappu/miqt)
- Switch Windows build to win64
- Rebuild artefacts with miqt v0.5.0, etcd-io/bbolt v1.3.11, go 1.19 (deb12), go 1.23 (win64)
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.3)
2024-07-18 v0.7.0 (yvbolt)
- SQLite, Bolt: Initial support for editing data (insert, per-cell update, delete)
- SQLite: Add context menu actions for compact (vacuum), export, and drop table
- App: New grid widget
- App: Add refresh button
- App: Bigger window size, use icons for toolbars, better UI colours for Windows
- App: Prevent submitting blank queries to database
- Refactor database interface and error handling
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.7.0)
2024-06-30 v0.6.0 (yvbolt)
- Debconf: Add as supported database
- SQLite: Support table names containing special characters
- SQLite: Improvements for experimental command-line driver
- Redis: Improve connection dialog window position
- App: Cosmetic fixes for frame borders, help dialog, and Windows fonts+colours
- Build: Change compression parameters for release builds
- Build: Compile CGO with -O2 for release builds
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.6.0)
2024-06-29 v0.5.0 (yvbolt)
- Pebble: Add as supported database
- Bolt: Support opening as readonly
- Bolt: Support creating new databases
- Bolt: Support adding/removing recursive child buckets
- SQLite: Support custom CLI driver that parses `/usr/bin/sqlite3 -json` output (experimental)
- Redis: Improve query parser to support quoted strings
- App: Support refreshing elements in nav tree
- App: Help menu option to show driver versions
- App: Add image icons for refresh and close context menu actions
- Build: Add makefile for cross-compiling release binaries
- [⬇️ Download here](https://git.ivysaur.me/code.ivysaur.me/yvbolt/releases/tag/v0.5.0)
2024-06-23 v0.4.0 (yvbolt)
- Redis: Add as supported database
- Badger: Allow creating in-memory databases
- App: Allow selecting partial query text to execute
- App: Allow closing database connections from context menu
- App: Allow scrolling large content on Properties pane
- App: Preload recursive navigation
- App: Automatically switch to selected database when new connection is created
- App: Add help website link
- App: Add database logo images
2024-06-25 v0.3.0 (yvbolt)
- Badger: Add BadgerDB v4 as supported database
- SQLite: Add support for CGo-free SQLite driver under cross-compilation
- Bolt: Update Bolt to v1.4.0-alpha.1
- App: Add support for running custom queries
- App: Add status bar showing currently selected DB
- App: Fix missing icons in nav when selecting items
- App: Fix extra quotemarks when browsing string content of database
2024-06-08 v0.2.0 (yvbolt)
- SQLite: Add SQLite support (now requires CGo)
- App: Add images for menu and navigation items
2024-06-03 v0.1.0 (yvbolt)
- Initial public release
2020-04-12 1.0.2 (qbolt)
- 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 (qbolt)
- 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
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.1)
2017-05-21 1.0.0 (qbolt)
- Initial public release
- 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.
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.0)

9
LICENSE Normal file
View File

@@ -0,0 +1,9 @@
ISC License
Copyright 2025 mappy
Copyright 2025 The QBolt Author(s)
Copyright 2025 The yvbolt Author(s)
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

127
Makefile
View File

@@ -1,67 +1,80 @@
VERSION := 1.1.0
GOFLAGS_L := -ldflags='-s -w -X main.Version=v$(VERSION)' -buildvcs=false -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)'
GOFLAGS_W := -ldflags='-s -w -X main.Version=v$(VERSION) -H windowsgui' -buildvcs=false --tags=windowsqtstatic -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)'
SHELL := /bin/bash
GO := go
MIQT_DOCKER := miqt-docker
MIQT_UIC := miqt-uic
MIQT_RCC := miqt-rcc
GO_WINRES := go-winres
SOURCES := $(wildcard *.go *.ui *.qrc) resources.go resources.rcc mainwindow_ui.go itemwindow_ui.go rsrc_windows_amd64.syso
SHELL:=/bin/bash
SOURCES := $(shell find . -name '*.go' -type f)
GIT_REV := $(shell git describe --exact-match --tags 2>/dev/null || printf "%s-%s" $$(git describe --tags --abbrev=0) $$(git rev-parse HEAD | head -c8))
.DEFAULT_GOAL := dist
MIQT_UIC ?= ~/go/bin/miqt-uic
MIQT_RCC ?= ~/go/bin/miqt-rcc
MIQT_DOCKER ?= ~/go/bin/miqt-docker
GO_WINRES ?= ~/go/bin/go-winres
.PHONY: all
all: build/qbolt build/qbolt.exe
.PHONY: generate
generate:
/bin/bash -c '( echo "<RCC>" ; echo " <qresource prefix=\"/\">" ; for f in assets/* ; do echo " <file>$$f</file>" ; done ; echo " </qresource>" ; echo "</RCC>" ) > embed.qrc'
$(MIQT_UIC) -InFile mainwindow.ui -OutFile mainwindow.go -Qt6
$(MIQT_UIC) -InFile connectionDialog.ui -OutFile connectionDialog.go -Qt6
$(MIQT_UIC) -InFile connectionManagerDialog.ui -OutFile connectionManagerDialog.go -Qt6
$(MIQT_RCC) -Input embed.qrc -Qt6
.PHONY: designer
designer:
/usr/lib/qt6/bin/designer &
.PHONY: optimize-images
optimize-images:
# Strip iCCC colour chunks that libpng/Qt complain about at runtime
for f in assets/*.png ; do convert "$$f" -strip "$$f" ; done
optipng -quiet -o5 assets/*.png
make generate
qbolt: $(SOURCES)
# Target a debian-12 baseline build
sed -i -re 's/".+"/"$(GIT_REV)"/g' version.go
$(MIQT_DOCKER) linux64-go1.26-qt6.4-dynamic -minify-build
git checkout -- version.go
chmod 755 qbolt
upx --lzma qbolt
qbolt.exe: $(SOURCES)
sed -i -re 's/".+"/"$(GIT_REV)"/g' version.go
$(MIQT_DOCKER) win64-cross-go1.26-qt6.5-static -windows-build --tags=windowsqtstatic
git checkout -- version.go
$(GO_WINRES) patch --in winres.json --no-backup --product-version git-tag --file-version git-tag qbolt.exe
upx --lzma qbolt.exe
qbolt.apk: $(SOURCES)
$(MIQT_DOCKER) android-armv8a-go1.23-qt6.6-dynamic -android-build
qbolt.linux64.tar.xz: qbolt
rm -f qbolt.linux64.tar.xz
XZ_OPT='-T0 -9' tar caf qbolt.linux64.tar.xz --owner=0 --group=0 qbolt
qbolt.win64.zip: qbolt.exe
rm -f qbolt.win64.zip
zip -9 qbolt.win64.zip qbolt.exe
.PHONY: dist
dist: build/qbolt-${VERSION}-windows-x86_64.zip build/qbolt-${VERSION}-debian12-x86_64.tar.xz
dist: qbolt.linux64.tar.xz qbolt.win64.zip
.PHONY: clean
clean:
rm -f qbolt
rm -rf build
mkdir -p build
touch build/.create_dir
rm -f windows-manifest.json
rm -f rsrc_windows_amd64.syso
rm -f resources.go
rm -f resources.rcc
# Generated files
resources.rcc resources.go: resources.qrc
$(MIQT_RCC) -Qt6 -Input resources.qrc
mainwindow_ui.go: mainwindow.ui
$(MIQT_UIC) -Qt6 -InFile mainwindow.ui -OutFile mainwindow_ui.go
git checkout -- version.go
rm -f qbolt.exe qbolt qbolt.linux64.tar.xz qbolt.win64.zip
itemwindow_ui.go: itemwindow.ui
$(MIQT_UIC) -Qt6 -InFile itemwindow.ui -OutFile itemwindow_ui.go
windows-manifest.json: windows-manifest.template.json Makefile
cat windows-manifest.template.json | sed -re 's_%VERSION%_$(VERSION)_' > windows-manifest.json
rsrc_windows_amd64.syso: windows-manifest.json
$(GO_WINRES) make --in windows-manifest.json
rm rsrc_windows_386.syso || true # we do not build x86_32
# Linux release
build/qbolt: $(SOURCES)
CGO_CFLAGS='-Os -ffunction-sections -fdata-sections -flto=auto' CGO_CXXFLAGS='-Os -ffunction-sections -fdata-sections -flto=auto' CGO_LDFLAGS='-Wl,--gc-sections -flto=auto -fwhole-program' $(GO) build $(GOFLAGS_L) -o build/qbolt
upx build/qbolt
#####
# Test databases in Docker
build/qbolt-${VERSION}-debian12-x86_64.tar.xz: build/qbolt
XZ_OPTS=-9e tar caf build/qbolt-${VERSION}-debian12-x86_64.tar.xz -C build qbolt --owner=0 --group=0
# Windows release (docker)
.PHONY: test-mongo
test-mongo:
sudo docker run --rm -p 127.0.0.1:27017:27017 -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=toor mongo:latest
build/qbolt.exe: $(SOURCES)
# -flto causes internal compiler error
$(MIQT_DOCKER) win64-qt6-static /bin/bash -c "CGO_CFLAGS='-Os -ffunction-sections -fdata-sections' CGO_CXXFLAGS='-Os -ffunction-sections -fdata-sections' CGO_LDFLAGS='-Wl,--gc-sections -fwhole-program' go build $(GOFLAGS_W) -o build/qbolt.exe"
# Must be stripped before upx'ing - @ref https://github.com/msys2/MSYS2-packages/issues/454
# However this removes the rsrc, loses the icon and causes a Defender detection
# strip build/qbolt.exe
# upx build/qbolt.exe
.PHONY: test-redis
test-redis:
sudo docker run --rm -p 127.0.0.1:6379:6379 redis:latest
build/qbolt-${VERSION}-windows-x86_64.zip: build/qbolt.exe
zip -9 -j build/qbolt-${VERSION}-windows-x86_64.zip build/qbolt.exe
.PHONY: test-etcd
test-etcd:
# v3.5 series: last version to support both etcd v2 and v3 APIs
# Optional: can use `--experimental-enable-v2v3 ''` flag to map v2 API into the v3 namespace,
# otherwise they are separate storages
# Test URL: http://127.0.0.1:2379/
sudo docker run --rm -p 2379:2379 -p 2380:2380 gcr.io/etcd-development/etcd:v3.5.26 /usr/local/bin/etcd --name s1 --enable-v2 --experimental-enable-v2v3 'mapping-prefix-here' --data-dir /etcd-data --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://0.0.0.0:2379

106
README.md
View File

@@ -1,62 +1,68 @@
# qbolt
# QBolt
A graphical database manager for BoltDB.
QBolt allows you to graphically view and edit the content of Bolt databases.
Written in Golang (Qt)
A graphical interface for multiple databases.
## Features
- Open existing database or create new database
- 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
- Lightweight native desktop application, running on Linux, Windows, macOS, and Android
- Supports many database types
- Connect to multiple databases at once
- Browse table/bucket content
- Use context menu to perform special table/bucket actions
- Edit content, and add/delete rows for supported databases
- "Set cell to null" via context menu
- View database/bucket statistics and metadata
- Run custom SQL queries
- Select text to run partial query
- Optimised grid renderer
- Uses virtual-scrolling and a type-safe column-store
- Safe handling for non-UTF8 key and data fields
- Hex viewer for binary data
- Detect integer Unix timestamps (seconds or milliseconds) and show local+UTC time in hover tooltip
- Configurable appearance (style, density, alternating row backgrounds)
- Connection Manager saves connections with AEAD AES256-GCM using OS keychain
- Command-line feature to import/export saved connections
- SSH tunnel for supported databases
## Supported databases
There are currently 19 supported databases:
Database |Read |Editing |Query |Connection options |Context menu actions
-------------|------|---------|------|--------------------|--------
Badger v4 |Yes |Yes |No |Encrypted, readonly, in-memory, advanced |Backup, restore, compact
Bitcask |Yes |Yes |No |Readonly, autorecovery |Backup
BuntDB |Yes |Yes |No |In-memory, advanced |Shrink
Bolt |Yes |Yes |No |Readonly, advanced |Create/delete child buckets, import/export as zip
Debconf |Yes |No |No | |
Etcd |Yes |Yes |No |v2/v3 |(v2) Create/delete child databases
Freedesktop.org Secret Service |Yes |No | No | |Unlock, create new collection
LevelDB |Yes |Yes |No |Readonly |
LMDB |Yes |Yes |No |Multi-DB, readonly |Create/delete child databases
LotusDB |Yes |Yes |No | |
MongoDB |Yes |No |Yes |SSH tunnel |Create/delete child databases and collections
Pebble |Yes |Yes |No |Readonly, in-memory |
Pogreb |Yes |Yes |No | |Compact
Redis |Yes |No |Yes |SSH tunnel, RESP v3 |
RoseDB |Yes |Yes |No | |
SQLite |Yes |Yes |Yes |**SSH tunnel**, CLI driver, in-memory |Vacuum, export
SSH Agent |Yes |No |No |Unix/TCP |Lock, unlock
Starskey |Yes |Yes |No |Compression |
VoidDB |Yes |Yes |No |Multi-keyspace |Create/delete child keyspaces
## License
Source code content of `qbolt-x.x.x-src.tar.gz` is released under the ISC license.
BoltDB is released under the MIT license.
The Windows binary is released under LGPL-3+ owing to the static copy of Qt.
The code in this project is licensed under the ISC license (see `LICENSE` file for details) with the following caveats:
## See also
- This project depends on third-party libraries under additional open source licenses.
- This project redistributes images from the [famfamfam/silk icon set](https://github.com/markjames/famfamfam-silk-icons) under the [CC-BY 2.5 license](http://creativecommons.org/licenses/by/2.5/).
- This project includes trademarked logo images for each supported database type.
- The Windows binary is released under LGPL-3+ owing to the static copy of Qt.
- BoltDB https://github.com/boltdb/bolt
## Download
Get the latest version from [the releases page »](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases)
## Changelog
2025-05-04 1.1.0
- New feature to import/export database as zip archive
- Upgrade to Qt 6
- Add keyboard shortcuts for refresh
- Improve High DPI support
- Rebuild artefacts with miqt v0.10.0, etcd-io/bbolt v1.4.0, go 1.23, Qt 6.8 (win64)
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.1.0)
2024-10-05 1.0.3
- Port from hybrid Go/C++ to now using [MIQT](https://github.com/mappu/miqt)
- Switch Windows build to win64
- Rebuild artefacts with miqt v0.5.0, etcd-io/bbolt v1.3.11, go 1.19 (deb12), go 1.23 (win64)
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.3)
2020-04-12 1.0.2
- Rebuild artefacts with etcd-io/bbolt v1.3.5, go 1.15, Qt 5.15, and new GCC versions
- Switch from hg to Git
- Use Go modules
- Add support for building Windows binary in Docker
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.2)
2017-06-19 1.0.1
- Feature: Option to open database as read-only
- Fix an issue with support for bucket names and keys not surviving UTF-8 roundtrips (now binary-clean)
- Fix an issue with crashing when deleting a bucket other than the selected one
- Fix a cosmetic issue with application icon on Windows
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.1)
2017-05-21 1.0.0
- Initial public release
- 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.
- [⬇ Download here](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases/tag/v1.0.0)
See [the full change history »](https://git.ivysaur.me/code.ivysaur.me/qbolt/src/branch/master/CHANGELOG.md)

141
TODO Normal file
View File

@@ -0,0 +1,141 @@
- Drag and drop database into UI (QBolt parity)
- Portable mode (portable.txt or portable/ dir)
- Syntax highlighting in editor
- Autorefresh
- Sshagent
- want to trigger an async refresh from inside the LDB after lock/unlock
- support adding/removing keys (will need per-row actions)
- SSH: knownhost parser is stricter than openssh, does not support hostname if there is known a knownhost for the IP address
- Bolt: import/export should support passworded zips
- Table: BSON view can't see data
- Table: quick filter
- QSortFilterProxyModel
- Cancellation
- Loading animations for connection + queries
- Delay rendering properties/data tab until tab is focused
- Mutation
- Debconf: Support insert/update/delete
- Redis: Support insert/update/delete
- SecretService: Support insert/update/delete
- Binary data viewer
- Detect jpg/png and show as image
- More DB types
- MySQL (& MariaDB/TiDB)
- Postgres
- CLI using psql
- Firebird/interbase (embedded and remote)
- Lungo: Mini embeddable Mongo - https://github.com/256dpi/lungo
- MSSQL (recursive navigation for instances)
- NutsDB https://github.com/nutsdb/nutsdb
- Sniper https://github.com/recoilme/sniper
- DuckDB https://github.com/duckdb/duckdb-go
- Other K/V stores from https://github.com/smallnest/kvbench
- Windows registry
- Allow entering path for quick navigation
- LDAP
- Dolt
- Memcache
- Listing all keys is not well supported, needs hacks
- Chai (built on Pebble) - https://github.com/chaisql/chai
- CloverDB (built on Bolt/Badger) - https://github.com/ostafen/clover
- APCu - need some sort of hook into the storage engine
- UnisonDB - https://github.com/ankur-anand/unisondb
- CSV file
- Allow querying with sqlite or duckDB?
- Parquet file
- Allow querying with duckDB?
- SSDB (Redis-compatible)
- Time-series DBs
- Prometheus
- VictoriaMetrics
- FrostDB https://github.com/polarsignals/frostdb
- https://dbdb.io/browse?programming=go-lang&q=
- Maxmind GeoIP MMDB format
- https://github.com/maxmind/mmdbwriter
- KeePass kdbx
- Not-quite-DBs
- IRC client
- Docker daemon (images, containers, ...)
- ssh known-hosts
- golang.org/x/crypto/ssh/knownhosts - already using this package
- Generic ODBC, database/sql, ...
- Other language DBs
- C, C++
- Berkeley BDB
- Tokyo Cabinet, Kyoto Cabinet, Tkrzw
- https://github.com/TerraTech/go-tokyocabinet needs pkg-config tokyocabinet
- https://github.com/estraier/tkrzw-go needs pkg-config tkrzw
- cdb (DJB's Constant Database)
- Other classic DBM (Samba tdb, GNU gdbm, ...)
- `/var/cache/man/index.db` is a gdbm file
- RocksDB
- https://github.com/tecbot/gorocksdb Go bindings, need pkg-config rocksdb
- Rust
- Stoolap https://github.com/stoolap/stoolap
- Rust, needs C binding layer https://github.com/mozilla/cbindgen
- JDBC Java databases
- H2, HSQLDB, Apache Derby
- SQLite CLI driver:
- Context support
- Write support
- Type handling for columns
- Binary data is losing its \uXXXX escaping and appearing as string
- Unix timestamps are appearing with scientific notation
- Configure binary path
- Error handling: if an error occurs, listing db tables has problems/shows separators
- https://ricardoanderegg.com/posts/sqlite-remote-explorer-gui/
- https://github.com/litements/litexplore
- Badger:
- v1/v2/v3 support
- option to use namespace separators for virtual buckets / isolate specific key ranges?
- SQLite:
- drop table doesn't autorefresh nav since callback is late
- more accurate type handling
- generated columns, hidden columns
- switch to table_xinfo
- https://sqlite.org/gencol.html - probably want to show
- https://sqlite.org/vtab.html#hiddencol hidden columns - probably want to leave hidden (option to show??)
- views
- other special objects
- triggers? udf functions?
- virtual tables, shadow tables
- toggle showing system tables
- e.g. sqlite_schema a.k.a. sqlite_master not present in default list; sqlite_sequence, is present
- both show in pragma_table_list()
- integrity checks / quick_check
- attach additional db to same connection ("schemas")
- view the contents of an index, using imposter tables - https://sqlite.org/imposter.html
- ~~autoincrement: if column is autoincrement and left blank on insert, do not populate in INSERT statement~~ works with implicitly null columns
- BUG: non-nullable columns are being detected as nullable
- LMDB: dupsort mode (duplicate keys / entries-per-key)
- MongoDB
- UI for replica sets, ssl certs, cluster, custom auth database
- SSH tunnel: error `ssh: tcpChan: deadline not supported` - needs workaround
- Backup/restore
- drop db/collection doesn't autorefresh nav since server is asynchronous
- VoidDB:
- drop multidb doesn't autorefresh nav
- SSH tunnel
- option to use external/system SSH
- Popup prompt for SSHkey password
- Dynamic SSH_AUTH_SOCK env instead of static
- SSH over Cockpit
- Performance
- Warning if data table is filtered to 1000 rows, or add pagination
- Context/interrupt slow queries
- Query history
- Query log
- Test suite
- `CREATE TABLE foo (id integer primary key, aaa text not null, bbb text not null);`
- Ability to convert database types
- Export all data from grid
- Export all data from all buckets within a DB
- Reconnect
- Connection manager: clone entry
- Lotus, Rose, Pebble, ...: Support other advanced options
- UI: Save appearance settings to file
- UI: Apply appearance settings to query result table
- UI: List all available QStyles
- Etcd:
- Support SSH tunnel
- Other actions, compaction, backup/restore, ...

BIN
assets/add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

BIN
assets/arrow_refresh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

BIN
assets/chart_bar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

BIN
assets/compress.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 B

BIN
assets/connect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

BIN
rsrc/database.png → assets/database.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 B

After

Width:  |  Height:  |  Size: 337 B

BIN
assets/database_add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

BIN
assets/database_delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

BIN
assets/database_key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

BIN
assets/database_save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

BIN
assets/delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

BIN
assets/disconnect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

BIN
assets/help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 755 B

BIN
assets/key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 B

BIN
assets/lightning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

BIN
assets/lightning_go.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

BIN
assets/note_delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

BIN
assets/page_key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

BIN
assets/pencil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

BIN
assets/pencil_add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

BIN
assets/pencil_delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 558 B

BIN
assets/pencil_go.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

BIN
assets/resultset_next.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

BIN
assets/table.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

BIN
assets/table_add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

BIN
assets/table_delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 B

BIN
assets/table_save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

BIN
assets/vendor_buntdb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

BIN
assets/vendor_cockroach.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

BIN
assets/vendor_debian.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

BIN
assets/vendor_dgraph.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

BIN
assets/vendor_etcd.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

BIN
assets/vendor_github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

BIN
assets/vendor_leveldb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

BIN
assets/vendor_lmdb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

BIN
assets/vendor_lotus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
assets/vendor_mongodb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

BIN
assets/vendor_mysql.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 545 B

BIN
assets/vendor_pogreb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

BIN
assets/vendor_qt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

BIN
assets/vendor_redis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 B

BIN
assets/vendor_riak.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

BIN
assets/vendor_rosedb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

BIN
assets/vendor_sqlite.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

BIN
assets/vendor_ssh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

BIN
assets/vendor_starskey.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

BIN
assets/vendor_voiddb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

223
bolt.go
View File

@@ -1,223 +0,0 @@
package main
import (
"encoding/json"
"errors"
"os"
"time"
bolt "go.etcd.io/bbolt"
)
func Bolt_Open(readOnly bool, path string) (*bolt.DB, error) {
opts := *bolt.DefaultOptions
opts.Timeout = 10 * time.Second
opts.ReadOnly = readOnly
return bolt.Open(path, os.FileMode(0644), &opts)
}
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(db *bolt.DB, browse []string, fn func(tx *bolt.Tx, bucket *bolt.Bucket) error) error {
if len(browse) == 0 {
// not a bucket
return errors.New("No bucket selected")
}
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(tx, bucket)
})
}
func Bolt_CreateBucket(db *bolt.DB, browse []string, newBucket string) 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
}
})
}
func Bolt_DeleteBucket(db *bolt.DB, browse []string, delBucket string) 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))
}
})
}
func Bolt_SetItem(db *bolt.DB, browse []string, key, val []byte) error {
if len(browse) == 0 {
return errors.New("Can't create top-level items")
}
return db.Update(func(tx *bolt.Tx) error {
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
return bucket.Put(key, val)
})
}
func Bolt_DeleteItem(db *bolt.DB, browse []string, key []byte) error {
if len(browse) == 0 {
return errors.New("Can't create top-level items")
}
return db.Update(func(tx *bolt.Tx) error {
bucket, err := walkBuckets(tx, browse)
if err != nil {
return err
}
return bucket.Delete(key)
})
}
func Bolt_DBStats(db *bolt.DB) (string, error) {
jBytes, err := json.MarshalIndent(db.Stats(), "", " ")
if err != nil {
return "", err
}
return string(jBytes), nil
}
func Bolt_BucketStats(db *bolt.DB, browse []string) (string, error) {
var stats bolt.BucketStats
err := withBrowse_ReadOnly(db, browse, func(tx *bolt.Tx, bucket *bolt.Bucket) error {
stats = bucket.Stats()
return nil
})
if err != nil {
return "", err
}
jBytes, err := json.MarshalIndent(stats, "", " ")
if err != nil {
return "", err
}
return string(jBytes), err
}
func Bolt_ListBuckets(db *bolt.DB, browse []string, cb func(b string) error) error {
if len(browse) == 0 {
// root mode
return db.View(func(tx *bolt.Tx) error {
return tx.ForEach(func(k []byte, _ *bolt.Bucket) error {
return cb(string(k))
})
})
}
// Nested-mode
return withBrowse_ReadOnly(db, browse, func(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 {
return cb(string(k))
}
return nil
})
})
}
type ListItemInfo struct {
Name []byte
DataLen int64
}
func Bolt_ListItems(db *bolt.DB, browse []string, cb func(ListItemInfo) error) error {
if len(browse) == 0 {
return errors.New("No bucket specified")
}
// Nested-mode
return withBrowse_ReadOnly(db, browse, func(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
}
kcopy := make([]byte, len(k))
copy(kcopy, k)
return cb(ListItemInfo{kcopy, int64(len(v))})
})
})
}
func Bolt_GetItem(db *bolt.DB, browse []string, key []byte) ([]byte, error) {
var ret []byte
err := withBrowse_ReadOnly(db, browse, func(tx *bolt.Tx, bucket *bolt.Bucket) error {
d := bucket.Get([]byte(key))
ret = make([]byte, len(d))
copy(ret, d)
return nil
})
return ret, err
}
func Bolt_Close(db *bolt.DB) error {
return db.Close()
}

View File

471
config.go Normal file
View File

@@ -0,0 +1,471 @@
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"reflect"
"time"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
)
type DBConnector interface {
Connect(context.Context) (loadedDatabase, error)
fmt.Stringer
}
type DetailedStringer interface {
DetailedString() string
}
type ConnectionConfig struct {
Type autoconfig.OneOf
Badger *badgerConnection `ylabel:"BadgerDB v4" yicon:":/assets/vendor_dgraph.png" json:",omitempty"`
Bitcask *bitcaskDBConnection `yicon:":/assets/vendor_riak.png" json:",omitempty"`
Bolt *boltConfig `yicon:":/assets/vendor_github.png" json:",omitempty"`
BuntDB *buntDBConnection `ylabel:"BuntDB" yicon:":/assets/vendor_buntdb.png" json:",omitempty"`
Etcd *etcdConn `ylabel:"etcd" yicon:":/assets/vendor_etcd.png" json:",omitempty"`
Debconf *debconfConnection `yicon:":/assets/vendor_debian.png" json:",omitempty"`
SecretService *secretServiceConnection `ylabel:"Freedesktop.org Secret Service" yicon:":/assets/vendor_freedesktop.png" json:",omitempty"`
LevelDB *leveldbConnection `ylabel:"LevelDB" yicon:":/assets/vendor_leveldb.png" json:",omitempty"`
LMDB *lmdbConnection `yicon:":/assets/vendor_lmdb.png" json:",omitempty"`
LotusDB *lotusDBConnection `ylabel:"LotusDB" yicon:":/assets/vendor_lotus.png" json:",omitempty"`
MongoDB *mongoConnection `ylabel:"MongoDB" yicon:":/assets/vendor_mongodb.png" json:",omitempty"`
Pebble *pebbleConnection `yicon:":/assets/vendor_cockroach.png" json:",omitempty"`
Pogreb *pogrebConn `yicon:":/assets/vendor_pogreb.png" json:",omitempty"`
Redis *redisConnectionOptions `yicon:":/assets/vendor_redis.png" json:",omitempty"`
RoseDB *roseDBConn `ylabel:"RoseDB" yicon:":/assets/vendor_rosedb.png" json:",omitempty"`
SQLite *sqliteConnection `ylabel:"SQLite" yicon:":/assets/vendor_sqlite.png" json:",omitempty"`
SSHAgent *sshAgentConn `yicon:":/assets/vendor_ssh.png" json:",omitempty"`
Starskey *starskeyConnection `yicon:":/assets/vendor_starskey.png" json:",omitempty"`
VoidDB *voidDBConn `ylabel:"VoidDB" yicon:":/assets/vendor_voiddb.png" json:",omitempty"`
}
func NewConnectionConfig() *ConnectionConfig {
return &ConnectionConfig{
Type: "Bolt", // favouritism
}
}
// Icon gets the Qt string name for the icon of this database by parsing the
// 'yicon' struct tag with reflection.
// If there is no selection or if it's misconfigured, falls back to a generic
// database icon.
func (cc *ConnectionConfig) Icon() string {
taginfo, ok := reflect.ValueOf(cc).Type().Elem().FieldByName(string(cc.Type))
if !ok {
return ":/assets/database.png"
}
yicon, ok := taginfo.Tag.Lookup("yicon")
if !ok {
return ":/assets/database.png"
}
return yicon
}
// String is used as the connection's name when (A) connecting directly, to show
// in the left-hand nav; and (B) when saving a new connection, the saved name
// It's usually a simple, single word or filename.
func (cc *ConnectionConfig) String() string {
if selection, err := cc.selection(); err == nil {
if stringer, ok := selection.(fmt.Stringer); ok {
if candidate := stringer.String(); candidate != "" && candidate != "." {
return candidate
}
}
}
if string(cc.Type) == "" {
return "Not configured"
}
return string(cc.Type)
}
// Tooltip is used as the hover tooltip in the connection manager.
// It can show more detail (e.g. full path spec).
// If there is no useful more detail, returns empty-string.
func (cc *ConnectionConfig) Tooltip() string {
// Try DetailedString() if there is such a method
if selection, err := cc.selection(); err == nil {
if getDetails, ok := selection.(DetailedStringer); ok {
if candidate := getDetails.DetailedString(); candidate != "" && candidate != "." {
return candidate
}
}
}
// Otherwise - just same as .String()
return cc.String()
}
func (cc *ConnectionConfig) selection() (DBConnector, error) {
selection := reflect.ValueOf(cc).Elem().FieldByName(string(cc.Type))
if !selection.IsValid() {
return nil, fmt.Errorf("Invalid database engine %q", cc.Type)
}
con, ok := selection.Interface().(DBConnector)
if !ok {
return nil, fmt.Errorf("Can't connect to database on type %q (weird)", cc.Type)
}
return con, nil
}
func (cc *ConnectionConfig) Connect(ctx context.Context) (loadedDatabase, error) {
dbc, err := cc.selection()
if err != nil {
return nil, fmt.Errorf("Invalid database engine %q", cc.Type)
}
return dbc.Connect(ctx)
}
var _ DBConnector = &ConnectionConfig{}
func (f *App) OnMnuConnectClick() {
config := NewConnectionConfig()
f.showConnectDialog(config)
}
func (f *App) showConnectDialog(config *ConnectionConfig) {
// autoconfig.OpenDialog() does not give us a "yes/no" result back from the
// dialog, so we still have to custom construct the dialog and use the
// formlayout option instead
dlg := NewConnectDialogUi()
dlg.ConnectDialog.SetParent2(f.ui.MainWindow.QWidget, qt.Dialog)
dlg.ConnectDialog.SetModal(true)
dlg.ConnectDialog.SetAttribute(qt.WA_DeleteOnClose)
dlg.formLayout.SetSizeConstraint(qt.QLayout__SetMinAndMaxSize) // Expand dialog to fit form content
saver := autoconfig.MakeConfigArea(config, dlg.formLayout)
dlg.ConnectDialog.OnAccept(func(super func()) {
// Validate connection before closing
dlg.buttonBox.SetEnabled(false)
defer dlg.buttonBox.SetEnabled(true)
dlg.buttonBox.Repaint()
// Save changes from UI into struct
saver()
// Connect -> get ld
ctx := context.Background() // TODO do in background thread?
ld, err := config.Connect(ctx)
if err != nil {
_ = qt.QMessageBox_Critical(dlg.ConnectDialog.QWidget, APPNAME, fmt.Sprintf("Connecting to %s database: %s", config.Type, err.Error()))
// Prevent the dialog from closing: do not call super()
return
}
displayName := config.String()
// Add ld to mainwindow
f.addTopLevelDatabaseConnection(ld, displayName, config.Tooltip())
// Connection OK
// Offer to save into connection-manager
if res := qt.QMessageBox_Question2(dlg.ConnectDialog.QWidget, APPNAME, "Connection successful. Save the details into Connection Manager?", qt.QMessageBox__Save, qt.QMessageBox__Ignore); res == int(qt.QMessageBox__Save) {
f.TrySaveIntoConnectionManager(config, displayName)
}
// Default accept behaviour is: setResult(Accepted), emits onFinished; && Hide()
super()
})
dlg.ConnectDialog.Open() // Modal, unlike .Show()
}
type ConnMgrLoadError struct {
e error
}
func (c ConnMgrLoadError) Error() string {
return fmt.Sprintf("Failed to load saved connections: %s", c.e)
}
func (c ConnMgrLoadError) Unwrap() error {
return c.e
}
type ConnMgrSaveError struct {
e error
}
func (c ConnMgrSaveError) Error() string {
return fmt.Sprintf("Failed to save connections: %s", c.e)
}
func (c ConnMgrSaveError) Unwrap() error {
return c.e
}
func (f *App) TrySaveIntoConnectionManager(cc *ConnectionConfig, displayName string) {
data, err := f.getConnectionManagerContents()
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrLoadError{err}.Error())
}
if displayName == "" {
displayName = cc.String()
}
displayName = data.getNonConflictingName(displayName)
data.Entries = append(data.Entries, SavedConfigEntry{
Description: displayName,
Connection: *cc,
})
err = f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
}
}
func (f *App) OnMnuConnectionManagerClick() {
data, err := f.getConnectionManagerContents()
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrLoadError{err}.Error())
return
}
dlg := NewconnectionManagerDialogUi()
dlg.connectionManagerDialog.SetParent2(f.ui.MainWindow.QWidget, qt.Dialog)
dlg.connectionManagerDialog.SetModal(true)
dlg.connectionManagerDialog.SetAttribute(qt.WA_DeleteOnClose)
dlg.treeWidget.SetRootIsDecorated(false)
dlg.treeWidget.SetSelectionMode(qt.QAbstractItemView__ExtendedSelection)
addEntryFor := func(entry SavedConfigEntry) {
itm := qt.NewQTreeWidgetItem()
itm.SetText(0, entry.Description)
itm.SetIcon(0, qt.NewQIcon4(entry.Connection.Icon()))
if tooltip := entry.Connection.Tooltip(); tooltip != "" && tooltip != entry.Description {
itm.SetToolTip(0, tooltip)
}
dlg.treeWidget.AddTopLevelItem(itm)
}
refreshEntryFor := func(idx *qt.QModelIndex, entry SavedConfigEntry) {
itm := dlg.treeWidget.ItemFromIndex(idx)
itm.SetText(0, entry.Description) // Update label
itm.SetIcon(0, qt.NewQIcon4(entry.Connection.Icon())) // Update icon
}
// Populate entries
for _, entry := range data.Entries {
addEntryFor(entry)
}
refreshButtonState := func() {
ct := len(dlg.treeWidget.SelectedItems())
dlg.connectBtn.SetEnabled(ct > 0)
dlg.connDelete.SetEnabled(ct > 0)
dlg.connEdit.SetEnabled(ct == 1)
}
connectToItem := func(itm *qt.QTreeWidgetItem) bool {
ctx := context.Background() // TODO do in background thread?
dlg.connectBtn.SetEnabled(false)
dlg.treeWidget.SetEnabled(false)
defer func() {
dlg.connectBtn.SetEnabled(true)
dlg.treeWidget.SetEnabled(true) // FIXME block the other buttons too!
}()
entry := data.Entries[dlg.treeWidget.IndexFromItem(itm).Row()]
ld, err := entry.Connection.Connect(ctx)
if err != nil {
_ = qt.QMessageBox_Critical(dlg.connectionManagerDialog.QWidget, APPNAME, fmt.Sprintf("Connecting to %s database: %s", entry.Connection.Type, err.Error()))
return false
}
// Add ld to mainwindow
// Don't use the displayName from the Connect() function - use the saved
// displayname instead
f.addTopLevelDatabaseConnection(ld, entry.Description, entry.Connection.Tooltip())
return true
}
dlg.treeWidget.OnItemDoubleClicked(func(itm *qt.QTreeWidgetItem, _ int) {
// connect to specific item
if connectToItem(itm) {
dlg.connectionManagerDialog.Accept()
}
})
dlg.connectBtn.OnClicked(func() {
// Based on selectedItems, not currentItem
itms := dlg.treeWidget.SelectedItems()
var ok bool = true
for _, itm := range itms {
itmConnectOK := connectToItem(itm) // connect to selected
ok = ok && itmConnectOK
}
if ok {
dlg.connectionManagerDialog.Accept()
}
})
dlg.connDelete.SetEnabled(false)
dlg.connEdit.SetEnabled(false)
dlg.treeWidget.OnItemSelectionChanged(refreshButtonState)
dlg.connDelete.OnClicked(func() {
itms := dlg.treeWidget.SelectedItems()
for _, itm := range itms {
idx := dlg.treeWidget.IndexFromItem(itm).Row() // n.b. will dynamically change as the item is removed
itm.Delete()
data.Entries = slice_remove_index(data.Entries, idx)
}
// All changes made in both ui + data model
// Save data model changes
err := f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
})
dlg.connEdit.OnClicked(func() {
// Edit is based on selectedIndex, not currentIndex
items := dlg.treeWidget.SelectedItems()
if len(items) != 1 {
return
}
idx := dlg.treeWidget.IndexFromItem(items[0])
row := idx.Row()
props := data.Entries[row]
autoconfig.OpenDialog(&props, dlg.connectionManagerDialog.QWidget, "Editing connection", func() {
data.Entries[row] = props
refreshEntryFor(idx, props)
err := f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
})
})
dlg.connAdd.OnClicked(func() {
obj := SavedConfigEntry{
Description: "New connection (" + time.Now().Format(time.DateTime) + ")",
}
vv := NewConnectionConfig()
obj.Connection = *vv
autoconfig.OpenDialog(&obj, dlg.connectionManagerDialog.QWidget, "New connection", func() {
obj.Description = data.getNonConflictingName(obj.Description)
data.Entries = append(data.Entries, obj)
addEntryFor(obj)
err := f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
})
})
miniMenu := qt.NewQMenu(dlg.toolsBtn.QWidget)
exportAction := miniMenu.AddActionWithText("Export connections...")
importAction := miniMenu.AddActionWithText("Import connections...")
dlg.toolsBtn.SetMenu(miniMenu)
dlg.toolsBtn.SetPopupMode(qt.QToolButton__InstantPopup)
exportAction.OnTriggered(func() {
saveAs := qt.QFileDialog_GetSaveFileName4(dlg.toolsBtn.QWidget, "Export connections...", "", "JSON files (*.json);;All files (*)")
if saveAs == "" {
return // cancelled
}
jbytes, err := json.MarshalIndent(data, "", " ")
if err != nil {
qt.QMessageBox_Warning(dlg.toolsBtn.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
err = os.WriteFile(saveAs, jbytes, 0600)
if err != nil {
qt.QMessageBox_Warning(dlg.toolsBtn.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
// OK
qt.QMessageBox_Information(dlg.toolsBtn.QWidget, APPNAME, "The connections have been exported successfully.")
})
importAction.OnTriggered(func() {
loadFile := qt.QFileDialog_GetOpenFileName4(dlg.toolsBtn.QWidget, "Import connections...", "", "JSON files (*.json);;All files (*)")
if loadFile == "" {
return // cancelled
}
jbytes, err := os.ReadFile(loadFile)
if err != nil {
qt.QMessageBox_Warning(dlg.toolsBtn.QWidget, APPNAME, ConnMgrLoadError{err}.Error())
return
}
var importData SavedConfig
err = json.Unmarshal(jbytes, &importData)
if err != nil {
qt.QMessageBox_Warning(dlg.toolsBtn.QWidget, APPNAME, ConnMgrLoadError{err}.Error())
return
}
// Add to UI
for _, newEntry := range importData.Entries {
addEntryFor(newEntry)
}
// Add to data model
data.Entries = append(data.Entries, importData.Entries...)
// Save data model to disk
err = f.saveConnectionManagerContents(data)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, ConnMgrSaveError{err}.Error())
return
}
})
refreshButtonState()
dlg.connectionManagerDialog.Show()
}

232
configSave.go Normal file
View File

@@ -0,0 +1,232 @@
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/zalando/go-keyring"
)
type SavedConfigEntry struct {
Description string
Connection ConnectionConfig
}
type SavedConfig struct {
UserAgent string // APPNAME/{ver}
Entries []SavedConfigEntry
}
// getNonConflictingName returns a version of the input name that does not
// conflict with any existing saved entry.
// It adds " (2)", " (3)", etc. prefixes.
func (s *SavedConfig) getNonConflictingName(nameSuggest string) string {
existingNames := make(map[string]struct{}, len(s.Entries))
for _, e := range s.Entries {
existingNames[e.Description] = struct{}{}
}
i := 0
for {
i++ // starts at 1
testName := nameSuggest
if i > 1 {
testName += ` (` + strconv.Itoa(i) + `)`
}
if _, ok := existingNames[testName]; !ok {
return testName
}
}
}
const (
saveSettingsFilename = `settings.dat`
keychainUserName = `settings-encryption-key`
)
func (f *App) getConnectionManagerContentsJson() ([]byte, error) {
cfgd, err := os.UserConfigDir()
if err != nil {
return nil, err
}
_ = os.MkdirAll(filepath.Join(cfgd, APPNAME), 0700)
ciphertext, err := os.ReadFile(filepath.Join(cfgd, APPNAME, saveSettingsFilename))
if err != nil {
return nil, err
}
// Get decryption key from OS keychain
// Since we successfully loaded the file on disk, the keychain entry must exist
details, err := keyring.Get(APPNAME, keychainUserName)
if err != nil {
return nil, err
}
// Decrypt
// The encryption key is random 256 bytes, no need for a KDF
encryptionKeyBytes, err := hex.DecodeString(details)
if err != nil {
return nil, err
}
cc, err := aes.NewCipher(encryptionKeyBytes)
if err != nil {
return nil, err
}
cw, err := cipher.NewGCMWithRandomNonce(cc)
if err != nil {
return nil, err
}
return cw.Open(ciphertext[:0], nil, ciphertext, nil) // @ref https://pkg.go.dev/crypto/cipher#AEAD
}
func (f *App) getConnectionManagerContents() (*SavedConfig, error) {
plaintext, err := f.getConnectionManagerContentsJson()
if err != nil {
if os.IsNotExist(err) {
// No file exists. Use blank
return &SavedConfig{
UserAgent: APPNAME,
}, nil
}
return nil, err
}
var ret SavedConfig
err = json.Unmarshal(plaintext, &ret)
if err != nil {
return nil, err
}
// Before returning the configuration, sort the entries alphabetically
sort.Slice(ret.Entries, func(i, j int) bool {
// FIXME there is probably a slightly more efficient way of doing this
return strings.ToLower(ret.Entries[i].Description) < strings.ToLower(ret.Entries[j].Description)
})
// Success
return &ret, nil
}
func (f *App) saveConnectionManagerContents(sc *SavedConfig) error {
// Force update the saved version
sc.UserAgent = APPNAME + `/` + appVersion // e.g. QBolt/v0.0.0-devel
// Marshal
plaintext, err := json.Marshal(sc)
if err != nil {
return err
}
return f.saveConnectionManagerContentsJson(plaintext)
}
func (f *App) saveConnectionManagerContentsJson(plaintext []byte) error {
cfgd, err := os.UserConfigDir()
if err != nil {
return err
}
// If the file exists already, we must have an existing encryption key
var exists bool
savePath := filepath.Join(cfgd, APPNAME, saveSettingsFilename)
if _, err := os.Stat(savePath); err != nil {
if os.IsNotExist(err) {
_ = os.MkdirAll(filepath.Join(cfgd, APPNAME), 0700)
exists = false
} else {
return err // some other real error
}
} else {
exists = true
}
details, err := keyring.Get(APPNAME, keychainUserName)
if err != nil {
if errors.Is(err, keyring.ErrNotFound) {
// Does not exist in keyring
// That is OK if there is no saved file yet / we can generate a random
// key. But if the file does already exist, this is fatal
if exists {
// File exists on disk but there is no encryption key to parse it
// Fatal
return fmt.Errorf(
"There is already a saved file on disk (%q), but there is no matching encryption key in the system keychain provider. Has your keychain been destroyed? To confirm, please delete the saved settings file.",
savePath,
)
} else {
// Generate new
// 256 bits (32 bytes) of cryto-random data, and then hex encode
randBuff := make([]byte, 32)
n, err := rand.Read(randBuff)
if err != nil {
return err
}
if n != len(randBuff) {
return io.ErrShortWrite
}
details = hex.EncodeToString(randBuff)
err = keyring.Set(APPNAME, keychainUserName, details)
if err != nil {
return err
}
// Safe
}
} else {
// Real keychain error
return err
}
}
// Encrypt
encryptionKeyBytes, err := hex.DecodeString(details)
if err != nil {
return err
}
cc, err := aes.NewCipher(encryptionKeyBytes)
if err != nil {
return err
}
cw, err := cipher.NewGCMWithRandomNonce(cc)
if err != nil {
return err
}
ciphertext := cw.Seal(plaintext[:0], nil, plaintext, nil)
// Save to disk
err = os.WriteFile(savePath, ciphertext, 0600)
if err != nil {
return err
}
// Save OK
return nil
}

69
connectionDialog.go Normal file
View File

@@ -0,0 +1,69 @@
// Generated by miqt-uic. To update this file, edit the .ui file in
// Qt Designer, and then run 'go generate'.
//
//go:generate miqt-uic -InFile connectionDialog.ui -OutFile connectionDialog.go -Qt6
package main
import (
qt "github.com/mappu/miqt/qt6"
)
type ConnectDialogUi struct {
ConnectDialog *qt.QDialog
gridLayout *qt.QGridLayout
buttonBox *qt.QDialogButtonBox
formWidget *qt.QWidget
formLayout *qt.QFormLayout
}
// NewConnectDialogUi creates all Qt widget classes for ConnectDialog.
func NewConnectDialogUi() *ConnectDialogUi {
ui := &ConnectDialogUi{}
ui.ConnectDialog = qt.NewQDialog(nil)
ConnectDialog__objectName := qt.NewQAnyStringView3("ConnectDialog")
ui.ConnectDialog.SetObjectName(*ConnectDialog__objectName)
ConnectDialog__objectName.Delete() // setter copied value
ui.ConnectDialog.Resize(385, 364)
icon0 := qt.NewQIcon()
icon0.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.ConnectDialog.SetWindowIcon(icon0)
ui.gridLayout = qt.NewQGridLayout(ui.ConnectDialog.QWidget)
gridLayout__objectName := qt.NewQAnyStringView3("gridLayout")
ui.gridLayout.SetObjectName(*gridLayout__objectName)
gridLayout__objectName.Delete() // setter copied value
ui.gridLayout.SetContentsMargins(11, 11, 11, 11)
ui.gridLayout.SetSpacing(6)
ui.buttonBox = qt.NewQDialogButtonBox(ui.ConnectDialog.QWidget)
buttonBox__objectName := qt.NewQAnyStringView3("buttonBox")
ui.buttonBox.SetObjectName(*buttonBox__objectName)
buttonBox__objectName.Delete() // setter copied value
ui.buttonBox.SetOrientation(qt.Horizontal)
ui.buttonBox.SetStandardButtons(qt.QDialogButtonBox__Cancel | qt.QDialogButtonBox__Ok)
ui.gridLayout.AddWidget2(ui.buttonBox.QWidget, 2, 0)
ui.formWidget = qt.NewQWidget(ui.ConnectDialog.QWidget)
formWidget__objectName := qt.NewQAnyStringView3("formWidget")
ui.formWidget.SetObjectName(*formWidget__objectName)
formWidget__objectName.Delete() // setter copied value
ui.formLayout = qt.NewQFormLayout(ui.formWidget)
formLayout__objectName := qt.NewQAnyStringView3("formLayout")
ui.formLayout.SetObjectName(*formLayout__objectName)
formLayout__objectName.Delete() // setter copied value
ui.formLayout.SetContentsMargins(0, 0, 0, 11)
ui.formLayout.SetSpacing(6)
ui.gridLayout.AddWidget2(ui.formWidget, 1, 0)
ui.buttonBox.OnAccepted(ui.ConnectDialog.Accept)
ui.buttonBox.OnRejected(ui.ConnectDialog.Reject)
ui.Retranslate()
return ui
}
// Retranslate reapplies all text translations.
func (ui *ConnectDialogUi) Retranslate() {
ui.ConnectDialog.SetWindowTitle(qt.QCoreApplication_Tr("Connect..."))
}

85
connectionDialog.ui Normal file
View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConnectDialog</class>
<widget class="QDialog" name="ConnectDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>385</width>
<height>364</height>
</rect>
</property>
<property name="windowTitle">
<string>Connect...</string>
</property>
<property name="windowIcon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QWidget" name="formWidget" native="true">
<layout class="QFormLayout" name="formLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="embed.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ConnectDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ConnectDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

136
connectionManagerDialog.go Normal file
View File

@@ -0,0 +1,136 @@
// Generated by miqt-uic. To update this file, edit the .ui file in
// Qt Designer, and then run 'go generate'.
//
//go:generate miqt-uic -InFile connectionManagerDialog.ui -OutFile connectionManagerDialog.go -Qt6
package main
import (
qt "github.com/mappu/miqt/qt6"
)
type connectionManagerDialogUi struct {
connectionManagerDialog *qt.QDialog
verticalLayout_2 *qt.QVBoxLayout
horizontalLayout *qt.QHBoxLayout
treeWidget *qt.QTreeWidget
verticalLayout *qt.QVBoxLayout
connAdd *qt.QToolButton
connEdit *qt.QToolButton
connDelete *qt.QToolButton
verticalSpacer *qt.QSpacerItem
horizontalLayout_2 *qt.QHBoxLayout
toolsBtn *qt.QToolButton
horizontalSpacer *qt.QSpacerItem
connectBtn *qt.QPushButton
}
// NewconnectionManagerDialogUi creates all Qt widget classes for connectionManagerDialog.
func NewconnectionManagerDialogUi() *connectionManagerDialogUi {
ui := &connectionManagerDialogUi{}
ui.connectionManagerDialog = qt.NewQDialog(nil)
connectionManagerDialog__objectName := qt.NewQAnyStringView3("connectionManagerDialog")
ui.connectionManagerDialog.SetObjectName(*connectionManagerDialog__objectName)
connectionManagerDialog__objectName.Delete() // setter copied value
ui.connectionManagerDialog.Resize(332, 449)
ui.verticalLayout_2 = qt.NewQVBoxLayout(ui.connectionManagerDialog.QWidget)
verticalLayout_2__objectName := qt.NewQAnyStringView3("verticalLayout_2")
ui.verticalLayout_2.SetObjectName(*verticalLayout_2__objectName)
verticalLayout_2__objectName.Delete() // setter copied value
ui.verticalLayout_2.SetContentsMargins(11, 11, 11, 11)
ui.verticalLayout_2.SetSpacing(6)
ui.horizontalLayout = qt.NewQHBoxLayout2()
horizontalLayout__objectName := qt.NewQAnyStringView3("horizontalLayout")
ui.horizontalLayout.SetObjectName(*horizontalLayout__objectName)
horizontalLayout__objectName.Delete() // setter copied value
ui.horizontalLayout.SetContentsMargins(0, 0, 0, 0)
ui.horizontalLayout.SetSpacing(6)
ui.treeWidget = qt.NewQTreeWidget(ui.connectionManagerDialog.QWidget)
treeWidget__objectName := qt.NewQAnyStringView3("treeWidget")
ui.treeWidget.SetObjectName(*treeWidget__objectName)
treeWidget__objectName.Delete() // setter copied value
ui.horizontalLayout.AddWidget(ui.treeWidget.QWidget)
ui.verticalLayout = qt.NewQVBoxLayout2()
verticalLayout__objectName := qt.NewQAnyStringView3("verticalLayout")
ui.verticalLayout.SetObjectName(*verticalLayout__objectName)
verticalLayout__objectName.Delete() // setter copied value
ui.verticalLayout.SetContentsMargins(0, 0, 0, 0)
ui.verticalLayout.SetSpacing(6)
ui.connAdd = qt.NewQToolButton(ui.connectionManagerDialog.QWidget)
connAdd__objectName := qt.NewQAnyStringView3("connAdd")
ui.connAdd.SetObjectName(*connAdd__objectName)
connAdd__objectName.Delete() // setter copied value
icon0 := qt.NewQIcon()
icon0.AddFile4(":/assets/add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.connAdd.SetIcon(icon0)
ui.connAdd.SetAutoRaise(true)
ui.verticalLayout.AddWidget(ui.connAdd.QWidget)
ui.connEdit = qt.NewQToolButton(ui.connectionManagerDialog.QWidget)
connEdit__objectName := qt.NewQAnyStringView3("connEdit")
ui.connEdit.SetObjectName(*connEdit__objectName)
connEdit__objectName.Delete() // setter copied value
icon1 := qt.NewQIcon()
icon1.AddFile4(":/assets/pencil.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.connEdit.SetIcon(icon1)
ui.connEdit.SetAutoRaise(true)
ui.verticalLayout.AddWidget(ui.connEdit.QWidget)
ui.connDelete = qt.NewQToolButton(ui.connectionManagerDialog.QWidget)
connDelete__objectName := qt.NewQAnyStringView3("connDelete")
ui.connDelete.SetObjectName(*connDelete__objectName)
connDelete__objectName.Delete() // setter copied value
icon2 := qt.NewQIcon()
icon2.AddFile4(":/assets/delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.connDelete.SetIcon(icon2)
ui.connDelete.SetAutoRaise(true)
ui.verticalLayout.AddWidget(ui.connDelete.QWidget)
ui.verticalSpacer = qt.NewQSpacerItem4(20, 40, qt.QSizePolicy__Minimum, qt.QSizePolicy__Expanding)
ui.verticalLayout.AddItem(ui.verticalSpacer.QLayoutItem)
ui.horizontalLayout.AddLayout(ui.verticalLayout.QLayout)
ui.verticalLayout_2.AddLayout(ui.horizontalLayout.QLayout)
ui.horizontalLayout_2 = qt.NewQHBoxLayout2()
horizontalLayout_2__objectName := qt.NewQAnyStringView3("horizontalLayout_2")
ui.horizontalLayout_2.SetObjectName(*horizontalLayout_2__objectName)
horizontalLayout_2__objectName.Delete() // setter copied value
ui.horizontalLayout_2.SetContentsMargins(0, 0, 0, 0)
ui.horizontalLayout_2.SetSpacing(6)
ui.toolsBtn = qt.NewQToolButton(ui.connectionManagerDialog.QWidget)
toolsBtn__objectName := qt.NewQAnyStringView3("toolsBtn")
ui.toolsBtn.SetObjectName(*toolsBtn__objectName)
toolsBtn__objectName.Delete() // setter copied value
ui.toolsBtn.SetAutoRaise(true)
ui.horizontalLayout_2.AddWidget(ui.toolsBtn.QWidget)
ui.horizontalSpacer = qt.NewQSpacerItem4(40, 20, qt.QSizePolicy__Expanding, qt.QSizePolicy__Minimum)
ui.horizontalLayout_2.AddItem(ui.horizontalSpacer.QLayoutItem)
ui.connectBtn = qt.NewQPushButton(ui.connectionManagerDialog.QWidget)
connectBtn__objectName := qt.NewQAnyStringView3("connectBtn")
ui.connectBtn.SetObjectName(*connectBtn__objectName)
connectBtn__objectName.Delete() // setter copied value
icon3 := qt.NewQIcon()
icon3.AddFile4(":/assets/database_lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
ui.connectBtn.SetIcon(icon3)
ui.horizontalLayout_2.AddWidget(ui.connectBtn.QWidget)
ui.verticalLayout_2.AddLayout(ui.horizontalLayout_2.QLayout)
ui.Retranslate()
return ui
}
// Retranslate reapplies all text translations.
func (ui *connectionManagerDialogUi) Retranslate() {
ui.connectionManagerDialog.SetWindowTitle(qt.QCoreApplication_Tr("Connection Manager"))
ui.treeWidget.HeaderItem().SetText(0, qt.QTreeWidget_Tr("Connection"))
ui.toolsBtn.SetText(qt.QDialog_Tr("Tools"))
ui.connectBtn.SetText(qt.QDialog_Tr("Connect"))
}

124
connectionManagerDialog.ui Normal file
View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>connectionManagerDialog</class>
<widget class="QDialog" name="connectionManagerDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>332</width>
<height>449</height>
</rect>
</property>
<property name="windowTitle">
<string>Connection Manager</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTreeWidget" name="treeWidget">
<column>
<property name="text">
<string>Connection</string>
</property>
</column>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QToolButton" name="connAdd">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/add.png</normaloff>:/assets/add.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="connEdit">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/pencil.png</normaloff>:/assets/pencil.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="connDelete">
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/delete.png</normaloff>:/assets/delete.png</iconset>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QToolButton" name="toolsBtn">
<property name="text">
<string>Tools</string>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="connectBtn">
<property name="text">
<string>Connect</string>
</property>
<property name="icon">
<iconset resource="embed.qrc">
<normaloff>:/assets/database_lightning.png</normaloff>:/assets/database_lightning.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="embed.qrc"/>
</resources>
<connections/>
</ui>

253
db_badger.go Normal file
View File

@@ -0,0 +1,253 @@
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/dgraph-io/badger/v4"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
)
type badgerLoadedDatabase struct {
db *badger.DB
}
func (ld *badgerLoadedDatabase) DriverName() string {
return "BadgerDB v4"
}
func (ld *badgerLoadedDatabase) Properties(bucketPath []string) (string, error) {
content := fmt.Sprintf("Table statistics: %#v", ld.db.Tables())
return content, nil
}
func (ld *badgerLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
// Badger always uses Key + Value as the columns
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
err := ld.db.View(func(txn *badger.Txn) error {
// Create iterator
opts := badger.DefaultIteratorOptions
opts.PrefetchSize = 64
it := txn.NewIterator(opts)
defer it.Close()
for it.Rewind(); it.Valid(); it.Next() {
item := it.Item()
k := item.Key()
err := item.Value(func(v []byte) error {
f.AddRow_PK_Data(k, k, v)
return nil
})
if err != nil {
return err
}
}
return nil
})
if err != nil {
return err
}
// Valid
f.Ready()
return nil
}
func (n *badgerLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
return n.db.Update(func(txn *badger.Txn) error {
return ApplyChanges_binColumn(f, txn.Set, txn.Delete)
})
}
func (ld *badgerLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *badgerLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
return []contextAction{
{Name: "Export backup...", Callback: ld.ExportBackup},
{Name: "Import backup...", Callback: ld.ImportBackup},
{Name: "Compact", Callback: ld.CompactGC},
}, nil
}
func (ld *badgerLoadedDatabase) ExportBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error {
saveAs := qt.QFileDialog_GetSaveFileName4(sender.TreeWidget().QWidget, "Export backup...", "", "Badger database backups (*.bak);;All files (*)")
if saveAs == "" {
return nil
}
fh, err := os.OpenFile(saveAs, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0600)
if err != nil {
return err
}
defer fh.Close()
_, err = ld.db.Backup(fh, 0)
return err
}
func (ld *badgerLoadedDatabase) ImportBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error {
openPath := qt.QFileDialog_GetOpenFileName4(sender.TreeWidget().QWidget, "Import backup...", "", "Badger database backups (*.bak);;All files (*)")
if openPath == "" {
return nil
}
fh, err := os.Open(openPath)
if err != nil {
return err
}
defer fh.Close()
return ld.db.Load(fh, 16) // concurrency - just a guess
}
func (ld *badgerLoadedDatabase) CompactGC(sender *qt.QTreeWidgetItem, bucketPath []string) error {
// Move data from value log into levels ->
err := ld.db.RunValueLogGC(0.0001) // Compact if we would save 0.1% of disk space
if err != nil {
return fmt.Errorf("RunValueLogGC: %w", err)
}
// -> then flatten all levels
err = ld.db.Flatten(4)
if err != nil {
return fmt.Errorf("Flatten: %w", err)
}
return nil
}
func (ld *badgerLoadedDatabase) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &badgerLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &badgerLoadedDatabase{} // interface assertion
//
type BadgerResettableOptions badger.Options
func (bao *BadgerResettableOptions) Reset() {
if bao == nil {
return
}
opts := badger.DefaultOptions("")
opts.Logger = nil // Have to wipe this out otherwise we can't JSON marshal our struct
*bao = BadgerResettableOptions(opts)
}
type badgerConnection struct {
Type autoconfig.OneOf
Disk *struct {
Directory autoconfig.ExistingDirectory
Readonly bool
Encryption *encryptionKey
} `json:",omitempty"`
Memory *struct{} `json:",omitempty"`
Advanced *struct {
*BadgerResettableOptions `ylabel:"Options"`
} `json:",omitempty"`
}
type encryptionKey struct {
Method autoconfig.EnumList `yenum:"Text;;Hex;;Passphrase (SHA256 KDF to AES-256)"`
Key autoconfig.Password
}
func (e encryptionKey) Get() ([]byte, error) {
switch e.Method {
case 0: // Text
return []byte(e.Key), nil
case 1: // Hex
// For Badger, the input must be 16/24/32 bytes for AES-128/192/256
// The library checks this, we don't need to
return hex.DecodeString(string(e.Key))
case 2: // Passphrase (SHA256 KDF to AES-256)
hasher := sha256.New()
hasher.Write([]byte(e.Key))
return hasher.Sum(nil), nil
default:
return nil, fmt.Errorf("Unsupported encoding method for encryption key")
}
}
func (bdc *badgerConnection) String() string {
return filepath.Base(bdc.DetailedString())
}
func (bdc *badgerConnection) DetailedString() string {
if bdc.Type == "Disk" {
return string(bdc.Disk.Directory)
} else if bdc.Type == "Memory" {
return `:memory:` // SQLite-style naming
} else if bdc.Type == "Advanced" {
return bdc.Advanced.BadgerResettableOptions.Dir
}
return "" // unreachable
}
var _ DBConnector = &badgerConnection{} // interface assertion
func (bdc *badgerConnection) Connect(ctx context.Context) (loadedDatabase, error) {
var opts badger.Options
// Basic options
if bdc.Type == "Disk" {
opts = badger.DefaultOptions(string(bdc.Disk.Directory))
opts.ReadOnly = bdc.Disk.Readonly
opts.MetricsEnabled = false
if bdc.Disk.Encryption != nil {
ehx, err := bdc.Disk.Encryption.Get()
if err != nil {
return nil, fmt.Errorf("Loading encryption key: %w", err)
}
if !(len(ehx) == 16 || len(ehx) == 24 || len(ehx) == 32) {
return nil, fmt.Errorf("Encryption key must be 16/24/32 bytes long, got %d", len(ehx))
}
opts.EncryptionKey = ehx
}
} else if bdc.Type == "Memory" {
opts = badger.DefaultOptions("").WithInMemory(true)
} else if bdc.Type == "Advanced" {
if bdc.Advanced.BadgerResettableOptions == nil {
return nil, errors.New("Options not provided")
}
opts = badger.Options(*bdc.Advanced.BadgerResettableOptions)
// Reinstate default logger that we wiped out
tmpOpts := badger.DefaultOptions("")
opts.Logger = tmpOpts.Logger
}
db, err := badger.Open(opts)
if err != nil {
return nil, err
}
return &badgerLoadedDatabase{db: db}, nil
}

119
db_bitcask.go Normal file
View File

@@ -0,0 +1,119 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"go.mills.io/bitcask/v2"
)
type bitcaskLdb struct {
db *bitcask.Bitcask
}
func (ld *bitcaskLdb) DriverName() string {
return "Bitcask"
}
func (ld *bitcaskLdb) Properties(bucketPath []string) (string, error) {
stats, err := ld.db.Stats()
if err != nil {
return "", err
}
return fmt.Sprintf("Database: %s\n\nStats:\n%#v\n", ld.db.Path(), stats), nil
}
func (ld *bitcaskLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
cur := ld.db.Iterator()
defer cur.Close()
for {
itm, err := cur.Next()
if err != nil {
if err == bitcask.ErrStopIteration {
break // OK
}
return err
}
f.AddRow_PK_Data([]byte(itm.Key()), []byte(itm.Key()), []byte(itm.Value()))
}
// Valid
f.Ready()
return nil
}
func (n *bitcaskLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return ApplyChanges_binColumn(
f,
func(k, v []byte) error { return n.db.Put(bitcask.Key(k), bitcask.Value(v)) },
func(k []byte) error { return n.db.Delete(bitcask.Key(k)) },
)
}
func (ld *bitcaskLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *bitcaskLdb) actionBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error {
backupDir := qt.QFileDialog_GetExistingDirectory3(sender.TreeWidget().QWidget, APPNAME, "Select an output directory to backup to...")
if backupDir == "" {
return nil // cancelled
}
return ld.db.Backup(backupDir)
}
func (ld *bitcaskLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return []contextAction{
{"Backup...", ld.actionBackup},
}, nil
}
func (ld *bitcaskLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &bitcaskLdb{} // interface assertion
var _ editableLoadedDatabase = &bitcaskLdb{} // interface assertion
//
type bitcaskDBConnection struct {
Directory autoconfig.ExistingDirectory
Readonly bool
AutoRecovery bool
}
func (c *bitcaskDBConnection) Reset() {
c.AutoRecovery = true
}
func (c *bitcaskDBConnection) Connect(ctx context.Context) (loadedDatabase, error) {
db, err := bitcask.Open(
string(c.Directory),
bitcask.WithOpenReadonly(c.Readonly),
bitcask.WithAutoRecovery(c.AutoRecovery),
)
if err != nil {
return nil, err
}
return &bitcaskLdb{db: db}, nil
}
func (c *bitcaskDBConnection) String() string {
return filepath.Base(string(c.Directory))
}
var _ DBConnector = &bitcaskDBConnection{} // interface assertion

363
db_bolt.go Normal file
View File

@@ -0,0 +1,363 @@
package main
import (
"bytes"
"context"
"errors"
"fmt"
"path/filepath"
"sort"
"strings"
"time"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"go.etcd.io/bbolt"
"go.etcd.io/bbolt/version"
)
type boltLoadedDatabase struct {
path string
db *bbolt.DB
}
type boltAdvancedOptions struct {
bbolt.Options
}
func (bao *boltAdvancedOptions) Reset() {
bao.Options = *bbolt.DefaultOptions
// Interfaces not JSON marshallable
bao.OpenFile = nil
bao.Logger = nil
}
func (bao *boltAdvancedOptions) String() string {
return "Configured" // Override bbolt.Options.String() for autoconfig
}
type boltConfig struct {
Path autoconfig.ExistingFile `yfilter:"Bolt database (*.db);;All files (*)"`
Readonly bool
AdvancedOptions *boltAdvancedOptions `json:",omitempty"`
}
var _ DBConnector = &boltConfig{} // interface assertion
func (bc *boltConfig) String() string {
ret := filepath.Base(string(bc.Path))
if bc.Readonly {
ret += " (read-only)"
}
return ret
}
func (bc *boltConfig) DetailedString() string {
return string(bc.Path)
}
func (bc *boltConfig) Connect(ctx context.Context) (loadedDatabase, error) {
opts := bbolt.Options{
Timeout: 1 * time.Second,
ReadOnly: bc.Readonly,
}
if bc.AdvancedOptions != nil {
// Use them instead
// Q? fixup OpenFile/Logger?
opts = bc.AdvancedOptions.Options
}
db, err := bbolt.Open(string(bc.Path), 0644, &opts)
if err != nil {
return nil, fmt.Errorf("Failed to load database '%s': %w", bc.Path, err)
}
ld := &boltLoadedDatabase{
path: string(bc.Path),
db: db,
}
return ld, nil
}
func (ld *boltLoadedDatabase) DriverName() string {
return "Bolt " + version.Version
}
func (ld *boltLoadedDatabase) Properties(bucketPath []string) (string, error) {
content := fmt.Sprintf("Selected database: %#v", ld.db.Stats())
return content, nil
}
func (ld *boltLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// Load properties
if len(bucketPath) == 0 {
return nil // Can't have data outside of the top bucket
}
// Load data
// Bolt always uses Key + Value as the columns
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
err := ld.db.View(func(tx *bbolt.Tx) error {
b := boltTargetBucket(tx, bucketPath)
if b == nil {
// no such bucket
return nil
}
// Valid
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
// Nil values mean it's a bucket
// Hide from the data table, it will be shown in the nav instead
if v == nil {
continue
}
f.AddRow_PK_Data(k, k, v)
}
return nil
})
if err != nil {
return err
}
// Valid
f.Ready()
return nil
}
// ApplyChanges_binColumn is a helper function to apply edits to K/V stores that
// can use a common abstraction.
// It always uses the "binColumn" type i.e. []byte.
func ApplyChanges_binColumn(f *tableState, Put func(k, v []byte) error, Delete func(k []byte) error) error {
// Columns are two binColumn
keyCol := f.columns[0].(*binColumn)
valCol := f.columns[1].(*binColumn)
// Edit
for rowid, _ /*editcells*/ := range f.updateRows {
k_orig := f.primaryKeys[rowid]
k_new := keyCol.vals[rowid]
v := valCol.vals[rowid]
if !bytes.Equal(k_orig, k_new) {
// Editing the primary key
// Delete k_orig and only put in k_new
err := Delete(k_orig)
if err != nil {
return fmt.Errorf("Deleting cell %q: %w", formatUtf8(k_orig), err)
}
}
err := Put(k_new, []byte(v))
if err != nil {
return fmt.Errorf("Updating cell %q: %w", formatUtf8(k_new), err)
}
}
// Delete by key (affects rowids after re-render)
for rowid, _ := range f.deleteRows {
k := f.primaryKeys[rowid]
err := Delete(k)
if err != nil {
return fmt.Errorf("Deleting cell %q: %w", formatUtf8(k), err)
}
}
// Insert all new entries
for rowid, _ := range f.insertRows {
// Newly inserted rows will not have a valid value stashed in the
// f.primaryKeys slice
// Have to get it from the data content of the cell directly
k := keyCol.vals[rowid]
v := valCol.vals[rowid] // There's only one value cell
err := Put(k, v)
if err != nil {
return fmt.Errorf("Inserting cell %q: %w", formatUtf8(k), err)
}
}
// Done
return nil
}
func (n *boltLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
if n.db.IsReadOnly() {
return errors.New("Database was opened read-only")
}
return n.db.Update(func(tx *bbolt.Tx) error {
// Get current bucket handle
b := boltTargetBucket(tx, bucketPath)
return ApplyChanges_binColumn(f, b.Put, b.Delete)
})
}
func (ld *boltLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
// In the bolt implementation, the nav is a recursive tree of child buckets
return boltChildBucketNames(ld.db, bucketPath)
}
func (ld *boltLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
ret := []contextAction{
{"Add bucket...", ld.AddChildBucket},
}
if len(bucketPath) > 0 {
ret = append(ret, contextAction{"Delete bucket", ld.DeleteBucket})
}
if len(bucketPath) == 0 {
ret = append(ret, contextAction{"Export to .zip...", ld.exportToZip})
}
return ret, nil
}
func (ld *boltLoadedDatabase) exportToZip(sender *qt.QTreeWidgetItem, bucketPath []string) error {
// Popup for output file
savePath := qt.QFileDialog_GetSaveFileName4(sender.TreeWidget().QWidget, "Save backup as...", "", "Zip archive (*.zip);;All files (*)")
if savePath == "" {
return nil // cancelled
}
return ld.exportDatabaseToZip(savePath)
}
func (ld *boltLoadedDatabase) AddChildBucket(sender *qt.QTreeWidgetItem, bucketPath []string) error {
bucketName := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new bucket:")
if bucketName == "" {
return nil // cancel
}
err := ld.db.Update(func(tx *bbolt.Tx) error {
parent := boltTargetBucket(tx, bucketPath)
if parent != nil {
_, err := parent.CreateBucket([]byte(bucketName))
return err
}
// Top-level
_, err := tx.CreateBucket([]byte(bucketName))
return err
})
if err != nil {
return fmt.Errorf("Error adding bucket: %w", err)
}
return nil
}
func (ld *boltLoadedDatabase) DeleteBucket(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the bucket %q?", bucketPath[0])) {
return nil // cancelled
}
err := ld.db.Update(func(tx *bbolt.Tx) error {
// Find parent of this bucket.
if len(bucketPath) >= 2 {
// child bucket
parent := boltTargetBucket(tx, bucketPath[0:len(bucketPath)-1])
return parent.DeleteBucket([]byte(bucketPath[len(bucketPath)-1]))
} else {
// top-level bucket
return tx.DeleteBucket([]byte(bucketPath[0]))
}
})
if err != nil {
return fmt.Errorf("Error deleting bucket %q: %w", strings.Join(bucketPath, `/`), err)
}
return nil
}
func (ld *boltLoadedDatabase) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &boltLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &boltLoadedDatabase{} // interface assertion
//
// boltTargetBucket resolves the bucketPath to a specific bolt.Bucket.
// If the path is empty, this function returns <nil> and you must manually access
// the root bucket.
// If the bucket does not exist, this function returns <nil>. It does not create
// the bucket.
func boltTargetBucket(tx *bbolt.Tx, path []string) *bbolt.Bucket {
// If we are already deep in buckets, go directly there to find children
if len(path) == 0 {
return nil
}
b := tx.Bucket([]byte(path[0]))
if b == nil {
return nil // unexpectedly missing
}
for i := 1; i < len(path); i += 1 {
b = b.Bucket([]byte(path[i]))
if b == nil {
return nil // unexpectedly missing
}
}
return b // OK
}
func boltChildBucketNames(db *bbolt.DB, path []string) ([]string, error) {
var nextBucketNames []string
err := db.View(func(tx *bbolt.Tx) error {
// If we are already deep in buckets, go directly there to find children
if len(path) > 0 {
b := tx.Bucket([]byte(path[0]))
if b == nil {
return fmt.Errorf("Root bucket %q: %w", path[0], ErrNavNotExist)
}
for i := 1; i < len(path); i += 1 {
b = b.Bucket([]byte(path[i]))
if b == nil {
return fmt.Errorf("Bucket %q: %w", strings.Join(path[0:i], `/`), ErrNavNotExist)
}
}
// Find child buckets of this bucket
b.ForEachBucket(func(bucketName []byte) error {
nextBucketNames = append(nextBucketNames, string(bucketName))
return nil
})
} else {
// Find root bucket names
return tx.ForEach(func(bucketName []byte, _ *bbolt.Bucket) error {
nextBucketNames = append(nextBucketNames, string(bucketName))
return nil
})
}
// OK
return nil
})
if err != nil {
return nil, err
}
sort.Strings(nextBucketNames)
return nextBucketNames, nil
}

230
db_bolt_zip.go Normal file
View File

@@ -0,0 +1,230 @@
package main
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/fs"
"os"
"path"
"strings"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"go.etcd.io/bbolt"
)
func (ld *boltLoadedDatabase) exportDatabaseToZip(zippath string) error {
fh, err := os.OpenFile(zippath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("Error opening output file: %w", err)
}
defer fh.Close()
zw := zip.NewWriter(fh)
// Filenames in zip files cannot contain `/` characters. Mangle it
// TODO undo this transfomation on import(!)
safename := func(n string) string {
return strings.ReplaceAll(string(n), `/`, `__`)
}
err = ld.db.View(func(tx *bbolt.Tx) error {
var process func(currentPath []string) error
process = func(currentPath []string) error {
// Create folder-entry for our own bucket
ourFolderName := path.Join(slice_apply(currentPath, safename)...)
ourBucket := zip.FileHeader{
Name: ourFolderName + `/`, // Trailing slash = directory
}
ourBucket.SetMode(fs.ModeDir | 0755)
_, err := zw.CreateHeader(&ourBucket)
if err != nil {
return err
}
// Create file entries for all non-bucket children
b := boltTargetBucket(tx, currentPath)
var childBuckets []string
var c *bbolt.Cursor
if b != nil {
c = b.Cursor() // in bucket
} else {
c = tx.Cursor()
}
for k, v := c.First(); k != nil; k, v = c.Next() {
if v == nil {
// That's a bucket
childBuckets = append(childBuckets, string(k))
continue
}
fileItem := zip.FileHeader{
Name: path.Join(ourFolderName, safename(string(k))),
}
fileItem.SetMode(0644)
fileW, err := zw.CreateHeader(&fileItem)
if err != nil {
return err
}
_, err = io.CopyN(fileW, bytes.NewReader(v), int64(len(v)))
if err != nil {
return err
}
}
// Recurse for all bucket-type children
for _, childBucketName := range childBuckets {
process(slice_and(currentPath, childBucketName))
}
// Done
return nil
}
return process([]string{})
})
if err != nil {
return err
}
err = zw.Flush()
if err != nil {
return err
}
err = zw.Close()
if err != nil {
return err
}
return fh.Close()
}
func (f *App) Bolt_ImportZipToDatabase_OnTriggered() {
zippath := qt.QFileDialog_GetOpenFileName4(f.ui.MainWindow.QWidget, "Select a zip archive to import...", "", "Zip archives (*.zip);;All files (*)")
if zippath == "" {
return // cancelled
}
dbpath := qt.QFileDialog_GetSaveFileName4(f.ui.MainWindow.QWidget, "Select an output file to save as...", "", "Bolt database (*.db);;All files (*)")
if dbpath == "" {
return // cancelled
}
err := Bolt_ImportZipToDatabase(dbpath, zippath)
if err != nil {
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
return
}
res := qt.QMessageBox_Question2(f.ui.MainWindow.QWidget, APPNAME, "The import was successful. Would you like to open the Bolt database now?", qt.QMessageBox__Yes, qt.QMessageBox__No)
if res != int(qt.QMessageBox__Yes) {
return
}
config := NewConnectionConfig()
config.Type = "Bolt"
config.Bolt = &boltConfig{
Path: autoconfig.ExistingFile(dbpath),
Readonly: false,
}
f.showConnectDialog(config)
}
func Bolt_ImportZipToDatabase(dbpath, zippath string) error {
db, err := bbolt.Open(dbpath, 0644, nil)
if err != nil {
return fmt.Errorf("Opening target database: %w", err)
}
defer db.Close()
fh, err := os.OpenFile(zippath, os.O_RDONLY, 0400)
if err != nil {
return fmt.Errorf("Opening input archive: %w", err)
}
defer fh.Close()
fstat, err := fh.Stat()
if err != nil {
return err
}
zr, err := zip.NewReader(fh, fstat.Size())
if err != nil {
return fmt.Errorf("Reading zip file format: %w", err)
}
return db.Update(func(tx *bbolt.Tx) error {
for _, zf := range zr.File {
if strings.HasSuffix(zf.Name, `/`) || (zf.Mode()&fs.ModeDir) != 0 {
// Bucket
if zf.Name == `/` {
continue // virtual entry for top-level directory, skip
}
bucketPath := strings.Split(strings.TrimSuffix(zf.Name, `/`), `/`)
parentBucket := boltTargetBucket(tx, bucketPath[0:len(bucketPath)-1])
newBucketName := []byte(bucketPath[len(bucketPath)-1])
var err error
if parentBucket == nil {
_, err = tx.CreateBucket(newBucketName) // at top level
} else {
_, err = parentBucket.CreateBucket(newBucketName) // child bucket
}
if err != nil {
return fmt.Errorf("Creating bucket %q: %w", zf.Name, err)
}
} else {
// Object
objectPath := strings.Split(zf.Name, `/`)
rc, err := zf.Open()
if err != nil {
return err
}
content, err := io.ReadAll(rc)
if err != nil {
return err
}
parentBucket := boltTargetBucket(tx, objectPath[0:len(objectPath)-1]) // Can't be nil, items always exist within a bucket
objectKey := []byte(objectPath[len(objectPath)-1])
err = parentBucket.Put(objectKey, content)
if err != nil {
return err
}
err = rc.Close()
if err != nil {
return err
}
}
}
// Done
return nil
})
}

156
db_bunt.go Normal file
View File

@@ -0,0 +1,156 @@
package main
import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"github.com/tidwall/buntdb"
)
type buntLdb struct {
db *buntdb.DB
}
func (ld *buntLdb) DriverName() string {
return "BuntDB"
}
func (ld *buntLdb) Properties(bucketPath []string) (string, error) {
idxInfo, err := ld.db.Indexes()
if err != nil {
return "", err
}
return fmt.Sprintf("Indexes (%d):\n%s", len(idxInfo), "-"+strings.Join(idxInfo, "\n- ")+"\n"), nil
}
func (ld *buntLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
// Interestingly, BuntDB internally uses string data, not []byte data
// That would stop us from using ApplyChanges_binColumn that only works with
// []byte data, so, fake []byte casts ourselves
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
err := ld.db.View(func(tx *buntdb.Tx) error {
return tx.Ascend("", func(k, v string) bool {
f.AddRow_PK_Data([]byte(k), []byte(k), []byte(v))
return true
})
})
if err != nil {
return err
}
// Valid
f.Ready()
return nil
}
func (n *buntLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return n.db.Update(func(tx *buntdb.Tx) error {
return ApplyChanges_binColumn(
f,
func(k, v []byte) error {
_, _, err := tx.Set(string(k), string(v), nil)
return err
},
func(k []byte) error {
_, err := tx.Delete(string(k))
return err
},
)
})
}
func (ld *buntLdb) doShrink(sender *qt.QTreeWidgetItem, bucketPath []string) error {
return ld.db.Shrink()
}
func (ld *buntLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *buntLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return []contextAction{
{"Shrink", ld.doShrink},
}, nil
}
func (ld *buntLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &buntLdb{} // interface assertion
var _ editableLoadedDatabase = &buntLdb{} // interface assertion
//
type BuntDBLocation struct {
Type autoconfig.OneOf
Disk *struct {
File autoconfig.ExistingFile
} `json:",omitempty"`
Memory *struct{} `json:",omitempty"`
}
type buntDBAdvancedOptions struct {
buntdb.Config
}
func (bao *buntDBAdvancedOptions) Reset() {
// buntdb does not expose the default configuration, it's only set in the
// DB .Open() constructor
// Extract the defaults out from a temporary `:memory:` database
if memDb, err := buntdb.Open(`:memory:`); err == nil {
defer memDb.Close()
_ = memDb.ReadConfig(&bao.Config)
}
}
type buntDBConnection struct {
BuntDBLocation
AdvancedOptions *buntDBAdvancedOptions `json:",omitempty"`
}
var _ DBConnector = &buntDBConnection{} // interface assertion
func (ldc *buntDBConnection) String() string {
if ldc.Disk != nil {
return filepath.Base(string(ldc.Disk.File))
} else {
return `:memory:`
}
}
func (ldc *buntDBConnection) Connect(ctx context.Context) (loadedDatabase, error) {
var path string
if ldc.Disk != nil {
path = string(ldc.Disk.File)
} else {
path = `:memory:` // Special string known by driver
}
db, err := buntdb.Open(path)
if err != nil {
return nil, err
}
if ldc.AdvancedOptions != nil {
err = db.SetConfig(ldc.AdvancedOptions.Config)
if err != nil {
_ = db.Close()
return nil, err
}
}
return &buntLdb{db: db}, nil
}

127
db_debconf.go Normal file
View File

@@ -0,0 +1,127 @@
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"qbolt/debconf"
"github.com/mappu/autoconfig"
)
type debconfLoadedDatabase struct {
db *debconf.Database
}
func (ld *debconfLoadedDatabase) DriverName() string {
return "debconf"
}
func (ld *debconfLoadedDatabase) Properties(bucketPath []string) (string, error) {
content := fmt.Sprintf("Applications: %d\nUnique attributes: %d\n", len(ld.db.Entries), len(ld.db.AllColumnNames))
return content, nil
}
func (ld *debconfLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
if len(bucketPath) == 0 {
return nil // No data at root level
}
// Find application entry for bucketPath
appInfo, ok := ld.db.FindApplicationByName(bucketPath[0])
if !ok {
return fmt.Errorf("Invalid application %q", bucketPath[0])
}
// Process entries for specific application
indexes := make(map[string]int)
cols := make([]TableColumn, 0, len(ld.db.AllColumnNames))
for i := 0; i < len(ld.db.AllColumnNames); i++ {
cols = append(cols, &stringColumn{})
}
f.SetupColumns(cols, ld.db.AllColumnNames)
for i, cname := range ld.db.AllColumnNames {
indexes[cname] = i
}
for _, entry := range appInfo.Entries {
rpos := f.AddRow()
f.SetCell(rpos, 0, entry.Name)
for _, proppair := range entry.Properties {
f.SetCell(rpos, indexes[proppair[0]], proppair[1])
}
}
// Valid
f.Ready()
return nil
}
func (ld *debconfLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
if len(bucketPath) == 0 {
ret := make([]string, 0, len(ld.db.Entries))
for _, app := range ld.db.Entries {
ret = append(ret, app.Name)
}
return ret, nil
} else {
return []string{}, nil // No further children
}
}
func (ld *debconfLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *debconfLoadedDatabase) Close() {
}
var _ loadedDatabase = &debconfLoadedDatabase{} // interface assertion
//
//
type debconfConnection struct {
Database autoconfig.ExistingFile `yfilter:"Debconf database (*.dat);;All files (*)"`
}
var _ DBConnector = &debconfConnection{} // interface assertion
func (dc *debconfConnection) Reset() {
if runtime.GOOS == "linux" {
dc.Database = "/var/cache/debconf/config.dat" // Prefill default path
}
}
func (dc *debconfConnection) String() string {
return filepath.Base(string(dc.Database))
}
func (dc *debconfConnection) Connect(ctx context.Context) (loadedDatabase, error) {
fh, err := os.OpenFile(string(dc.Database), os.O_RDONLY, 0400)
if err != nil {
return nil, err
}
defer fh.Close()
db, err := debconf.Parse(fh)
if err != nil {
return nil, err
}
return &debconfLoadedDatabase{db: db}, nil
}

67
db_embeddedversions.go Normal file
View File

@@ -0,0 +1,67 @@
package main
import (
"context"
"errors"
"fmt"
"runtime/debug"
)
type evLdb struct {
mods *debug.BuildInfo
}
func (ld *evLdb) DriverName() string {
return APPNAME + " " + appVersion
}
func (ld *evLdb) Properties(bucketPath []string) (string, error) {
return fmt.Sprintf(
"%s %s\n- %d package dependencies\n- Compiler: %s",
APPNAME, appVersion, len(ld.mods.Deps), ld.mods.GoVersion,
), nil
}
func (ld *evLdb) RenderForNav(f *tableState, bucketPath []string) error {
f.SetupColumns([]TableColumn{&goModuleColumn{}, &stringColumn{}, &stringColumn{}}, []string{"Library", "Version", "Hash"})
for _, dep := range ld.mods.Deps {
f.AddRowData(dep.Path, dep.Version, dep.Sum)
}
// Valid
f.Ready()
return nil
}
func (ld *evLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No further children
}
func (ld *evLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *evLdb) Close() {}
var _ loadedDatabase = &evLdb{} // interface assertion
//
type evLdbConnection struct{}
var _ DBConnector = &evLdbConnection{} // interface assertion
func (dc *evLdbConnection) String() string {
return APPNAME
}
func (dc *evLdbConnection) Connect(ctx context.Context) (loadedDatabase, error) {
mods, ok := debug.ReadBuildInfo()
if !ok {
return nil, errors.New("Missing build info")
}
return &evLdb{mods: mods}, nil
}

319
db_etcd.go Normal file
View File

@@ -0,0 +1,319 @@
package main
import (
"context"
"fmt"
"strings"
"time"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
etcd2 "go.etcd.io/etcd/client/v2"
etcd3 "go.etcd.io/etcd/client/v3"
)
///////////////////////////
// Client v3
///////////////////////////
type etcd3Ldb struct {
cl *etcd3.Client
}
func (ld *etcd3Ldb) DriverName() string {
return "etcd v3"
}
func (ld *etcd3Ldb) Properties(bucketPath []string) (string, error) {
return "", nil // No properties
}
func (ld *etcd3Ldb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
ctx := context.TODO()
resp, err := ld.cl.Get(ctx, "", etcd3.WithPrefix())
if err != nil {
return err
}
for _, kv := range resp.Kvs {
// Strange - reading data, you get it as a []byte, but setting it, you use strings
f.AddRow_PK_Data(kv.Key, kv.Key, kv.Value)
}
f.Ready()
return nil
}
func (n *etcd3Ldb) ApplyChanges(f *tableState, bucketPath []string) error {
ctx := context.TODO()
// TODO use a transaction
put := func(k, v []byte) error {
_, err := n.cl.Put(ctx, string(k), string(v))
return err
}
del := func(k []byte) error {
_, err := n.cl.Delete(ctx, string(k))
return err
}
return ApplyChanges_binColumn(f, put, del)
}
func (ld *etcd3Ldb) NavChildren(bucketPath []string) ([]string, error) {
// TODO: auth, members, ...
return []string{}, nil // No children
}
func (ld *etcd3Ldb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // no actions
}
func (ld *etcd3Ldb) Close() {
_ = ld.cl.Close()
}
var _ loadedDatabase = &etcd3Ldb{} // interface assertion
var _ editableLoadedDatabase = &etcd3Ldb{} // interface assertion
///////////////////////////
// Client v2
///////////////////////////
type etcd2Ldb struct {
cl etcd2.Client // interface, by value
}
func (ld *etcd2Ldb) DriverName() string {
return "etcd v2"
}
func (ld *etcd2Ldb) Properties(bucketPath []string) (string, error) {
return "", nil // No properties
}
// etcd2_dirPath always has a leading + trailing `/` character.
func etcd2_dirPath(bucketPath []string) string {
if len(bucketPath) == 0 {
return `/`
}
return `/` + strings.Join(bucketPath, `/`) + `/`
}
func (ld *etcd2Ldb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
// Etcd v2 always uses string types, not []byte types
// However we use []byte columns anyway to get access to ApplyChanges_binColumn
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
// In etcd v2, the bucketPath should always turn into something with a final /
dirPath := etcd2_dirPath(bucketPath)
ctx := context.TODO()
resp, err := etcd2.NewKeysAPI(ld.cl).Get(ctx, dirPath, nil) // TODO any way to request only non-directories here?
if err != nil {
return err
}
if !resp.Node.Dir {
return fmt.Errorf("Path %q expected directory, got data", dirPath)
}
for _, child := range resp.Node.Nodes {
if child.Dir {
continue // Only looking for data here, not directories
}
if !strings.HasPrefix(child.Key, dirPath) {
return fmt.Errorf("Requested contents of %q but entry %q is missing expected prefix", dirPath, child.Key)
}
realKey := child.Key[len(dirPath):]
f.AddRow_PK_Data([]byte(realKey), []byte(realKey), []byte(child.Value))
}
f.Ready()
return nil
}
func (n *etcd2Ldb) ApplyChanges(f *tableState, bucketPath []string) error {
ctx := context.TODO()
ka := etcd2.NewKeysAPI(n.cl)
dirPath := etcd2_dirPath(bucketPath) // Has trailing slash
// TODO use a transaction
put := func(k, v []byte) error {
_, err := ka.Set(ctx, dirPath+string(k), string(v), nil)
return err
}
del := func(k []byte) error {
_, err := ka.Delete(ctx, dirPath+string(k), nil)
return err
}
return ApplyChanges_binColumn(f, put, del)
}
func (ld *etcd2Ldb) NavChildren(bucketPath []string) ([]string, error) {
// In etcd v2, the bucketPath should always turn into something with a final /
dirPath := etcd2_dirPath(bucketPath) // Has trailing slash
ctx := context.TODO()
resp, err := etcd2.NewKeysAPI(ld.cl).Get(ctx, dirPath, nil) // TODO any way to request only directories here?
if err != nil {
return nil, err
}
if !resp.Node.Dir {
return nil, fmt.Errorf("Path %q expected directory, got data", dirPath)
}
var ret []string
for _, child := range resp.Node.Nodes {
if !child.Dir {
continue // Only looking for data here, not directories
}
if !strings.HasPrefix(child.Key, dirPath) {
return nil, fmt.Errorf("Requested contents of %q but entry %q is missing expected prefix", dirPath, child.Key)
}
realKey := child.Key[len(dirPath):]
ret = append(ret, realKey)
}
return ret, nil
}
func (ld *etcd2Ldb) createDirectory(sender *qt.QTreeWidgetItem, bucketPath []string) error {
dirName := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new directory:")
if dirName == "" {
return nil // cancel
}
ctx := context.TODO()
ka := etcd2.NewKeysAPI(ld.cl)
newDirKey := etcd2_dirPath(bucketPath) + `/` + dirName
_, err := ka.Set(ctx, newDirKey, "", &etcd2.SetOptions{Dir: true})
return err
// TODO move the UI to select the newly created bucket
}
func (ld *etcd2Ldb) deleteDirectory(sender *qt.QTreeWidgetItem, bucketPath []string) error {
dirKey := etcd2_dirPath(bucketPath)
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the bucket %q and all its contents?", dirKey)) {
return nil // cancelled
}
ctx := context.TODO()
ka := etcd2.NewKeysAPI(ld.cl)
_, err := ka.Delete(ctx, dirKey, &etcd2.DeleteOptions{Dir: true, Recursive: true})
return err
// TODO The app tries to refresh the non-existent bucket which results in an error
// Need a way to move the current selection up to the parent level
}
func (ld *etcd2Ldb) NavContext(bucketPath []string) ([]contextAction, error) {
ret := []contextAction{
contextAction{"Create directory...", ld.createDirectory},
}
if len(bucketPath) > 0 {
ret = append(ret, contextAction{"Delete directory...", ld.deleteDirectory})
}
return ret, nil
}
func (ld *etcd2Ldb) Close() {
// v2 has nothing to close in the client
}
var _ loadedDatabase = &etcd2Ldb{} // interface assertion
var _ editableLoadedDatabase = &etcd2Ldb{} // interface assertion
///////////////////////////
// Connection
///////////////////////////
type etcdConn struct {
Endpoints []string
Username string
Password autoconfig.Password
H1 autoconfig.Header `ylabel:"Protocol version:" json:",omitempty"`
Protocol struct {
Type autoconfig.OneOf
V3 *struct{} `ylabel:"v3" json:",omitempty"`
V2 *struct{} `ylabel:"v2" json:",omitempty"`
}
}
var _ DBConnector = &etcdConn{} // interface assertion
func (c *etcdConn) String() string {
if len(c.Endpoints) == 1 {
return c.Endpoints[0]
} else if len(c.Endpoints) > 1 {
return fmt.Sprintf("%d endpoints", c.Endpoints)
} else {
return ""
}
}
func (c *etcdConn) Connect(ctx context.Context) (loadedDatabase, error) {
if c.Protocol.V3 != nil {
cfg := etcd3.Config{
Endpoints: c.Endpoints,
Username: c.Username,
Password: string(c.Password),
DialTimeout: 5 * time.Second,
// TODO SSH tunnel
}
cl, err := etcd3.New(cfg)
if err != nil {
return nil, err
}
return &etcd3Ldb{cl: cl}, nil
} else if c.Protocol.V2 != nil {
cfg := etcd2.Config{
Endpoints: c.Endpoints,
Username: c.Username,
Password: string(c.Password),
// TODO SSH tunnel
}
cl, err := etcd2.New(cfg)
if err != nil {
return nil, err
}
return &etcd2Ldb{cl: cl}, nil
} else {
return nil, fmt.Errorf("Unknown protocol version %q", c.Protocol.Type)
}
}

112
db_leveldb.go Normal file
View File

@@ -0,0 +1,112 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/mappu/autoconfig"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/opt"
)
type leveldbLoadedDatabase struct {
db *leveldb.DB
}
func (ld *leveldbLoadedDatabase) DriverName() string {
return "LevelDB"
}
func (ld *leveldbLoadedDatabase) Properties(bucketPath []string) (string, error) {
var s leveldb.DBStats
err := ld.db.Stats(&s)
if err != nil {
return "", fmt.Errorf("Stats: %w", err)
}
content := fmt.Sprintf("LevelDB stats: %#v", s)
return content, nil
}
func (ld *leveldbLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
// leveldb always uses Key + Value as the columns
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
itn := ld.db.NewIterator(nil, nil)
defer itn.Release()
for itn.Next() {
f.AddRow_PK_Data(itn.Key(), itn.Key(), itn.Value())
}
// Valid
f.Ready()
return nil
}
func (n *leveldbLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
txn, err := n.db.OpenTransaction()
if err != nil {
return fmt.Errorf("OpenTransaction: %w", err)
}
err = ApplyChanges_binColumn(
f,
func(k, v []byte) error { return txn.Put(k, v, nil) },
func(k []byte) error { return txn.Delete(k, nil) },
)
if err != nil {
txn.Discard()
return err
}
return txn.Commit()
}
func (ld *leveldbLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *leveldbLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *leveldbLoadedDatabase) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &leveldbLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &leveldbLoadedDatabase{} // interface assertion
//
type leveldbConnection struct {
Directory autoconfig.ExistingDirectory
Readonly bool
}
var _ DBConnector = &leveldbConnection{} // interface assertion
func (pdc *leveldbConnection) String() string {
ret := filepath.Base(string(pdc.Directory))
if pdc.Readonly {
ret += " (read-only)"
}
return ret
}
func (pdc *leveldbConnection) Connect(ctx context.Context) (loadedDatabase, error) {
var o opt.Options
o.ReadOnly = pdc.Readonly
db, err := leveldb.OpenFile(string(pdc.Directory), &o)
if err != nil {
return nil, err
}
return &leveldbLoadedDatabase{db: db}, nil
}

295
db_lmdb.go Normal file
View File

@@ -0,0 +1,295 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/ledgerwatch/lmdb-go/lmdb"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
)
type lmdbDatabase struct {
db *lmdb.Env
isMulti bool
}
func (ld *lmdbDatabase) DriverName() string {
return lmdb.VersionString() // Already includes "LMDB" prefix
}
func (ld *lmdbDatabase) Properties(bucketPath []string) (string, error) {
info, err := ld.db.Info()
if err != nil {
return "", err
}
return fmt.Sprintf("LMDB info: %#v", info), nil
}
func (ld *lmdbDatabase) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
if ld.isMulti && len(bucketPath) == 0 {
// In multi-database mode, the only things in the root DB are keys
// naming the child databases
// Show no data, it will be shown in the nav area instead
return nil
}
// LMDB always uses Key + Value as the columns
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
err := ld.db.View(func(txn *lmdb.Txn) error {
var dbi lmdb.DBI
var err error
if len(bucketPath) == 0 {
dbi, err = txn.OpenRoot(0)
} else {
dbi, err = txn.OpenDBI(bucketPath[0], 0)
}
if err != nil {
return err
}
// defer ld.db.CloseDBI(dbi)
itn, err := txn.OpenCursor(dbi)
defer itn.Close()
if err != nil {
return err
}
// FIXME is this correct?
kbuff := make([]byte, ld.db.MaxKeySize())
vbuff := make([]byte, 4096)
var op uint = lmdb.First
for {
key, val, err := itn.Get(kbuff, vbuff, op)
if err != nil {
if lmdb.IsNotFound(err) {
break // reached end of iteration
}
return err // a real error
}
op = lmdb.Next
f.AddRow_PK_Data(key, key, val)
}
// Done
return nil
})
if err != nil {
return err
}
// Valid
f.Ready()
return nil
}
func (ld *lmdbDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
return ld.db.Update(func(txn *lmdb.Txn) error {
var dbi lmdb.DBI
var err error
if len(bucketPath) == 0 {
dbi, err = txn.OpenRoot(0)
} else {
dbi, err = txn.OpenDBI(bucketPath[0], 0)
}
if err != nil {
return err
}
return ApplyChanges_binColumn(
f,
func(k, v []byte) error { return txn.Put(dbi, k, v, 0) },
func(k []byte) error { return txn.Del(dbi, k, nil) },
)
})
}
func (ld *lmdbDatabase) NavChildren(bucketPath []string) ([]string, error) {
// """To use named databases (with name != NULL), mdb_env_set_maxdbs() must
// be called before opening the environment. Database names are keys in the
// unnamed database, and may be read but not written."""
if len(bucketPath) == 0 && ld.isMulti {
// Read all keys from root DB
var allKeys []string
err := ld.db.View(func(txn *lmdb.Txn) error {
dbi, err := txn.OpenRoot(0)
if err != nil {
return err
}
//defer ld.db.CloseDBI(dbi)
itn, err := txn.OpenCursor(dbi)
defer itn.Close()
if err != nil {
return err
}
kbuff := make([]byte, ld.db.MaxKeySize())
var op uint = lmdb.First
for {
key, _, err := itn.Get(kbuff, nil, op)
if err != nil {
if lmdb.IsNotFound(err) {
break // reached end of iteration
}
return err // a real error
}
op = lmdb.Next
allKeys = append(allKeys, string(key))
}
// Done
return nil
})
return allKeys, err
} else {
return []string{}, nil // No children
}
}
func (ld *lmdbDatabase) createChildDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
// Prompt for child database name
ok := false
childDbName := qt.QInputDialog_GetText4(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the child database:", qt.QLineEdit__Normal, "", &ok)
if !ok {
return nil // Cancelled
}
return ld.db.Update(func(txn *lmdb.Txn) error {
_, err := txn.CreateDBI(childDbName)
return err
})
}
func (ld *lmdbDatabase) truncateAllContent(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to empty the child database %q?", bucketPath[0])) {
return nil // cancelled
}
return ld.drop(bucketPath[0], true)
}
func (ld *lmdbDatabase) deleteChildDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the child database %q?", bucketPath[0])) {
return nil // cancelled
}
return ld.drop(bucketPath[0], true)
}
func (ld *lmdbDatabase) drop(multiDbName string, delet bool) error {
return ld.db.Update(func(txn *lmdb.Txn) error {
dbi, err := txn.OpenDBI(multiDbName, 0)
if err != nil {
return err
}
return txn.Drop(dbi, true)
})
}
func (ld *lmdbDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
if ld.isMulti {
// Allow create/delete databases
if len(bucketPath) == 0 {
return []contextAction{
contextAction{"Add child database...", ld.createChildDatabase},
}, nil
} else {
return []contextAction{
contextAction{"Truncate and remove all contents...", ld.truncateAllContent},
contextAction{"Remove child database...", ld.deleteChildDatabase},
}, nil
}
}
return nil, nil // No special actions are supported
}
func (ld *lmdbDatabase) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &lmdbDatabase{} // interface assertion
var _ editableLoadedDatabase = &lmdbDatabase{} // interface assertion
//
type lmdbConnection struct {
Storage struct {
Type autoconfig.OneOf
Directory *autoconfig.ExistingDirectory `json:",omitempty"`
File *struct {
Path autoconfig.ExistingFile `yfilter:"LMDB database (*.mdb);;All files (*)"`
} `json:",omitempty"`
}
MultiDB *struct {
Slots int
} `ylabel:"Multiple databases mode"`
Readonly bool
}
var _ DBConnector = &lmdbConnection{} // interface assertion
func (pdc *lmdbConnection) String() string {
if pdc.Storage.Directory != nil {
return filepath.Base(string(*pdc.Storage.Directory))
} else if pdc.Storage.File != nil {
return filepath.Base(string(pdc.Storage.File.Path))
}
return "" // unreachable
}
func (pdc *lmdbConnection) Connect(ctx context.Context) (loadedDatabase, error) {
env, err := lmdb.NewEnv()
if err != nil {
return nil, err
}
var openPath string
var flags uint = 0
if pdc.Readonly {
flags |= lmdb.Readonly
}
if pdc.Storage.Directory != nil {
openPath = string(*pdc.Storage.Directory)
} else if pdc.Storage.File != nil {
openPath = string(pdc.Storage.File.Path)
flags |= lmdb.NoSubdir
}
isMulti := false
if pdc.MultiDB != nil {
env.SetMaxDBs(pdc.MultiDB.Slots)
isMulti = true
}
err = env.Open(openPath, flags, 0644)
if err != nil {
_ = env.Close()
return nil, err
}
return &lmdbDatabase{db: env, isMulti: isMulti}, nil
}

94
db_lotusdb.go Normal file
View File

@@ -0,0 +1,94 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/lotusdblabs/lotusdb/v2"
"github.com/mappu/autoconfig"
)
type lotusLdb struct {
db *lotusdb.DB
}
func (ld *lotusLdb) DriverName() string {
return "LotusDB"
}
func (ld *lotusLdb) Properties(bucketPath []string) (string, error) {
return "", nil
}
func (ld *lotusLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
cur, err := ld.db.NewIterator(lotusdb.IteratorOptions{})
if err != nil {
return fmt.Errorf("NewIterator: %w", err)
}
defer cur.Close()
for cur.Valid() {
f.AddRow_PK_Data(cur.Key(), cur.Key(), cur.Value())
cur.Next()
}
// Valid
f.Ready()
return nil
}
func (n *lotusLdb) ApplyChanges(f *tableState, bucketPath []string) error {
txn := n.db.NewBatch(lotusdb.DefaultBatchOptions)
err := ApplyChanges_binColumn(f, txn.Put, txn.Delete)
if err != nil {
return err
}
return txn.Commit()
}
func (ld *lotusLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *lotusLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *lotusLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &lotusLdb{} // interface assertion
var _ editableLoadedDatabase = &lotusLdb{} // interface assertion
//
type lotusDBConnection struct {
Directory autoconfig.ExistingDirectory
}
var _ DBConnector = &lotusDBConnection{} // interface assertion
func (ldc *lotusDBConnection) String() string {
return filepath.Base(string(ldc.Directory))
}
func (ldc *lotusDBConnection) Connect(ctx context.Context) (loadedDatabase, error) {
opts := lotusdb.DefaultOptions // copy
opts.DirPath = string(ldc.Directory)
db, err := lotusdb.Open(opts)
if err != nil {
return nil, err
}
return &lotusLdb{db: db}, nil
}

354
db_mongo.go Normal file
View File

@@ -0,0 +1,354 @@
package main
import (
"context"
"errors"
"fmt"
"time"
"golang.org/x/crypto/ssh"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"go.mongodb.org/mongo-driver/v2/mongo/readpref"
"go.mongodb.org/mongo-driver/v2/version"
)
// MongoDB support
// To test: `make test-mongo`
type mongoLdb struct {
client *mongo.Client
sshc *ssh.Client
}
func (ld *mongoLdb) DriverName() string {
return "MongoDB " + version.Driver
}
func (ld *mongoLdb) Properties(bucketPath []string) (string, error) {
ctx := context.Background()
if len(bucketPath) == 0 {
return "", nil // no properties
} else if len(bucketPath) == 1 {
// Database is selected
db := ld.client.Database(bucketPath[0])
return "Database " + db.Name(), nil
} else if len(bucketPath) == 2 {
// Collection is selected
db := ld.client.Database(bucketPath[0])
coll := db.Collection(bucketPath[1])
info := "Database " + db.Name() + "\n"
info += "Collection " + coll.Name() + "\n"
// Document count
count, err := coll.EstimatedDocumentCount(ctx)
if err != nil {
return "", fmt.Errorf("Estimating document count: %w", err)
}
info += fmt.Sprintf("Estimated document count: %d", count) + "\n"
// Index info
allIndexes, err := coll.Indexes().ListSpecifications(ctx)
if err != nil {
return "", fmt.Errorf("Checking indexes: %w", err)
}
info += fmt.Sprintf("\nIndexes (%d):\n", len(allIndexes))
for _, idxInfo := range allIndexes {
info += fmt.Sprintf("- %q (namespace=%q, version=%d)\n", idxInfo.Name, idxInfo.Namespace, idxInfo.Version)
}
return info, nil
} else {
return "", errors.New("??")
}
}
func (ld *mongoLdb) RenderForNav(f *tableState, bucketPath []string) error {
ctx := context.Background()
if len(bucketPath) == 0 || len(bucketPath) == 1 {
// Leave the table disabled
return nil
} else if len(bucketPath) == 2 {
f.SetupColumns([]TableColumn{&stringColumn{}, &bsonColumn{}}, []string{"_id", "Document"})
db := ld.client.Database(bucketPath[0])
coll := db.Collection(bucketPath[1])
cur, err := coll.Find(ctx, bson.D{}) // An empty document as filter = find all results
if err != nil {
return fmt.Errorf("Find: %w", err)
}
defer cur.Close(ctx)
return ld.populateRows(ctx, cur, f)
} else {
return errors.New("??")
}
}
func (ld *mongoLdb) populateRows(ctx context.Context, cur *mongo.Cursor, f *tableState) error {
for cur.Next(ctx) {
var result bson.D
if err := cur.Decode(&result); err != nil {
return fmt.Errorf("Decode: %w", err)
}
// The document is always an ordered map[string]any.
// MongoDB enforces there is an "_id" key.
idValue, ok := bson_find_id(result)
if !ok {
return errors.New("Surprised to find a document missing an '_id' field")
}
f.AddRow_PK_Data([]byte(idValue), idValue, result)
}
if err := cur.Err(); err != nil {
return fmt.Errorf("Cursor: %w", err)
}
// Done
f.Ready()
return nil
}
func (ld *mongoLdb) NavChildren(bucketPath []string) ([]string, error) {
ctx := context.Background()
if len(bucketPath) == 0 {
return ld.client.ListDatabaseNames(ctx, bson.D{})
} else if len(bucketPath) == 1 {
db := ld.client.Database(bucketPath[0])
return db.ListCollectionNames(ctx, bson.D{})
}
return []string{}, nil // No children
}
func (ld *mongoLdb) actionNewDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
ctx := context.Background()
dbName := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new database:")
if dbName == "" {
return nil // cancel
}
// MongoDB: databases just start to exist when you write to them
newDb := ld.client.Database(dbName)
return newDb.CreateCollection(ctx, "my.new.collection")
}
func (ld *mongoLdb) actionNewCollection(sender *qt.QTreeWidgetItem, bucketPath []string) error {
ctx := context.Background()
collName := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new collection:")
if collName == "" {
return nil // cancel
}
// MongoDB: databases just start to exist when you write to them
newDb := ld.client.Database(bucketPath[0])
return newDb.CreateCollection(ctx, collName)
}
func (ld *mongoLdb) actionDropDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
ctx := context.Background()
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to drop the database %q?", bucketPath[0])) {
return nil // cancelled
}
db := ld.client.Database(bucketPath[0])
return db.Drop(ctx)
}
func (ld *mongoLdb) actionDropCollection(sender *qt.QTreeWidgetItem, bucketPath []string) error {
ctx := context.Background()
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the collection %q?", bucketPath[1])) {
return nil // cancelled
}
db := ld.client.Database(bucketPath[0])
coll := db.Collection(bucketPath[1])
return coll.Drop(ctx)
}
func (ld *mongoLdb) ExecQuery(query string, bucketPath []string, resultArea *tableState) error {
ctx := context.Background()
if len(bucketPath) == 0 {
return errors.New("Please select a database first.")
}
db := ld.client.Database(bucketPath[0])
// The query should be JSON, e.g.
// { "hello": 1 } or
// { "explain": { "count": "system.users" } } or
// { "listDatabases": 1 }
doc := bson.D{}
err := doc.UnmarshalJSON([]byte(query))
if err != nil {
return fmt.Errorf("Parsing JSON query: %w", err)
}
// FIXME how to tell if the response will have a cursor or a singleResult?
if false {
// Cursor
cur, err := db.RunCommandCursor(ctx, doc)
if err != nil {
return fmt.Errorf("Running command: %w", err)
}
defer cur.Close(ctx)
return ld.populateRows(ctx, cur, resultArea)
} else {
// Single result
res := db.RunCommand(ctx, doc)
response := bson.D{}
err = res.Decode(&response)
if err != nil {
return fmt.Errorf("Decoding response: %w", err)
}
responseJson, err := response.MarshalJSON()
if err != nil {
return fmt.Errorf("Decoding response: %w", err)
}
resultArea.SetupColumns([]TableColumn{&binColumn{}}, []string{"Response"})
resultArea.AddRowData(responseJson)
resultArea.Ready()
return nil
}
}
func (ld *mongoLdb) NavContext(bucketPath []string) ([]contextAction, error) {
if len(bucketPath) == 0 {
// Top-level connection
return []contextAction{
{"Create database...", ld.actionNewDatabase},
}, nil
} else if len(bucketPath) == 1 {
// Database selected
return []contextAction{
{"Create collection...", ld.actionNewCollection},
{"Drop database...", ld.actionDropDatabase},
}, nil
} else if len(bucketPath) == 2 {
// Collection selected
return []contextAction{
{"Drop collection...", ld.actionDropCollection},
}, nil
} else {
return nil, errors.New("???")
}
}
func (ld *mongoLdb) Close() {
_ = ld.client.Disconnect(context.Background())
if ld.sshc != nil {
_ = ld.sshc.Close()
}
}
var _ loadedDatabase = &mongoLdb{} // interface assertion
var _ queryableLoadedDatabase = &mongoLdb{} // interface assertion
//
type mongoConnection struct {
Conn struct {
Mode autoconfig.OneOf
Connection_String *string `json:",omitempty"`
}
SSH_Tunnel *SSHTunnel
}
var _ DBConnector = &mongoConnection{} // interface assertion
func (moc *mongoConnection) Reset() {
moc.Conn.Mode = "Connection_String"
moc.Conn.Connection_String = address_of("mongodb://localhost:27017")
}
func (moc *mongoConnection) String() string {
return "MongoDB" // TODO could be improved
}
func (moc *mongoConnection) Connect(ctx context.Context) (loadedDatabase, error) {
ctx, _ = context.WithTimeout(ctx, 5*time.Second) // shadow parent ctx
opts := options.Client().ApplyURI(string(*moc.Conn.Connection_String))
// Our used library supports all compressors
opts.SetCompressors([]string{"zstd", "snappy", "zlib"})
ret := mongoLdb{}
if moc.SSH_Tunnel != nil {
sshc, err := moc.SSH_Tunnel.Open(ctx)
if err != nil {
return nil, err
}
opts.Dialer = sshc // interface implements DialContext()
ret.sshc = sshc
// The crypto/ssh library does not support deadlines over tcp tunnels
// Go-redis has a workaround for this, but go-mongo does not
}
//
client, err := mongo.Connect(opts)
if err != nil {
return nil, err
}
ret.client = client
// Connect() does not block for server discovery. Check that the server really
// is reachable
err = client.Ping(ctx, readpref.Primary())
if err != nil {
return nil, err
}
// We should be able to ListDatabases - there may be an authentication error
_, err = client.ListDatabaseNames(ctx, bson.D{})
if err != nil {
return nil, err
}
return &ret, nil
}

24
db_none.go Normal file
View File

@@ -0,0 +1,24 @@
package main
type noLoadedDatabase struct{}
func (n *noLoadedDatabase) DriverName() string {
return "No database selected"
}
func (n *noLoadedDatabase) Properties(bucketPath []string) (string, error) {
return "Open a database to get started...", nil
}
func (n *noLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
return nil
}
func (n *noLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil
}
func (ld *noLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (n *noLoadedDatabase) Close() {}

130
db_pebble.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/cockroachdb/pebble"
"github.com/cockroachdb/pebble/vfs"
"github.com/mappu/autoconfig"
)
type pebbleLoadedDatabase struct {
db *pebble.DB
}
func (ld *pebbleLoadedDatabase) DriverName() string {
return "Pebble"
}
func (ld *pebbleLoadedDatabase) Properties(bucketPath []string) (string, error) {
content := fmt.Sprintf("Pebble metrics: %#v", ld.db.Metrics())
return content, nil
}
func (ld *pebbleLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
ctx := context.Background()
// Load data
// pebble always uses Key + Value as the columns
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
itr, err := ld.db.NewIterWithContext(ctx, nil)
if err != nil {
return err
}
defer itr.Close()
for itr.First(); itr.Valid(); itr.Next() {
k := itr.Key()
v, err := itr.ValueAndErr()
if err != nil {
return fmt.Errorf("Failed to load data for key %q: %w", formatAny(k), err)
}
f.AddRow_PK_Data(k, k, v)
}
// Valid
f.Ready()
return nil
}
func (n *pebbleLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) error {
txn := n.db.NewBatch()
err := ApplyChanges_binColumn(
f,
func(k, v []byte) error { return txn.Set(k, v, nil) },
func(k []byte) error { return txn.Delete(k, nil) },
)
if err != nil {
return err
}
return txn.Commit(nil)
}
func (ld *pebbleLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *pebbleLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *pebbleLoadedDatabase) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &pebbleLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &pebbleLoadedDatabase{} // interface assertion
//
type pebbleConnection struct {
Type autoconfig.OneOf
Disk *struct {
Directory autoconfig.ExistingDirectory
Readonly bool
} `json:",omitempty"`
Memory *struct{} `json:",omitempty"`
}
var _ DBConnector = &pebbleConnection{} // interface assertion
func (pdc *pebbleConnection) String() string {
if pdc.Disk != nil {
return filepath.Base(string(pdc.Disk.Directory))
} else {
return `:memory:` // SQLite-style naming
}
}
func (pdc *pebbleConnection) Connect(ctx context.Context) (loadedDatabase, error) {
opts := (&pebble.Options{}).EnsureDefaults()
if pdc.Disk != nil {
opts.ReadOnly = pdc.Disk.Readonly
db, err := pebble.Open(string(pdc.Disk.Directory), opts)
if err != nil {
return nil, err
}
return &pebbleLoadedDatabase{db: db}, nil
} else {
// Memory != nil
db, err := pebble.Open("", &pebble.Options{FS: vfs.NewMem()})
if err != nil {
return nil, err
}
return &pebbleLoadedDatabase{db: db}, nil
}
}

101
db_pogreb.go Normal file
View File

@@ -0,0 +1,101 @@
package main
import (
"context"
"errors"
"fmt"
"path/filepath"
"github.com/akrylysov/pogreb"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
)
type pogrebLdb struct {
db *pogreb.DB
}
func (ld *pogrebLdb) DriverName() string {
return "Pogreb"
}
func (ld *pogrebLdb) Properties(bucketPath []string) (string, error) {
m := ld.db.Metrics()
return fmt.Sprintf("Metrics: %#v\n", m), nil
}
func (ld *pogrebLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
itn := ld.db.Items()
for {
k, v, err := itn.Next()
if err != nil {
if errors.Is(err, pogreb.ErrIterationDone) {
break
}
return err // Real error
}
f.AddRow_PK_Data(k, k, v)
}
f.Ready()
return nil
}
func (n *pogrebLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return ApplyChanges_binColumn(f, n.db.Put, n.db.Delete)
}
func (ld *pogrebLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *pogrebLdb) compactDB(sender *qt.QTreeWidgetItem, bucketPath []string) error {
info, err := ld.db.Compact()
if err != nil {
return err
}
qt.QMessageBox_Information(sender.TreeWidget().QWidget, APPNAME,
fmt.Sprintf("Compaction completed successfully.\n\nStatistics: %#v", info))
return nil
}
func (ld *pogrebLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return []contextAction{
{"Compact", ld.compactDB},
}, nil
}
func (ld *pogrebLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &pogrebLdb{} // interface assertion
var _ editableLoadedDatabase = &pogrebLdb{} // interface assertion
//
type pogrebConn struct {
Directory autoconfig.ExistingDirectory
}
var _ DBConnector = &pogrebConn{} // interface assertion
func (c *pogrebConn) String() string {
return filepath.Base(string(c.Directory))
}
func (c *pogrebConn) Connect(ctx context.Context) (loadedDatabase, error) {
db, err := pogreb.Open(string(c.Directory), nil)
if err != nil {
return nil, err
}
return &pogrebLdb{db: db}, nil
}

275
db_redis.go Normal file
View File

@@ -0,0 +1,275 @@
package main
import (
"context"
"fmt"
"strconv"
"qbolt/lexer"
"github.com/mappu/autoconfig"
"github.com/redis/go-redis/v9"
"golang.org/x/crypto/ssh"
)
type redisConnectionOptions struct {
Address autoconfig.AddressPort
Password autoconfig.Password
UseRespv3 bool `ylabel:"Use RESP v3 protocol"`
SSH_Tunnel *SSHTunnel
}
var _ DBConnector = &redisConnectionOptions{} // interface assertion
func (config *redisConnectionOptions) String() string {
return config.Address.Address
}
func (config *redisConnectionOptions) Reset() {
config.Address.Port = 6379
}
func (config *redisConnectionOptions) Connect(ctx context.Context) (loadedDatabase, error) {
ld := &redisLoadedDatabase{
currentDb: 0,
maxDb: 1,
serverVersion: "<unknown>",
}
opts := redis.Options{
Addr: fmt.Sprintf("%s:%d", config.Address.Address, config.Address.Port),
Password: string(config.Password),
UnstableResp3: config.UseRespv3,
}
if config.SSH_Tunnel != nil {
sshc, err := config.SSH_Tunnel.Open(ctx)
if err != nil {
return nil, err
}
// When redis wants to open a tcp conn, don't dial directly, dial via the SSH tunnel
// Setting 'Dialer' takes priority over Addr/Network fields.
// ssh.Client.DialContext has a matching signature to redis.Options.Dialer
opts.Dialer = sshc.DialContext
ld.sshc = sshc
}
// Patch in a hook to remember current DB after keepalive reconnection
opts.DB = 0 // Default
opts.OnConnect = func(ctx context.Context, cn *redis.Conn) error {
return cn.Select(ctx, ld.currentDb).Err()
}
// NewClient doesn't necessarily connect, so it can't throw an err
ld.db = redis.NewClient(&opts)
// Make an INFO request (mandatory)
info, err := ld.db.InfoMap(ctx).Result()
if err != nil {
return nil, fmt.Errorf("During INFO: %w", err)
}
if serverInfo, ok := info["Server"]; ok {
if v, ok := serverInfo["redis_version"]; ok {
ld.serverVersion = v
}
}
// List available databases (usually 1..16) with "0" as default
// If this fails, probably the target redis does not support multiple databases
// (e.g. Redis Cluster). Assume max=0.
if maxDatabases, err := ld.db.ConfigGet(ctx, "databases").Result(); err == nil {
// Got a result. Must parse it
m, err := strconv.ParseInt(maxDatabases["databases"], 10, 64)
if err != nil {
return nil, fmt.Errorf("During CONFIG GET databases: %v", err)
}
ld.maxDb = int(m)
}
return ld, nil
}
type redisLoadedDatabase struct {
db *redis.Client
sshc *ssh.Client
currentDb int
maxDb int
serverVersion string // populated at connection-time from INFO command
}
func (ld *redisLoadedDatabase) DriverName() string {
return "Redis " + ld.serverVersion
}
func (ld *redisLoadedDatabase) Properties(bucketPath []string) (string, error) {
ctx := context.Background()
if len(bucketPath) == 0 {
// Top-level: Show info() on main Properties tab
infostr, err := ld.db.Info(ctx).Result()
if err != nil {
return "", fmt.Errorf("Retreiving database info: %w", err)
}
return infostr, nil
} else {
return "Database " + bucketPath[0], nil
}
}
func (ld *redisLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
ctx := context.Background()
if len(bucketPath) == 0 {
// Leave data tab disabled (default behaviour)
return nil
} else if len(bucketPath) == 1 {
// One selected database
// Figure out its content
err := ld.db.Do(ctx, "SELECT", bucketPath[0]).Err()
if err != nil {
return fmt.Errorf("Switching to database %q: %w", bucketPath[0], err)
}
allKeys, err := ld.db.Keys(ctx, "*").Result()
if err != nil {
return fmt.Errorf("Listing keys in database %q: %w", bucketPath[0], err)
}
// Redis always uses Key string, Type string, Value []byte as the columns
f.SetupColumns(
[]TableColumn{&binColumn{}, &stringColumn{}, &binColumn{}},
[]string{"Key", "Type", "Value"},
)
for _, key := range allKeys {
typeName, err := ld.db.Type(ctx, key).Result()
if err != nil {
return fmt.Errorf("Loading %q/%q: %w", bucketPath[0], key, err)
}
rpos := f.AddRow()
f.SetCell(rpos, 0, key)
f.SetCell(rpos, 1, typeName)
switch typeName {
case "string":
val, err := ld.db.Get(ctx, key).Result()
if err != nil {
return fmt.Errorf("Loading %q/%q: %w", bucketPath[0], key, err)
}
f.SetCell(rpos, 2, []byte(val))
case "hash":
val, err := ld.db.HGetAll(ctx, key).Result()
if err != nil {
return fmt.Errorf("Loading %q/%q: %w", bucketPath[0], key, err)
}
// It's a map[string]string
f.SetCell(rpos, 2, []byte(formatAny(val)))
case "lists":
fallthrough
case "sets":
fallthrough
case "sorted sets":
fallthrough
case "stream":
fallthrough
default:
f.SetCell(rpos, 2, []byte("<<<other object type>>>"))
}
}
// Valid
f.Ready()
return nil
} else {
return fmt.Errorf("Unexpected nav position %q", bucketPath)
}
}
func (ld *redisLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
// ctx := context.Background()
if len(bucketPath) == 0 {
// Top-level: list of all child databases (usually 16x)
ret := make([]string, 0, ld.maxDb)
for i := 0; i < ld.maxDb; i++ {
ret = append(ret, fmt.Sprintf("%d", i))
}
return ret, nil
} else if len(bucketPath) == 1 {
// One selected database
// No child keys underneath it
return []string{}, nil
} else {
return nil, fmt.Errorf("Unexpected nav position %q", bucketPath)
}
}
func (ld *redisLoadedDatabase) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *redisLoadedDatabase) ExecQuery(query string, _ []string, resultArea *tableState) error {
ctx := context.Background()
// Need to parse the query into separate string+args fields for the protocol
fields, err := lexer.Fields(query)
if err != nil {
return fmt.Errorf("Parsing the query: %w", err)
}
fields_boxed := box_interface(fields)
ret, err := ld.db.Do(ctx, fields_boxed...).Result()
if err != nil {
return fmt.Errorf("The redis query returned an error: %w", err)
}
resultArea.SetupColumns([]TableColumn{&stringColumn{}}, []string{"Result"})
// The result is probably a single value or a string slice
switch ret := ret.(type) {
case []string:
// Multiple values
for _, single := range ret {
resultArea.AddRowData(single)
}
default:
// Single value
// Unknown object type
resultArea.AddRowData(formatAny(ret)) // formatUtf8
}
resultArea.Ready()
return nil
}
func (ld *redisLoadedDatabase) Close() {
if ld.sshc != nil {
_ = ld.sshc.Close() // TODO does this also SIGINT remote processes?
}
_ = ld.db.Close()
}
var _ loadedDatabase = &redisLoadedDatabase{} // interface assertion
var _ queryableLoadedDatabase = &redisLoadedDatabase{} // interface assertion

80
db_rosedb.go Normal file
View File

@@ -0,0 +1,80 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/mappu/autoconfig"
"github.com/rosedblabs/rosedb/v2"
)
type roseLdb struct {
db *rosedb.DB
}
func (ld *roseLdb) DriverName() string {
return "RoseDB"
}
func (ld *roseLdb) Properties(bucketPath []string) (string, error) {
stats := ld.db.Stat()
return fmt.Sprintf("Statistics: %#v\n", stats), nil
}
func (ld *roseLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
ld.db.Ascend(func(k, v []byte) (bool, error) {
f.AddRow_PK_Data(k, k, v)
return true, nil
})
f.Ready()
return nil
}
func (n *roseLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return ApplyChanges_binColumn(f, n.db.Put, n.db.Delete)
}
func (ld *roseLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *roseLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No supported actions
}
func (ld *roseLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &roseLdb{} // interface assertion
var _ editableLoadedDatabase = &roseLdb{} // interface assertion
//
type roseDBConn struct {
Directory autoconfig.ExistingDirectory
}
var _ DBConnector = &roseDBConn{} // interface assertion
func (c *roseDBConn) String() string {
return filepath.Base(string(c.Directory))
}
func (c *roseDBConn) Connect(ctx context.Context) (loadedDatabase, error) {
options := rosedb.DefaultOptions // copy
options.DirPath = string(c.Directory)
db, err := rosedb.Open(options)
if err != nil {
return nil, err
}
return &roseLdb{db: db}, nil
}

195
db_secretsvc_linux.go Normal file
View File

@@ -0,0 +1,195 @@
package main
import (
"context"
"errors"
"fmt"
"strings"
dbus "github.com/godbus/dbus/v5"
qt "github.com/mappu/miqt/qt6"
secretservice "github.com/zalando/go-keyring/secret_service"
)
type secretServiceDb struct {
svc *secretservice.SecretService
session dbus.BusObject
}
func (ld *secretServiceDb) DriverName() string {
return "FreeDesktop.org Secret Service"
}
func (ld *secretServiceDb) Properties(bucketPath []string) (string, error) {
return "", nil // No properties
}
func (ld *secretServiceDb) RenderForNav(f *tableState, bucketPath []string) error {
const (
collectionBasePath = "/org/freedesktop/secrets/collection/"
itemInterface = "org.freedesktop.Secret.Item"
serviceName = "org.freedesktop.secrets"
)
if len(bucketPath) == 0 {
// No data
} else if len(bucketPath) == 1 {
f.SetupColumns(
[]TableColumn{&stringColumn{}, &stringColumn{}, &stringColumn{}, &stringColumn{}, &stringColumn{}, &binColumn{}},
[]string{"ID", "Label", "Attributes", "ContentType", "Parameters", "Value"},
)
// Collection is selected
collection := ld.svc.GetCollection(bucketPath[0])
// Perform an empty search to find all items
allItems, err := ld.svc.SearchItems(collection, map[string]string{})
if err != nil {
return err
}
for _, item := range allItems {
//
obj := ld.svc.Object(serviceName, item)
label, err := obj.GetProperty(itemInterface + ".Label")
if err != nil {
return fmt.Errorf("Reading label %q: %w", item, err)
}
// Attributes: a map[string]string{}
// Is this optional?
// Calling attrs.String() gives a JSON representation
attrs, err := obj.GetProperty(itemInterface + ".Attributes")
if err != nil {
return fmt.Errorf("Reading attributes %q: %w", item, err)
}
secr, err := ld.svc.GetSecret(item, ld.session.Path())
if err != nil {
return fmt.Errorf("Reading secret %q: %w", item, err)
}
f.AddRowData(
string(item), // ID
label.Value().(string), // Label
attrs.String(), // Attributes (JSON)
secr.ContentType, // ContentType
string(secr.Parameters), // Parameters
secr.Value, // Value - []byte
)
}
// Valid
f.Ready()
}
return nil
}
func (ld *secretServiceDb) listCollections() ([]string, error) {
const (
serviceName = "org.freedesktop.secrets"
servicePath = "/org/freedesktop/secrets"
collectionsInterface = "org.freedesktop.Secret.Service.Collections"
collectionBasePath = "/org/freedesktop/secrets/collection/"
)
obj := ld.svc.Conn.Object(serviceName, servicePath)
val, err := obj.GetProperty(collectionsInterface)
if err != nil {
return nil, err
}
paths := val.Value().([]dbus.ObjectPath)
ret := make([]string, 0, len(paths))
for _, p := range paths {
// They are expected to have {collectionBasePath} as prefix
colName := string(p)
if strings.HasPrefix(colName, collectionBasePath) {
colName = colName[len(collectionBasePath):]
}
ret = append(ret, colName)
}
return ret, nil
}
func (ld *secretServiceDb) NavChildren(bucketPath []string) ([]string, error) {
if len(bucketPath) == 0 {
return ld.listCollections()
}
return []string{}, nil // No children
}
func (ld *secretServiceDb) newCollection(sender *qt.QTreeWidgetItem, bucketPath []string) error {
name := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new collection:")
if name == "" {
return nil // cancelled
}
_, err := ld.svc.CreateCollection(name)
return err
}
func (ld *secretServiceDb) unlockKeychain(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if len(bucketPath) != 1 {
return errors.New("Invalid selection")
}
coll := ld.svc.GetCollection(bucketPath[0])
return ld.svc.Unlock(coll.Path())
}
func (ld *secretServiceDb) NavContext(bucketPath []string) ([]contextAction, error) {
if len(bucketPath) == 0 {
return []contextAction{
contextAction{"Create collection...", ld.newCollection},
}, nil
} else if len(bucketPath) == 1 {
return []contextAction{
contextAction{"Unlock", ld.unlockKeychain},
}, nil
} else {
return nil, nil // Unreachable
}
}
func (ld *secretServiceDb) Close() {
_ = ld.svc.Close(ld.session)
}
var _ loadedDatabase = &secretServiceDb{} // interface assertion
//
type secretServiceConnection struct {
}
var _ DBConnector = &secretServiceConnection{} // interface assertion
func (ssc *secretServiceConnection) String() string {
return "dbus://SessionBus/org.freedesktop.secrets"
}
func (ssc *secretServiceConnection) Connect(ctx context.Context) (loadedDatabase, error) {
svc, err := secretservice.NewSecretService()
if err != nil {
return nil, err
}
session, err := svc.OpenSession()
if err != nil {
return nil, err
}
return &secretServiceDb{svc: svc, session: session}, nil
}

25
db_secretsvc_other.go Normal file
View File

@@ -0,0 +1,25 @@
//go:build !linux
// +build !linux
package main
import (
"context"
"errors"
"github.com/mappu/autoconfig"
)
type secretServiceConnection struct {
H1 autoconfig.Header `ylabel:"Not supported on this operating system"`
}
var _ DBConnector = &secretServiceConnection{} // interface assertion
func (ssc *secretServiceConnection) String() string {
return ""
}
func (ssc *secretServiceConnection) Connect(ctx context.Context) (loadedDatabase, error) {
return nil, "", errors.New("Not supported on this operating system")
}

509
db_sqlite.go Normal file
View File

@@ -0,0 +1,509 @@
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"path/filepath"
"reflect"
"strings"
"qbolt/sqliteclidriver"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
mattn_sqlite3 "github.com/mattn/go-sqlite3"
)
const (
sqliteTablesCaption = "Tables"
)
type sqliteLoadedDatabase struct {
db *sql.DB
}
func (ld *sqliteLoadedDatabase) DriverName() string {
if _, ok := ld.db.Driver().(*sqliteclidriver.SCDriver); ok {
return "SQLite (sqliteclidriver)"
}
ver1, _, _ := mattn_sqlite3.Version()
return "SQLite " + ver1
}
func (ld *sqliteLoadedDatabase) Properties(bucketPath []string) (string, error) {
if len(bucketPath) == 0 || len(bucketPath) == 1 {
return "Please select...", nil // No properties
} else {
tableName := bucketPath[1]
// Get some basic properties
r := ld.db.QueryRow(`SELECT sql FROM sqlite_schema WHERE name = ?;`, tableName)
var schemaStmt string
err := r.Scan(&schemaStmt)
if err != nil {
schemaStmt = fmt.Sprintf("* Failed to describe table %q: %s", tableName, err.Error())
}
// Display table properties
return fmt.Sprintf("Selected table %q\n\nSchema:\n\n%s", tableName, schemaStmt), nil
}
}
func (ld *sqliteLoadedDatabase) RenderForNav(f *tableState, bucketPath []string) error {
if len(bucketPath) == 0 {
return nil
} else if len(bucketPath) == 1 {
// Category (tables, ...)
return nil
} else if len(bucketPath) == 2 && bucketPath[0] == sqliteTablesCaption {
// Render for specific table
tableName := bucketPath[1]
// Select count(*) so we know to display a warning if there are too many entries
// TODO
// Select * with small limit
datar, err := ld.db.Query(`SELECT rowid, * FROM [` + tableName + `]`) // WARNING can't prepare this parameter, but it comes from the DB (trusted)
if err != nil {
return fmt.Errorf("Failed to load data for table %q: %w", tableName, err)
}
defer datar.Close()
err = populateRows(datar, f, true)
if err != nil {
return err
}
// We successfully populated the data grid
f.Ready()
return nil
} else {
// ??? unknown
return errors.New("?")
}
}
func (ld *sqliteLoadedDatabase) sqliteGetColumnNamesForTable(tableName string) ([]string, error) {
colr, err := ld.db.Query(`SELECT name FROM pragma_table_info( ? )`, tableName)
if err != nil {
return nil, fmt.Errorf("Query: %w", err)
}
defer colr.Close()
var ret []string
for colr.Next() {
var columnName string
err = colr.Scan(&columnName)
if err != nil {
return nil, fmt.Errorf("Scan: %w", colr.Err())
}
ret = append(ret, columnName)
}
if colr.Err() != nil {
return nil, colr.Err()
}
return ret, nil
}
func populateRows(rr *sql.Rows, dest *tableState, firstColumnIsExtraRowID bool) error {
numColumns := len(dest.columns)
var pfields []interface{} = nil
cNames, err := rr.Columns()
if err != nil {
return err
}
cTypes, err := rr.ColumnTypes()
if err != nil {
return err
}
if len(cNames) != len(cTypes) {
return errors.New("unexpected column metadata mismatch") // assert
}
numColumns = len(cNames)
rrMakeCtypes := []TableColumn{}
for i := 0; i < len(cTypes); i++ {
if firstColumnIsExtraRowID && i == 0 {
pfields = append(pfields, new(int64))
continue
}
nullable, ok := cTypes[i].Nullable()
if !ok {
return errors.New("can't tell if column is nullable?")
}
// TODO support all SQLite column names/type-affinities
// TODO support nullable variants for all types here
// @ref https://www.sqlite.org/datatype3.html
switch cTypes[i].DatabaseTypeName() {
case "BLOB":
// Binary column
rrMakeCtypes = append(rrMakeCtypes, &binColumn{})
pfields = append(pfields, new([]byte))
case "INTEGER":
if nullable {
rrMakeCtypes = append(rrMakeCtypes, &sqlNullInt64Column{})
pfields = append(pfields, new(sql.NullInt64))
} else {
rrMakeCtypes = append(rrMakeCtypes, &int64Column{})
pfields = append(pfields, new(int64))
}
default:
if nullable {
rrMakeCtypes = append(rrMakeCtypes, &sqlNullStringColumn{})
pfields = append(pfields, new(sql.NullString))
} else {
rrMakeCtypes = append(rrMakeCtypes, &stringColumn{})
pfields = append(pfields, new(string))
}
}
}
// Resetup table
if firstColumnIsExtraRowID && len(cNames) > 0 {
// Real SQLite driver: gives us back names even if 0 rows
// sqliteclidriver: Gives back no names if there were 0 rows
cNames = cNames[1:]
}
dest.SetupColumns(rrMakeCtypes, cNames)
for rr.Next() {
err := rr.Scan(pfields...)
if err != nil {
return fmt.Errorf("Scan: %w", err)
}
rpos := dest.AddRow()
if firstColumnIsExtraRowID {
dest.SetRowPrimaryKey(rpos, int64_to_binary8(*(pfields[0].(*int64))))
for i := 1; i < numColumns; i += 1 { // skip first column
interior := reflect.ValueOf(pfields[i]).Elem().Interface()
dest.SetCell(rpos, i-1, interior)
}
} else {
// all columns
for i := 0; i < numColumns; i += 1 {
interior := reflect.ValueOf(pfields[i]).Elem().Interface()
dest.SetCell(rpos, i, interior)
}
}
}
return rr.Err()
}
func (ld *sqliteLoadedDatabase) ExecQuery(query string, _ []string, resultArea *tableState) error {
rr, err := ld.db.Query(query)
if err != nil {
return err
}
defer rr.Close()
err = populateRows(rr, resultArea, false)
if err != nil {
return err
}
resultArea.Ready()
return nil
}
type RowQueryContexter interface {
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func (n *sqliteLoadedDatabase) ApplyChanges(f *tableState, bucketPath []string) (retErr error) {
if len(bucketPath) != 2 {
return errors.New("invalid selection")
}
tableName := bucketPath[1]
ctx := context.Background()
tx, err := n.db.BeginTx(ctx, nil)
if err != nil {
return err
}
var commitOK bool = false
defer func() {
if !commitOK {
err := tx.Rollback()
if err != nil {
retErr = err
}
}
}()
// SQLite can only LIMIT 1 on update/delete if it was compiled with
// SQLITE_ENABLE_UPDATE_DELETE_LIMIT, which isn't the case for the mattn
// cgo library
// Skip that, and just rely on primary key uniqueness
// Edit
for aRow, editcells := range f.updateRows {
stmt := `UPDATE [` + tableName + `] SET `
params := []interface{}{}
for ct, cell := range editcells {
if ct > 0 {
stmt += `, `
}
stmt += `[` + f.columnLabels[cell] + `] = ?`
params = append(params, f.columns[cell].GetCell(aRow))
}
stmt += ` WHERE [rowid] = ?`
// Update by primary key (stored separately)
pkVal := binary8_to_int64(f.primaryKeys[aRow])
params = append(params, pkVal)
_, err = tx.ExecContext(ctx, stmt, params...)
if err != nil {
return fmt.Errorf("Updating row %q: %w", pkVal, err)
}
}
// Delete by key (affects rowids after re-render)
for aRow, _ := range f.deleteRows {
pkVal := binary8_to_int64(f.primaryKeys[aRow])
stmt := `DELETE FROM [` + tableName + `] WHERE [rowid] = ?`
_, err = tx.ExecContext(ctx, stmt, pkVal)
if err != nil {
return fmt.Errorf("Deleting row %q: %w", pkVal, err)
}
}
// Insert all new entries
for aRow, _ := range f.insertRows {
stmt := `INSERT INTO [` + tableName + `] ([` + strings.Join(f.columnLabels, `], [`) + `]) VALUES (`
params := []interface{}{}
for colid := 0; colid < len(f.columnLabels); colid++ {
if colid > 0 {
stmt += `, `
}
stmt += "?"
params = append(params, f.columns[colid].GetCell(aRow))
}
stmt += `)`
_, err = tx.ExecContext(ctx, stmt, params...)
if err != nil {
return fmt.Errorf("Inserting row: %w", err)
}
}
err = tx.Commit()
if err != nil {
return err
}
commitOK = true // no need for rollback
return nil
}
func (ld *sqliteLoadedDatabase) NavChildren(bucketPath []string) ([]string, error) {
if len(bucketPath) == 0 {
// The top-level children are always:
return []string{sqliteTablesCaption}, nil
}
if len(bucketPath) == 1 && bucketPath[0] == sqliteTablesCaption {
// The sequence and stat1 tables are not marked as hidden tables
// They are created automatically when using (A) autoincrement and (B) ???.
rr, err := ld.db.Query(`SELECT name FROM sqlite_master WHERE type='table' AND name <> 'sqlite_sequence' AND name <> 'sqlite_stat1' ORDER BY name ASC;`)
if err != nil {
return nil, err
}
defer rr.Close()
var gather []string
for rr.Next() {
var tableName string
err = rr.Scan(&tableName)
if err != nil {
return nil, err
}
gather = append(gather, tableName)
}
if rr.Err() != nil {
return nil, rr.Err()
}
return gather, nil
}
if len(bucketPath) == 2 {
return nil, nil // Never any deeper children
}
return nil, fmt.Errorf("unknown nav path %#v", bucketPath)
}
func (ld *sqliteLoadedDatabase) NavContext(bucketPath []string) (ret []contextAction, err error) {
if len(bucketPath) == 0 {
ret = append(ret, contextAction{"Compact database", ld.CompactDatabase})
ret = append(ret, contextAction{"Export backup...", ld.ExportBackup})
}
if len(bucketPath) == 2 {
ret = append(ret, contextAction{"Drop table", ld.DropTable})
}
return
}
func (ld *sqliteLoadedDatabase) CompactDatabase(sender *qt.QTreeWidgetItem, bucketPath []string) error {
_, err := ld.db.Exec(`VACUUM;`)
return err
}
func (ld *sqliteLoadedDatabase) ExportBackup(sender *qt.QTreeWidgetItem, bucketPath []string) error {
// Popup for output file
savePath := qt.QFileDialog_GetSaveFileName4(sender.TreeWidget().QWidget, "Save backup as...", "", "SQLite database (*.db *.db3 *.sqlite *.sqlite3);;All files (*)")
if savePath == "" {
return nil // cancelled
}
_, err := ld.db.Exec(`VACUUM INTO ?`, savePath)
return err
}
func (ld *sqliteLoadedDatabase) DropTable(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if len(bucketPath) != 2 {
return errors.New("Invalid selection")
}
//
tableName := bucketPath[1]
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to drop the table %q?", tableName)) {
return nil // cancelled
}
_, err := ld.db.Exec(`DROP TABLE "` + tableName + `"`) // WARNING can't prepare this parameter, but it comes from the DB (trusted)
return err
}
func (ld *sqliteLoadedDatabase) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &sqliteLoadedDatabase{} // interface assertion
var _ editableLoadedDatabase = &sqliteLoadedDatabase{} // interface assertion
var _ queryableLoadedDatabase = &sqliteLoadedDatabase{} // interface assertion
//
type sqliteConnection struct {
Type autoconfig.OneOf
Disk *struct {
Database autoconfig.ExistingFile `yfilter:"SQLite database (*.db *.db3 *.sqlite *.sqlite3);;All files (*)"`
CliDriver bool `ylabel:"Use experimental CLI driver"`
} `json:",omitempty"`
Memory *struct{} `json:",omitempty"`
SSH *struct {
SSHServer *SSHTunnel
Database string
} `json:",omitempty"`
}
var _ DBConnector = &sqliteConnection{} // interface assertion
func (sc *sqliteConnection) String() string {
if sc.Disk != nil {
return filepath.Base(string(sc.Disk.Database))
} else if sc.Memory != nil {
return `:memory:`
} else if sc.SSH != nil {
return filepath.Base(string(sc.SSH.Database)) + " (SSH)"
}
return "" // unreachable
}
func (sc *sqliteConnection) Connect(ctx context.Context) (loadedDatabase, error) {
if sc.Disk != nil {
driver := "sqlite3"
if sc.Disk.CliDriver {
driver = "sqliteclidriver"
}
db, err := sql.Open(driver, string(sc.Disk.Database))
if err != nil {
return nil, err
}
return &sqliteLoadedDatabase{db: db}, nil
} else if sc.Memory != nil { // memory
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
return nil, err
}
return &sqliteLoadedDatabase{db: db}, nil
} else if sc.SSH != nil {
if sc.SSH.SSHServer == nil {
return nil, errors.New("Invalid configuration")
}
cl, err := sc.SSH.SSHServer.Open(ctx)
if err != nil {
return nil, err
}
db := sqliteclidriver.OpenSSH(cl, sc.SSH.Database)
return &sqliteLoadedDatabase{db: db}, nil
} else {
return nil, errors.New("Invalid configuration")
}
}

185
db_sshagent.go Normal file
View File

@@ -0,0 +1,185 @@
package main
import (
"context"
"fmt"
"net"
"net/netip"
"os"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"golang.org/x/crypto/ssh/agent"
)
type sshAgentLdb struct {
conn agent.ExtendedAgent
}
func (ld *sshAgentLdb) DriverName() string {
return "ssh-agent"
}
func (ld *sshAgentLdb) Properties(bucketPath []string) (string, error) {
return "", nil
}
func (ld *sshAgentLdb) RenderForNav(f *tableState, bucketPath []string) error {
keys, err := ld.conn.List()
if err != nil {
return err
}
f.SetupColumns([]TableColumn{&stringColumn{}, &stringColumn{}, &binColumn{}}, []string{"Comment", "Type", "Public Key"})
for _, key := range keys {
// The publicKey blob is the effective primary-key for DB manipulation
f.AddRow_PK_Data(key.Blob, key.Comment, key.Format, key.Blob)
}
f.Ready()
return nil
}
func (ld *sshAgentLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *sshAgentLdb) lockPrompt(sender *qt.QTreeWidgetItem, bucketPath []string) error {
parent := sender.TreeWidget().QWidget
props := encryptionKey{}
autoconfig.OpenDialog(&props, parent, "Enter lock password...", func() {
key, err := props.Get()
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, err.Error())
return
}
if len(key) == 0 {
// Cancelled
return
}
err = ld.conn.Lock(key)
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, "Locking SSH agent: "+err.Error())
return
}
})
return nil // n.b. refreshes now
}
func (ld *sshAgentLdb) unlockPrompt(sender *qt.QTreeWidgetItem, bucketPath []string) error {
parent := sender.TreeWidget().QWidget
props := encryptionKey{}
autoconfig.OpenDialog(&props, parent, "Enter unlock password...", func() {
key, err := props.Get()
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, err.Error())
return
}
if len(key) == 0 {
// Cancelled
return
}
err = ld.conn.Unlock(key)
if err != nil {
qt.QMessageBox_Warning(parent, APPNAME, "Unlocking SSH agent: "+err.Error())
return
}
// Trigger a refresh
})
return nil // n.b. refreshes now, which may cause double-error if we are still locked
}
func (ld *sshAgentLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return []contextAction{
{"Lock agent...", ld.lockPrompt},
{"Unlock agent...", ld.unlockPrompt},
}, nil
}
func (ld *sshAgentLdb) Close() {
// noop
}
var _ loadedDatabase = &sshAgentLdb{} // interface assertion
//
type sshAgentConn struct {
Type autoconfig.OneOf
Unix *autoconfig.ExistingFile `json:",omitempty"`
TCP *autoconfig.AddressPort `json:",omitempty"`
}
var _ DBConnector = &sshAgentConn{} // interface assertion
func (c *sshAgentConn) String() string {
return "SSH Agent" // TODO could be improved
}
func (c *sshAgentConn) Reset() {
if sshAuthSock := os.Getenv("SSH_AUTH_SOCK"); sshAuthSock != "" {
if _, err := os.Stat(sshAuthSock); err == nil {
// File
c.Type = "Unix"
val := autoconfig.ExistingFile(sshAuthSock)
c.Unix = &val
} else if props, err := netip.ParseAddrPort(sshAuthSock); err == nil {
// IP:Port
c.Type = "TCP"
val := autoconfig.AddressPort{
Address: props.Addr().String(),
Port: int(props.Port()),
}
c.TCP = &val
} else {
// Can't parse env var
}
}
}
func (c *sshAgentConn) getAgent() (agent.ExtendedAgent, error) {
if c.Unix != nil {
conn, err := net.Dial("unix", string(*c.Unix))
if err != nil {
return nil, fmt.Errorf("Connecting to SSH agent %q: %w", c.Unix, err)
}
return agent.NewClient(conn), nil
} else if c.TCP != nil {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.TCP.Address, c.TCP.Port))
if err != nil {
return nil, fmt.Errorf("Connecting to SSH agent %q: %w", c.TCP.String(), err)
}
return agent.NewClient(conn), nil
} else {
return nil, fmt.Errorf("No connection details specified")
}
}
func (c *sshAgentConn) Connect(ctx context.Context) (loadedDatabase, error) {
agent, err := c.getAgent()
if err != nil {
return nil, err
}
return &sshAgentLdb{conn: agent}, nil
}

121
db_starskey.go Normal file
View File

@@ -0,0 +1,121 @@
package main
import (
"context"
"fmt"
"path/filepath"
"github.com/mappu/autoconfig"
"github.com/starskey-io/starskey"
)
type starskeyLdb struct {
db *starskey.Starskey
}
func (ld *starskeyLdb) DriverName() string {
return "Starskey"
}
func (ld *starskeyLdb) Properties(bucketPath []string) (string, error) {
return "", nil // No properties
}
func (ld *starskeyLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
// Starskey always uses Key + Value as the columns
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
// It's possible to create a transaction in Starskey, but you can't enumerate keys
// within the transaction - doesn't really help us
var allKeys [][]byte
_, err := ld.db.FilterKeys(func(key []byte) bool {
allKeys = append(allKeys, slice_dup(key))
return false // don't get value in here
})
if err != nil {
return fmt.Errorf("FilterKeys: %w", err)
}
for _, key := range allKeys {
val, err := ld.db.Get(key)
if err != nil {
// We get <nil, nil> if not found, so any error is a real error
return fmt.Errorf("Reading key %q: %w", string(key), err)
}
if val == nil {
// Key not found
// The hack to use FilterKeys() means this can happen if the key is
// pointing to a tombstone
continue
}
f.AddRow_PK_Data(key, key, val)
}
// Valid
f.Ready()
return nil
}
func (n *starskeyLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return ApplyChanges_binColumn(f, n.db.Put, n.db.Delete)
}
func (ld *starskeyLdb) NavChildren(bucketPath []string) ([]string, error) {
return []string{}, nil // No children
}
func (ld *starskeyLdb) NavContext(bucketPath []string) ([]contextAction, error) {
return nil, nil // No special actions are supported
}
func (ld *starskeyLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &starskeyLdb{} // interface assertion
var _ editableLoadedDatabase = &starskeyLdb{} // interface assertion
//
type starskeyConnection struct {
Directory autoconfig.ExistingDirectory
Compression autoconfig.EnumList `yenum:"No compression;;Snappy;;S2"`
}
var _ DBConnector = &starskeyConnection{} // interface assertion
func (pdc *starskeyConnection) String() string {
return filepath.Base(string(pdc.Directory))
}
func (pdc *starskeyConnection) Connect(ctx context.Context) (loadedDatabase, error) {
cfg := starskey.Config{
Permission: 0755,
Directory: string(pdc.Directory),
FlushThreshold: (1024 * 1024) * 24, // Upstream default (24 MiB)
MaxLevel: 3, // Upstream default
SizeFactor: 10, // Upstream default
}
if pdc.Compression == 0 {
} else if pdc.Compression == 1 {
cfg.Compression = true
cfg.CompressionOption = starskey.SnappyCompression
} else if pdc.Compression == 2 {
cfg.Compression = true
cfg.CompressionOption = starskey.S2Compression
}
db, err := starskey.Open(&cfg)
if err != nil {
return nil, err
}
return &starskeyLdb{db: db}, nil
}

254
db_voiddb_linux.go Normal file
View File

@@ -0,0 +1,254 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/mappu/autoconfig"
qt "github.com/mappu/miqt/qt6"
"github.com/voidDB/voidDB"
"github.com/voidDB/voidDB/common"
"github.com/voidDB/voidDB/cursor"
)
type voidLdb struct {
db *voidDB.Void
multiKeyspace bool
}
func (ld *voidLdb) DriverName() string {
return "VoidDB"
}
func (ld *voidLdb) Properties(bucketPath []string) (string, error) {
return "", nil
}
// cursor gets a VoidDB cursor for the given transaction and bucketPath.
func (ld *voidLdb) cursor(txn *voidDB.Txn, bucketPath []string) (*cursor.Cursor, error) {
if ld.multiKeyspace {
if len(bucketPath) == 0 {
return nil, errors.New("No data at VoidDB root keyspace when in multi-keyspace mode")
}
return txn.OpenCursor([]byte(bucketPath[0]))
} else {
if len(bucketPath) != 0 {
return nil, errors.New("Invalid navigation")
}
return txn.Cursor, nil // The Txn embeds a new cursor over the default keyspace
}
}
func (ld *voidLdb) RenderForNav(f *tableState, bucketPath []string) error {
// Load data
f.SetupColumns([]TableColumn{&binColumn{}, &binColumn{}}, []string{"Key", "Value"})
if ld.multiKeyspace && len(bucketPath) == 0 {
// Nothing to see/do in the default keyspace, sorry
// Exit without calling .Ready()
return nil
}
err := ld.db.View(func(tx *voidDB.Txn) error {
cur, err := ld.cursor(tx, bucketPath)
if err != nil {
return err
}
for {
k, v, err := cur.GetNext()
if err != nil {
if errors.Is(err, common.ErrorNotFound) {
break // Reached end
}
return fmt.Errorf("GetNext: %w", err) // Some other real error
}
f.AddRow_PK_Data(k, k, v)
}
return nil
})
if err != nil {
return err
}
f.Ready()
return nil
}
func (n *voidLdb) ApplyChanges(f *tableState, bucketPath []string) error {
return n.db.Update(true, func(tx *voidDB.Txn) error {
cur, err := n.cursor(tx, bucketPath)
if err != nil {
return err
}
return ApplyChanges_binColumn(
f,
cur.Put,
func(k []byte) error {
// To delete keys in VoidDB, first move the cursor to the target
_, err := cur.Get(k)
if err != nil {
return fmt.Errorf("Finding key %q for deletion: %w", formatUtf8(k), err)
}
return cur.Del()
},
)
})
}
func (ld *voidLdb) NavChildren(bucketPath []string) ([]string, error) {
if len(bucketPath) == 0 && ld.multiKeyspace {
// In multi-db mode, the root keyspace is filled with pointers to
// other keyspaces
// Iterate the names
var rootNames []string
err := ld.db.View(func(tx *voidDB.Txn) error {
for {
k, _, err := tx.GetNext()
if err != nil {
if errors.Is(err, common.ErrorNotFound) {
return nil // Reached end
}
return fmt.Errorf("GetNext: %w", err) // Some other real error
}
rootNames = append(rootNames, string(k))
}
})
if err != nil {
return nil, err
}
return rootNames, nil
}
return []string{}, nil // No children
}
// addKeyspace is valid only in multi-keyspace mode.
func (ld *voidLdb) addKeyspace(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if len(bucketPath) != 0 {
return errors.New("Can only add keyspaces at the root")
}
ksName := qt.QInputDialog_GetText(sender.TreeWidget().QWidget, APPNAME, "Enter a name for the new keyspace:")
if ksName == "" {
return nil // cancel
}
return ld.db.Update(true, func(tx *voidDB.Txn) error {
_, err := tx.OpenCursor([]byte(ksName))
return err
})
}
// deleteKeyspace is valid only in multi-keyspace mode. It deletes the target
// keyspace and all its content.
func (ld *voidLdb) deleteKeyspace(sender *qt.QTreeWidgetItem, bucketPath []string) error {
if len(bucketPath) != 1 {
return errors.New("Invalid navigation")
}
if !vcl_confirm_dialog(sender.TreeWidget().QWidget, fmt.Sprintf("Are you sure you want to delete the keyspace %q?", bucketPath[0])) {
return nil // cancelled
}
return ld.db.Update(true, func(tx *voidDB.Txn) error {
// Open the keyspace
cur, err := tx.OpenCursor([]byte(bucketPath[0]))
if err != nil {
return err
}
// Delete all its content
for {
_, _, err := cur.GetNext()
if err != nil {
if errors.Is(err, common.ErrorNotFound) {
break // Done
}
return fmt.Errorf("GetNext: %w", err) // real error
}
err = cur.Del()
if err != nil {
return err
}
}
// Delete the root-level keyspace pointer
// In VoidDB you delete by moving the cursor to the target item first
_, err = tx.Get([]byte(bucketPath[0]))
if err != nil {
return err
}
return tx.Del()
})
}
func (ld *voidLdb) NavContext(bucketPath []string) ([]contextAction, error) {
if ld.multiKeyspace {
if len(bucketPath) == 0 {
return []contextAction{
{"Add keyspace...", ld.addKeyspace},
}, nil
} else {
return []contextAction{
{"Delete keyspace...", ld.deleteKeyspace},
}, nil
}
}
return nil, nil // No supported actions
}
func (ld *voidLdb) Close() {
_ = ld.db.Close()
}
var _ loadedDatabase = &voidLdb{} // interface assertion
var _ editableLoadedDatabase = &voidLdb{} // interface assertion
//
type voidDBConn struct {
Path autoconfig.ExistingFile
MultiKeyspace bool `ylabel:"Use multiple keyspaces"`
}
var _ DBConnector = &voidDBConn{} // interface assertion
func (c *voidDBConn) String() string {
return filepath.Base(string(c.Path))
}
func (c *voidDBConn) Connect(ctx context.Context) (loadedDatabase, error) {
const capacity = 1 << 40 // 1 TiB max size before returning ErrorFull
openFunc := voidDB.OpenVoid
if _, err := os.Stat(string(c.Path)); os.IsNotExist(err) {
openFunc = voidDB.NewVoid
}
db, err := openFunc(string(c.Path), capacity)
if err != nil {
return nil, err
}
return &voidLdb{db: db, multiKeyspace: c.MultiKeyspace}, nil
}

25
db_voiddb_other.go Normal file
View File

@@ -0,0 +1,25 @@
//go:build !linux
// +build !linux
package main
import (
"context"
"errors"
"github.com/mappu/autoconfig"
)
type voidDBConn struct {
H1 autoconfig.Header `ylabel:"Not supported on this operating system"`
}
var _ DBConnector = &voidDBConn{} // interface assertion
func (ssc *voidDBConn) String() {
return ""
}
func (ssc *voidDBConn) Connect(ctx context.Context) (loadedDatabase, error) {
return nil, errors.New("Not supported on this operating system")
}

132
debconf/debconf.go Normal file
View File

@@ -0,0 +1,132 @@
package debconf
import (
"bufio"
"fmt"
"io"
"strings"
)
const (
DefaultConfigDat = `/var/cache/debconf/config.dat`
DefaultPasswordsDat = `/var/cache/debconf/passwords.dat`
DefaultTemplatesDat = `/var/cache/debconf/templates.dat`
)
type Entry struct {
Name string
Properties [][2]string
}
type Application struct {
Name string
Entries []Entry
}
type Database struct {
Entries []Application
AllColumnNames []string
}
func (d *Database) FindApplicationByName(search string) (*Application, bool) {
for idx, app := range d.Entries {
if app.Name == search {
return &d.Entries[idx], true // TODO faster lookup?
}
}
return nil, false
}
func Parse(r io.Reader) (*Database, error) {
sc := bufio.NewScanner(r)
var entries []Entry
var wip Entry
var linenum int = 0
knownColumnNames := map[string]struct{}{
"Name": struct{}{},
}
var discoveredColumns []string = []string{"Name"}
for sc.Scan() {
linenum++
line := sc.Text()
if line == "" {
if wip.Name != "" {
entries = append(entries, wip)
wip = Entry{}
}
continue
}
if line[0] == ' ' {
// continuation of last text entry
if len(wip.Properties) == 0 {
return nil, fmt.Errorf("Continuation of nonexistent entry on line %d", linenum)
}
wip.Properties[len(wip.Properties)-1][1] += line[1:]
} else {
// New pair on current element
key, rest, ok := strings.Cut(line, `:`)
if !ok {
return nil, fmt.Errorf("Missing : on line %d", linenum)
}
if _, ok := knownColumnNames[key]; !ok {
knownColumnNames[key] = struct{}{}
discoveredColumns = append(discoveredColumns, key)
}
rest = strings.TrimLeft(rest, " \t")
if key == `Name` {
wip.Name = rest
} else {
wip.Properties = append(wip.Properties, [2]string{key, rest})
}
}
}
if sc.Err() != nil {
return nil, sc.Err()
}
if wip.Name != "" {
entries = append(entries, wip)
}
// Group all entries by Application
apps := make([]Application, 0)
appIndexes := make(map[string]int, 0)
for _, entry := range entries {
spos := strings.Index(entry.Name, `/`)
appName := entry.Name[0:spos]
idx, ok := appIndexes[appName]
if !ok {
appIndexes[appName] = len(apps)
apps = append(apps, Application{
Name: appName,
Entries: []Entry{entry},
})
} else {
tmp := apps[idx]
tmp.Entries = append(tmp.Entries, entry)
apps[idx] = tmp
}
}
return &Database{
Entries: apps,
AllColumnNames: discoveredColumns,
}, nil
}

30
debconf/debconf_test.go Normal file
View File

@@ -0,0 +1,30 @@
package debconf
import (
"os"
"testing"
)
func TestDebconfParse(t *testing.T) {
src, err := os.Open(DefaultConfigDat)
if err != nil {
if os.IsNotExist(err) {
t.Skip(err)
}
t.Fatal(err)
}
defer src.Close()
db, err := Parse(src)
if err != nil {
t.Fatalf("Parse: %v", err)
}
if len(db.Entries) == 0 {
t.Errorf("expected >0 entries, got %v", len(db.Entries))
}
if len(db.AllColumnNames) == 0 {
t.Errorf("expected >0 column names, got %v", len(db.AllColumnNames))
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

BIN
doc/screenshot-000.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

View File

@@ -1,82 +0,0 @@
package main
import (
"math/rand"
"os"
bolt "go.etcd.io/bbolt"
)
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)
}

View File

@@ -1,6 +1,6 @@
package main
//go:generate miqt-rcc -Input "resources.qrc" -OutputGo "resources.go" -OutputRcc "resources.rcc" -Qt6
//go:generate miqt-rcc -Input "embed.qrc" -OutputGo "embed.go" -OutputRcc "embed.rcc" -Qt6
import (
"embed"
@@ -8,7 +8,7 @@ import (
qt "github.com/mappu/miqt/qt6"
)
//go:embed resources.rcc
//go:embed embed.rcc
var _resourceRcc []byte
func init() {

53
embed.qrc Normal file
View File

@@ -0,0 +1,53 @@
<RCC>
<qresource prefix="/">
<file>assets/add.png</file>
<file>assets/arrow_refresh.png</file>
<file>assets/chart_bar.png</file>
<file>assets/compress.png</file>
<file>assets/connect.png</file>
<file>assets/database.png</file>
<file>assets/database_add.png</file>
<file>assets/database_delete.png</file>
<file>assets/database_key.png</file>
<file>assets/database_lightning.png</file>
<file>assets/database_save.png</file>
<file>assets/delete.png</file>
<file>assets/disconnect.png</file>
<file>assets/help.png</file>
<file>assets/key.png</file>
<file>assets/lightning.png</file>
<file>assets/lightning_go.png</file>
<file>assets/note_delete.png</file>
<file>assets/page_key.png</file>
<file>assets/pencil.png</file>
<file>assets/pencil_add.png</file>
<file>assets/pencil_delete.png</file>
<file>assets/pencil_go.png</file>
<file>assets/resultset_next.png</file>
<file>assets/table.png</file>
<file>assets/table_add.png</file>
<file>assets/table_delete.png</file>
<file>assets/table_save.png</file>
<file>assets/vendor_buntdb.png</file>
<file>assets/vendor_cockroach.png</file>
<file>assets/vendor_debian.png</file>
<file>assets/vendor_dgraph.png</file>
<file>assets/vendor_etcd.png</file>
<file>assets/vendor_freedesktop.png</file>
<file>assets/vendor_github.png</file>
<file>assets/vendor_leveldb.png</file>
<file>assets/vendor_lmdb.png</file>
<file>assets/vendor_lotus.png</file>
<file>assets/vendor_mongodb.png</file>
<file>assets/vendor_mysql.png</file>
<file>assets/vendor_pogreb.png</file>
<file>assets/vendor_qt.png</file>
<file>assets/vendor_redis.png</file>
<file>assets/vendor_riak.png</file>
<file>assets/vendor_rosedb.png</file>
<file>assets/vendor_sqlite.png</file>
<file>assets/vendor_ssh.png</file>
<file>assets/vendor_starskey.png</file>
<file>assets/vendor_voiddb.png</file>
</qresource>
</RCC>

BIN
embed.rcc Normal file

Binary file not shown.

167
export.go
View File

@@ -1,167 +0,0 @@
package main
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/fs"
"os"
"path"
"strings"
)
func Bolt_ExportDatabaseToZip(dbpath, zippath string) error {
db, err := Bolt_Open(true, dbpath)
if err != nil {
return fmt.Errorf("Error opening database: %w", err)
}
defer db.Close()
fh, err := os.OpenFile(zippath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("Error opening output file: %w", err)
}
defer fh.Close()
zw := zip.NewWriter(fh)
// Filenames in zip files cannot contain `/` characters. Mangle it
safename := func(n string) string {
return strings.ReplaceAll(string(n), `/`, `__`)
}
var process func(currentPath []string) error
process = func(currentPath []string) error {
return Bolt_ListBuckets(db, currentPath, func(bucket string) error {
// Create entry for our own bucket
ourBucket := zip.FileHeader{
Name: path.Join(path.Join(Apply(currentPath, safename)...), safename(bucket)) + `/`, // Trailing slash = directory
}
ourBucket.SetMode(fs.ModeDir | 0755)
_, err := zw.CreateHeader(&ourBucket)
if err != nil {
return err
}
// Child pathspec
childPath := CopySliceAdd(currentPath, bucket)
// Create file entries for all non-bucket children
err = Bolt_ListItems(db, childPath, func(li ListItemInfo) error {
fileItem := zip.FileHeader{
Name: path.Join(path.Join(Apply(childPath, safename)...), safename(string(li.Name))),
}
fileItem.SetMode(0644)
fileW, err := zw.CreateHeader(&fileItem)
if err != nil {
return err
}
buff, err := Bolt_GetItem(db, childPath, []byte(li.Name))
if err != nil {
return err
}
_, err = io.CopyN(fileW, bytes.NewReader(buff), li.DataLen)
return err
})
if err != nil {
return err
}
// Recurse for all bucket-type children
process(childPath)
// Done
return nil
})
}
err = process([]string{})
if err != nil {
return err
}
err = zw.Flush()
if err != nil {
return err
}
err = zw.Close()
if err != nil {
return err
}
return fh.Close()
}
func Bolt_ImportZipToDatabase(dbpath, zippath string) error {
db, err := Bolt_Open(false, dbpath)
if err != nil {
return fmt.Errorf("Error opening target database: %w", err)
}
defer db.Close()
fh, err := os.OpenFile(zippath, os.O_RDONLY, 0400)
if err != nil {
return fmt.Errorf("Error opening input archive: %w", err)
}
defer fh.Close()
fstat, err := fh.Stat()
if err != nil {
return err
}
zr, err := zip.NewReader(fh, fstat.Size())
if err != nil {
return fmt.Errorf("Reading zip file format: %w", err)
}
for _, zf := range zr.File {
if strings.HasSuffix(zf.Name, `/`) || (zf.Mode()&fs.ModeDir) != 0 {
// Bucket
bucketPath := strings.Split(strings.TrimSuffix(zf.Name, `/`), `/`)
err = Bolt_CreateBucket(db, bucketPath[0:len(bucketPath)-1], bucketPath[len(bucketPath)-1])
if err != nil {
return fmt.Errorf("Creating bucket %q: %w", zf.Name, err)
}
} else {
// Object
objectPath := strings.Split(zf.Name, `/`)
rc, err := zf.Open()
if err != nil {
return err
}
content, err := io.ReadAll(rc)
if err != nil {
return err
}
err = Bolt_SetItem(db, objectPath[0:len(objectPath)-1], []byte(objectPath[len(objectPath)-1]), content)
if err != nil {
return err
}
err = rc.Close()
if err != nil {
return err
}
}
}
// Done
return nil
}

119
go.mod
View File

@@ -1,12 +1,119 @@
module code.ivysaur.me/qbolt
module qbolt
go 1.23.0
go 1.24.0
toolchain go1.23.3
toolchain go1.24.4
require (
github.com/mappu/miqt v0.10.0
go.etcd.io/bbolt v1.4.0
github.com/akrylysov/pogreb v0.10.2
github.com/cockroachdb/pebble v1.1.5
github.com/dgraph-io/badger/v4 v4.8.0
github.com/godbus/dbus/v5 v5.2.0
github.com/google/uuid v1.6.0
github.com/ledgerwatch/lmdb-go v1.18.2
github.com/lotusdblabs/lotusdb/v2 v2.1.0
github.com/mappu/autoconfig v0.6.1-0.20260124043120-621a5fcf917e
github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf
github.com/mattn/go-sqlite3 v1.14.32
github.com/redis/go-redis/v9 v9.17.2
github.com/rosedblabs/rosedb/v2 v2.3.6
github.com/starskey-io/starskey v0.1.9
github.com/stretchr/testify v1.11.1
github.com/syndtr/goleveldb v1.0.0
github.com/tidwall/buntdb v1.3.2
github.com/voidDB/voidDB v0.1.18
github.com/zalando/go-keyring v0.2.6
go.etcd.io/bbolt v1.4.3
go.etcd.io/etcd/client/v2 v2.305.26
go.etcd.io/etcd/client/v3 v3.6.7
go.mills.io/bitcask/v2 v2.1.5
go.mongodb.org/mongo-driver/v2 v2.4.1
golang.org/x/crypto v0.46.0
)
require golang.org/x/sys v0.32.0 // indirect
require (
al.essio.dev/pkg/shellescape v1.6.0 // indirect
github.com/DataDog/zstd v1.5.7 // indirect
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bwmarrin/snowflake v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cockroachdb/errors v1.12.0 // indirect
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 // indirect
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 // indirect
github.com/cockroachdb/redact v1.1.6 // indirect
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/danieljoos/wincred v1.2.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/getsentry/sentry-go v0.40.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/flatbuffers v25.9.23+incompatible // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/hashicorp/go-immutable-radix/v2 v2.0.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattetti/filebuffer v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.4 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/robfig/cron/v3 v3.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rosedblabs/diskhash v0.0.0-20230910084041-289755737e2a // indirect
github.com/rosedblabs/wal v1.3.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tidwall/btree v1.7.0 // indirect
github.com/tidwall/gjson v1.14.3 // indirect
github.com/tidwall/grect v0.1.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/rtred v0.1.2 // indirect
github.com/tidwall/tinyqueue v0.1.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.etcd.io/etcd/api/v3 v3.6.7 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.7 // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d // indirect
google.golang.org/grpc v1.71.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Some files were not shown because too many files have changed in this diff Show More