From a4b6a3cfdf9e890cfc4772552cbe5d5a020414e5 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Sun, 29 Dec 2024 18:29:43 +0100 Subject: [PATCH] restructured data storage, values now have their own sub bucket --- TODO.md | 2 + app/attr.go | 43 ++++++++-- app/crypto.go | 33 +++----- app/db.go | 204 ++++++++++++++++++++++++++++++++++++---------- app/dbentry.pb.go | 66 +++++++++------ app/dbentry.proto | 15 ++-- cfg/config.go | 2 +- cmd/crud.go | 65 +++++++++++++-- cmd/extra.go | 14 +--- cmd/root.go | 1 + output/list.go | 11 +-- output/single.go | 25 +++--- 12 files changed, 330 insertions(+), 151 deletions(-) diff --git a/TODO.md b/TODO.md index 476114a..f13e0ca 100644 --- a/TODO.md +++ b/TODO.md @@ -33,3 +33,5 @@ behind. However, maybe change the list command to just list everything and add an extra find command for fulltext or tag search. Maybe still provide filter options in list command but only filter for keys. + +DONE: most of the above, except the tag stuff. manpage needs update and tests. diff --git a/app/attr.go b/app/attr.go index 7f09087..ead3293 100644 --- a/app/attr.go +++ b/app/attr.go @@ -20,17 +20,19 @@ import ( "fmt" "io" "os" + "strings" "unicode/utf8" ) type DbAttr struct { Key string - Val string - Bin []byte + Preview string + Val []byte Args []string Tags []string File string Encrypted bool + Binary bool } func (attr *DbAttr) ParseKV() error { @@ -43,7 +45,7 @@ func (attr *DbAttr) ParseKV() error { } case 2: attr.Key = attr.Args[0] - attr.Val = attr.Args[1] + attr.Val = []byte(attr.Args[1]) if attr.Args[1] == "-" { attr.File = "-" @@ -51,7 +53,29 @@ func (attr *DbAttr) ParseKV() error { } if attr.File != "" { - return attr.GetFileValue() + if err := attr.GetFileValue(); err != nil { + return err + } + } + + if attr.Binary { + attr.Preview = "" + } else { + if len(attr.Val) > MaxValueWidth { + attr.Preview = string(attr.Val)[0:MaxValueWidth] + "..." + + if strings.Contains(attr.Preview, "\n") { + parts := strings.Split(attr.Preview, "\n") + if len(parts) > 0 { + attr.Preview = parts[0] + } + } + } else { + attr.Preview = string(attr.Val) + } + } + if attr.Encrypted { + attr.Preview = "" } return nil @@ -82,11 +106,12 @@ func (attr *DbAttr) GetFileValue() error { } // poor man's text file test - sdata := string(data) - if utf8.ValidString(sdata) { - attr.Val = sdata + attr.Val = data + + if utf8.ValidString(string(data)) { + attr.Binary = false } else { - attr.Bin = data + attr.Binary = true } } else { // read from console stdin @@ -101,7 +126,7 @@ func (attr *DbAttr) GetFileValue() error { data += input + "\n" } - attr.Val = data + attr.Val = []byte(data) } return nil diff --git a/app/crypto.go b/app/crypto.go index c9ee9d9..4c65ea9 100644 --- a/app/crypto.go +++ b/app/crypto.go @@ -18,7 +18,6 @@ package app import ( "crypto/rand" - "encoding/base64" "errors" "fmt" "os" @@ -104,7 +103,7 @@ func GetRandom(size int, capacity int) ([]byte, error) { // modifying it. // // The cipher text consists of: -// base64(password-salt) + base64(12 byte nonce + ciphertext + 16 byte mac) +// password-salt) + (12 byte nonce + ciphertext + 16 byte mac) func Encrypt(pass []byte, attr *DbAttr) error { key, err := DeriveKey(pass, nil) if err != nil { @@ -116,25 +115,17 @@ func Encrypt(pass []byte, attr *DbAttr) error { return fmt.Errorf("failed to create AEAD cipher: %w", err) } - var plain []byte - if attr.Val != "" { - plain = []byte(attr.Val) - } else { - plain = attr.Bin - } - - total := aead.NonceSize() + len(plain) + aead.Overhead() + total := aead.NonceSize() + len(attr.Val) + aead.Overhead() nonce, err := GetRandom(aead.NonceSize(), total) if err != nil { return err } - cipher := aead.Seal(nonce, nonce, plain, nil) + cipher := aead.Seal(nonce, nonce, attr.Val, nil) - attr.Bin = nil - attr.Val = base64.RawStdEncoding.EncodeToString(key.Salt) + - base64.RawStdEncoding.EncodeToString(cipher) + attr.Val = append(attr.Val, key.Salt...) + attr.Val = append(attr.Val, cipher...) attr.Encrypted = true @@ -142,21 +133,17 @@ func Encrypt(pass []byte, attr *DbAttr) error { } // Do the reverse -func Decrypt(pass []byte, cipherb64 string) ([]byte, error) { - salt, err := base64.RawStdEncoding.Strict().DecodeString(cipherb64[0:B64SaltLen]) - if err != nil { - return nil, fmt.Errorf("failed to encode to base64: %w", err) +func Decrypt(pass []byte, cipherb []byte) ([]byte, error) { + if len(cipherb) < B64SaltLen { + return nil, fmt.Errorf("encrypted cipher block too small") } - key, err := DeriveKey(pass, salt) + key, err := DeriveKey(pass, cipherb[0:B64SaltLen]) if err != nil { return nil, err } - cipher, err := base64.RawStdEncoding.Strict().DecodeString(cipherb64[B64SaltLen:]) - if err != nil { - return nil, fmt.Errorf("failed to encode to base64: %w", err) - } + cipher := cipherb[B64SaltLen:] aead, err := chacha20poly1305.New(key.Key) if err != nil { diff --git a/app/db.go b/app/db.go index ad75367..e18d766 100644 --- a/app/db.go +++ b/app/db.go @@ -53,32 +53,6 @@ type DbInfo struct { Path string } -// Post process an entry for list output. -// Do NOT call it during write processing! -func (entry *DbEntry) Normalize() { - entry.Size = uint64(len(entry.Value)) - - if entry.Encrypted { - entry.Value = "" - } - - if len(entry.Bin) > 0 { - entry.Value = "" - entry.Size = uint64(len(entry.Bin)) - } - - if strings.Contains(entry.Value, "\n") { - parts := strings.Split(entry.Value, "\n") - if len(parts) > 0 { - entry.Value = parts[0] - } - } - - if len(entry.Value) > MaxValueWidth { - entry.Value = entry.Value[0:MaxValueWidth] + "..." - } -} - type DbEntries []DbEntry type DbTag struct { @@ -126,7 +100,12 @@ func (db *DB) List(attr *DbAttr) (DbEntries, error) { err := db.DB.View(func(tx *bolt.Tx) error { - bucket := tx.Bucket([]byte(db.Bucket)) + root := tx.Bucket([]byte(db.Bucket)) + if root == nil { + return nil + } + + bucket := root.Bucket([]byte("meta")) if bucket == nil { return nil } @@ -141,8 +120,7 @@ func (db *DB) List(attr *DbAttr) (DbEntries, error) { switch { case filter != nil: - if filter.MatchString(entry.Value) || - filter.MatchString(entry.Key) || + if filter.MatchString(entry.Key) || filter.MatchString(strings.Join(entry.Tags, " ")) { include = true } @@ -183,18 +161,24 @@ func (db *DB) Set(attr *DbAttr) error { entry := DbEntry{ Key: attr.Key, - Value: attr.Val, - Bin: attr.Bin, + Binary: attr.Binary, Tags: attr.Tags, Encrypted: attr.Encrypted, Created: timestamppb.Now(), + Size: uint64(len(attr.Val)), + Preview: attr.Preview, } // check if the entry already exists and if yes, check if it has // any tags. if so, we initialize our update struct with these // tags unless it has new tags configured. err := db.DB.View(func(tx *bolt.Tx) error { - bucket := tx.Bucket([]byte(db.Bucket)) + root := tx.Bucket([]byte(db.Bucket)) + if root == nil { + return nil + } + + bucket := root.Bucket([]byte("meta")) if bucket == nil { return nil } @@ -221,23 +205,43 @@ func (db *DB) Set(attr *DbAttr) error { return err } + // marshall our data + pbentry, err := proto.Marshal(&entry) + if err != nil { + return fmt.Errorf("failed to marshall protobuf: %w", err) + } + err = db.DB.Update(func(tx *bolt.Tx) error { - // insert data - bucket, err := tx.CreateBucketIfNotExists([]byte(db.Bucket)) + // create root bucket + root, err := tx.CreateBucketIfNotExists([]byte(db.Bucket)) if err != nil { return fmt.Errorf("failed to create DB bucket: %w", err) } - pbentry, err := proto.Marshal(&entry) + // create meta bucket + bucket, err := root.CreateBucketIfNotExists([]byte("meta")) if err != nil { - return fmt.Errorf("failed to marshall protobuf: %w", err) + return fmt.Errorf("failed to create DB meta sub bucket: %w", err) } + // write meta data err = bucket.Put([]byte(entry.Key), []byte(pbentry)) if err != nil { return fmt.Errorf("failed to insert data: %w", err) } + // create data bucket + databucket, err := root.CreateBucketIfNotExists([]byte("data")) + if err != nil { + return fmt.Errorf("failed to create DB data sub bucket: %w", err) + } + + // write value + err = databucket.Put([]byte(entry.Key), attr.Val) + if err != nil { + return fmt.Errorf("failed to insert data: %w", err) + } + return nil }) @@ -257,21 +261,35 @@ func (db *DB) Get(attr *DbAttr) (*DbEntry, error) { entry := DbEntry{} err := db.DB.View(func(tx *bolt.Tx) error { - bucket := tx.Bucket([]byte(db.Bucket)) + root := tx.Bucket([]byte(db.Bucket)) + if root == nil { + return nil + } + + bucket := root.Bucket([]byte("meta")) if bucket == nil { return nil } pbentry := bucket.Get([]byte(attr.Key)) if pbentry == nil { - // FIXME: shall we return a key not found error? - return nil + return fmt.Errorf("no such key: %s", attr.Key) } if err := proto.Unmarshal(pbentry, &entry); err != nil { return fmt.Errorf("failed to unmarshal from protobuf: %w", err) } + databucket := root.Bucket([]byte("data")) + if databucket == nil { + return fmt.Errorf("failed to retrieve data sub bucket") + } + + entry.Value = databucket.Get([]byte(attr.Key)) + if len(entry.Value) == 0 { + return fmt.Errorf("no such key: %s", attr.Key) + } + return nil }) @@ -308,7 +326,7 @@ func (db *DB) Import(attr *DbAttr) (string, error) { return "", err } - if attr.Val == "" { + if len(attr.Val) == 0 { return "", errors.New("empty json file") } @@ -336,10 +354,16 @@ func (db *DB) Import(attr *DbAttr) (string, error) { defer db.Close() err := db.DB.Update(func(tx *bolt.Tx) error { - // insert data - bucket, err := tx.CreateBucketIfNotExists([]byte(db.Bucket)) + // create root bucket + root, err := tx.CreateBucketIfNotExists([]byte(db.Bucket)) if err != nil { - return fmt.Errorf("failed to create bucket: %w", err) + return fmt.Errorf("failed to create DB bucket: %w", err) + } + + // create meta bucket + bucket, err := root.CreateBucketIfNotExists([]byte("meta")) + if err != nil { + return fmt.Errorf("failed to create DB meta sub bucket: %w", err) } for _, entry := range entries { @@ -348,10 +372,23 @@ func (db *DB) Import(attr *DbAttr) (string, error) { return fmt.Errorf("failed to marshall protobuf: %w", err) } + // write meta data err = bucket.Put([]byte(entry.Key), []byte(pbentry)) if err != nil { return fmt.Errorf("failed to insert data into DB: %w", err) } + + // create data bucket + databucket, err := root.CreateBucketIfNotExists([]byte("data")) + if err != nil { + return fmt.Errorf("failed to create DB data sub bucket: %w", err) + } + + // write value + err = databucket.Put([]byte(entry.Key), entry.Value) + if err != nil { + return fmt.Errorf("failed to insert data: %w", err) + } } return nil @@ -408,3 +445,84 @@ func (db *DB) Info() (*DbInfo, error) { return info, err } + +func (db *DB) Find(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 { + filter = regexp.MustCompile(attr.Args[0]) + } + + err := db.DB.View(func(tx *bolt.Tx) error { + + root := tx.Bucket([]byte(db.Bucket)) + if root == nil { + return nil + } + + bucket := root.Bucket([]byte("meta")) + if bucket == nil { + return nil + } + + databucket := root.Bucket([]byte("data")) + if databucket == nil { + return fmt.Errorf("failed to retrieve data sub bucket") + } + + err := bucket.ForEach(func(key, pbentry []byte) error { + var entry DbEntry + if err := proto.Unmarshal(pbentry, &entry); err != nil { + return fmt.Errorf("failed to unmarshal from protobuf: %w", err) + } + + entry.Value = databucket.Get([]byte(entry.Key)) + + var include bool + + switch { + case filter != nil: + if filter.MatchString(entry.Key) || + filter.MatchString(strings.Join(entry.Tags, " ")) { + include = true + } + + if !entry.Binary && !include { + if filter.MatchString(string(entry.Value)) { + include = true + } + } + case len(attr.Tags) > 0: + for _, search := range attr.Tags { + for _, tag := range entry.Tags { + if tag == search { + include = true + break + } + } + + if include { + break + } + } + default: + include = true + } + + if include { + entries = append(entries, entry) + } + + return nil + }) + + return err + }) + return entries, err +} diff --git a/app/dbentry.pb.go b/app/dbentry.pb.go index 06f7469..61642fc 100644 --- a/app/dbentry.pb.go +++ b/app/dbentry.pb.go @@ -1,3 +1,5 @@ +// -*-c++-*- + // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.1 @@ -25,12 +27,13 @@ type DbEntry struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=Id,proto3" json:"Id,omitempty"` Key string `protobuf:"bytes,2,opt,name=Key,proto3" json:"Key,omitempty"` - Value string `protobuf:"bytes,3,opt,name=Value,proto3" json:"Value,omitempty"` - Bin []byte `protobuf:"bytes,4,opt,name=Bin,proto3" json:"Bin,omitempty"` - Tags []string `protobuf:"bytes,5,rep,name=Tags,proto3" json:"Tags,omitempty"` - Created *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=Created,proto3" json:"Created,omitempty"` - Size uint64 `protobuf:"varint,7,opt,name=Size,proto3" json:"Size,omitempty"` - Encrypted bool `protobuf:"varint,8,opt,name=Encrypted,proto3" json:"Encrypted,omitempty"` + Preview string `protobuf:"bytes,3,opt,name=Preview,proto3" json:"Preview,omitempty"` + Tags []string `protobuf:"bytes,4,rep,name=Tags,proto3" json:"Tags,omitempty"` + Created *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=Created,proto3" json:"Created,omitempty"` + Size uint64 `protobuf:"varint,6,opt,name=Size,proto3" json:"Size,omitempty"` + Encrypted bool `protobuf:"varint,7,opt,name=Encrypted,proto3" json:"Encrypted,omitempty"` + Binary bool `protobuf:"varint,8,opt,name=Binary,proto3" json:"Binary,omitempty"` + Value []byte `protobuf:"bytes,9,opt,name=Value,proto3" json:"Value,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -79,20 +82,13 @@ func (x *DbEntry) GetKey() string { return "" } -func (x *DbEntry) GetValue() string { +func (x *DbEntry) GetPreview() string { if x != nil { - return x.Value + return x.Preview } return "" } -func (x *DbEntry) GetBin() []byte { - if x != nil { - return x.Bin - } - return nil -} - func (x *DbEntry) GetTags() []string { if x != nil { return x.Tags @@ -121,26 +117,42 @@ func (x *DbEntry) GetEncrypted() bool { return false } +func (x *DbEntry) GetBinary() bool { + if x != nil { + return x.Binary + } + return false +} + +func (x *DbEntry) GetValue() []byte { + if x != nil { + return x.Value + } + return nil +} + var File_app_dbentry_proto protoreflect.FileDescriptor var file_app_dbentry_proto_rawDesc = []byte{ 0x0a, 0x11, 0x61, 0x70, 0x70, 0x2f, 0x64, 0x62, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x70, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xcf, 0x01, 0x0a, 0x07, 0x44, 0x62, + 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xef, 0x01, 0x0a, 0x07, 0x44, 0x62, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x10, 0x0a, - 0x03, 0x42, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x42, 0x69, 0x6e, 0x12, - 0x12, 0x0a, 0x04, 0x54, 0x61, 0x67, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x54, - 0x61, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x07, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x52, 0x07, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x69, 0x7a, - 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1c, 0x0a, - 0x09, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x09, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x42, 0x1e, 0x5a, 0x1c, 0x67, + 0x28, 0x09, 0x52, 0x03, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x72, 0x65, 0x76, 0x69, + 0x65, 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x50, 0x72, 0x65, 0x76, 0x69, 0x65, + 0x77, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x61, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x04, 0x54, 0x61, 0x67, 0x73, 0x12, 0x34, 0x0a, 0x07, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x07, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x53, + 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x53, 0x69, 0x7a, 0x65, 0x12, + 0x1c, 0x0a, 0x09, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x12, 0x16, 0x0a, + 0x06, 0x42, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x42, + 0x69, 0x6e, 0x61, 0x72, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x1e, 0x5a, 0x1c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x6c, 0x69, 0x6e, 0x64, 0x65, 0x6e, 0x2f, 0x61, 0x6e, 0x79, 0x64, 0x62, 0x2f, 0x61, 0x70, 0x70, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, diff --git a/app/dbentry.proto b/app/dbentry.proto index 8904334..fa8d113 100644 --- a/app/dbentry.proto +++ b/app/dbentry.proto @@ -1,3 +1,5 @@ +// -*-c++-*- + syntax = "proto3"; package app; @@ -8,10 +10,11 @@ option go_package = "github.com/tlinden/anydb/app"; message DbEntry { string Id = 1; string Key = 2; - string Value = 3; - bytes Bin = 4; - repeated string Tags = 5; - google.protobuf.Timestamp Created = 6; - uint64 Size = 7; - bool Encrypted = 8; + string Preview = 3; + repeated string Tags = 4; + google.protobuf.Timestamp Created = 5; + uint64 Size = 6; + bool Encrypted = 7; + bool Binary = 8; + bytes Value = 9; } diff --git a/cfg/config.go b/cfg/config.go index 47ff633..e932ed6 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -26,7 +26,7 @@ import ( "github.com/tlinden/anydb/common" ) -var Version string = "v0.0.7" +var Version string = "v0.1.0" type BucketConfig struct { Encrypt bool diff --git a/cmd/crud.go b/cmd/crud.go index fb72b2e..d2b208a 100644 --- a/cmd/crud.go +++ b/cmd/crud.go @@ -20,7 +20,6 @@ import ( "errors" "os" "strings" - "unicode/utf8" "github.com/spf13/cobra" "github.com/tlinden/anydb/app" @@ -124,12 +123,7 @@ func Get(conf *cfg.Config) *cobra.Command { return err } - if utf8.ValidString(string(clear)) { - entry.Value = string(clear) - } else { - entry.Bin = clear - } - + entry.Value = clear entry.Encrypted = false } @@ -188,7 +182,7 @@ func List(conf *cfg.Config) *cobra.Command { ) var cmd = &cobra.Command{ - Use: "list [] [-t ] [-m ] [-n -N] [-T ] [-i]", + Use: "list [] [-m ] [-n -N] [-T ] [-i]", Short: "List database contents", Long: `List database contents`, RunE: func(cmd *cobra.Command, args []string) error { @@ -221,6 +215,58 @@ 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.Template, "template", "T", "", "go template for '-m template'") + cmd.PersistentFlags().BoolVarP(&wide, "wide-output", "l", false, "output mode: wide") + cmd.PersistentFlags().BoolVarP(&conf.NoHeaders, "no-headers", "n", false, "omit headers in tables") + cmd.PersistentFlags().BoolVarP(&conf.NoHumanize, "no-human", "N", false, "do not translate to human readable values") + cmd.PersistentFlags().BoolVarP(&conf.CaseInsensitive, "case-insensitive", "i", false, "filter case insensitive") + + cmd.Aliases = append(cmd.Aliases, "ls") + + return cmd +} + +func Find(conf *cfg.Config) *cobra.Command { + var ( + attr app.DbAttr + wide bool + ) + + var cmd = &cobra.Command{ + Use: "find | -t [-m ] [-n -N] [-T ] [-i]", + Short: "Find database contents", + Long: `Find database contents`, + RunE: func(cmd *cobra.Command, args []string) error { + // errors at this stage do not cause the usage to be shown + cmd.SilenceUsage = true + + if len(args) > 0 { + if conf.CaseInsensitive { + attr.Args = []string{"(?i)" + args[0]} + } else { + attr.Args = args + } + } + + // turn comma list into slice, if needed + if len(attr.Tags) == 1 && strings.Contains(attr.Tags[0], ",") { + attr.Tags = strings.Split(attr.Tags[0], ",") + } + + if wide { + conf.Mode = "wide" + } + + entries, err := conf.DB.Find(&attr) + if err != nil { + return err + } + + return output.List(os.Stdout, conf, entries) + }, + } + cmd.PersistentFlags().StringVarP(&conf.Mode, "mode", "m", "", "output format (table|wide|json|template), wide is a verbose table. (default 'table')") cmd.PersistentFlags().StringVarP(&conf.Template, "template", "T", "", "go template for '-m template'") cmd.PersistentFlags().BoolVarP(&wide, "wide-output", "l", false, "output mode: wide") @@ -230,7 +276,8 @@ func List(conf *cfg.Config) *cobra.Command { cmd.PersistentFlags().StringArrayVarP(&attr.Tags, "tags", "t", nil, "tags, multiple allowed") cmd.Aliases = append(cmd.Aliases, "/") - cmd.Aliases = append(cmd.Aliases, "ls") + cmd.Aliases = append(cmd.Aliases, "f") + cmd.Aliases = append(cmd.Aliases, "search") return cmd } diff --git a/cmd/extra.go b/cmd/extra.go index 8c889e0..795f06f 100644 --- a/cmd/extra.go +++ b/cmd/extra.go @@ -23,7 +23,6 @@ import ( "io" "os" "os/exec" - "unicode/utf8" "github.com/spf13/cobra" "github.com/tlinden/anydb/app" @@ -199,7 +198,7 @@ func Edit(conf *cfg.Config) *cobra.Command { return err } - if len(entry.Value) == 0 && len(entry.Bin) > 0 { + if len(entry.Value) == 0 && entry.Binary { return errors.New("key contains binary uneditable content") } @@ -216,12 +215,7 @@ func Edit(conf *cfg.Config) *cobra.Command { return err } - if utf8.ValidString(string(clear)) { - entry.Value = string(clear) - } else { - entry.Bin = clear - } - + entry.Value = clear entry.Encrypted = false } @@ -231,7 +225,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.Value) + newcontent, err := editContent(editor, string(entry.Value)) if err != nil { return err } @@ -241,7 +235,7 @@ func Edit(conf *cfg.Config) *cobra.Command { Key: attr.Key, Tags: attr.Tags, Encrypted: attr.Encrypted, - Val: newcontent, + Val: []byte(newcontent), } // encrypt if needed diff --git a/cmd/root.go b/cmd/root.go index c6d1f57..d448eb8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -122,6 +122,7 @@ func Execute() { // CRUD rootCmd.AddCommand(Set(&conf)) rootCmd.AddCommand(List(&conf)) + rootCmd.AddCommand(Find(&conf)) rootCmd.AddCommand(Get(&conf)) rootCmd.AddCommand(Del(&conf)) diff --git a/output/list.go b/output/list.go index 1d29efe..cc4e479 100644 --- a/output/list.go +++ b/output/list.go @@ -65,8 +65,6 @@ func ListTemplate(writer io.Writer, conf *cfg.Config, entries app.DbEntries) err buf := bytes.Buffer{} for _, row := range entries { - row.Normalize() - buf.Reset() err = tmpl.Execute(&buf, row) if err != nil { @@ -94,8 +92,6 @@ func ListTable(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error } for _, row := range entries { - row.Normalize() - if conf.Mode == "wide" { switch conf.NoHumanize { case true: @@ -104,21 +100,20 @@ func ListTable(writer io.Writer, conf *cfg.Config, entries app.DbEntries) error strings.Join(row.Tags, ","), strconv.FormatUint(row.Size, 10), row.Created.AsTime().Format("02.01.2006T03:04.05"), - row.Value, + row.Preview, }) default: table.Append([]string{ row.Key, strings.Join(row.Tags, ","), humanize.Bytes(uint64(row.Size)), - //row.Created.Format("02.01.2006T03:04.05"), humanize.Time(row.Created.AsTime()), - row.Value, + row.Preview, }) } } else { - table.Append([]string{row.Key, row.Value}) + table.Append([]string{row.Key, row.Preview}) } } diff --git a/output/single.go b/output/single.go index 7294f83..2e08c45 100644 --- a/output/single.go +++ b/output/single.go @@ -22,7 +22,6 @@ import ( "io" "os" "reflect" - "strings" "github.com/dustin/go-humanize" "github.com/tlinden/anydb/app" @@ -40,16 +39,15 @@ func Print(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app.DbEn switch conf.Mode { case "simple", "": - if len(entry.Bin) > 0 { + if entry.Binary { if isatty { fmt.Println("binary data omitted") } else { - os.Stdout.Write(entry.Bin) + os.Stdout.Write(entry.Value) } } else { - fmt.Print(entry.Value) - - if !strings.HasSuffix(entry.Value, "\n") { + fmt.Print(string(entry.Value)) + if entry.Value[entry.Size-1] != '\n' { // always add a terminal newline fmt.Println() } @@ -87,17 +85,14 @@ func WriteFile(writer io.Writer, conf *cfg.Config, attr *app.DbAttr, entry *app. fileHandle = fd } - if len(entry.Bin) > 0 { + if entry.Binary { // binary file content - _, err = fileHandle.Write(entry.Bin) - } else { - val := entry.Value - if !strings.HasSuffix(val, "\n") { - // always add a terminal newline - val += "\n" - } + _, err = fileHandle.Write(entry.Value) - _, err = fileHandle.Write([]byte(val)) + if entry.Value[entry.Size-1] != '\n' { + // always add a terminal newline + _, err = fileHandle.Write([]byte{'\n'}) + } } if err != nil {