mirror of
https://codeberg.org/scip/anydb.git
synced 2026-10-08 11:14:20 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b66c57f4c | |||
| 38d3d2c442 | |||
| b4fa28d0c5 | |||
| 37c7e808ed |
6
.golangci.bck.yaml
Normal file
6
.golangci.bck.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
linters:
|
||||
exclusions:
|
||||
rules:
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "QF1008:"
|
||||
7
.golangci.yaml
Normal file
7
.golangci.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
version: "2"
|
||||
linters:
|
||||
exclusions:
|
||||
rules:
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "QF1008:"
|
||||
@@ -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
|
||||
|
||||
@@ -3,9 +3,6 @@ matrix:
|
||||
- linux/amd64
|
||||
goversion:
|
||||
- 1.24
|
||||
lintversion:
|
||||
- v2.12.2
|
||||
|
||||
|
||||
labels:
|
||||
platform: ${platform}
|
||||
@@ -24,7 +21,7 @@ steps:
|
||||
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]
|
||||
|
||||
@@ -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
|
||||
|
||||
8
Makefile
8
Makefile
@@ -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
|
||||
|
||||
28
anydb.1
28
anydb.1
@@ -133,12 +133,13 @@
|
||||
.\" ========================================================================
|
||||
.\"
|
||||
.IX Title "ANYDB 1"
|
||||
.TH ANYDB 1 "2026-08-31" "1" "User Commands"
|
||||
.TH ANYDB 1 "2025-11-03" "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
|
||||
|
||||
17
anydb.pod
17
anydb.pod
@@ -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
|
||||
|
||||
16
app/attr.go
16
app/attr.go
@@ -1,5 +1,3 @@
|
||||
package app
|
||||
|
||||
/*
|
||||
Copyright © 2024-2025 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 (
|
||||
"fmt"
|
||||
@@ -126,24 +125,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
182
app/db.go
182
app/db.go
@@ -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"
|
||||
@@ -114,7 +113,6 @@ func (db *DB) Open() error {
|
||||
}
|
||||
|
||||
db.DB = b
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -130,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
|
||||
@@ -162,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 {
|
||||
@@ -173,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)
|
||||
@@ -183,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
|
||||
}
|
||||
}
|
||||
@@ -238,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") {
|
||||
@@ -247,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,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))
|
||||
@@ -278,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)
|
||||
}
|
||||
@@ -290,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
|
||||
}
|
||||
@@ -304,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
|
||||
@@ -311,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)
|
||||
}
|
||||
@@ -356,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))
|
||||
}
|
||||
|
||||
@@ -370,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
|
||||
@@ -400,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)
|
||||
}
|
||||
@@ -412,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)
|
||||
}
|
||||
@@ -420,6 +482,7 @@ func (db *DB) Import(attr *DbAttr) (string, error) {
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", cleanError(newfile, err)
|
||||
}
|
||||
@@ -457,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
|
||||
@@ -494,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)
|
||||
@@ -507,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
|
||||
@@ -522,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
|
||||
}
|
||||
|
||||
16
app/io.go
16
app/io.go
@@ -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
|
||||
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
"github.com/tlinden/yadu"
|
||||
)
|
||||
|
||||
var Version string = "v0.3.1"
|
||||
var Version string = "v0.3.0"
|
||||
|
||||
type BucketConfig struct {
|
||||
Encrypt bool
|
||||
@@ -83,7 +83,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)
|
||||
|
||||
22
cmd/anydb.go
22
cmd/anydb.go
@@ -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:
|
||||
|
||||
12
cmd/crud.go
12
cmd/crud.go
@@ -112,19 +112,19 @@ func Get(conf *cfg.Config) *cobra.Command {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -133,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'")
|
||||
@@ -217,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")
|
||||
|
||||
20
cmd/extra.go
20
cmd/extra.go
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...)) }
|
||||
|
||||
24
main.go
24
main.go
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
package rest
|
||||
|
||||
import (
|
||||
// "github.com/alecthomas/repr"
|
||||
//"github.com/alecthomas/repr"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
@@ -28,7 +28,7 @@ import (
|
||||
)
|
||||
|
||||
type SetContext struct {
|
||||
Query string `form:"query" json:"query"`
|
||||
Query string `json:"query" form:"query"`
|
||||
}
|
||||
|
||||
type ListResponse struct {
|
||||
@@ -50,7 +50,6 @@ func RestList(resp http.ResponseWriter, req *http.Request, conf *cfg.Config) {
|
||||
if err != nil {
|
||||
if err.Error() != `EOF` {
|
||||
http.Error(resp, err.Error(), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -59,7 +58,6 @@ func RestList(resp http.ResponseWriter, req *http.Request, conf *cfg.Config) {
|
||||
entries, err := conf.DB.List(attr, attr.Fulltext)
|
||||
if err != nil {
|
||||
JsonStatus(resp, http.StatusForbidden, "Unable to list keys: "+err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,6 +69,7 @@ func RestList(resp http.ResponseWriter, req *http.Request, conf *cfg.Config) {
|
||||
Success: true,
|
||||
Entries: entries,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -79,7 +78,6 @@ func RestList(resp http.ResponseWriter, req *http.Request, conf *cfg.Config) {
|
||||
func RestGet(resp http.ResponseWriter, req *http.Request, key string, conf *cfg.Config) {
|
||||
if key == "" {
|
||||
JsonStatus(resp, http.StatusForbidden, "key not provided")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,13 +85,10 @@ func RestGet(resp http.ResponseWriter, req *http.Request, key string, conf *cfg.
|
||||
entry, err := conf.DB.Get(&app.DbAttr{Key: key})
|
||||
if err != nil {
|
||||
JsonStatus(resp, http.StatusForbidden, "Unable to get key: "+err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if entry.GetKey() == "" {
|
||||
if entry.Key == "" {
|
||||
JsonStatus(resp, http.StatusForbidden, "Key does not exist")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -105,6 +100,7 @@ func RestGet(resp http.ResponseWriter, req *http.Request, key string, conf *cfg.
|
||||
Success: true,
|
||||
Entry: entry,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -113,7 +109,6 @@ func RestGet(resp http.ResponseWriter, req *http.Request, key string, conf *cfg.
|
||||
func RestDelete(resp http.ResponseWriter, req *http.Request, key string, conf *cfg.Config) {
|
||||
if key == "" {
|
||||
JsonStatus(resp, http.StatusForbidden, "key not provided")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -121,7 +116,6 @@ func RestDelete(resp http.ResponseWriter, req *http.Request, key string, conf *c
|
||||
err := conf.DB.Del(&app.DbAttr{Key: key})
|
||||
if err != nil {
|
||||
JsonStatus(resp, http.StatusForbidden, "Unable to delete key: "+err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -134,7 +128,6 @@ func RestSet(resp http.ResponseWriter, req *http.Request, conf *cfg.Config) {
|
||||
err := json.NewDecoder(req.Body).Decode(&attr)
|
||||
if err != nil {
|
||||
http.Error(resp, err.Error(), http.StatusBadRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -148,7 +141,6 @@ func RestSet(resp http.ResponseWriter, req *http.Request, conf *cfg.Config) {
|
||||
err = conf.DB.Set(attr)
|
||||
if err != nil {
|
||||
JsonStatus(resp, http.StatusForbidden, "Unable to set key: "+err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
package rest
|
||||
|
||||
/*
|
||||
This logging middleware is based on
|
||||
|
||||
@@ -9,6 +7,7 @@ https://github.com/elithrar/admission-control/blob/v0.6.3/request_logger.go
|
||||
|
||||
I am using slog and added a couple of small modifications.
|
||||
*/
|
||||
package rest
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
@@ -21,7 +20,6 @@ import (
|
||||
// written HTTP status code to be captured for logging.
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
|
||||
status int
|
||||
size int
|
||||
wroteHeader bool
|
||||
@@ -50,6 +48,7 @@ func (rw *responseWriter) WriteHeader(code int) {
|
||||
}
|
||||
|
||||
func (rw *responseWriter) Write(data []byte) (int, error) {
|
||||
|
||||
written, err := rw.ResponseWriter.Write(data)
|
||||
rw.size += written
|
||||
|
||||
@@ -76,9 +75,7 @@ func LogHandler() func(http.Handler) http.Handler {
|
||||
next.ServeHTTP(wrapped, req)
|
||||
|
||||
header := wrapped.Header()["Content-Type"]
|
||||
|
||||
var contenttype string
|
||||
|
||||
contenttype := ""
|
||||
if header == nil {
|
||||
contenttype = "text/plain"
|
||||
} else {
|
||||
|
||||
@@ -85,6 +85,7 @@ func JsonStatus(resp http.ResponseWriter, code int, msg string) {
|
||||
Message: msg,
|
||||
Success: success,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user