add list command, fix set command

This commit is contained in:
2024-12-17 14:23:56 +01:00
parent d1d2328fcd
commit 9e6bbd5419
9 changed files with 381 additions and 31 deletions

91
app/attr.go Normal file
View File

@@ -0,0 +1,91 @@
package app
import (
"fmt"
"io"
"os"
"unicode/utf8"
)
type DbAttr struct {
Key string
Val string
Bin []byte
Args []string
Tags []string
File string
}
func (attr *DbAttr) ParseKV() error {
switch len(attr.Args) {
case 1:
// 1 arg = key + read from file or stdin
attr.Key = attr.Args[0]
if attr.File == "" {
attr.File = "-"
}
case 2:
attr.Key = attr.Args[0]
attr.Val = attr.Args[1]
if attr.Args[1] == "-" {
attr.File = "-"
}
}
if attr.File != "" {
return attr.GetFileValue()
}
return nil
}
func (attr *DbAttr) GetFileValue() error {
var fd io.Reader
if attr.File == "-" {
stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
fd = os.Stdin
}
} else {
filehandle, err := os.OpenFile(attr.File, os.O_RDONLY, 0600)
if err != nil {
return err
}
fd = filehandle
}
if fd != nil {
// read from file or stdin pipe
data, err := io.ReadAll(fd)
if err != nil {
return err
}
// poor man's text file test
sdata := string(data)
if utf8.ValidString(sdata) {
attr.Val = sdata
} else {
attr.Bin = data
}
} else {
// read from console stdin
var input string
var data string
for {
_, err := fmt.Scanln(&input)
if err != nil {
break
}
data += input + "\n"
}
attr.Val = data
}
return nil
}

199
app/db.go
View File

@@ -1,59 +1,206 @@
package app
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"time"
"github.com/asdine/storm/v3"
bolt "go.etcd.io/bbolt"
)
type DB struct {
Debug bool
DB *storm.DB
}
type DbAttr struct {
Key string
Args []string
Tags []string
File string
Debug bool
Dbfile string
DB *bolt.DB
}
type DbEntry struct {
ID int `storm:"id,increment"`
Key string `storm:"unique"`
Value string `storm:"index"` // FIXME: turn info []byte or add blob?
Tags []string `storm:"index"`
CreatedAt time.Time `storm:"index"`
Id string `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
Bin []byte `json:"bin"`
Tags []string `json:"tags"`
Created time.Time `json:"created"`
}
type DbEntries []DbEntry
type DbTag struct {
Keys []string `json:"key"`
}
const BucketData string = "data"
const BucketTags string = "tags"
func New(file string, debug bool) (*DB, error) {
if _, err := os.Stat(filepath.Dir(file)); os.IsNotExist(err) {
os.MkdirAll(filepath.Dir(file), 0700)
}
db, err := storm.Open(file)
if err != nil {
return nil, err
}
// FIXME: defer db.Close() here leads to: Error: database not open
return &DB{Debug: debug, Dbfile: file}, nil
}
return &DB{Debug: debug, DB: db}, nil
func (db *DB) Open() error {
b, err := bolt.Open(db.Dbfile, 0600, nil)
if err != nil {
return err
}
db.DB = b
return nil
}
func (db *DB) Close() error {
return db.DB.Close()
}
func (db *DB) Set(attr *DbAttr) error {
entry := DbEntry{Key: attr.Key, Tags: attr.Tags}
func (db *DB) List(attr *DbAttr) (DbEntries, error) {
if err := db.Open(); err != nil {
return nil, err
}
defer db.Close()
var entries DbEntries
var filter *regexp.Regexp
if len(attr.Args) > 0 {
entry.Value = attr.Args[0]
filter = regexp.MustCompile(attr.Args[0])
}
// FIXME: check attr.File or STDIN
err := db.DB.View(func(tx *bolt.Tx) error {
return db.DB.Save(&entry)
bucket := tx.Bucket([]byte(BucketData))
if bucket == nil {
return nil
}
err := bucket.ForEach(func(key, jsonentry []byte) error {
var entry DbEntry
if err := json.Unmarshal(jsonentry, &entry); err != nil {
return fmt.Errorf("unable to unmarshal json: %s", err)
}
var include bool
switch {
case filter != nil:
if filter.MatchString(entry.Value) ||
filter.MatchString(entry.Key) ||
filter.MatchString(strings.Join(entry.Tags, " ")) {
include = true
}
case len(attr.Tags) > 0:
for _, search := range attr.Tags {
for _, tag := range entry.Tags {
if tag == search {
include = true
break
}
}
if include {
break
}
}
default:
include = true
}
if include {
entries = append(entries, entry)
}
return nil
})
return err
})
return entries, err
}
func (db *DB) Set(attr *DbAttr) error {
if err := db.Open(); err != nil {
return err
}
defer db.Close()
if err := attr.ParseKV(); err != nil {
return err
}
entry := DbEntry{
Key: attr.Key,
Value: attr.Val,
Bin: attr.Bin,
Tags: attr.Tags,
Created: time.Now(),
}
err := db.DB.Update(func(tx *bolt.Tx) error {
// insert data
bucket, err := tx.CreateBucketIfNotExists([]byte(BucketData))
if err != nil {
return fmt.Errorf("create bucket: %s", err)
}
jsonentry, err := json.Marshal(entry)
if err != nil {
return fmt.Errorf("json marshalling failure: %s", err)
}
err = bucket.Put([]byte(entry.Key), []byte(jsonentry))
if err != nil {
return fmt.Errorf("insert data: %s", err)
}
// insert tag, if any
// FIXME: check removed tags
if len(attr.Tags) > 0 {
bucket, err := tx.CreateBucketIfNotExists([]byte(BucketTags))
if err != nil {
return fmt.Errorf("create bucket: %s", err)
}
for _, tag := range entry.Tags {
dbtag := &DbTag{}
jsontag := bucket.Get([]byte(tag))
if jsontag == nil {
// the tag is empty so far, initialize it
dbtag.Keys = []string{entry.Key}
} else {
if err := json.Unmarshal(jsontag, dbtag); err != nil {
return fmt.Errorf("unable to unmarshal json: %s", err)
}
if !slices.Contains(dbtag.Keys, entry.Key) {
// current key is not yet assigned to the tag, append it
dbtag.Keys = append(dbtag.Keys, entry.Key)
}
}
jsontag, err = json.Marshal(dbtag)
if err != nil {
return fmt.Errorf("json marshalling failure: %s", err)
}
err = bucket.Put([]byte(tag), []byte(jsontag))
if err != nil {
return fmt.Errorf("insert data: %s", err)
}
}
}
return nil
})
if err != nil {
return err
}
return nil
}

10
app/generic.go Normal file
View File

@@ -0,0 +1,10 @@
package app
// look if a key in a map exists, generic variant
func Exists[K comparable, V any](m map[K]V, v K) bool {
if _, ok := m[v]; ok {
return true
}
return false
}