Compare commits
362 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d5ca19b47 | |||
| a0e70636a1 | |||
| 13e139e023 | |||
| 1f7d03e67a | |||
| 0b1f662e99 | |||
| e5f36b0f66 | |||
| 0eccb12744 | |||
| f2d3240153 | |||
| 9fb4302000 | |||
| e3f94f1eba | |||
| 2259b3f455 | |||
| b4e8733798 | |||
| ace5e3e65e | |||
| e132500fd8 | |||
| a6bb412a34 | |||
| 51f2a69ed2 | |||
| 1db2d9781d | |||
| 9f662a7fa2 | |||
| 034bd8114a | |||
| 7242e8644b | |||
| 820285066b | |||
| 94c517a324 | |||
| 9fbc7b4ee6 | |||
| 421cabb7a5 | |||
| 910ef0dd9a | |||
| a9c6b135c7 | |||
| 02d4b918d6 | |||
| 0798547b39 | |||
| 3d114ec1a4 | |||
| 4913e36ad6 | |||
| bafabdf690 | |||
| 4f2381ee33 | |||
| d876013ade | |||
| e42eb6ace7 | |||
| 4789c8c706 | |||
| cad3e9d496 | |||
| fc63b992ca | |||
| 1877417327 | |||
| 13b5878fe5 | |||
| fe5d218291 | |||
| 2abca95d72 | |||
| 7228bc5ba7 | |||
| 3087ba498d | |||
| 847521541f | |||
| 1379e912e5 | |||
| 69e17868a4 | |||
| 45650ead8c | |||
| e1f8c40143 | |||
| f3496db2c7 | |||
| 13511a389a | |||
| 2c1faf98c7 | |||
| 9a42a4021d | |||
| d2587949d6 | |||
| 7c24b1f24a | |||
| 882517dffc | |||
| f98db57a97 | |||
| 7a68362149 | |||
| f716afee74 | |||
| aecab00e70 | |||
| 65d921ddde | |||
| 5d39d90044 | |||
| 6a685a2562 | |||
| d4ee91ed10 | |||
| 9fc11d8c61 | |||
| cb09e0eb84 | |||
| 12a60f57e3 | |||
| a3e32178ac | |||
| 9e76cb32fe | |||
| 63e47ae505 | |||
| 319da7c13c | |||
| e03c635e7b | |||
| b2b5f8ba54 | |||
| dcff85cfe5 | |||
| 65cbfec7af | |||
| c2c997e53f | |||
| 9d2afaf57c | |||
| 8b410bca89 | |||
| e11b5b2100 | |||
| f292780972 | |||
| 89bd3ed27a | |||
| 2a22e92be4 | |||
| 5d829befca | |||
| 2eb7385516 | |||
| 543f573c7f | |||
| 463daba2cf | |||
| 9588e5189e | |||
| 30716df112 | |||
| ad8af93545 | |||
| c306a6d1a5 | |||
| 6a90605bd1 | |||
| c69089841a | |||
| ddbc30ed01 | |||
| 292e13a3e0 | |||
| 7aa6703ee0 | |||
| f90f76c097 | |||
| 134d4cf290 | |||
| 773470b30c | |||
| f55ab455be | |||
| b77ea21378 | |||
| 002298b4ff | |||
| 293627ab17 | |||
| 14219d6d49 | |||
| 37a6f479b8 | |||
| 176c51549c | |||
| 17c0fb3332 | |||
| 4a4e8a76a3 | |||
| af77b83e27 | |||
| 6b187c4142 | |||
| da56321624 | |||
| 4447e149b9 | |||
| 5850ba8836 | |||
| 81206ae1f0 | |||
| 5be200c672 | |||
| 25d8af1043 | |||
| 6abb5b6159 | |||
| 1a20ca1a9c | |||
| 38c5b88055 | |||
| fb20955bf1 | |||
| 8f76b5858d | |||
| d8cad1e59e | |||
| 6bb1d0ab9f | |||
| 35e06396e1 | |||
| 649cff7178 | |||
| 8236078ace | |||
| 4e084b914f | |||
| fc9965d757 | |||
| ff315a8e1c | |||
| f4863923a5 | |||
| 7fe5fa02f6 | |||
| 18139ee11b | |||
| daa79bf0d6 | |||
| 541fe5b0a8 | |||
| 6cc8213490 | |||
| 5bf36d70c5 | |||
| 593df7abba | |||
| 5776226130 | |||
| fab96d4602 | |||
| abfa27d20e | |||
| 104456049d | |||
| 9ea060fda7 | |||
| d568b75530 | |||
| c0a11d936a | |||
| 994cef8357 | |||
| 82ddab1431 | |||
| d1b0b4986e | |||
| 25d1609220 | |||
| 2609223ea6 | |||
| 8be4c79556 | |||
| 5006ac6e91 | |||
| e2eb81da77 | |||
| 0f5bf963ba | |||
| 8612ad630e | |||
| 0dc90a546b | |||
| 5c9ad3bd9d | |||
| d6c6dd594c | |||
| 12fb79bb8b | |||
| 9003982da8 | |||
| 548f3dc68c | |||
| 43f334331b | |||
| cccb06caf0 | |||
| f257901965 | |||
| 8e36e00460 | |||
| 07a749bb03 | |||
| fb069ed3b2 | |||
| f5d2e69007 | |||
| 35c2b01843 | |||
| 858af50136 | |||
| 13647edc0d | |||
| 8b2c09d859 | |||
| 03c3a142df | |||
| 318e634d9b | |||
| a51d568a87 | |||
| 99c20e76de | |||
| bc09478278 | |||
| 18d782e7e5 | |||
| 4e6a359b10 | |||
| f42bdf219b | |||
| 2f2972f97e | |||
| 5cf6a838ae | |||
| 8e5e80af79 | |||
| 49890599ea | |||
| 4e13d8dffd | |||
| 20e5efa711 | |||
| 6378740051 | |||
| b8ce7a667b | |||
| 281ca18d90 | |||
| 1725de6ace | |||
| 0ad7c03db0 | |||
| 1cdd0d113b | |||
| a0fae43690 | |||
| 04ac766125 | |||
| 9ac26467c0 | |||
| cbfc038839 | |||
| 7ae6462da0 | |||
| 09a3e5b90f | |||
| 1cddd17017 | |||
| 8fdc3a0428 | |||
| 18f10fc1b4 | |||
| 18331ae007 | |||
| f08242e93c | |||
| 23965230e2 | |||
| e43b261752 | |||
| f9b4cb71a5 | |||
| 96c05641bd | |||
| 2c5d1946ec | |||
| a6cbc5a9ed | |||
| c541e8b941 | |||
| 877f291a1f | |||
| 6145320858 | |||
| 8296a2fec9 | |||
| 223d13be58 | |||
| eca27dcd4f | |||
| 0f2a3e021a | |||
| 90259fb2b9 | |||
| 7573cf0453 | |||
| 6dd0635c9e | |||
| ce3d08740f | |||
| 8f5e1054fb | |||
| 1ac96eb133 | |||
| 53e9b6555e | |||
| e1a9f187cb | |||
| ee3110162b | |||
| 35a83eb483 | |||
| 60add3be86 | |||
| 2f65ffdd70 | |||
| aad92d27e9 | |||
| 21151be8a3 | |||
| f78eec1872 | |||
| 8af27f8834 | |||
| 0d3b90b879 | |||
| 2b59efc410 | |||
| 7fbf2ef1ed | |||
| d7e3363173 | |||
| cecfc338d4 | |||
| 35f09fc072 | |||
| 2163b46907 | |||
| 81b6b08e7b | |||
| f31724a110 | |||
| 063a8ca837 | |||
| 1cfc94a42b | |||
| 053e07c319 | |||
| 0b91c379b8 | |||
| 7b4cc885f5 | |||
| 3b17ddd8a4 | |||
| 8d051a14e5 | |||
| 4735c391bd | |||
| 0866e5edac | |||
| 5c44dc5f54 | |||
| a7dd1ca340 | |||
| abcf7dbfe5 | |||
| d359f42b24 | |||
| 7cec5cee4c | |||
| be91cd54c6 | |||
| b141aaaa6c | |||
| 493ab846b9 | |||
| d3ebcb4666 | |||
| 50cf207eae | |||
| e5cbbb6822 | |||
| 18674568dd | |||
| 748dd96267 | |||
| c5578daa9f | |||
| 3bc7f539ad | |||
| 52677224c1 | |||
| 0a31eab9f2 | |||
| 011063597d | |||
| 71c182692a | |||
| e15af5a544 | |||
| ba7228ad44 | |||
| 650c9e7183 | |||
| 471737f421 | |||
| d2b9618da0 | |||
| f0f0ff7904 | |||
| 639da11ab3 | |||
| fc084d7190 | |||
| e2111017eb | |||
| ef70e5825a | |||
| 15b29b32ce | |||
| 975e120530 | |||
| 5b8883d31a | |||
| ce43f5765c | |||
| feffa67677 | |||
| a14e58297a | |||
| 2b309fbda7 | |||
| 70db402cdf | |||
| a82d5e6b26 | |||
| 28570b0b96 | |||
| 9bf84fa140 | |||
| eb34221620 | |||
| cc3ba4d9f0 | |||
| f79d17afed | |||
| d7c2282335 | |||
| 43002a9fde | |||
| bc33d26cfd | |||
| 38d9e6238f | |||
| a653ef8ca4 | |||
| cc336366c9 | |||
| 8f105183eb | |||
| b45faa2e73 | |||
| 7e5d17100d | |||
| a817e5fa21 | |||
| 3d185033f3 | |||
| 924957d00d | |||
| d674078071 | |||
| 5992d19906 | |||
| 8cac46e9f2 | |||
| db5f6816c5 | |||
| 3a4bdbde94 | |||
| 0363bc65f4 | |||
| a47898e099 | |||
| 1487b18a3a | |||
| 04ef53720f | |||
| bc82aacd57 | |||
| 6dcc1afd6b | |||
| 645ab29cdd | |||
| 4202b9b970 | |||
| 42bbe3957a | |||
| 5e2aae9032 | |||
| f54577f93f | |||
| f4d2d2ec39 | |||
| 5cd3f6c765 | |||
| 5d268d22af | |||
| 5e0422e10f | |||
| ef30a0d210 | |||
| 38847b3a7e | |||
| f432b52652 | |||
| f64522dfa1 | |||
| 79bce40581 | |||
| 3731ee1781 | |||
| 2481a1da20 | |||
| 1035086ed4 | |||
| 038eb44f48 | |||
| 841575700e | |||
| 617393b627 | |||
| 91f9c5fc30 | |||
| 6234f02ea6 | |||
| 8b1e7064e7 | |||
| 00a96bfe84 | |||
| 232a1dd0e8 | |||
| cb4b35b059 | |||
| f913b63c58 | |||
| d97c8872de | |||
| 78d4b90189 | |||
| e30e3e6138 | |||
| f22d149a66 | |||
| 6008ae44a2 | |||
| 99096b2360 | |||
| 0e92459779 | |||
| a13eaaf523 | |||
| dca8d277d3 | |||
| 15c739c0b9 | |||
| fc2da972ef | |||
| e45eea7111 | |||
| 1e62d79c07 | |||
| c74c5ae5c0 | |||
| b0092c4a6e | |||
| 5ce6368c4a | |||
| 3e7e54da4b | |||
| 9f80d687b2 | |||
| 96411e877f | |||
| ac7e078c02 | |||
| 6fb8d1ba0c | |||
| 8d8374be24 |
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
testdata/
|
||||
qbolt
|
||||
qbolt.exe
|
||||
qbolt.linux64.tar.xz
|
||||
qbolt.win64.zip
|
||||
@@ -1,5 +0,0 @@
|
||||
mode:regexp
|
||||
^build-qbolt-
|
||||
^dummy-data/dummy-data$
|
||||
\.pro\.user$
|
||||
^build/
|
||||
190
CHANGELOG.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# Changelog
|
||||
|
||||
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
@@ -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.
|
||||
123
Makefile
@@ -1,68 +1,67 @@
|
||||
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
|
||||
|
||||
export PATH := /usr/lib/mxe/usr/bin:$(PATH)
|
||||
GOFLAGS := -ldflags='-s -w' -gcflags='-trimpath=$(CURDIR)' -asmflags='-trimpath=$(CURDIR)'
|
||||
VERSION := 1.0.1
|
||||
|
||||
.PHONY: all libs dist clean
|
||||
|
||||
all: \
|
||||
build/linux/qbolt \
|
||||
build/win32/release/qbolt.exe
|
||||
|
||||
libs: \
|
||||
build/linux/qbolt.a \
|
||||
build/win32/qbolt.a
|
||||
.PHONY: generate
|
||||
generate:
|
||||
$(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
|
||||
|
||||
dist: \
|
||||
build/dist/qbolt-${VERSION}-win32.zip \
|
||||
build/dist/qbolt-${VERSION}-src.tar.gz \
|
||||
build/dist/qbolt-${VERSION}-linux_amd64.tar.xz
|
||||
.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: qbolt.linux64.tar.xz qbolt.win64.zip
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
if [ -f qbolt/qbolt.a ] ; then rm qbolt/qbolt.a ; fi
|
||||
if [ -f qbolt ] ; then rm qbolt ; fi
|
||||
if [ -d build ] ; then rm -r build ; fi
|
||||
|
||||
# Build core golang shared library (linux)
|
||||
|
||||
build/linux/qbolt.a: *.go
|
||||
mkdir -p build/linux
|
||||
go build ${GOFLAGS} -buildmode=c-archive -o build/linux/qbolt.a
|
||||
|
||||
# Build core golang shared library (win32)
|
||||
|
||||
build/win32/qbolt.a: *.go
|
||||
mkdir -p build/win32
|
||||
CC=/usr/lib/mxe/usr/bin/i686-w64-mingw32.static-gcc CGO_ENABLED=1 GOARCH=386 GOOS=windows \
|
||||
go build ${GOFLAGS} -buildmode=c-archive -o build/win32/qbolt.a
|
||||
git checkout -- version.go
|
||||
rm -f qbolt.exe qbolt qbolt.linux64.tar.xz qbolt.win64.zip
|
||||
|
||||
# Linux binaries
|
||||
|
||||
build/linux/qbolt: build/linux/qbolt.a qbolt/*
|
||||
cd build/linux && qmake ../../qbolt/qbolt.pro && make
|
||||
#####
|
||||
# Test databases in Docker
|
||||
|
||||
# Linux distribution
|
||||
|
||||
build/dist/qbolt-${VERSION}-linux_amd64.tar.xz: build/linux/qbolt
|
||||
XZ_OPTS=-9 tar caf build/dist/qbolt-${VERSION}-linux_amd64.tar.xz -C build/linux qbolt --owner=0 --group=0
|
||||
|
||||
# Windows binaries
|
||||
|
||||
build/win32/release/qbolt.exe: build/win32/qbolt.a qbolt/*
|
||||
cd build/win32 && i686-w64-mingw32.static-qmake-qt5 ../../qbolt/qbolt.pro && make
|
||||
|
||||
# Windows distribution
|
||||
|
||||
build/win32/dist/qbolt.exe: build/win32/release/qbolt.exe
|
||||
mkdir -p build/win32/dist
|
||||
cp build/win32/release/qbolt.exe build/win32/dist/qbolt.exe
|
||||
upx --lzma build/win32/dist/qbolt.exe
|
||||
|
||||
build/dist/qbolt-${VERSION}-win32.zip: build/win32/dist/qbolt.exe
|
||||
mkdir -p build/dist
|
||||
zip -0 -j build/dist/qbolt-${VERSION}-win32.zip build/win32/dist/qbolt.exe
|
||||
|
||||
# Source code archives
|
||||
|
||||
build/dist/qbolt-${VERSION}-src.tar.gz:
|
||||
hg archive build/dist/qbolt-${VERSION}-src.tar.gz
|
||||
.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
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ObjectReference int64
|
||||
|
||||
var NullObjectReference error = errors.New("Null object reference")
|
||||
|
||||
// GoMemoryStore is a int->interface storage structure so that Go pointers are
|
||||
// never exposed to C code.
|
||||
type GoMemoryStore struct {
|
||||
mtx sync.RWMutex
|
||||
items map[int64]interface{}
|
||||
next int64
|
||||
}
|
||||
|
||||
func NewGoMemoryStore() *GoMemoryStore {
|
||||
ret := GoMemoryStore{}
|
||||
ret.items = make(map[int64]interface{})
|
||||
return &ret
|
||||
}
|
||||
|
||||
func (this *GoMemoryStore) Put(itm interface{}) ObjectReference {
|
||||
this.mtx.Lock()
|
||||
defer this.mtx.Unlock()
|
||||
|
||||
key := this.next
|
||||
this.items[key] = itm
|
||||
this.next++
|
||||
return ObjectReference(key)
|
||||
}
|
||||
|
||||
func (this *GoMemoryStore) Get(i ObjectReference) (interface{}, bool) {
|
||||
this.mtx.RLock()
|
||||
defer this.mtx.RUnlock()
|
||||
|
||||
ret, ok := this.items[int64(i)]
|
||||
return ret, ok
|
||||
}
|
||||
|
||||
func (this *GoMemoryStore) Delete(i ObjectReference) {
|
||||
this.mtx.Lock()
|
||||
defer this.mtx.Unlock()
|
||||
|
||||
delete(this.items, int64(i))
|
||||
}
|
||||
|
||||
var gms *GoMemoryStore = nil
|
||||
|
||||
func init() {
|
||||
gms = NewGoMemoryStore()
|
||||
}
|
||||
56
README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# QBolt
|
||||
|
||||
A graphical interface for multiple databases.
|
||||
|
||||
## Features
|
||||
|
||||
- Native desktop application, running on Linux, Windows, macOS, and Android
|
||||
- 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
|
||||
- View database/bucket statistics and metadata
|
||||
- Run custom SQL queries
|
||||
- Select text to run partial query
|
||||
- Safe handling for non-UTF8 key and data fields
|
||||
- Hex viewer for binary data
|
||||
- Connection Manager saves connections with AEAD AES256-GCM using OS keychain
|
||||
|
||||
## Supported databases
|
||||
|
||||
There are currently 15 supported databases:
|
||||
|
||||
Database |Read |Editing |Query |Connection options |Context menu actions
|
||||
-------------|------|---------|------|--------------------|--------
|
||||
Badger v4 |Yes |Yes |No |Encrypted, readonly, in-memory |Backup, restore, compact
|
||||
Bitcask |Yes |Yes |No |Readonly, autorecovery |Backup
|
||||
Bolt |Yes |Yes |No |Readonly |Create/delete child buckets, import/export as zip
|
||||
Debconf |Yes |No |No | |
|
||||
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 |
|
||||
Redis |Yes |No |Yes |SSH tunnel, RESP v3 |
|
||||
RoseDB |Yes |Yes |No | |
|
||||
SQLite |Yes |Yes |Yes |CLI driver, in-memory |Vacuum, export
|
||||
SSH Agent |Yes |No |No |Unix/TCP |Lock, unlock
|
||||
Starskey |Yes |Yes |No |Compression |
|
||||
|
||||
## License
|
||||
|
||||
The code in this project is licensed under the ISC license (see `LICENSE` file for details) with the following caveats:
|
||||
|
||||
- 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.
|
||||
|
||||
## Download
|
||||
|
||||
Get the latest version from [the releases page »](https://git.ivysaur.me/code.ivysaur.me/qbolt/releases)
|
||||
|
||||
## Changelog
|
||||
|
||||
See [the full change history »](https://git.ivysaur.me/code.ivysaur.me/qbolt/src/branch/master/CHANGELOG.md)
|
||||
112
TODO
Normal file
@@ -0,0 +1,112 @@
|
||||
- BUG: Connecting to multiple DBs from connection manager: if one has an error, the error popup from the others is lost
|
||||
- BUG: ExecQuery being called multiple times on error?
|
||||
- Drag and drop database into UI (QBolt parity)
|
||||
- Merge with QBolt
|
||||
- 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)
|
||||
- Bolt: import/export should support passworded zips
|
||||
- Table: BSON view can't see data
|
||||
- Table: quick filter
|
||||
- QSortFilterProxyModel
|
||||
- Cancellation
|
||||
- Loading animations for connection + queries
|
||||
- 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
|
||||
- Lungo: Mini embeddable Mongo - https://github.com/256dpi/lungo
|
||||
- MSSQL (recursive navigation for instances)
|
||||
- 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
|
||||
- VoidDB - https://github.com/voidDB/voidDB
|
||||
- UnisonDB - https://github.com/ankur-anand/unisondb
|
||||
- Etcd
|
||||
- v2: hierarchal
|
||||
- v3: flat key namespace
|
||||
- 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-agent
|
||||
- ssh known-hosts
|
||||
- golang.org/x/crypto/ssh/knownhosts - already using this package
|
||||
- Generic ODBC, database/sql, ...
|
||||
- Other language DBs
|
||||
- C, C++
|
||||
- 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)
|
||||
- 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
|
||||
- Attach to SSH tunnel
|
||||
- Configure binary path
|
||||
- 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
|
||||
- SQLite:
|
||||
- drop table doesn't autorefresh nav since callback is late
|
||||
- more accurate type handling
|
||||
- binary data currently shows as "<<binary>>", edits wrongly
|
||||
- views
|
||||
- other special objects (triggers? udf functions?)
|
||||
- remove current hardcoded LIMIT 1000
|
||||
- attach additional db to same connection
|
||||
- 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
|
||||
- SSH tunnel
|
||||
- option to use external/system SSH
|
||||
- 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
|
||||
@@ -1,38 +0,0 @@
|
||||
A graphical database manager for BoltDB.
|
||||
|
||||
QBolt allows you to graphically view and edit the content of Bolt databases.
|
||||
|
||||
The project consists of two parts; a C binding (CGo) for the embeddable Bolt database engine, and a graphical interface built in C++/Qt that links to it.
|
||||
|
||||
Written in C++ (Qt), Golang (CGo)
|
||||
|
||||
=FEATURES=
|
||||
|
||||
- Open existing database or create new database
|
||||
- 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
|
||||
|
||||
=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.
|
||||
|
||||
=SEE ALSO=
|
||||
|
||||
- BoltDB https://github.com/boltdb/bolt
|
||||
|
||||
=CHANGELOG=
|
||||
|
||||
2017-06-19 1.0.1
|
||||
- Feature: Option to open database as read-only
|
||||
- Fix an issue with support for bucket names and keys not surviving UTF-8 roundtrips (now binary-clean)
|
||||
- Fix an issue with crashing when deleting a bucket other than the selected one
|
||||
- Fix a cosmetic issue with application icon on Windows
|
||||
|
||||
2017-05-21 1.0.0
|
||||
- Initial public release
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
- Reduce unnecessary memory copies between QString/QByteArray
|
||||
|
||||
- Convert from item-based to model-based Qt widgets - needs deep integration with Bolt cursors...
|
||||
|
||||
- Rename buckets
|
||||
|
||||
- Delete multiple buckets
|
||||
|
||||
BIN
_dist/image0.png
|
Before Width: | Height: | Size: 50 KiB |
BIN
_dist/image1.png
|
Before Width: | Height: | Size: 90 KiB |
BIN
_dist/image2.png
|
Before Width: | Height: | Size: 75 KiB |
BIN
_dist/image3.png
|
Before Width: | Height: | Size: 59 KiB |
BIN
assets/add.png
Normal file
|
After Width: | Height: | Size: 714 B |
BIN
assets/arrow_refresh.png
Normal file
|
After Width: | Height: | Size: 707 B |
BIN
assets/chart_bar.png
Normal file
|
After Width: | Height: | Size: 637 B |
BIN
assets/connect.png
Normal file
|
After Width: | Height: | Size: 754 B |
BIN
qbolt/rsrc/database.png → assets/database.png
Executable file → Normal file
|
Before Width: | Height: | Size: 390 B After Width: | Height: | Size: 337 B |
BIN
assets/database_add.png
Normal file
|
After Width: | Height: | Size: 685 B |
BIN
assets/database_delete.png
Normal file
|
After Width: | Height: | Size: 715 B |
BIN
assets/database_key.png
Normal file
|
After Width: | Height: | Size: 803 B |
BIN
assets/database_lightning.png
Normal file
|
After Width: | Height: | Size: 819 B |
BIN
assets/database_save.png
Normal file
|
After Width: | Height: | Size: 812 B |
BIN
assets/delete.png
Normal file
|
After Width: | Height: | Size: 692 B |
BIN
assets/disconnect.png
Normal file
|
After Width: | Height: | Size: 793 B |
BIN
assets/help.png
Normal file
|
After Width: | Height: | Size: 755 B |
BIN
assets/lightning.png
Normal file
|
After Width: | Height: | Size: 644 B |
BIN
assets/lightning_go.png
Normal file
|
After Width: | Height: | Size: 770 B |
BIN
assets/pencil.png
Normal file
|
After Width: | Height: | Size: 391 B |
BIN
assets/pencil_add.png
Normal file
|
After Width: | Height: | Size: 540 B |
BIN
assets/pencil_delete.png
Normal file
|
After Width: | Height: | Size: 558 B |
BIN
assets/pencil_go.png
Normal file
|
After Width: | Height: | Size: 615 B |
BIN
assets/resultset_next.png
Normal file
|
After Width: | Height: | Size: 348 B |
BIN
assets/table.png
Normal file
|
After Width: | Height: | Size: 551 B |
BIN
assets/table_add.png
Normal file
|
After Width: | Height: | Size: 629 B |
BIN
assets/table_delete.png
Normal file
|
After Width: | Height: | Size: 645 B |
BIN
assets/table_save.png
Normal file
|
After Width: | Height: | Size: 744 B |
BIN
assets/vendor_cockroach.png
Normal file
|
After Width: | Height: | Size: 353 B |
BIN
assets/vendor_debian.png
Normal file
|
After Width: | Height: | Size: 622 B |
BIN
assets/vendor_dgraph.png
Normal file
|
After Width: | Height: | Size: 763 B |
BIN
assets/vendor_freedesktop.png
Normal file
|
After Width: | Height: | Size: 821 B |
BIN
assets/vendor_github.png
Normal file
|
After Width: | Height: | Size: 303 B |
BIN
assets/vendor_leveldb.png
Normal file
|
After Width: | Height: | Size: 624 B |
BIN
assets/vendor_lmdb.png
Normal file
|
After Width: | Height: | Size: 738 B |
BIN
assets/vendor_lotus.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/vendor_mongodb.png
Normal file
|
After Width: | Height: | Size: 460 B |
BIN
assets/vendor_mysql.png
Normal file
|
After Width: | Height: | Size: 545 B |
BIN
assets/vendor_qt.png
Normal file
|
After Width: | Height: | Size: 384 B |
BIN
assets/vendor_redis.png
Normal file
|
After Width: | Height: | Size: 825 B |
BIN
assets/vendor_riak.png
Normal file
|
After Width: | Height: | Size: 719 B |
BIN
assets/vendor_rosedb.png
Normal file
|
After Width: | Height: | Size: 970 B |
BIN
assets/vendor_sqlite.png
Normal file
|
After Width: | Height: | Size: 665 B |
BIN
assets/vendor_ssh.png
Normal file
|
After Width: | Height: | Size: 377 B |
BIN
assets/vendor_starskey.png
Normal file
|
After Width: | Height: | Size: 203 B |
427
config.go
Normal file
@@ -0,0 +1,427 @@
|
||||
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, string, error)
|
||||
}
|
||||
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (cc *ConnectionConfig) String() string {
|
||||
if selection, err := cc.selection(); err == nil {
|
||||
if stringer, ok := selection.(fmt.Stringer); ok {
|
||||
return stringer.String()
|
||||
}
|
||||
}
|
||||
|
||||
if string(cc.Type) == "" {
|
||||
return "Not configured"
|
||||
}
|
||||
|
||||
return string(cc.Type)
|
||||
}
|
||||
|
||||
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, string, 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)
|
||||
|
||||
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, displayName, 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
|
||||
}
|
||||
|
||||
// Add ld to mainwindow
|
||||
f.addTopLevelDatabaseConnection(ld, displayName)
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
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, ConnMgrSaveError{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()))
|
||||
|
||||
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)
|
||||
|
||||
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 {
|
||||
ok = ok && connectToItem(itm) // connect to selected
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
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()
|
||||
}
|
||||
198
configSave.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
type SavedConfigEntry struct {
|
||||
Description string
|
||||
Connection ConnectionConfig
|
||||
}
|
||||
|
||||
type SavedConfig struct {
|
||||
UserAgent string // APPNAME/{ver}
|
||||
Entries []SavedConfigEntry
|
||||
}
|
||||
|
||||
const (
|
||||
saveSettingsFilename = `settings.dat`
|
||||
keychainUserName = `settings-encryption-key`
|
||||
)
|
||||
|
||||
func (f *App) getConnectionManagerContents() (*SavedConfig, 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 {
|
||||
if os.IsNotExist(err) {
|
||||
// No file exists. Use blank
|
||||
return &SavedConfig{
|
||||
UserAgent: APPNAME,
|
||||
}, 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
|
||||
}
|
||||
|
||||
plaintext, err := cw.Open(ciphertext[:0], nil, ciphertext, nil) // @ref https://pkg.go.dev/crypto/cipher#AEAD
|
||||
if err != 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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
208
db_badger.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"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([]columnType{columnType_popupData, columnType_popupData}, []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 kvstore_ApplyChanges(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 badgerConnection struct {
|
||||
Type autoconfig.OneOf
|
||||
Disk *struct {
|
||||
Directory autoconfig.ExistingDirectory
|
||||
Readonly bool
|
||||
Encryption *encryptionKey
|
||||
}
|
||||
Memory *struct{}
|
||||
}
|
||||
|
||||
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) Connect(ctx context.Context) (loadedDatabase, string, error) {
|
||||
if bdc.Disk != nil {
|
||||
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
|
||||
}
|
||||
|
||||
db, err := badger.Open(opts)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return &badgerLoadedDatabase{db: db}, filepath.Base(string(bdc.Disk.Directory)), nil
|
||||
} else { // memory
|
||||
db, err := badger.Open(badger.DefaultOptions("").WithInMemory(true))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return &badgerLoadedDatabase{db: db}, `:memory:`, nil // SQLite-style naming
|
||||
|
||||
}
|
||||
}
|
||||
113
db_bitcask.go
Normal file
@@ -0,0 +1,113 @@
|
||||
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([]columnType{columnType_popupData, columnType_popupData}, []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 kvstore_ApplyChanges(
|
||||
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, string, 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}, filepath.Base(string(c.Directory)), nil
|
||||
}
|
||||
337
db_bolt.go
Normal file
@@ -0,0 +1,337 @@
|
||||
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 boltConfig struct {
|
||||
Path autoconfig.ExistingFile `yfilter:"Bolt database (*.db);;All files (*)"`
|
||||
Readonly bool
|
||||
}
|
||||
|
||||
func (bc *boltConfig) String() string { // n.b. only used for default names in connection manager
|
||||
return filepath.Base(string(bc.Path))
|
||||
}
|
||||
|
||||
func (bc *boltConfig) Connect(ctx context.Context) (loadedDatabase, string, error) {
|
||||
|
||||
opts := bbolt.Options{
|
||||
Timeout: 1 * time.Second,
|
||||
ReadOnly: bc.Readonly,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
displayName := filepath.Base(string(bc.Path))
|
||||
if bc.Readonly {
|
||||
displayName += " (read-only)"
|
||||
}
|
||||
|
||||
ld := &boltLoadedDatabase{
|
||||
path: string(bc.Path),
|
||||
db: db,
|
||||
}
|
||||
|
||||
return ld, displayName, 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([]columnType{columnType_popupData, columnType_popupData}, []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
|
||||
}
|
||||
|
||||
// kvstore_ApplyChanges is a helper function to apply edits to K/V stores that
|
||||
// can use a common abstraction.
|
||||
// It always uses the "popupData" type i.e. []byte.
|
||||
func kvstore_ApplyChanges(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 kvstore_ApplyChanges(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
@@ -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
|
||||
})
|
||||
}
|
||||
117
db_debconf.go
Normal file
@@ -0,0 +1,117 @@
|
||||
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)
|
||||
|
||||
f.SetupColumns(slice_repeat(columnType_inlineText, len(ld.db.AllColumnNames)), 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 (*)"`
|
||||
}
|
||||
|
||||
func (dc *debconfConnection) Reset() {
|
||||
if runtime.GOOS == "linux" {
|
||||
dc.Database = "/var/cache/debconf/config.dat" // Prefill default path
|
||||
}
|
||||
}
|
||||
|
||||
func (dc *debconfConnection) Connect(ctx context.Context) (loadedDatabase, string, 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}, filepath.Base(string(dc.Database)), nil
|
||||
}
|
||||
61
db_embeddedversions.go
Normal file
@@ -0,0 +1,61 @@
|
||||
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([]columnType{columnType_inlineText, columnType_inlineText, columnType_inlineText}, []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{}
|
||||
|
||||
func (dc *evLdbConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
|
||||
mods, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return nil, "", errors.New("Missing build info")
|
||||
}
|
||||
|
||||
return &evLdb{mods: mods}, APPNAME, nil
|
||||
}
|
||||
102
db_leveldb.go
Normal file
@@ -0,0 +1,102 @@
|
||||
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([]columnType{columnType_popupData, columnType_popupData}, []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 = kvstore_ApplyChanges(
|
||||
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
|
||||
}
|
||||
|
||||
func (pdc *leveldbConnection) Connect(ctx context.Context) (loadedDatabase, string, 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}, filepath.Base(string(pdc.Directory)), nil
|
||||
}
|
||||
284
db_lmdb.go
Normal file
@@ -0,0 +1,284 @@
|
||||
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([]columnType{columnType_popupData, columnType_popupData}, []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 kvstore_ApplyChanges(
|
||||
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
|
||||
File *struct {
|
||||
Path autoconfig.ExistingFile `yfilter:"LMDB database (*.mdb);;All files (*)"`
|
||||
}
|
||||
}
|
||||
MultiDB *struct {
|
||||
Slots int
|
||||
} `ylabel:"Multiple databases mode"`
|
||||
Readonly bool
|
||||
}
|
||||
|
||||
func (pdc *lmdbConnection) Connect(ctx context.Context) (loadedDatabase, string, 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}, filepath.Base(openPath), nil
|
||||
}
|
||||
88
db_lotusdb.go
Normal file
@@ -0,0 +1,88 @@
|
||||
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([]columnType{columnType_popupData, columnType_popupData}, []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 := kvstore_ApplyChanges(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
|
||||
}
|
||||
|
||||
func (ldc *lotusDBConnection) Connect(ctx context.Context) (loadedDatabase, string, 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}, filepath.Base(string(ldc.Directory)), nil
|
||||
}
|
||||
348
db_mongo.go
Normal file
@@ -0,0 +1,348 @@
|
||||
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([]columnType{columnType_inlineText, columnType_bsonDoc}, []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([]columnType{columnType_popupData}, []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
|
||||
}
|
||||
|
||||
SSH_Tunnel *SSHTunnel
|
||||
}
|
||||
|
||||
func (moc *mongoConnection) Reset() {
|
||||
moc.Conn.Mode = "Connection_String"
|
||||
moc.Conn.Connection_String = address_of("mongodb://localhost:27017")
|
||||
}
|
||||
|
||||
func (moc *mongoConnection) Connect(ctx context.Context) (loadedDatabase, string, 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, "MongoDB", nil
|
||||
}
|
||||
24
db_none.go
Normal 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() {}
|
||||
120
db_pebble.go
Normal file
@@ -0,0 +1,120 @@
|
||||
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([]columnType{columnType_popupData, columnType_popupData}, []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 := kvstore_ApplyChanges(
|
||||
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
|
||||
}
|
||||
Memory *struct{}
|
||||
}
|
||||
|
||||
func (pdc *pebbleConnection) Connect(ctx context.Context) (loadedDatabase, string, 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}, filepath.Base(string(pdc.Disk.Directory)), nil
|
||||
|
||||
} else {
|
||||
// Memory != nil
|
||||
db, err := pebble.Open("", &pebble.Options{FS: vfs.NewMem()})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return &pebbleLoadedDatabase{db: db}, `:memory:`, nil // SQLite-style naming
|
||||
}
|
||||
}
|
||||
271
db_redis.go
Normal file
@@ -0,0 +1,271 @@
|
||||
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
|
||||
}
|
||||
|
||||
func (config *redisConnectionOptions) Reset() {
|
||||
config.Address.Port = 6379
|
||||
}
|
||||
|
||||
func (config *redisConnectionOptions) Connect(ctx context.Context) (loadedDatabase, string, 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)
|
||||
}
|
||||
|
||||
displayName := config.Address.Address
|
||||
|
||||
return ld, displayName, 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(
|
||||
[]columnType{columnType_popupData, columnType_inlineText, columnType_popupData},
|
||||
[]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([]columnType{columnType_inlineText}, []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
|
||||
74
db_rosedb.go
Normal file
@@ -0,0 +1,74 @@
|
||||
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([]columnType{columnType_popupData, columnType_popupData}, []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 kvstore_ApplyChanges(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
|
||||
}
|
||||
|
||||
func (c *roseDBConn) Connect(ctx context.Context) (loadedDatabase, string, 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}, filepath.Base(string(c.Directory)), nil
|
||||
}
|
||||
189
db_secretsvc_linux.go
Normal file
@@ -0,0 +1,189 @@
|
||||
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(
|
||||
[]columnType{columnType_inlineText, columnType_inlineText, columnType_inlineText, columnType_inlineText, columnType_inlineText, columnType_popupData},
|
||||
[]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 {
|
||||
}
|
||||
|
||||
func (ssc *secretServiceConnection) Connect(ctx context.Context) (loadedDatabase, string, 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}, "dbus://SessionBus/org.freedesktop.secrets", nil
|
||||
}
|
||||
19
db_secretsvc_other.go
Normal file
@@ -0,0 +1,19 @@
|
||||
//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"`
|
||||
}
|
||||
|
||||
func (ssc *secretServiceConnection) Connect(ctx context.Context) (loadedDatabase, string, error) {
|
||||
return nil, "", errors.New("Not supported on this operating system")
|
||||
}
|
||||
435
db_sqlite.go
Normal file
@@ -0,0 +1,435 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"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 {
|
||||
|
||||
ctx := context.TODO()
|
||||
|
||||
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]
|
||||
|
||||
// Load column details
|
||||
// Use SELECT form instead of common PRAGMA table_info so we can just get names
|
||||
// We could possibly get this from the main data select, but this will
|
||||
// work even when there are 0 results
|
||||
columnNames, err := ld.sqliteGetColumnNamesForTable(tableName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to load columns for table %q: %w", tableName, err)
|
||||
}
|
||||
|
||||
populateColumns(columnNames, f)
|
||||
|
||||
// Find primary key, if any
|
||||
var primaryKeyIdx int = -1
|
||||
if primaryKeyColumnName, err := ld.getPrimaryKeyForTable(ctx, ld.db, tableName); err == nil {
|
||||
if search, ok := slice_find(columnNames, primaryKeyColumnName); ok {
|
||||
primaryKeyIdx = search
|
||||
}
|
||||
}
|
||||
|
||||
// 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 * FROM "` + tableName + `" LIMIT 1000`) // 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, primaryKeyIdx)
|
||||
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 populateColumns(names []string, dest *tableState) {
|
||||
// FIXME better column types?
|
||||
dest.SetupColumns(slice_repeat(columnType_inlineText, len(names)), names)
|
||||
}
|
||||
|
||||
func populateRows(rr *sql.Rows, dest *tableState, pk_index int) error {
|
||||
|
||||
numColumns := len(dest.columns)
|
||||
|
||||
for rr.Next() {
|
||||
fields := make([]interface{}, numColumns)
|
||||
pfields := make([]interface{}, numColumns)
|
||||
for i := 0; i < numColumns; i += 1 {
|
||||
pfields[i] = &fields[i]
|
||||
}
|
||||
|
||||
err := rr.Scan(pfields...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Scan: %w", err)
|
||||
}
|
||||
|
||||
rpos := dest.AddRow()
|
||||
for i := 0; i < len(fields); i += 1 {
|
||||
dest.SetCell(rpos, i, formatAny(fields[i])) // FIXME stop doing string conversion here
|
||||
}
|
||||
|
||||
if pk_index >= 0 {
|
||||
dest.SetRowPrimaryKey(rpos, []byte(formatAny(fields[pk_index]))) // FIXME stop doing string conversion here
|
||||
}
|
||||
|
||||
}
|
||||
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()
|
||||
|
||||
columns, err := rr.Columns()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
populateColumns(columns, resultArea)
|
||||
|
||||
err = populateRows(rr, resultArea, -1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resultArea.Ready()
|
||||
return nil
|
||||
}
|
||||
|
||||
type RowQueryContexter interface {
|
||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||
}
|
||||
|
||||
func (n *sqliteLoadedDatabase) getPrimaryKeyForTable(ctx context.Context, tx RowQueryContexter, tableName string) (string, error) {
|
||||
var primaryColumnName string
|
||||
err := tx.QueryRowContext(ctx, `SELECT l.name FROM pragma_table_info(?) as l WHERE l.pk = 1;`, tableName).Scan(&primaryColumnName)
|
||||
return primaryColumnName, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Query sqlite table metadata to determine which of these is the PRIMARY KEY
|
||||
primaryColumnName, err := n.getPrimaryKeyForTable(ctx, tx, tableName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Finding primary key for update: %w", 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 rowid, editcells := range f.updateRows {
|
||||
stmt := `UPDATE [` + tableName + `] SET `
|
||||
params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind)
|
||||
|
||||
for ct, cell := range editcells {
|
||||
if ct > 0 {
|
||||
stmt += `, `
|
||||
}
|
||||
stmt += `[` + f.columnLabels[cell] + `] = ?`
|
||||
params = append(params, (f.columns[cell].(*stringColumn)).vals[rowid]) // FIXME stop doing string conversion
|
||||
}
|
||||
stmt += ` WHERE [` + primaryColumnName + `] = ?`
|
||||
|
||||
// Update by primary key (stored separately)
|
||||
pkVal := string(f.primaryKeys[rowid]) // FIXME avoid string marshalling
|
||||
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 rowid, _ := range f.deleteRows {
|
||||
pkVal := string(f.primaryKeys[rowid]) // FIXME avoid string marshalling
|
||||
|
||||
stmt := `DELETE FROM [` + tableName + `] WHERE [` + primaryColumnName + `] = ?`
|
||||
|
||||
_, err = tx.ExecContext(ctx, stmt, pkVal)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Deleting row %q: %w", pkVal, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert all new entries
|
||||
for rowid, _ := range f.insertRows {
|
||||
stmt := `INSERT INTO [` + tableName + `] ([` + strings.Join(f.columnLabels, `], [`) + `]) VALUES (`
|
||||
params := []interface{}{} // FIXME reinstate types for the driver (although SQLite doesn't mind)
|
||||
|
||||
for colid := 0; colid < len(f.columnLabels); colid++ {
|
||||
if colid > 0 {
|
||||
stmt += `, `
|
||||
}
|
||||
stmt += "?"
|
||||
params = append(params, (f.columns[colid].(*stringColumn)).vals[rowid]) // FIXME stop doing string conversion
|
||||
}
|
||||
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 {
|
||||
rr, err := ld.db.Query(`SELECT name FROM sqlite_master WHERE type='table' 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"`
|
||||
}
|
||||
Memory *struct{}
|
||||
}
|
||||
|
||||
func (sc *sqliteConnection) Connect(ctx context.Context) (loadedDatabase, string, 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}, filepath.Base(string(sc.Disk.Database)), nil
|
||||
|
||||
} else { // memory
|
||||
db, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return &sqliteLoadedDatabase{db: db}, ":memory:", nil
|
||||
}
|
||||
}
|
||||
179
db_sshagent.go
Normal file
@@ -0,0 +1,179 @@
|
||||
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([]columnType{columnType_inlineText, columnType_inlineText, columnType_popupData}, []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
|
||||
TCP *autoconfig.AddressPort
|
||||
}
|
||||
|
||||
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, string, error) {
|
||||
agent, err := c.getAgent()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return &sshAgentLdb{conn: agent}, "SSH Agent", nil
|
||||
}
|
||||
115
db_starskey.go
Normal file
@@ -0,0 +1,115 @@
|
||||
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([]columnType{columnType_popupData, columnType_popupData}, []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 kvstore_ApplyChanges(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"`
|
||||
}
|
||||
|
||||
func (pdc *starskeyConnection) Connect(ctx context.Context) (loadedDatabase, string, 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}, filepath.Base(string(pdc.Directory)), nil
|
||||
}
|
||||
132
debconf/debconf.go
Normal 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
@@ -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))
|
||||
}
|
||||
}
|
||||
BIN
doc/screenshot-000.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
@@ -1,83 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
//"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
func random_name() string {
|
||||
ret := make([]byte, 12)
|
||||
rand.Read(ret)
|
||||
return string(ret)
|
||||
//return fmt.Sprintf("%08x-%08x-%08x", rand.Int63(), rand.Int63(), rand.Int63())
|
||||
}
|
||||
|
||||
func fill_bucket(tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
// fill with some basic items
|
||||
for i := 0; i < 30; i += 1 {
|
||||
err := bucket.Put([]byte(random_name()), []byte("SAMPLE CONTENT "+random_name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 1/20 (5%) chance of recursion
|
||||
if rand.Intn(100) >= 95 {
|
||||
|
||||
for i := 0; i < 5; i += 1 {
|
||||
|
||||
child, err := bucket.CreateBucket([]byte(random_name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = fill_bucket(tx, child)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
db, err := bolt.Open("sample.db", 0644, bolt.DefaultOptions)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = db.Update(func(tx *bolt.Tx) error {
|
||||
// top-level buckets
|
||||
for i := 0; i < 50; i += 1 {
|
||||
bucketName := random_name()
|
||||
bucket, err := tx.CreateBucket([]byte(bucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = fill_bucket(tx, bucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
17
embed.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
//go:generate miqt-rcc -Input "embed.qrc" -OutputGo "embed.go" -OutputRcc "embed.rcc" -Qt6
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
qt "github.com/mappu/miqt/qt6"
|
||||
)
|
||||
|
||||
//go:embed embed.rcc
|
||||
var _resourceRcc []byte
|
||||
|
||||
func init() {
|
||||
_ = embed.FS{}
|
||||
qt.QResource_RegisterResourceWithRccData(&_resourceRcc[0])
|
||||
}
|
||||
45
embed.qrc
Normal file
@@ -0,0 +1,45 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>assets/vendor_qt.png</file>
|
||||
<file>assets/connect.png</file>
|
||||
<file>assets/disconnect.png</file>
|
||||
<file>assets/database_key.png</file>
|
||||
<file>assets/help.png</file>
|
||||
<file>assets/add.png</file>
|
||||
<file>assets/arrow_refresh.png</file>
|
||||
<file>assets/chart_bar.png</file>
|
||||
<file>assets/database.png</file>
|
||||
<file>assets/database_add.png</file>
|
||||
<file>assets/database_delete.png</file>
|
||||
<file>assets/database_lightning.png</file>
|
||||
<file>assets/database_save.png</file>
|
||||
<file>assets/delete.png</file>
|
||||
<file>assets/lightning.png</file>
|
||||
<file>assets/lightning_go.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_cockroach.png</file>
|
||||
<file>assets/vendor_debian.png</file>
|
||||
<file>assets/vendor_dgraph.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_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>
|
||||
</qresource>
|
||||
</RCC>
|
||||
93
go.mod
Normal file
@@ -0,0 +1,93 @@
|
||||
module qbolt
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
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.4.1
|
||||
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/zalando/go-keyring v0.2.6
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
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 (
|
||||
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/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/snappy v1.0.0 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/flatbuffers v25.9.23+incompatible // indirect
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.0.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // 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/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/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.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.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/protobuf v1.36.10 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
279
go.sum
Normal file
@@ -0,0 +1,279 @@
|
||||
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
|
||||
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
|
||||
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81 h1:uHogIJ9bXH75ZYrXnVShHIyywFiUZ7OOabwd9Sfd8rw=
|
||||
github.com/abcum/lcp v0.0.0-20201209214815-7a3f3840be81/go.mod h1:6ZvnjTZX1LNo1oLpfaJK8h+MXqHxcBFBIwkgsv+xlv0=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
||||
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4=
|
||||
github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
|
||||
github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo=
|
||||
github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
|
||||
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k=
|
||||
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo=
|
||||
github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
|
||||
github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
|
||||
github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314=
|
||||
github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
|
||||
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs=
|
||||
github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w=
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
|
||||
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/godbus/dbus/v5 v5.2.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/flatbuffers v25.9.23+incompatible h1:rGZKv+wOb6QPzIdkM2KxhBZCDrA0DeN6DNmRDrqIsQU=
|
||||
github.com/google/flatbuffers v25.9.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.0.0 h1:nq9lQ5I71Heg2lRb2/+szuIWKY3Y73d8YKyXyN91WzU=
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.0.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/ledgerwatch/lmdb-go v1.18.2 h1:6YKp/KYcqGunNRHKZBBhiYADcIcWKzvu5QZv89RhnFQ=
|
||||
github.com/ledgerwatch/lmdb-go v1.18.2/go.mod h1:NKRpCxksoTQPyxsUcBiVOe0135uqnJsnf6cElxmOL0o=
|
||||
github.com/lotusdblabs/lotusdb/v2 v2.1.0 h1:rCBrwED8Po12FzrxxX4zppxoHb2O+sCtddyW4kyDiCQ=
|
||||
github.com/lotusdblabs/lotusdb/v2 v2.1.0/go.mod h1:MyOEvqL3Hxm3HiBOYZ4BlZBnqCIcc2QQkF34VBD76fk=
|
||||
github.com/mappu/autoconfig v0.4.1 h1:ekO7mzN+beFu7VhNfJxNlL/5wkYcP9PAl9VTG4EDxYs=
|
||||
github.com/mappu/autoconfig v0.4.1/go.mod h1:kca6kaYjqOwkZnY9z5YKo0oI/7Bxe2fMIHhYzXwGHEE=
|
||||
github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf h1:SmBzNUevLUzu1msJ5xzWH/Kot+GtOtoz0u9la42dRU4=
|
||||
github.com/mappu/miqt v0.12.1-0.20251106063543-466b60e47edf/go.mod h1:xFg7ADaO1QSkmXPsPODoKe/bydJpRG9fgCYyIDl/h1U=
|
||||
github.com/mattetti/filebuffer v1.0.1 h1:gG7pyfnSIZCxdoKq+cPa8T0hhYtD9NxCdI4D7PTjRLM=
|
||||
github.com/mattetti/filebuffer v1.0.1/go.mod h1:YdMURNDOttIiruleeVr6f56OrMc+MydEnTcXwtkxNVs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E=
|
||||
github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rosedblabs/diskhash v0.0.0-20230910084041-289755737e2a h1:BNp46nsknQivr3Gxzc6ytzG7xtBscBnLYZIkr0UfCko=
|
||||
github.com/rosedblabs/diskhash v0.0.0-20230910084041-289755737e2a/go.mod h1:3xvIg+7iOFUL/vMCE/6DwE6Yecb0okVYJBEfpdC/E+8=
|
||||
github.com/rosedblabs/rosedb/v2 v2.3.6 h1:o8vVOp61hFdORrz/PTosqU21/Z2Bug5I7cy1D3MZh2M=
|
||||
github.com/rosedblabs/rosedb/v2 v2.3.6/go.mod h1:/de9n2CoYaAGBDxZTJC5Jb0LCQOtoA3GOom+9QD9Z98=
|
||||
github.com/rosedblabs/wal v1.3.6 h1:oxZYTPX/u4JuGDW98wQ1YamWqerlrlSUFKhgP6Gd/Ao=
|
||||
github.com/rosedblabs/wal v1.3.6/go.mod h1:wdq54KJUyVTOv1uddMc6Cdh2d/YCIo8yjcwJAb1RCEM=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/starskey-io/starskey v0.1.9 h1:lABmD5KQgkpJZTCwSt+BHSOPXe82B9smbuScRL6T8Zk=
|
||||
github.com/starskey-io/starskey v0.1.9/go.mod h1:qly4ec2C/4Y45jhpL+q4m+Uxzg3mjj0t7RjpJslB3ao=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
|
||||
github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
|
||||
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.mills.io/bitcask/v2 v2.1.5 h1:SKPa0TPasJJZ8rNbLDvV3+lRXvdyQ0mwBobm2RH7J7w=
|
||||
go.mills.io/bitcask/v2 v2.1.5/go.mod h1:ZQFykoTTCvMwy24lBstZhSRQuleYIB4EzWKSOgEv6+k=
|
||||
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
|
||||
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.mongodb.org/mongo-driver/v2 v2.4.1 h1:hGDMngUao03OVQ6sgV5csk+RWOIkF+CuLsTPobNMGNI=
|
||||
go.mongodb.org/mongo-driver/v2 v2.4.1/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
|
||||
golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
155
lexer/lexer.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func isWhitespace(r byte) bool {
|
||||
return (r == ' ' || r == '\t' || r == '\r' || r == '\n')
|
||||
}
|
||||
|
||||
func Quote(input string) string {
|
||||
return `"` + strings.ReplaceAll(strings.ReplaceAll(input, `\`, `\\`), `"`, `\"`) + `"`
|
||||
}
|
||||
|
||||
// Fields splits a string into separate tokens using something kind of vaguely
|
||||
// like how SQL would do it.
|
||||
// The result still includes the quote and backslash characters.
|
||||
func Fields(input string) ([]string, error) {
|
||||
|
||||
const (
|
||||
StateToplevel = 0
|
||||
StateWhitespace = 1
|
||||
StateInDoubleQuote = 2
|
||||
StateInDoubleQuoteSlash = 3
|
||||
StateInSingleQuote = 4
|
||||
StateInSingleQuoteSlash = 5
|
||||
)
|
||||
|
||||
var (
|
||||
ret []string
|
||||
state int = StateToplevel
|
||||
wip string
|
||||
)
|
||||
|
||||
for pos := 0; pos < len(input); pos++ {
|
||||
c := input[pos]
|
||||
|
||||
switch state {
|
||||
|
||||
case StateToplevel:
|
||||
if isWhitespace(c) {
|
||||
state = StateWhitespace
|
||||
if len(wip) != 0 {
|
||||
ret = append(ret, wip)
|
||||
wip = ""
|
||||
}
|
||||
|
||||
} else if c == '"' {
|
||||
if len(wip) != 0 {
|
||||
return nil, fmt.Errorf(`Unexpected " at char %d`, pos)
|
||||
}
|
||||
|
||||
wip += string(c)
|
||||
state = StateInDoubleQuote
|
||||
|
||||
} else if c == '\'' {
|
||||
if len(wip) != 0 {
|
||||
return nil, fmt.Errorf(`Unexpected ' at char %d`, pos)
|
||||
}
|
||||
|
||||
wip += string(c)
|
||||
state = StateInSingleQuote
|
||||
|
||||
} else if c == '\\' {
|
||||
return nil, fmt.Errorf(`Unexpected \ at char %d`, pos)
|
||||
|
||||
} else if c == '(' || c == ')' || c == '?' || c == ',' || c == '+' || c == '*' || c == '-' || c == '/' || c == '%' || c == ';' || c == '=' {
|
||||
// Tokenize separately, even if they appear touching another top-level token
|
||||
// Should still be safe to re-join
|
||||
if len(wip) != 0 {
|
||||
ret = append(ret, wip)
|
||||
wip = ""
|
||||
}
|
||||
ret = append(ret, string(c))
|
||||
|
||||
} else {
|
||||
wip += string(c)
|
||||
}
|
||||
|
||||
case StateWhitespace:
|
||||
if isWhitespace(c) {
|
||||
// continue
|
||||
} else {
|
||||
state = StateToplevel
|
||||
pos-- // reparse
|
||||
}
|
||||
|
||||
case StateInDoubleQuote:
|
||||
if c == '"' {
|
||||
wip += string(c)
|
||||
ret = append(ret, wip)
|
||||
wip = ""
|
||||
state = StateToplevel
|
||||
|
||||
} else if c == '\\' {
|
||||
wip += string(c)
|
||||
state = StateInDoubleQuoteSlash
|
||||
|
||||
} else {
|
||||
wip += string(c)
|
||||
}
|
||||
|
||||
case StateInDoubleQuoteSlash:
|
||||
if isWhitespace(c) {
|
||||
return nil, fmt.Errorf(`Unexpected whitespace after \ at char %d`, pos)
|
||||
|
||||
} else {
|
||||
wip += string(c)
|
||||
state = StateInDoubleQuote
|
||||
}
|
||||
|
||||
case StateInSingleQuote:
|
||||
if c == '\'' {
|
||||
wip += string(c)
|
||||
ret = append(ret, wip)
|
||||
wip = ""
|
||||
state = StateToplevel
|
||||
|
||||
} else if c == '\\' {
|
||||
wip += string(c)
|
||||
state = StateInSingleQuoteSlash
|
||||
|
||||
} else {
|
||||
wip += string(c)
|
||||
}
|
||||
|
||||
case StateInSingleQuoteSlash:
|
||||
if isWhitespace(c) {
|
||||
return nil, fmt.Errorf(`Unexpected whitespace after \ at char %d`, pos)
|
||||
|
||||
} else {
|
||||
wip += string(c)
|
||||
state = StateInSingleQuote
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Reached the end of input stream
|
||||
switch state {
|
||||
case StateToplevel:
|
||||
if len(wip) > 0 {
|
||||
ret = append(ret, wip)
|
||||
wip = ""
|
||||
}
|
||||
return ret, nil
|
||||
|
||||
case StateWhitespace:
|
||||
return ret, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf(`Unexpected end of quoted input`)
|
||||
}
|
||||
}
|
||||
125
lexer/lexer_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package lexer
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLexer(t *testing.T) {
|
||||
type testCase struct {
|
||||
input string
|
||||
expect []string
|
||||
expectErr bool
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
|
||||
testCase{
|
||||
input: "foo bar baz",
|
||||
expect: []string{"foo", "bar", "baz"},
|
||||
expectErr: false,
|
||||
},
|
||||
|
||||
// Quotes
|
||||
|
||||
testCase{
|
||||
input: `foo "bar" baz`,
|
||||
expect: []string{"foo", `"bar"`, "baz"},
|
||||
expectErr: false,
|
||||
},
|
||||
testCase{
|
||||
input: `foo "bar baz" quux`,
|
||||
expect: []string{"foo", `"bar baz"`, "quux"},
|
||||
expectErr: false,
|
||||
},
|
||||
testCase{
|
||||
input: `foo 'bar baz' quux`,
|
||||
expect: []string{"foo", `'bar baz'`, "quux"},
|
||||
expectErr: false,
|
||||
},
|
||||
|
||||
// Escape characters
|
||||
|
||||
testCase{
|
||||
input: `foo 'bar \n baz' quux`,
|
||||
expect: []string{"foo", `'bar \n baz'`, "quux"},
|
||||
expectErr: false,
|
||||
},
|
||||
testCase{
|
||||
input: `foo "bar\"" baz`,
|
||||
expect: []string{"foo", `"bar\""`, "baz"},
|
||||
expectErr: false,
|
||||
},
|
||||
|
||||
// Collapsing whitespace
|
||||
|
||||
testCase{
|
||||
input: " foo bar \r\t\n baz\n",
|
||||
expect: []string{"foo", "bar", "baz"},
|
||||
expectErr: false,
|
||||
},
|
||||
|
||||
// Special characters lexed as separate tokens, but only at top level
|
||||
|
||||
testCase{
|
||||
input: `3+5*(2.3/6);`,
|
||||
expect: []string{"3", `+`, "5", "*", "(", "2.3", "/", "6", ")", ";"},
|
||||
expectErr: false,
|
||||
},
|
||||
testCase{
|
||||
input: `SELECT "3+5*(2.3/6)" AS expression;`,
|
||||
expect: []string{"SELECT", `"3+5*(2.3/6)"`, "AS", "expression", ";"},
|
||||
expectErr: false,
|
||||
},
|
||||
testCase{
|
||||
input: `INSERT INTO foo (bar, baz) VALUES (?, ?);`,
|
||||
expect: []string{"INSERT", "INTO", "foo", "(", "bar", ",", "baz", ")", "VALUES", "(", "?", ",", "?", ")", ";"},
|
||||
expectErr: false,
|
||||
},
|
||||
|
||||
// Errors
|
||||
|
||||
testCase{
|
||||
input: `foo "bar`,
|
||||
expect: nil,
|
||||
expectErr: true, // mismatched quotes
|
||||
},
|
||||
testCase{
|
||||
input: `foo 'bar`,
|
||||
expect: nil,
|
||||
expectErr: true, // mismatched quotes
|
||||
},
|
||||
testCase{
|
||||
input: `foo \"bar"`,
|
||||
expect: nil,
|
||||
expectErr: true, // invalid top-level escape
|
||||
},
|
||||
testCase{
|
||||
input: `foo "bar\ "`,
|
||||
expect: nil,
|
||||
expectErr: true, // escaping nothing
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
out, err := Fields(tc.input)
|
||||
|
||||
if err != nil {
|
||||
if !tc.expectErr {
|
||||
t.Errorf("Test %q got error %v, expected nil", tc.input, err)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if tc.expectErr {
|
||||
t.Errorf("Test %q got error <nil>, expected error", tc.input)
|
||||
continue
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(out, tc.expect) {
|
||||
t.Errorf("Test %q\n- got: %#v\n- expected %#v", tc.input, out, tc.expect)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
32
loadedDatabase.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
qt "github.com/mappu/miqt/qt6"
|
||||
)
|
||||
|
||||
var ErrNavNotExist error = errors.New("The selected item no longer exists")
|
||||
|
||||
type contextAction struct {
|
||||
Name string
|
||||
Callback func(sender *qt.QTreeWidgetItem, bucketPath []string) error
|
||||
}
|
||||
|
||||
// loadedDatabase is a DB-agnostic interface for each loaded database.
|
||||
type loadedDatabase interface {
|
||||
DriverName() string
|
||||
Properties(bucketPath []string) (string, error)
|
||||
RenderForNav(f *tableState, bucketPath []string) error
|
||||
NavChildren(bucketPath []string) ([]string, error)
|
||||
NavContext(bucketPath []string) ([]contextAction, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
type queryableLoadedDatabase interface {
|
||||
ExecQuery(query string, bucketPath []string, resultArea *tableState) error
|
||||
}
|
||||
|
||||
type editableLoadedDatabase interface {
|
||||
ApplyChanges(f *tableState, bucketPath []string) error
|
||||
}
|
||||
954
main.go
@@ -1,398 +1,640 @@
|
||||
package main
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/mappu/miqt/qt6/mainthread"
|
||||
|
||||
qt "github.com/mappu/miqt/qt6"
|
||||
)
|
||||
|
||||
const (
|
||||
ERROR_AND_STOP_CALLING int64 = 100
|
||||
ERROR_AND_KEEP_CALLING = 101
|
||||
FINISHED_OK = 102
|
||||
REAL_MESSAGE = 103
|
||||
APPNAME = "QBolt"
|
||||
HOMEPAGE_URL = "https://code.ivysaur.me/qbolt"
|
||||
)
|
||||
|
||||
const Magic int64 = 0x10203040
|
||||
type App struct {
|
||||
ui *MainWindowUi
|
||||
|
||||
//export GetMagic
|
||||
func GetMagic() int64 {
|
||||
return Magic
|
||||
contentTbl *tableState
|
||||
resultsTbl *tableState
|
||||
|
||||
none *noLoadedDatabase
|
||||
|
||||
dbs_next int
|
||||
dbs map[int]loadedDatabase
|
||||
}
|
||||
|
||||
//export Bolt_Open
|
||||
func Bolt_Open(readOnly bool, path string) (ObjectReference, *C.char, int) {
|
||||
opts := *bolt.DefaultOptions
|
||||
opts.Timeout = 10 * time.Second
|
||||
opts.ReadOnly = readOnly
|
||||
func newApp() *App {
|
||||
|
||||
ptrDB, err := bolt.Open(path, os.FileMode(0644), &opts)
|
||||
if err != nil {
|
||||
errMsg := err.Error()
|
||||
return 0, C.CString(errMsg), len(errMsg)
|
||||
}
|
||||
a := &App{}
|
||||
|
||||
dbRef := gms.Put(ptrDB)
|
||||
return dbRef, nil, 0
|
||||
}
|
||||
a.dbs_next = 0
|
||||
a.dbs = make(map[int]loadedDatabase, 0)
|
||||
|
||||
func withBoltDBReference(b ObjectReference, fn func(db *bolt.DB) error) error {
|
||||
dbIFC, ok := gms.Get(b)
|
||||
if !ok {
|
||||
return NullObjectReference
|
||||
}
|
||||
a.ui = NewMainWindowUi()
|
||||
a.ui.MainWindow.SetWindowTitle(APPNAME + " " + appVersion)
|
||||
|
||||
ptrDB, ok := dbIFC.(*bolt.DB)
|
||||
if !ok {
|
||||
return NullObjectReference
|
||||
}
|
||||
// Using stylesheet works better than picking a palette colour across all
|
||||
// different QStyles
|
||||
a.ui.propertiesBox.SetStyleSheet("background-color: transparent;")
|
||||
|
||||
return fn(ptrDB)
|
||||
}
|
||||
//
|
||||
|
||||
func walkBuckets(tx *bolt.Tx, browse []string) (*bolt.Bucket, error) {
|
||||
bucket := tx.Bucket([]byte(browse[0]))
|
||||
if bucket == nil {
|
||||
return nil, errors.New("Unknown bucket")
|
||||
}
|
||||
a.ui.actionConnect.OnTriggered(a.OnMnuConnectClick)
|
||||
a.ui.actionConnectionManager.OnTriggered(a.OnMnuConnectionManagerClick)
|
||||
|
||||
for i := 1; i < len(browse); i += 1 {
|
||||
bucket = bucket.Bucket([]byte(browse[i]))
|
||||
if bucket == nil {
|
||||
return nil, errors.New("Unknown bucket")
|
||||
}
|
||||
}
|
||||
a.ui.actionE_xit.OnTriggered(a.OnMnuFileExitClick)
|
||||
|
||||
return bucket, nil
|
||||
}
|
||||
a.ui.mnuExecute.OnTriggered(a.OnQueryExecute)
|
||||
|
||||
func withBrowse_ReadOnly(b_ref ObjectReference, browse []string, fn func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error) error {
|
||||
if len(browse) == 0 {
|
||||
// not a bucket
|
||||
return errors.New("No bucket selected")
|
||||
}
|
||||
a.ui.mnuDriverVersions.OnTriggered(a.OnMenuHelpVersion)
|
||||
a.ui.mnuHelpAbout.OnTriggered(a.OnMnuHelpHomepage)
|
||||
|
||||
return withBoltDBReference(b_ref, func(db *bolt.DB) error {
|
||||
return db.View(func(tx *bolt.Tx) error {
|
||||
a.ui.actionCreate_Bolt_database_from_zip.OnTriggered(a.Bolt_ImportZipToDatabase_OnTriggered)
|
||||
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walked the bucket chain, now run the user callback
|
||||
return fn(db, tx, bucket)
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func err2triple(err error) (int64, *C.char, int) {
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
return ERROR_AND_STOP_CALLING, C.CString(msg), len(msg)
|
||||
}
|
||||
|
||||
return FINISHED_OK, nil, 0
|
||||
}
|
||||
|
||||
//export Bolt_CreateBucket
|
||||
func Bolt_CreateBucket(b_ref ObjectReference, browse []string, newBucket string) (int64, *C.char, int) {
|
||||
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
if len(browse) == 0 {
|
||||
// Top-level bucket
|
||||
_, err := tx.CreateBucket([]byte(newBucket))
|
||||
return err
|
||||
|
||||
} else {
|
||||
// Deeper bucket
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walked the bucket chain, now create the new bucket
|
||||
_, err = bucket.CreateBucket([]byte(newBucket))
|
||||
return err
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
//export Bolt_DeleteBucket
|
||||
func Bolt_DeleteBucket(b_ref ObjectReference, browse []string, delBucket string) (int64, *C.char, int) {
|
||||
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
if len(browse) == 0 {
|
||||
// Top-level bucket
|
||||
return tx.DeleteBucket([]byte(delBucket))
|
||||
|
||||
} else {
|
||||
// Deeper bucket
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Walked the bucket chain, now delete the selected bucket
|
||||
return bucket.DeleteBucket([]byte(delBucket))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
//export Bolt_SetItem
|
||||
func Bolt_SetItem(b_ref ObjectReference, browse []string, key, val string) (int64, *C.char, int) {
|
||||
if len(browse) == 0 {
|
||||
return err2triple(errors.New("Can't create top-level items"))
|
||||
}
|
||||
|
||||
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Put([]byte(key), []byte(val))
|
||||
})
|
||||
})
|
||||
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
//export Bolt_DeleteItem
|
||||
func Bolt_DeleteItem(b_ref ObjectReference, browse []string, key string) (int64, *C.char, int) {
|
||||
if len(browse) == 0 {
|
||||
return err2triple(errors.New("Can't create top-level items"))
|
||||
}
|
||||
|
||||
err := withBoltDBReference(b_ref, func(db *bolt.DB) error {
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
bucket, err := walkBuckets(tx, browse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bucket.Delete([]byte(key))
|
||||
})
|
||||
})
|
||||
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
type CallResponse struct {
|
||||
s string
|
||||
e error
|
||||
}
|
||||
|
||||
//export Bolt_DBStats
|
||||
func Bolt_DBStats(b ObjectReference) (int64, *C.char, int) {
|
||||
var stats bolt.Stats
|
||||
|
||||
err := withBoltDBReference(b, func(db *bolt.DB) error {
|
||||
stats = db.Stats()
|
||||
return nil
|
||||
})
|
||||
|
||||
jBytes, err := json.Marshal(stats)
|
||||
if err != nil {
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
return REAL_MESSAGE, C.CString(string(jBytes)), len(jBytes)
|
||||
}
|
||||
|
||||
//export Bolt_BucketStats
|
||||
func Bolt_BucketStats(b ObjectReference, browse []string) (int64, *C.char, int) {
|
||||
var stats bolt.BucketStats
|
||||
|
||||
err := withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
stats = bucket.Stats()
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
jBytes, err := json.Marshal(stats)
|
||||
if err != nil {
|
||||
return err2triple(err)
|
||||
}
|
||||
|
||||
return REAL_MESSAGE, C.CString(string(jBytes)), len(jBytes)
|
||||
}
|
||||
|
||||
type NextCall struct {
|
||||
content chan CallResponse
|
||||
}
|
||||
|
||||
//export Bolt_ListBuckets
|
||||
func Bolt_ListBuckets(b ObjectReference, browse []string) ObjectReference {
|
||||
pNC := &NextCall{
|
||||
content: make(chan CallResponse, 0),
|
||||
}
|
||||
|
||||
pNC_Ref := gms.Put(pNC)
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
|
||||
if len(browse) == 0 {
|
||||
// root mode
|
||||
err = withBoltDBReference(b, func(db *bolt.DB) error {
|
||||
return db.View(func(tx *bolt.Tx) error {
|
||||
return tx.ForEach(func(k []byte, _ *bolt.Bucket) error {
|
||||
pNC.content <- CallResponse{s: string(k)}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
} else {
|
||||
// Nested-mode
|
||||
err = withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
return bucket.ForEach(func(k, v []byte) error {
|
||||
// non-nil v means it's a data item
|
||||
if v == nil {
|
||||
pNC.content <- CallResponse{s: string(k)}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
//
|
||||
|
||||
a.ui.Buckets.OnCurrentItemChanged(a.OnNavChange)
|
||||
a.ui.Buckets.SetContextMenuPolicy(qt.CustomContextMenu)
|
||||
a.ui.Buckets.OnCustomContextMenuRequested(a.OnNavContextPopup)
|
||||
a.ui.Buckets.OnExpanded(func(index *qt.QModelIndex) {
|
||||
if index == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
pNC.content <- CallResponse{e: err}
|
||||
item := a.ui.Buckets.ItemFromIndex(index)
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
|
||||
close(pNC.content)
|
||||
}()
|
||||
|
||||
return pNC_Ref
|
||||
}
|
||||
|
||||
//export Bolt_ListItems
|
||||
func Bolt_ListItems(b ObjectReference, browse []string) ObjectReference {
|
||||
|
||||
pNC := &NextCall{
|
||||
content: make(chan CallResponse, 0),
|
||||
}
|
||||
|
||||
pNC_Ref := gms.Put(pNC)
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
|
||||
if len(browse) == 0 {
|
||||
err = errors.New("No bucket specified")
|
||||
} else {
|
||||
// Nested-mode
|
||||
err = withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
return bucket.ForEach(func(k, v []byte) error {
|
||||
if v == nil {
|
||||
return nil // nil v means it's a bucket, skip
|
||||
}
|
||||
|
||||
itemLength := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(itemLength, uint64(len(v)))
|
||||
pNC.content <- CallResponse{s: string(itemLength) + string(k)}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
pNC.content <- CallResponse{e: err}
|
||||
}
|
||||
|
||||
close(pNC.content)
|
||||
}()
|
||||
|
||||
return pNC_Ref
|
||||
}
|
||||
|
||||
//export Bolt_GetItem
|
||||
func Bolt_GetItem(b ObjectReference, browse []string, key string) (int64, *C.char, int) {
|
||||
var ret *C.char = nil
|
||||
var ret_len = 0
|
||||
|
||||
err := withBrowse_ReadOnly(b, browse, func(db *bolt.DB, tx *bolt.Tx, bucket *bolt.Bucket) error {
|
||||
d := bucket.Get([]byte(key))
|
||||
ret = C.CString(string(d))
|
||||
ret_len = len(d)
|
||||
return nil
|
||||
a.OnNavExpanding(item)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err2triple(err)
|
||||
}
|
||||
//
|
||||
|
||||
return REAL_MESSAGE, ret, ret_len
|
||||
}
|
||||
a.ui.actionRefresh.OnTriggered(a.RefreshCurrentItem)
|
||||
|
||||
//export GetNext
|
||||
func GetNext(oRef ObjectReference) (int64, *C.char, int) {
|
||||
pNC_Iface, ok := gms.Get(oRef)
|
||||
if !ok {
|
||||
return err2triple(NullObjectReference)
|
||||
}
|
||||
|
||||
pNC, ok := pNC_Iface.(*NextCall)
|
||||
if !ok {
|
||||
return err2triple(NullObjectReference)
|
||||
}
|
||||
|
||||
cr, ok := <-pNC.content
|
||||
if !ok {
|
||||
gms.Delete(oRef)
|
||||
return err2triple(nil)
|
||||
}
|
||||
|
||||
if cr.e != nil {
|
||||
msg := cr.e.Error()
|
||||
return ERROR_AND_KEEP_CALLING, C.CString(msg), len(msg)
|
||||
}
|
||||
|
||||
return REAL_MESSAGE, C.CString(cr.s), len(cr.s)
|
||||
}
|
||||
|
||||
//export Bolt_ListBucketsAtRoot
|
||||
func Bolt_ListBucketsAtRoot(b ObjectReference) ObjectReference {
|
||||
return Bolt_ListBuckets(b, nil)
|
||||
}
|
||||
|
||||
//export Bolt_Close
|
||||
func Bolt_Close(b ObjectReference) (*C.char, int) {
|
||||
err := withBoltDBReference(b, func(db *bolt.DB) error {
|
||||
return db.Close()
|
||||
a.ui.actionAbout_Qt.OnTriggered(func() {
|
||||
qt.QMessageBox_AboutQt2(a.ui.MainWindow.QWidget, APPNAME)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
return C.CString(msg), len(msg)
|
||||
a.contentTbl = NewTableState(a.ui.contentBox)
|
||||
a.contentTbl.OnEdited = func() {
|
||||
a.ui.actionApply_changes.SetEnabled(a.contentTbl.AnyChanges())
|
||||
}
|
||||
a.ui.actionDelete_row.OnTriggered(func() {
|
||||
a.contentTbl.DeleteSelectedRows()
|
||||
})
|
||||
a.ui.actionAddRow.OnTriggered(func() {
|
||||
a.contentTbl.InsertNewRow()
|
||||
})
|
||||
a.ui.actionApply_changes.OnTriggered(a.OnDataCommitClick)
|
||||
|
||||
gms.Delete(b)
|
||||
return nil, 0
|
||||
// a.ui.queryInput.OnTextChanged(a.OnQueryTextChanged) // apply syntax highlighting
|
||||
|
||||
a.ui.mnuExecute.OnTriggered(a.OnQueryExecute)
|
||||
|
||||
a.ui.queryInput.SetFont(vcl_monospace())
|
||||
|
||||
a.resultsTbl = NewTableState(a.ui.queryResult)
|
||||
|
||||
a.none = &noLoadedDatabase{}
|
||||
a.refreshContent(nil) // calls f.none.RenderForNav and sets up status bar content
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func main() {
|
||||
// virtual
|
||||
// Qt 6 already uses PassThrough as the default fractional scaling policy,
|
||||
// no need to change anything here
|
||||
|
||||
qt.NewQApplication(os.Args)
|
||||
|
||||
app := newApp()
|
||||
app.ui.MainWindow.Show()
|
||||
|
||||
qt.QApplication_Exec()
|
||||
}
|
||||
|
||||
func (f *App) OnMnuFileExitClick() {
|
||||
f.ui.MainWindow.Close()
|
||||
}
|
||||
|
||||
func (f *App) OnMnuHelpHomepage() {
|
||||
url := qt.NewQUrl3(HOMEPAGE_URL)
|
||||
ok := qt.QDesktopServices_OpenUrl(url)
|
||||
url.Delete()
|
||||
|
||||
if !ok {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "Error opening browser")
|
||||
}
|
||||
}
|
||||
|
||||
func (f *App) OnMenuHelpVersion() {
|
||||
connector := evLdbConnection{}
|
||||
|
||||
ld, name, err := connector.Connect(context.Background())
|
||||
if err != nil {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
f.addTopLevelDatabaseConnection(ld, name)
|
||||
}
|
||||
|
||||
func (f *App) OnNavContextPopup(pos *qt.QPoint) {
|
||||
|
||||
curItem := f.ui.Buckets.ItemAt(pos) // f.ui.Buckets.Selected()
|
||||
if curItem == nil {
|
||||
// Nothing is selected at all
|
||||
return
|
||||
}
|
||||
|
||||
bucketPath, err := f.getBucketPathFor(curItem)
|
||||
if err != nil {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
ld := f.getLoadedDatabaseFor(curItem)
|
||||
|
||||
mnu := qt.NewQMenu2()
|
||||
|
||||
mnuRefresh := mnu.AddAction2(qt.NewQIcon4(`:/assets/arrow_refresh.png`), "Refresh")
|
||||
mnuRefresh.OnTriggered(func() { f.OnNavContextRefresh(curItem) })
|
||||
|
||||
// Check what custom actions the ndata->db itself wants to add
|
||||
actions, err := ld.NavContext(bucketPath)
|
||||
if err != nil {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(actions) > 0 {
|
||||
mnu.AddSeparator()
|
||||
|
||||
for _, action := range actions {
|
||||
cb := action.Callback // Copy to avoid reuse of loop variable
|
||||
|
||||
mnuAction := mnu.AddActionWithText(action.Name)
|
||||
mnuAction.OnTriggered(func() {
|
||||
err := cb(curItem, bucketPath)
|
||||
if err != nil {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
|
||||
}
|
||||
f.OnNavContextRefresh(curItem)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if curItem.Parent() == nil {
|
||||
// Top-level item (database connection). Allow closing by right-click.
|
||||
mnu.AddSeparator()
|
||||
|
||||
mnuClose := mnu.AddAction2(qt.NewQIcon4(`:/assets/database_delete.png`), "Close")
|
||||
mnuClose.OnTriggered(f.OnNavContextClose)
|
||||
}
|
||||
|
||||
// Show popup
|
||||
globalPos := f.ui.Buckets.MapToGlobalWithQPoint(pos)
|
||||
mnu.Popup(globalPos)
|
||||
mnu.SetAttribute2(qt.WA_DeleteOnClose, true)
|
||||
}
|
||||
|
||||
func (f *App) RefreshCurrentItem() {
|
||||
curItem := f.ui.Buckets.CurrentItem()
|
||||
if curItem == nil {
|
||||
return // nothing to do
|
||||
}
|
||||
|
||||
f.OnNavContextRefresh(curItem) // Refresh LHS pane/children
|
||||
f.OnNavChange(curItem, curItem) // Refresh RHS pane/data content and warn if unsaved changes
|
||||
}
|
||||
|
||||
func (f *App) OnNavContextRefresh(item *qt.QTreeWidgetItem) {
|
||||
isExpanded := item.IsExpanded()
|
||||
|
||||
// Reset nav node to 'unloaded' state
|
||||
item.SetExpanded(false)
|
||||
children := item.TakeChildren()
|
||||
for _, child := range children {
|
||||
child.Delete()
|
||||
}
|
||||
|
||||
// Set up with fake children again
|
||||
item.SetChildIndicatorPolicy(qt.QTreeWidgetItem__ShowIndicator) // dynamically populate in OnNavExpanding
|
||||
|
||||
// Trigger a virtual reload
|
||||
// Calls OnNavExpanding to dynamically detect children
|
||||
|
||||
f.OnNavExpanding(item)
|
||||
|
||||
// Restore previous gui state
|
||||
item.SetExpanded(isExpanded)
|
||||
}
|
||||
|
||||
func (f *App) OnDataCommitClick() {
|
||||
if !f.ui.contentBox.IsEnabled() {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "Can't apply changes: editing is disabled")
|
||||
return // Not an active data view
|
||||
}
|
||||
|
||||
if !f.contentTbl.allowEdit {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "Can't apply changes: database is not editable")
|
||||
return // ??? Shouldn't be able to reach this point
|
||||
}
|
||||
|
||||
node := f.ui.Buckets.CurrentItem()
|
||||
if node == nil {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "No database selected")
|
||||
return
|
||||
}
|
||||
|
||||
ld := f.getLoadedDatabaseFor(node)
|
||||
editableLd, ok := ld.(editableLoadedDatabase)
|
||||
if !ok {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "Unsupported action for this database")
|
||||
return
|
||||
}
|
||||
|
||||
bucketPath, err := f.getBucketPathFor(node)
|
||||
if err != nil {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = editableLd.ApplyChanges(f.contentTbl, bucketPath)
|
||||
if err != nil {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
|
||||
// fallthrough to refresh
|
||||
}
|
||||
|
||||
// Refresh content
|
||||
// This disables the 'apply changes' button again as there are no remaining edits
|
||||
// Don't call onNavChange here because we don't want to warn about changes
|
||||
f.refreshContent(node) // Refresh RHS pane/data content
|
||||
|
||||
// Preserve scroll position
|
||||
// (Happens automatically with QTableView)
|
||||
}
|
||||
|
||||
func (f *App) OnNavContextClose() {
|
||||
curItem := f.ui.Buckets.CurrentItem()
|
||||
if curItem == nil {
|
||||
return // Nothing selected (shouldn't happen)
|
||||
}
|
||||
if curItem.Parent() != nil {
|
||||
return // Selection is not top-level DB connection (shouldn't happen)
|
||||
}
|
||||
|
||||
ldID := curItem.Data(0, LoadedDatabaseIdRole).ToInt()
|
||||
ld := f.dbs[ldID]
|
||||
|
||||
ld.Close()
|
||||
curItem.Delete() // n.b. This triggers OnNavChange, which will then re-render from noLoadedDatabase{}
|
||||
|
||||
// We can also nil-out the entry in our .db array so Go can GC everything else
|
||||
// Keep the slot, though, just to avoid complexity
|
||||
f.dbs[ldID] = nil
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
func (f *App) OnQueryTextChanged() {
|
||||
|
||||
// FIXME changing the text colour calls the onchange handler recursively
|
||||
// FIXME changing the text colour pushes into the undo stack
|
||||
|
||||
// Preserve
|
||||
f.ui.queryInput.Lines().BeginUpdate()
|
||||
origPos := f.ui.queryInput.SelStart()
|
||||
origLen := f.ui.queryInput.SelLength()
|
||||
defer func() {
|
||||
f.ui.queryInput.SetSelStart(origPos)
|
||||
f.ui.queryInput.SetSelLength(origLen)
|
||||
f.ui.queryInput.Lines().EndUpdate()
|
||||
}()
|
||||
|
||||
tx := strings.ToLower(f.ui.queryInput.Text())
|
||||
|
||||
// Reset all existing colors
|
||||
f.ui.queryInput.SetSelStart(0)
|
||||
f.ui.queryInput.SetSelLength(int32(len(tx)))
|
||||
f.ui.queryInput.SelAttributes().SetColor(colors.ClBlack)
|
||||
|
||||
searchPos := 0
|
||||
for {
|
||||
matchPos := strings.Index(tx[searchPos:], "select")
|
||||
if matchPos == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
matchPos += searchPos // compensate for slicing
|
||||
|
||||
f.ui.queryInput.SetSelStart(int32(matchPos))
|
||||
f.ui.queryInput.SetSelLength(6)
|
||||
f.ui.queryInput.SelAttributes().SetColor(colors.ClRed)
|
||||
|
||||
searchPos = matchPos + 6 // strlen(SELECT)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func (f *App) OnQueryExecute() {
|
||||
// If query tab is not selected, switch to it, but do not exec
|
||||
if f.ui.tabWidget.CurrentIndex() != 2 {
|
||||
f.ui.tabWidget.SetCurrentIndex(2)
|
||||
return
|
||||
}
|
||||
|
||||
queryString := f.ui.queryInput.ToPlainText()
|
||||
|
||||
if selectedText := f.ui.queryInput.TextCursor().SelectedText(); len(selectedText) > 0 {
|
||||
queryString = selectedText // Just the selected text
|
||||
}
|
||||
|
||||
if strings.TrimSpace(queryString) == "" {
|
||||
return // prevent blank query
|
||||
}
|
||||
|
||||
// Execute
|
||||
node := f.ui.Buckets.CurrentItem()
|
||||
if node == nil {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "No database selected")
|
||||
return
|
||||
}
|
||||
|
||||
ld := f.getLoadedDatabaseFor(node)
|
||||
|
||||
// bucketPath is only used for some databases, not for others
|
||||
bucketPath, err := f.getBucketPathFor(node)
|
||||
if err != nil {
|
||||
panic(err) // shouldn't happen
|
||||
}
|
||||
|
||||
queryableLd, ok := ld.(queryableLoadedDatabase)
|
||||
if !ok {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, "Unsupported action for this database")
|
||||
return
|
||||
}
|
||||
|
||||
f.resultsTbl.Wipeout()
|
||||
err = queryableLd.ExecQuery(queryString, bucketPath, f.resultsTbl)
|
||||
if err != nil {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (f *App) OnNavChange(node *qt.QTreeWidgetItem, prev *qt.QTreeWidgetItem) {
|
||||
|
||||
// If there are unsaved changes, we may want to block this change / switch
|
||||
// back to the previous selection
|
||||
// Prompt for confirmation first
|
||||
if f.contentTbl.AnyChanges() {
|
||||
ok := qt.QMessageBox_Warning2(f.ui.MainWindow.QWidget, APPNAME, "Your changes have not been saved.", qt.QMessageBox__Ok, qt.QMessageBox__Discard)
|
||||
if ok != int(qt.QMessageBox__Discard) {
|
||||
|
||||
// We want to stop this change
|
||||
if prev == nil {
|
||||
return // that's enough
|
||||
}
|
||||
|
||||
// We need to block recursive triggering
|
||||
// First time, to update , but doesn't update selection stat
|
||||
f.ui.Buckets.BlockSignals(true)
|
||||
f.ui.Buckets.SetCurrentItem(prev) // Also sets selection
|
||||
f.ui.Buckets.BlockSignals(false)
|
||||
|
||||
f.ui.Buckets.SetEnabled(false)
|
||||
|
||||
// On next event loop tick:
|
||||
mainthread.Start(func() {
|
||||
|
||||
// Second time
|
||||
// Since it's already set, won't trigger onNavChange
|
||||
f.ui.Buckets.SetCurrentItem(prev)
|
||||
f.ui.Buckets.SetEnabled(true)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if node != prev {
|
||||
// Changing tables (not just refreshing the current table)
|
||||
// Reset scroll position to top
|
||||
f.ui.contentBox.ScrollToTop()
|
||||
}
|
||||
|
||||
// OK, continue with change
|
||||
f.refreshContent(node)
|
||||
}
|
||||
|
||||
func (f *App) refreshContent(node *qt.QTreeWidgetItem) {
|
||||
|
||||
var ld loadedDatabase = f.none
|
||||
var bucketPath []string = nil
|
||||
|
||||
if node != nil {
|
||||
ld = f.getLoadedDatabaseFor(node)
|
||||
var err error
|
||||
bucketPath, err = f.getBucketPathFor(node)
|
||||
if err != nil {
|
||||
panic(err) // shouldn't happen
|
||||
}
|
||||
}
|
||||
|
||||
f.ui.propertiesBox.Clear()
|
||||
propertiesText, err := ld.Properties(bucketPath)
|
||||
if err != nil {
|
||||
f.ui.propertiesBox.SetPlainText("Error loading properties: " + err.Error())
|
||||
} else {
|
||||
f.ui.propertiesBox.SetPlainText(propertiesText)
|
||||
}
|
||||
|
||||
// Load database content
|
||||
f.contentTbl.Wipeout()
|
||||
err = ld.RenderForNav(f.contentTbl, bucketPath) // Handover to the database type's own renderer function
|
||||
if err != nil {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, fmt.Sprintf("Loading contents of bucket %v: %s", bucketPath, err.Error()))
|
||||
|
||||
// Ensure elements are disabled
|
||||
f.ui.contentBox.SetEnabled(false)
|
||||
}
|
||||
|
||||
// Toggle the Edit functionality
|
||||
// Do this *after* RenderForNav as it disables editing by default.
|
||||
_, editable := ld.(editableLoadedDatabase)
|
||||
editable = editable && f.contentTbl.IsReady() // if there was a failure loading, don't allow edit
|
||||
f.ui.actionApply_changes.SetEnabled(false) // will be enabled after add/delete/edit
|
||||
f.ui.actionDelete_row.SetEnabled(editable)
|
||||
f.ui.actionAddRow.SetEnabled(editable)
|
||||
f.contentTbl.SetAllowEditing(editable)
|
||||
|
||||
// Toggle the Query functionality
|
||||
_, queryable := ld.(queryableLoadedDatabase)
|
||||
f.ui.queryInput.SetEnabled(queryable)
|
||||
f.ui.queryResult.SetEnabled(queryable)
|
||||
f.ui.mnuExecute.SetEnabled(queryable)
|
||||
|
||||
// We're in charge of common status bar text updates
|
||||
// Find database displayname
|
||||
f.ui.MainWindow.StatusBar().ShowMessage(f.DatabaseDisplayName(node) + " | " + ld.DriverName())
|
||||
}
|
||||
|
||||
func (f *App) DatabaseDisplayName(item *qt.QTreeWidgetItem) string {
|
||||
if item == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// The database displayname is the text content from the topmost parent of this item
|
||||
for {
|
||||
parent := item.Parent()
|
||||
if parent == nil {
|
||||
break
|
||||
}
|
||||
|
||||
item = parent
|
||||
}
|
||||
|
||||
return item.Text(0)
|
||||
}
|
||||
|
||||
func (f *App) handleNavExpansion(item *qt.QTreeWidgetItem, recurseDepth int) {
|
||||
|
||||
if item == nil {
|
||||
panic("Expanding a nil item?")
|
||||
}
|
||||
|
||||
if item.ChildCount() > 0 {
|
||||
// We've already virtual-expanded this item once, don't repeat it
|
||||
return
|
||||
}
|
||||
|
||||
bucketPath, err := f.getBucketPathFor(item)
|
||||
if err != nil {
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = f.NavLoadChildren(item, bucketPath)
|
||||
if err != nil {
|
||||
|
||||
if errors.Is(err, ErrNavNotExist) {
|
||||
// The nav entry has been deleted.
|
||||
// This is a normal thing to happen when e.g. deleting a table
|
||||
// f.StatusBar.SetSimpleText(err.Error()) // Just gets overridden when the selection changes
|
||||
item.Delete()
|
||||
return
|
||||
}
|
||||
|
||||
qt.QMessageBox_Warning(f.ui.MainWindow.QWidget, APPNAME, err.Error())
|
||||
item.SetChildIndicatorPolicy(qt.QTreeWidgetItem__DontShowIndicatorWhenChildless) // disable dynamic-expansion in future
|
||||
return
|
||||
}
|
||||
|
||||
if recurseDepth > 0 {
|
||||
// While we're here - preload one single level deep (not any deeper)
|
||||
// This makes it "seem" like we don't have empty virtual subfolders most of the time
|
||||
|
||||
childCt := item.ChildCount()
|
||||
for i := 0; i < childCt; i++ {
|
||||
|
||||
cc := item.Child(i)
|
||||
f.handleNavExpansion(cc, recurseDepth-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *App) OnNavExpanding(item *qt.QTreeWidgetItem) {
|
||||
f.handleNavExpansion(item, 1)
|
||||
}
|
||||
|
||||
const (
|
||||
LoadedDatabaseIdRole = int(qt.UserRole + 100)
|
||||
BucketPathBSliceRole = int(qt.UserRole + 101)
|
||||
DataPrimaryKeyRole = int(qt.UserRole + 102) // sqlite
|
||||
)
|
||||
|
||||
// getLoadedDatabaseFor gets the ld ptr out of a child nav item.
|
||||
func (f *App) getLoadedDatabaseFor(item *qt.QTreeWidgetItem) loadedDatabase {
|
||||
ldID := item.Data(0, LoadedDatabaseIdRole).ToInt()
|
||||
return f.dbs[ldID]
|
||||
}
|
||||
|
||||
// getBucketPathFor gets the bucketPath array out of a child nav item.
|
||||
func (f *App) getBucketPathFor(item *qt.QTreeWidgetItem) ([]string, error) {
|
||||
bucketPathJsonArr := item.Data(0, BucketPathBSliceRole).ToByteArray()
|
||||
|
||||
return pathlist_decode(bucketPathJsonArr)
|
||||
}
|
||||
|
||||
func (f *App) NavLoadChildren(item *qt.QTreeWidgetItem, bucketPath []string) error {
|
||||
|
||||
ldID := item.Data(0, LoadedDatabaseIdRole).ToInt()
|
||||
ld := f.dbs[ldID]
|
||||
|
||||
// Find the child buckets from this point under the element
|
||||
nextBucketNames, err := ld.NavChildren(bucketPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to find child buckets under %q: %w", strings.Join(bucketPath, `/`), err)
|
||||
}
|
||||
|
||||
item.SetChildIndicatorPolicy(qt.QTreeWidgetItem__DontShowIndicatorWhenChildless) // n.b. maybe childless
|
||||
|
||||
// Populate child nodes
|
||||
for _, bucketName := range nextBucketNames {
|
||||
|
||||
node := qt.NewQTreeWidgetItem()
|
||||
node.SetText(0, formatUtf8([]byte(bucketName)))
|
||||
node.SetIcon(0, qt.NewQIcon4(`:/assets/table.png`))
|
||||
node.SetChildIndicatorPolicy(qt.QTreeWidgetItem__ShowIndicator) // dynamically populate in OnNavExpanding
|
||||
|
||||
childPath := make([]string, 0, len(bucketPath)+1)
|
||||
childPath = append(childPath, bucketPath...)
|
||||
childPath = append(childPath, bucketName)
|
||||
|
||||
childPathJson, err := pathlist_encode(childPath)
|
||||
if err != nil {
|
||||
panic(err) // Shouldn't happen
|
||||
}
|
||||
|
||||
node.SetData(0, LoadedDatabaseIdRole, qt.NewQVariant4(ldID))
|
||||
node.SetData(0, BucketPathBSliceRole, qt.NewQVariant12(childPathJson))
|
||||
|
||||
item.AddChild(node)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *App) addTopLevelDatabaseConnection(ld loadedDatabase, displayName string) {
|
||||
|
||||
nav := qt.NewQTreeWidgetItem()
|
||||
nav.SetText(0, displayName)
|
||||
nav.SetChildIndicatorPolicy(qt.QTreeWidgetItem__ShowIndicator) // dynamically populate in OnNavExpanding
|
||||
nav.SetIcon(0, qt.NewQIcon4(`:/assets/database.png`))
|
||||
|
||||
// Keepalive for the *ld pointer itself
|
||||
ldID := f.dbs_next
|
||||
f.dbs_next++
|
||||
f.dbs[ldID] = ld
|
||||
|
||||
emptyData, err := pathlist_encode([]string{}) // Initial browse position
|
||||
if err != nil {
|
||||
panic(err) // can't happen
|
||||
}
|
||||
|
||||
nav.SetData(0, LoadedDatabaseIdRole, qt.NewQVariant4(ldID))
|
||||
nav.SetData(0, BucketPathBSliceRole, qt.NewQVariant12(emptyData))
|
||||
|
||||
f.ui.Buckets.AddTopLevelItem(nav)
|
||||
f.ui.Buckets.SetCurrentItem(nav) // Select new element
|
||||
|
||||
f.handleNavExpansion(nav, 0) // Load child contents but do not recurse further
|
||||
}
|
||||
|
||||
370
mainwindow.go
Normal file
@@ -0,0 +1,370 @@
|
||||
// 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 mainwindow.ui -OutFile mainwindow.go -Qt6
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
qt "github.com/mappu/miqt/qt6"
|
||||
)
|
||||
|
||||
type MainWindowUi struct {
|
||||
MainWindow *qt.QMainWindow
|
||||
centralwidget *qt.QWidget
|
||||
gridLayout *qt.QGridLayout
|
||||
splitter *qt.QSplitter
|
||||
Buckets *qt.QTreeWidget
|
||||
tabWidget *qt.QTabWidget
|
||||
tabProperties *qt.QWidget
|
||||
gridLayout_2 *qt.QGridLayout
|
||||
propertiesBox *qt.QTextEdit
|
||||
tabData *qt.QWidget
|
||||
verticalLayout *qt.QVBoxLayout
|
||||
contentBox *qt.QTableView
|
||||
tabQuery *qt.QWidget
|
||||
verticalLayout_2 *qt.QVBoxLayout
|
||||
splitter_2 *qt.QSplitter
|
||||
queryInput *qt.QPlainTextEdit
|
||||
queryResult *qt.QTableView
|
||||
menubar *qt.QMenuBar
|
||||
menu_File *qt.QMenu
|
||||
menu_Query *qt.QMenu
|
||||
menu_Help *qt.QMenu
|
||||
menu_Data *qt.QMenu
|
||||
menu_Tools *qt.QMenu
|
||||
statusbar *qt.QStatusBar
|
||||
toolBar *qt.QToolBar
|
||||
actionE_xit *qt.QAction
|
||||
mnuExecute *qt.QAction
|
||||
mnuDriverVersions *qt.QAction
|
||||
mnuHelpAbout *qt.QAction
|
||||
actionConnect *qt.QAction
|
||||
actionRefresh *qt.QAction
|
||||
actionAbout_Qt *qt.QAction
|
||||
actionAddRow *qt.QAction
|
||||
actionDelete_row *qt.QAction
|
||||
actionApply_changes *qt.QAction
|
||||
actionConnectionManager *qt.QAction
|
||||
actionCreate_Bolt_database_from_zip *qt.QAction
|
||||
}
|
||||
|
||||
// NewMainWindowUi creates all Qt widget classes for MainWindow.
|
||||
func NewMainWindowUi() *MainWindowUi {
|
||||
ui := &MainWindowUi{}
|
||||
ui.MainWindow = qt.NewQMainWindow(nil)
|
||||
MainWindow__objectName := qt.NewQAnyStringView3("MainWindow")
|
||||
ui.MainWindow.SetObjectName(*MainWindow__objectName)
|
||||
MainWindow__objectName.Delete() // setter copied value
|
||||
ui.MainWindow.Resize(1280, 640)
|
||||
icon0 := qt.NewQIcon()
|
||||
icon0.AddFile4(":/assets/database_lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.MainWindow.SetWindowIcon(icon0)
|
||||
ui.actionE_xit = qt.NewQAction()
|
||||
actionE_xit__objectName := qt.NewQAnyStringView3("actionE_xit")
|
||||
ui.actionE_xit.SetObjectName(*actionE_xit__objectName)
|
||||
actionE_xit__objectName.Delete() // setter copied value
|
||||
/* miqt-uic: no handler for QAction property 'menuRole' */
|
||||
ui.mnuExecute = qt.NewQAction()
|
||||
mnuExecute__objectName := qt.NewQAnyStringView3("mnuExecute")
|
||||
ui.mnuExecute.SetObjectName(*mnuExecute__objectName)
|
||||
mnuExecute__objectName.Delete() // setter copied value
|
||||
icon1 := qt.NewQIcon()
|
||||
icon1.AddFile4(":/assets/resultset_next.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.mnuExecute.SetIcon(icon1)
|
||||
ui.mnuDriverVersions = qt.NewQAction()
|
||||
mnuDriverVersions__objectName := qt.NewQAnyStringView3("mnuDriverVersions")
|
||||
ui.mnuDriverVersions.SetObjectName(*mnuDriverVersions__objectName)
|
||||
mnuDriverVersions__objectName.Delete() // setter copied value
|
||||
ui.mnuHelpAbout = qt.NewQAction()
|
||||
mnuHelpAbout__objectName := qt.NewQAnyStringView3("mnuHelpAbout")
|
||||
ui.mnuHelpAbout.SetObjectName(*mnuHelpAbout__objectName)
|
||||
mnuHelpAbout__objectName.Delete() // setter copied value
|
||||
icon2 := qt.NewQIcon()
|
||||
icon2.AddFile4(":/assets/help.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.mnuHelpAbout.SetIcon(icon2)
|
||||
/* miqt-uic: no handler for QAction property 'menuRole' */
|
||||
ui.actionConnect = qt.NewQAction()
|
||||
actionConnect__objectName := qt.NewQAnyStringView3("actionConnect")
|
||||
ui.actionConnect.SetObjectName(*actionConnect__objectName)
|
||||
actionConnect__objectName.Delete() // setter copied value
|
||||
icon3 := qt.NewQIcon()
|
||||
icon3.AddFile4(":/assets/database_add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.actionConnect.SetIcon(icon3)
|
||||
ui.actionRefresh = qt.NewQAction()
|
||||
actionRefresh__objectName := qt.NewQAnyStringView3("actionRefresh")
|
||||
ui.actionRefresh.SetObjectName(*actionRefresh__objectName)
|
||||
actionRefresh__objectName.Delete() // setter copied value
|
||||
icon4 := qt.NewQIcon()
|
||||
icon4.AddFile4(":/assets/arrow_refresh.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.actionRefresh.SetIcon(icon4)
|
||||
/* miqt-uic: no handler for QAction property 'menuRole' */
|
||||
ui.actionAbout_Qt = qt.NewQAction()
|
||||
actionAbout_Qt__objectName := qt.NewQAnyStringView3("actionAbout_Qt")
|
||||
ui.actionAbout_Qt.SetObjectName(*actionAbout_Qt__objectName)
|
||||
actionAbout_Qt__objectName.Delete() // setter copied value
|
||||
icon5 := qt.NewQIcon()
|
||||
icon5.AddFile4(":/assets/vendor_qt.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.actionAbout_Qt.SetIcon(icon5)
|
||||
/* miqt-uic: no handler for QAction property 'menuRole' */
|
||||
ui.actionAddRow = qt.NewQAction()
|
||||
actionAddRow__objectName := qt.NewQAnyStringView3("actionAddRow")
|
||||
ui.actionAddRow.SetObjectName(*actionAddRow__objectName)
|
||||
actionAddRow__objectName.Delete() // setter copied value
|
||||
icon6 := qt.NewQIcon()
|
||||
icon6.AddFile4(":/assets/add.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.actionAddRow.SetIcon(icon6)
|
||||
/* miqt-uic: no handler for QAction property 'menuRole' */
|
||||
ui.actionDelete_row = qt.NewQAction()
|
||||
actionDelete_row__objectName := qt.NewQAnyStringView3("actionDelete_row")
|
||||
ui.actionDelete_row.SetObjectName(*actionDelete_row__objectName)
|
||||
actionDelete_row__objectName.Delete() // setter copied value
|
||||
icon7 := qt.NewQIcon()
|
||||
icon7.AddFile4(":/assets/delete.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.actionDelete_row.SetIcon(icon7)
|
||||
/* miqt-uic: no handler for QAction property 'menuRole' */
|
||||
ui.actionApply_changes = qt.NewQAction()
|
||||
actionApply_changes__objectName := qt.NewQAnyStringView3("actionApply_changes")
|
||||
ui.actionApply_changes.SetObjectName(*actionApply_changes__objectName)
|
||||
actionApply_changes__objectName.Delete() // setter copied value
|
||||
icon8 := qt.NewQIcon()
|
||||
icon8.AddFile4(":/assets/pencil_go.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.actionApply_changes.SetIcon(icon8)
|
||||
/* miqt-uic: no handler for QAction property 'menuRole' */
|
||||
ui.actionConnectionManager = qt.NewQAction()
|
||||
actionConnectionManager__objectName := qt.NewQAnyStringView3("actionConnectionManager")
|
||||
ui.actionConnectionManager.SetObjectName(*actionConnectionManager__objectName)
|
||||
actionConnectionManager__objectName.Delete() // setter copied value
|
||||
icon9 := qt.NewQIcon()
|
||||
icon9.AddFile4(":/assets/database_key.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.actionConnectionManager.SetIcon(icon9)
|
||||
/* miqt-uic: no handler for QAction property 'menuRole' */
|
||||
ui.actionCreate_Bolt_database_from_zip = qt.NewQAction()
|
||||
actionCreate_Bolt_database_from_zip__objectName := qt.NewQAnyStringView3("actionCreate_Bolt_database_from_zip")
|
||||
ui.actionCreate_Bolt_database_from_zip.SetObjectName(*actionCreate_Bolt_database_from_zip__objectName)
|
||||
actionCreate_Bolt_database_from_zip__objectName.Delete() // setter copied value
|
||||
ui.centralwidget = qt.NewQWidget(ui.MainWindow.QWidget)
|
||||
centralwidget__objectName := qt.NewQAnyStringView3("centralwidget")
|
||||
ui.centralwidget.SetObjectName(*centralwidget__objectName)
|
||||
centralwidget__objectName.Delete() // setter copied value
|
||||
ui.gridLayout = qt.NewQGridLayout(ui.centralwidget)
|
||||
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.splitter = qt.NewQSplitter(ui.centralwidget)
|
||||
splitter__objectName := qt.NewQAnyStringView3("splitter")
|
||||
ui.splitter.SetObjectName(*splitter__objectName)
|
||||
splitter__objectName.Delete() // setter copied value
|
||||
ui.splitter.SetOrientation(qt.Horizontal)
|
||||
ui.splitter.SetChildrenCollapsible(false)
|
||||
ui.Buckets = qt.NewQTreeWidget(ui.splitter.QWidget)
|
||||
Buckets__objectName := qt.NewQAnyStringView3("Buckets")
|
||||
ui.Buckets.SetObjectName(*Buckets__objectName)
|
||||
Buckets__objectName.Delete() // setter copied value
|
||||
ui.Buckets.SetUniformRowHeights(true)
|
||||
ui.Buckets.SetHeaderHidden(true)
|
||||
/* miqt-uic: no handler for Buckets attribute 'headerVisible' */
|
||||
ui.Buckets.HeaderItem().SetText(0, "1")
|
||||
ui.splitter.AddWidget(ui.Buckets.QWidget)
|
||||
ui.tabWidget = qt.NewQTabWidget(ui.splitter.QWidget)
|
||||
tabWidget__objectName := qt.NewQAnyStringView3("tabWidget")
|
||||
ui.tabWidget.SetObjectName(*tabWidget__objectName)
|
||||
tabWidget__objectName.Delete() // setter copied value
|
||||
tabWidget__sizePolicy := qt.NewQSizePolicy()
|
||||
tabWidget__sizePolicy.SetHorizontalPolicy(qt.QSizePolicy__Expanding)
|
||||
tabWidget__sizePolicy.SetVerticalPolicy(qt.QSizePolicy__Expanding)
|
||||
tabWidget__sizePolicy.SetHorizontalStretch(1)
|
||||
tabWidget__sizePolicy.SetVerticalStretch(0)
|
||||
tabWidget__sizePolicy.SetHeightForWidth(ui.tabWidget.SizePolicy().HasHeightForWidth())
|
||||
ui.tabWidget.SetSizePolicy(*tabWidget__sizePolicy)
|
||||
tabWidget__sizePolicy.Delete() // setter copies values
|
||||
ui.tabProperties = qt.NewQWidget(ui.tabWidget.QWidget)
|
||||
tabProperties__objectName := qt.NewQAnyStringView3("tabProperties")
|
||||
ui.tabProperties.SetObjectName(*tabProperties__objectName)
|
||||
tabProperties__objectName.Delete() // setter copied value
|
||||
ui.gridLayout_2 = qt.NewQGridLayout(ui.tabProperties)
|
||||
gridLayout_2__objectName := qt.NewQAnyStringView3("gridLayout_2")
|
||||
ui.gridLayout_2.SetObjectName(*gridLayout_2__objectName)
|
||||
gridLayout_2__objectName.Delete() // setter copied value
|
||||
ui.gridLayout_2.SetContentsMargins(11, 11, 11, 11)
|
||||
ui.gridLayout_2.SetSpacing(6)
|
||||
ui.propertiesBox = qt.NewQTextEdit(ui.tabProperties)
|
||||
propertiesBox__objectName := qt.NewQAnyStringView3("propertiesBox")
|
||||
ui.propertiesBox.SetObjectName(*propertiesBox__objectName)
|
||||
propertiesBox__objectName.Delete() // setter copied value
|
||||
ui.propertiesBox.SetFrameShape(qt.QFrame__NoFrame)
|
||||
ui.propertiesBox.SetReadOnly(true)
|
||||
|
||||
ui.gridLayout_2.AddWidget2(ui.propertiesBox.QWidget, 0, 0)
|
||||
icon10 := qt.NewQIcon()
|
||||
icon10.AddFile4(":/assets/chart_bar.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.tabWidget.AddTab2(ui.tabProperties, icon10, "")
|
||||
ui.tabData = qt.NewQWidget(ui.tabWidget.QWidget)
|
||||
tabData__objectName := qt.NewQAnyStringView3("tabData")
|
||||
ui.tabData.SetObjectName(*tabData__objectName)
|
||||
tabData__objectName.Delete() // setter copied value
|
||||
ui.verticalLayout = qt.NewQVBoxLayout(ui.tabData)
|
||||
verticalLayout__objectName := qt.NewQAnyStringView3("verticalLayout")
|
||||
ui.verticalLayout.SetObjectName(*verticalLayout__objectName)
|
||||
verticalLayout__objectName.Delete() // setter copied value
|
||||
ui.verticalLayout.SetContentsMargins(11, 11, 11, 11)
|
||||
ui.verticalLayout.SetSpacing(6)
|
||||
ui.contentBox = qt.NewQTableView(ui.tabData)
|
||||
contentBox__objectName := qt.NewQAnyStringView3("contentBox")
|
||||
ui.contentBox.SetObjectName(*contentBox__objectName)
|
||||
contentBox__objectName.Delete() // setter copied value
|
||||
ui.contentBox.SetVerticalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
|
||||
ui.contentBox.SetHorizontalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
|
||||
|
||||
ui.verticalLayout.AddWidget(ui.contentBox.QWidget)
|
||||
icon11 := qt.NewQIcon()
|
||||
icon11.AddFile4(":/assets/table.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.tabWidget.AddTab2(ui.tabData, icon11, "")
|
||||
ui.tabQuery = qt.NewQWidget(ui.tabWidget.QWidget)
|
||||
tabQuery__objectName := qt.NewQAnyStringView3("tabQuery")
|
||||
ui.tabQuery.SetObjectName(*tabQuery__objectName)
|
||||
tabQuery__objectName.Delete() // setter copied value
|
||||
ui.verticalLayout_2 = qt.NewQVBoxLayout(ui.tabQuery)
|
||||
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.splitter_2 = qt.NewQSplitter(ui.tabQuery)
|
||||
splitter_2__objectName := qt.NewQAnyStringView3("splitter_2")
|
||||
ui.splitter_2.SetObjectName(*splitter_2__objectName)
|
||||
splitter_2__objectName.Delete() // setter copied value
|
||||
ui.splitter_2.SetOrientation(qt.Vertical)
|
||||
ui.splitter_2.SetChildrenCollapsible(false)
|
||||
ui.queryInput = qt.NewQPlainTextEdit(ui.splitter_2.QWidget)
|
||||
queryInput__objectName := qt.NewQAnyStringView3("queryInput")
|
||||
ui.queryInput.SetObjectName(*queryInput__objectName)
|
||||
queryInput__objectName.Delete() // setter copied value
|
||||
/* miqt-uic: no handler for queryInput property 'font' */
|
||||
ui.splitter_2.AddWidget(ui.queryInput.QWidget)
|
||||
ui.queryResult = qt.NewQTableView(ui.splitter_2.QWidget)
|
||||
queryResult__objectName := qt.NewQAnyStringView3("queryResult")
|
||||
ui.queryResult.SetObjectName(*queryResult__objectName)
|
||||
queryResult__objectName.Delete() // setter copied value
|
||||
ui.queryResult.SetVerticalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
|
||||
ui.queryResult.SetHorizontalScrollMode(qt.QAbstractItemView__ScrollPerPixel)
|
||||
ui.splitter_2.AddWidget(ui.queryResult.QWidget)
|
||||
|
||||
ui.verticalLayout_2.AddWidget(ui.splitter_2.QWidget)
|
||||
icon12 := qt.NewQIcon()
|
||||
icon12.AddFile4(":/assets/lightning.png", qt.NewQSize(), qt.QIcon__Normal, qt.QIcon__Off)
|
||||
ui.tabWidget.AddTab2(ui.tabQuery, icon12, "")
|
||||
ui.splitter.AddWidget(ui.tabWidget.QWidget)
|
||||
|
||||
ui.gridLayout.AddWidget2(ui.splitter.QWidget, 0, 0)
|
||||
ui.MainWindow.SetCentralWidget(ui.centralwidget) // Set central widget
|
||||
ui.menubar = qt.NewQMenuBar(ui.MainWindow.QWidget)
|
||||
menubar__objectName := qt.NewQAnyStringView3("menubar")
|
||||
ui.menubar.SetObjectName(*menubar__objectName)
|
||||
menubar__objectName.Delete() // setter copied value
|
||||
ui.menubar.Resize(1280, 29)
|
||||
ui.menu_File = qt.NewQMenu(ui.menubar.QWidget)
|
||||
menu_File__objectName := qt.NewQAnyStringView3("menu_File")
|
||||
ui.menu_File.SetObjectName(*menu_File__objectName)
|
||||
menu_File__objectName.Delete() // setter copied value
|
||||
ui.menu_File.QWidget.AddAction(ui.actionConnectionManager)
|
||||
ui.menu_File.QWidget.AddAction(ui.actionConnect)
|
||||
ui.menu_File.AddSeparator()
|
||||
ui.menu_File.QWidget.AddAction(ui.actionE_xit)
|
||||
ui.menu_Query = qt.NewQMenu(ui.menubar.QWidget)
|
||||
menu_Query__objectName := qt.NewQAnyStringView3("menu_Query")
|
||||
ui.menu_Query.SetObjectName(*menu_Query__objectName)
|
||||
menu_Query__objectName.Delete() // setter copied value
|
||||
ui.menu_Query.QWidget.AddAction(ui.mnuExecute)
|
||||
ui.menu_Help = qt.NewQMenu(ui.menubar.QWidget)
|
||||
menu_Help__objectName := qt.NewQAnyStringView3("menu_Help")
|
||||
ui.menu_Help.SetObjectName(*menu_Help__objectName)
|
||||
menu_Help__objectName.Delete() // setter copied value
|
||||
ui.menu_Help.QWidget.AddAction(ui.mnuDriverVersions)
|
||||
ui.menu_Help.AddSeparator()
|
||||
ui.menu_Help.QWidget.AddAction(ui.actionAbout_Qt)
|
||||
ui.menu_Help.QWidget.AddAction(ui.mnuHelpAbout)
|
||||
ui.menu_Data = qt.NewQMenu(ui.menubar.QWidget)
|
||||
menu_Data__objectName := qt.NewQAnyStringView3("menu_Data")
|
||||
ui.menu_Data.SetObjectName(*menu_Data__objectName)
|
||||
menu_Data__objectName.Delete() // setter copied value
|
||||
ui.menu_Data.QWidget.AddAction(ui.actionRefresh)
|
||||
ui.menu_Data.AddSeparator()
|
||||
ui.menu_Data.QWidget.AddAction(ui.actionAddRow)
|
||||
ui.menu_Data.QWidget.AddAction(ui.actionDelete_row)
|
||||
ui.menu_Data.QWidget.AddAction(ui.actionApply_changes)
|
||||
ui.menu_Tools = qt.NewQMenu(ui.menubar.QWidget)
|
||||
menu_Tools__objectName := qt.NewQAnyStringView3("menu_Tools")
|
||||
ui.menu_Tools.SetObjectName(*menu_Tools__objectName)
|
||||
menu_Tools__objectName.Delete() // setter copied value
|
||||
ui.menu_Tools.QWidget.AddAction(ui.actionCreate_Bolt_database_from_zip)
|
||||
ui.menubar.AddMenu(ui.menu_File)
|
||||
ui.menubar.AddMenu(ui.menu_Data)
|
||||
ui.menubar.AddMenu(ui.menu_Query)
|
||||
ui.menubar.AddMenu(ui.menu_Tools)
|
||||
ui.menubar.AddMenu(ui.menu_Help)
|
||||
ui.MainWindow.SetMenuBar(ui.menubar)
|
||||
ui.statusbar = qt.NewQStatusBar(ui.MainWindow.QWidget)
|
||||
statusbar__objectName := qt.NewQAnyStringView3("statusbar")
|
||||
ui.statusbar.SetObjectName(*statusbar__objectName)
|
||||
statusbar__objectName.Delete() // setter copied value
|
||||
ui.MainWindow.SetStatusBar(ui.statusbar)
|
||||
ui.toolBar = qt.NewQToolBar(ui.MainWindow.QWidget)
|
||||
toolBar__objectName := qt.NewQAnyStringView3("toolBar")
|
||||
ui.toolBar.SetObjectName(*toolBar__objectName)
|
||||
toolBar__objectName.Delete() // setter copied value
|
||||
/* miqt-uic: no handler for toolBar property 'iconSize' */
|
||||
ui.toolBar.SetToolButtonStyle(qt.ToolButtonIconOnly)
|
||||
ui.MainWindow.AddToolBar(qt.TopToolBarArea, ui.toolBar)
|
||||
/* miqt-uic: no handler for toolBar attribute 'toolBarBreak' */
|
||||
ui.toolBar.QWidget.AddAction(ui.actionConnectionManager)
|
||||
ui.toolBar.QWidget.AddAction(ui.actionConnect)
|
||||
ui.toolBar.AddSeparator()
|
||||
ui.toolBar.QWidget.AddAction(ui.actionRefresh)
|
||||
ui.toolBar.QWidget.AddAction(ui.actionAddRow)
|
||||
ui.toolBar.QWidget.AddAction(ui.actionDelete_row)
|
||||
ui.toolBar.QWidget.AddAction(ui.actionApply_changes)
|
||||
ui.toolBar.AddSeparator()
|
||||
ui.toolBar.QWidget.AddAction(ui.mnuExecute)
|
||||
|
||||
ui.Retranslate()
|
||||
|
||||
ui.tabWidget.SetCurrentIndex(0)
|
||||
|
||||
return ui
|
||||
}
|
||||
|
||||
// Retranslate reapplies all text translations.
|
||||
func (ui *MainWindowUi) Retranslate() {
|
||||
ui.MainWindow.SetWindowTitle(qt.QCoreApplication_Tr("QBolt"))
|
||||
ui.actionE_xit.SetText(qt.QMainWindow_Tr("E&xit"))
|
||||
ui.mnuExecute.SetText(qt.QMainWindow_Tr("E&xecute"))
|
||||
ui.mnuExecute.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F9")))
|
||||
ui.mnuDriverVersions.SetText(qt.QMainWindow_Tr("Driver versions..."))
|
||||
ui.mnuHelpAbout.SetText(qt.QMainWindow_Tr("&About QBolt"))
|
||||
ui.mnuHelpAbout.SetToolTip(qt.QMainWindow_Tr("About QBolt"))
|
||||
ui.mnuHelpAbout.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F1")))
|
||||
ui.actionConnect.SetText(qt.QMainWindow_Tr("Connect..."))
|
||||
ui.actionConnect.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("Ctrl+O")))
|
||||
ui.actionRefresh.SetText(qt.QMainWindow_Tr("Refresh"))
|
||||
ui.actionRefresh.SetShortcut(qt.NewQKeySequence2(qt.QMainWindow_Tr("F5")))
|
||||
ui.actionAbout_Qt.SetText(qt.QMainWindow_Tr("About Qt"))
|
||||
ui.actionAddRow.SetText(qt.QMainWindow_Tr("Add row"))
|
||||
ui.actionAddRow.SetToolTip(qt.QMainWindow_Tr("Add row"))
|
||||
ui.actionDelete_row.SetText(qt.QMainWindow_Tr("Delete row"))
|
||||
ui.actionApply_changes.SetText(qt.QMainWindow_Tr("Apply changes"))
|
||||
ui.actionConnectionManager.SetText(qt.QMainWindow_Tr("Connection Manager"))
|
||||
ui.actionConnectionManager.SetToolTip(qt.QMainWindow_Tr("Connection Manager"))
|
||||
ui.actionCreate_Bolt_database_from_zip.SetText(qt.QMainWindow_Tr("Create Bolt database from zip"))
|
||||
ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabProperties), qt.QTabWidget_Tr("Properties"))
|
||||
ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabData), qt.QTabWidget_Tr("Data"))
|
||||
ui.tabWidget.SetTabText(ui.tabWidget.IndexOf(ui.tabQuery), qt.QTabWidget_Tr("Query"))
|
||||
ui.menu_File.SetTitle(qt.QMenuBar_Tr("&File"))
|
||||
ui.menu_Query.SetTitle(qt.QMenuBar_Tr("&Query"))
|
||||
ui.menu_Help.SetTitle(qt.QMenuBar_Tr("&Help"))
|
||||
ui.menu_Data.SetTitle(qt.QMenuBar_Tr("&Data"))
|
||||
ui.menu_Tools.SetTitle(qt.QMenuBar_Tr("&Tools"))
|
||||
ui.toolBar.SetWindowTitle(qt.QMainWindow_Tr("toolBar"))
|
||||
}
|
||||
370
mainwindow.ui
Normal file
@@ -0,0 +1,370 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1280</width>
|
||||
<height>640</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>QBolt</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/database_lightning.png</normaloff>:/assets/database_lightning.png</iconset>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<widget class="QTreeWidget" name="Buckets">
|
||||
<property name="uniformRowHeights">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="headerHidden">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="headerVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string notr="true">1</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>1</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tabProperties">
|
||||
<attribute name="icon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/chart_bar.png</normaloff>:/assets/chart_bar.png</iconset>
|
||||
</attribute>
|
||||
<attribute name="title">
|
||||
<string>Properties</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QTextEdit" name="propertiesBox">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Shape::NoFrame</enum>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tabData">
|
||||
<attribute name="icon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/table.png</normaloff>:/assets/table.png</iconset>
|
||||
</attribute>
|
||||
<attribute name="title">
|
||||
<string>Data</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTableView" name="contentBox">
|
||||
<property name="verticalScrollMode">
|
||||
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
|
||||
</property>
|
||||
<property name="horizontalScrollMode">
|
||||
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="tabQuery">
|
||||
<attribute name="icon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/lightning.png</normaloff>:/assets/lightning.png</iconset>
|
||||
</attribute>
|
||||
<attribute name="title">
|
||||
<string>Query</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="childrenCollapsible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<widget class="QPlainTextEdit" name="queryInput">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Monospace</family>
|
||||
</font>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QTableView" name="queryResult">
|
||||
<property name="verticalScrollMode">
|
||||
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
|
||||
</property>
|
||||
<property name="horizontalScrollMode">
|
||||
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1280</width>
|
||||
<height>29</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menu_File">
|
||||
<property name="title">
|
||||
<string>&File</string>
|
||||
</property>
|
||||
<addaction name="actionConnectionManager"/>
|
||||
<addaction name="actionConnect"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionE_xit"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_Query">
|
||||
<property name="title">
|
||||
<string>&Query</string>
|
||||
</property>
|
||||
<addaction name="mnuExecute"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_Help">
|
||||
<property name="title">
|
||||
<string>&Help</string>
|
||||
</property>
|
||||
<addaction name="mnuDriverVersions"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionAbout_Qt"/>
|
||||
<addaction name="mnuHelpAbout"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_Data">
|
||||
<property name="title">
|
||||
<string>&Data</string>
|
||||
</property>
|
||||
<addaction name="actionRefresh"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionAddRow"/>
|
||||
<addaction name="actionDelete_row"/>
|
||||
<addaction name="actionApply_changes"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_Tools">
|
||||
<property name="title">
|
||||
<string>&Tools</string>
|
||||
</property>
|
||||
<addaction name="actionCreate_Bolt_database_from_zip"/>
|
||||
</widget>
|
||||
<addaction name="menu_File"/>
|
||||
<addaction name="menu_Data"/>
|
||||
<addaction name="menu_Query"/>
|
||||
<addaction name="menu_Tools"/>
|
||||
<addaction name="menu_Help"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
<widget class="QToolBar" name="toolBar">
|
||||
<property name="windowTitle">
|
||||
<string>toolBar</string>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
<height>16</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonStyle::ToolButtonIconOnly</enum>
|
||||
</property>
|
||||
<attribute name="toolBarArea">
|
||||
<enum>TopToolBarArea</enum>
|
||||
</attribute>
|
||||
<attribute name="toolBarBreak">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<addaction name="actionConnectionManager"/>
|
||||
<addaction name="actionConnect"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionRefresh"/>
|
||||
<addaction name="actionAddRow"/>
|
||||
<addaction name="actionDelete_row"/>
|
||||
<addaction name="actionApply_changes"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="mnuExecute"/>
|
||||
</widget>
|
||||
<action name="actionE_xit">
|
||||
<property name="text">
|
||||
<string>E&xit</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::QuitRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="mnuExecute">
|
||||
<property name="icon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/resultset_next.png</normaloff>:/assets/resultset_next.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>E&xecute</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F9</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="mnuDriverVersions">
|
||||
<property name="text">
|
||||
<string>Driver versions...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="mnuHelpAbout">
|
||||
<property name="icon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/help.png</normaloff>:/assets/help.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>&About QBolt</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>About QBolt</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F1</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::AboutRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionConnect">
|
||||
<property name="icon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/database_add.png</normaloff>:/assets/database_add.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Connect...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+O</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionRefresh">
|
||||
<property name="icon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/arrow_refresh.png</normaloff>:/assets/arrow_refresh.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Refresh</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F5</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAbout_Qt">
|
||||
<property name="icon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/vendor_qt.png</normaloff>:/assets/vendor_qt.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>About Qt</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::AboutQtRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAddRow">
|
||||
<property name="icon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/add.png</normaloff>:/assets/add.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add row</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Add row</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionDelete_row">
|
||||
<property name="icon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/delete.png</normaloff>:/assets/delete.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Delete row</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionApply_changes">
|
||||
<property name="icon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/pencil_go.png</normaloff>:/assets/pencil_go.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Apply changes</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionConnectionManager">
|
||||
<property name="icon">
|
||||
<iconset resource="embed.qrc">
|
||||
<normaloff>:/assets/database_key.png</normaloff>:/assets/database_key.png</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Connection Manager</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Connection Manager</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::MenuRole::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCreate_Bolt_database_from_zip">
|
||||
<property name="text">
|
||||
<string>Create Bolt database from zip</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="embed.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
219
qbolt/boltdb.cpp
@@ -1,219 +0,0 @@
|
||||
#include "boltdb.h"
|
||||
|
||||
#include <QtEndian>
|
||||
|
||||
BoltDB::BoltDB()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
BoltDB* BoltDB::createFrom(QString filePath, bool readOnly, QString &errorOut)
|
||||
{
|
||||
QByteArray filePathBytes(filePath.toUtf8());
|
||||
GoString filePathGS = Interop::toGoString_WeakRef(&filePathBytes);
|
||||
|
||||
auto open_ret = ::Bolt_Open(readOnly, filePathGS);
|
||||
if (open_ret.r2 != 0) {
|
||||
errorOut = QString::fromUtf8(open_ret.r1, open_ret.r2);
|
||||
free(open_ret.r1);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
BoltDB *ret = new BoltDB();
|
||||
ret->gmsDbRef = open_ret.r0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static const int ERROR_AND_STOP_CALLING = 100;
|
||||
static const int ERROR_AND_KEEP_CALLING = 101;
|
||||
static const int FINISHED_OK = 102;
|
||||
static const int REAL_MESSAGE = 103;
|
||||
|
||||
static bool handleTriple(int r0, char* r1, int64_t r2, QString& errorOut) {
|
||||
if (r0 == ERROR_AND_STOP_CALLING) {
|
||||
errorOut = QString::fromUtf8(r1, r2);
|
||||
free(r1);
|
||||
return false;
|
||||
|
||||
} else if (r0 == FINISHED_OK) {
|
||||
return true;
|
||||
|
||||
} else {
|
||||
// ?? unreachable
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
bool BoltDB::addBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut)
|
||||
{
|
||||
GoSliceManagedWrapper browse(bucketPath);
|
||||
GoString bucketNameGS = Interop::toGoString_WeakRef(&bucketName);
|
||||
auto resp = ::Bolt_CreateBucket(this->gmsDbRef, browse.slice, bucketNameGS);
|
||||
|
||||
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
|
||||
}
|
||||
|
||||
bool BoltDB::deleteBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut)
|
||||
{
|
||||
GoSliceManagedWrapper browse(bucketPath);
|
||||
GoString bucketNameGS = Interop::toGoString_WeakRef(&bucketName);
|
||||
auto resp = ::Bolt_DeleteBucket(this->gmsDbRef, browse.slice, bucketNameGS);
|
||||
|
||||
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
|
||||
}
|
||||
|
||||
bool BoltDB::setItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QByteArray value, QString& errorOut)
|
||||
{
|
||||
GoSliceManagedWrapper browse(bucketPath);
|
||||
GoString keyNameGS = Interop::toGoString_WeakRef(&keyName);
|
||||
GoString valueGS = Interop::toGoString_WeakRef(&value);
|
||||
auto resp = ::Bolt_SetItem(this->gmsDbRef, browse.slice, keyNameGS, valueGS);
|
||||
|
||||
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
|
||||
}
|
||||
|
||||
bool BoltDB::deleteItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QString& errorOut)
|
||||
{
|
||||
GoSliceManagedWrapper browse(bucketPath);
|
||||
GoString keyNameGS = Interop::toGoString_WeakRef(&keyName);
|
||||
auto resp = ::Bolt_DeleteItem(this->gmsDbRef, browse.slice, keyNameGS);
|
||||
|
||||
return handleTriple(resp.r0, resp.r1, resp.r2, errorOut);
|
||||
}
|
||||
|
||||
|
||||
bool BoltDB::listBucketsAtRoot(QString& errorOut, NameReciever cb)
|
||||
{
|
||||
auto listJob = ::Bolt_ListBucketsAtRoot(this->gmsDbRef);
|
||||
return pumpNext(listJob, errorOut, cb);
|
||||
}
|
||||
|
||||
bool BoltDB::listBuckets(const QList<QByteArray>& bucketPath, QString& errorOut, NameReciever cb)
|
||||
{
|
||||
if (bucketPath.size() == 0) {
|
||||
return listBucketsAtRoot(errorOut, cb);
|
||||
}
|
||||
|
||||
GoSliceManagedWrapper browse(bucketPath);
|
||||
auto listJob = ::Bolt_ListBuckets(this->gmsDbRef, browse.slice);
|
||||
return pumpNext(listJob, errorOut, cb);
|
||||
}
|
||||
|
||||
bool BoltDB::listKeys(const QList<QByteArray>& bucketPath, QString& errorOut, std::function<void(QByteArray, int64_t)> cb)
|
||||
{
|
||||
GoSliceManagedWrapper browse(bucketPath);
|
||||
auto listJob = ::Bolt_ListItems(this->gmsDbRef, browse.slice);
|
||||
return pumpNext(listJob, errorOut, [=](QByteArray b) {
|
||||
// First 8 bytes are little-endian uint64 len
|
||||
int64_t dataLen = qFromLittleEndian<qint64>(b.mid(0, 8));
|
||||
cb(b.mid(8), dataLen);
|
||||
});
|
||||
}
|
||||
|
||||
bool BoltDB::getData(const QList<QByteArray>& bucketPath, QByteArray key, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
|
||||
{
|
||||
GoSliceManagedWrapper browse(bucketPath);
|
||||
GoString keyGS = Interop::toGoString_WeakRef(&key);
|
||||
auto resp = ::Bolt_GetItem(this->gmsDbRef, browse.slice, keyGS);
|
||||
|
||||
if (resp.r0 == ERROR_AND_STOP_CALLING) {
|
||||
onError(QString::fromUtf8(resp.r1, resp.r2));
|
||||
free(resp.r1);
|
||||
return false;
|
||||
|
||||
} else if (resp.r0 == REAL_MESSAGE) {
|
||||
onSuccess(QByteArray(resp.r1, resp.r2));
|
||||
free(resp.r1);
|
||||
return true;
|
||||
|
||||
} else {
|
||||
// ?? unreachable
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
bool BoltDB::pumpNext(GoInt64 jobRef, QString& errorOut, NameReciever cb)
|
||||
{
|
||||
errorOut.clear();
|
||||
|
||||
for(;;) {
|
||||
auto gnr = ::GetNext(jobRef);
|
||||
|
||||
if (gnr.r0 == ERROR_AND_STOP_CALLING) {
|
||||
errorOut.append(QString::fromUtf8(gnr.r1, gnr.r2)); // log error
|
||||
free(gnr.r1);
|
||||
break; // done
|
||||
|
||||
} else if (gnr.r0 == ERROR_AND_KEEP_CALLING) {
|
||||
errorOut.append(QString::fromUtf8(gnr.r1, gnr.r2)); // log error
|
||||
free(gnr.r1);
|
||||
continue;
|
||||
|
||||
} else if (gnr.r0 == FINISHED_OK) {
|
||||
// Once we hit this, the go-side will clean up the channel / associated goroutines
|
||||
break;
|
||||
|
||||
} else if (gnr.r0 == REAL_MESSAGE) {
|
||||
cb(QByteArray(gnr.r1, gnr.r2));
|
||||
free(gnr.r1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return (errorOut.length() == 0);
|
||||
}
|
||||
|
||||
bool BoltDB::getStatsJSON(std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
|
||||
{
|
||||
auto statresp = Bolt_DBStats(this->gmsDbRef);
|
||||
|
||||
if (statresp.r0 == ERROR_AND_STOP_CALLING) {
|
||||
onError(QString::fromUtf8(statresp.r1, statresp.r2));
|
||||
free(statresp.r1);
|
||||
return false;
|
||||
|
||||
} else if (statresp.r0 == REAL_MESSAGE) {
|
||||
onSuccess(QByteArray(statresp.r1, statresp.r2));
|
||||
free(statresp.r1);
|
||||
return true;
|
||||
|
||||
} else {
|
||||
// ?? shouldn't be reachable
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool BoltDB::getBucketStatsJSON(const QList<QByteArray>& bucketPath, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError)
|
||||
{
|
||||
GoSliceManagedWrapper sliceWrapper(bucketPath);
|
||||
auto statresp = Bolt_BucketStats(this->gmsDbRef, sliceWrapper.slice);
|
||||
|
||||
if (statresp.r0 == ERROR_AND_STOP_CALLING) {
|
||||
QString err = QString::fromUtf8(statresp.r1, statresp.r2);
|
||||
free(statresp.r1);
|
||||
onError(err);
|
||||
return false;
|
||||
|
||||
} else if (statresp.r0 == REAL_MESSAGE) {
|
||||
onSuccess(QByteArray(statresp.r1, statresp.r2));
|
||||
free(statresp.r1);
|
||||
return true;
|
||||
|
||||
} else {
|
||||
// ?? shouldn't be reachable
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
BoltDB::~BoltDB()
|
||||
{
|
||||
auto err = ::Bolt_Close(this->gmsDbRef);
|
||||
if (err.r1 != 0) {
|
||||
// Error closing database!
|
||||
// Need to display an alert... somewhere
|
||||
|
||||
free(err.r0);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
#ifndef BOLTDB_H
|
||||
#define BOLTDB_H
|
||||
|
||||
#include "interop.h"
|
||||
#include <functional>
|
||||
|
||||
typedef std::function<void(QByteArray)> NameReciever;
|
||||
|
||||
class BoltDB
|
||||
{
|
||||
protected:
|
||||
BoltDB();
|
||||
|
||||
GoInt64 gmsDbRef;
|
||||
|
||||
public:
|
||||
static BoltDB* createFrom(QString filePath, bool readOnly, QString &errorOut);
|
||||
|
||||
bool listBucketsAtRoot(QString& errorOut, NameReciever cb);
|
||||
|
||||
bool listBuckets(const QList<QByteArray>& bucketPath, QString& errorOut, NameReciever cb);
|
||||
|
||||
bool addBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut);
|
||||
|
||||
bool deleteBucket(const QList<QByteArray>& bucketPath, QByteArray bucketName, QString& errorOut);
|
||||
|
||||
bool setItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QByteArray value, QString& errorOut);
|
||||
|
||||
bool deleteItem(const QList<QByteArray>& bucketPath, QByteArray keyName, QString& errorOut);
|
||||
|
||||
bool listKeys(const QList<QByteArray>& bucketPath, QString& errorOut, std::function<void(QByteArray, int64_t)> cb);
|
||||
|
||||
bool getData(const QList<QByteArray>& bucketPath, QByteArray key, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError);
|
||||
|
||||
bool getStatsJSON(std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError);
|
||||
|
||||
bool getBucketStatsJSON(const QList<QByteArray>& bucketPath, std::function<void(QByteArray)> onSuccess, std::function<void(QString)> onError);
|
||||
|
||||
~BoltDB();
|
||||
|
||||
protected:
|
||||
|
||||
bool pumpNext(GoInt64 jobRef, QString& errorOut, NameReciever cb);
|
||||
};
|
||||
|
||||
#endif // BOLTDB_H
|
||||
@@ -1,42 +0,0 @@
|
||||
#include "interop.h"
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
Interop::Interop()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
GoString Interop::toGoString_WeakRef(QByteArray *qba) {
|
||||
return GoString{qba->data(), qba->length()};
|
||||
}
|
||||
|
||||
int64_t Interop::GetMagic() {
|
||||
return ::GetMagic();
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
GoSliceManagedWrapper::GoSliceManagedWrapper(const QList<QByteArray>& qsl) :
|
||||
rawStrings(),
|
||||
slice(),
|
||||
strings(nullptr)
|
||||
{
|
||||
rawStrings.reserve(qsl.size());
|
||||
strings = new GoString[qsl.size()];
|
||||
|
||||
for (int i = 0; i < qsl.size(); ++i) {
|
||||
rawStrings.push_back( qsl.at(i) );
|
||||
strings[i].p = rawStrings[i].data();
|
||||
strings[i].n = rawStrings[i].size();
|
||||
}
|
||||
|
||||
slice.data = static_cast<void*>(strings);
|
||||
slice.len = qsl.size(); // * sizeof(GoString);
|
||||
slice.cap = slice.len;
|
||||
}
|
||||
|
||||
GoSliceManagedWrapper::~GoSliceManagedWrapper()
|
||||
{
|
||||
delete[] strings;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#ifndef INTEROP_H
|
||||
#define INTEROP_H
|
||||
|
||||
#include "qbolt_cgo.h"
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
|
||||
class GoSliceManagedWrapper {
|
||||
Q_DISABLE_COPY(GoSliceManagedWrapper)
|
||||
|
||||
public:
|
||||
GoSliceManagedWrapper(const QList<QByteArray>& qsl);
|
||||
~GoSliceManagedWrapper();
|
||||
protected:
|
||||
QList<QByteArray> rawStrings;
|
||||
public:
|
||||
GoSlice slice;
|
||||
GoString *strings;
|
||||
};
|
||||
|
||||
class Interop
|
||||
{
|
||||
public:
|
||||
Interop();
|
||||
|
||||
static GoString toGoString_WeakRef(QByteArray *qba);
|
||||
|
||||
static int64_t GetMagic();
|
||||
};
|
||||
|
||||
#endif // INTEROP_H
|
||||