summaryrefslogtreecommitdiff
path: root/internal/cli/kdbx_store.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-17 08:45:18 +0300
committerPaul Buetow <paul@buetow.org>2026-04-17 08:45:18 +0300
commiteb89e32c6675d4ed50f66580346e5ea8f3d7b5b2 (patch)
tree916d5299642622d3344778222e88db16aeace1ae /internal/cli/kdbx_store.go
parent9e2bf4af8b7b3b4ca2980aa6285482e7db0cd151 (diff)
refactor: deduplicate cli/kdbx_store.go using exported keepass helpers (tasks k4+l4)
Remove ~90 lines of duplicated code from kdbx_store.go by delegating to EnsureGroup, UpsertEntryByTitle, SetEntryField, SplitDescriptionPath, SanitizeRelativePath exported from internal/keepass. kdbxStore.Save() now delegates to keepass.AtomicSave, eliminating the double-close risk. migrate_kdbx.go updated to use the exported helpers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/cli/kdbx_store.go')
-rw-r--r--internal/cli/kdbx_store.go131
1 files changed, 22 insertions, 109 deletions
diff --git a/internal/cli/kdbx_store.go b/internal/cli/kdbx_store.go
index 86c4f9a..bdaeb31 100644
--- a/internal/cli/kdbx_store.go
+++ b/internal/cli/kdbx_store.go
@@ -3,10 +3,10 @@ package cli
import (
"fmt"
"os"
- "path/filepath"
- "strings"
gokeepasslib "github.com/tobischo/gokeepasslib/v3"
+
+ "codeberg.org/snonux/foostore/internal/keepass"
)
// KDBXStore is the minimal interface needed by migrate-kdbx.
@@ -56,127 +56,40 @@ func OpenKDBXStore(dbPath, password string) (KDBXStore, error) {
}, nil
}
+// UpsertTextEntry creates or updates a text entry in groupPath with the given
+// title, password, and notes. Delegates field manipulation to keepass.SetEntryField
+// and group navigation to keepass.EnsureGroup to avoid duplication.
func (s *kdbxStore) UpsertTextEntry(groupPath []string, title, password, notes string) (bool, error) {
- g := s.ensureGroup(groupPath)
- entry, overwrote := upsertEntryByTitle(g, title)
- setEntryField(entry, "Title", title)
- setEntryField(entry, "Password", password)
- setEntryField(entry, "Notes", notes)
+ g := keepass.EnsureGroup(&s.db.Content.Root.Groups[0], groupPath)
+ entry, overwrote := keepass.UpsertEntryByTitle(g, title)
+ keepass.SetEntryField(entry, "Title", title)
+ keepass.SetEntryField(entry, "Password", password)
+ keepass.SetEntryField(entry, "Notes", notes)
return overwrote, nil
}
+// UpsertBinaryEntry creates or updates a binary attachment entry in groupPath.
+// Delegates field manipulation to keepass.SetEntryField and group navigation
+// to keepass.EnsureGroup to avoid duplication.
func (s *kdbxStore) UpsertBinaryEntry(groupPath []string, title, filename string, content []byte) (bool, error) {
- g := s.ensureGroup(groupPath)
- entry, overwrote := upsertEntryByTitle(g, title)
- setEntryField(entry, "Title", title)
- setEntryField(entry, "Password", "")
+ g := keepass.EnsureGroup(&s.db.Content.Root.Groups[0], groupPath)
+ entry, overwrote := keepass.UpsertEntryByTitle(g, title)
+ keepass.SetEntryField(entry, "Title", title)
+ keepass.SetEntryField(entry, "Password", "")
b := s.db.AddBinary(content)
entry.Binaries = []gokeepasslib.BinaryReference{b.CreateReference(filename)}
// Keep notes concise for binary-only entries.
- setEntryField(entry, "Notes", fmt.Sprintf("Migrated binary attachment: %s", filename))
+ keepass.SetEntryField(entry, "Notes", fmt.Sprintf("Migrated binary attachment: %s", filename))
return overwrote, nil
}
-func (s *kdbxStore) ensureGroup(groupPath []string) *gokeepasslib.Group {
- g := &s.db.Content.Root.Groups[0]
- for _, segment := range groupPath {
- if segment == "" {
- continue
- }
- found := -1
- for i := range g.Groups {
- if g.Groups[i].Name == segment {
- found = i
- break
- }
- }
- if found == -1 {
- ng := gokeepasslib.NewGroup()
- ng.Name = segment
- g.Groups = append(g.Groups, ng)
- found = len(g.Groups) - 1
- }
- g = &g.Groups[found]
- }
- return g
-}
-
-func upsertEntryByTitle(g *gokeepasslib.Group, title string) (*gokeepasslib.Entry, bool) {
- for i := range g.Entries {
- if g.Entries[i].GetTitle() == title {
- return &g.Entries[i], true
- }
- }
- e := gokeepasslib.NewEntry()
- g.Entries = append(g.Entries, e)
- return &g.Entries[len(g.Entries)-1], false
-}
-
+// Save locks protected entries and atomically writes the database to disk.
+// Delegates the tmp→encode→rename sequence to keepass.AtomicSave to avoid
+// duplicating that logic here (keepass.Backend.save() uses the same helper).
func (s *kdbxStore) Save() error {
if err := s.db.LockProtectedEntries(); err != nil {
return fmt.Errorf("locking kdbx entries: %w", err)
}
-
- tmpPath := s.path + ".tmp"
- out, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
- if err != nil {
- return fmt.Errorf("creating temporary kdbx %q: %w", tmpPath, err)
- }
- defer out.Close()
-
- if err := gokeepasslib.NewEncoder(out).Encode(s.db); err != nil {
- return fmt.Errorf("encoding kdbx to %q: %w", tmpPath, err)
- }
- if err := out.Close(); err != nil {
- return fmt.Errorf("closing temporary kdbx %q: %w", tmpPath, err)
- }
- if err := os.Rename(tmpPath, s.path); err != nil {
- return fmt.Errorf("replacing kdbx %q: %w", s.path, err)
- }
- return nil
-}
-
-func setEntryField(entry *gokeepasslib.Entry, key, value string) {
- for i := range entry.Values {
- if entry.Values[i].Key == key {
- entry.Values[i].Value.Content = value
- return
- }
- }
-
- entry.Values = append(entry.Values, gokeepasslib.ValueData{
- Key: key,
- Value: gokeepasslib.V{
- Content: value,
- },
- })
-}
-
-func splitDescriptionPath(description string) ([]string, string, error) {
- safePath, err := sanitizeRelativePath(description)
- if err != nil {
- return nil, "", err
- }
-
- parts := strings.Split(safePath, "/")
- if len(parts) == 1 {
- return nil, parts[0], nil
- }
- return parts[:len(parts)-1], parts[len(parts)-1], nil
-}
-
-func sanitizeRelativePath(path string) (string, error) {
- normalised := strings.ReplaceAll(path, "\\", "/")
- normalised = strings.TrimSpace(normalised)
- if normalised == "" {
- return "", fmt.Errorf("empty entry description")
- }
-
- clean := filepath.Clean(normalised)
- clean = strings.TrimPrefix(clean, "/")
- if clean == "." || clean == "" || clean == ".." || strings.HasPrefix(clean, "../") {
- return "", fmt.Errorf("unsafe entry description path %q", path)
- }
- return clean, nil
+ return keepass.AtomicSave(s.db, s.path)
}