2 Commits

Author SHA1 Message Date
46d641301d fix badges 2025-11-03 09:14:20 +01:00
d1702949a2 moving to codeberg 2025-11-03 09:09:14 +01:00
31 changed files with 439 additions and 765 deletions

6
.golangci.bck.yaml Normal file
View File

@@ -0,0 +1,6 @@
linters:
exclusions:
rules:
- linters:
- staticcheck
text: "QF1008:"

7
.golangci.yaml Normal file
View File

@@ -0,0 +1,7 @@
version: "2"
linters:
exclusions:
rules:
- linters:
- staticcheck
text: "QF1008:"

View File

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

View File

@@ -66,4 +66,4 @@ release:
---
Full Changelog: [{{ .PreviousTag }}...{{ .Tag }}](https://codeberg.org/scip/anydb/compare/{{ .PreviousTag }}...{{ .Tag }})
Full Changelog: [{{ .PreviousTag }}...{{ .Tag }}](https://codeberg.org/scip/epuppy/compare/{{ .PreviousTag }}...{{ .Tag }})

View File

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

View File

@@ -6,7 +6,7 @@ variables:
steps:
dryrun:
image: docker.io/woodpeckerci/plugin-docker-buildx:6.0.3
image: docker.io/woodpeckerci/plugin-docker-buildx:latest
settings:
dockerfile: Dockerfile
platforms: linux/amd64
@@ -17,7 +17,7 @@ steps:
event: [pull_request]
publish:
image: docker.io/woodpeckerci/plugin-docker-buildx:6.0.3
image: docker.io/woodpeckerci/plugin-docker-buildx:latest
settings:
dockerfile: Dockerfile
platforms: linux/amd64

View File

@@ -14,7 +14,7 @@
# 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.26-alpine as builder
FROM golang:1.24-alpine as builder
RUN apk update
RUN apk upgrade

View File

@@ -97,8 +97,12 @@ goupdate:
go get -t -u=patch ./...
lint:
golangci-lint run --show-stats=false
@ # gocritic check -enableAll *.go
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

View File

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

View File

@@ -46,11 +46,11 @@ 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://codeberg.org/scip/anydb/raw/branch/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://codeberg.org/scip/anydb/raw/branch/main/demo/advanced.gif)
![advanced demo](https://codeberg.org/scip/anydb/raw/branch/demo/advanced.gif)
## Installation

30
anydb.1
View File

@@ -1,4 +1,4 @@
.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.42)
.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.40)
.\"
.\" Standard preamble:
.\" ========================================================================
@@ -133,12 +133,13 @@
.\" ========================================================================
.\"
.IX Title "ANYDB 1"
.TH ANYDB 1 "2026-09-01" "1" "User Commands"
.TH ANYDB 1 "2025-02-11" "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 "NAME"
.SH "anydb"
.IX Header "anydb"
anydb \- a personal key value store
.SH "SYNOPSIS"
.IX Header "SYNOPSIS"
@@ -345,7 +346,7 @@ subcommand, see there for more details. The options \f(CW\*(C`\-n\*(C'\fR and
have the same meaning as in the list command. The \*(L"json\*(R" mode prints
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 flexibility, it is detailed in the section
provides the most flexibily, it is detailed in the section
\&\s-1TEMPLATES\s0.
.SS "list"
.IX Subsection "list"
@@ -604,7 +605,7 @@ being used is the \s-1GO\s0 template language, refer to
.PP
Each template operates on one or more entries, no loop construct is
required, the template provided applies to every matching entry
separateley.
separatley.
.PP
The following template variables can be used:
.IP "\fB.Key\fR \- string" 4
@@ -612,19 +613,8 @@ The following template variables can be used:
.PD 0
.IP "\fB.Value\fR \- string" 4
.IX Item ".Value - string"
.PD
To be able to use the value in a template, you'll need to add the
\&\f(CW\*(C`\-s\*(C'\fR flag so that values are included in output data
stream. Otherwise you can only use \f(CW\*(C`.Preview\*(C'\fR.
.Sp
Be careful if you use \f(CW\*(C`\-s\*(C'\fR and \f(CW\*(C`.Value\*(C'\fR though: if a value is a
multiline string, it may clutter your output.
.IP "\fB.Preview\fR string" 4
.IX Item ".Preview string"
First line of entry value (if it is a string).
.IP "\fB.Bin\fR \- []byte" 4
.IX Item ".Bin - []byte"
.PD 0
.IP "\fB.Created\fR \- timestamp.Time" 4
.IX Item ".Created - timestamp.Time"
.PD
@@ -739,7 +729,7 @@ Some curl example calls to the \s-1API:\s0
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"data\*(R":\*(L"bar\*(R"}'
\-d '{\*(L"key\*(R":\*(L"foo\*(R",\*(L"val\*(R":\*(L"bar\*(R"}'
.PP
Retrieve the value:
.PP
@@ -752,12 +742,6 @@ List all keys:
.Vb 1
\& curl localhost:8787/anydb/v1/
.Ve
.PP
Delete an entry:
.PP
.Vb 1
\& curl \-s \-X DELETE http://localhost:8787/anydb/v1/foo
.Ve
.SH "BUGS"
.IX Header "BUGS"
In order to report a bug, unexpected behavior, feature requests

View File

@@ -1,4 +1,4 @@
=head1 NAME
=head1 anydb
anydb - a personal key value store
@@ -196,7 +196,7 @@ subcommand, see there for more details. The options C<-n> and C<-N>
have the same meaning as in the list command. The "json" mode prints
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 flexibility, it is detailed in the section
provides the most flexibily, it is detailed in the section
L<TEMPLATES>.
=head2 list
@@ -442,7 +442,7 @@ L<https://pkg.go.dev/text/template> for details.
Each template operates on one or more entries, no loop construct is
required, the template provided applies to every matching entry
separateley.
separatley.
The following template variables can be used:
@@ -452,17 +452,6 @@ The following template variables can be used:
=item B<.Value> - string
To be able to use the value in a template, you'll need to add the
C<-s> flag so that values are included in output data
stream. Otherwise you can only use C<.Preview>.
Be careful if you use C<-s> and C<.Value> though: if a value is a
multiline string, it may clutter your output.
=item B<.Preview> string
First line of entry value (if it is a string).
=item B<.Bin> - []byte
=item B<.Created> - timestamp.Time
@@ -580,7 +569,7 @@ 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","data":"bar"}'
-d '{"key":"foo","val":"bar"}'
Retrieve the value:
@@ -590,11 +579,6 @@ List all keys:
curl localhost:8787/anydb/v1/
Delete an entry:
curl -s -X DELETE http://localhost:8787/anydb/v1/foo
=head1 BUGS
In order to report a bug, unexpected behavior, feature requests

View File

@@ -1,7 +1,5 @@
package app
/*
Copyright © 2024-2025 Thomas von Dein
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
@@ -16,6 +14,7 @@ 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"
@@ -29,7 +28,6 @@ type DbAttr struct {
Key string
Preview string
Val []byte
Data string // alias
Args []string
Tags []string
File string
@@ -65,12 +63,6 @@ func (attr *DbAttr) ParseKV() error {
}
}
attr.SetPreview()
return nil
}
func (attr *DbAttr) SetPreview() {
switch {
case attr.Binary:
attr.Preview = "<binary-content>"
@@ -90,6 +82,8 @@ func (attr *DbAttr) SetPreview() {
attr.Preview = string(attr.Val)
}
}
return nil
}
func (attr *DbAttr) GetFileValue() error {
@@ -126,24 +120,17 @@ func (attr *DbAttr) GetFileValue() error {
}
} else {
// read from console stdin
var (
input string
data string
buf strings.Builder
)
var input string
var data string
for {
_, err := fmt.Scanln(&input)
if err != nil {
break
}
buf.WriteString(input)
buf.WriteString("\n")
data += input + "\n"
}
data += buf.String()
attr.Val = []byte(data)
}

View File

@@ -1,5 +1,3 @@
package app
/*
Copyright © 2024 Thomas von Dein
@@ -16,6 +14,7 @@ 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"
@@ -36,7 +35,7 @@ const (
ArgonParallel uint8 = 2
ArgonSaltLen int = 16
ArgonKeyLen uint32 = 32
B64SaltLen int = 16 // 22
B64SaltLen int = 16 //22
)
type Key struct {
@@ -48,8 +47,7 @@ type Key struct {
// password
func AskForPassword() ([]byte, error) {
fmt.Fprint(os.Stderr, "Password: ")
pass, err := term.ReadPassword(syscall.Stdin)
pass, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return nil, fmt.Errorf("failed to read password: %w", err)
}
@@ -59,8 +57,6 @@ func AskForPassword() ([]byte, error) {
return pass, nil
}
// DeriveKey derives a key from password.
//
// 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
@@ -82,7 +78,7 @@ func DeriveKey(password []byte, salt []byte) (*Key, error) {
}
hash := argon2.IDKey(
password, salt,
[]byte(password), salt,
ArgonIter,
ArgonMem,
ArgonParallel,
@@ -99,7 +95,6 @@ func DeriveKey(password []byte, salt []byte) (*Key, error) {
// 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)
@@ -108,7 +103,7 @@ func GetRandom(size int, capacity int) ([]byte, error) {
return buf, nil
}
// Encrypt clear text given in attr using ChaCha20 and authenticate
// 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.
//
@@ -148,7 +143,7 @@ func Encrypt(pass []byte, attr *DbAttr) error {
// Do the reverse
func Decrypt(pass []byte, cipherb []byte) ([]byte, error) {
if len(cipherb) < B64SaltLen {
return nil, errors.New("encrypted cipher block too small")
return nil, fmt.Errorf("encrypted cipher block too small")
}
key, err := DeriveKey(pass, cipherb[0:B64SaltLen])

190
app/db.go
View File

@@ -25,7 +25,6 @@ import (
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"time"
@@ -65,7 +64,7 @@ type DbTag struct {
}
func (entry *DbEntry) Taglist() string {
return strings.Join(entry.GetTags(), ",")
return strings.Join(entry.Tags, ",")
}
const BucketData string = "data"
@@ -92,14 +91,6 @@ func New(file string, bucket string, debug bool) (*DB, error) {
return &DB{Debug: debug, Dbfile: file, Bucket: bucket}, nil
}
func (db *DB) Exists() error {
if !common.FileExists(db.Dbfile) {
return fmt.Errorf("db file %s doesn't exist", db.Dbfile)
}
return nil
}
func (db *DB) Open() error {
slog.Debug("opening DB", "dbfile", db.Dbfile)
@@ -122,7 +113,6 @@ func (db *DB) Open() error {
}
db.DB = b
return nil
}
@@ -138,10 +128,8 @@ func (db *DB) List(attr *DbAttr, fulltext bool) (DbEntries, error) {
}
defer db.Close()
var (
entries DbEntries
filter *regexp.Regexp
)
var entries DbEntries
var filter *regexp.Regexp
if len(attr.Args) > 0 {
// via cli
@@ -170,7 +158,7 @@ func (db *DB) List(attr *DbAttr, fulltext bool) (DbEntries, error) {
databucket := root.Bucket([]byte("data"))
if databucket == nil {
return errors.New("failed to retrieve data sub bucket")
return fmt.Errorf("failed to retrieve data sub bucket")
}
err := bucket.ForEach(func(key, pbentry []byte) error {
@@ -181,7 +169,7 @@ func (db *DB) List(attr *DbAttr, fulltext bool) (DbEntries, error) {
if fulltext {
// avoid crash due to access fault
value := databucket.Get([]byte(entry.GetKey())) // empty is ok
value := databucket.Get([]byte(entry.Key)) // empty is ok
vc := make([]byte, len(value))
copy(vc, value)
entry.Value = string(vc)
@@ -191,21 +179,26 @@ func (db *DB) List(attr *DbAttr, fulltext bool) (DbEntries, error) {
switch {
case filter != nil:
if filter.MatchString(entry.GetKey()) ||
filter.MatchString(strings.Join(entry.GetTags(), " ")) {
if filter.MatchString(entry.Key) ||
filter.MatchString(strings.Join(entry.Tags, " ")) {
include = true
}
if !entry.GetBinary() && !include && fulltext {
if filter.MatchString(entry.GetValue()) {
if !entry.Binary && !include && fulltext {
if filter.MatchString(string(entry.Value)) {
include = true
}
}
case len(attr.Tags) > 0:
for _, search := range attr.Tags {
if slices.Contains(entry.GetTags(), search) {
include = true
for _, tag := range entry.Tags {
if tag == search {
include = true
break
}
}
if include {
break
}
}
@@ -246,7 +239,6 @@ func (db *DB) Set(attr *DbAttr) error {
// any tags. if so, we initialize our update struct with these
// tags unless it has new tags configured.
slog.Debug("+++ GET")
oldentry, err := db.txGet(attr)
if err != nil {
if !strings.Contains(err.Error(), "no such key") {
@@ -255,9 +247,9 @@ func (db *DB) Set(attr *DbAttr) error {
}
if oldentry != nil {
if len(oldentry.GetTags()) > 0 && len(entry.GetTags()) == 0 {
if len(oldentry.Tags) > 0 && len(entry.Tags) == 0 {
// initialize update entry with tags from old entry
entry.Tags = oldentry.GetTags()
entry.Tags = oldentry.Tags
}
}
@@ -269,7 +261,6 @@ func (db *DB) Set(attr *DbAttr) error {
}
slog.Debug("+++ UPDATE")
err = db.DB.Update(func(tx *bolt.Tx) error {
// create root bucket
root, err := tx.CreateBucketIfNotExists([]byte(db.Bucket))
@@ -286,7 +277,7 @@ func (db *DB) Set(attr *DbAttr) error {
slog.Debug("opened/created buckets", "root", root, "data", bucket)
// write meta data
err = bucket.Put([]byte(entry.GetKey()), pbentry)
err = bucket.Put([]byte(entry.Key), []byte(pbentry))
if err != nil {
return fmt.Errorf("failed to insert data: %w", err)
}
@@ -298,13 +289,14 @@ func (db *DB) Set(attr *DbAttr) error {
}
// write value
err = databucket.Put([]byte(entry.GetKey()), attr.Val)
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
}
@@ -312,6 +304,67 @@ func (db *DB) Set(attr *DbAttr) error {
return nil
}
// internal DB getter, assumes db.DB has already been
// opened successfully. Do NOT call this w/o valid
// DB handle!
func (db *DB) txGet(attr *DbAttr) (*DbEntry, error) {
entry := DbEntry{}
err := db.DB.View(func(tx *bolt.Tx) error {
// root bucket
root := tx.Bucket([]byte(db.Bucket))
if root == nil {
return nil
}
// get meta sub bucket
bucket := root.Bucket([]byte("meta"))
if bucket == nil {
return nil
}
slog.Debug("opened buckets", "root", root, "data", bucket)
// retrieve meta data
pbentry := bucket.Get([]byte(attr.Key))
if pbentry == nil {
return fmt.Errorf("no such key: %s", attr.Key)
}
// put into struct
if err := proto.Unmarshal(pbentry, &entry); err != nil {
return fmt.Errorf("failed to unmarshal from protobuf: %w", err)
}
// get data sub bucket
databucket := root.Bucket([]byte("data"))
if databucket == nil {
return 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, err
}
return &entry, nil
}
func (db *DB) Get(attr *DbAttr) (*DbEntry, error) {
if err := db.Open(); err != nil {
return nil, err
@@ -319,6 +372,7 @@ func (db *DB) Get(attr *DbAttr) (*DbEntry, error) {
defer db.Close()
entry, err := db.txGet(attr)
if err != nil {
return nil, fmt.Errorf("failed to read from DB: %w", err)
}
@@ -364,11 +418,10 @@ func (db *DB) Import(attr *DbAttr) (string, error) {
}
var entries DbEntries
now := time.Now()
newfile := db.Dbfile + now.Format("-02.01.2006T03:04.05")
if err := json.Unmarshal(attr.Val, &entries); err != nil {
if err := json.Unmarshal([]byte(attr.Val), &entries); err != nil {
return "", cleanError(newfile, fmt.Errorf("failed to unmarshal json: %w", err))
}
@@ -378,6 +431,7 @@ func (db *DB) Import(attr *DbAttr) (string, error) {
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
@@ -408,7 +462,7 @@ func (db *DB) Import(attr *DbAttr) (string, error) {
}
// write meta data
err = bucket.Put([]byte(entry.GetKey()), pbentry)
err = bucket.Put([]byte(entry.Key), []byte(pbentry))
if err != nil {
return fmt.Errorf("failed to insert data into DB: %w", err)
}
@@ -420,7 +474,7 @@ func (db *DB) Import(attr *DbAttr) (string, error) {
}
// write value
err = databucket.Put([]byte(entry.GetKey()), []byte(entry.GetValue()))
err = databucket.Put([]byte(entry.Key), []byte(entry.Value))
if err != nil {
return fmt.Errorf("failed to insert data: %w", err)
}
@@ -428,6 +482,7 @@ func (db *DB) Import(attr *DbAttr) (string, error) {
return nil
})
if err != nil {
return "", cleanError(newfile, err)
}
@@ -465,14 +520,16 @@ func (db *DB) Info() (*DbInfo, error) {
}
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
@@ -502,7 +559,7 @@ func (db *DB) Getall(attr *DbAttr) (DbEntries, error) {
// get data sub bucket
databucket := root.Bucket([]byte("data"))
if databucket == nil {
return errors.New("failed to retrieve data sub bucket")
return fmt.Errorf("failed to retrieve data sub bucket")
}
slog.Debug("opened buckets", "root", root, "data", bucket)
@@ -515,7 +572,7 @@ func (db *DB) Getall(attr *DbAttr) (DbEntries, error) {
}
// retrieve the value from the data sub bucket
value := databucket.Get([]byte(entry.GetKey()))
value := databucket.Get([]byte(entry.Key))
// we need to make a copy of it, otherwise we'll get an
// "unexpected fault address" error
@@ -530,66 +587,5 @@ func (db *DB) Getall(attr *DbAttr) (DbEntries, error) {
return err
})
return entries, err
}
// internal DB getter, assumes db.DB has already been
// opened successfully. Do NOT call this w/o valid
// DB handle!
func (db *DB) txGet(attr *DbAttr) (*DbEntry, error) {
entry := DbEntry{}
err := db.DB.View(func(tx *bolt.Tx) error {
// root bucket
root := tx.Bucket([]byte(db.Bucket))
if root == nil {
return nil
}
// get meta sub bucket
bucket := root.Bucket([]byte("meta"))
if bucket == nil {
return nil
}
slog.Debug("opened buckets", "root", root, "data", bucket)
// retrieve meta data
pbentry := bucket.Get([]byte(attr.Key))
if pbentry == nil {
return fmt.Errorf("no such key: %s", attr.Key)
}
// put into struct
if err := proto.Unmarshal(pbentry, &entry); err != nil {
return fmt.Errorf("failed to unmarshal from protobuf: %w", err)
}
// get data sub bucket
databucket := root.Bucket([]byte("data"))
if databucket == nil {
return errors.New("failed to retrieve data sub bucket")
}
// retrieve actual data value
value := databucket.Get([]byte(attr.Key))
if len(value) == 0 {
return fmt.Errorf("no such key: %s", attr.Key)
}
// we need to make a copy of it, otherwise we'll get an
// "unexpected fault address" error
vc := make([]byte, len(value))
copy(vc, value)
entry.Value = string(vc)
return nil
})
if err != nil {
return nil, err
}
return &entry, nil
}

View File

@@ -1,5 +1,3 @@
package app
/*
Copyright © 2024 Thomas von Dein
@@ -16,24 +14,18 @@ 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 (
"errors"
"os"
)
import "os"
func cleanError(file string, err error) error {
// remove given [backup] file and forward the given error
oserr := os.Remove(file)
if oserr != nil {
return errors.Join(err, oserr)
}
return err
return os.Remove(file)
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if err != nil {
// return false on any error
return false

View File

@@ -1,7 +1,5 @@
package cfg
/*
Copyright © 2025-2026 Thomas von Dein
Copyright © 2025 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
@@ -16,22 +14,19 @@ 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"
"log/slog"
"os"
"runtime/debug"
"github.com/pelletier/go-toml"
"codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/common"
"github.com/lmittmann/tint"
"github.com/pelletier/go-toml/v2"
"github.com/tlinden/yadu"
)
var Version string = "v0.3.2"
var Version string = "v0.2.6"
type BucketConfig struct {
Encrypt bool
@@ -63,8 +58,6 @@ func (conf *Config) GetConfig(files []string) error {
}
}
conf.SetLogger()
return nil
}
@@ -84,7 +77,6 @@ func (conf *Config) ParseConfigFile(file string) error {
}
add := Config{}
err = toml.Unmarshal(data, &add)
if err != nil {
return fmt.Errorf("failed to unmarshall toml: %w", err)
@@ -122,35 +114,3 @@ func (conf *Config) ParseConfigFile(file string) error {
return nil
}
func (conf *Config) SetLogger() {
if conf.Debug {
buildInfo, _ := debug.ReadBuildInfo()
opts := &yadu.Options{
Level: slog.LevelDebug,
AddSource: true,
}
slog.SetLogLoggerLevel(slog.LevelDebug)
handler := yadu.NewHandler(os.Stdout, opts)
debuglogger := slog.New(handler).With(
slog.Group("program_info",
slog.Int("pid", os.Getpid()),
slog.String("go_version", buildInfo.GoVersion),
),
)
slog.SetDefault(debuglogger)
slog.Debug("parsed config", "conf", conf)
} else {
opts := &tint.Options{
Level: slog.LevelInfo,
AddSource: false,
}
logger := slog.New(tint.NewTextHandler(os.Stderr, opts))
slog.SetDefault(logger)
}
}

View File

@@ -1,7 +1,7 @@
package cmd
var manpage = `
NAME
anydb
anydb - a personal key value store
SYNOPSIS
@@ -179,7 +179,7 @@ SUBCOMMANDS
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 flexibility, it is detailed in the section TEMPLATES.
most flexibily, it is detailed in the section TEMPLATES.
list
The list subcommand displays a list of all database entries.
@@ -407,22 +407,12 @@ TEMPLATES
Each template operates on one or more entries, no loop construct is
required, the template provided applies to every matching entry
separateley.
separatley.
The following template variables can be used:
.Key - string
.Value - string
To be able to use the value in a template, you'll need to add the
"-s" flag so that values are included in output data stream.
Otherwise you can only use ".Preview".
Be careful if you use "-s" and ".Value" though: if a value is a
multiline string, it may clutter your output.
.Preview string
First line of entry value (if it is a string).
.Bin - []byte
.Created - timestamp.Time
To retrieve a string representation of the timestamp, use
@@ -514,7 +504,7 @@ REST API
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","data":"bar"}'
application/json' \ -d '{"key":"foo","val":"bar"}'
Retrieve the value:
@@ -524,10 +514,6 @@ REST API
curl localhost:8787/anydb/v1/
Delete an entry:
curl -s -X DELETE http://localhost:8787/anydb/v1/foo
BUGS
In order to report a bug, unexpected behavior, feature requests or to
submit a patch, please open an issue on github:

View File

@@ -1,5 +1,5 @@
/*
Copyright © 2024-2025 Thomas von Dein
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
@@ -21,10 +21,10 @@ import (
"os"
"strings"
"github.com/spf13/cobra"
"codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
"codeberg.org/scip/anydb/output"
"github.com/spf13/cobra"
)
func Set(conf *cfg.Config) *cobra.Command {
@@ -107,28 +107,24 @@ func Get(conf *cfg.Config) *cobra.Command {
attr.Key = args[0]
}
if err := conf.DB.Exists(); err != nil {
return err
}
entry, err := conf.DB.Get(&attr)
if err != nil {
return err
}
if entry.GetEncrypted() {
if entry.Encrypted {
pass, err := getPassword()
if err != nil {
return err
}
clear, err := app.Decrypt(pass, []byte(entry.GetValue()))
clear, err := app.Decrypt(pass, []byte(entry.Value))
if err != nil {
return err
}
entry.Value = string(clear)
entry.Size = uint64(len(entry.GetValue()))
entry.Size = uint64(len(entry.Value))
entry.Encrypted = false
}
@@ -137,8 +133,7 @@ func Get(conf *cfg.Config) *cobra.Command {
}
cmd.PersistentFlags().StringVarP(&attr.File, "output", "o", "", "output value to file (ignores -m)")
cmd.PersistentFlags().StringVarP(&conf.Mode, "mode", "m", "",
"output format (simple|wide|json|template) (default 'simple')")
cmd.PersistentFlags().StringVarP(&conf.Mode, "mode", "m", "", "output format (simple|wide|json|template) (default 'simple')")
cmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "n", false, "omit headers in tables")
cmd.PersistentFlags().BoolVarP(&conf.NoHumanize, "no-human", "N", false, "do not translate to human readable values")
cmd.PersistentFlags().StringVarP(&conf.Template, "template", "T", "", "go template for '-m template'")
@@ -212,10 +207,6 @@ func List(conf *cfg.Config) *cobra.Command {
conf.Mode = "wide"
}
if err := conf.DB.Exists(); err != nil {
return err
}
entries, err := conf.DB.List(&attr, conf.Fulltext)
if err != nil {
return err
@@ -225,8 +216,7 @@ func List(conf *cfg.Config) *cobra.Command {
},
}
cmd.PersistentFlags().StringVarP(&conf.Mode, "mode", "m", "",
"output format (table|wide|json|template), wide is a verbose table. (default 'table')")
cmd.PersistentFlags().StringVarP(&conf.Mode, "mode", "m", "", "output format (table|wide|json|template), wide is a verbose table. (default 'table')")
cmd.PersistentFlags().StringVarP(&conf.Template, "template", "T", "", "go template for '-m template'")
cmd.PersistentFlags().BoolVarP(&wide, "wide-output", "l", false, "output mode: wide")
cmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "n", false, "omit headers in tables")

View File

@@ -1,5 +1,3 @@
package cmd
/*
Copyright © 2024 Thomas von Dein
@@ -16,6 +14,7 @@ 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"
@@ -26,11 +25,11 @@ import (
"os"
"os/exec"
"github.com/spf13/cobra"
"codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
"codeberg.org/scip/anydb/output"
"codeberg.org/scip/anydb/rest"
"github.com/spf13/cobra"
)
func Export(conf *cfg.Config) *cobra.Command {
@@ -58,7 +57,6 @@ func Export(conf *cfg.Config) *cobra.Command {
}
cmd.PersistentFlags().StringVarP(&attr.File, "output-file", "o", "", "filename or - for STDIN")
if err := cmd.MarkPersistentFlagRequired("output-file"); err != nil {
panic(err)
}
@@ -88,14 +86,12 @@ func Import(conf *cfg.Config) *cobra.Command {
}
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)
}
@@ -128,6 +124,7 @@ func Man(conf *cfg.Config) *cobra.Command {
man.Stderr = os.Stderr
err := man.Run()
if err != nil {
return fmt.Errorf("failed to execute 'less': %w", err)
}
@@ -208,20 +205,19 @@ func Edit(conf *cfg.Config) *cobra.Command {
return err
}
if len(entry.GetValue()) == 0 && entry.GetBinary() {
if len(entry.Value) == 0 && entry.Binary {
return errors.New("key contains binary uneditable content")
}
// decrypt if needed
if entry.GetEncrypted() {
if entry.Encrypted {
pass, err := getPassword()
if err != nil {
return err
}
password = pass
clear, err := app.Decrypt(pass, []byte(entry.GetValue()))
clear, err := app.Decrypt(pass, []byte(entry.Value))
if err != nil {
return err
}
@@ -236,7 +232,7 @@ func Edit(conf *cfg.Config) *cobra.Command {
// 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, entry.GetValue())
newcontent, err := editContent(editor, string(entry.Value))
if err != nil {
return err
}
@@ -332,7 +328,7 @@ func editContent(editor string, content string) (string, error) {
newcontentstr := string(newcontent)
if content == newcontentstr {
return "", errors.New("content not modified, aborting")
return "", fmt.Errorf("content not modified, aborting")
}
return newcontentstr, nil

View File

@@ -24,9 +24,9 @@ import (
"path/filepath"
"runtime/debug"
"github.com/spf13/cobra"
"codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
"github.com/spf13/cobra"
"github.com/tlinden/yadu"
)
@@ -69,6 +69,7 @@ func Execute() {
Short: "anydb",
Long: `A personal key value store`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
var configs []string
if configfile != "" {
configs = []string{configfile}
@@ -109,14 +110,12 @@ func Execute() {
}
conf.DB = db
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if ShowVersion {
fmt.Printf("This is anydb version %s\n", cfg.Version)
return nil
}

View File

@@ -25,6 +25,7 @@ func CleanError(file string, err error) error {
func FileExists(filename string) bool {
info, err := os.Stat(filename)
if err != nil {
// return false on any error
return false

View File

@@ -9,15 +9,15 @@ type Slogger struct {
*slog.Logger
}
func (l Slogger) Debug(v ...any) {}
func (l Slogger) Debugf(format string, v ...any) { l.Logger.Debug(fmt.Sprintf(format, v...)) }
func (l Slogger) Error(v ...any) {}
func (l Slogger) Errorf(format string, v ...any) { l.Logger.Error(fmt.Sprintf(format, v...)) }
func (l Slogger) Info(v ...any) {}
func (l Slogger) Infof(format string, v ...any) { l.Logger.Info(fmt.Sprintf(format, v...)) }
func (l Slogger) Warning(v ...any) {}
func (l Slogger) Warningf(format string, v ...any) { l.Warn(fmt.Sprintf(format, v...)) }
func (l Slogger) Fatal(v ...any) {}
func (l Slogger) Fatalf(format string, v ...any) { l.Logger.Error(fmt.Sprintf(format, v...)) }
func (l Slogger) Panic(v ...any) {}
func (l Slogger) Panicf(format string, v ...any) { l.Logger.Error(fmt.Sprintf(format, v...)) }
func (l Slogger) Debug(v ...interface{}) {}
func (l Slogger) Debugf(format string, v ...interface{}) { l.Logger.Debug(fmt.Sprintf(format, v...)) }
func (l Slogger) Error(v ...interface{}) {}
func (l Slogger) Errorf(format string, v ...interface{}) { l.Logger.Error(fmt.Sprintf(format, v...)) }
func (l Slogger) Info(v ...interface{}) {}
func (l Slogger) Infof(format string, v ...interface{}) { l.Logger.Info(fmt.Sprintf(format, v...)) }
func (l Slogger) Warning(v ...interface{}) {}
func (l Slogger) Warningf(format string, v ...interface{}) { l.Logger.Warn(fmt.Sprintf(format, v...)) }
func (l Slogger) Fatal(v ...interface{}) {}
func (l Slogger) Fatalf(format string, v ...interface{}) { l.Logger.Error(fmt.Sprintf(format, v...)) }
func (l Slogger) Panic(v ...interface{}) {}
func (l Slogger) Panicf(format string, v ...interface{}) { l.Logger.Error(fmt.Sprintf(format, v...)) }

48
go.mod
View File

@@ -1,38 +1,40 @@
module codeberg.org/scip/anydb
go 1.26.0
go 1.24.0
toolchain go1.26.4
toolchain go1.24.1
require (
github.com/dustin/go-humanize v1.0.1
github.com/gofiber/fiber/v2 v2.52.9
github.com/inconshreveable/mousetrap v1.1.0
github.com/lmittmann/tint v1.2.0
github.com/olekukonko/tablewriter v1.1.4
github.com/pelletier/go-toml/v2 v2.4.3
github.com/rogpeppe/go-internal v1.15.0
github.com/spf13/cobra v1.10.2
github.com/olekukonko/tablewriter v1.1.0
github.com/pelletier/go-toml v1.9.5
github.com/rogpeppe/go-internal v1.14.1
github.com/spf13/cobra v1.10.1
github.com/tlinden/yadu v0.1.3
go.etcd.io/bbolt v1.5.0
go.etcd.io/bbolt v1.4.3
golang.org/x/crypto v0.42.0
golang.org/x/term v0.35.0
google.golang.org/protobuf v1.36.12
google.golang.org/protobuf v1.36.9
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.28 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect
github.com/olekukonko/ll v0.1.8 // indirect
github.com/spf13/pflag v1.0.10 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/tools v0.36.0 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/fatih/color v1.16.0 // 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/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.9 // 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.36.0 // indirect
golang.org/x/tools v0.26.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

101
go.sum
View File

@@ -1,71 +1,74 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/lmittmann/tint v1.2.0 h1:AogHRHy8HUJUnNJBHJlYa+fR4YY8mko2cnCp67xn9JY=
github.com/lmittmann/tint v1.2.0/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
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.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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY=
github.com/tlinden/yadu v0.1.3/go.mod h1:l3bRmHKL9zGAR6pnBHY2HRPxBecf7L74BoBgOOpTcUA=
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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.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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

24
main.go
View File

@@ -23,8 +23,8 @@ import (
"os"
"runtime"
"codeberg.org/scip/anydb/cmd"
"github.com/inconshreveable/mousetrap"
"codeberg.org/scip/anydb/cmd"
)
func main() {
@@ -34,13 +34,30 @@ func main() {
Main()
}
func init() {
// if we're running on Windows AND if the user double clicked the
// exe file from explorer, we tell them and then wait until any
// key has been hit, which will make the cmd window disappear and
// thus give the user time to read it.
if runtime.GOOS == "windows" {
if mousetrap.StartedByExplorer() {
fmt.Println("Do no double click 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)
}
}
}
}
func Main() int {
cmd.Execute()
return 0
}
// nolint gochecknoinits
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
@@ -52,7 +69,6 @@ func init() {
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

@@ -26,12 +26,12 @@ import (
"strings"
tpl "text/template"
"codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
"github.com/dustin/go-humanize"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
)
func List(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
@@ -54,7 +54,6 @@ func ListJson(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error {
}
fmt.Println(string(jsonentries))
return nil
}
@@ -68,7 +67,6 @@ func ListTemplate(writer io.Writer, conf *cfg.Config, entries app.DbEntries) err
for _, row := range entries {
buf.Reset()
err = tmpl.Execute(&buf, row)
if err != nil {
return fmt.Errorf("failed to execute output template: %w", err)
@@ -135,27 +133,28 @@ func ListTable(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error
case true:
if err :=
table.Append([]string{
row.GetKey(),
strings.Join(row.GetTags(), ","),
strconv.FormatUint(row.GetSize(), 10),
row.GetCreated().AsTime().Format("02.01.2006T03:04.05"),
row.GetPreview(),
row.Key,
strings.Join(row.Tags, ","),
strconv.FormatUint(row.Size, 10),
row.Created.AsTime().Format("02.01.2006T03:04.05"),
row.Preview,
}); err != nil {
return fmt.Errorf("failed to add data to table: %w", err)
}
default:
if err := table.Append([]string{
row.GetKey(),
strings.Join(row.GetTags(), ","),
humanize.Bytes(row.GetSize()),
humanize.Time(row.GetCreated().AsTime()),
row.GetPreview(),
row.Key,
strings.Join(row.Tags, ","),
humanize.Bytes(uint64(row.Size)),
humanize.Time(row.Created.AsTime()),
row.Preview,
}); err != nil {
return fmt.Errorf("failed to add data to table: %w", err)
}
}
} else {
if err := table.Append([]string{row.GetKey(), row.GetPreview()}); err != nil {
if err := table.Append([]string{row.Key, row.Preview}); err != nil {
return fmt.Errorf("failed to add data to table: %w", err)
}
}

View File

@@ -24,11 +24,11 @@ import (
"os"
"reflect"
"github.com/dustin/go-humanize"
"codeberg.org/scip/anydb/app"
"codeberg.org/scip/anydb/cfg"
"github.com/dustin/go-humanize"
"golang.org/x/term"
// "github.com/alecthomas/repr"
//"github.com/alecthomas/repr"
)
func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error {
@@ -40,18 +40,17 @@ func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEn
switch conf.Mode {
case "simple", "":
if entry.GetBinary() {
if entry.Binary {
if isatty {
fmt.Println("binary data omitted")
} else {
if _, err := os.Stdout.WriteString(entry.GetValue()); err != nil {
if _, err := os.Stdout.WriteString(entry.Value); err != nil {
return err
}
}
} else {
fmt.Print(entry.GetValue())
if entry.GetValue()[entry.GetSize()-1] != '\n' {
fmt.Print(string(entry.Value))
if entry.Value[entry.Size-1] != '\n' {
// always add a terminal newline
fmt.Println()
}
@@ -73,10 +72,8 @@ func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEn
}
func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEntry) error {
var (
fileHandle *os.File
err error
)
var fileHandle *os.File
var err error
if attr.File == "-" {
fileHandle = os.Stdout
@@ -95,10 +92,10 @@ func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.
}
// actually write file content
_, err = fileHandle.WriteString(entry.GetValue())
_, err = fileHandle.WriteString(entry.Value)
if !entry.GetBinary() {
if entry.GetValue()[entry.GetSize()-1] != '\n' {
if !entry.Binary {
if entry.Value[entry.Size-1] != '\n' {
// always add a terminal newline
_, err = fileHandle.Write([]byte{'\n'})
}
@@ -140,7 +137,7 @@ func Info(writer io.Writer, conf *cfg.Config, info *app.DbInfo) error {
if conf.Debug {
val := reflect.ValueOf(&bucket.Stats).Elem()
for i := range val.NumField() {
for i := 0; i < val.NumField(); i++ {
if _, err := fmt.Fprintf(writer, "%19s: %v\n", val.Type().Field(i).Name, val.Field(i)); err != nil {
return fmt.Errorf("failed to write output: %w", err)
}

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
Copyright © 2024-2025 Thomas von Dein
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
@@ -17,10 +17,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package rest
import (
"encoding/json"
"log"
"net/http"
"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"
"codeberg.org/scip/anydb/cfg"
)
@@ -31,61 +31,84 @@ type Result struct {
Code int `json:"code"`
}
const apiprefix = `/anydb/v1/`
func Runserver(conf *cfg.Config, args []string) error {
// setup api server
mux := http.NewServeMux()
router := SetupServer(conf)
// just in case someone tries to access the non-api url
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
Home(w)
// 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",
})
mux.HandleFunc("GET "+apiprefix+"{$}", func(w http.ResponseWriter, r *http.Request) {
RestList(w, r, conf)
})
router.Use(logger.New(logger.Config{
Format: "${pid} ${ip}:${port} ${status} - ${method} ${path}\n",
DisableColors: true,
}))
mux.HandleFunc("POST "+apiprefix+"{$}", func(w http.ResponseWriter, r *http.Request) {
RestList(w, r, conf)
})
router.Use(cors.New(cors.Config{
AllowMethods: "GET,PUT,POST,DELETE",
ExposeHeaders: "Content-Type,Accept",
}))
mux.HandleFunc("GET "+apiprefix+"{key}", func(w http.ResponseWriter, r *http.Request) {
key := r.PathValue("key")
RestGet(w, r, key, conf)
})
router.Use(compress.New(compress.Config{
Level: compress.LevelBestSpeed,
}))
mux.HandleFunc("DELETE "+apiprefix+"{key}", func(w http.ResponseWriter, r *http.Request) {
key := r.PathValue("key")
RestDelete(w, r, key, conf)
})
mux.HandleFunc("PUT "+apiprefix, func(w http.ResponseWriter, r *http.Request) {
RestSet(w, r, conf)
})
logger := LogHandler()
return http.ListenAndServe(conf.Listen, logger(mux))
return router
}
/*
Wrapper to respond with proper json status, message and code,
shall be prepared and called by the handlers directly.
*/
func JsonStatus(resp http.ResponseWriter, code int, msg string) {
success := code == http.StatusOK
func JsonStatus(c *fiber.Ctx, code int, msg string) error {
success := code == fiber.StatusOK
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(code)
err := json.NewEncoder(resp).Encode(
Result{
Code: code,
Message: msg,
Success: success,
})
if err != nil {
log.Fatal(err)
}
return c.Status(code).JSON(Result{
Code: code,
Message: msg,
Success: success,
})
}