Reconstruct KeePassGO repository
This commit is contained in:
+337
@@ -0,0 +1,337 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.julianfamily.org/keepassgo/appstate"
|
||||
"git.julianfamily.org/keepassgo/clipboard"
|
||||
"git.julianfamily.org/keepassgo/passwords"
|
||||
"git.julianfamily.org/keepassgo/vault"
|
||||
)
|
||||
|
||||
func (u *ui) loadSelectedEntryIntoEditor() {
|
||||
item, ok := u.selectedEntry()
|
||||
if !ok {
|
||||
u.entryID.SetText("")
|
||||
u.entryTitle.SetText("")
|
||||
u.entryUsername.SetText("")
|
||||
u.entryPassword.SetText("")
|
||||
u.entryURL.SetText("")
|
||||
u.entryNotes.SetText("")
|
||||
u.entryTags.SetText("")
|
||||
u.entryPath.SetText(strings.Join(u.currentPath, " / "))
|
||||
u.entryFields.SetText("")
|
||||
u.attachmentName.SetText("")
|
||||
u.attachmentPath.SetText("")
|
||||
u.exportAttachmentPath.SetText("")
|
||||
return
|
||||
}
|
||||
|
||||
u.entryID.SetText(item.ID)
|
||||
u.entryTitle.SetText(item.Title)
|
||||
u.entryUsername.SetText(item.Username)
|
||||
u.entryPassword.SetText(item.Password)
|
||||
u.entryURL.SetText(item.URL)
|
||||
u.entryNotes.SetText(item.Notes)
|
||||
u.entryTags.SetText(strings.Join(item.Tags, ", "))
|
||||
u.entryPath.SetText(strings.Join(item.Path, " / "))
|
||||
u.entryFields.SetText(marshalFields(item.Fields))
|
||||
u.attachmentName.SetText("")
|
||||
u.attachmentPath.SetText("")
|
||||
u.exportAttachmentPath.SetText("")
|
||||
}
|
||||
|
||||
func (u *ui) saveEntryAction() error {
|
||||
entry, err := u.editorEntry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.state.UpsertEntry(entry); err != nil {
|
||||
return err
|
||||
}
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) duplicateSelectedEntryAction() error {
|
||||
baseID := strings.TrimSpace(u.state.SelectedEntryID)
|
||||
if baseID == "" {
|
||||
return fmt.Errorf("no entry selected")
|
||||
}
|
||||
|
||||
duplicateID := baseID + "-copy"
|
||||
if _, err := u.state.DuplicateSelectedEntry(duplicateID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.loadSelectedEntryIntoEditor()
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) deleteSelectedEntryAction() error {
|
||||
if err := u.state.DeleteSelectedEntry(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.loadSelectedEntryIntoEditor()
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) restoreSelectedRecycleEntryAction() error {
|
||||
id := strings.TrimSpace(u.state.SelectedEntryID)
|
||||
if id == "" {
|
||||
return fmt.Errorf("no recycle-bin entry selected")
|
||||
}
|
||||
if err := u.state.RestoreEntry(id); err != nil {
|
||||
return err
|
||||
}
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) saveTemplateAction() error {
|
||||
entry, err := u.editorEntry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(entry.Path) == 0 {
|
||||
entry.Path = []string{"Templates"}
|
||||
}
|
||||
if err := u.state.UpsertTemplate(entry); err != nil {
|
||||
return err
|
||||
}
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) deleteSelectedTemplateAction() error {
|
||||
id := strings.TrimSpace(u.state.SelectedEntryID)
|
||||
if id == "" {
|
||||
return fmt.Errorf("no template selected")
|
||||
}
|
||||
if err := u.state.DeleteTemplate(id); err != nil {
|
||||
return err
|
||||
}
|
||||
u.loadSelectedEntryIntoEditor()
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) createGroupAction() error {
|
||||
return u.state.CreateGroup(strings.TrimSpace(u.groupName.Text()))
|
||||
}
|
||||
|
||||
func (u *ui) renameGroupAction() error {
|
||||
return u.state.RenameCurrentGroup(strings.TrimSpace(u.groupName.Text()))
|
||||
}
|
||||
|
||||
func (u *ui) deleteCurrentGroupAction() error {
|
||||
session, ok := u.state.Session.(appstate.MutableSession)
|
||||
if !ok {
|
||||
return fmt.Errorf("session is not mutable")
|
||||
}
|
||||
|
||||
model, err := session.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := model.DeleteGroup(u.currentPath); err != nil {
|
||||
return err
|
||||
}
|
||||
session.Replace(model)
|
||||
if len(u.currentPath) > 0 {
|
||||
u.currentPath = append([]string(nil), u.currentPath[:len(u.currentPath)-1]...)
|
||||
u.state.CurrentPath = append([]string(nil), u.currentPath...)
|
||||
}
|
||||
u.state.Dirty = true
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) instantiateSelectedTemplateAction() error {
|
||||
templateID := strings.TrimSpace(u.state.SelectedEntryID)
|
||||
if templateID == "" {
|
||||
return fmt.Errorf("no template selected")
|
||||
}
|
||||
|
||||
entry, err := u.editorEntry()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := u.state.InstantiateTemplate(templateID, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) addAttachmentAction() error {
|
||||
content, err := os.ReadFile(strings.TrimSpace(u.attachmentPath.Text()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read attachment: %w", err)
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(u.attachmentName.Text())
|
||||
if name == "" {
|
||||
return fmt.Errorf("attachment name is required")
|
||||
}
|
||||
|
||||
if err := u.state.AddAttachmentToSelectedEntry(name, content); err != nil {
|
||||
return err
|
||||
}
|
||||
u.loadSelectedEntryIntoEditor()
|
||||
u.attachmentName.SetText(name)
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) exportAttachmentAction() error {
|
||||
item, ok := u.selectedEntry()
|
||||
if !ok {
|
||||
return fmt.Errorf("no entry selected")
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(u.attachmentName.Text())
|
||||
content, ok := item.Attachments[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("attachment not found")
|
||||
}
|
||||
|
||||
if err := os.WriteFile(strings.TrimSpace(u.exportAttachmentPath.Text()), content, 0o600); err != nil {
|
||||
return fmt.Errorf("write attachment export: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) removeAttachmentAction() error {
|
||||
name := strings.TrimSpace(u.attachmentName.Text())
|
||||
if name == "" {
|
||||
return fmt.Errorf("attachment name is required")
|
||||
}
|
||||
|
||||
if err := u.state.DeleteAttachmentFromSelectedEntry(name); err != nil {
|
||||
return err
|
||||
}
|
||||
u.loadSelectedEntryIntoEditor()
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) restoreSelectedHistoryAction() error {
|
||||
index, err := strconv.Atoi(strings.TrimSpace(u.historyIndex.Text()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid history index: %w", err)
|
||||
}
|
||||
if err := u.state.RestoreSelectedEntryVersion(index); err != nil {
|
||||
return err
|
||||
}
|
||||
u.loadSelectedEntryIntoEditor()
|
||||
u.filter()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) copySelectedFieldAction(target clipboard.Target) error {
|
||||
model, err := u.state.Session.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service := clipboard.Service{}
|
||||
return service.Copy(model, u.state.SelectedEntryID, target)
|
||||
}
|
||||
|
||||
func (u *ui) generatePasswordAction() error {
|
||||
profiles := passwords.DefaultProfiles()
|
||||
profile, ok := profiles[strings.TrimSpace(u.passwordProfile.Text())]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown password profile")
|
||||
}
|
||||
|
||||
password, err := passwords.Generate(profile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.entryPassword.SetText(password)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *ui) editorEntry() (vault.Entry, error) {
|
||||
path := parsePath(u.entryPath.Text())
|
||||
fields, err := parseFields(u.entryFields.Text())
|
||||
if err != nil {
|
||||
return vault.Entry{}, err
|
||||
}
|
||||
|
||||
return vault.Entry{
|
||||
ID: strings.TrimSpace(u.entryID.Text()),
|
||||
Title: strings.TrimSpace(u.entryTitle.Text()),
|
||||
Username: strings.TrimSpace(u.entryUsername.Text()),
|
||||
Password: u.entryPassword.Text(),
|
||||
URL: strings.TrimSpace(u.entryURL.Text()),
|
||||
Notes: strings.TrimSpace(u.entryNotes.Text()),
|
||||
Tags: parseTags(u.entryTags.Text()),
|
||||
Path: path,
|
||||
Fields: fields,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parsePath(text string) []string {
|
||||
var out []string
|
||||
for _, part := range strings.Split(text, "/") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, part)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseTags(text string) []string {
|
||||
var out []string
|
||||
for _, part := range strings.Split(text, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, part)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseFields(text string) (map[string]string, error) {
|
||||
if strings.TrimSpace(text) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
fields := map[string]string{}
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
key, value, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid field line %q", line)
|
||||
}
|
||||
fields[strings.TrimSpace(key)] = strings.TrimSpace(value)
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func marshalFields(fields map[string]string) string {
|
||||
if len(fields) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
lines := make([]string, 0, len(fields))
|
||||
for key, value := range fields {
|
||||
lines = append(lines, key+"="+value)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
Reference in New Issue
Block a user