diff options
Diffstat (limited to 'internal/keepass/write.go')
| -rw-r--r-- | internal/keepass/write.go | 103 |
1 files changed, 103 insertions, 0 deletions
diff --git a/internal/keepass/write.go b/internal/keepass/write.go new file mode 100644 index 0000000..f79a38d --- /dev/null +++ b/internal/keepass/write.go @@ -0,0 +1,103 @@ +package keepass + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" +) + +// Add creates or updates a KeePass entry from plaintext data. +// +// Attachment detection: if the last path component does not match any existing +// standalone entry but its parent does, the call is routed to addAttachment so +// that "Group/Title/file.bin" attaches raw bytes to "Group/Title". The parent +// entry must already exist; return an error if it does not. +// Otherwise, the description is treated as a normal text entry and its fields +// are parsed from data via parseContent. +func (b *Backend) Add(ctx context.Context, description, data string) error { + if parentDesc, attachName, ok := b.isAttachmentPath(description); ok { + return b.addAttachment(parentDesc, attachName, []byte(data)) + } + return b.addTextEntry(description, data) +} + +// addTextEntry creates or updates the KeePass entry at description using the +// fields parsed from data. This is the normal (non-attachment) Add path. +func (b *Backend) addTextEntry(description, data string) error { + groupPath, title, err := SplitDescriptionPath(description) + if err != nil { + return fmt.Errorf("keepass add: %w", err) + } + password, user, url, notes := parseContent([]byte(data)) + g := EnsureGroup(b.root(), groupPath) + entry, _ := UpsertEntryByTitle(g, title) + SetEntryField(entry, "Title", title) + SetEntryField(entry, "Password", password) + SetEntryField(entry, "UserName", user) + SetEntryField(entry, "URL", url) + SetEntryField(entry, "Notes", notes) + return b.save() +} + +// Import reads a file from srcPath and stores it under destPath. +// When force is false and an entry already exists at destPath, the import is +// skipped silently (a warning is printed to stderr) and nil is returned — +// matching the interface contract and the behaviour of store.Store.Import. +func (b *Backend) Import(ctx context.Context, srcPath, destPath string, force bool) error { + if !force && b.entryExists(destPath) { + // Skip without error; warn on stderr so the operator knows something + // was skipped, consistent with store.Store.Import(force=false). + fmt.Fprintf(os.Stderr, "Warning: keepass entry %q already exists, skipping (use force to overwrite)\n", destPath) + return nil + } + content, err := os.ReadFile(srcPath) + if err != nil { + return fmt.Errorf("keepass import: reading %q: %w", srcPath, err) + } + return b.Add(ctx, destPath, string(content)) +} + +// entryExists reports whether an entry with the given description already +// exists in the KeePass database. Used by Import to detect duplicates. +func (b *Backend) entryExists(description string) bool { + for _, ve := range walkEntries(b.root()) { + if ve.description == description { + return true + } + } + return false +} + +// ImportRecursive walks directory recursively and imports every regular file +// under destDir, preserving the relative sub-directory structure in the +// description path. Uses filepath.WalkDir to descend into sub-directories. +func (b *Backend) ImportRecursive(ctx context.Context, directory, destDir string) error { + baseDir := strings.TrimRight(destDir, "/") + return walkDirFilesRecursive(directory, func(relFile string) error { + destPath := filepath.Join(baseDir, relFile) + srcPath := filepath.Join(directory, relFile) + return b.Import(ctx, srcPath, destPath, false) + }) +} + +// walkDirFilesRecursive walks a directory tree calling fn(relativeFilePath) +// for each regular file found at any depth. Uses filepath.WalkDir so that +// sub-directories are descended into. +func walkDirFilesRecursive(dir string, fn func(string) error) error { + return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("keepass: walking %q: %w", path, err) + } + if d.IsDir() { + return nil + } + rel, relErr := filepath.Rel(dir, path) + if relErr != nil { + return fmt.Errorf("keepass: computing relative path for %q: %w", path, relErr) + } + // Use forward slashes in the description regardless of OS separator. + return fn(filepath.ToSlash(rel)) + }) +} |
