summaryrefslogtreecommitdiff
path: root/internal/keepass/remove.go
blob: 40bbc9fa6862657a3f23d46c9d825332efe80f5a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package keepass

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"codeberg.org/snonux/foostore/internal/store"
)

// Remove finds all indexes matching searchTerm and prompts before deleting.
func (b *Backend) Remove(ctx context.Context, searchTerm string, input io.Reader) error {
	var indexes store.IndexSlice
	if err := b.WalkIndexes(ctx, searchTerm, func(idx *store.Index) error {
		indexes = append(indexes, idx)
		return nil
	}); err != nil {
		return err
	}
	sort.Sort(indexes)

	scanner := bufio.NewScanner(input)
	for _, idx := range indexes {
		if err := b.confirmAndRemove(idx, scanner); err != nil {
			return err
		}
	}
	return nil
}

// confirmAndRemove prompts the user for deletion confirmation and removes the
// entry (or attachment) on "y".
func (b *Backend) confirmAndRemove(idx *store.Index, scanner *bufio.Scanner) error {
	for {
		fmt.Print(idx.String())
		fmt.Print("You really want to delete this? (y/n): ")
		if !scanner.Scan() {
			return nil
		}
		switch strings.TrimSpace(scanner.Text()) {
		case "y":
			return b.removeEntry(idx.Description)
		case "n":
			return nil
		}
	}
}

// removeEntry deletes the KeePass entry (or attachment) matching description
// and saves the DB.
//
// If description is a virtual attachment path ("Group/Title/file.bin"), the
// attachment is removed from the parent entry rather than removing the entry
// itself. For regular entries the full entry is deleted from its group.
func (b *Backend) removeEntry(description string) error {
	// Detect virtual attachment paths: if the parent exists as a text entry but
	// description itself does not, route to attachment removal.
	if parentDesc, attachName, ok := b.isAttachmentPath(description); ok {
		return b.removeAttachment(parentDesc, attachName)
	}
	return b.removeTextEntry(description)
}

// removeTextEntry deletes a regular (non-attachment) entry from its group.
func (b *Backend) removeTextEntry(description string) error {
	groupPath, title, err := SplitDescriptionPath(description)
	if err != nil {
		return fmt.Errorf("keepass remove: %w", err)
	}
	g := EnsureGroup(b.root(), groupPath)
	for i, e := range g.Entries {
		if e.GetTitle() == title {
			g.Entries = append(g.Entries[:i], g.Entries[i+1:]...)
			return b.save()
		}
	}
	return fmt.Errorf("keepass remove: entry %q not found", description)
}

// ShredAllExported securely deletes every regular file in cfg.ExportDir.
// Delegates to store.ShredFile for the actual destruction.
func (b *Backend) ShredAllExported(ctx context.Context) error {
	entries, err := os.ReadDir(b.cfg.ExportDir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return fmt.Errorf("listing export dir: %w", err)
	}
	var lastErr error
	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		filePath := filepath.Join(b.cfg.ExportDir, e.Name())
		if err := store.ShredFile(ctx, filePath); err != nil {
			lastErr = err
		}
	}
	return lastErr
}