mirror of
https://codeberg.org/scip/anydb.git
synced 2025-12-17 04:20:59 +01:00
add list command, fix set command
This commit is contained in:
91
app/attr.go
Normal file
91
app/attr.go
Normal 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
199
app/db.go
@@ -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
10
app/generic.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user