43 Commits

Author SHA1 Message Date
eb9e152cbe fix references to codeberg 2025-11-03 10:35:59 +01:00
9cecf1c09e moved to codeberg 2025-11-03 09:33:05 +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
83de01b349 fix release builder 2025-02-10 15:49:22 +01:00
b4cf1e4d1c link to the bug 2025-02-10 15:41:30 +01:00
42dc598deb Merge branch 'main' of github.com:TLINDEN/anydb 2025-02-10 15:40:05 +01:00
ff169a7198 add warning about #19 2025-02-10 15:39:40 +01:00
T.v.Dein
6c4f119bfa Fixes and additions: (#20)
- fix encryption bug #19, which was a regression
- added encryption unit test
- added debug logging here and there

Co-authored-by: Thomas von Dein <tom@vondein.org>
2025-02-10 15:34:04 +01:00
16b6075329 ci tuning 2025-02-10 15:33:21 +01:00
07808187d5 enhance changelog generation, update release builder 2025-02-05 17:49:26 +01:00
43 changed files with 29 additions and 5647 deletions

View File

@@ -1,47 +0,0 @@
name: build-and-test
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
version: [1.21,'1.22']
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.21
- 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,45 +0,0 @@
name: build-and-test
on:
push:
tags:
- "*"
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.22.11
- 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"
- name: Create Release
uses: mikepenz/action-gh-release@v0.2.0-a03
with:
body: ${{steps.github_release.outputs.changelog}}

View File

@@ -1,42 +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/>.
#
FROM golang:1.22-alpine as builder
RUN apk update
RUN apk upgrade
RUN apk add --no-cache git make
RUN git --version
WORKDIR /work
COPY go.mod .
COPY . .
RUN go mod download
RUN make
FROM alpine:latest
LABEL maintainer="Thomas von Dein <git@daemon.de>"
WORKDIR /app
COPY --from=builder /work/anydb /app/anydb
ENV LANG C.UTF-8
USER 1001:1001
ENTRYPOINT ["/app/anydb"]
CMD ["-h"]

111
Makefile
View File

@@ -1,111 +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/>.
#
# no need to modify anything below
tool = anydb
version = $(shell egrep "= .v" cfg/config.go | cut -d'=' -f2 | cut -d'"' -f 2)
archs = android darwin freebsd linux netbsd openbsd windows
PREFIX = /usr/local
UID = root
GID = 0
BRANCH = $(shell git branch --show-current)
COMMIT = $(shell git rev-parse --short=8 HEAD)
BUILD = $(shell date +%Y.%m.%d.%H%M%S)
VERSION := $(if $(filter $(BRANCH), development),$(version)-$(BRANCH)-$(COMMIT)-$(BUILD),$(version))
HAVE_POD := $(shell pod2text -h 2>/dev/null)
all: $(tool).1 cmd/$(tool).go app/dbentry.pb.go buildlocal
%.1: %.pod
ifdef HAVE_POD
pod2man -c "User Commands" -r 1 -s 1 $*.pod > $*.1
endif
cmd/%.go: %.pod
ifdef HAVE_POD
echo "package cmd" > cmd/$*.go
echo >> cmd/$*.go
echo "var manpage = \`" >> cmd/$*.go
pod2text $*.pod >> cmd/$*.go
echo "\`" >> cmd/$*.go
endif
# echo "var usage = \`" >> cmd/$*.go
# awk '/SYNOPS/{f=1;next} /DESCR/{f=0} f' $*.pod | sed 's/^ //' >> cmd/$*.go
# echo "\`" >> cmd/$*.go
app/dbentry.pb.go: 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
rm -rf app/github.com
buildlocal:
go build -ldflags "-X 'github.com/tlinden/anydb/cfg.VERSION=$(VERSION)'"
# binaries are being built by ci workflow on tag creation
release:
gh release create $(version) --generate-notes
install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/
clean:
rm -rf $(tool) releases coverage.out
test:
ANYDB_PASSWORD=test go test -v ./...
singletest:
@echo "Call like this: ''make singletest TEST=TestPrepareColumns MOD=lib"
ANYDB_PASSWORD=test go test -run $(TEST) github.com/tlinden/anydb/$(MOD)
cover-report:
go test ./... -cover -coverprofile=coverage.out
go tool cover -html=coverage.out
show-versions: buildlocal
@echo "### anydb version:"
@./anydb --version
@echo
@echo "### go module versions:"
@go list -m all
@echo
@echo "### go version used for building:"
@grep -m 1 go go.mod
goupdate:
go get -t -u=patch ./...
lint:
golangci-lint run
# 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:
make -C demo demo
.PHONY: demo

View File

@@ -1,12 +1,27 @@
## A personal key value store
[![Actions](https://github.com/tlinden/anydb/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/anydb/actions)
[![License](https://img.shields.io/badge/license-GPL-blue.svg)](https://github.com/tlinden/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/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://github.com/TLINDEN/anydb/releases/latest)
> [!IMPORTANT]
> This software is now being maintained on [Codeberg](https://codeberg.org/scip/anydb/).
[![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://codeberg.org/scip/anydb/blob/master/LICENSE)
[![Go Report Card](https://goreportcard.com/badge/codeberg.org/scip/anydb)](https://goreportcard.com/report/codeberg.org/scip/anydb)
[![GitHub release](https://img.shields.io/github/v/release/tlinden/anydb?color=%2300a719)](https://codeberg.org/scip/anydb/releases)
[![Documentation](https://img.shields.io/badge/manpage-documentation-blue)](https://github.com/TLINDEN/anydb/blob/master/anydb.pod)
> [!CAUTION]
> 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.
> If you are using anydb 0.1.3, you are urgently advised to
> upgrade to 0.2.0
Anydb is a simple to use commandline tool to store anything you'd
like, even binary files etc. It is a re-implementation of
[skate](https://github.com/charmbracelet/skate) for the following
@@ -34,17 +49,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:
![simple demo](https://github.com/TLINDEN/anydb/blob/main/demo/intro.gif)
![simple demo](https://codeberg.org/scip/anydb/raw/branch/demo/intro.gif)
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/demo/advanced.gif)
## Installation
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.
Download it and put it into some directory within your `$PATH` variable.
@@ -57,7 +72,7 @@ There are multiple ways to install **anydb**:
- You can also install from source. Issue the following commands in your shell:
```shell
git clone https://github.com/TLINDEN/anydb.git
git clone https://codeberg.org/scip/anydb.git
cd anydb
make
sudo make install
@@ -65,7 +80,7 @@ There are multiple ways to install **anydb**:
- Or, if you have the GO toolkit installed, just install it like this:
```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
@@ -92,14 +107,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
`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
The documentation is provided as a unix man-page. It will be
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
to be installed though): `perldoc anydb.pod`.
@@ -116,7 +131,7 @@ best way for me to forget to do something.
In order to report a bug, unexpected behavior, feature requests or to
submit a patch, please open an issue on github:
https://github.com/TLINDEN/anydb/issues.
https://codeberg.org/scip/anydb/issues.
## Copyright and license
@@ -128,7 +143,7 @@ T.v.Dein <tom AT vondein DOT org>
## Project homepage
https://github.com/TLINDEN/anydb
https://codeberg.org/scip/anydb
## Copyright and License

19
TODO.md
View File

@@ -1,19 +0,0 @@
## Features
- repl
- mime-type => exec app + value
- add waitgroup to db.go funcs
- RestList does not support any params?
## DB Structure
- put tags into sub bucket see #1
tags bucket
key/tag => tag/key
tag/key => tag
A delete would just delete all keys from all values and then:
lookup in tags bucket for all key/*, then iterate over the values and
remove all tag/key's. Then deleting a key would not leave any residue
behind.

759
anydb.1
View File

@@ -1,759 +0,0 @@
.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.42)
.\"
.\" Standard preamble:
.\" ========================================================================
.de Sp \" Vertical space (when we can't use .PP)
.if t .sp .5v
.if n .sp
..
.de Vb \" Begin verbatim text
.ft CW
.nf
.ne \\$1
..
.de Ve \" End verbatim text
.ft R
.fi
..
.\" Set up some character translations and predefined strings. \*(-- will
.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left
.\" double quote, and \*(R" will give a right double quote. \*(C+ will
.\" give a nicer C++. Capital omega is used to do unbreakable dashes and
.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff,
.\" nothing in troff, for use with C<>.
.tr \(*W-
.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p'
.ie n \{\
. ds -- \(*W-
. ds PI pi
. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch
. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch
. ds L" ""
. ds R" ""
. ds C` ""
. ds C' ""
'br\}
.el\{\
. ds -- \|\(em\|
. ds PI \(*p
. ds L" ``
. ds R" ''
. ds C`
. ds C'
'br\}
.\"
.\" Escape single quotes in literal strings from groff's Unicode transform.
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.\"
.\" If the F register is >0, we'll generate index entries on stderr for
.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index
.\" entries marked with X<> in POD. Of course, you'll have to process the
.\" output yourself in some meaningful fashion.
.\"
.\" Avoid warning from groff about undefined register 'F'.
.de IX
..
.nr rF 0
.if \n(.g .if rF .nr rF 1
.if (\n(rF:(\n(.g==0)) \{\
. if \nF \{\
. de IX
. tm Index:\\$1\t\\n%\t"\\$2"
..
. if !\nF==2 \{\
. nr % 0
. nr F 2
. \}
. \}
.\}
.rr rF
.\"
.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2).
.\" Fear. Run. Save yourself. No user-serviceable parts.
. \" fudge factors for nroff and troff
.if n \{\
. ds #H 0
. ds #V .8m
. ds #F .3m
. ds #[ \f1
. ds #] \fP
.\}
.if t \{\
. ds #H ((1u-(\\\\n(.fu%2u))*.13m)
. ds #V .6m
. ds #F 0
. ds #[ \&
. ds #] \&
.\}
. \" simple accents for nroff and troff
.if n \{\
. ds ' \&
. ds ` \&
. ds ^ \&
. ds , \&
. ds ~ ~
. ds /
.\}
.if t \{\
. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u"
. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u'
. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u'
. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u'
. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u'
. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u'
.\}
. \" troff and (daisy-wheel) nroff accents
.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V'
.ds 8 \h'\*(#H'\(*b\h'-\*(#H'
.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#]
.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H'
.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u'
.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#]
.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#]
.ds ae a\h'-(\w'a'u*4/10)'e
.ds Ae A\h'-(\w'A'u*4/10)'E
. \" corrections for vroff
.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u'
.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u'
. \" for low resolution devices (crt and lpr)
.if \n(.H>23 .if \n(.V>19 \
\{\
. ds : e
. ds 8 ss
. ds o a
. ds d- d\h'-1'\(ga
. ds D- D\h'-1'\(hy
. ds th \o'bp'
. ds Th \o'LP'
. ds ae ae
. ds Ae AE
.\}
.rm #[ #] #H #V #F C
.\" ========================================================================
.\"
.IX Title "ANYDB 1"
.TH ANYDB 1 "2025-01-01" "1" "User Commands"
.\" For nroff, turn off justification. Always turn off hyphenation; it makes
.\" way too many mistakes in technical documents.
.if n .ad l
.nh
.SH "anydb"
.IX Header "anydb"
anydb \- a personal key value store
.SH "SYNOPSIS"
.IX Header "SYNOPSIS"
.Vb 3
\& Usage:
\& anydb <command> [options] [flags]
\& anydb [command]
\&
\& Available Commands:
\& completion Generate the autocompletion script for the specified shell
\& del Delete key
\& edit Edit a key
\& export Export database to json
\& get Retrieve value for a key
\& help Help about any command
\& import Import database dump
\& info info
\& list List database contents
\& man show manual page
\& serve run REST API listener
\& set Insert key/value pair
\&
\& Flags:
\& \-b, \-\-bucket string use other bucket (default: data) (default "data")
\& \-c, \-\-config string toml config file
\& \-f, \-\-dbfile string DB file to use (default "/home/scip/.config/anydb/default.db")
\& \-d, \-\-debug Enable debugging
\& \-h, \-\-help help for anydb
\& \-v, \-\-version Print program version
\&
\& Use "anydb [command] \-\-help" for more information about a command.
.Ve
.SH "DESCRIPTION"
.IX Header "DESCRIPTION"
Anydb is a commandline personal key value store, it is simple to use
and can be used to store anything you'd like, even binary files
etc. It uses a key/value store (bbolt) in your home directory.
.PP
The tool provides a number of subcommands to use it, there are global
options and each subcommand has its own set of options.
.SH "GLOBAL OPTIONS"
.IX Header "GLOBAL OPTIONS"
.ie n .IP """\-f, \-\-dbfile filename""" 4
.el .IP "\f(CW\-f, \-\-dbfile filename\fR" 4
.IX Item "-f, --dbfile filename"
The default location of your databas is
\&\f(CW\*(C`$HOME/.config/anydb/default.db\*(C'\fR. You can change this with the \f(CW\*(C`\-f\*(C'\fR
option.
.ie n .IP """\-b, \-\-bucket name""" 4
.el .IP "\f(CW\-b, \-\-bucket name\fR" 4
.IX Item "-b, --bucket name"
Data in a bbolt key-value-store are managed in so called
buckets. These are kind of namespaces, where each key must be
unique. However, a database may contain more than one bucket.
.Sp
By default anydb uses a bucket named \*(L"data\*(R", but you can change this
using the option \f(CW\*(C`\-b\*(C'\fR.
.Sp
Buckets can be configured to always encrypt values, see \s-1ENCRYTPTION\s0.
.ie n .IP """\-c, \-\-config filename""" 4
.el .IP "\f(CW\-c, \-\-config filename\fR" 4
.IX Item "-c, --config filename"
Under normal circumstances you don't need a configuration file. But if
you want, you can provide one using the option \f(CW\*(C`\-c\*(C'\fR.
.Sp
Anydb looks for a couple of default locations for a config file. You
only need this option if you want to supply a configuration on a
non-standard location. See \s-1CONFIGURATION\s0 for more details.
.ie n .IP """\-d, \-\-debug""" 4
.el .IP "\f(CW\-d, \-\-debug\fR" 4
.IX Item "-d, --debug"
Enable debug output.
.ie n .IP """\-h, \-\-help""" 4
.el .IP "\f(CW\-h, \-\-help\fR" 4
.IX Item "-h, --help"
Show the usage of anydb.
.ie n .IP """\-v, \-\-version""" 4
.el .IP "\f(CW\-v, \-\-version\fR" 4
.IX Item "-v, --version"
Show the program version.
.PP
All of these options can be used with subcommands as well.
.SH "SUBCOMMANDS"
.IX Header "SUBCOMMANDS"
.SS "completion"
.IX Subsection "completion"
The \fBcompletion\fR command can be used to setup completion for
anydb. Just put something like this into your shell's configuration
file:
.PP
.Vb 1
\& source <(anydb completion bash)
.Ve
.PP
If you use another shell, specify it instead of bash, of course.
.SS "set"
.IX Subsection "set"
The \fBset\fR command is being used to insert or update a key-value pair.
.PP
Usage:
.PP
.Vb 2
\& Usage:
\& anydb set <key> [<value> | \-r <file>] [\-t <tag>] [flags]
\&
\& Aliases:
\& set, add, s, +
\&
\& Flags:
\& \-e, \-\-encrypt encrypt value
\& \-r, \-\-file string Filename or \- for STDIN
\& \-h, \-\-help help for set
\& \-t, \-\-tags tag,tag,... tags, multiple allowed
.Ve
.PP
The standard way to insert a new entry is really simple:
.PP
.Vb 1
\& anydb set key value
.Ve
.PP
If you don't specify a value, anydb expects you to feed it some data
via \s-1STDIN.\s0 For example:
.PP
.Vb 1
\& anydb set key < file
.Ve
.PP
You might as well specify a file directly using the \f(CW\*(C`\-f\*(C'\fR option:
.PP
.Vb 1
\& anydb set key \-f file
.Ve
.PP
Values can be encrypted using \fBChaCha20Poly1305\fR when you specify the
\&\f(CW\*(C`\-e\*(C'\fR option. Anydb will ask you interactively for a passphrase. You
can also provide the passphrase using the environment variable
\&\f(CW\*(C`ANYDB_PASSWORD\*(C'\fR. To encrypt the value, a cryptographically secure
key will be derived from the passphrase using the ArgonID2
algorithm. Each value can be encrypted with another passphrase. So,
the database itself is not encrypted, just the values.
.PP
You can supply tags by using the option \f(CW\*(C`\-t\*(C'\fR. Multiple tags can be
provided either by separating them with a comma or by using multiple
\&\f(CW\*(C`\-t\*(C'\fR parameters:
.PP
.Vb 2
\& anydb set key value \-t tag1,tag2
\& anydb set key value \-t tag1 \-t tag2
.Ve
.PP
You can later filter entries by tag or by a combination of tags.
.PP
To edit or modify an entry, just use the \fBset\fR command with the same
key, the value in the database will be overwritten with the new
value. An alternative option is the \fBedit\fR command, see below.
.SS "get"
.IX Subsection "get"
To retrieve the value of a key, use the \fBget\fR subcommand.
.PP
Usage:
.PP
.Vb 2
\& Usage:
\& anydb get <key> [\-o <file>] [\-m <mode>] [\-n \-N] [\-T <tpl>] [flags]
\&
\& Aliases:
\& get, show, g, .
\&
\& Flags:
\& \-h, \-\-help help for get
\& \-m, \-\-mode string output format (simple|wide|json|template) (default \*(Aqsimple\*(Aq)
\& \-n, \-\-no\-headers omit headers in tables
\& \-N, \-\-no\-human do not translate to human readable values
\& \-o, \-\-output string output value to file (ignores \-m)
\& \-T, \-\-template string go template for \*(Aq\-m template\*(Aq
.Ve
.PP
In its simplest form you just call the \fBget\fR subcommand with the key
you want to have the value for. The value is being printed to \s-1STDOUT\s0
by default:
.PP
.Vb 1
\& anydb get key
.Ve
.PP
If the value is binary content, it will not just being printed. In
those cases you need to either redirect output into a file or use the
option \f(CW\*(C`\-o\*(C'\fR to write to a file:
.PP
.Vb 2
\& anydb get key > file
\& anydb get key \-o file
.Ve
.PP
If the value is encrypted, you will be asked for the passphrase to
decrypt it. If the environment variable \f(CW\*(C`ANYDB_PASSWORD\*(C'\fR is set, its
value will be used instead.
.PP
There are different output modes you can choose from: simple, wide and
json. The \*(L"simple\*(R" mode is the default one, it just prints the value
as is. The \*(L"wide\*(R" mode prints a tabular output similar to the \fBlist\fR
subcommand, see there for more details. The options \f(CW\*(C`\-n\*(C'\fR and \f(CW\*(C`\-N\*(C'\fR
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
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
\&\s-1TEMPLATES\s0.
.SS "list"
.IX Subsection "list"
The \fBlist\fR subcommand displays a list of all database entries.
.PP
Usage:
.PP
.Vb 2
\& Usage:
\& anydb list [<filter\-regex> | \-t <tag> ] [\-m <mode>] [\-nNif] [\-T <tpl>] [flags]
\&
\& Aliases:
\& list, ls, /, find, search
\&
\& Flags:
\& \-i, \-\-case\-insensitive filter case insensitive
\& \-h, \-\-help help for list
\& \-m, \-\-mode string output format (table|wide|json|template), wide is a verbose table. (default \*(Aqtable\*(Aq)
\& \-n, \-\-no\-headers omit headers in tables
\& \-N, \-\-no\-human do not translate to human readable values
\& \-s, \-\-search\-fulltext perform a full text search
\& \-t, \-\-tags stringArray tags, multiple allowed
\& \-T, \-\-template string go template for \*(Aq\-m template\*(Aq
\& \-l, \-\-wide\-output output mode: wide
.Ve
.PP
In its simplest form \- without any options \- , the \fBlist\fR command
just prints all keys with their values to \s-1STDOUT.\s0 Values are being
truncated to maximum of 60 characters, that is, multiline values are
not completely shown in order to keep the tabular view readable.
.PP
To get more informations about each entry, use the \f(CW\*(C`\-o wide\*(C'\fR or \f(CW\*(C`\-l\*(C'\fR
option. In addition to the key and value also the size, update
timestamp and tags will be printed. Time and size values are converted
into a human readable form, you can suppress this behavior with the
\&\f(CW\*(C`\-N\*(C'\fR option. You may omit the headers using the option \f(CW\*(C`\-n\*(C'\fR
.PP
Sometimes you might want to filter the list of entries. Either because
your database grew too large or because you're searching for
something. In that case you have two options: You may supply one or
more tags or provide a filter regexp. To filter by tag, do:
.PP
.Vb 3
\& anydb list \-t tag1
\& anydb list \-t tag1,tag2
\& anydb list \-t tag1 \-t tag2
.Ve
.PP
To filter using a regular expression, do:
.PP
.Vb 1
\& anydb list "foo.*bar"
.Ve
.PP
Regular expressions follow the golang \fBre2\fR syntax. For more details
about the syntax, refer to
<https://github.com/google/re2/wiki/Syntax>. Please note, that this
regexp dialect is not \s-1PCRE\s0 compatible, but supports most of its
features.
.PP
If you want to search case insensitive, add the option \f(CW\*(C`\-i\*(C'\fR.
.PP
By default anydb only searches through the keys. If you want to search
through the values as well, then use the \f(CW\*(C`\-s\*(C'\fR option, which enables
full-text search.
.PP
You can \- as with the \fBget\fR command \- use other output modes. The
default mode is \*(L"table\*(R". The \*(L"wide\*(R" mode is, as already mentioned, a
more detailed table. Also supported is \*(L"json\*(R" mode and \*(L"template\*(R"
mode. For details about using templates see \s-1TEMPLATES\s0.
.SS "del"
.IX Subsection "del"
Use the \fBdel\fR command to delete database entries.
.PP
Usage:
.PP
.Vb 2
\& Usage:
\& anydb del <key> [flags]
\&
\& Aliases:
\& del, d, rm
\&
\& Flags:
\& \-h, \-\-help help for del
.Ve
.PP
The subcommand \fBdel\fR does not provide any further options, it just
deletes the entry referred to by the given key. No questions are being
asked.
.SS "edit"
.IX Subsection "edit"
The \fBedit\fR command makes it easier to modify larger entries.
.PP
Usage:
.PP
.Vb 2
\& Usage:
\& anydb edit <key> [flags]
\&
\& Aliases:
\& edit, modify, mod, ed, vi
\&
\& Flags:
\& \-h, \-\-help help for edit
.Ve
.PP
The subcommand \fBedit\fR does not provide any further options. It
works like this:
.IP "1. Write the value info a temporary file." 4
.IX Item "1. Write the value info a temporary file."
.PD 0
.IP "2. Execute the editor (which one, see below!) with that file." 4
.IX Item "2. Execute the editor (which one, see below!) with that file."
.IP "3. Now you can edit the file and save+close it when done." 4
.IX Item "3. Now you can edit the file and save+close it when done."
.IP "4. Anydb picks up the file and if the content has changed, puts its value into the \s-1DB.\s0" 4
.IX Item "4. Anydb picks up the file and if the content has changed, puts its value into the DB."
.PD
.PP
By default anydb executes the \f(CW\*(C`vi\*(C'\fR command. You can modify this
behavior by setting the environment variable \f(CW\*(C`EDITOR\*(C'\fR appropriately.
.PP
Please note, that this does not work with binary content!
.SS "export"
.IX Subsection "export"
Since the bbolt database file is not portable across platforms (it is
bound to the endianess of the \s-1CPU\s0 it was being created on), you might
want to create a backup file of your database. You can do this with
the \fBexport\fR subcommand.
.PP
Usage:
.PP
.Vb 2
\& Usage:
\& anydb export \-o <json filename> [flags]
\&
\& Aliases:
\& export, dump, backup
\&
\& Flags:
\& \-h, \-\-help help for export
\& \-o, \-\-output string output to file
.Ve
.PP
The database dump is a \s-1JSON\s0 representation of the whole database and
will be printed to the file specified with the \f(CW\*(C`\-o\*(C'\fR option. If you
specify \*(L"\-\*(R" as the filename, it will be written to \s-1STDIN.\s0
.PP
.Vb 2
\& anydb export \-o dump.json
\& anydb export \-o \- > dump.json
.Ve
.PP
Please note, that encrypted values will not be decrypted. This might
change in a future version of anydb.
.SS "import"
.IX Subsection "import"
The \fBimport\fR subcommand can be used to restore a database from a \s-1JSON\s0
dump.
.PP
Usage:
.PP
.Vb 2
\& Usage:
\& anydb import \-i <json file> [flags]
\&
\& Aliases:
\& import, restore
\&
\& Flags:
\& \-r, \-\-file string Filename or \- for STDIN
\& \-h, \-\-help help for import
\& \-t, \-\-tags stringArray tags, multiple allowed
.Ve
.PP
The \f(CW\*(C`import\*(C'\fR subcommand reads the \s-1JSON\s0 contents from
the file specified with the \f(CW\*(C`\-i\*(C'\fR option. If you specify \*(L"\-\*(R" as the
filename, it will be read from \s-1STDIN.\s0
.PP
.Vb 3
\& anydb import \-i \- < dump.json
\& anydb import \-i dump.json
\& cat dump.json | anydb import \-i \-
.Ve
.PP
If there is already a database, it will be saved by appending a
timestamp and a new database with the contents of the dump will be
created.
.SS "serve"
.IX Subsection "serve"
Anydb provides a RESTful \s-1API,\s0 which you can use to manage the database
from somewhere else. The \s-1API\s0 does not provide any authentication or
any other security measures, so better only use it on localhost.
.PP
Usage:
.PP
.Vb 2
\& Usage:
\& anydb serve [\-l host:port] [flags]
\&
\& Flags:
\& \-h, \-\-help help for serve
\& \-l, \-\-listen string host:port (default "localhost:8787")
.Ve
.PP
To start the listener, just execute the \fBserve\fR subcommand. You can
tweak the ip address and tcp port using the \f(CW\*(C`\-l\*(C'\fR option. The listener
will not fork and run in the foreground. Logs are being printed to
\&\s-1STDOUT\s0 as long as the listener runs.
.PP
For more details about the \s-1API,\s0 please see the \*(L"\s-1REST API\*(R"\s0 section.
.SS "info"
.IX Subsection "info"
The \fBinfo\fR subcommand shows you some information about your current
database.
.PP
Usage:
.PP
.Vb 2
\& Usage:
\& anydb info [flags]
\&
\& Flags:
\& \-h, \-\-help help for info
\& \-N, \-\-no\-human do not translate to human readable values
.Ve
.PP
Data being shown are: filename and size, number of keys per bucket. If
you supply the \f(CW\*(C`\-d\*(C'\fR option (debug), some bbolt internals are being
displayed as well.
.SS "man"
.IX Subsection "man"
The \fBman\fR subcommand shows an unformatted text variant of the manual
page (which are currently reading).
.PP
Usage:
.PP
.Vb 2
\& Usage:
\& anydb man [flags]
\&
\& Flags:
\& \-h, \-\-help help for man
.Ve
.PP
The manual is being piped into the \f(CW\*(C`more\*(C'\fR command, which is being
expected to exist according to the \s-1POSIX\s0 standard on all supported
unix platforms. It might not work on Windows.
.SH "TEMPLATES"
.IX Header "TEMPLATES"
The \fBget\fR and \fBlist\fR commands support a template feature, which is
very handy to create you own kind of formatting. The template syntax
being used is the \s-1GO\s0 template language, refer to
<https://pkg.go.dev/text/template> for details.
.PP
Each template operates on one or more entries, no loop construct is
required, the template provided applies to every matching entry
separatley.
.PP
The following template variables can be used:
.IP "\fBKey\fR \- string" 4
.IX Item "Key - string"
.PD 0
.IP "\fBValue\fR \- string" 4
.IX Item "Value - string"
.IP "\fBBin\fR \- []byte" 4
.IX Item "Bin - []byte"
.IP "\fBCreated\fR \- time.Time" 4
.IX Item "Created - time.Time"
.IP "\fBTags\fR \- []string" 4
.IX Item "Tags - []string"
.IP "\fBEncrypted\fR bool" 4
.IX Item "Encrypted bool"
.PD
.PP
Prepend a single dot (\*(L".\*(R") before each variable name.
.PP
Here are some examples how to use the feature:
.PP
Only show the keys of all entries:
.PP
.Vb 1
\& anydb list \-m template \-T "{{ .Key }}"
.Ve
.PP
Format the list in a way so that is possible to evaluate it in a
shell:
.PP
.Vb 2
\& eval $(anydb get foo \-m template \-T "key=\*(Aq{{ .Key }}\*(Aq value=\*(Aq{{ .Value }}\*(Aq ts=\*(Aq{{ .Created}}\*(Aq")
\& echo "Key: $key, Value: $value"
.Ve
.PP
Print the values in \s-1CSV\s0 format \s-1ONLY\s0 if they have some tag:
.PP
.Vb 1
\& anydb list \-m template \-T "{{ if .Tags }}{{ .Key }},{{ .Value }},{{ .Created}}{{ end }}"
.Ve
.SH "CONFIGURATION"
.IX Header "CONFIGURATION"
Anydb looks at the following locations for a configuration file, in
that order:
.ie n .IP """$HOME/.config/anydb/anydb.toml""" 4
.el .IP "\f(CW$HOME/.config/anydb/anydb.toml\fR" 4
.IX Item "$HOME/.config/anydb/anydb.toml"
.PD 0
.ie n .IP """$HOME/.anydb.toml""" 4
.el .IP "\f(CW$HOME/.anydb.toml\fR" 4
.IX Item "$HOME/.anydb.toml"
.ie n .IP """anydb.toml"" in the current directory" 4
.el .IP "\f(CWanydb.toml\fR in the current directory" 4
.IX Item "anydb.toml in the current directory"
.ie n .IP "or specify one using ""\-c""" 4
.el .IP "or specify one using \f(CW\-c\fR" 4
.IX Item "or specify one using -c"
.PD
.PP
The configuration format uses the \s-1TOML\s0 language, refer to
<https://toml.io/en/> for more details. The key names correspond to
the commandline options in most cases.
.PP
Configuration follows a certain precedence: the files are tried to be
read in the given order, followed by commandline options. That is, the
last configuration file wins, unless the user provides a commandline
option, then this setting will be taken.
.PP
A complete configuration file might look like this:
.PP
.Vb 7
\& # defaults
\& dbfile = "~/.config/anydb/default.db"
\& dbbucket = "data"
\& noheaders = false
\& nohumanize = false
\& encrypt = false
\& listen = "localhost:8787"
\&
\& # different setups for different buckets
\& [buckets.data]
\& encrypt = true
\&
\& [buckets.test]
\& encrypt = false
.Ve
.PP
Under normal circumstances you don't need a configuration
file. However, if you want to use different buckets, then this might
be a handy option. Buckets are being configured in ini-style with the
term \*(L"bucket.\*(R" followed by the bucket name. In the example above we
enable encryption for the default bucket \*(L"data\*(R" and disable it for a
bucket \*(L"test\*(R". To use different buckets, use the \f(CW\*(C`\-b\*(C'\fR option.
.SH "REST API"
.IX Header "REST API"
The subcommand \fBserve\fR starts a simple \s-1HTTP\s0 service, which responds
to RESTful \s-1HTTP\s0 requests. The listener responds to all requests with a
\&\s-1JSON\s0 encoded response. The response contains the status and the
content \- if any \- of the requested resource.
.PP
The following requests are supported:
.IP "\fB\s-1GET\s0 /anydb/v1/\fR" 4
.IX Item "GET /anydb/v1/"
Returns a \s-1JSON\s0 encoded list of all entries.
.IP "\fB\s-1GET\s0 /anydb/v1/key\fR" 4
.IX Item "GET /anydb/v1/key"
Returns the \s-1JSON\s0 encoded entry, if found.
.IP "\fB\s-1PUT\s0 /anydb/v1/\fR" 4
.IX Item "PUT /anydb/v1/"
Create an entry. Expects a \s-1JSON\s0 encoded request object in \s-1POST\s0 data.
.IP "\fB\s-1DELETE\s0 /anydb/v1/key\fR" 4
.IX Item "DELETE /anydb/v1/key"
Delete an entry.
.PP
Some curl example calls to the \s-1API:\s0
.PP
Post a new key:
curl \-X \s-1PUT\s0 localhost:8787/anydb/v1/ \e
\-H 'Content\-Type: application/json' \e
\-d '{\*(L"key\*(R":\*(L"foo\*(R",\*(L"val\*(R":\*(L"bar\*(R"}'
.PP
Retrieve the value:
.PP
.Vb 1
\& curl localhost:8787/anydb/v1/foo
.Ve
.PP
List all keys:
.PP
.Vb 1
\& curl localhost:8787/anydb/v1/
.Ve
.SH "BUGS"
.IX Header "BUGS"
In order to report a bug, unexpected behavior, feature requests
or to submit a patch, please open an issue on github:
<https://github.com/TLINDEN/anydb/issues>.
.PP
Please repeat the failing command with debugging enabled \f(CW\*(C`\-d\*(C'\fR and
include the output in the issue.
.SH "LIMITATIONS"
.IX Header "LIMITATIONS"
The \s-1REST API\s0 list request doesn't provide any filtering capabilities yet.
.SH "LICENSE"
.IX Header "LICENSE"
This software is licensed under the \s-1GNU GENERAL PUBLIC LICENSE\s0 version 3.
.PP
Copyright (c) 2024 by Thomas von Dein
.SH "AUTHORS"
.IX Header "AUTHORS"
Thomas von Dein \fBtom \s-1AT\s0 vondein \s-1DOT\s0 org\fR

606
anydb.pod
View File

@@ -1,606 +0,0 @@
=head1 anydb
anydb - a personal key value store
=head1 SYNOPSIS
Usage:
anydb <command> [options] [flags]
anydb [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
del Delete key
edit Edit a key
export Export database to json
get Retrieve value for a key
help Help about any command
import Import database dump
info info
list List database contents
man show manual page
serve run REST API listener
set Insert key/value pair
Flags:
-b, --bucket string use other bucket (default: data) (default "data")
-c, --config string toml config file
-f, --dbfile string DB file to use (default "/home/scip/.config/anydb/default.db")
-d, --debug Enable debugging
-h, --help help for anydb
-v, --version Print program version
Use "anydb [command] --help" for more information about a command.
=head1 DESCRIPTION
Anydb is a commandline personal key value store, it is simple to use
and can be used to store anything you'd like, even binary files
etc. It uses a key/value store (bbolt) in your home directory.
The tool provides a number of subcommands to use it, there are global
options and each subcommand has its own set of options.
=head1 GLOBAL OPTIONS
=over
=item C<-f, --dbfile filename>
The default location of your databas is
C<$HOME/.config/anydb/default.db>. You can change this with the C<-f>
option.
=item C<-b, --bucket name>
Data in a bbolt key-value-store are managed in so called
buckets. These are kind of namespaces, where each key must be
unique. However, a database may contain more than one bucket.
By default anydb uses a bucket named "data", but you can change this
using the option C<-b>.
Buckets can be configured to always encrypt values, see L<ENCRYTPTION>.
=item C<-c, --config filename>
Under normal circumstances you don't need a configuration file. But if
you want, you can provide one using the option C<-c>.
Anydb looks for a couple of default locations for a config file. You
only need this option if you want to supply a configuration on a
non-standard location. See L<CONFIGURATION> for more details.
=item C<-d, --debug>
Enable debug output.
=item C<-h, --help>
Show the usage of anydb.
=item C<-v, --version>
Show the program version.
=back
All of these options can be used with subcommands as well.
=head1 SUBCOMMANDS
=head2 completion
The B<completion> command can be used to setup completion for
anydb. Just put something like this into your shell's configuration
file:
source <(anydb completion bash)
If you use another shell, specify it instead of bash, of course.
=head2 set
The B<set> command is being used to insert or update a key-value pair.
Usage:
Usage:
anydb set <key> [<value> | -r <file>] [-t <tag>] [flags]
Aliases:
set, add, s, +
Flags:
-e, --encrypt encrypt value
-r, --file string Filename or - for STDIN
-h, --help help for set
-t, --tags tag,tag,... tags, multiple allowed
The standard way to insert a new entry is really simple:
anydb set key value
If you don't specify a value, anydb expects you to feed it some data
via STDIN. For example:
anydb set key < file
You might as well specify a file directly using the C<-f> option:
anydb set key -f file
Values can be encrypted using B<ChaCha20Poly1305> when you specify the
C<-e> option. Anydb will ask you interactively for a passphrase. You
can also provide the passphrase using the environment variable
C<ANYDB_PASSWORD>. To encrypt the value, a cryptographically secure
key will be derived from the passphrase using the ArgonID2
algorithm. Each value can be encrypted with another passphrase. So,
the database itself is not encrypted, just the values.
You can supply tags by using the option C<-t>. Multiple tags can be
provided either by separating them with a comma or by using multiple
C<-t> parameters:
anydb set key value -t tag1,tag2
anydb set key value -t tag1 -t tag2
You can later filter entries by tag or by a combination of tags.
To edit or modify an entry, just use the B<set> command with the same
key, the value in the database will be overwritten with the new
value. An alternative option is the B<edit> command, see below.
=head2 get
To retrieve the value of a key, use the B<get> subcommand.
Usage:
Usage:
anydb get <key> [-o <file>] [-m <mode>] [-n -N] [-T <tpl>] [flags]
Aliases:
get, show, g, .
Flags:
-h, --help help for get
-m, --mode string output format (simple|wide|json|template) (default 'simple')
-n, --no-headers omit headers in tables
-N, --no-human do not translate to human readable values
-o, --output string output value to file (ignores -m)
-T, --template string go template for '-m template'
In its simplest form you just call the B<get> subcommand with the key
you want to have the value for. The value is being printed to STDOUT
by default:
anydb get key
If the value is binary content, it will not just being printed. In
those cases you need to either redirect output into a file or use the
option C<-o> to write to a file:
anydb get key > file
anydb get key -o file
If the value is encrypted, you will be asked for the passphrase to
decrypt it. If the environment variable C<ANYDB_PASSWORD> is set, its
value will be used instead.
There are different output modes you can choose from: simple, wide and
json. The "simple" mode is the default one, it just prints the value
as is. The "wide" mode prints a tabular output similar to the B<list>
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
the raw JSON representation of the whole database entry. Decryption
will only take place in "simple" and "json" mode. The "template" mode
provides the most flexibily, it is detailed in the section
L<TEMPLATES>.
=head2 list
The B<list> subcommand displays a list of all database entries.
Usage:
Usage:
anydb list [<filter-regex> | -t <tag> ] [-m <mode>] [-nNif] [-T <tpl>] [flags]
Aliases:
list, ls, /, find, search
Flags:
-i, --case-insensitive filter case insensitive
-h, --help help for list
-m, --mode string output format (table|wide|json|template), wide is a verbose table. (default 'table')
-n, --no-headers omit headers in tables
-N, --no-human do not translate to human readable values
-s, --search-fulltext perform a full text search
-t, --tags stringArray tags, multiple allowed
-T, --template string go template for '-m template'
-l, --wide-output output mode: wide
In its simplest form - without any options - , the B<list> command
just prints all keys with their values to STDOUT. Values are being
truncated to maximum of 60 characters, that is, multiline values are
not completely shown in order to keep the tabular view readable.
To get more informations about each entry, use the C<-o wide> or C<-l>
option. In addition to the key and value also the size, update
timestamp and tags will be printed. Time and size values are converted
into a human readable form, you can suppress this behavior with the
C<-N> option. You may omit the headers using the option C<-n>
Sometimes you might want to filter the list of entries. Either because
your database grew too large or because you're searching for
something. In that case you have two options: You may supply one or
more tags or provide a filter regexp. To filter by tag, do:
anydb list -t tag1
anydb list -t tag1,tag2
anydb list -t tag1 -t tag2
To filter using a regular expression, do:
anydb list "foo.*bar"
Regular expressions follow the golang B<re2> syntax. For more details
about the syntax, refer to
L<https://github.com/google/re2/wiki/Syntax>. Please note, that this
regexp dialect is not PCRE compatible, but supports most of its
features.
If you want to search case insensitive, add the option C<-i>.
By default anydb only searches through the keys. If you want to search
through the values as well, then use the C<-s> option, which enables
full-text search.
You can - as with the B<get> command - use other output modes. The
default mode is "table". The "wide" mode is, as already mentioned, a
more detailed table. Also supported is "json" mode and "template"
mode. For details about using templates see L<TEMPLATES>.
=head2 del
Use the B<del> command to delete database entries.
Usage:
Usage:
anydb del <key> [flags]
Aliases:
del, d, rm
Flags:
-h, --help help for del
The subcommand B<del> does not provide any further options, it just
deletes the entry referred to by the given key. No questions are being
asked.
=head2 edit
The B<edit> command makes it easier to modify larger entries.
Usage:
Usage:
anydb edit <key> [flags]
Aliases:
edit, modify, mod, ed, vi
Flags:
-h, --help help for edit
The subcommand B<edit> does not provide any further options. It
works like this:
=over
=item 1. Write the value info a temporary file.
=item 2. Execute the editor (which one, see below!) with that file.
=item 3. Now you can edit the file and save+close it when done.
=item 4. Anydb picks up the file and if the content has changed, puts its value into the DB.
=back
By default anydb executes the C<vi> command. You can modify this
behavior by setting the environment variable C<EDITOR> appropriately.
Please note, that this does not work with binary content!
=head2 export
Since the bbolt database file is not portable across platforms (it is
bound to the endianess of the CPU it was being created on), you might
want to create a backup file of your database. You can do this with
the B<export> subcommand.
Usage:
Usage:
anydb export -o <json filename> [flags]
Aliases:
export, dump, backup
Flags:
-h, --help help for export
-o, --output string output to file
The database dump is a JSON representation of the whole database and
will be printed to the file specified with the C<-o> option. If you
specify "-" as the filename, it will be written to STDIN.
anydb export -o dump.json
anydb export -o - > dump.json
Please note, that encrypted values will not be decrypted. This might
change in a future version of anydb.
=head2 import
The B<import> subcommand can be used to restore a database from a JSON
dump.
Usage:
Usage:
anydb import -i <json file> [flags]
Aliases:
import, restore
Flags:
-r, --file string Filename or - for STDIN
-h, --help help for import
-t, --tags stringArray tags, multiple allowed
The C<import> subcommand reads the JSON contents from
the file specified with the C<-i> option. If you specify "-" as the
filename, it will be read from STDIN.
anydb import -i - < dump.json
anydb import -i dump.json
cat dump.json | anydb import -i -
If there is already a database, it will be saved by appending a
timestamp and a new database with the contents of the dump will be
created.
=head2 serve
Anydb provides a RESTful API, which you can use to manage the database
from somewhere else. The API does not provide any authentication or
any other security measures, so better only use it on localhost.
Usage:
Usage:
anydb serve [-l host:port] [flags]
Flags:
-h, --help help for serve
-l, --listen string host:port (default "localhost:8787")
To start the listener, just execute the B<serve> subcommand. You can
tweak the ip address and tcp port using the C<-l> option. The listener
will not fork and run in the foreground. Logs are being printed to
STDOUT as long as the listener runs.
For more details about the API, please see the L<REST API> section.
=head2 info
The B<info> subcommand shows you some information about your current
database.
Usage:
Usage:
anydb info [flags]
Flags:
-h, --help help for info
-N, --no-human do not translate to human readable values
Data being shown are: filename and size, number of keys per bucket. If
you supply the C<-d> option (debug), some bbolt internals are being
displayed as well.
=head2 man
The B<man> subcommand shows an unformatted text variant of the manual
page (which are currently reading).
Usage:
Usage:
anydb man [flags]
Flags:
-h, --help help for man
The manual is being piped into the C<more> command, which is being
expected to exist according to the POSIX standard on all supported
unix platforms. It might not work on Windows.
=head1 TEMPLATES
The B<get> and B<list> commands support a template feature, which is
very handy to create you own kind of formatting. The template syntax
being used is the GO template language, refer to
L<https://pkg.go.dev/text/template> for details.
Each template operates on one or more entries, no loop construct is
required, the template provided applies to every matching entry
separatley.
The following template variables can be used:
=over
=item B<.Key> - string
=item B<.Value> - string
=item B<.Bin> - []byte
=item B<.Created> - timestamp.Time
To retrieve a string representation of the timestamp, use C<.Created.AsTime>.
If you need a unix timestamp since epoch, use C<.Created.Unix>.
=item B<.Tags> - []string
=item B<.Encrypted> bool
=back
Prepend a single dot (".") before each variable name.
Here are some examples how to use the feature:
Only show the keys of all entries:
anydb list -m template -T "{{ .Key }}"
Format the list in a way so that is possible to evaluate it in a
shell:
eval $(anydb get foo -m template -T "key='{{ .Key }}' value='{{ .Value }}' ts='{{ .Created.AsTime}}'")
echo "Key: $key, Value: $value, When: $ts"
Print the values in CSV format ONLY if they have some tag:
anydb list -m template -T "{{ if .Tags }}{{ .Key }},{{ .Value }},{{ .Created.AsTime}}{{ end }}"
=head1 CONFIGURATION
Anydb looks at the following locations for a configuration file, in
that order:
=over
=item C<$HOME/.config/anydb/anydb.toml>
=item C<$HOME/.anydb.toml>
=item C<anydb.toml> in the current directory
=item or specify one using C<-c>
=back
The configuration format uses the TOML language, refer to
L<https://toml.io/en/> for more details. The key names correspond to
the commandline options in most cases.
Configuration follows a certain precedence: the files are tried to be
read in the given order, followed by commandline options. That is, the
last configuration file wins, unless the user provides a commandline
option, then this setting will be taken.
A complete configuration file might look like this:
# defaults
dbfile = "~/.config/anydb/default.db"
dbbucket = "data"
noheaders = false
nohumanize = false
encrypt = false
listen = "localhost:8787"
# different setups for different buckets
[buckets.data]
encrypt = true
[buckets.test]
encrypt = false
Under normal circumstances you don't need a configuration
file. However, if you want to use different buckets, then this might
be a handy option. Buckets are being configured in ini-style with the
term "bucket." followed by the bucket name. In the example above we
enable encryption for the default bucket "data" and disable it for a
bucket "test". To use different buckets, use the C<-b> option.
=head1 REST API
The subcommand B<serve> starts a simple HTTP service, which responds
to RESTful HTTP requests. The listener responds to all requests with a
JSON encoded response. The response contains the status and the
content - if any - of the requested resource.
The following requests are supported:
=over
=item B<GET /anydb/v1/>
Returns a JSON encoded list of all entries.
=item B<GET /anydb/v1/key>
Returns the JSON encoded entry, if found.
=item B<PUT /anydb/v1/>
Create an entry. Expects a JSON encoded request object in POST data.
=item B<DELETE /anydb/v1/key>
Delete an entry.
=back
Some curl example calls to the API:
Post a new key:
curl -X PUT localhost:8787/anydb/v1/ \
-H 'Content-Type: application/json' \
-d '{"key":"foo","val":"bar"}'
Retrieve the value:
curl localhost:8787/anydb/v1/foo
List all keys:
curl localhost:8787/anydb/v1/
=head1 BUGS
In order to report a bug, unexpected behavior, feature requests
or to submit a patch, please open an issue on github:
L<https://github.com/TLINDEN/anydb/issues>.
Please repeat the failing command with debugging enabled C<-d> and
include the output in the issue.
=head1 LIMITATIONS
The REST API list request doesn't provide any filtering capabilities yet.
=head1 LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version 3.
Copyright (c) 2024 by Thomas von Dein
=head1 AUTHORS
Thomas von Dein B<tom AT vondein DOT org>
=cut

View File

@@ -1,138 +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 app
import (
"fmt"
"io"
"os"
"strings"
"unicode/utf8"
)
type DbAttr struct {
Key string
Preview string
Val []byte
Args []string
Tags []string
File string
Encrypted bool
Binary bool
// conf flags, needed for incoming rest requests
Fulltext bool
}
// check if value is to be read from a file or stdin, setup preview
// text according to flags, lowercase key
func (attr *DbAttr) ParseKV() error {
attr.Key = strings.ToLower(attr.Args[0])
switch len(attr.Args) {
case 1:
// 1 arg = key + read from file or stdin
if attr.File == "" {
attr.File = "-"
}
case 2:
attr.Val = []byte(attr.Args[1])
if attr.Args[1] == "-" {
attr.File = "-"
}
}
if attr.File != "" {
if err := attr.GetFileValue(); err != nil {
return err
}
}
switch {
case attr.Binary:
attr.Preview = "<binary-content>"
case attr.Encrypted:
attr.Preview = "<encrypted-content>"
default:
if len(attr.Val) > MaxValueWidth {
attr.Preview = string(attr.Val)[0:MaxValueWidth] + "..."
if strings.Contains(attr.Preview, "\n") {
parts := strings.Split(attr.Preview, "\n")
if len(parts) > 0 {
attr.Preview = parts[0]
}
}
} else {
attr.Preview = string(attr.Val)
}
}
return nil
}
func (attr *DbAttr) GetFileValue() error {
var fd io.Reader
if attr.File == "-" {
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
fd = os.Stdin
}
} else {
filehandle, err := os.OpenFile(attr.File, os.O_RDONLY, 0600)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", attr.File, err)
}
fd = filehandle
}
if fd != nil {
// read from file or stdin pipe
data, err := io.ReadAll(fd)
if err != nil {
return fmt.Errorf("failed to read from pipe: %w", err)
}
// poor man's text file test
attr.Val = data
if utf8.ValidString(string(data)) {
attr.Binary = false
} else {
attr.Binary = true
}
} else {
// read from console stdin
var input string
var data string
for {
_, err := fmt.Scanln(&input)
if err != nil {
break
}
data += input + "\n"
}
attr.Val = []byte(data)
}
return nil
}

View File

@@ -1,160 +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 app
import (
"crypto/rand"
"errors"
"fmt"
"os"
"syscall"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/term"
)
const (
ArgonMem uint32 = 64 * 1024
ArgonIter uint32 = 5
ArgonParallel uint8 = 2
ArgonSaltLen int = 16
ArgonKeyLen uint32 = 32
B64SaltLen int = 22
)
type Key struct {
Salt []byte
Key []byte
}
// called from interactive thread, hides input and returns clear text
// password
func AskForPassword() ([]byte, error) {
fmt.Fprint(os.Stderr, "Password: ")
pass, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return nil, fmt.Errorf("failed to read password: %w", err)
}
fmt.Fprintln(os.Stderr)
return pass, nil
}
// We're using the Argon2id key derivation algorithm to derive a
// secure key from the given password. This is important, because
// users might use unsecure passwords. The resulting encrypted data
// might of course easily be decrypted using brute force methods if a
// weak password was used, but that would cost, because of the key
// derivation. It does several rounds of hash calculations which take
// a considerable amount of cpu time. For our legal user that's no
// problem because it's being executed only once, but an attacker has
// to do it in a forever loop, which will take a lot of time.
func DeriveKey(password []byte, salt []byte) (*Key, error) {
if salt == nil {
// none given, new password
newsalt, err := GetRandom(ArgonSaltLen, ArgonSaltLen)
if err != nil {
return nil, err
}
salt = newsalt
}
hash := argon2.IDKey(
[]byte(password), salt,
ArgonIter,
ArgonMem,
ArgonParallel,
ArgonKeyLen,
)
return &Key{Key: hash, Salt: salt}, nil
}
// Retrieve a random chunk of given size
func GetRandom(size int, capacity int) ([]byte, error) {
buf := make([]byte, size, capacity)
_, err := rand.Read(buf)
if err != nil {
return nil, fmt.Errorf("failed to retrieve random bytes: %w", err)
}
return buf, nil
}
// Encrypt clear text given in attr using ChaCha20 and auhtenticate
// using the mac Poly1305. The cipher text will be put into attr, thus
// modifying it.
//
// The cipher text consists of:
// password-salt) + (12 byte nonce + ciphertext + 16 byte mac)
func Encrypt(pass []byte, attr *DbAttr) error {
key, err := DeriveKey(pass, nil)
if err != nil {
return err
}
aead, err := chacha20poly1305.New(key.Key)
if err != nil {
return fmt.Errorf("failed to create AEAD cipher: %w", err)
}
total := aead.NonceSize() + len(attr.Val) + aead.Overhead()
nonce, err := GetRandom(aead.NonceSize(), total)
if err != nil {
return err
}
cipher := aead.Seal(nonce, nonce, attr.Val, nil)
attr.Val = append(attr.Val, key.Salt...)
attr.Val = append(attr.Val, cipher...)
attr.Encrypted = true
return nil
}
// Do the reverse
func Decrypt(pass []byte, cipherb []byte) ([]byte, error) {
if len(cipherb) < B64SaltLen {
return nil, fmt.Errorf("encrypted cipher block too small")
}
key, err := DeriveKey(pass, cipherb[0:B64SaltLen])
if err != nil {
return nil, err
}
cipher := cipherb[B64SaltLen:]
aead, err := chacha20poly1305.New(key.Key)
if err != nil {
return nil, fmt.Errorf("failed to create AEAD cipher: %w", err)
}
if len(cipher) < aead.NonceSize() {
return nil, errors.New("ciphertext too short")
}
nonce, ciphertext := cipher[:aead.NonceSize()], cipher[aead.NonceSize():]
return aead.Open(nil, nonce, ciphertext, nil)
}

555
app/db.go
View File

@@ -1,555 +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 app
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
)
const MaxValueWidth int = 60
type DB struct {
Debug bool
Dbfile string
Bucket string
DB *bolt.DB
}
type BucketInfo struct {
Name string
Keys int
Size int
Sequence uint64
Stats bolt.BucketStats
}
type DbInfo struct {
Buckets []BucketInfo
Path string
}
type DbEntries []*DbEntry
type DbTag struct {
Keys []string `json:"key"`
}
const BucketData string = "data"
func GetDbFile(file string) string {
if file != "" {
return file
}
file = os.Getenv("ANYDB_DB")
if file != "" {
return file
}
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
return filepath.Join(home, ".config", "anydb", "default.db")
}
func New(file string, bucket string, debug bool) (*DB, error) {
return &DB{Debug: debug, Dbfile: file, Bucket: bucket}, nil
}
func (db *DB) Open() error {
if _, err := os.Stat(filepath.Dir(db.Dbfile)); os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Dir(db.Dbfile), 0700); err != nil {
return err
}
}
b, err := bolt.Open(db.Dbfile, 0600, nil)
if err != nil {
return fmt.Errorf("failed to open DB %s: %w", db.Dbfile, err)
}
db.DB = b
return nil
}
func (db *DB) Close() error {
return db.DB.Close()
}
func (db *DB) List(attr *DbAttr, fulltext bool) (DbEntries, error) {
if err := db.Open(); err != nil {
return nil, err
}
defer db.Close()
var entries DbEntries
var filter *regexp.Regexp
if len(attr.Args) > 0 {
// via cli
filter = regexp.MustCompile(attr.Args[0])
}
if len(attr.Key) > 0 {
// via api
filter = regexp.MustCompile(attr.Key)
}
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
}
databucket := root.Bucket([]byte("data"))
if databucket == nil {
return fmt.Errorf("failed to retrieve data sub bucket")
}
err := bucket.ForEach(func(key, pbentry []byte) error {
var entry DbEntry
if err := proto.Unmarshal(pbentry, &entry); err != nil {
return fmt.Errorf("failed to unmarshal from protobuf: %w", err)
}
if fulltext {
// avoid crash due to access fault
value := databucket.Get([]byte(entry.Key)) // empty is ok
vc := make([]byte, len(value))
copy(vc, value)
entry.Value = string(vc)
}
var include bool
switch {
case filter != nil:
if filter.MatchString(entry.Key) ||
filter.MatchString(strings.Join(entry.Tags, " ")) {
include = true
}
if !entry.Binary && !include && fulltext {
if filter.MatchString(string(entry.Value)) {
include = true
}
}
case len(attr.Tags) > 0:
for _, search := range attr.Tags {
for _, tag := range entry.Tags {
if tag == search {
include = true
break
}
}
if include {
break
}
}
default:
include = true
}
if include {
entries = append(entries, &entry)
}
return nil
})
return err
})
return entries, err
}
func (db *DB) Set(attr *DbAttr) error {
if err := db.Open(); err != nil {
return err
}
defer db.Close()
entry := DbEntry{
Key: attr.Key,
Binary: attr.Binary,
Tags: attr.Tags,
Encrypted: attr.Encrypted,
Created: timestamppb.Now(),
Size: uint64(len(attr.Val)),
Preview: attr.Preview,
}
// check if the entry already exists and if yes, check if it has
// any tags. if so, we initialize our update struct with these
// tags unless it has new tags configured.
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
}
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
})
if err != nil {
return err
}
// marshall our data
pbentry, err := proto.Marshal(&entry)
if err != nil {
return fmt.Errorf("failed to marshall protobuf: %w", err)
}
err = db.DB.Update(func(tx *bolt.Tx) error {
// create root bucket
root, err := tx.CreateBucketIfNotExists([]byte(db.Bucket))
if err != nil {
return fmt.Errorf("failed to create DB bucket: %w", err)
}
// create meta bucket
bucket, err := root.CreateBucketIfNotExists([]byte("meta"))
if err != nil {
return fmt.Errorf("failed to create DB meta sub bucket: %w", err)
}
// write meta data
err = bucket.Put([]byte(entry.Key), []byte(pbentry))
if err != nil {
return fmt.Errorf("failed to insert data: %w", err)
}
// create data bucket
databucket, err := root.CreateBucketIfNotExists([]byte("data"))
if err != nil {
return fmt.Errorf("failed to create DB data sub bucket: %w", err)
}
// write value
err = databucket.Put([]byte(entry.Key), attr.Val)
if err != nil {
return fmt.Errorf("failed to insert data: %w", err)
}
return nil
})
if err != nil {
return err
}
return nil
}
func (db *DB) Get(attr *DbAttr) (*DbEntry, error) {
if err := db.Open(); err != nil {
return nil, err
}
defer db.Close()
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
}
// 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 {
return nil, fmt.Errorf("failed to read from DB: %w", err)
}
return &entry, nil
}
func (db *DB) Del(attr *DbAttr) error {
if err := db.Open(); err != nil {
return err
}
defer db.Close()
err := db.DB.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(db.Bucket))
if bucket == nil {
return nil
}
return bucket.Delete([]byte(attr.Key))
})
return err
}
func (db *DB) Import(attr *DbAttr) (string, error) {
// open json file into attr.Val
if err := attr.GetFileValue(); err != nil {
return "", err
}
if len(attr.Val) == 0 {
return "", errors.New("empty json file")
}
var entries DbEntries
now := time.Now()
newfile := db.Dbfile + now.Format("-02.01.2006T03:04.05")
if err := json.Unmarshal([]byte(attr.Val), &entries); err != nil {
return "", cleanError(newfile, fmt.Errorf("failed to unmarshal json: %w", err))
}
if fileExists(db.Dbfile) {
// backup the old file
err := os.Rename(db.Dbfile, newfile)
if err != nil {
return "", fmt.Errorf("failed to rename file %s to %s: %w", db.Dbfile, newfile, err)
}
}
// should now be a new db file
if err := db.Open(); err != nil {
return "", cleanError(newfile, err)
}
defer db.Close()
err := db.DB.Update(func(tx *bolt.Tx) error {
// create root bucket
root, err := tx.CreateBucketIfNotExists([]byte(db.Bucket))
if err != nil {
return fmt.Errorf("failed to create DB bucket: %w", err)
}
// create meta bucket
bucket, err := root.CreateBucketIfNotExists([]byte("meta"))
if err != nil {
return fmt.Errorf("failed to create DB meta sub bucket: %w", err)
}
for _, entry := range entries {
pbentry, err := proto.Marshal(entry)
if err != nil {
return fmt.Errorf("failed to marshall protobuf: %w", err)
}
// write meta data
err = bucket.Put([]byte(entry.Key), []byte(pbentry))
if err != nil {
return fmt.Errorf("failed to insert data into DB: %w", err)
}
// create data bucket
databucket, err := root.CreateBucketIfNotExists([]byte("data"))
if err != nil {
return fmt.Errorf("failed to create DB data sub bucket: %w", err)
}
// write value
err = databucket.Put([]byte(entry.Key), []byte(entry.Value))
if err != nil {
return fmt.Errorf("failed to insert data: %w", err)
}
}
return nil
})
if err != nil {
return "", cleanError(newfile, err)
}
return fmt.Sprintf("backed up database file to %s\nimported %d database entries\n",
newfile, len(entries)), nil
}
func (db *DB) Info() (*DbInfo, error) {
if err := db.Open(); err != nil {
return nil, err
}
defer db.Close()
info := &DbInfo{Path: db.Dbfile}
err := db.DB.View(func(tx *bolt.Tx) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
stats := bucket.Stats()
binfo := BucketInfo{
Name: string(name),
Sequence: bucket.Sequence(),
Keys: stats.KeyN,
Stats: bucket.Stats(),
}
err := bucket.ForEach(func(key, entry []byte) error {
binfo.Size += len(entry) + len(key)
return nil
})
if err != nil {
return fmt.Errorf("failed to read keys: %w", err)
}
info.Buckets = append(info.Buckets, binfo)
return nil
})
if err != nil {
return fmt.Errorf("failed to read from DB: %w", err)
}
return nil
})
return info, err
}
func (db *DB) Getall(attr *DbAttr) (DbEntries, error) {
if err := db.Open(); err != nil {
return nil, err
}
defer db.Close()
var entries DbEntries
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
}
// get data sub bucket
databucket := root.Bucket([]byte("data"))
if databucket == nil {
return fmt.Errorf("failed to retrieve data sub bucket")
}
// iterate over all db entries in meta sub bucket
err := bucket.ForEach(func(key, pbentry []byte) error {
var entry DbEntry
if err := proto.Unmarshal(pbentry, &entry); err != nil {
return fmt.Errorf("failed to unmarshal from protobuf: %w", err)
}
// retrieve the value from the data sub bucket
value := databucket.Get([]byte(entry.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)
entries = append(entries, &entry)
return nil
})
return err
})
return entries, err
}

View File

@@ -1,210 +0,0 @@
// -*-c++-*-
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.1
// protoc v3.21.12
// source: app/dbentry.proto
package app
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type DbEntry struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=Id,proto3" json:"Id,omitempty"`
Key string `protobuf:"bytes,2,opt,name=Key,proto3" json:"Key,omitempty"`
Preview string `protobuf:"bytes,3,opt,name=Preview,proto3" json:"Preview,omitempty"`
Tags []string `protobuf:"bytes,4,rep,name=Tags,proto3" json:"Tags,omitempty"`
Created *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=Created,proto3" json:"Created,omitempty"`
Size uint64 `protobuf:"varint,6,opt,name=Size,proto3" json:"Size,omitempty"`
Encrypted bool `protobuf:"varint,7,opt,name=Encrypted,proto3" json:"Encrypted,omitempty"`
Binary bool `protobuf:"varint,8,opt,name=Binary,proto3" json:"Binary,omitempty"`
Value string `protobuf:"bytes,9,opt,name=Value,proto3" json:"Value,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DbEntry) Reset() {
*x = DbEntry{}
mi := &file_app_dbentry_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DbEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DbEntry) ProtoMessage() {}
func (x *DbEntry) ProtoReflect() protoreflect.Message {
mi := &file_app_dbentry_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DbEntry.ProtoReflect.Descriptor instead.
func (*DbEntry) Descriptor() ([]byte, []int) {
return file_app_dbentry_proto_rawDescGZIP(), []int{0}
}
func (x *DbEntry) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *DbEntry) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *DbEntry) GetPreview() string {
if x != nil {
return x.Preview
}
return ""
}
func (x *DbEntry) GetTags() []string {
if x != nil {
return x.Tags
}
return nil
}
func (x *DbEntry) GetCreated() *timestamppb.Timestamp {
if x != nil {
return x.Created
}
return nil
}
func (x *DbEntry) GetSize() uint64 {
if x != nil {
return x.Size
}
return 0
}
func (x *DbEntry) GetEncrypted() bool {
if x != nil {
return x.Encrypted
}
return false
}
func (x *DbEntry) GetBinary() bool {
if x != nil {
return x.Binary
}
return false
}
func (x *DbEntry) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
var File_app_dbentry_proto protoreflect.FileDescriptor
var file_app_dbentry_proto_rawDesc = []byte{
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,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xef, 0x01, 0x0a, 0x07, 0x44, 0x62,
0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x02, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x65, 0x76, 0x69,
0x65, 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65,
0x77, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52,
0x04, 0x54, 0x61, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x07, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
0x6d, 0x70, 0x52, 0x07, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x53,
0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x12,
0x1c, 0x0a, 0x09, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01,
0x28, 0x08, 0x52, 0x09, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x12, 0x16, 0x0a,
0x06, 0x42, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x42,
0x69, 0x6e, 0x61, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x09,
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x1e, 0x5a, 0x1c, 0x67,
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,
0x74, 0x6f, 0x33,
}
var (
file_app_dbentry_proto_rawDescOnce sync.Once
file_app_dbentry_proto_rawDescData = file_app_dbentry_proto_rawDesc
)
func file_app_dbentry_proto_rawDescGZIP() []byte {
file_app_dbentry_proto_rawDescOnce.Do(func() {
file_app_dbentry_proto_rawDescData = protoimpl.X.CompressGZIP(file_app_dbentry_proto_rawDescData)
})
return file_app_dbentry_proto_rawDescData
}
var file_app_dbentry_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_app_dbentry_proto_goTypes = []any{
(*DbEntry)(nil), // 0: app.DbEntry
(*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp
}
var file_app_dbentry_proto_depIdxs = []int32{
1, // 0: app.DbEntry.Created:type_name -> google.protobuf.Timestamp
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_app_dbentry_proto_init() }
func file_app_dbentry_proto_init() {
if File_app_dbentry_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_app_dbentry_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_app_dbentry_proto_goTypes,
DependencyIndexes: file_app_dbentry_proto_depIdxs,
MessageInfos: file_app_dbentry_proto_msgTypes,
}.Build()
File_app_dbentry_proto = out.File
file_app_dbentry_proto_rawDesc = nil
file_app_dbentry_proto_goTypes = nil
file_app_dbentry_proto_depIdxs = nil
}

View File

@@ -1,20 +0,0 @@
// -*-c++-*-
syntax = "proto3";
package app;
import "google/protobuf/timestamp.proto";
option go_package = "github.com/tlinden/anydb/app";
message DbEntry {
string Id = 1;
string Key = 2;
string Preview = 3;
repeated string Tags = 4;
google.protobuf.Timestamp Created = 5;
uint64 Size = 6;
bool Encrypted = 7;
bool Binary = 8;
string Value = 9;
}

View File

@@ -1,26 +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 app
// look if a key in a map exists, generic variant
func Exists[K comparable, V any](m map[K]V, v K) bool {
if _, ok := m[v]; ok {
return true
}
return false
}

View File

@@ -1,36 +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 app
import "os"
func cleanError(file string, err error) error {
// remove given [backup] file and forward the given error
os.Remove(file)
return err
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if err != nil {
// return false on any error
return false
}
return !info.IsDir()
}

View File

@@ -1,116 +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 cfg
import (
"fmt"
"io"
"os"
"github.com/pelletier/go-toml"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/common"
)
var Version string = "v0.1.4"
type BucketConfig struct {
Encrypt bool
}
type Config struct {
Debug bool
Dbfile string
Dbbucket string
Template string
Mode string // wide, table, yaml, json
NoHeaders bool
NoHumanize bool
Encrypt bool // one entry
CaseInsensitive bool
Fulltext bool
Listen string
Buckets map[string]BucketConfig // config file only
Tags []string // internal
DB *app.DB // internal
File string // internal
}
func (conf *Config) GetConfig(files []string) error {
for _, file := range files {
if err := conf.ParseConfigFile(file); err != nil {
return err
}
}
return nil
}
func (conf *Config) ParseConfigFile(file string) error {
if !common.FileExists(file) {
return nil
}
fd, err := os.OpenFile(file, os.O_RDONLY, 0600)
if err != nil {
return fmt.Errorf("failed to open config file %s: %w", file, err)
}
data, err := io.ReadAll(fd)
if err != nil {
return fmt.Errorf("failed to read from config file: %w", err)
}
add := Config{}
err = toml.Unmarshal(data, &add)
if err != nil {
return fmt.Errorf("failed to unmarshall toml: %w", err)
}
// merge new values into existing config
switch {
case add.Debug != conf.Debug:
conf.Debug = add.Debug
case add.Dbfile != "":
conf.Dbfile = add.Dbfile
case add.Dbbucket != "":
conf.Dbbucket = add.Dbbucket
case add.Template != "":
conf.Template = add.Template
case add.NoHeaders != conf.NoHeaders:
conf.NoHeaders = add.NoHeaders
case add.NoHumanize != conf.NoHumanize:
conf.NoHumanize = add.NoHumanize
case add.Encrypt != conf.Encrypt:
conf.Encrypt = add.Encrypt
case add.Listen != "":
conf.Listen = add.Listen
}
// only supported in config files
conf.Buckets = add.Buckets
// determine bucket encryption mode
for name, bucket := range conf.Buckets {
if name == conf.Dbbucket {
conf.Encrypt = bucket.Encrypt
}
}
return nil
}

View File

@@ -1,534 +0,0 @@
package cmd
var manpage = `
anydb
anydb - a personal key value store
SYNOPSIS
Usage:
anydb <command> [options] [flags]
anydb [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
del Delete key
edit Edit a key
export Export database to json
get Retrieve value for a key
help Help about any command
import Import database dump
info info
list List database contents
man show manual page
serve run REST API listener
set Insert key/value pair
Flags:
-b, --bucket string use other bucket (default: data) (default "data")
-c, --config string toml config file
-f, --dbfile string DB file to use (default "/home/scip/.config/anydb/default.db")
-d, --debug Enable debugging
-h, --help help for anydb
-v, --version Print program version
Use "anydb [command] --help" for more information about a command.
DESCRIPTION
Anydb is a commandline personal key value store, it is simple to use and
can be used to store anything you'd like, even binary files etc. It uses
a key/value store (bbolt) in your home directory.
The tool provides a number of subcommands to use it, there are global
options and each subcommand has its own set of options.
GLOBAL OPTIONS
"-f, --dbfile filename"
The default location of your databas is
"$HOME/.config/anydb/default.db". You can change this with the "-f"
option.
"-b, --bucket name"
Data in a bbolt key-value-store are managed in so called buckets.
These are kind of namespaces, where each key must be unique.
However, a database may contain more than one bucket.
By default anydb uses a bucket named "data", but you can change this
using the option "-b".
Buckets can be configured to always encrypt values, see ENCRYTPTION.
"-c, --config filename"
Under normal circumstances you don't need a configuration file. But
if you want, you can provide one using the option "-c".
Anydb looks for a couple of default locations for a config file. You
only need this option if you want to supply a configuration on a
non-standard location. See CONFIGURATION for more details.
"-d, --debug"
Enable debug output.
"-h, --help"
Show the usage of anydb.
"-v, --version"
Show the program version.
All of these options can be used with subcommands as well.
SUBCOMMANDS
completion
The completion command can be used to setup completion for anydb. Just
put something like this into your shell's configuration file:
source <(anydb completion bash)
If you use another shell, specify it instead of bash, of course.
set
The set command is being used to insert or update a key-value pair.
Usage:
Usage:
anydb set <key> [<value> | -r <file>] [-t <tag>] [flags]
Aliases:
set, add, s, +
Flags:
-e, --encrypt encrypt value
-r, --file string Filename or - for STDIN
-h, --help help for set
-t, --tags tag,tag,... tags, multiple allowed
The standard way to insert a new entry is really simple:
anydb set key value
If you don't specify a value, anydb expects you to feed it some data via
STDIN. For example:
anydb set key < file
You might as well specify a file directly using the "-f" option:
anydb set key -f file
Values can be encrypted using ChaCha20Poly1305 when you specify the "-e"
option. Anydb will ask you interactively for a passphrase. You can also
provide the passphrase using the environment variable "ANYDB_PASSWORD".
To encrypt the value, a cryptographically secure key will be derived
from the passphrase using the ArgonID2 algorithm. Each value can be
encrypted with another passphrase. So, the database itself is not
encrypted, just the values.
You can supply tags by using the option "-t". Multiple tags can be
provided either by separating them with a comma or by using multiple
"-t" parameters:
anydb set key value -t tag1,tag2
anydb set key value -t tag1 -t tag2
You can later filter entries by tag or by a combination of tags.
To edit or modify an entry, just use the set command with the same key,
the value in the database will be overwritten with the new value. An
alternative option is the edit command, see below.
get
To retrieve the value of a key, use the get subcommand.
Usage:
Usage:
anydb get <key> [-o <file>] [-m <mode>] [-n -N] [-T <tpl>] [flags]
Aliases:
get, show, g, .
Flags:
-h, --help help for get
-m, --mode string output format (simple|wide|json|template) (default 'simple')
-n, --no-headers omit headers in tables
-N, --no-human do not translate to human readable values
-o, --output string output value to file (ignores -m)
-T, --template string go template for '-m template'
In its simplest form you just call the get subcommand with the key you
want to have the value for. The value is being printed to STDOUT by
default:
anydb get key
If the value is binary content, it will not just being printed. In those
cases you need to either redirect output into a file or use the option
"-o" to write to a file:
anydb get key > file
anydb get key -o file
If the value is encrypted, you will be asked for the passphrase to
decrypt it. If the environment variable "ANYDB_PASSWORD" is set, its
value will be used instead.
There are different output modes you can choose from: simple, wide and
json. The "simple" mode is the default one, it just prints the value as
is. The "wide" mode prints a tabular output similar to the list
subcommand, see there for more details. The options "-n" and "-N" have
the same meaning as in the list command. The "json" mode prints the raw
JSON representation of the whole database entry. Decryption will only
take place in "simple" and "json" mode. The "template" mode provides the
most flexibily, it is detailed in the section TEMPLATES.
list
The list subcommand displays a list of all database entries.
Usage:
Usage:
anydb list [<filter-regex> | -t <tag> ] [-m <mode>] [-nNif] [-T <tpl>] [flags]
Aliases:
list, ls, /, find, search
Flags:
-i, --case-insensitive filter case insensitive
-h, --help help for list
-m, --mode string output format (table|wide|json|template), wide is a verbose table. (default 'table')
-n, --no-headers omit headers in tables
-N, --no-human do not translate to human readable values
-s, --search-fulltext perform a full text search
-t, --tags stringArray tags, multiple allowed
-T, --template string go template for '-m template'
-l, --wide-output output mode: wide
In its simplest form - without any options - , the list command just
prints all keys with their values to STDOUT. Values are being truncated
to maximum of 60 characters, that is, multiline values are not
completely shown in order to keep the tabular view readable.
To get more informations about each entry, use the "-o wide" or "-l"
option. In addition to the key and value also the size, update timestamp
and tags will be printed. Time and size values are converted into a
human readable form, you can suppress this behavior with the "-N"
option. You may omit the headers using the option "-n"
Sometimes you might want to filter the list of entries. Either because
your database grew too large or because you're searching for something.
In that case you have two options: You may supply one or more tags or
provide a filter regexp. To filter by tag, do:
anydb list -t tag1
anydb list -t tag1,tag2
anydb list -t tag1 -t tag2
To filter using a regular expression, do:
anydb list "foo.*bar"
Regular expressions follow the golang re2 syntax. For more details about
the syntax, refer to <https://github.com/google/re2/wiki/Syntax>. Please
note, that this regexp dialect is not PCRE compatible, but supports most
of its features.
If you want to search case insensitive, add the option "-i".
By default anydb only searches through the keys. If you want to search
through the values as well, then use the "-s" option, which enables
full-text search.
You can - as with the get command - use other output modes. The default
mode is "table". The "wide" mode is, as already mentioned, a more
detailed table. Also supported is "json" mode and "template" mode. For
details about using templates see TEMPLATES.
del
Use the del command to delete database entries.
Usage:
Usage:
anydb del <key> [flags]
Aliases:
del, d, rm
Flags:
-h, --help help for del
The subcommand del does not provide any further options, it just deletes
the entry referred to by the given key. No questions are being asked.
edit
The edit command makes it easier to modify larger entries.
Usage:
Usage:
anydb edit <key> [flags]
Aliases:
edit, modify, mod, ed, vi
Flags:
-h, --help help for edit
The subcommand edit does not provide any further options. It works like
this:
1. Write the value info a temporary file.
2. Execute the editor (which one, see below!) with that file.
3. Now you can edit the file and save+close it when done.
4. Anydb picks up the file and if the content has changed, puts its
value into the DB.
By default anydb executes the "vi" command. You can modify this behavior
by setting the environment variable "EDITOR" appropriately.
Please note, that this does not work with binary content!
export
Since the bbolt database file is not portable across platforms (it is
bound to the endianess of the CPU it was being created on), you might
want to create a backup file of your database. You can do this with the
export subcommand.
Usage:
Usage:
anydb export -o <json filename> [flags]
Aliases:
export, dump, backup
Flags:
-h, --help help for export
-o, --output string output to file
The database dump is a JSON representation of the whole database and
will be printed to the file specified with the "-o" option. If you
specify "-" as the filename, it will be written to STDIN.
anydb export -o dump.json
anydb export -o - > dump.json
Please note, that encrypted values will not be decrypted. This might
change in a future version of anydb.
import
The import subcommand can be used to restore a database from a JSON
dump.
Usage:
Usage:
anydb import -i <json file> [flags]
Aliases:
import, restore
Flags:
-r, --file string Filename or - for STDIN
-h, --help help for import
-t, --tags stringArray tags, multiple allowed
The "import" subcommand reads the JSON contents from the file specified
with the "-i" option. If you specify "-" as the filename, it will be
read from STDIN.
anydb import -i - < dump.json
anydb import -i dump.json
cat dump.json | anydb import -i -
If there is already a database, it will be saved by appending a
timestamp and a new database with the contents of the dump will be
created.
serve
Anydb provides a RESTful API, which you can use to manage the database
from somewhere else. The API does not provide any authentication or any
other security measures, so better only use it on localhost.
Usage:
Usage:
anydb serve [-l host:port] [flags]
Flags:
-h, --help help for serve
-l, --listen string host:port (default "localhost:8787")
To start the listener, just execute the serve subcommand. You can tweak
the ip address and tcp port using the "-l" option. The listener will not
fork and run in the foreground. Logs are being printed to STDOUT as long
as the listener runs.
For more details about the API, please see the "REST API" section.
info
The info subcommand shows you some information about your current
database.
Usage:
Usage:
anydb info [flags]
Flags:
-h, --help help for info
-N, --no-human do not translate to human readable values
Data being shown are: filename and size, number of keys per bucket. If
you supply the "-d" option (debug), some bbolt internals are being
displayed as well.
man
The man subcommand shows an unformatted text variant of the manual page
(which are currently reading).
Usage:
Usage:
anydb man [flags]
Flags:
-h, --help help for man
The manual is being piped into the "more" command, which is being
expected to exist according to the POSIX standard on all supported unix
platforms. It might not work on Windows.
TEMPLATES
The get and list commands support a template feature, which is very
handy to create you own kind of formatting. The template syntax being
used is the GO template language, refer to
<https://pkg.go.dev/text/template> for details.
Each template operates on one or more entries, no loop construct is
required, the template provided applies to every matching entry
separatley.
The following template variables can be used:
Key - string
Value - string
Bin - []byte
Created - time.Time
Tags - []string
Encrypted bool
Prepend a single dot (".") before each variable name.
Here are some examples how to use the feature:
Only show the keys of all entries:
anydb list -m template -T "{{ .Key }}"
Format the list in a way so that is possible to evaluate it in a shell:
eval $(anydb get foo -m template -T "key='{{ .Key }}' value='{{ .Value }}' ts='{{ .Created}}'")
echo "Key: $key, Value: $value"
Print the values in CSV format ONLY if they have some tag:
anydb list -m template -T "{{ if .Tags }}{{ .Key }},{{ .Value }},{{ .Created}}{{ end }}"
CONFIGURATION
Anydb looks at the following locations for a configuration file, in that
order:
"$HOME/.config/anydb/anydb.toml"
"$HOME/.anydb.toml"
"anydb.toml" in the current directory
or specify one using "-c"
The configuration format uses the TOML language, refer to
<https://toml.io/en/> for more details. The key names correspond to the
commandline options in most cases.
Configuration follows a certain precedence: the files are tried to be
read in the given order, followed by commandline options. That is, the
last configuration file wins, unless the user provides a commandline
option, then this setting will be taken.
A complete configuration file might look like this:
# defaults
dbfile = "~/.config/anydb/default.db"
dbbucket = "data"
noheaders = false
nohumanize = false
encrypt = false
listen = "localhost:8787"
# different setups for different buckets
[buckets.data]
encrypt = true
[buckets.test]
encrypt = false
Under normal circumstances you don't need a configuration file. However,
if you want to use different buckets, then this might be a handy option.
Buckets are being configured in ini-style with the term "bucket."
followed by the bucket name. In the example above we enable encryption
for the default bucket "data" and disable it for a bucket "test". To use
different buckets, use the "-b" option.
REST API
The subcommand serve starts a simple HTTP service, which responds to
RESTful HTTP requests. The listener responds to all requests with a JSON
encoded response. The response contains the status and the content - if
any - of the requested resource.
The following requests are supported:
GET /anydb/v1/
Returns a JSON encoded list of all entries.
GET /anydb/v1/key
Returns the JSON encoded entry, if found.
PUT /anydb/v1/
Create an entry. Expects a JSON encoded request object in POST data.
DELETE /anydb/v1/key
Delete an entry.
Some curl example calls to the API:
Post a new key: curl -X PUT localhost:8787/anydb/v1/ \ -H 'Content-Type:
application/json' \ -d '{"key":"foo","val":"bar"}'
Retrieve the value:
curl localhost:8787/anydb/v1/foo
List all keys:
curl localhost:8787/anydb/v1/
BUGS
In order to report a bug, unexpected behavior, feature requests or to
submit a patch, please open an issue on github:
<https://github.com/TLINDEN/anydb/issues>.
Please repeat the failing command with debugging enabled "-d" and
include the output in the issue.
LIMITATIONS
The REST API list request doesn't provide any filtering capabilities
yet.
LICENSE
This software is licensed under the GNU GENERAL PUBLIC LICENSE version
3.
Copyright (c) 2024 by Thomas von Dein
AUTHORS
Thomas von Dein tom AT vondein DOT org
`

View File

@@ -1,252 +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 cmd
import (
"errors"
"os"
"strings"
"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 {
var (
attr app.DbAttr
)
var cmd = &cobra.Command{
Use: "set <key> [<value> | -r <file>] [-t <tag>]",
Short: "Insert key/value pair",
Long: `Insert key/value pair`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("no key/value pair specified")
}
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
if len(args) > 0 {
attr.Args = args
}
// turn comma list into slice, if needed
if len(attr.Tags) == 1 && strings.Contains(attr.Tags[0], ",") {
attr.Tags = strings.Split(attr.Tags[0], ",")
}
// check if value given as file or via stdin and fill attr accordingly
if err := attr.ParseKV(); err != nil {
return err
}
// encrypt?
if conf.Encrypt {
pass, err := getPassword()
if err != nil {
return err
}
err = app.Encrypt(pass, &attr)
if err != nil {
return err
}
}
return conf.DB.Set(&attr)
},
}
cmd.PersistentFlags().BoolVarP(&conf.Encrypt, "encrypt", "e", false, "encrypt value")
cmd.PersistentFlags().StringVarP(&attr.File, "file", "r", "", "Filename or - for STDIN")
cmd.PersistentFlags().StringArrayVarP(&attr.Tags, "tags", "t", nil, "tags, multiple allowed")
cmd.Aliases = append(cmd.Aliases, "add")
cmd.Aliases = append(cmd.Aliases, "s")
cmd.Aliases = append(cmd.Aliases, "+")
return cmd
}
func Get(conf *cfg.Config) *cobra.Command {
var (
attr app.DbAttr
)
var cmd = &cobra.Command{
Use: "get <key> [-o <file>] [-m <mode>] [-n -N] [-T <tpl>]",
Short: "Retrieve value for a key",
Long: `Retrieve value for a key`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("no key specified")
}
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
if len(args) > 0 {
attr.Key = args[0]
}
entry, err := conf.DB.Get(&attr)
if err != nil {
return err
}
if entry.Encrypted {
pass, err := getPassword()
if err != nil {
return err
}
clear, err := app.Decrypt(pass, []byte(entry.Value))
if err != nil {
return err
}
entry.Value = string(clear)
entry.Encrypted = false
}
return output.Print(os.Stdout, conf, &attr, entry)
},
}
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().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().StringVarP(&conf.Template, "template", "T", "", "go template for '-m template'")
cmd.Aliases = append(cmd.Aliases, "show")
cmd.Aliases = append(cmd.Aliases, "g")
cmd.Aliases = append(cmd.Aliases, ".")
return cmd
}
func Del(conf *cfg.Config) *cobra.Command {
var (
attr app.DbAttr
)
var cmd = &cobra.Command{
Use: "del <key>",
Short: "Delete key",
Long: `Delete key and value matching key`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("No key specified")
}
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
if len(args) > 0 {
attr.Key = args[0]
}
return conf.DB.Del(&attr)
},
}
cmd.Aliases = append(cmd.Aliases, "d")
cmd.Aliases = append(cmd.Aliases, "rm")
return cmd
}
func List(conf *cfg.Config) *cobra.Command {
var (
attr app.DbAttr
wide bool
)
var cmd = &cobra.Command{
Use: "list [<filter-regex> | -t <tag> ] [-m <mode>] [-nNis] [-T <tpl>]",
Short: "List database contents",
Long: `List database contents`,
RunE: func(cmd *cobra.Command, args []string) error {
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
if len(args) > 0 {
if conf.CaseInsensitive {
attr.Args = []string{"(?i)" + args[0]}
} else {
attr.Args = args
}
}
// turn comma list into slice, if needed
if len(attr.Tags) == 1 && strings.Contains(attr.Tags[0], ",") {
attr.Tags = strings.Split(attr.Tags[0], ",")
}
if wide {
conf.Mode = "wide"
}
entries, err := conf.DB.List(&attr, conf.Fulltext)
if err != nil {
return err
}
return output.List(os.Stdout, conf, entries)
},
}
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().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.NoHumanize, "no-human", "N", false, "do not translate to human readable values")
cmd.PersistentFlags().BoolVarP(&conf.CaseInsensitive, "case-insensitive", "i", false, "filter case insensitive")
cmd.PersistentFlags().BoolVarP(&conf.Fulltext, "search-fulltext", "s", false, "perform a full text search")
cmd.PersistentFlags().StringArrayVarP(&attr.Tags, "tags", "t", nil, "tags, multiple allowed")
cmd.Aliases = append(cmd.Aliases, "ls")
cmd.Aliases = append(cmd.Aliases, "/")
cmd.Aliases = append(cmd.Aliases, "find")
cmd.Aliases = append(cmd.Aliases, "search")
return cmd
}
func getPassword() ([]byte, error) {
var pass []byte
envpass := os.Getenv("ANYDB_PASSWORD")
if envpass == "" {
readpass, err := app.AskForPassword()
if err != nil {
return nil, err
}
pass = readpass
} else {
pass = []byte(envpass)
}
return pass, nil
}

View File

@@ -1,326 +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 cmd
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"github.com/spf13/cobra"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg"
"github.com/tlinden/anydb/output"
"github.com/tlinden/anydb/rest"
)
func Export(conf *cfg.Config) *cobra.Command {
var (
attr app.DbAttr
)
var cmd = &cobra.Command{
Use: "export -o <json filename>",
Short: "Export database to json file",
Long: `Export database to json file`,
RunE: func(cmd *cobra.Command, args []string) error {
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
conf.Mode = "json"
entries, err := conf.DB.Getall(&attr)
if err != nil {
return err
}
return output.WriteJSON(&attr, conf, entries)
},
}
cmd.PersistentFlags().StringVarP(&attr.File, "output-file", "o", "", "filename or - for STDIN")
if err := cmd.MarkPersistentFlagRequired("output-file"); err != nil {
panic(err)
}
cmd.Aliases = append(cmd.Aliases, "dump")
cmd.Aliases = append(cmd.Aliases, "backup")
return cmd
}
func Import(conf *cfg.Config) *cobra.Command {
var (
attr app.DbAttr
)
var cmd = &cobra.Command{
Use: "import -i <json file>",
Short: "Import database dump",
Long: `Import database dump`,
RunE: func(cmd *cobra.Command, args []string) error {
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
out, err := conf.DB.Import(&attr)
if err != nil {
return err
}
fmt.Print(out)
return nil
},
}
cmd.PersistentFlags().StringVarP(&attr.File, "import-file", "i", "", "filename or - for STDIN")
cmd.PersistentFlags().StringArrayVarP(&attr.Tags, "tags", "t", nil, "tags, multiple allowed")
if err := cmd.MarkPersistentFlagRequired("import-file"); err != nil {
panic(err)
}
cmd.Aliases = append(cmd.Aliases, "restore")
return cmd
}
func Help(conf *cfg.Config) *cobra.Command {
return nil
}
func Man(conf *cfg.Config) *cobra.Command {
var cmd = &cobra.Command{
Use: "man",
Short: "show manual page",
Long: `show manual page`,
RunE: func(cmd *cobra.Command, args []string) error {
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
man := exec.Command("less", "-")
var b bytes.Buffer
b.WriteString(manpage)
man.Stdout = os.Stdout
man.Stdin = &b
man.Stderr = os.Stderr
err := man.Run()
if err != nil {
return fmt.Errorf("failed to execute 'less': %w", err)
}
return nil
},
}
return cmd
}
func Serve(conf *cfg.Config) *cobra.Command {
var cmd = &cobra.Command{
Use: "serve [-l host:port]",
Short: "run REST API listener",
Long: `run REST API listener`,
RunE: func(cmd *cobra.Command, args []string) error {
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
return rest.Runserver(conf, nil)
},
}
cmd.PersistentFlags().StringVarP(&conf.Listen, "listen", "l", "localhost:8787", "host:port")
return cmd
}
func Info(conf *cfg.Config) *cobra.Command {
var cmd = &cobra.Command{
Use: "info",
Short: "info",
Long: `show info about database`,
RunE: func(cmd *cobra.Command, args []string) error {
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
info, err := conf.DB.Info()
if err != nil {
return err
}
return output.Info(os.Stdout, conf, info)
},
}
cmd.PersistentFlags().BoolVarP(&conf.NoHumanize, "no-human", "N", false, "do not translate to human readable values")
return cmd
}
func Edit(conf *cfg.Config) *cobra.Command {
var (
attr app.DbAttr
)
var cmd = &cobra.Command{
Use: "edit <key>",
Short: "Edit a key",
Long: `Edit a key`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return errors.New("no key specified")
}
// errors at this stage do not cause the usage to be shown
cmd.SilenceUsage = true
password := []byte{}
if len(args) > 0 {
attr.Key = args[0]
}
// fetch entry
entry, err := conf.DB.Get(&attr)
if err != nil {
return err
}
if len(entry.Value) == 0 && entry.Binary {
return errors.New("key contains binary uneditable content")
}
// decrypt if needed
if entry.Encrypted {
pass, err := getPassword()
if err != nil {
return err
}
password = pass
clear, err := app.Decrypt(pass, []byte(entry.Value))
if err != nil {
return err
}
entry.Value = string(clear)
entry.Encrypted = false
}
// determine editor, vi is default
editor := getEditor()
// save file to a temp file, call the editor with it, read
// it back in and compare the content with the original
// one
newcontent, err := editContent(editor, string(entry.Value))
if err != nil {
return err
}
// all is valid, fill our DB feeder
newattr := app.DbAttr{
Key: attr.Key,
Tags: attr.Tags,
Encrypted: attr.Encrypted,
Val: []byte(newcontent),
}
// encrypt if needed
if conf.Encrypt {
err = app.Encrypt(password, &attr)
if err != nil {
return err
}
}
// done
return conf.DB.Set(&newattr)
},
}
cmd.Aliases = append(cmd.Aliases, "modify")
cmd.Aliases = append(cmd.Aliases, "mod")
cmd.Aliases = append(cmd.Aliases, "ed")
cmd.Aliases = append(cmd.Aliases, "vi")
return cmd
}
func getEditor() string {
editor := "vi"
enveditor, present := os.LookupEnv("EDITOR")
if present {
if editor != "" {
editor = enveditor
}
}
return editor
}
// taken from github.com/tlinden/rpn/ (my own program)
func editContent(editor string, content string) (string, error) {
// create a temp file
tmp, err := os.CreateTemp("", "stack")
if err != nil {
return "", fmt.Errorf("failed to create templ file: %w", err)
}
defer os.Remove(tmp.Name())
// put the content into a tmp file
_, err = tmp.WriteString(content)
if err != nil {
return "", fmt.Errorf("failed to write value to temp file: %w", err)
}
// execute editor with our tmp file containing current stack
cmd := exec.Command(editor, tmp.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
return "", fmt.Errorf("failed to run editor command %s: %w", editor, err)
}
// read the file back in
modified, err := os.Open(tmp.Name())
if err != nil {
return "", fmt.Errorf("failed to open temp file: %w", err)
}
defer modified.Close()
newcontent, err := io.ReadAll(modified)
if err != nil {
return "", fmt.Errorf("failed to read from temp file: %w", err)
}
newcontentstr := string(newcontent)
if content == newcontentstr {
return "", fmt.Errorf("content not modified, aborting")
}
return newcontentstr, nil
}

View File

@@ -1,146 +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 cmd
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/alecthomas/repr"
"github.com/spf13/cobra"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg"
)
func completion(cmd *cobra.Command, mode string) error {
switch mode {
case "bash":
return cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
return cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
return cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
default:
return errors.New("invalid shell parameter! Valid ones: bash|zsh|fish|powershell")
}
}
func Execute() {
var (
conf cfg.Config
configfile string
ShowVersion bool
ShowCompletion string
)
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
SearchConfigs := []string{
filepath.Join(home, ".config", "anydb", "anydb.toml"),
filepath.Join(home, ".anydb.toml"),
"anydb.toml",
}
var rootCmd = &cobra.Command{
Use: "anydb <command> [options]",
Short: "anydb",
Long: `A personal key value store`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
dbfile := app.GetDbFile(conf.Dbfile)
db, err := app.New(dbfile, conf.Dbbucket, conf.Debug)
if err != nil {
return err
}
conf.DB = db
var configs []string
if configfile != "" {
configs = []string{configfile}
} else {
configs = SearchConfigs
}
if err := conf.GetConfig(configs); err != nil {
return err
}
if conf.Debug {
repr.Println(conf)
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if ShowVersion {
fmt.Printf("This is anydb version %s\n", cfg.Version)
return nil
}
if len(ShowCompletion) > 0 {
return completion(cmd, ShowCompletion)
}
if len(args) == 0 {
return errors.New("no command specified")
}
return nil
},
}
// options
rootCmd.PersistentFlags().BoolVarP(&ShowVersion, "version", "v", false, "Print program version")
rootCmd.PersistentFlags().BoolVarP(&conf.Debug, "debug", "d", false, "Enable debugging")
rootCmd.PersistentFlags().StringVarP(&conf.Dbfile, "dbfile", "f",
"", "DB file to use (default: ~/.config/anydb/default.db)")
rootCmd.PersistentFlags().StringVarP(&conf.Dbbucket, "bucket", "b",
app.BucketData, "use other bucket (default: "+app.BucketData+")")
rootCmd.PersistentFlags().StringVarP(&configfile, "config", "c", "", "toml config file")
// CRUD
rootCmd.AddCommand(Set(&conf))
rootCmd.AddCommand(List(&conf))
rootCmd.AddCommand(Get(&conf))
rootCmd.AddCommand(Del(&conf))
// backup
rootCmd.AddCommand(Export(&conf))
rootCmd.AddCommand(Import(&conf))
// REST API
rootCmd.AddCommand(Serve(&conf))
// auxiliary
rootCmd.AddCommand(Man(&conf))
rootCmd.AddCommand(Info(&conf))
rootCmd.AddCommand(Edit(&conf))
err = rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}

View File

@@ -1,36 +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 common
import "os"
func CleanError(file string, err error) error {
// remove given [backup] file and forward the given error
os.Remove(file)
return err
}
func FileExists(filename string) bool {
info, err := os.Stat(filename)
if err != nil {
// return false on any error
return false
}
return !info.IsDir()
}

View File

@@ -1,21 +0,0 @@
.PHONY: demo clean check clean-demo
VHS = vhs
clean-demo:
rm -f local.db*
%.gif: %.tape
@echo "vhs $<"
env PATH=..:$(PATH) ANYDB_DB=local.db vhs $<
clean:
rm -vf *.db* *.json
check:
ls -l ../anydb
demo: check clean-demo intro.gif advanced.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 MiB

View File

@@ -1,182 +0,0 @@
# -*-sh-*-
Output advanced.gif
Set FontSize 20
Set Width 1000
Set Height 800
Set Theme { "name": "Whimsy", "black": "#535178", "red": "#ef6487", "green": "#5eca89", "yellow": "#fdd877", "blue": "#65aef7", "magenta": "#aa7ff0", "cyan": "#43c1be", "white": "#ffffff", "brightBlack": "#535178", "brightRed": "#ef6487", "brightGreen": "#5eca89", "brightYellow": "#fdd877", "brightBlue": "#65aef7", "brightMagenta": "#aa7ff0", "brightCyan": "#43c1be", "brightWhite": "#ffffff", "background": "#29283b", "foreground": "#b3b0d6", "selection": "#3d3c58", "cursor": "#b3b0d6" }
Set WindowBar Colorful
Set BorderRadius 10
Set Shell zsh
Set FontFamily "IBM Plex Mono"
Set CursorBlink false
Set PlaybackSpeed 1
Set TypingSpeed .05
Hide
Type `PROMPT=''`
Enter
Type "setopt interactivecomments"
Enter
Type "autoload -U colors && colors"
Enter
Type `PS1="%{$fg[magenta]%}demo> %{$reset_color%}"`
Enter
Type "clear"
Enter
Show
Type "# you can assign tags"
Enter
Sleep 1s
Type "anydb set foo bar -t note,important"
Enter
Sleep 3s
Enter
Type "# and filter for them"
Enter
Sleep 1s
Type "anydb list -t important"
Enter
Sleep 3s
Enter
Type "# beside tags filtering you can also use regexps for searching"
Enter
Type "# note, by default the list command only searches through keys"
Enter
Sleep 1s
Type "anydb list '[a-z]+'"
Enter
Sleep 3s
Enter
Type "# do a full text search"
Enter
Sleep 1s
Type "anydb list '[a-z]+' -s"
Enter
Sleep 3s
Enter
Type "# anydb also supports a wide output"
Enter
Sleep 1s
Type "anydb list -m wide"
Enter
Sleep 3s
Enter
Type "# there are shortcuts as well"
Enter
Sleep 1s
Type "anydb ls -l"
Enter
Sleep 2s
Type "anydb /"
Enter
Sleep 3s
Enter
Type "# other outputs are possible as well"
Enter
Sleep 1s
Type "anydb list -m json"
Enter
Sleep 3s
Enter
Type "# you can backup your database"
Enter
Sleep 1s
Type "anydb export -o backup.json"
Enter
Sleep 3s
Enter
Type "# and import it somewhere else"
Enter
Sleep 1s
Type "rm local.db"
Enter
Sleep 1s
Type "anydb ls -l"
Enter
Sleep 1s
Type "anydb import -i backup.json"
Enter
Sleep 1s
Type "anydb ls -l"
Enter
Sleep 3s
Enter
Type "# you can encrypt entries. anydb asks for a passphrase"
Enter
Type "# and will do the same when you retrieve the key using the"
Enter
Type "# get command. anydb will ask you interactively for a password"
Enter
Sleep 1s
Type "anydb set address 'Beatstreet 42' -e"
Enter
Type "pass"
Enter
Sleep 3s
Enter
Type "# but you can provide it via an environment variable too"
Enter
Sleep 1s
Type "ANYDB_PASSWORD=foo anydb set -e secretkey blahblah"
Enter
Sleep 3s
Enter
Type "# using template output mode you can freely design how to print stuff"
Enter
Type "# here, we print the values in CSV format ONLY if they have some tag"
Enter
Type "# also note, that we're printing the creation timestamp as epoch"
Sleep 1s
Type `anydb ls -m template -T "{{ if .Tags }}{{ .Key }},{{ .Value }},{{ .Created.AsTime.Unix}}{{ end }}"`
Enter
Sleep 3s
Enter
Type "# or, to simulate skate's -k or -v"
Enter
Sleep 1s
Type `anydb ls -m template -T "{{ .Key }}"`
Enter
Sleep 1s
Type `anydb ls -m template -T "{{ .Value }}"`
Enter
Sleep 3s
Enter
Type "# maybe you want to digest the item in a shell script? also"
Enter
Type "# note, that both the list and get commands support templates"
Enter
Sleep 1s
Type `eval $(anydb get kitty -m template -T "value='{{ .Value }}'"); echo "value: $value"`
Enter
Sleep 3s
Enter
Type "# sometimes you need to know some details about the current database"
Enter
Type "# add -d for more details"
Enter
Sleep 1
Type "anydb info"
Enter
Sleep 3s
Enter
Type "# Try it out yourself: github.com/tlinden/anydb!"
Enter
Sleep 4s

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

View File

@@ -1,76 +0,0 @@
# -*-sh-*-
Output intro.gif
Set FontSize 20
Set Width 1000
Set Height 800
Set Theme { "name": "Whimsy", "black": "#535178", "red": "#ef6487", "green": "#5eca89", "yellow": "#fdd877", "blue": "#65aef7", "magenta": "#aa7ff0", "cyan": "#43c1be", "white": "#ffffff", "brightBlack": "#535178", "brightRed": "#ef6487", "brightGreen": "#5eca89", "brightYellow": "#fdd877", "brightBlue": "#65aef7", "brightMagenta": "#aa7ff0", "brightCyan": "#43c1be", "brightWhite": "#ffffff", "background": "#29283b", "foreground": "#b3b0d6", "selection": "#3d3c58", "cursor": "#b3b0d6" }
Set WindowBar Colorful
Set BorderRadius 10
Set Shell zsh
Set FontFamily "IBM Plex Mono"
Set CursorBlink false
Set PlaybackSpeed 1
Set TypingSpeed .05
Hide
Type `PROMPT=''`
Enter
Type "setopt interactivecomments"
Enter
Type "autoload -U colors && colors"
Enter
Type `PS1="%{$fg[magenta]%}demo> %{$reset_color%}"`
Enter
Type "clear"
Enter
Show
Type "# Store something"
Enter
Sleep 1s
Type "anydb set kitty meow"
Enter
Sleep 3s
Enter
Type `# What's in the store?`
Enter
Sleep 1s
Type "anydb ls"
Enter
Sleep 3s
Enter
Type "# Fetch something"
Enter
Sleep 1s
Type "anydb get kitty"
Enter
Sleep 3s
Enter
Type "# Unicode also works, of course"
Enter
Sleep 1s
Type "anydb set 猫咪 喵"
Enter
Sleep 2s
Type "anydb get 猫咪"
Enter
Sleep 3s
Enter
Type "# Do creative things with anydb list"
Enter
Sleep 1s
Type "anydb set penelope marmalade"
Enter
Type "anydb set christian tacos"
Enter
Type "anydb set muesli muesli"
Enter
Type "anydb list | xargs -n 2 printf '%s loves %s.\n'"
Enter
Sleep 3s

View File

@@ -1,38 +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/>.
#
version: "3.9"
services:
init:
image: alpine:latest
user: "root"
group_add:
- '${GROUP_ID}'
volumes:
- ${OUTDIR}:/backup
command: chown -R ${USER_ID}:${USER_ID} /backup
anydb:
container_name: anydb
user: "${USER_ID}:${USER_ID}"
volumes:
- ${OUTDIR}:/backup
working_dir: /backup
build: .
image: anydb:latest
depends_on:
init:
condition: service_completed_successfully

View File

@@ -1,14 +0,0 @@
# defaults
dbfile = "~/.config/anydb/default.db"
dbbucket = "data"
noheaders = false
nohumanize = false
encrypt = false
listen = "localhost:8787"
# different setups for different buckets
[buckets.data]
encrypt = true
[buckets.test]
encrypt = false

36
go.mod
View File

@@ -1,36 +0,0 @@
module github.com/tlinden/anydb
go 1.22.1
require (
github.com/alecthomas/repr v0.4.0
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/olekukonko/tablewriter v0.0.5
github.com/pelletier/go-toml v1.9.5
github.com/rogpeppe/go-internal v1.13.1
github.com/spf13/cobra v1.8.1
go.etcd.io/bbolt v1.3.11
golang.org/x/crypto v0.31.0
golang.org/x/term v0.27.0
google.golang.org/protobuf v1.36.4
)
require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/gofiber/fiber/v3 v3.0.0-beta.3 // indirect
github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.6 // 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/sys v0.29.0 // indirect
golang.org/x/tools v0.22.0 // indirect
)

113
go.sum
View File

@@ -1,113 +0,0 @@
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
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/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/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/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
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/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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/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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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/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-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/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/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=
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

55
main.go
View File

@@ -1,55 +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 main
import (
"bufio"
"fmt"
"os"
"runtime"
"github.com/inconshreveable/mousetrap"
"github.com/tlinden/anydb/cmd"
)
func main() {
Main()
}
func Main() int {
cmd.Execute()
return 0
}
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("Please do no double click anydb.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)
}
}
}
}

View File

@@ -1,36 +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 main
import (
"os"
"testing"
"github.com/rogpeppe/go-internal/testscript"
)
func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"anydb": Main,
}))
}
func TestAnydb(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "t",
})
}

View File

@@ -1,75 +0,0 @@
#!/bin/bash
# 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/>.
# get list with: go tool dist list
DIST="darwin/amd64
freebsd/amd64
linux/amd64
netbsd/amd64
openbsd/amd64
windows/amd64
freebsd/arm64
linux/arm64
netbsd/arm64
openbsd/arm64
windows/arm64"
tool="$1"
version="$2"
if test -z "$version"; then
echo "Usage: $0 <tool name> <release version>"
exit 1
fi
rm -rf releases
mkdir -p releases
for D in $DIST; do
os=${D/\/*/}
arch=${D/*\//}
binfile="releases/${tool}-${os}-${arch}-${version}"
if test "$os" = "windows"; then
binfile="${binfile}.exe"
fi
tardir="${tool}-${os}-${arch}-${version}"
tarfile="releases/${tool}-${os}-${arch}-${version}.tar.gz"
set -x
GOOS=${os} GOARCH=${arch} go build -tags osusergo,netgo -ldflags "-extldflags=-static" -o ${binfile}
mkdir -p ${tardir}
cp ${binfile} README.md LICENSE ${tardir}/
echo 'tool = anydb
PREFIX = /usr/local
UID = root
GID = 0
install:
install -d -o $(UID) -g $(GID) $(PREFIX)/bin
install -d -o $(UID) -g $(GID) $(PREFIX)/man/man1
install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/
install -o $(UID) -g $(GID) -m 444 $(tool).1 $(PREFIX)/man/man1/' > ${tardir}/Makefile
tar cpzf ${tarfile} ${tardir}
sha256sum ${binfile} | cut -d' ' -f1 > ${binfile}.sha256
sha256sum ${tarfile} | cut -d' ' -f1 > ${tarfile}.sha256
rm -rf ${tardir}
set +x
done

View File

@@ -1,50 +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 output
import (
"encoding/json"
"fmt"
"os"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg"
)
func WriteJSON(attr *app.DbAttr, conf *cfg.Config, entries app.DbEntries) error {
jsonentries, err := json.Marshal(entries)
if err != nil {
return fmt.Errorf("failed to marshall json: %w", err)
}
if attr.File == "-" {
fmt.Println(string(jsonentries))
} else {
fd, err := os.OpenFile(attr.File, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("failed to open file %s for writing: %w", attr.File, err)
}
if _, err := fd.Write(jsonentries); err != nil {
return fmt.Errorf("failed writing to file %s: %w", attr.File, err)
}
fmt.Printf("database contents exported to %s\n", attr.File)
}
return nil
}

View File

@@ -1,137 +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 output
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"strconv"
"strings"
tpl "text/template"
"github.com/dustin/go-humanize"
"github.com/olekukonko/tablewriter"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg"
)
func List(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
switch conf.Mode {
case "wide", "", "table":
return ListTable(writer, conf, entries)
case "json":
return ListJson(writer, conf, entries)
case "template":
return ListTemplate(writer, conf, entries)
default:
return errors.New("unsupported mode")
}
}
func ListJson(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
jsonentries, err := json.Marshal(entries)
if err != nil {
return fmt.Errorf("failed marshall json: %s", err)
}
fmt.Println(string(jsonentries))
return nil
}
func ListTemplate(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
tmpl, err := tpl.New("list").Parse(conf.Template)
if err != nil {
return fmt.Errorf("failed to parse output template: %w", err)
}
buf := bytes.Buffer{}
for _, row := range entries {
buf.Reset()
err = tmpl.Execute(&buf, row)
if err != nil {
return fmt.Errorf("failed to execute output template: %w", err)
}
if buf.Len() > 0 {
fmt.Fprintln(writer, buf.String())
}
}
return nil
}
func ListTable(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
tableString := &strings.Builder{}
table := tablewriter.NewWriter(tableString)
if !conf.NoHeaders {
if conf.Mode == "wide" {
table.SetHeader([]string{"KEY", "TAGS", "SIZE", "UPDATED", "VALUE"})
} else {
table.SetHeader([]string{"KEY", "VALUE"})
}
}
for _, row := range entries {
if conf.Mode == "wide" {
switch conf.NoHumanize {
case true:
table.Append([]string{
row.Key,
strings.Join(row.Tags, ","),
strconv.FormatUint(row.Size, 10),
row.Created.AsTime().Format("02.01.2006T03:04.05"),
row.Preview,
})
default:
table.Append([]string{
row.Key,
strings.Join(row.Tags, ","),
humanize.Bytes(uint64(row.Size)),
humanize.Time(row.Created.AsTime()),
row.Preview,
})
}
} else {
table.Append([]string{row.Key, row.Preview})
}
}
table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
table.SetAutoWrapText(false)
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
table.Render()
fmt.Fprint(writer, tableString.String())
return nil
}

View File

@@ -1,135 +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 output
import (
"encoding/json"
"fmt"
"io"
"os"
"reflect"
"github.com/dustin/go-humanize"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg"
"golang.org/x/term"
//"github.com/alecthomas/repr"
)
func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error {
if attr.File != "" {
return WriteFile(writer, conf, attr, entry)
}
isatty := term.IsTerminal(int(os.Stdout.Fd()))
switch conf.Mode {
case "simple", "":
if entry.Binary {
if isatty {
fmt.Println("binary data omitted")
} else {
os.Stdout.WriteString(entry.Value)
}
} else {
fmt.Print(string(entry.Value))
if entry.Value[entry.Size-1] != '\n' {
// always add a terminal newline
fmt.Println()
}
}
case "json":
jsonentry, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("failed to marshall json: %s", err)
}
fmt.Println(string(jsonentry))
case "wide":
return ListTable(writer, conf, app.DbEntries{entry})
case "template":
return ListTemplate(writer, conf, app.DbEntries{entry})
}
return nil
}
func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error {
var fileHandle *os.File
var err error
if attr.File == "-" {
fileHandle = os.Stdout
} else {
fd, err := os.OpenFile(attr.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
return fmt.Errorf("failed to open file %s for writing: %w", attr.File, err)
}
defer fd.Close()
fileHandle = fd
}
// actually write file content
_, err = fileHandle.WriteString(entry.Value)
if !entry.Binary {
if entry.Value[entry.Size-1] != '\n' {
// always add a terminal newline
_, err = fileHandle.Write([]byte{'\n'})
}
}
if err != nil {
return fmt.Errorf("failed to write to file %s: %w", attr.File, err)
}
return nil
}
func Info(writer io.Writer, conf *cfg.Config, info *app.DbInfo) error {
fmt.Fprintf(writer, "Database: %s\n", info.Path)
for _, bucket := range info.Buckets {
if conf.NoHumanize {
fmt.Fprintf(
writer,
"%19s: %s\n%19s: %d\n%19s: %d\n%19s: %t\n",
"Bucket", bucket.Name,
"Size", bucket.Size,
"Keys", bucket.Keys,
"Encrypted", conf.Encrypt)
} else {
fmt.Fprintf(
writer,
"%19s: %s\n%19s: %s\n%19s: %d\n",
"Bucket", bucket.Name,
"Size", humanize.Bytes(uint64(bucket.Size)),
"Keys", bucket.Keys)
}
if conf.Debug {
val := reflect.ValueOf(&bucket.Stats).Elem()
for i := 0; i < val.NumField(); i++ {
fmt.Fprintf(writer, "%19s: %v\n", val.Type().Field(i).Name, val.Field(i))
}
}
fmt.Fprintln(writer)
}
return nil
}

View File

@@ -1,140 +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 rest
import (
//"github.com/alecthomas/repr"
"github.com/gofiber/fiber/v2"
"github.com/tlinden/anydb/app"
"github.com/tlinden/anydb/cfg"
)
type SetContext struct {
Query string `json:"query" form:"query"`
}
type ListResponse struct {
Success bool
Code int
Entries app.DbEntries
}
type SingleResponse struct {
Success bool
Code int
Entry *app.DbEntry
}
func RestList(c *fiber.Ctx, conf *cfg.Config) error {
attr := new(app.DbAttr)
if len(c.Body()) > 0 {
if err := c.BodyParser(attr); err != nil {
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{
"errors": err.Error(),
})
}
}
// get list
entries, err := conf.DB.List(attr, attr.Fulltext)
if err != nil {
return JsonStatus(c, fiber.StatusForbidden,
"Unable to list keys: "+err.Error())
}
return c.Status(fiber.StatusOK).JSON(
ListResponse{
Success: true,
Code: fiber.StatusOK,
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 {
return JsonStatus(c, fiber.StatusForbidden,
"Unable to set key: "+err.Error())
}
return c.Status(fiber.StatusOK).JSON(
Result{
Success: true,
Code: fiber.StatusOK,
},
)
}

View File

@@ -1,118 +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 rest
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/tlinden/anydb/cfg"
)
// used to return to the api client
type Result struct {
Success bool `json:"success"`
Message string `json:"message"`
Code int `json:"code"`
}
func Runserver(conf *cfg.Config, args []string) error {
// setup api server
router := SetupServer(conf)
// public rest api routes
api := router.Group("/anydb/v1")
{
api.Get("/", func(c *fiber.Ctx) error {
return RestList(c, conf)
})
api.Post("/", func(c *fiber.Ctx) error {
// same thing as above but allows to supply parameters, see app.Dbattr{}
return RestList(c, conf)
})
api.Get("/:key", func(c *fiber.Ctx) error {
return RestGet(c, conf)
})
api.Delete("/:key", func(c *fiber.Ctx) error {
return RestDelete(c, conf)
})
api.Put("/", func(c *fiber.Ctx) error {
return RestSet(c, 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{
Format: "${pid} ${ip}:${port} ${status} - ${method} ${path}\n",
DisableColors: true,
}))
router.Use(cors.New(cors.Config{
AllowMethods: "GET,PUT,POST,DELETE",
ExposeHeaders: "Content-Type,Accept",
}))
router.Use(compress.New(compress.Config{
Level: compress.LevelBestSpeed,
}))
return router
}
/*
Wrapper to respond with proper json status, message and code,
shall be prepared and called by the handlers directly.
*/
func JsonStatus(c *fiber.Ctx, code int, msg string) error {
success := true
if code != fiber.StatusOK {
success = false
}
return c.Status(code).JSON(Result{
Code: code,
Message: msg,
Success: success,
})
}

View File

@@ -1,51 +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/>.
#
# simple file, we cannot use redirection here, so dd is our friend
exec dd if=/dev/random of=file.txt count=5 bs=10
# add file to db
exec anydb -f test.db set datum -r file.txt
# check for existence
exec anydb -f test.db get datum -o out.txt
exists out.txt
# check if its filled (50 bytes == count=5 x bs=10)
exec ls -l out.txt
stdout 50
# look if it's inside the db
exec anydb -f test.db ls
stdout datum.*binary-content
# do the same thing with text content, start with a new text entry
exec anydb -f test.db set feed alpha
# which we write to a file
exec anydb -f test.db get feed -o out2.txt
exists out2.txt
# check if its filled (5 bytes + newline)
exec ls -l out2.txt
stdout 6
# compare content
exec cat out2.txt
stdout alpha

View File

@@ -1,25 +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/>.
#
# check default outputs
exec anydb -v
stdout 'This is anydb version'
! exec anydb
stderr 'Available Commands:'

View File

@@ -1,32 +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/>.
#
# setup simple db
exec anydb -f test.db set foo bar
# create backup
exec anydb -f test.db export -o backup.json
stdout 'database contents exported to backup.json'
# import into new db
exec anydb -f new.db import -i backup.json
stdout 'imported.*entries'
# check contents
exec anydb -f new.db list bar -s
stdout foo.*bar

View File

@@ -1,81 +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/>.
#
# simple entry
exec anydb -f test.db set foo bar
# single entry uc()
exec anydb -f test.db set MUCHAS gracias
# entry with tags
exec anydb -f test.db set color grey -t flower,plant
# simple list
exec anydb -f test.db list
stdout foo.*bar
# wide list
exec anydb -f test.db list -m wide
stdout 'plant.*now.*grey'
# list tagged
exec anydb -f test.db list -t flower
! stdout bar
# list with filter
exec anydb -f test.db list b.r -s
stdout bar
# list with -i filter
exec anydb -f test.db list -is mucha
stdout mucha
# get single entry
exec anydb -f test.db get color
stdout grey
# modify
exec anydb -f test.db set foo blah
# check modified
exec anydb -f test.db get foo
stdout blah
# modify tagged
exec anydb -f test.db set color grey -t butterfly
# check modified tagged
exec anydb -f test.db list -t butterfly
stdout grey
# check modified tagged, make sure
exec anydb -f test.db list -t flower
! stdout grey
# check json output
exec anydb -f test.db list -m json
stdout '^\[\{'
exec anydb -f test.db get color -m json
stdout '^\{'
# delete entry
exec anydb -f test.db del foo
# check deleted
exec anydb -f test.db list
! stdout bar