46 Commits
ui ... v0.3.2

Author SHA1 Message Date
T. von Dein
2a01c6f9a7 fix-stdin-int (#30) 2026-09-01 13:17:24 +02:00
T. von Dein
497b8b3201 update go to 1.26 and dependencies (#29) 2026-09-01 10:42:56 +02:00
T. von Dein
1d45b2c6fe update linter config, satisfy linter (#28) 2026-08-31 22:06:25 +02:00
T. von Dein
5c9900d985 fix #26: check db file existence before trying to read from it (#27) 2026-08-31 21:12:50 +02:00
T. von Dein
cafc65aea7 fix #24: clarify template use of .Value (#25) 2026-04-29 08:33:10 +02:00
4fa42e27e4 pin to older release, see woodpecker-plugins/docker-buildx#306 2026-01-07 10:40:22 +01:00
T. von Dein
3b37a67e02 Replace fiber with net.http.ServeMux (#23) 2025-12-23 14:38:45 +01:00
5a705b0af0 fix links 2025-11-05 08:59:43 +01:00
7129f644d3 fix changelog link 2025-11-05 08:47:43 +01:00
9cb5a4b800 add test dependencies, separate test ci 2025-11-03 10:43:06 +01:00
53d69b5278 add dist makefile 2025-11-03 10:01:24 +01:00
T. von Dein
a16b9e796c move to codeberg (#22) 2025-11-03 09:15:27 +01:00
eb39f57199 fix copy/paste error 2025-10-14 16:51:22 +02:00
471d89eccd bump version 2025-10-02 22:26:47 +02:00
dependabot[bot]
b74bb79a45 Bump golang.org/x/crypto from 0.41.0 to 0.42.0 (#65) 2025-10-02 22:26:01 +02:00
dependabot[bot]
39436215d7 Bump github.com/spf13/cobra from 1.9.1 to 1.10.1 (#63) 2025-10-02 22:25:36 +02:00
dependabot[bot]
074547c356 Bump docker/login-action from 3.5.0 to 3.6.0 (#62) 2025-10-02 22:23:44 +02:00
dependabot[bot]
79d031d6a8 Bump actions/setup-go from 5 to 6 (#61) 2025-10-02 22:22:51 +02:00
dependabot[bot]
e42f664dfd Bump github.com/olekukonko/tablewriter from 1.0.9 to 1.1.0 (#64) 2025-10-02 22:22:18 +02:00
dependabot[bot]
e75062d2fd Bump google.golang.org/protobuf from 1.36.6 to 1.36.9 (#66) 2025-10-02 22:21:01 +02:00
6481e0cd15 push only ci 2025-10-02 22:20:41 +02:00
T.v.Dein
62c975206f Update golang (#60) 2025-09-18 20:13:02 +02:00
T.v.Dein
36a7d9f0c8 bump version (#59) 2025-09-18 19:55:15 +02:00
dependabot[bot]
c3eb61c6d2 Bump golang.org/x/term from 0.32.0 to 0.34.0 (#58) 2025-09-18 19:51:35 +02:00
dependabot[bot]
c8b3fd782e Bump actions/checkout from 4 to 5 (#57) 2025-09-18 19:51:15 +02:00
dependabot[bot]
d7c20ed245 Bump go.etcd.io/bbolt from 1.4.1 to 1.4.3 (#56) 2025-09-18 19:50:51 +02:00
dependabot[bot]
0e4eb60271 Bump golang.org/x/crypto from 0.38.0 to 0.41.0 (#55) 2025-09-18 19:47:19 +02:00
dependabot[bot]
9b25725f5f Bump docker/login-action from 3.4.0 to 3.5.0 (#54) 2025-09-18 19:46:47 +02:00
dependabot[bot]
e34c25c889 Bump github.com/gofiber/fiber/v2 from 2.52.8 to 2.52.9 (#53) 2025-09-18 19:45:54 +02:00
dependabot[bot]
b3a7064160 Bump github.com/olekukonko/tablewriter from 1.0.7 to 1.0.9 (#52) 2025-09-18 19:45:23 +02:00
T.v.Dein
1b00aeb23c update boltdb (#47) 2025-06-12 13:44:32 +02:00
T.v.Dein
aad9b31169 update dependencies (#46)
* update dependencies
2025-06-10 16:12:54 +02:00
dependabot[bot]
f22719a92b Bump golang.org/x/crypto from 0.31.0 to 0.37.0 (#35) 2025-04-25 14:27:49 +02:00
dependabot[bot]
b9f9655d7c Bump github.com/spf13/cobra from 1.8.1 to 1.9.1 (#28) 2025-04-25 14:18:06 +02:00
dependabot[bot]
edc08f12a7 Bump github.com/rogpeppe/go-internal from 1.13.1 to 1.14.1 (#29) 2025-04-25 14:13:34 +02:00
dependabot[bot]
e549ae8ed5 Bump docker/build-push-action from 6.13.0 to 6.15.0 (#32) 2025-04-25 13:58:35 +02:00
dependabot[bot]
2c0cc36551 Bump google.golang.org/protobuf from 1.36.5 to 1.36.6 (#33) 2025-04-25 13:58:17 +02:00
dependabot[bot]
7fe8004d41 Bump docker/login-action from 3.3.0 to 3.4.0 (#36) 2025-04-25 13:57:16 +02:00
T.v.Dein
12e3029145 refactored db.Set(): remove redundant View() call, use db.txGet() (#38)
* refactored db.Set(): remove redundant View() call, use db.txGet()
2025-04-25 13:56:46 +02:00
438248d267 build release bins w/o symbols and debug 2025-02-19 18:10:55 +01:00
05a812ebde add caution about deletion bug 2025-02-18 15:23:03 +01:00
T.v.Dein
ef85582488 fix #26: delete test now tests for correct value to not exist (#27)
Co-authored-by: Thomas von Dein <tom@vondein.org>
2025-02-18 15:19:05 +01:00
T.v.Dein
eb18e97c0d Add little UI script using fzf (#25)
* added Taglist template helper
* added simple fzf ui script
2025-02-17 13:47:20 +01:00
T.v.Dein
be13ec1111 fix #23: delete didn't work anymore (#24) 2025-02-17 13:46:59 +01:00
T.v.Dein
44bb6a0a26 add bolt logger interface with slog for debugging (#22) 2025-02-17 13:46:41 +01:00
T.v.Dein
6d2800c72e fix #5: fix ci testscript under windows by adding clean dep to test (#21)
Co-authored-by: Thomas von Dein <tom@vondein.org>
2025-02-12 13:00:58 +01:00
45 changed files with 1340 additions and 1303 deletions

96
.gh-dash.yml Normal file
View File

@@ -0,0 +1,96 @@
prSections:
- title: Responsible PRs
filters: repo:tlinden/anydb is:open NOT dependabot
layout:
repoName:
hidden: true
- title: Responsible Dependabot PRs
filters: repo:tlinden/anydb is:open dependabot
layout:
repoName:
hidden: true
issuesSections:
- title: Responsible Issues
filters: is:open repo:tlinden/anydb -author:@me
layout:
repoName:
hidden: true
- title: Note-to-Self Issues
filters: is:open repo:tlinden/anydb author:@me
layout:
creator:
hidden: true
repoName:
hidden: true
defaults:
preview:
open: false
width: 100
keybindings:
universal:
- key: "shift+down"
builtin: pageDown
- key: "shift+up"
builtin: pageUp
prs:
- key: g
name: gitu
command: >
cd {{.RepoPath}} && /home/scip/bin/gitu
- key: M
name: squash-merge
command: gh pr merge --rebase --squash --admin --repo {{.RepoName}} {{.PrNumber}}
- key: i
name: show ci checks
command: gh pr checks --repo {{.RepoName}} {{.PrNumber}} | glow -p
- key: e
name: edit pr
command: ~/.config/gh-dash/edit-gh-pr {{.RepoName}} {{.PrNumber}}
- key: E
name: open repo in emacs
command: emacsclient {{.RepoPath}} &
issues:
- key: v
name: view
command: gh issue view --repo {{.RepoName}} {{.IssueNumber}} | glow -p
- key: l
name: add label
command: gh issue --repo {{.RepoName}} edit {{.IssueNumber}} --add-label $(gum choose bug enhancement question dependencies wontfix)
- key: L
name: remove label
command: gh issue --repo {{.RepoName}} edit {{.IssueNumber}} --remove-label $(gum choose bug enhancement question dependencies wontfix)
- key: E
name: open repo in emacs
command: emacsclient {{.RepoPath}} &
theme:
ui:
sectionsShowCount: true
table:
compact: false
showSeparator: true
colors:
text:
primary: "#E2E1ED"
secondary: "#6770cb"
inverted: "#242347"
faint: "#b0793b"
warning: "#E0AF68"
success: "#3DF294"
background:
selected: "#1B1B33"
border:
primary: "#383B5B"
secondary: "#39386B"
faint: "#8d3e0b"
repoPaths:
:owner/:repo: ~/dev/:repo
pager:
diff: delta

View File

@@ -1,56 +0,0 @@
name: build-and-test
#on: [push, pull_request]
on:
pull_request:
branches:
- main
push:
branches:
- main
jobs:
build:
strategy:
matrix:
version: ['1.23']
os: [ubuntu-latest, macos-latest]
name: Build
runs-on: ${{ matrix.os }}
steps:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.version }}
id: go
- name: checkout
uses: actions/checkout@v4
- name: build
run: go build
- name: test
run: make test
- name: Update coverage report
uses: ncruces/go-coverage-report@main
with:
report: true
chart: true
amend: true
if: |
matrix.os == 'ubuntu-latest' &&
github.event_name == 'push'
continue-on-error: true
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v5
with:
go-version: 1.23
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v6

View File

@@ -1,34 +0,0 @@
name: build-push-image
on:
push:
tags:
- 'v*'
jobs:
build-and-push-image:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
with:
registry: https://ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991
with:
push: true
tags: ghcr.io/tlinden/anydb:${{ github.ref_name}}
- name: Build and push latest Docker image
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991
with:
push: true
tags: ghcr.io/tlinden/anydb:latest

View File

@@ -1,87 +0,0 @@
name: build-release
on:
push:
tags:
- "v*.*.*"
jobs:
release:
name: Build Release Assets
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.23.5
- name: Build the executables
run: ./mkrel.sh anydb ${{ github.ref_name}}
- name: List the executables
run: ls -l ./releases
- name: Upload the binaries
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
file: ./releases/*
file_glob: true
- name: Build Changelog
id: github_release
uses: mikepenz/release-changelog-builder-action@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
mode: "PR"
configurationJson: |
{
"template": "#{{CHANGELOG}}\n\n**Full Changelog**: #{{RELEASE_DIFF}}",
"pr_template": "- #{{TITLE}} (##{{NUMBER}}) by #{{AUTHOR}}\n#{{BODY}}",
"empty_template": "- no changes",
"categories": [
{
"title": "## New Features",
"labels": ["add", "feature"]
},
{
"title": "## Bug Fixes",
"labels": ["fix", "bug", "revert"]
},
{
"title": "## Documentation Enhancements",
"labels": ["doc"]
},
{
"title": "## Refactoring Efforts",
"labels": ["refactor"]
},
{
"title": "## Miscellaneus Changes",
"labels": []
}
],
"ignore_labels": [
"duplicate", "good first issue", "help wanted", "invalid", "question", "wontfix"
],
"label_extractor": [
{
"pattern": "(.) (.+)",
"target": "$1"
},
{
"pattern": "(.) (.+)",
"target": "$1",
"on_property": "title"
}
]
}
- name: Create Release
uses: softprops/action-gh-release@v2
with:
body: ${{steps.github_release.outputs.changelog}}

97
.golangci.yml Normal file
View File

@@ -0,0 +1,97 @@
version: "2"
linters:
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- clickhouselint
- containedctx
- copyloopvar
- decorder
- dogsled
- durationcheck
- embeddedstructfieldcheck
- errchkjson
- errname
- exhaustive
- exptostd
- fatcontext
- funcorder
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoinits
- gochecksumtype
- gocritic
- goheader
- gomoddirectives
- gomodguard_v2
- goprintffuncname
- gosmopolitan
- grouper
- iface
- importas
- inamedparam
- interfacebloat
- intrange
- iotamixing
- lll
- loggercheck
- makezero
- misspell
- modernize
- nakedret
- nilerr
- nilnesserr
- nlreturn
- nonamedreturns
- nosprintfhostport
- perfsprint
- prealloc
- promlinter
- protogetter
- reassign
- recvcheck
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- tagalign
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unqueryvet
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- whitespace
- wsl_v5
- zerologlint
settings:
varnamelen:
ignore-names:
- err
- wg
- mu
- ts
- to
- es
- op
- fd
- id
- fn

71
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,71 @@
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
before:
hooks:
- go mod tidy
gitea_urls:
api: https://codeberg.org/api/v1
download: https://codeberg.org
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
- freebsd
goarch:
- amd64
archives:
- formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}_{{ .Tag }}
# use zip for windows archives
format_overrides:
- goos: windows
formats: [zip]
- goos: linux
formats: [tar.gz,binary]
files:
- src: "*.md"
strip_parent: true
- src: "docs/*"
strip_parent: true
- src: Makefile.dist
dst: Makefile
wrap_in_directory: true
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
groups:
- title: Improved
regexp: '^.*?(feat|add|new)(\([[:word:]]+\))??!?:.+$'
order: 0
- title: Fixed
regexp: '^.*?(bug|fix)(\([[:word:]]+\))??!?:.+$'
order: 1
- title: Changed
order: 999
release:
header: "# Release Notes"
footer: >-
---
Full Changelog: [{{ .PreviousTag }}...{{ .Tag }}](https://codeberg.org/scip/anydb/compare/{{ .PreviousTag }}...{{ .Tag }})

39
.woodpecker/build.yaml Normal file
View File

@@ -0,0 +1,39 @@
matrix:
platform:
- linux/amd64
goversion:
- 1.26
lintversion:
- v2.12.2
labels:
platform: ${platform}
steps:
build:
when:
event: [push]
image: golang:${goversion}
commands:
- go get
- go build
linter:
when:
event: [push]
image: golang:${goversion}
commands:
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin ${lintversion}
- golangci-lint --version
- golangci-lint run ./...
depends_on: [build]
test:
when:
event: [push]
image: golang:${goversion}
commands:
- go get
- go test -v -cover
depends_on: [build,linter]

32
.woodpecker/image.yaml Normal file
View File

@@ -0,0 +1,32 @@
# https://woodpecker-ci.org/plugins/docker-buildx
# enable Package unit and go to /scip/-/packages after building to link to proj
variables:
- &repo codeberg.org/${CI_REPO_OWNER}/anydb
steps:
dryrun:
image: docker.io/woodpeckerci/plugin-docker-buildx:6.0.3
settings:
dockerfile: Dockerfile
platforms: linux/amd64
dry_run: true
repo: *repo
tags: latest
when:
event: [pull_request]
publish:
image: docker.io/woodpeckerci/plugin-docker-buildx:6.0.3
settings:
dockerfile: Dockerfile
platforms: linux/amd64
repo: *repo
registry: codeberg.org
tags: latest,${CI_COMMIT_SHA:0:8},${CI_COMMIT_TAG}
username: ${CI_REPO_OWNER}
password:
from_secret: REGISTRY_TOKEN
when:
event: [tag]
branch: main

15
.woodpecker/release.yaml Normal file
View File

@@ -0,0 +1,15 @@
# build release
labels:
platform: linux/amd64
steps:
goreleaser:
image: goreleaser/goreleaser
when:
event: [tag]
environment:
GITEA_TOKEN:
from_secret: DEPLOY_TOKEN
commands:
- goreleaser release --clean --verbose

View File

@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
FROM golang:1.23-alpine as builder FROM golang:1.26-alpine as builder
RUN apk update RUN apk update
RUN apk upgrade RUN apk upgrade

View File

@@ -51,11 +51,11 @@ endif
app/dbentry.pb.go: app/dbentry.proto app/dbentry.pb.go: app/dbentry.proto
protoc -I=. --go_out=app app/dbentry.proto protoc -I=. --go_out=app app/dbentry.proto
mv app/github.com/tlinden/anydb/app/dbentry.pb.go app/dbentry.pb.go mv app/codeberg.org/scip/anydb/app/dbentry.pb.go app/dbentry.pb.go
rm -rf app/github.com rm -rf app/github.com
buildlocal: buildlocal:
go build -ldflags "-X 'github.com/tlinden/anydb/cfg.VERSION=$(VERSION)'" go build -ldflags "-X 'codeberg.org/scip/anydb/cfg.VERSION=$(VERSION)'"
# binaries are being built by ci workflow on tag creation # binaries are being built by ci workflow on tag creation
release: release:
@@ -70,12 +70,12 @@ install: buildlocal
clean: clean:
rm -rf $(tool) releases coverage.out rm -rf $(tool) releases coverage.out
test: test: clean
ANYDB_PASSWORD=test go test -v ./... ANYDB_PASSWORD=test go test -v ./...
singletest: singletest:
@echo "Call like this: ''make singletest TEST=TestPrepareColumns MOD=lib" @echo "Call like this: ''make singletest TEST=TestPrepareColumns MOD=lib"
ANYDB_PASSWORD=test go test -run $(TEST) github.com/tlinden/anydb/$(MOD) ANYDB_PASSWORD=test go test -run $(TEST) codeberg.org/scip/anydb/$(MOD)
cover-report: cover-report:
go test ./... -cover -coverprofile=coverage.out go test ./... -cover -coverprofile=coverage.out
@@ -97,12 +97,8 @@ goupdate:
go get -t -u=patch ./... go get -t -u=patch ./...
lint: lint:
golangci-lint run golangci-lint run --show-stats=false
@ # gocritic check -enableAll *.go
# keep til ireturn
lint-full:
golangci-lint run --enable-all --exclude-use-default --disable exhaustivestruct,exhaustruct,depguard,interfacer,deadcode,golint,structcheck,scopelint,varcheck,ifshort,maligned,nosnakecase,godot,funlen,gofumpt,cyclop,noctx,gochecknoglobals,paralleltest,forbidigo,gci,godox,goimports,ireturn,stylecheck,testpackage,mirror,nestif,revive,goerr113,gomnd
gocritic check -enableAll *.go
demo: demo:
make -C demo demo make -C demo demo

20
Makefile.dist Normal file
View File

@@ -0,0 +1,20 @@
# -*-make-*-
.PHONY: install all
tool = rpn
PREFIX = /usr/local
UID = root
GID = 0
all:
@echo "Type 'sudo make install' to install the tool."
@echo "To change prefix, type 'sudo make install PREFIX=/opt'"
install:
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -d -o $(UID) -g $(GID) $(PREFIX)/share/doc
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
install -o $(UID) -g $(GID) -m 444 *.md $(PREFIX)/share/doc/

View File

@@ -1,18 +1,24 @@
## A personal key value store ## A personal key value store
[![Actions](https://github.com/tlinden/anydb/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/anydb/actions) [![status-badge](https://ci.codeberg.org/api/badges/15517/status.svg)](https://ci.codeberg.org/repos/15517)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/anydb/blob/master/LICENSE) [![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://codeberg.org/scip/anydb/blob/master/LICENSE)
[![Go Coverage](https://github.com/tlinden/anydb/wiki/coverage.svg)](https://raw.githack.com/wiki/tlinden/anydb/coverage.html) [![Go Report Card](https://goreportcard.com/badge/codeberg.org/scip/anydb)](https://goreportcard.com/report/codeberg.org/scip/anydb)
[![Go Report Card](https://goreportcard.com/badge/github.com/tlinden/anydb)](https://goreportcard.com/report/github.com/tlinden/anydb) [![GitHub release](https://img.shields.io/github/v/release/tlinden/anydb?color=%2300a719)](https://codeberg.org/scip/anydb/releases)
[![GitHub release](https://img.shields.io/github/v/release/tlinden/anydb?color=%2300a719)](https://github.com/TLINDEN/anydb/releases/latest) [![Documentation](https://img.shields.io/badge/manpage-documentation-blue)](https://codeberg.org/scip/anydb/raw/branch/main/anydb.pod)
[![Documentation](https://img.shields.io/badge/manpage-documentation-blue)](https://github.com/TLINDEN/anydb/blob/master/anydb.pod)
> [!CAUTION] > [!CAUTION]
> Version 0.1.3 introduced a [regression bug](https://github.com/TLINDEN/anydb/issues/19), > Between version 0.1.0 and version 0.2.1 deletion of keys did not work. There
> is a unit test to check for this, but this unit test had a bug as well and
> didn't catch it. The bug and the test have been fixed. You are advised to
> upgrade to 0.2.1 and above.
> [!CAUTION]
> Version 0.1.3 introduced a [regression](https://codeberg.org/scip/anydb/issues/19),
> which caused the encryption feature not to work correctly anymore. > which caused the encryption feature not to work correctly anymore.
> If you are using anydb 0.1.3, you are urgently advised to > If you are using anydb 0.1.3, you are urgently advised to
> upgrade to 0.2.0 > upgrade to 0.2.0
Anydb is a simple to use commandline tool to store anything you'd Anydb is a simple to use commandline tool to store anything you'd
like, even binary files etc. It is a re-implementation of like, even binary files etc. It is a re-implementation of
[skate](https://github.com/charmbracelet/skate) for the following [skate](https://github.com/charmbracelet/skate) for the following
@@ -40,17 +46,17 @@ And I wrote a very similar [tool](https://www.daemon.de/projects/dbtool/) 24 yea
**anydb** can do all the things you can do with skate: **anydb** can do all the things you can do with skate:
![simple demo](https://github.com/TLINDEN/anydb/blob/main/demo/intro.gif) ![simple demo](https://codeberg.org/scip/anydb/raw/branch/main/demo/intro.gif)
However, there are more features than just that! However, there are more features than just that!
![advanced demo](https://github.com/TLINDEN/anydb/blob/main/demo/advanced.gif) ![advanced demo](https://codeberg.org/scip/anydb/raw/branch/main/demo/advanced.gif)
## Installation ## Installation
There are multiple ways to install **anydb**: There are multiple ways to install **anydb**:
- Go to the [latest release page](https://github.com/tlinden/anydb/releases/latest), - Go to the [latest release page](https://codeberg.org/scip/anydb/releases),
locate the binary for your operating system and platform. locate the binary for your operating system and platform.
Download it and put it into some directory within your `$PATH` variable. Download it and put it into some directory within your `$PATH` variable.
@@ -63,7 +69,7 @@ There are multiple ways to install **anydb**:
- You can also install from source. Issue the following commands in your shell: - You can also install from source. Issue the following commands in your shell:
```shell ```shell
git clone https://github.com/TLINDEN/anydb.git git clone https://codeberg.org/scip/anydb.git
cd anydb cd anydb
make make
sudo make install sudo make install
@@ -71,7 +77,7 @@ There are multiple ways to install **anydb**:
- Or, if you have the GO toolkit installed, just install it like this: - Or, if you have the GO toolkit installed, just install it like this:
```shell ```shell
go install github.com/tlinden/anydb@latest go install codeberg.org/scip/anydb@latest
``` ```
If you do not find a binary release for your platform, please don't If you do not find a binary release for your platform, please don't
@@ -98,14 +104,14 @@ Here, we operate in a local directory `mydb`, which we'll use as HOME
inside the docker container. anydb will store its database in inside the docker container. anydb will store its database in
`mydb/.config/anydb/default.db`. `mydb/.config/anydb/default.db`.
A list of available images is [here](https://github.com/tlinden/anydb/pkgs/container/anydb/versions?filters%5Bversion_type%5D=tagged) A list of available images is [here](https://codeberg.org/scip/anydb/pkgs/container/anydb/versions?filters%5Bversion_type%5D=tagged)
## Documentation ## Documentation
The documentation is provided as a unix man-page. It will be The documentation is provided as a unix man-page. It will be
automatically installed if you install from source. However, you can automatically installed if you install from source. However, you can
[read the man-page online](https://github.com/TLINDEN/anydb/blob/master/anydb.pod) [read the man-page online](https://codeberg.org/scip/anydb/blob/master/anydb.pod)
Or if you cloned the repository you can read it this way (perl needs Or if you cloned the repository you can read it this way (perl needs
to be installed though): `perldoc anydb.pod`. to be installed though): `perldoc anydb.pod`.
@@ -122,7 +128,7 @@ best way for me to forget to do something.
In order to report a bug, unexpected behavior, feature requests or to In order to report a bug, unexpected behavior, feature requests or to
submit a patch, please open an issue on github: submit a patch, please open an issue on github:
https://github.com/TLINDEN/anydb/issues. https://codeberg.org/scip/anydb/issues.
## Copyright and license ## Copyright and license
@@ -134,7 +140,7 @@ T.v.Dein <tom AT vondein DOT org>
## Project homepage ## Project homepage
https://github.com/TLINDEN/anydb https://codeberg.org/scip/anydb
## Copyright and License ## Copyright and License

30
anydb.1
View File

@@ -133,13 +133,12 @@
.\" ======================================================================== .\" ========================================================================
.\" .\"
.IX Title "ANYDB 1" .IX Title "ANYDB 1"
.TH ANYDB 1 "2025-02-10" "1" "User Commands" .TH ANYDB 1 "2026-09-01" "1" "User Commands"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents. .\" way too many mistakes in technical documents.
.if n .ad l .if n .ad l
.nh .nh
.SH "anydb" .SH "NAME"
.IX Header "anydb"
anydb \- a personal key value store anydb \- a personal key value store
.SH "SYNOPSIS" .SH "SYNOPSIS"
.IX Header "SYNOPSIS" .IX Header "SYNOPSIS"
@@ -346,7 +345,7 @@ subcommand, see there for more details. The options \f(CW\*(C`\-n\*(C'\fR and
have the same meaning as in the list command. The \*(L"json\*(R" mode prints have the same meaning as in the list command. The \*(L"json\*(R" mode prints
the raw \s-1JSON\s0 representation of the whole database entry. Decryption the raw \s-1JSON\s0 representation of the whole database entry. Decryption
will only take place in \*(L"simple\*(R" and \*(L"json\*(R" mode. The \*(L"template\*(R" mode will only take place in \*(L"simple\*(R" and \*(L"json\*(R" mode. The \*(L"template\*(R" mode
provides the most flexibily, it is detailed in the section provides the most flexibility, it is detailed in the section
\&\s-1TEMPLATES\s0. \&\s-1TEMPLATES\s0.
.SS "list" .SS "list"
.IX Subsection "list" .IX Subsection "list"
@@ -605,7 +604,7 @@ being used is the \s-1GO\s0 template language, refer to
.PP .PP
Each template operates on one or more entries, no loop construct is Each template operates on one or more entries, no loop construct is
required, the template provided applies to every matching entry required, the template provided applies to every matching entry
separatley. separateley.
.PP .PP
The following template variables can be used: The following template variables can be used:
.IP "\fB.Key\fR \- string" 4 .IP "\fB.Key\fR \- string" 4
@@ -613,8 +612,19 @@ The following template variables can be used:
.PD 0 .PD 0
.IP "\fB.Value\fR \- string" 4 .IP "\fB.Value\fR \- string" 4
.IX Item ".Value - string" .IX Item ".Value - string"
.PD
To be able to use the value in a template, you'll need to add the
\&\f(CW\*(C`\-s\*(C'\fR flag so that values are included in output data
stream. Otherwise you can only use \f(CW\*(C`.Preview\*(C'\fR.
.Sp
Be careful if you use \f(CW\*(C`\-s\*(C'\fR and \f(CW\*(C`.Value\*(C'\fR though: if a value is a
multiline string, it may clutter your output.
.IP "\fB.Preview\fR string" 4
.IX Item ".Preview string"
First line of entry value (if it is a string).
.IP "\fB.Bin\fR \- []byte" 4 .IP "\fB.Bin\fR \- []byte" 4
.IX Item ".Bin - []byte" .IX Item ".Bin - []byte"
.PD 0
.IP "\fB.Created\fR \- timestamp.Time" 4 .IP "\fB.Created\fR \- timestamp.Time" 4
.IX Item ".Created - timestamp.Time" .IX Item ".Created - timestamp.Time"
.PD .PD
@@ -729,7 +739,7 @@ Some curl example calls to the \s-1API:\s0
Post a new key: Post a new key:
curl \-X \s-1PUT\s0 localhost:8787/anydb/v1/ \e curl \-X \s-1PUT\s0 localhost:8787/anydb/v1/ \e
\-H 'Content\-Type: application/json' \e \-H 'Content\-Type: application/json' \e
\-d '{\*(L"key\*(R":\*(L"foo\*(R",\*(L"val\*(R":\*(L"bar\*(R"}' \-d '{\*(L"key\*(R":\*(L"foo\*(R",\*(L"data\*(R":\*(L"bar\*(R"}'
.PP .PP
Retrieve the value: Retrieve the value:
.PP .PP
@@ -742,11 +752,17 @@ List all keys:
.Vb 1 .Vb 1
\& curl localhost:8787/anydb/v1/ \& curl localhost:8787/anydb/v1/
.Ve .Ve
.PP
Delete an entry:
.PP
.Vb 1
\& curl \-s \-X DELETE http://localhost:8787/anydb/v1/foo
.Ve
.SH "BUGS" .SH "BUGS"
.IX Header "BUGS" .IX Header "BUGS"
In order to report a bug, unexpected behavior, feature requests In order to report a bug, unexpected behavior, feature requests
or to submit a patch, please open an issue on github: or to submit a patch, please open an issue on github:
<https://github.com/TLINDEN/anydb/issues>. <https://codeberg.org/scip/anydb/issues>.
.PP .PP
Please repeat the failing command with debugging enabled \f(CW\*(C`\-d\*(C'\fR and Please repeat the failing command with debugging enabled \f(CW\*(C`\-d\*(C'\fR and
include the output in the issue. include the output in the issue.

View File

@@ -1,4 +1,4 @@
=head1 anydb =head1 NAME
anydb - a personal key value store anydb - a personal key value store
@@ -196,7 +196,7 @@ subcommand, see there for more details. The options C<-n> and C<-N>
have the same meaning as in the list command. The "json" mode prints have the same meaning as in the list command. The "json" mode prints
the raw JSON representation of the whole database entry. Decryption the raw JSON representation of the whole database entry. Decryption
will only take place in "simple" and "json" mode. The "template" mode will only take place in "simple" and "json" mode. The "template" mode
provides the most flexibily, it is detailed in the section provides the most flexibility, it is detailed in the section
L<TEMPLATES>. L<TEMPLATES>.
=head2 list =head2 list
@@ -442,7 +442,7 @@ L<https://pkg.go.dev/text/template> for details.
Each template operates on one or more entries, no loop construct is Each template operates on one or more entries, no loop construct is
required, the template provided applies to every matching entry required, the template provided applies to every matching entry
separatley. separateley.
The following template variables can be used: The following template variables can be used:
@@ -452,6 +452,17 @@ The following template variables can be used:
=item B<.Value> - string =item B<.Value> - string
To be able to use the value in a template, you'll need to add the
C<-s> flag so that values are included in output data
stream. Otherwise you can only use C<.Preview>.
Be careful if you use C<-s> and C<.Value> though: if a value is a
multiline string, it may clutter your output.
=item B<.Preview> string
First line of entry value (if it is a string).
=item B<.Bin> - []byte =item B<.Bin> - []byte
=item B<.Created> - timestamp.Time =item B<.Created> - timestamp.Time
@@ -569,7 +580,7 @@ Some curl example calls to the API:
Post a new key: Post a new key:
curl -X PUT localhost:8787/anydb/v1/ \ curl -X PUT localhost:8787/anydb/v1/ \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{"key":"foo","val":"bar"}' -d '{"key":"foo","data":"bar"}'
Retrieve the value: Retrieve the value:
@@ -579,11 +590,16 @@ List all keys:
curl localhost:8787/anydb/v1/ curl localhost:8787/anydb/v1/
Delete an entry:
curl -s -X DELETE http://localhost:8787/anydb/v1/foo
=head1 BUGS =head1 BUGS
In order to report a bug, unexpected behavior, feature requests In order to report a bug, unexpected behavior, feature requests
or to submit a patch, please open an issue on github: or to submit a patch, please open an issue on github:
L<https://github.com/TLINDEN/anydb/issues>. L<https://codeberg.org/scip/anydb/issues>.
Please repeat the failing command with debugging enabled C<-d> and Please repeat the failing command with debugging enabled C<-d> and
include the output in the issue. include the output in the issue.

View File

@@ -1,5 +1,7 @@
package app
/* /*
Copyright © 2024 Thomas von Dein Copyright © 2024-2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -14,7 +16,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package app
import ( import (
"fmt" "fmt"
@@ -28,6 +29,7 @@ type DbAttr struct {
Key string Key string
Preview string Preview string
Val []byte Val []byte
Data string // alias
Args []string Args []string
Tags []string Tags []string
File string File string
@@ -63,6 +65,12 @@ func (attr *DbAttr) ParseKV() error {
} }
} }
attr.SetPreview()
return nil
}
func (attr *DbAttr) SetPreview() {
switch { switch {
case attr.Binary: case attr.Binary:
attr.Preview = "<binary-content>" attr.Preview = "<binary-content>"
@@ -82,8 +90,6 @@ func (attr *DbAttr) ParseKV() error {
attr.Preview = string(attr.Val) attr.Preview = string(attr.Val)
} }
} }
return nil
} }
func (attr *DbAttr) GetFileValue() error { func (attr *DbAttr) GetFileValue() error {
@@ -120,17 +126,24 @@ func (attr *DbAttr) GetFileValue() error {
} }
} else { } else {
// read from console stdin // read from console stdin
var input string var (
var data string input string
data string
buf strings.Builder
)
for { for {
_, err := fmt.Scanln(&input) _, err := fmt.Scanln(&input)
if err != nil { if err != nil {
break break
} }
data += input + "\n"
buf.WriteString(input)
buf.WriteString("\n")
} }
data += buf.String()
attr.Val = []byte(data) attr.Val = []byte(data)
} }

View File

@@ -1,3 +1,5 @@
package app
/* /*
Copyright © 2024 Thomas von Dein Copyright © 2024 Thomas von Dein
@@ -14,7 +16,6 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package app
import ( import (
"crypto/rand" "crypto/rand"
@@ -35,7 +36,7 @@ const (
ArgonParallel uint8 = 2 ArgonParallel uint8 = 2
ArgonSaltLen int = 16 ArgonSaltLen int = 16
ArgonKeyLen uint32 = 32 ArgonKeyLen uint32 = 32
B64SaltLen int = 16 //22 B64SaltLen int = 16 // 22
) )
type Key struct { type Key struct {
@@ -47,6 +48,8 @@ type Key struct {
// password // password
func AskForPassword() ([]byte, error) { func AskForPassword() ([]byte, error) {
fmt.Fprint(os.Stderr, "Password: ") fmt.Fprint(os.Stderr, "Password: ")
// nolint: unconvert
pass, err := term.ReadPassword(int(syscall.Stdin)) pass, err := term.ReadPassword(int(syscall.Stdin))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read password: %w", err) return nil, fmt.Errorf("failed to read password: %w", err)
@@ -57,6 +60,8 @@ func AskForPassword() ([]byte, error) {
return pass, nil return pass, nil
} }
// DeriveKey derives a key from password.
//
// We're using the Argon2id key derivation algorithm to derive a // We're using the Argon2id key derivation algorithm to derive a
// secure key from the given password. This is important, because // secure key from the given password. This is important, because
// users might use unsecure passwords. The resulting encrypted data // users might use unsecure passwords. The resulting encrypted data
@@ -78,7 +83,7 @@ func DeriveKey(password []byte, salt []byte) (*Key, error) {
} }
hash := argon2.IDKey( hash := argon2.IDKey(
[]byte(password), salt, password, salt,
ArgonIter, ArgonIter,
ArgonMem, ArgonMem,
ArgonParallel, ArgonParallel,
@@ -95,6 +100,7 @@ func DeriveKey(password []byte, salt []byte) (*Key, error) {
// Retrieve a random chunk of given size // Retrieve a random chunk of given size
func GetRandom(size int, capacity int) ([]byte, error) { func GetRandom(size int, capacity int) ([]byte, error) {
buf := make([]byte, size, capacity) buf := make([]byte, size, capacity)
_, err := rand.Read(buf) _, err := rand.Read(buf)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to retrieve random bytes: %w", err) return nil, fmt.Errorf("failed to retrieve random bytes: %w", err)
@@ -103,7 +109,7 @@ func GetRandom(size int, capacity int) ([]byte, error) {
return buf, nil return buf, nil
} }
// Encrypt clear text given in attr using ChaCha20 and auhtenticate // Encrypt clear text given in attr using ChaCha20 and authenticate
// using the mac Poly1305. The cipher text will be put into attr, thus // using the mac Poly1305. The cipher text will be put into attr, thus
// modifying it. // modifying it.
// //
@@ -143,7 +149,7 @@ func Encrypt(pass []byte, attr *DbAttr) error {
// Do the reverse // Do the reverse
func Decrypt(pass []byte, cipherb []byte) ([]byte, error) { func Decrypt(pass []byte, cipherb []byte) ([]byte, error) {
if len(cipherb) < B64SaltLen { if len(cipherb) < B64SaltLen {
return nil, fmt.Errorf("encrypted cipher block too small") return nil, errors.New("encrypted cipher block too small")
} }
key, err := DeriveKey(pass, cipherb[0:B64SaltLen]) key, err := DeriveKey(pass, cipherb[0:B64SaltLen])

253
app/db.go
View File

@@ -20,13 +20,17 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"log"
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"slices"
"strings" "strings"
"time" "time"
common "codeberg.org/scip/anydb/common"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
timestamppb "google.golang.org/protobuf/types/known/timestamppb" timestamppb "google.golang.org/protobuf/types/known/timestamppb"
@@ -60,15 +64,8 @@ type DbTag struct {
Keys []string `json:"key"` Keys []string `json:"key"`
} }
// required for ui func (entry *DbEntry) Taglist() string {
func (entry DbEntry) FilterValue() string { return strings.Join(entry.GetTags(), ",")
return entry.Preview
}
func (entry DbEntry) Description() string {
return "Val: " + entry.Preview
}
func (entry DbEntry) Title() string {
return "Key: " + entry.Key
} }
const BucketData string = "data" const BucketData string = "data"
@@ -95,6 +92,14 @@ func New(file string, bucket string, debug bool) (*DB, error) {
return &DB{Debug: debug, Dbfile: file, Bucket: bucket}, nil return &DB{Debug: debug, Dbfile: file, Bucket: bucket}, nil
} }
func (db *DB) Exists() error {
if !common.FileExists(db.Dbfile) {
return fmt.Errorf("db file %s doesn't exist", db.Dbfile)
}
return nil
}
func (db *DB) Open() error { func (db *DB) Open() error {
slog.Debug("opening DB", "dbfile", db.Dbfile) slog.Debug("opening DB", "dbfile", db.Dbfile)
@@ -104,17 +109,27 @@ func (db *DB) Open() error {
} }
} }
b, err := bolt.Open(db.Dbfile, 0600, nil) var opts *bolt.Options
if db.Debug {
log := common.Slogger{Logger: slog.Default()}
opts = &bolt.Options{Logger: log}
}
b, err := bolt.Open(db.Dbfile, 0600, opts)
if err != nil { if err != nil {
return fmt.Errorf("failed to open DB %s: %w", db.Dbfile, err) return fmt.Errorf("failed to open DB %s: %w", db.Dbfile, err)
} }
db.DB = b db.DB = b
return nil return nil
} }
func (db *DB) Close() error { func (db *DB) Close() {
return db.DB.Close() if err := db.DB.Close(); err != nil {
log.Fatal(err)
}
} }
func (db *DB) List(attr *DbAttr, fulltext bool) (DbEntries, error) { func (db *DB) List(attr *DbAttr, fulltext bool) (DbEntries, error) {
@@ -123,8 +138,10 @@ func (db *DB) List(attr *DbAttr, fulltext bool) (DbEntries, error) {
} }
defer db.Close() defer db.Close()
var entries DbEntries var (
var filter *regexp.Regexp entries DbEntries
filter *regexp.Regexp
)
if len(attr.Args) > 0 { if len(attr.Args) > 0 {
// via cli // via cli
@@ -153,7 +170,7 @@ func (db *DB) List(attr *DbAttr, fulltext bool) (DbEntries, error) {
databucket := root.Bucket([]byte("data")) databucket := root.Bucket([]byte("data"))
if databucket == nil { if databucket == nil {
return fmt.Errorf("failed to retrieve data sub bucket") return errors.New("failed to retrieve data sub bucket")
} }
err := bucket.ForEach(func(key, pbentry []byte) error { err := bucket.ForEach(func(key, pbentry []byte) error {
@@ -164,7 +181,7 @@ func (db *DB) List(attr *DbAttr, fulltext bool) (DbEntries, error) {
if fulltext { if fulltext {
// avoid crash due to access fault // avoid crash due to access fault
value := databucket.Get([]byte(entry.Key)) // empty is ok value := databucket.Get([]byte(entry.GetKey())) // empty is ok
vc := make([]byte, len(value)) vc := make([]byte, len(value))
copy(vc, value) copy(vc, value)
entry.Value = string(vc) entry.Value = string(vc)
@@ -174,26 +191,21 @@ func (db *DB) List(attr *DbAttr, fulltext bool) (DbEntries, error) {
switch { switch {
case filter != nil: case filter != nil:
if filter.MatchString(entry.Key) || if filter.MatchString(entry.GetKey()) ||
filter.MatchString(strings.Join(entry.Tags, " ")) { filter.MatchString(strings.Join(entry.GetTags(), " ")) {
include = true include = true
} }
if !entry.Binary && !include && fulltext { if !entry.GetBinary() && !include && fulltext {
if filter.MatchString(string(entry.Value)) { if filter.MatchString(entry.GetValue()) {
include = true include = true
} }
} }
case len(attr.Tags) > 0: case len(attr.Tags) > 0:
for _, search := range attr.Tags { for _, search := range attr.Tags {
for _, tag := range entry.Tags { if slices.Contains(entry.GetTags(), search) {
if tag == search {
include = true include = true
break
}
}
if include {
break break
} }
} }
@@ -233,48 +245,31 @@ func (db *DB) Set(attr *DbAttr) error {
// check if the entry already exists and if yes, check if it has // check if the entry already exists and if yes, check if it has
// any tags. if so, we initialize our update struct with these // any tags. if so, we initialize our update struct with these
// tags unless it has new tags configured. // tags unless it has new tags configured.
// FIXME: use Get() slog.Debug("+++ GET")
err := db.DB.View(func(tx *bolt.Tx) error {
root := tx.Bucket([]byte(db.Bucket))
if root == nil {
return nil
}
bucket := root.Bucket([]byte("meta"))
if bucket == nil {
return nil
}
slog.Debug("opened buckets", "root", root, "data", bucket)
pbentry := bucket.Get([]byte(entry.Key))
if pbentry == nil {
return nil
}
var oldentry DbEntry
if err := proto.Unmarshal(pbentry, &oldentry); err != nil {
return fmt.Errorf("failed to unmarshal from protobuf: %w", err)
}
if len(oldentry.Tags) > 0 && len(entry.Tags) == 0 {
// initialize update entry with tags from old entry
entry.Tags = oldentry.Tags
}
return nil
})
oldentry, err := db.txGet(attr)
if err != nil { if err != nil {
if !strings.Contains(err.Error(), "no such key") {
return err return err
} }
}
if oldentry != nil {
if len(oldentry.GetTags()) > 0 && len(entry.GetTags()) == 0 {
// initialize update entry with tags from old entry
entry.Tags = oldentry.GetTags()
}
}
slog.Debug("+++ MARSHAL")
// marshall our data // marshall our data
pbentry, err := proto.Marshal(&entry) pbentry, err := proto.Marshal(&entry)
if err != nil { if err != nil {
return fmt.Errorf("failed to marshall protobuf: %w", err) return fmt.Errorf("failed to marshall protobuf: %w", err)
} }
slog.Debug("+++ UPDATE")
err = db.DB.Update(func(tx *bolt.Tx) error { err = db.DB.Update(func(tx *bolt.Tx) error {
// create root bucket // create root bucket
root, err := tx.CreateBucketIfNotExists([]byte(db.Bucket)) root, err := tx.CreateBucketIfNotExists([]byte(db.Bucket))
@@ -291,7 +286,7 @@ func (db *DB) Set(attr *DbAttr) error {
slog.Debug("opened/created buckets", "root", root, "data", bucket) slog.Debug("opened/created buckets", "root", root, "data", bucket)
// write meta data // write meta data
err = bucket.Put([]byte(entry.Key), []byte(pbentry)) err = bucket.Put([]byte(entry.GetKey()), pbentry)
if err != nil { if err != nil {
return fmt.Errorf("failed to insert data: %w", err) return fmt.Errorf("failed to insert data: %w", err)
} }
@@ -303,14 +298,13 @@ func (db *DB) Set(attr *DbAttr) error {
} }
// write value // write value
err = databucket.Put([]byte(entry.Key), attr.Val) err = databucket.Put([]byte(entry.GetKey()), attr.Val)
if err != nil { if err != nil {
return fmt.Errorf("failed to insert data: %w", err) return fmt.Errorf("failed to insert data: %w", err)
} }
return nil return nil
}) })
if err != nil { if err != nil {
return err return err
} }
@@ -324,61 +318,12 @@ func (db *DB) Get(attr *DbAttr) (*DbEntry, error) {
} }
defer db.Close() defer db.Close()
entry := DbEntry{} entry, err := db.txGet(attr)
err := db.DB.View(func(tx *bolt.Tx) error {
// root bucket
root := tx.Bucket([]byte(db.Bucket))
if root == nil {
return nil
}
// get meta sub bucket
bucket := root.Bucket([]byte("meta"))
if bucket == nil {
return nil
}
slog.Debug("opened buckets", "root", root, "data", bucket)
// retrieve meta data
pbentry := bucket.Get([]byte(attr.Key))
if pbentry == nil {
return fmt.Errorf("no such key: %s", attr.Key)
}
// put into struct
if err := proto.Unmarshal(pbentry, &entry); err != nil {
return fmt.Errorf("failed to unmarshal from protobuf: %w", err)
}
// get data sub bucket
databucket := root.Bucket([]byte("data"))
if databucket == nil {
return fmt.Errorf("failed to retrieve data sub bucket")
}
// retrieve actual data value
value := databucket.Get([]byte(attr.Key))
if len(value) == 0 {
return fmt.Errorf("no such key: %s", attr.Key)
}
// we need to make a copy of it, otherwise we'll get an
// "unexpected fault address" error
vc := make([]byte, len(value))
copy(vc, value)
entry.Value = string(vc)
return nil
})
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read from DB: %w", err) return nil, fmt.Errorf("failed to read from DB: %w", err)
} }
return &entry, nil return entry, nil
} }
func (db *DB) Del(attr *DbAttr) error { func (db *DB) Del(attr *DbAttr) error {
@@ -388,8 +333,14 @@ func (db *DB) Del(attr *DbAttr) error {
defer db.Close() defer db.Close()
err := db.DB.Update(func(tx *bolt.Tx) error { err := db.DB.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(db.Bucket)) // root bucket
root := tx.Bucket([]byte(db.Bucket))
if root == nil {
return nil
}
// get data sub bucket
bucket := root.Bucket([]byte("meta"))
if bucket == nil { if bucket == nil {
return nil return nil
} }
@@ -413,10 +364,11 @@ func (db *DB) Import(attr *DbAttr) (string, error) {
} }
var entries DbEntries var entries DbEntries
now := time.Now() now := time.Now()
newfile := db.Dbfile + now.Format("-02.01.2006T03:04.05") newfile := db.Dbfile + now.Format("-02.01.2006T03:04.05")
if err := json.Unmarshal([]byte(attr.Val), &entries); err != nil { if err := json.Unmarshal(attr.Val, &entries); err != nil {
return "", cleanError(newfile, fmt.Errorf("failed to unmarshal json: %w", err)) return "", cleanError(newfile, fmt.Errorf("failed to unmarshal json: %w", err))
} }
@@ -426,7 +378,6 @@ func (db *DB) Import(attr *DbAttr) (string, error) {
if err != nil { if err != nil {
return "", fmt.Errorf("failed to rename file %s to %s: %w", db.Dbfile, newfile, err) return "", fmt.Errorf("failed to rename file %s to %s: %w", db.Dbfile, newfile, err)
} }
} }
// should now be a new db file // should now be a new db file
@@ -457,7 +408,7 @@ func (db *DB) Import(attr *DbAttr) (string, error) {
} }
// write meta data // write meta data
err = bucket.Put([]byte(entry.Key), []byte(pbentry)) err = bucket.Put([]byte(entry.GetKey()), pbentry)
if err != nil { if err != nil {
return fmt.Errorf("failed to insert data into DB: %w", err) return fmt.Errorf("failed to insert data into DB: %w", err)
} }
@@ -469,7 +420,7 @@ func (db *DB) Import(attr *DbAttr) (string, error) {
} }
// write value // write value
err = databucket.Put([]byte(entry.Key), []byte(entry.Value)) err = databucket.Put([]byte(entry.GetKey()), []byte(entry.GetValue()))
if err != nil { if err != nil {
return fmt.Errorf("failed to insert data: %w", err) return fmt.Errorf("failed to insert data: %w", err)
} }
@@ -477,7 +428,6 @@ func (db *DB) Import(attr *DbAttr) (string, error) {
return nil return nil
}) })
if err != nil { if err != nil {
return "", cleanError(newfile, err) return "", cleanError(newfile, err)
} }
@@ -515,16 +465,14 @@ func (db *DB) Info() (*DbInfo, error) {
} }
info.Buckets = append(info.Buckets, binfo) info.Buckets = append(info.Buckets, binfo)
return nil return nil
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to read from DB: %w", err) return fmt.Errorf("failed to read from DB: %w", err)
} }
return nil return nil
}) })
return info, err return info, err
@@ -554,7 +502,7 @@ func (db *DB) Getall(attr *DbAttr) (DbEntries, error) {
// get data sub bucket // get data sub bucket
databucket := root.Bucket([]byte("data")) databucket := root.Bucket([]byte("data"))
if databucket == nil { if databucket == nil {
return fmt.Errorf("failed to retrieve data sub bucket") return errors.New("failed to retrieve data sub bucket")
} }
slog.Debug("opened buckets", "root", root, "data", bucket) slog.Debug("opened buckets", "root", root, "data", bucket)
@@ -567,7 +515,7 @@ func (db *DB) Getall(attr *DbAttr) (DbEntries, error) {
} }
// retrieve the value from the data sub bucket // retrieve the value from the data sub bucket
value := databucket.Get([]byte(entry.Key)) value := databucket.Get([]byte(entry.GetKey()))
// we need to make a copy of it, otherwise we'll get an // we need to make a copy of it, otherwise we'll get an
// "unexpected fault address" error // "unexpected fault address" error
@@ -582,5 +530,66 @@ func (db *DB) Getall(attr *DbAttr) (DbEntries, error) {
return err return err
}) })
return entries, err return entries, err
} }
// internal DB getter, assumes db.DB has already been
// opened successfully. Do NOT call this w/o valid
// DB handle!
func (db *DB) txGet(attr *DbAttr) (*DbEntry, error) {
entry := DbEntry{}
err := db.DB.View(func(tx *bolt.Tx) error {
// root bucket
root := tx.Bucket([]byte(db.Bucket))
if root == nil {
return nil
}
// get meta sub bucket
bucket := root.Bucket([]byte("meta"))
if bucket == nil {
return nil
}
slog.Debug("opened buckets", "root", root, "data", bucket)
// retrieve meta data
pbentry := bucket.Get([]byte(attr.Key))
if pbentry == nil {
return fmt.Errorf("no such key: %s", attr.Key)
}
// put into struct
if err := proto.Unmarshal(pbentry, &entry); err != nil {
return fmt.Errorf("failed to unmarshal from protobuf: %w", err)
}
// get data sub bucket
databucket := root.Bucket([]byte("data"))
if databucket == nil {
return errors.New("failed to retrieve data sub bucket")
}
// retrieve actual data value
value := databucket.Get([]byte(attr.Key))
if len(value) == 0 {
return fmt.Errorf("no such key: %s", attr.Key)
}
// we need to make a copy of it, otherwise we'll get an
// "unexpected fault address" error
vc := make([]byte, len(value))
copy(vc, value)
entry.Value = string(vc)
return nil
})
if err != nil {
return nil, err
}
return &entry, nil
}

View File

@@ -2,8 +2,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.36.1 // protoc-gen-go v1.36.5
// protoc v3.21.12 // protoc v4.24.4
// source: app/dbentry.proto // source: app/dbentry.proto
package app package app
@@ -14,6 +14,7 @@ import (
timestamppb "google.golang.org/protobuf/types/known/timestamppb" timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
unsafe "unsafe"
) )
const ( const (
@@ -133,7 +134,7 @@ func (x *DbEntry) GetValue() string {
var File_app_dbentry_proto protoreflect.FileDescriptor var File_app_dbentry_proto protoreflect.FileDescriptor
var file_app_dbentry_proto_rawDesc = []byte{ var file_app_dbentry_proto_rawDesc = string([]byte{
0x0a, 0x11, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x62, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x0a, 0x11, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x62, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x70, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x70, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
@@ -156,16 +157,16 @@ var file_app_dbentry_proto_rawDesc = []byte{
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x6c, 0x69, 0x6e, 0x64, 0x65, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x6c, 0x69, 0x6e, 0x64, 0x65,
0x6e, 0x2f, 0x61, 0x6e, 0x79, 0x64, 0x62, 0x2f, 0x61, 0x70, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x6e, 0x2f, 0x61, 0x6e, 0x79, 0x64, 0x62, 0x2f, 0x61, 0x70, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33, 0x74, 0x6f, 0x33,
} })
var ( var (
file_app_dbentry_proto_rawDescOnce sync.Once file_app_dbentry_proto_rawDescOnce sync.Once
file_app_dbentry_proto_rawDescData = file_app_dbentry_proto_rawDesc file_app_dbentry_proto_rawDescData []byte
) )
func file_app_dbentry_proto_rawDescGZIP() []byte { func file_app_dbentry_proto_rawDescGZIP() []byte {
file_app_dbentry_proto_rawDescOnce.Do(func() { file_app_dbentry_proto_rawDescOnce.Do(func() {
file_app_dbentry_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_dbentry_proto_rawDescData) file_app_dbentry_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_dbentry_proto_rawDesc), len(file_app_dbentry_proto_rawDesc)))
}) })
return file_app_dbentry_proto_rawDescData return file_app_dbentry_proto_rawDescData
} }
@@ -193,7 +194,7 @@ func file_app_dbentry_proto_init() {
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_dbentry_proto_rawDesc, RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_dbentry_proto_rawDesc), len(file_app_dbentry_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 1, NumMessages: 1,
NumExtensions: 0, NumExtensions: 0,
@@ -204,7 +205,6 @@ func file_app_dbentry_proto_init() {
MessageInfos: file_app_dbentry_proto_msgTypes, MessageInfos: file_app_dbentry_proto_msgTypes,
}.Build() }.Build()
File_app_dbentry_proto = out.File File_app_dbentry_proto = out.File
file_app_dbentry_proto_rawDesc = nil
file_app_dbentry_proto_goTypes = nil file_app_dbentry_proto_goTypes = nil
file_app_dbentry_proto_depIdxs = nil file_app_dbentry_proto_depIdxs = nil
} }

View File

@@ -5,7 +5,7 @@ package app;
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
option go_package = "github.com/tlinden/anydb/app"; option go_package = "codeberg.org/scip/anydb/app";
message DbEntry { message DbEntry {
string Id = 1; string Id = 1;

View File

@@ -1,3 +1,5 @@
package app
/* /*
Copyright © 2024 Thomas von Dein Copyright © 2024 Thomas von Dein
@@ -14,19 +16,24 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package app
import "os" import (
"errors"
"os"
)
func cleanError(file string, err error) error { func cleanError(file string, err error) error {
// remove given [backup] file and forward the given error // remove given [backup] file and forward the given error
os.Remove(file) oserr := os.Remove(file)
if oserr != nil {
return errors.Join(err, oserr)
}
return err return err
} }
func fileExists(filename string) bool { func fileExists(filename string) bool {
info, err := os.Stat(filename) info, err := os.Stat(filename)
if err != nil { if err != nil {
// return false on any error // return false on any error
return false return false

View File

@@ -1,5 +1,7 @@
package cfg
/* /*
Copyright © 2024 Thomas von Dein Copyright © 2025-2026 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -14,19 +16,22 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package cfg
import ( import (
"fmt" "fmt"
"io" "io"
"log/slog"
"os" "os"
"runtime/debug"
"github.com/pelletier/go-toml" "codeberg.org/scip/anydb/app"
"github.com/tlinden/anydb/app" "codeberg.org/scip/anydb/common"
"github.com/tlinden/anydb/common" "github.com/lmittmann/tint"
"github.com/pelletier/go-toml/v2"
"github.com/tlinden/yadu"
) )
var Version string = "v0.2.0" var Version string = "v0.3.2"
type BucketConfig struct { type BucketConfig struct {
Encrypt bool Encrypt bool
@@ -58,6 +63,8 @@ func (conf *Config) GetConfig(files []string) error {
} }
} }
conf.SetLogger()
return nil return nil
} }
@@ -77,6 +84,7 @@ func (conf *Config) ParseConfigFile(file string) error {
} }
add := Config{} add := Config{}
err = toml.Unmarshal(data, &add) err = toml.Unmarshal(data, &add)
if err != nil { if err != nil {
return fmt.Errorf("failed to unmarshall toml: %w", err) return fmt.Errorf("failed to unmarshall toml: %w", err)
@@ -114,3 +122,35 @@ func (conf *Config) ParseConfigFile(file string) error {
return nil return nil
} }
func (conf *Config) SetLogger() {
if conf.Debug {
buildInfo, _ := debug.ReadBuildInfo()
opts := &yadu.Options{
Level: slog.LevelDebug,
AddSource: true,
}
slog.SetLogLoggerLevel(slog.LevelDebug)
handler := yadu.NewHandler(os.Stdout, opts)
debuglogger := slog.New(handler).With(
slog.Group("program_info",
slog.Int("pid", os.Getpid()),
slog.String("go_version", buildInfo.GoVersion),
),
)
slog.SetDefault(debuglogger)
slog.Debug("parsed config", "conf", conf)
} else {
opts := &tint.Options{
Level: slog.LevelInfo,
AddSource: false,
}
logger := slog.New(tint.NewTextHandler(os.Stderr, opts))
slog.SetDefault(logger)
}
}

View File

@@ -1,7 +1,7 @@
package cmd package cmd
var manpage = ` var manpage = `
anydb NAME
anydb - a personal key value store anydb - a personal key value store
SYNOPSIS SYNOPSIS
@@ -179,7 +179,7 @@ SUBCOMMANDS
the same meaning as in the list command. The "json" mode prints the raw the same meaning as in the list command. The "json" mode prints the raw
JSON representation of the whole database entry. Decryption will only JSON representation of the whole database entry. Decryption will only
take place in "simple" and "json" mode. The "template" mode provides the take place in "simple" and "json" mode. The "template" mode provides the
most flexibily, it is detailed in the section TEMPLATES. most flexibility, it is detailed in the section TEMPLATES.
list list
The list subcommand displays a list of all database entries. The list subcommand displays a list of all database entries.
@@ -407,12 +407,22 @@ TEMPLATES
Each template operates on one or more entries, no loop construct is Each template operates on one or more entries, no loop construct is
required, the template provided applies to every matching entry required, the template provided applies to every matching entry
separatley. separateley.
The following template variables can be used: The following template variables can be used:
.Key - string .Key - string
.Value - string .Value - string
To be able to use the value in a template, you'll need to add the
"-s" flag so that values are included in output data stream.
Otherwise you can only use ".Preview".
Be careful if you use "-s" and ".Value" though: if a value is a
multiline string, it may clutter your output.
.Preview string
First line of entry value (if it is a string).
.Bin - []byte .Bin - []byte
.Created - timestamp.Time .Created - timestamp.Time
To retrieve a string representation of the timestamp, use To retrieve a string representation of the timestamp, use
@@ -504,7 +514,7 @@ REST API
Some curl example calls to the API: Some curl example calls to the API:
Post a new key: curl -X PUT localhost:8787/anydb/v1/ \ -H 'Content-Type: Post a new key: curl -X PUT localhost:8787/anydb/v1/ \ -H 'Content-Type:
application/json' \ -d '{"key":"foo","val":"bar"}' application/json' \ -d '{"key":"foo","data":"bar"}'
Retrieve the value: Retrieve the value:
@@ -514,10 +524,14 @@ REST API
curl localhost:8787/anydb/v1/ curl localhost:8787/anydb/v1/
Delete an entry:
curl -s -X DELETE http://localhost:8787/anydb/v1/foo
BUGS BUGS
In order to report a bug, unexpected behavior, feature requests or to In order to report a bug, unexpected behavior, feature requests or to
submit a patch, please open an issue on github: submit a patch, please open an issue on github:
<https://github.com/TLINDEN/anydb/issues>. <https://codeberg.org/scip/anydb/issues>.
Please repeat the failing command with debugging enabled "-d" and Please repeat the failing command with debugging enabled "-d" and
include the output in the issue. include the output in the issue.

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2024 Thomas von Dein Copyright © 2024-2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -21,10 +21,10 @@ import (
"os" "os"
"strings" "strings"
"codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
"codeberg.org/scip/anydb/output"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg"
"github.com/tlinden/anydb/output"
) )
func Set(conf *cfg.Config) *cobra.Command { func Set(conf *cfg.Config) *cobra.Command {
@@ -107,24 +107,28 @@ func Get(conf *cfg.Config) *cobra.Command {
attr.Key = args[0] attr.Key = args[0]
} }
if err := conf.DB.Exists(); err != nil {
return err
}
entry, err := conf.DB.Get(&attr) entry, err := conf.DB.Get(&attr)
if err != nil { if err != nil {
return err return err
} }
if entry.Encrypted { if entry.GetEncrypted() {
pass, err := getPassword() pass, err := getPassword()
if err != nil { if err != nil {
return err return err
} }
clear, err := app.Decrypt(pass, []byte(entry.Value)) clear, err := app.Decrypt(pass, []byte(entry.GetValue()))
if err != nil { if err != nil {
return err return err
} }
entry.Value = string(clear) entry.Value = string(clear)
entry.Size = uint64(len(entry.Value)) entry.Size = uint64(len(entry.GetValue()))
entry.Encrypted = false entry.Encrypted = false
} }
@@ -133,7 +137,8 @@ func Get(conf *cfg.Config) *cobra.Command {
} }
cmd.PersistentFlags().StringVarP(&attr.File, "output", "o", "", "output value to file (ignores -m)") cmd.PersistentFlags().StringVarP(&attr.File, "output", "o", "", "output value to file (ignores -m)")
cmd.PersistentFlags().StringVarP(&conf.Mode, "mode", "m", "", "output format (simple|wide|json|template) (default 'simple')") cmd.PersistentFlags().StringVarP(&conf.Mode, "mode", "m", "",
"output format (simple|wide|json|template) (default 'simple')")
cmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "n", false, "omit headers in tables") cmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "n", false, "omit headers in tables")
cmd.PersistentFlags().BoolVarP(&conf.NoHumanize, "no-human", "N", false, "do not translate to human readable values") cmd.PersistentFlags().BoolVarP(&conf.NoHumanize, "no-human", "N", false, "do not translate to human readable values")
cmd.PersistentFlags().StringVarP(&conf.Template, "template", "T", "", "go template for '-m template'") cmd.PersistentFlags().StringVarP(&conf.Template, "template", "T", "", "go template for '-m template'")
@@ -156,7 +161,7 @@ func Del(conf *cfg.Config) *cobra.Command {
Long: `Delete key and value matching key`, Long: `Delete key and value matching key`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return errors.New("No key specified") return errors.New("no key specified")
} }
// errors at this stage do not cause the usage to be shown // errors at this stage do not cause the usage to be shown
@@ -207,6 +212,10 @@ func List(conf *cfg.Config) *cobra.Command {
conf.Mode = "wide" conf.Mode = "wide"
} }
if err := conf.DB.Exists(); err != nil {
return err
}
entries, err := conf.DB.List(&attr, conf.Fulltext) entries, err := conf.DB.List(&attr, conf.Fulltext)
if err != nil { if err != nil {
return err return err
@@ -216,7 +225,8 @@ func List(conf *cfg.Config) *cobra.Command {
}, },
} }
cmd.PersistentFlags().StringVarP(&conf.Mode, "mode", "m", "", "output format (table|wide|json|template), wide is a verbose table. (default 'table')") cmd.PersistentFlags().StringVarP(&conf.Mode, "mode", "m", "",
"output format (table|wide|json|template), wide is a verbose table. (default 'table')")
cmd.PersistentFlags().StringVarP(&conf.Template, "template", "T", "", "go template for '-m template'") cmd.PersistentFlags().StringVarP(&conf.Template, "template", "T", "", "go template for '-m template'")
cmd.PersistentFlags().BoolVarP(&wide, "wide-output", "l", false, "output mode: wide") cmd.PersistentFlags().BoolVarP(&wide, "wide-output", "l", false, "output mode: wide")
cmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "n", false, "omit headers in tables") cmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "n", false, "omit headers in tables")

View File

@@ -1,3 +1,5 @@
package cmd
/* /*
Copyright © 2024 Thomas von Dein Copyright © 2024 Thomas von Dein
@@ -14,23 +16,21 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package cmd
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"os" "os"
"os/exec" "os/exec"
tea "github.com/charmbracelet/bubbletea" "codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
"codeberg.org/scip/anydb/output"
"codeberg.org/scip/anydb/rest"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg"
"github.com/tlinden/anydb/output"
"github.com/tlinden/anydb/rest"
"github.com/tlinden/anydb/ui"
) )
func Export(conf *cfg.Config) *cobra.Command { func Export(conf *cfg.Config) *cobra.Command {
@@ -58,6 +58,7 @@ func Export(conf *cfg.Config) *cobra.Command {
} }
cmd.PersistentFlags().StringVarP(&attr.File, "output-file", "o", "", "filename or - for STDIN") cmd.PersistentFlags().StringVarP(&attr.File, "output-file", "o", "", "filename or - for STDIN")
if err := cmd.MarkPersistentFlagRequired("output-file"); err != nil { if err := cmd.MarkPersistentFlagRequired("output-file"); err != nil {
panic(err) panic(err)
} }
@@ -87,12 +88,14 @@ func Import(conf *cfg.Config) *cobra.Command {
} }
fmt.Print(out) fmt.Print(out)
return nil return nil
}, },
} }
cmd.PersistentFlags().StringVarP(&attr.File, "import-file", "i", "", "filename or - for STDIN") cmd.PersistentFlags().StringVarP(&attr.File, "import-file", "i", "", "filename or - for STDIN")
cmd.PersistentFlags().StringArrayVarP(&attr.Tags, "tags", "t", nil, "tags, multiple allowed") cmd.PersistentFlags().StringArrayVarP(&attr.Tags, "tags", "t", nil, "tags, multiple allowed")
if err := cmd.MarkPersistentFlagRequired("import-file"); err != nil { if err := cmd.MarkPersistentFlagRequired("import-file"); err != nil {
panic(err) panic(err)
} }
@@ -125,7 +128,6 @@ func Man(conf *cfg.Config) *cobra.Command {
man.Stderr = os.Stderr man.Stderr = os.Stderr
err := man.Run() err := man.Run()
if err != nil { if err != nil {
return fmt.Errorf("failed to execute 'less': %w", err) return fmt.Errorf("failed to execute 'less': %w", err)
} }
@@ -206,19 +208,20 @@ func Edit(conf *cfg.Config) *cobra.Command {
return err return err
} }
if len(entry.Value) == 0 && entry.Binary { if len(entry.GetValue()) == 0 && entry.GetBinary() {
return errors.New("key contains binary uneditable content") return errors.New("key contains binary uneditable content")
} }
// decrypt if needed // decrypt if needed
if entry.Encrypted { if entry.GetEncrypted() {
pass, err := getPassword() pass, err := getPassword()
if err != nil { if err != nil {
return err return err
} }
password = pass password = pass
clear, err := app.Decrypt(pass, []byte(entry.Value)) clear, err := app.Decrypt(pass, []byte(entry.GetValue()))
if err != nil { if err != nil {
return err return err
} }
@@ -233,7 +236,7 @@ func Edit(conf *cfg.Config) *cobra.Command {
// save file to a temp file, call the editor with it, read // save file to a temp file, call the editor with it, read
// it back in and compare the content with the original // it back in and compare the content with the original
// one // one
newcontent, err := editContent(editor, string(entry.Value)) newcontent, err := editContent(editor, entry.GetValue())
if err != nil { if err != nil {
return err return err
} }
@@ -287,7 +290,11 @@ func editContent(editor string, content string) (string, error) {
if err != nil { if err != nil {
return "", fmt.Errorf("failed to create templ file: %w", err) return "", fmt.Errorf("failed to create templ file: %w", err)
} }
defer os.Remove(tmp.Name()) defer func() {
if err := os.Remove(tmp.Name()); err != nil {
log.Fatal(err)
}
}()
// put the content into a tmp file // put the content into a tmp file
_, err = tmp.WriteString(content) _, err = tmp.WriteString(content)
@@ -312,7 +319,11 @@ func editContent(editor string, content string) (string, error) {
if err != nil { if err != nil {
return "", fmt.Errorf("failed to open temp file: %w", err) return "", fmt.Errorf("failed to open temp file: %w", err)
} }
defer modified.Close() defer func() {
if err := modified.Close(); err != nil {
log.Fatal(err)
}
}()
newcontent, err := io.ReadAll(modified) newcontent, err := io.ReadAll(modified)
if err != nil { if err != nil {
@@ -321,36 +332,8 @@ func editContent(editor string, content string) (string, error) {
newcontentstr := string(newcontent) newcontentstr := string(newcontent)
if content == newcontentstr { if content == newcontentstr {
return "", fmt.Errorf("content not modified, aborting") return "", errors.New("content not modified, aborting")
} }
return newcontentstr, nil return newcontentstr, nil
} }
func Shell(conf *cfg.Config) *cobra.Command {
var cmd = &cobra.Command{
Use: "shell",
Short: "shell",
Long: `interactive ui`,
RunE: func(cmd *cobra.Command, args []string) error {
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
entries, err := conf.DB.List(&app.DbAttr{}, conf.Fulltext)
if err != nil {
return err
}
p := tea.NewProgram(ui.NewModel(conf, entries), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
return err
}
return nil
},
}
cmd.PersistentFlags().BoolVarP(&conf.NoHumanize, "no-human", "N", false, "do not translate to human readable values")
return cmd
}

View File

@@ -24,10 +24,9 @@ import (
"path/filepath" "path/filepath"
"runtime/debug" "runtime/debug"
slogmulti "github.com/samber/slog-multi" "codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg"
"github.com/tlinden/yadu" "github.com/tlinden/yadu"
) )
@@ -70,7 +69,6 @@ func Execute() {
Short: "anydb", Short: "anydb",
Long: `A personal key value store`, Long: `A personal key value store`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
var configs []string var configs []string
if configfile != "" { if configfile != "" {
configs = []string{configfile} configs = []string{configfile}
@@ -91,25 +89,13 @@ func Execute() {
slog.SetLogLoggerLevel(slog.LevelDebug) slog.SetLogLoggerLevel(slog.LevelDebug)
dbg, err := os.Create("debug.log") handler := yadu.NewHandler(os.Stdout, opts)
if err != nil { debuglogger := slog.New(handler).With(
return err
}
// FIXME: control this with a flag!
//outhandler := yadu.NewHandler(os.Stdout, opts)
filehandler := yadu.NewHandler(dbg, opts)
debuglogger := slog.New(slogmulti.Fanout(
//outhandler,
filehandler,
)).With(
slog.Group("program_info", slog.Group("program_info",
slog.Int("pid", os.Getpid()), slog.Int("pid", os.Getpid()),
slog.String("go_version", buildInfo.GoVersion), slog.String("go_version", buildInfo.GoVersion),
), ),
) )
slog.SetDefault(debuglogger) slog.SetDefault(debuglogger)
slog.Debug("parsed config", "conf", conf) slog.Debug("parsed config", "conf", conf)
@@ -123,12 +109,14 @@ func Execute() {
} }
conf.DB = db conf.DB = db
return nil return nil
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if ShowVersion { if ShowVersion {
fmt.Printf("This is anydb version %s\n", cfg.Version) fmt.Printf("This is anydb version %s\n", cfg.Version)
return nil return nil
} }
@@ -170,7 +158,6 @@ func Execute() {
rootCmd.AddCommand(Man(&conf)) rootCmd.AddCommand(Man(&conf))
rootCmd.AddCommand(Info(&conf)) rootCmd.AddCommand(Info(&conf))
rootCmd.AddCommand(Edit(&conf)) rootCmd.AddCommand(Edit(&conf))
rootCmd.AddCommand(Shell(&conf))
err = rootCmd.Execute() err = rootCmd.Execute()
if err != nil { if err != nil {

View File

@@ -20,13 +20,11 @@ import "os"
func CleanError(file string, err error) error { func CleanError(file string, err error) error {
// remove given [backup] file and forward the given error // remove given [backup] file and forward the given error
os.Remove(file) return os.Remove(file)
return err
} }
func FileExists(filename string) bool { func FileExists(filename string) bool {
info, err := os.Stat(filename) info, err := os.Stat(filename)
if err != nil { if err != nil {
// return false on any error // return false on any error
return false return false

23
common/logger.go Normal file
View File

@@ -0,0 +1,23 @@
package common
import (
"fmt"
"log/slog"
)
type Slogger struct {
*slog.Logger
}
func (l Slogger) Debug(v ...any) {}
func (l Slogger) Debugf(format string, v ...any) { l.Logger.Debug(fmt.Sprintf(format, v...)) }
func (l Slogger) Error(v ...any) {}
func (l Slogger) Errorf(format string, v ...any) { l.Logger.Error(fmt.Sprintf(format, v...)) }
func (l Slogger) Info(v ...any) {}
func (l Slogger) Infof(format string, v ...any) { l.Logger.Info(fmt.Sprintf(format, v...)) }
func (l Slogger) Warning(v ...any) {}
func (l Slogger) Warningf(format string, v ...any) { l.Warn(fmt.Sprintf(format, v...)) }
func (l Slogger) Fatal(v ...any) {}
func (l Slogger) Fatalf(format string, v ...any) { l.Logger.Error(fmt.Sprintf(format, v...)) }
func (l Slogger) Panic(v ...any) {}
func (l Slogger) Panicf(format string, v ...any) { l.Logger.Error(fmt.Sprintf(format, v...)) }

95
contrib/anydbui Executable file
View File

@@ -0,0 +1,95 @@
#!/bin/sh
template="{{.Key}} {{.Created.AsTime.Year}}-{{.Created.AsTime.Month}}-{{.Created.AsTime.Day}} {{.Taglist}} {{.Preview}}"
header="TITLE DATE TAGS PREVIEW"
# its possible to use another version of anydb for testing purposes
anydb="${ANYDB:-anydb}"
# list command
command="( echo '$header'; $anydb ls -m template --template '$template' ) | column -t -l4"
for binary in fzf $anydb column diff awk less; do
if ! type $binary > /dev/null 2>&1; then
echo "$binary is not installed!"
exit 1
fi
done
if type gum > /dev/null 2>&1; then
GUM=1
fi
_rand() {
awk 'BEGIN {srand(); printf( "%d\n", 1024 * rand() )}'
}
_list() {
(
echo "$header"
$anydb ls -m template --template "$template"
) | column -t -l4
}
_updater() {
_port="$1"
_cache="/tmp/anydbcache.$port"
_current="/tmp/anydbcache.$port.current"
touch $_current $_cache
while :; do
sleep 10
_list > $_current
if ! diff -q $_current $_cache > /dev/null 2>&1; then
curl -d "reload($command)+clear-screen" "http://127.0.0.1:$_port"
fi
cp $_current $_cache
done
}
_cleanup() {
# get rid of the update child
kill $pid
# clean up reloader cache
rm -f /tmp/anydbcache.${port}*
}
# fork background updater
port=$((8000 + $(_rand) % 1000))
_updater $port &
pid=$!
db=$($anydb info | grep Database | cut -d: -f2)
shorthelp="$db - [enter]read [c-e]edit [c-k]kill [c-c]exit [c-k]delete"
if test -n "$GUM"; then
color="Color \"#ff\" \"#0000ff\"" # white on blue
db="{{ $color \"Database:\"}}$db"
enter="{{ $color \"[Enter]\"}} Read"
edit="{{ $color \"[ctrl-c]\"}} Edit"
delete="{{ $color \"[ctrl-k]\"}} Delete"
template="$db $enter $edit $delete"
shorthelp=$(echo "$template" | gum format -t template)
fi
trap '_cleanup; (exit $?); exit' INT TERM EXIT
: | command="$command" fzf \
--bind "start:reload:$command" \
--header-lines 1 --layout=reverse --info=inline \
--height 100% --pointer="→" --separator="─" \
--scrollbar="│" --preview-window "right:50%" \
--border-label="$shorthelp" \
--border=bottom --layout=reverse --info=inline \
--height 100% --header-first --cycle --header-lines=1 --listen=$port \
--bind "enter:execute: clear; $anydb get {1} | less" \
--bind "ctrl-e:execute: $anydb edit {1}" \
--bind "ctrl-k:execute:$anydb del {1}"

View File

@@ -177,6 +177,6 @@ Enter
Sleep 3s Sleep 3s
Enter Enter
Type "# Try it out yourself: github.com/tlinden/anydb!" Type "# Try it out yourself: codeberg.org/scip/anydb!"
Enter Enter
Sleep 4s Sleep 4s

75
go.mod
View File

@@ -1,59 +1,38 @@
module github.com/tlinden/anydb module codeberg.org/scip/anydb
go 1.23 go 1.26.0
toolchain go1.23.5 toolchain go1.26.4
require ( require (
github.com/alecthomas/repr v0.4.0
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/gofiber/fiber/v2 v2.52.6
github.com/inconshreveable/mousetrap v1.1.0 github.com/inconshreveable/mousetrap v1.1.0
github.com/olekukonko/tablewriter v0.0.5 github.com/lmittmann/tint v1.2.0
github.com/pelletier/go-toml v1.9.5 github.com/olekukonko/tablewriter v1.1.4
github.com/rogpeppe/go-internal v1.13.1 github.com/pelletier/go-toml/v2 v2.4.3
github.com/spf13/cobra v1.8.1 github.com/rogpeppe/go-internal v1.15.0
go.etcd.io/bbolt v1.4.0 github.com/spf13/cobra v1.10.2
golang.org/x/crypto v0.31.0 github.com/tlinden/yadu v0.1.3
golang.org/x/term v0.27.0 go.etcd.io/bbolt v1.5.0
google.golang.org/protobuf v1.36.5 golang.org/x/crypto v0.42.0
golang.org/x/term v0.35.0
google.golang.org/protobuf v1.36.12
) )
require ( require (
github.com/andybalholm/brotli v1.1.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
github.com/charmbracelet/bubbles v0.20.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/charmbracelet/bubbletea v1.3.3 // indirect github.com/goccy/go-json v0.10.6 // indirect
github.com/charmbracelet/lipgloss v1.0.0 // indirect github.com/mattn/go-colorable v0.1.15 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/mattn/go-isatty v0.0.24 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/mattn/go-runewidth v0.0.28 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/fatih/color v1.16.0 // indirect github.com/olekukonko/errors v1.2.0 // indirect
github.com/gofiber/fiber/v3 v3.0.0-beta.3 // indirect github.com/olekukonko/ll v0.1.8 // indirect
github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/google/uuid v1.6.0 // indirect golang.org/x/sys v0.45.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect golang.org/x/tools v0.36.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/samber/slog-multi v1.4.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/tlinden/yadu v0.1.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

211
go.sum
View File

@@ -1,163 +1,72 @@
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbletea v1.3.3 h1:WpU6fCY0J2vDWM3zfS3vIDi/ULq3SYphZhkAGGvmEUY=
github.com/charmbracelet/bubbletea v1.3.3/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 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/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg=
github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY=
github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co=
github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/lmittmann/tint v1.2.0 h1:AogHRHy8HUJUnNJBHJlYa+fR4YY8mko2cnCp67xn9JY=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/lmittmann/tint v1.2.0/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/samber/slog-multi v1.4.0 h1:pwlPMIE7PrbTHQyKWDU+RIoxP1+HKTNOujk3/kdkbdg= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/samber/slog-multi v1.4.0/go.mod h1:FsQ4Uv2L+E/8TZt+/BVgYZ1LoDWCbfCU21wVIoMMrO8= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY= github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY=
github.com/tlinden/yadu v0.1.3/go.mod h1:l3bRmHKL9zGAR6pnBHY2HRPxBecf7L74BoBgOOpTcUA= github.com/tlinden/yadu v0.1.3/go.mod h1:l3bRmHKL9zGAR6pnBHY2HRPxBecf7L74BoBgOOpTcUA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

24
main.go
View File

@@ -23,8 +23,8 @@ import (
"os" "os"
"runtime" "runtime"
"codeberg.org/scip/anydb/cmd"
"github.com/inconshreveable/mousetrap" "github.com/inconshreveable/mousetrap"
"github.com/tlinden/anydb/cmd"
) )
func main() { func main() {
@@ -34,30 +34,13 @@ func main() {
Main() Main()
} }
func init() {
// if we're running on Windows AND if the user double clicked the
// exe file from explorer, we tell them and then wait until any
// key has been hit, which will make the cmd window disappear and
// thus give the user time to read it.
if runtime.GOOS == "windows" {
if mousetrap.StartedByExplorer() {
fmt.Println("Do no double click kleingebaeck.exe!")
fmt.Println("Please open a command shell and run it from there.")
fmt.Println()
fmt.Print("Press any key to quit: ")
_, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
panic(err)
}
}
}
}
func Main() int { func Main() int {
cmd.Execute() cmd.Execute()
return 0 return 0
} }
// nolint gochecknoinits
func init() { func init() {
// if we're running on Windows AND if the user double clicked the // if we're running on Windows AND if the user double clicked the
// exe file from explorer, we tell them and then wait until any // exe file from explorer, we tell them and then wait until any
@@ -69,6 +52,7 @@ func init() {
fmt.Println("Please open a command shell and run it from there.") fmt.Println("Please open a command shell and run it from there.")
fmt.Println() fmt.Println()
fmt.Print("Press any key to quit: ") fmt.Print("Press any key to quit: ")
_, err := bufio.NewReader(os.Stdin).ReadString('\n') _, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil { if err != nil {
panic(err) panic(err)

View File

@@ -17,16 +17,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package main package main
import ( import (
"os"
"testing" "testing"
"github.com/rogpeppe/go-internal/testscript" "github.com/rogpeppe/go-internal/testscript"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{ testscript.Main(m, map[string]func(){
"anydb": Main, "anydb": main,
})) })
} }
func TestAnydb(t *testing.T) { func TestAnydb(t *testing.T) {

View File

@@ -52,8 +52,15 @@ for D in $DIST; do
tardir="${tool}-${os}-${arch}-${version}" tardir="${tool}-${os}-${arch}-${version}"
tarfile="releases/${tool}-${os}-${arch}-${version}.tar.gz" tarfile="releases/${tool}-${os}-${arch}-${version}.tar.gz"
if test "$D" = "linux/amd64"; then
pie="-buildmode=pie"
fi
set -x set -x
GOOS=${os} GOARCH=${arch} go build -tags osusergo,netgo -ldflags "-extldflags=-static" -o ${binfile} GOOS=${os} GOARCH=${arch} go build -tags osusergo,netgo -ldflags "-extldflags=-static -w" --trimpath $pie -o ${binfile}
strip --strip-all ${binfile}
mkdir -p ${tardir} mkdir -p ${tardir}
cp ${binfile} README.md LICENSE ${tardir}/ cp ${binfile} README.md LICENSE ${tardir}/
echo 'tool = anydb echo 'tool = anydb

View File

@@ -21,8 +21,8 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/tlinden/anydb/app" "codeberg.org/scip/anydb/app"
"github.com/tlinden/anydb/cfg" "codeberg.org/scip/anydb/cfg"
) )
func WriteJSON(attr *app.DbAttr, conf *cfg.Config, entries app.DbEntries) error { func WriteJSON(attr *app.DbAttr, conf *cfg.Config, entries app.DbEntries) error {

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2024 Thomas von Dein Copyright © 2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -26,10 +26,12 @@ import (
"strings" "strings"
tpl "text/template" tpl "text/template"
"codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter"
"github.com/tlinden/anydb/app" "github.com/olekukonko/tablewriter/renderer"
"github.com/tlinden/anydb/cfg" "github.com/olekukonko/tablewriter/tw"
) )
func List(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error { func List(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
@@ -52,6 +54,7 @@ func ListJson(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
} }
fmt.Println(string(jsonentries)) fmt.Println(string(jsonentries))
return nil return nil
} }
@@ -65,13 +68,16 @@ func ListTemplate(writer io.Writer, conf *cfg.Config, entries app.DbEntries) err
for _, row := range entries { for _, row := range entries {
buf.Reset() buf.Reset()
err = tmpl.Execute(&buf, row) err = tmpl.Execute(&buf, row)
if err != nil { if err != nil {
return fmt.Errorf("failed to execute output template: %w", err) return fmt.Errorf("failed to execute output template: %w", err)
} }
if buf.Len() > 0 { if buf.Len() > 0 {
fmt.Fprintln(writer, buf.String()) if _, err := fmt.Fprintln(writer, buf.String()); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
} }
} }
@@ -80,13 +86,46 @@ func ListTemplate(writer io.Writer, conf *cfg.Config, entries app.DbEntries) err
func ListTable(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error { func ListTable(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
tableString := &strings.Builder{} tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
styleTSV := tw.NewSymbolCustom("space").WithColumn("\t")
table := tablewriter.NewTable(tableString,
tablewriter.WithRenderer(
renderer.NewBlueprint(tw.Rendition{
Borders: tw.BorderNone,
Symbols: styleTSV,
Settings: tw.Settings{
Separators: tw.Separators{BetweenRows: tw.Off, BetweenColumns: tw.On},
Lines: tw.Lines{ShowFooterLine: tw.Off, ShowHeaderLine: tw.Off},
},
})),
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoFormat: tw.Off,
},
Padding: tw.CellPadding{
Global: tw.Padding{Left: "", Right: ""},
},
},
Row: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoWrap: tw.WrapNone,
Alignment: tw.AlignLeft,
},
Padding: tw.CellPadding{
Global: tw.Padding{Left: "", Right: ""},
},
},
}),
tablewriter.WithPadding(tw.PaddingNone),
)
if !conf.NoHeaders { if !conf.NoHeaders {
if conf.Mode == "wide" { if conf.Mode == "wide" {
table.SetHeader([]string{"KEY", "TAGS", "SIZE", "UPDATED", "VALUE"}) table.Header([]string{"KEY", "TAGS", "SIZE", "UPDATED", "VALUE"})
} else { } else {
table.SetHeader([]string{"KEY", "VALUE"}) table.Header([]string{"KEY", "VALUE"})
} }
} }
@@ -94,44 +133,41 @@ func ListTable(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error
if conf.Mode == "wide" { if conf.Mode == "wide" {
switch conf.NoHumanize { switch conf.NoHumanize {
case true: case true:
if err :=
table.Append([]string{ table.Append([]string{
row.Key, row.GetKey(),
strings.Join(row.Tags, ","), strings.Join(row.GetTags(), ","),
strconv.FormatUint(row.Size, 10), strconv.FormatUint(row.GetSize(), 10),
row.Created.AsTime().Format("02.01.2006T03:04.05"), row.GetCreated().AsTime().Format("02.01.2006T03:04.05"),
row.Preview, row.GetPreview(),
}) }); err != nil {
return fmt.Errorf("failed to add data to table: %w", err)
}
default: default:
table.Append([]string{ if err := table.Append([]string{
row.Key, row.GetKey(),
strings.Join(row.Tags, ","), strings.Join(row.GetTags(), ","),
humanize.Bytes(uint64(row.Size)), humanize.Bytes(row.GetSize()),
humanize.Time(row.Created.AsTime()), humanize.Time(row.GetCreated().AsTime()),
row.Preview, row.GetPreview(),
}) }); err != nil {
return fmt.Errorf("failed to add data to table: %w", err)
}
} }
} else { } else {
table.Append([]string{row.Key, row.Preview}) if err := table.Append([]string{row.GetKey(), row.GetPreview()}); err != nil {
return fmt.Errorf("failed to add data to table: %w", err)
}
} }
} }
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) if err := table.Render(); err != nil {
table.SetAutoWrapText(false) return fmt.Errorf("failed to render table: %w", err)
table.SetAutoFormatHeaders(true) }
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetNoWhiteSpace(true)
table.SetTablePadding("\t") // pad with tabs if _, err := fmt.Fprint(writer, tableString.String()); err != nil {
return fmt.Errorf("failed to write output: %w", err)
table.Render() }
fmt.Fprint(writer, tableString.String())
return nil return nil
} }

View File

@@ -20,14 +20,15 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"os" "os"
"reflect" "reflect"
"codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg"
"golang.org/x/term" "golang.org/x/term"
//"github.com/alecthomas/repr" // "github.com/alecthomas/repr"
) )
func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error { func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error {
@@ -39,15 +40,18 @@ func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEn
switch conf.Mode { switch conf.Mode {
case "simple", "": case "simple", "":
if entry.Binary { if entry.GetBinary() {
if isatty { if isatty {
fmt.Println("binary data omitted") fmt.Println("binary data omitted")
} else { } else {
os.Stdout.WriteString(entry.Value) if _, err := os.Stdout.WriteString(entry.GetValue()); err != nil {
return err
}
} }
} else { } else {
fmt.Print(string(entry.Value)) fmt.Print(entry.GetValue())
if entry.Value[entry.Size-1] != '\n' {
if entry.GetValue()[entry.GetSize()-1] != '\n' {
// always add a terminal newline // always add a terminal newline
fmt.Println() fmt.Println()
} }
@@ -69,8 +73,10 @@ func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEn
} }
func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error { func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error {
var fileHandle *os.File var (
var err error fileHandle *os.File
err error
)
if attr.File == "-" { if attr.File == "-" {
fileHandle = os.Stdout fileHandle = os.Stdout
@@ -79,16 +85,20 @@ func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.
if err != nil { if err != nil {
return fmt.Errorf("failed to open file %s for writing: %w", attr.File, err) return fmt.Errorf("failed to open file %s for writing: %w", attr.File, err)
} }
defer fd.Close() defer func() {
if err := fd.Close(); err != nil {
log.Fatal(err)
}
}()
fileHandle = fd fileHandle = fd
} }
// actually write file content // actually write file content
_, err = fileHandle.WriteString(entry.Value) _, err = fileHandle.WriteString(entry.GetValue())
if !entry.Binary { if !entry.GetBinary() {
if entry.Value[entry.Size-1] != '\n' { if entry.GetValue()[entry.GetSize()-1] != '\n' {
// always add a terminal newline // always add a terminal newline
_, err = fileHandle.Write([]byte{'\n'}) _, err = fileHandle.Write([]byte{'\n'})
} }
@@ -102,34 +112,45 @@ func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.
} }
func Info(writer io.Writer, conf *cfg.Config, info *app.DbInfo) error { func Info(writer io.Writer, conf *cfg.Config, info *app.DbInfo) error {
fmt.Fprintf(writer, "Database: %s\n", info.Path) if _, err := fmt.Fprintf(writer, "Database: %s\n", info.Path); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
for _, bucket := range info.Buckets { for _, bucket := range info.Buckets {
if conf.NoHumanize { if conf.NoHumanize {
fmt.Fprintf( if _, err := fmt.Fprintf(
writer, writer,
"%19s: %s\n%19s: %d\n%19s: %d\n%19s: %t\n", "%19s: %s\n%19s: %d\n%19s: %d\n%19s: %t\n",
"Bucket", bucket.Name, "Bucket", bucket.Name,
"Size", bucket.Size, "Size", bucket.Size,
"Keys", bucket.Keys, "Keys", bucket.Keys,
"Encrypted", conf.Encrypt) "Encrypted", conf.Encrypt); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
} else { } else {
fmt.Fprintf( if _, err := fmt.Fprintf(
writer, writer,
"%19s: %s\n%19s: %s\n%19s: %d\n", "%19s: %s\n%19s: %s\n%19s: %d\n",
"Bucket", bucket.Name, "Bucket", bucket.Name,
"Size", humanize.Bytes(uint64(bucket.Size)), "Size", humanize.Bytes(uint64(bucket.Size)),
"Keys", bucket.Keys) "Keys", bucket.Keys); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
} }
if conf.Debug { if conf.Debug {
val := reflect.ValueOf(&bucket.Stats).Elem() val := reflect.ValueOf(&bucket.Stats).Elem()
for i := 0; i < val.NumField(); i++ { for i := range val.NumField() {
fmt.Fprintf(writer, "%19s: %v\n", val.Type().Field(i).Name, val.Field(i)) if _, err := fmt.Fprintf(writer, "%19s: %v\n", val.Type().Field(i).Name, val.Field(i)); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}
} }
} }
fmt.Fprintln(writer) if _, err := fmt.Fprintln(writer); err != nil {
return fmt.Errorf("failed to write output: %w", err)
} }
}
return nil return nil
} }

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2024 Thomas von Dein Copyright © 2024-2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -17,15 +17,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package rest package rest
import ( import (
//"github.com/alecthomas/repr" // "github.com/alecthomas/repr"
"github.com/gofiber/fiber/v2" "encoding/json"
"github.com/tlinden/anydb/app" "log"
"github.com/tlinden/anydb/cfg" "net/http"
"codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
) )
type SetContext struct { type SetContext struct {
Query string `json:"query" form:"query"` Query string `form:"query" json:"query"`
} }
type ListResponse struct { type ListResponse struct {
@@ -40,101 +43,121 @@ type SingleResponse struct {
Entry *app.DbEntry Entry *app.DbEntry
} }
func RestList(c *fiber.Ctx, conf *cfg.Config) error { func RestList(resp http.ResponseWriter, req *http.Request, conf *cfg.Config) {
attr := new(app.DbAttr) attr := new(app.DbAttr)
if len(c.Body()) > 0 { err := json.NewDecoder(req.Body).Decode(&attr)
if err := c.BodyParser(attr); err != nil { if err != nil {
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{ if err.Error() != `EOF` {
"errors": err.Error(), http.Error(resp, err.Error(), http.StatusBadRequest)
})
return
} }
} }
// get list // get list
entries, err := conf.DB.List(attr, attr.Fulltext) entries, err := conf.DB.List(attr, attr.Fulltext)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusForbidden, JsonStatus(resp, http.StatusForbidden, "Unable to list keys: "+err.Error())
"Unable to list keys: "+err.Error())
return
} }
return c.Status(fiber.StatusOK).JSON( resp.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(resp).Encode(
ListResponse{ ListResponse{
Code: http.StatusOK,
Success: true, Success: true,
Code: fiber.StatusOK,
Entries: entries, Entries: entries,
},
)
}
func RestGet(c *fiber.Ctx, conf *cfg.Config) error {
if c.Params("key") == "" {
return JsonStatus(c, fiber.StatusForbidden,
"key not provided")
}
// get list
entry, err := conf.DB.Get(&app.DbAttr{Key: c.Params("key")})
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Unable to get key: "+err.Error())
}
if entry.Key == "" {
return JsonStatus(c, fiber.StatusForbidden,
"Key does not exist")
}
return c.Status(fiber.StatusOK).JSON(
SingleResponse{
Success: true,
Code: fiber.StatusOK,
Entry: entry,
},
)
}
func RestDelete(c *fiber.Ctx, conf *cfg.Config) error {
if c.Params("key") == "" {
return JsonStatus(c, fiber.StatusForbidden,
"key not provided")
}
// get list
err := conf.DB.Del(&app.DbAttr{Key: c.Params("key")})
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Unable to delete key: "+err.Error())
}
return c.Status(fiber.StatusOK).JSON(
Result{
Success: true,
Code: fiber.StatusOK,
Message: "key deleted",
},
)
}
func RestSet(c *fiber.Ctx, conf *cfg.Config) error {
attr := new(app.DbAttr)
if err := c.BodyParser(attr); err != nil {
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{
"errors": err.Error(),
}) })
}
err := conf.DB.Set(attr)
if err != nil { if err != nil {
return JsonStatus(c, fiber.StatusForbidden, log.Fatal(err)
"Unable to set key: "+err.Error()) }
}
func RestGet(resp http.ResponseWriter, req *http.Request, key string, conf *cfg.Config) {
if key == "" {
JsonStatus(resp, http.StatusForbidden, "key not provided")
return
} }
return c.Status(fiber.StatusOK).JSON( // get list
Result{ entry, err := conf.DB.Get(&app.DbAttr{Key: key})
if err != nil {
JsonStatus(resp, http.StatusForbidden, "Unable to get key: "+err.Error())
return
}
if entry.GetKey() == "" {
JsonStatus(resp, http.StatusForbidden, "Key does not exist")
return
}
resp.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(resp).Encode(
SingleResponse{
Code: http.StatusOK,
Success: true, Success: true,
Code: fiber.StatusOK, Entry: entry,
}, })
) if err != nil {
log.Fatal(err)
}
}
func RestDelete(resp http.ResponseWriter, req *http.Request, key string, conf *cfg.Config) {
if key == "" {
JsonStatus(resp, http.StatusForbidden, "key not provided")
return
}
// get list
err := conf.DB.Del(&app.DbAttr{Key: key})
if err != nil {
JsonStatus(resp, http.StatusForbidden, "Unable to delete key: "+err.Error())
return
}
JsonStatus(resp, http.StatusOK, "key deleted")
}
func RestSet(resp http.ResponseWriter, req *http.Request, conf *cfg.Config) {
attr := new(app.DbAttr)
err := json.NewDecoder(req.Body).Decode(&attr)
if err != nil {
http.Error(resp, err.Error(), http.StatusBadRequest)
return
}
// attr.Data is a string, thus the decoder doesn't expect it to be
// base64 encoded. However, internally we need []byte, therefore
// we copy a cast to .Val. We also need to setup the .Preview
// value here.
attr.Val = []byte(attr.Data)
attr.SetPreview()
err = conf.DB.Set(attr)
if err != nil {
JsonStatus(resp, http.StatusForbidden, "Unable to set key: "+err.Error())
return
}
JsonStatus(resp, http.StatusOK, "key added/updated")
}
func Home(resp http.ResponseWriter) {
_, err := resp.Write([]byte("Use the REST API on " + apiprefix + "\r\n"))
if err != nil {
log.Fatal(err)
}
} }

101
rest/log.go Normal file
View File

@@ -0,0 +1,101 @@
package rest
/*
This logging middleware is based on
https://github.com/elithrar/admission-control/blob/v0.6.3/request_logger.go
by Matt Silverlock licensed under the Apache-2.0 license.
I am using slog and added a couple of small modifications.
*/
import (
"log/slog"
"net/http"
"runtime/debug"
"time"
)
// responseWriter is a minimal wrapper for http.ResponseWriter that allows the
// written HTTP status code to be captured for logging.
type responseWriter struct {
http.ResponseWriter
status int
size int
wroteHeader bool
}
func wrapResponseWriter(w http.ResponseWriter) *responseWriter {
return &responseWriter{ResponseWriter: w}
}
func (rw *responseWriter) Status() int {
return rw.status
}
func (rw *responseWriter) Size() int {
return rw.size
}
func (rw *responseWriter) WriteHeader(code int) {
if rw.wroteHeader {
return
}
rw.status = code
rw.ResponseWriter.WriteHeader(code)
rw.wroteHeader = true
}
func (rw *responseWriter) Write(data []byte) (int, error) {
written, err := rw.ResponseWriter.Write(data)
rw.size += written
return written, err
}
// LoggingMiddleware logs the incoming HTTP request & its duration.
func LogHandler() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(resp http.ResponseWriter, req *http.Request) {
defer func() {
if err := recover(); err != nil {
resp.WriteHeader(http.StatusInternalServerError)
slog.Info(
"internal server error",
"err", err,
"trace", string(debug.Stack()),
)
}
}()
start := time.Now()
wrapped := wrapResponseWriter(resp)
next.ServeHTTP(wrapped, req)
header := wrapped.Header()["Content-Type"]
var contenttype string
if header == nil {
contenttype = "text/plain"
} else {
contenttype = header[0]
}
slog.Info("request",
"ip", req.RemoteAddr,
"status", wrapped.status,
"method", req.Method,
"path", req.URL.EscapedPath(),
"size", wrapped.Size(),
"content-type", contenttype,
"duration", time.Since(start),
)
}
return http.HandlerFunc(fn)
}
}

View File

@@ -1,5 +1,5 @@
/* /*
Copyright © 2024 Thomas von Dein Copyright © 2024-2025 Thomas von Dein
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@@ -17,11 +17,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package rest package rest
import ( import (
"github.com/gofiber/fiber/v2" "encoding/json"
"github.com/gofiber/fiber/v2/middleware/compress" "log"
"github.com/gofiber/fiber/v2/middleware/cors" "net/http"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/tlinden/anydb/cfg" "codeberg.org/scip/anydb/cfg"
) )
// used to return to the api client // used to return to the api client
@@ -31,88 +31,61 @@ type Result struct {
Code int `json:"code"` Code int `json:"code"`
} }
const apiprefix = `/anydb/v1/`
func Runserver(conf *cfg.Config, args []string) error { func Runserver(conf *cfg.Config, args []string) error {
// setup api server // setup api server
router := SetupServer(conf) mux := http.NewServeMux()
// public rest api routes // just in case someone tries to access the non-api url
api := router.Group("/anydb/v1") mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
{ Home(w)
api.Get("/", func(c *fiber.Ctx) error {
return RestList(c, conf)
}) })
api.Post("/", func(c *fiber.Ctx) error { mux.HandleFunc("GET "+apiprefix+"{$}", func(w http.ResponseWriter, r *http.Request) {
// same thing as above but allows to supply parameters, see app.Dbattr{} RestList(w, r, conf)
return RestList(c, conf)
}) })
api.Get("/:key", func(c *fiber.Ctx) error { mux.HandleFunc("POST "+apiprefix+"{$}", func(w http.ResponseWriter, r *http.Request) {
return RestGet(c, conf) RestList(w, r, conf)
}) })
api.Delete("/:key", func(c *fiber.Ctx) error { mux.HandleFunc("GET "+apiprefix+"{key}", func(w http.ResponseWriter, r *http.Request) {
return RestDelete(c, conf) key := r.PathValue("key")
RestGet(w, r, key, conf)
}) })
api.Put("/", func(c *fiber.Ctx) error { mux.HandleFunc("DELETE "+apiprefix+"{key}", func(w http.ResponseWriter, r *http.Request) {
return RestSet(c, conf) key := r.PathValue("key")
}) RestDelete(w, r, key, conf)
}
// public routes
{
router.Get("/", func(c *fiber.Ctx) error {
return c.Send([]byte("Use the REST API"))
})
}
return router.Listen(conf.Listen)
}
func SetupServer(conf *cfg.Config) *fiber.App {
// disable colors
fiber.DefaultColors = fiber.Colors{}
router := fiber.New(fiber.Config{
CaseSensitive: true,
StrictRouting: true,
Immutable: true,
ServerHeader: "anydb serve",
AppName: "anydb",
}) })
router.Use(logger.New(logger.Config{ mux.HandleFunc("PUT "+apiprefix, func(w http.ResponseWriter, r *http.Request) {
Format: "${pid} ${ip}:${port} ${status} - ${method} ${path}\n", RestSet(w, r, conf)
DisableColors: true, })
}))
router.Use(cors.New(cors.Config{ logger := LogHandler()
AllowMethods: "GET,PUT,POST,DELETE",
ExposeHeaders: "Content-Type,Accept",
}))
router.Use(compress.New(compress.Config{ return http.ListenAndServe(conf.Listen, logger(mux))
Level: compress.LevelBestSpeed,
}))
return router
} }
/* /*
Wrapper to respond with proper json status, message and code, Wrapper to respond with proper json status, message and code,
shall be prepared and called by the handlers directly. shall be prepared and called by the handlers directly.
*/ */
func JsonStatus(c *fiber.Ctx, code int, msg string) error { func JsonStatus(resp http.ResponseWriter, code int, msg string) {
success := true success := code == http.StatusOK
if code != fiber.StatusOK { resp.Header().Set("Content-Type", "application/json")
success = false resp.WriteHeader(code)
}
return c.Status(code).JSON(Result{ err := json.NewEncoder(resp).Encode(
Result{
Code: code, Code: code,
Message: msg, Message: msg,
Success: success, Success: success,
}) })
if err != nil {
log.Fatal(err)
}
} }

View File

@@ -78,4 +78,4 @@ exec anydb -f test.db del foo
# check deleted # check deleted
exec anydb -f test.db list exec anydb -f test.db list
! stdout bar ! stdout blah

View File

@@ -1,99 +0,0 @@
package ui
import (
"log"
"log/slog"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg"
)
func newItemDelegate(keys *delegateKeyMap, config *cfg.Config) list.DefaultDelegate {
d := list.NewDefaultDelegate()
d.UpdateFunc = func(msg tea.Msg, m *list.Model) tea.Cmd {
var title string
if entry, ok := m.SelectedItem().(item); ok {
title = entry.Title()
slog.Debug("active entry", "entry", title)
} else {
slog.Debug("no active entry")
return nil
}
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, keys.choose):
return m.NewStatusMessage(statusMessageStyle("You chose " + title))
case key.Matches(msg, keys.remove):
if err := config.DB.Del(&app.DbAttr{Key: title}); err != nil {
log.Fatal(err)
}
index := m.Index()
m.RemoveItem(index)
if len(m.Items()) == 0 {
keys.remove.SetEnabled(false)
}
return m.NewStatusMessage(statusMessageStyle("Deleted " + title))
}
}
return nil
}
help := []key.Binding{keys.choose, keys.remove}
d.ShortHelpFunc = func() []key.Binding {
return help
}
d.FullHelpFunc = func() [][]key.Binding {
return [][]key.Binding{help}
}
return d
}
type delegateKeyMap struct {
choose key.Binding
remove key.Binding
}
// Additional short help entries. This satisfies the help.KeyMap interface and
// is entirely optional.
func (d delegateKeyMap) ShortHelp() []key.Binding {
return []key.Binding{
d.choose,
d.remove,
}
}
// Additional full help entries. This satisfies the help.KeyMap interface and
// is entirely optional.
func (d delegateKeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{
d.choose,
d.remove,
},
}
}
func newDelegateKeyMap() *delegateKeyMap {
return &delegateKeyMap{
choose: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "choose"),
),
remove: key.NewBinding(
key.WithKeys("x", "backspace"),
key.WithHelp("x", "delete"),
),
}
}

View File

@@ -1,95 +0,0 @@
/*
Copyright © 2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ui
import (
"fmt"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/tlinden/anydb/cfg"
)
type model struct {
spinner spinner.Model
conf *cfg.Config
quitting bool
err error
}
var quitKeys = key.NewBinding(
key.WithKeys("q", "esc", "ctrl+c"),
key.WithHelp("", "press q to quit"),
)
var (
appStyle = lipgloss.NewStyle().Padding(1, 2)
titleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFDF5")).
Background(lipgloss.Color("#25A065")).
Padding(0, 1)
statusMessageStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}).
Render
)
func InitialModel(conf *cfg.Config) model {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
return model{spinner: s, conf: conf}
}
func (m model) Init() tea.Cmd {
return m.spinner.Tick
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
if key.Matches(msg, quitKeys) {
m.quitting = true
return m, tea.Quit
}
return m, nil
case error:
m.err = msg
return m, nil
default:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
}
}
func (m model) View() string {
if m.err != nil {
return m.err.Error()
}
str := fmt.Sprintf("\n\n %s Loading forever... %s\n\n", m.spinner.View(), quitKeys.Help().Desc)
if m.quitting {
return str + "\n"
}
return str
}

View File

@@ -1,213 +0,0 @@
/*
Copyright © 2024 Thomas von Dein
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ui
import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg"
)
var (
appStyle = lipgloss.NewStyle().Padding(1, 2)
titleStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FFFDF5")).
Background(lipgloss.Color("#25A065")).
Padding(0, 1)
statusMessageStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}).
Render
)
type Loader struct {
items []list.Item
conf *cfg.Config
}
func (loader *Loader) Update() error {
entries, err := loader.conf.DB.List(&app.DbAttr{}, loader.conf.Fulltext)
if err != nil {
return err
}
loader.items = nil
for _, entry := range entries {
loader.items = append(loader.items, item{
title: entry.Key,
description: entry.Preview,
})
}
return nil
}
type model struct {
conf *cfg.Config
loader *Loader
quitting bool
err error
list list.Model
keys *listKeyMap
delegateKeys *delegateKeyMap
}
type listKeyMap struct {
toggleSpinner key.Binding
toggleTitleBar key.Binding
toggleStatusBar key.Binding
togglePagination key.Binding
toggleHelpMenu key.Binding
insertItem key.Binding
}
type item struct {
title string
description string
}
func (i item) Title() string { return i.title }
func (i item) Description() string { return i.description }
func (i item) FilterValue() string { return i.title }
func newListKeyMap() *listKeyMap {
return &listKeyMap{
insertItem: key.NewBinding(
key.WithKeys("a"),
key.WithHelp("a", "add item"),
),
toggleSpinner: key.NewBinding(
key.WithKeys("s"),
key.WithHelp("s", "toggle spinner"),
),
toggleTitleBar: key.NewBinding(
key.WithKeys("T"),
key.WithHelp("T", "toggle title"),
),
toggleStatusBar: key.NewBinding(
key.WithKeys("S"),
key.WithHelp("S", "toggle status"),
),
togglePagination: key.NewBinding(
key.WithKeys("P"),
key.WithHelp("P", "toggle pagination"),
),
toggleHelpMenu: key.NewBinding(
key.WithKeys("H"),
key.WithHelp("H", "toggle help"),
),
}
}
func NewModel(config *cfg.Config, entries app.DbEntries) model {
var (
delegateKeys = newDelegateKeyMap()
listKeys = newListKeyMap()
loader = Loader{conf: config}
)
// Setup list
if err := loader.Update(); err != nil {
panic(err)
}
delegate := newItemDelegate(delegateKeys, config)
dbList := list.New(loader.items, delegate, 0, 0)
dbList.Title = "DB Entries"
dbList.Styles.Title = titleStyle
dbList.AdditionalFullHelpKeys = func() []key.Binding {
return []key.Binding{
listKeys.toggleSpinner,
listKeys.insertItem,
listKeys.toggleTitleBar,
listKeys.toggleStatusBar,
listKeys.togglePagination,
listKeys.toggleHelpMenu,
}
}
return model{
list: dbList,
keys: listKeys,
delegateKeys: delegateKeys,
}
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
h, v := appStyle.GetFrameSize()
m.list.SetSize(msg.Width-h, msg.Height-v)
case tea.KeyMsg:
// Don't match any of the keys below if we're actively filtering.
if m.list.FilterState() == list.Filtering {
break
}
switch {
case key.Matches(msg, m.keys.toggleSpinner):
cmd := m.list.ToggleSpinner()
return m, cmd
case key.Matches(msg, m.keys.toggleTitleBar):
v := !m.list.ShowTitle()
m.list.SetShowTitle(v)
m.list.SetShowFilter(v)
m.list.SetFilteringEnabled(v)
return m, nil
case key.Matches(msg, m.keys.toggleStatusBar):
m.list.SetShowStatusBar(!m.list.ShowStatusBar())
return m, nil
case key.Matches(msg, m.keys.togglePagination):
m.list.SetShowPagination(!m.list.ShowPagination())
return m, nil
case key.Matches(msg, m.keys.toggleHelpMenu):
m.list.SetShowHelp(!m.list.ShowHelp())
return m, nil
case key.Matches(msg, m.keys.insertItem):
panic(1)
}
}
// This will also call our delegate's update function.
newListModel, cmd := m.list.Update(msg)
m.list = newListModel
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m model) View() string {
return appStyle.Render(m.list.View())
}