commit 241f959b3a6eb571b9e80448eee02f48c27f37c5
parent cb0c458a294196bfe84f5312361ded8c9da50d56
Author: Sean Enck <sean@ttypty.com>
Date: Sat, 26 Jul 2025 13:22:09 -0400
requesting moves now does this all as a single action
Diffstat:
3 files changed, 106 insertions(+), 65 deletions(-)
diff --git a/internal/app/move.go b/internal/app/move.go
@@ -48,7 +48,7 @@ func Move(cmd CommandOptions) error {
return fmt.Errorf("multiple moves can only be done at a leaf level")
}
r := moveRequest{cmd: cmd, src: e.Path, dst: kdbx.NewPath(dir, kdbx.Base(e.Path)), overwrite: false}
- if err := r.do(true); err != nil {
+ if _, err := r.do(true); err != nil {
return err
}
requests = append(requests, r)
@@ -58,46 +58,51 @@ func Move(cmd CommandOptions) error {
if rCount == 0 {
return errors.New("no source entries matched")
}
+ var moving []kdbx.MoveRequest
for _, r := range requests {
- if err := r.do(false); err != nil {
+ req, err := r.do(false)
+ if err != nil {
return err
}
+ if req != nil {
+ moving = append(moving, *req)
+ }
}
- return nil
+ return t.Move(moving...)
}
-func (r moveRequest) do(dryRun bool) error {
+func (r moveRequest) do(dryRun bool) (*kdbx.MoveRequest, error) {
tx := r.cmd.Transaction()
if !dryRun {
use, err := kdbx.NewTransaction()
if err != nil {
- return err
+ return nil, err
}
tx = use
}
srcExists, err := tx.Get(r.src, kdbx.SecretValue)
if err != nil {
- return errors.New("unable to get source entry")
+ return nil, errors.New("unable to get source entry")
}
if srcExists == nil {
- return errors.New("no source object found")
+ return nil, errors.New("no source object found")
}
dstExists, err := tx.Get(r.dst, kdbx.BlankValue)
if err != nil {
- return errors.New("unable to get destination object")
+ return nil, errors.New("unable to get destination object")
}
if dstExists != nil {
if r.overwrite {
if !r.cmd.Confirm("overwrite destination") {
- return nil
+ return nil, nil
}
} else {
- return errors.New("unable to overwrite entries when moving multiple items")
+ return nil, errors.New("unable to overwrite entries when moving multiple items")
}
}
if dryRun {
- return nil
+ return nil, nil
}
- return tx.Move(srcExists, r.dst)
+ return &kdbx.MoveRequest{Source: srcExists, Destination: r.dst}, nil
}
diff --git a/internal/kdbx/actions.go b/internal/kdbx/actions.go
@@ -14,6 +14,25 @@ import (
type (
action func(t Context) error
+
+ // MoveRequest allow for moving (or inserting) entities as actions
+ MoveRequest struct {
+ Source *Entity
+ Destination string
+ }
+
+ moveData struct {
+ src moveEntity
+ dst moveEntity
+ move bool
+ modTime time.Time
+ values map[string]string
+ }
+
+ moveEntity struct {
+ title string
+ offset []string
+ }
)
func (t *Transaction) act(cb action) error {
@@ -170,74 +189,88 @@ func findAndDo(isAdd bool, entityName string, offset []string, opEntity *gokeepa
return g, e, done
}
-// Move will move a src object to a dst location
-func (t *Transaction) Move(src *Entity, dst string) error {
- if src == nil {
- return errors.New("source entity is not set")
- }
- if strings.TrimSpace(src.Path) == "" {
- return errors.New("empty path not allowed")
- }
- if len(src.Values) == 0 {
- return errors.New("empty secrets not allowed")
+// Move will move (one or more) source objects to destination location
+func (t *Transaction) Move(moves ...MoveRequest) error {
+ if len(moves) == 0 {
+ return nil
}
- values := make(map[string]string)
- for k, v := range src.Values {
- found := false
- for _, mapping := range AllowedFields {
- if strings.EqualFold(k, mapping) {
- values[mapping] = v
- found = true
- break
+ var requests []moveData
+ for _, move := range moves {
+ if move.Source == nil {
+ return errors.New("source entity is not set")
+ }
+ if strings.TrimSpace(move.Source.Path) == "" {
+ return errors.New("empty path not allowed")
+ }
+ if len(move.Source.Values) == 0 {
+ return errors.New("empty secrets not allowed")
+ }
+ values := make(map[string]string)
+ for k, v := range move.Source.Values {
+ found := false
+ for _, mapping := range AllowedFields {
+ if strings.EqualFold(k, mapping) {
+ values[mapping] = v
+ found = true
+ break
+ }
+ }
+ if !found {
+ return fmt.Errorf("unknown entity field: %s", k)
+ }
+ }
+ mod := config.EnvDefaultModTime.Get()
+ modTime := time.Now()
+ if mod != "" {
+ p, err := time.Parse(config.ModTimeFormat, mod)
+ if err != nil {
+ return err
}
+ modTime = p
}
- if !found {
- return fmt.Errorf("unknown entity field: %s", k)
+ dOffset, dTitle, err := splitComponents(move.Destination)
+ if err != nil {
+ return err
}
- }
- mod := config.EnvDefaultModTime.Get()
- modTime := time.Now()
- if mod != "" {
- p, err := time.Parse(config.ModTimeFormat, mod)
+ sOffset, sTitle, err := splitComponents(move.Source.Path)
if err != nil {
return err
}
- modTime = p
+ sourceData := moveEntity{offset: sOffset, title: sTitle}
+ destData := moveEntity{offset: dOffset, title: dTitle}
+ requests = append(requests, moveData{src: sourceData, dst: destData, move: move.Destination != move.Source.Path, modTime: modTime, values: values})
}
- dOffset, dTitle, err := splitComponents(dst)
- if err != nil {
- return err
- }
- sOffset, sTitle, err := splitComponents(src.Path)
- if err != nil {
- return err
- }
- isMove := dst != src.Path
+ return t.doMoves(requests)
+}
+
+func (t *Transaction) doMoves(requests []moveData) error {
return t.change(func(c Context) error {
- c.removeEntity(sOffset, sTitle)
- if isMove {
- c.removeEntity(dOffset, dTitle)
- }
- e := gokeepasslib.NewEntry()
- e.Values = append(e.Values, value(titleKey, dTitle))
- e.Values = append(e.Values, value(modTimeKey, modTime.Format(time.RFC3339)))
- for k, v := range values {
- if k != NotesField && strings.Contains(v, "\n") {
- return fmt.Errorf("%s can NOT be multi-line", strings.ToLower(k))
+ for _, req := range requests {
+ c.removeEntity(req.src.offset, req.src.title)
+ if req.move {
+ c.removeEntity(req.dst.offset, req.dst.title)
}
- if k == OTPField {
- v = config.EnvTOTPFormat.Get(v)
+ e := gokeepasslib.NewEntry()
+ e.Values = append(e.Values, value(titleKey, req.dst.title))
+ e.Values = append(e.Values, value(modTimeKey, req.modTime.Format(time.RFC3339)))
+ for k, v := range req.values {
+ if k != NotesField && strings.Contains(v, "\n") {
+ return fmt.Errorf("%s can NOT be multi-line", strings.ToLower(k))
+ }
+ if k == OTPField {
+ v = config.EnvTOTPFormat.Get(v)
+ }
+ e.Values = append(e.Values, protectedValue(k, v))
}
- e.Values = append(e.Values, protectedValue(k, v))
+ c.alterEntities(true, req.dst.offset, req.dst.title, &e)
}
- c.alterEntities(true, dOffset, dTitle, &e)
return nil
})
}
// Insert is a move to the same location
func (t *Transaction) Insert(path string, val EntityValues) error {
- return t.Move(&Entity{Path: path, Values: val}, path)
+ return t.Move(MoveRequest{Source: &Entity{Path: path, Values: val}, Destination: path})
}
// Remove will remove a single entity
diff --git a/internal/kdbx/actions_test.go b/internal/kdbx/actions_test.go
@@ -83,10 +83,13 @@ func TestMove(t *testing.T) {
setup(t)
fullSetup(t, true).Insert(kdbx.NewPath("test", "test2", "test1"), map[string]string{"passworD": "pass"})
fullSetup(t, true).Insert(kdbx.NewPath("test", "test2", "test3"), map[string]string{"NoTES": "pass", "password": "xxx"})
- if err := fullSetup(t, true).Move(nil, ""); err == nil || err.Error() != "source entity is not set" {
+ if err := fullSetup(t, true).Move(); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if err := fullSetup(t, true).Move(kdbx.MoveRequest{nil, ""}); err == nil || err.Error() != "source entity is not set" {
t.Errorf("no error: %v", err)
}
- if err := fullSetup(t, true).Move(&kdbx.Entity{Path: kdbx.NewPath("test", "test2", "test3"), Values: map[string]string{"Notes": "abc"}}, kdbx.NewPath("test1", "test2", "test3")); err != nil {
+ if err := fullSetup(t, true).Move(kdbx.MoveRequest{&kdbx.Entity{Path: kdbx.NewPath("test", "test2", "test3"), Values: map[string]string{"Notes": "abc"}}, kdbx.NewPath("test1", "test2", "test3")}); err != nil {
t.Errorf("no error: %v", err)
}
q, err := fullSetup(t, true).Get(kdbx.NewPath("test1", "test2", "test3"), kdbx.SecretValue)
@@ -96,7 +99,7 @@ func TestMove(t *testing.T) {
if val, ok := q.Value("notes"); !ok || val != "abc" {
t.Errorf("invalid retrieval")
}
- if err := fullSetup(t, true).Move(&kdbx.Entity{Path: kdbx.NewPath("test", "test2", "test1"), Values: map[string]string{"password": "test"}}, kdbx.NewPath("test1", "test2", "test3")); err != nil {
+ if err := fullSetup(t, true).Move(kdbx.MoveRequest{&kdbx.Entity{Path: kdbx.NewPath("test", "test2", "test1"), Values: map[string]string{"password": "test"}}, kdbx.NewPath("test1", "test2", "test3")}); err != nil {
t.Errorf("no error: %v", err)
}
q, err = fullSetup(t, true).Get(kdbx.NewPath("test1", "test2", "test3"), kdbx.SecretValue)