fix: address CodeRabbit PR #2 findings (batch 2)

- datanode/medium.go: add compile-time Medium interface check
- docs/RFC.md: remove duplicated MemoryMedium Read/Write method entries
- docs/RFC-CORE-008-AGENT-EXPERIENCE.md: add text language tag to fenced code block
- io.go: rename WriteMode param path→filePath to avoid shadowing path package
- io.go: add directory collision check in WriteMode and MemoryWriteCloser.Close
- io.go: Copy now preserves source file permissions via Stat+WriteMode
- node/node.go: add goroutine-safety doc comment on Node
- node/node.go: Rename handles directory prefix batch-rename
- node/node.go: rename CopyFile→ExportFile, document local-only behaviour, wrap PathError with core.E()
- node/node.go: filter empty path components in Walk depth calculation
- node/node_test.go: update tests to use ExportFile
- sigil/crypto_sigil.go: make Key/Obfuscator unexported, add Key()/Obfuscator()/SetObfuscator() accessors
- sigil/crypto_sigil.go: rename receiver sigil→s to avoid shadowing package name
- sigil/crypto_sigil_test.go: update to use accessor methods
- store/medium.go: use KeyValueStore.ListGroups() instead of direct DB query
- store/medium.go: add doc comment that WriteMode does not persist file mode
- store/store.go: add ListGroups() method to KeyValueStore

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-04-05 12:39:57 +01:00
parent a43a16fb0d
commit 2c18322dfe
13 changed files with 148 additions and 83 deletions

View file

@ -14,6 +14,7 @@ import (
"time"
core "dappco.re/go/core"
coreio "dappco.re/go/core/io"
borgdatanode "forge.lthn.ai/Snider/Borg/pkg/datanode"
)
@ -29,6 +30,8 @@ var (
}
)
var _ coreio.Medium = (*Medium)(nil)
// Example: medium := datanode.New()
// Example: _ = medium.Write("jobs/run.log", "started")
// Example: snapshot, _ := medium.Snapshot()

View file

@ -41,7 +41,7 @@ AX does not replace UX or DX. End users still need good UX. Developers still nee
Names are tokens that agents pattern-match across languages and contexts. Abbreviations introduce mapping overhead.
```
```text
Config not Cfg
Service not Srv
Embed not Emb

View file

@ -422,21 +422,6 @@ _ = m.Write("notes.txt", "hello")
ok := m.IsFile("notes.txt")
```
**Read(path string) (string, error)**
Example:
```go
m := io.NewMemoryMedium()
_ = m.Write("notes.txt", "hello")
value, _ := m.Read("notes.txt")
```
**Write(path, content string) error**
Example:
```go
m := io.NewMemoryMedium()
_ = m.Write("notes.txt", "hello")
```
**Delete(path string) error**
Example:
```go
@ -869,13 +854,13 @@ _ = n.Write("file.txt", "data")
b, _ := n.ReadFile("file.txt")
```
**CopyFile(sourcePath, destinationPath string, perm fs.FileMode) error**
Copies a file to the local filesystem.
**ExportFile(sourcePath, destinationPath string, perm fs.FileMode) error**
Exports a file from the in-memory tree to the local filesystem. Operates on coreio.Local directly — use CopyTo for Medium-agnostic transfers.
Example:
```go
n := node.New()
_ = n.Write("file.txt", "data")
_ = n.CopyFile("file.txt", "/tmp/file.txt", 0644)
_ = n.ExportFile("file.txt", "/tmp/file.txt", 0644)
```
**CopyTo(target io.Medium, sourcePath, destPath string) error**

View file

@ -126,7 +126,7 @@ Test coverage is `Yes` when same-package tests directly execute or reference the
| `New` | `func New() *Node` | `dappco.re/go/core/io/node` | New creates a new, empty Node. | Yes |
| `Node.AddData` | `func (*Node) AddData(name string, content []byte)` | `dappco.re/go/core/io/node` | AddData stages content in the in-memory filesystem. | Yes |
| `Node.Append` | `func (*Node) Append(p string) (goio.WriteCloser, error)` | `dappco.re/go/core/io/node` | Append opens the named file for appending, creating it if needed. | No |
| `Node.CopyFile` | `func (*Node) CopyFile(src, dst string, perm fs.FileMode) error` | `dappco.re/go/core/io/node` | CopyFile copies a file from the in-memory tree to the local filesystem. | Yes |
| `Node.ExportFile` | `func (*Node) ExportFile(src, dst string, perm fs.FileMode) error` | `dappco.re/go/core/io/node` | ExportFile exports a file from the in-memory tree to the local filesystem. Use CopyTo for Medium-agnostic transfers. | Yes |
| `Node.CopyTo` | `func (*Node) CopyTo(target coreio.Medium, sourcePath, destPath string) error` | `dappco.re/go/core/io/node` | CopyTo copies a file (or directory tree) from the node to any Medium. | No |
| `Node.Create` | `func (*Node) Create(p string) (goio.WriteCloser, error)` | `dappco.re/go/core/io/node` | Create creates or truncates the named file, returning a WriteCloser. | No |
| `Node.Delete` | `func (*Node) Delete(p string) error` | `dappco.re/go/core/io/node` | Delete removes a single file. | No |

View file

@ -94,7 +94,7 @@ Key capabilities beyond `Medium`:
- **`ToTar()` / `FromTar()`** — serialise the entire tree to a tar archive and back. This enables snapshotting, transport, and archival.
- **`Walk()` with `WalkOptions`** — extends `fs.WalkDir` with `MaxDepth`, `Filter`, and `SkipErrors` controls.
- **`CopyFile(src, dst, perm)`** — copies a file from the in-memory tree to the real filesystem.
- **`ExportFile(src, dst, perm)`** — exports a file from the in-memory tree to the local filesystem. Use `CopyTo` for Medium-agnostic transfers.
- **`CopyTo(target Medium, src, dst)`** — copies a file or directory tree to any other `Medium`.
- **`ReadFile(name)`** — returns a defensive copy of file content, preventing callers from mutating internal state.

View file

@ -116,7 +116,7 @@ Notes:
| `(*node.Node).Delete`, `DeleteAll`, `Rename` | `node/node.go:411`, `421`, `445` | Caller path(s) | Direct map mutation keyed by caller-supplied names | Only strips a leading `/` | Arbitrary delete/rename of any key, including `../`-style names; no directory-safe rename logic |
| `(*node.Node).Stat`, `List`, `ReadDir`, `Exists`, `IsFile`, `IsDir` | `node/node.go:278`, `461`, `297`, `387`, `393`, `400` | Caller path/name | Directory inference from map keys and `fs` adapter methods | Only strips a leading `/` | Namespace enumeration and ambiguity around equivalent-looking path spellings |
| `(*node.Node).WalkNode`, `Walk` | `node/node.go:128`, `145` | Caller root path, callback, filters | `fs.WalkDir` over the in-memory tree | No root normalization beyond whatever `Node` already does | Attackers who can plant names can force callback traversal over weird paths; `SkipErrors` can suppress unexpected failures |
| `(*node.Node).CopyFile` | `node/node.go:200` | Caller source key, destination host path, permissions | Reads node content and calls `os.WriteFile(dst, ...)` directly | Only checks that `src` exists and is not a directory | Arbitrary host filesystem write to a caller-chosen `dst` path |
| `(*node.Node).ExportFile` | `node/node.go:200` | Caller source key, destination host path, permissions | Reads node content and calls `coreio.Local.WriteMode(dst, ...)` directly | Only checks that `src` exists and is not a directory | Arbitrary host filesystem write to a caller-chosen `dst` path |
| `(*node.Node).CopyTo` | `node/node.go:218` | Caller target medium, source path, destination path | Reads node entries and calls `target.Write(destPath or destPath/rel, content)` | Only checks that the source exists | Stored `../`-style node keys can propagate into destination paths, enabling traversal or overwrite depending on the target backend |
| `(*node.Node).EnsureDir` | `node/node.go:380` | Caller path (ignored) | No-op | Input is ignored | Semantic mismatch: callers may assume a directory boundary was created when directories remain implicit |

24
io.go
View file

@ -189,7 +189,11 @@ func Copy(sourceMedium Medium, sourcePath string, destinationMedium Medium, dest
if err != nil {
return core.E("io.Copy", core.Concat("read failed: ", sourcePath), err)
}
if err := destinationMedium.Write(destinationPath, content); err != nil {
mode := fs.FileMode(0644)
if info, err := sourceMedium.Stat(sourcePath); err == nil {
mode = info.Mode()
}
if err := destinationMedium.WriteMode(destinationPath, content, mode); err != nil {
return core.E("io.Copy", core.Concat("write failed: ", destinationPath), err)
}
return nil
@ -271,9 +275,9 @@ func (medium *MemoryMedium) Write(path, content string) error {
}
// Example: _ = io.NewMemoryMedium().WriteMode("keys/private.key", "secret", 0600)
func (medium *MemoryMedium) WriteMode(path, content string, mode fs.FileMode) error {
func (medium *MemoryMedium) WriteMode(filePath, content string, mode fs.FileMode) error {
// Verify no ancestor directory component is stored as a file.
ancestor := path.Dir(path)
ancestor := path.Dir(filePath)
for ancestor != "." && ancestor != "" {
if _, ok := medium.fileContents[ancestor]; ok {
return core.E("io.MemoryMedium.WriteMode", core.Concat("ancestor path is a file: ", ancestor), fs.ErrExist)
@ -284,10 +288,13 @@ func (medium *MemoryMedium) WriteMode(path, content string, mode fs.FileMode) er
}
ancestor = next
}
medium.ensureAncestorDirectories(path)
medium.fileContents[path] = content
medium.fileModes[path] = mode
medium.modificationTimes[path] = time.Now()
if _, ok := medium.directories[filePath]; ok {
return core.E("io.MemoryMedium.WriteMode", core.Concat("path is a directory: ", filePath), fs.ErrExist)
}
medium.ensureAncestorDirectories(filePath)
medium.fileContents[filePath] = content
medium.fileModes[filePath] = mode
medium.modificationTimes[filePath] = time.Now()
return nil
}
@ -537,6 +544,9 @@ func (writeCloser *MemoryWriteCloser) Write(data []byte) (int, error) {
}
func (writeCloser *MemoryWriteCloser) Close() error {
if _, ok := writeCloser.medium.directories[writeCloser.path]; ok {
return core.E("io.MemoryWriteCloser.Close", core.Concat("path is a directory: ", writeCloser.path), fs.ErrExist)
}
writeCloser.medium.ensureAncestorDirectories(writeCloser.path)
writeCloser.medium.fileContents[writeCloser.path] = string(writeCloser.data)
writeCloser.medium.fileModes[writeCloser.path] = writeCloser.mode

View file

@ -22,6 +22,8 @@ import (
// Example: nodeTree.AddData("config/app.yaml", []byte("port: 8080"))
// Example: snapshot, _ := nodeTree.ToTar()
// Example: restored, _ := node.FromTar(snapshot)
// Note: Node is not goroutine-safe. All methods must be called from a single goroutine,
// or the caller must provide external synchronisation.
type Node struct {
files map[string]*dataFile
}
@ -152,7 +154,13 @@ func (node *Node) Walk(root string, walkFunc fs.WalkDirFunc, options WalkOptions
if walkResult == nil && options.MaxDepth > 0 && entry != nil && entry.IsDir() && entryPath != root {
relativePath := core.TrimPrefix(entryPath, root)
relativePath = core.TrimPrefix(relativePath, "/")
depth := len(core.Split(relativePath, "/"))
parts := core.Split(relativePath, "/")
depth := 0
for _, part := range parts {
if part != "" {
depth++
}
}
if depth >= options.MaxDepth {
return fs.SkipDir
}
@ -174,23 +182,26 @@ func (node *Node) ReadFile(name string) ([]byte, error) {
return result, nil
}
// Example: _ = nodeTree.CopyFile("config/app.yaml", "backup/app.yaml", 0644)
func (node *Node) CopyFile(sourcePath, destinationPath string, permissions fs.FileMode) error {
// ExportFile writes a node file to the local filesystem. It operates on coreio.Local directly
// and is intentionally local-only — use CopyTo for Medium-agnostic transfers.
// Example: _ = nodeTree.ExportFile("config/app.yaml", "backup/app.yaml", 0644)
func (node *Node) ExportFile(sourcePath, destinationPath string, permissions fs.FileMode) error {
sourcePath = core.TrimPrefix(sourcePath, "/")
file, ok := node.files[sourcePath]
if !ok {
info, err := node.Stat(sourcePath)
if err != nil {
return core.E("node.CopyFile", core.Concat("source not found: ", sourcePath), fs.ErrNotExist)
return core.E("node.ExportFile", core.Concat("source not found: ", sourcePath), fs.ErrNotExist)
}
if info.IsDir() {
return core.E("node.CopyFile", core.Concat("source is a directory: ", sourcePath), fs.ErrInvalid)
return core.E("node.ExportFile", core.Concat("source is a directory: ", sourcePath), fs.ErrInvalid)
}
return core.E("node.CopyFile", core.Concat("source not found: ", sourcePath), fs.ErrNotExist)
// unreachable: Stat only succeeds for directories when file is absent
return core.E("node.ExportFile", core.Concat("source not found: ", sourcePath), fs.ErrNotExist)
}
parent := core.PathDir(destinationPath)
if parent != "." && parent != "" && parent != destinationPath && !coreio.Local.IsDir(parent) {
return &fs.PathError{Op: "copyfile", Path: destinationPath, Err: fs.ErrNotExist}
return core.E("node.ExportFile", core.Concat("parent directory not found: ", destinationPath), fs.ErrNotExist)
}
return coreio.Local.WriteMode(destinationPath, string(file.content), permissions)
}
@ -401,18 +412,42 @@ func (node *Node) DeleteAll(filePath string) error {
}
// Example: _ = nodeTree.Rename("drafts/todo.txt", "archive/todo.txt")
// Example: _ = nodeTree.Rename("drafts", "archive")
func (node *Node) Rename(oldPath, newPath string) error {
oldPath = core.TrimPrefix(oldPath, "/")
newPath = core.TrimPrefix(newPath, "/")
file, ok := node.files[oldPath]
if !ok {
return core.E("node.Rename", core.Concat("path not found: ", oldPath), fs.ErrNotExist)
if file, ok := node.files[oldPath]; ok {
file.name = newPath
node.files[newPath] = file
delete(node.files, oldPath)
return nil
}
file.name = newPath
node.files[newPath] = file
delete(node.files, oldPath)
// Directory rename: batch-rename all entries that share the prefix.
oldPrefix := oldPath + "/"
newPrefix := newPath + "/"
renamed := 0
toAdd := make(map[string]*dataFile)
toDelete := make([]string, 0)
for filePath, file := range node.files {
if core.HasPrefix(filePath, oldPrefix) {
updatedPath := core.Concat(newPrefix, core.TrimPrefix(filePath, oldPrefix))
file.name = updatedPath
toAdd[updatedPath] = file
toDelete = append(toDelete, filePath)
renamed++
}
}
for _, p := range toDelete {
delete(node.files, p)
}
for p, f := range toAdd {
node.files[p] = f
}
if renamed == 0 {
return core.E("node.Rename", core.Concat("path not found: ", oldPath), fs.ErrNotExist)
}
return nil
}

View file

@ -313,12 +313,12 @@ func TestNode_Walk_Good(t *testing.T) {
})
}
func TestNode_CopyFile_Good(t *testing.T) {
func TestNode_ExportFile_Good(t *testing.T) {
nodeTree := New()
nodeTree.AddData("foo.txt", []byte("foo"))
destinationPath := core.Path(t.TempDir(), "test.txt")
err := nodeTree.CopyFile("foo.txt", destinationPath, 0644)
err := nodeTree.ExportFile("foo.txt", destinationPath, 0644)
require.NoError(t, err)
content, err := coreio.Local.Read(destinationPath)
@ -326,24 +326,24 @@ func TestNode_CopyFile_Good(t *testing.T) {
assert.Equal(t, "foo", content)
}
func TestNode_CopyFile_Bad(t *testing.T) {
func TestNode_ExportFile_Bad(t *testing.T) {
nodeTree := New()
destinationPath := core.Path(t.TempDir(), "test.txt")
err := nodeTree.CopyFile("nonexistent.txt", destinationPath, 0644)
err := nodeTree.ExportFile("nonexistent.txt", destinationPath, 0644)
assert.Error(t, err)
nodeTree.AddData("foo.txt", []byte("foo"))
err = nodeTree.CopyFile("foo.txt", "/nonexistent_dir/test.txt", 0644)
err = nodeTree.ExportFile("foo.txt", "/nonexistent_dir/test.txt", 0644)
assert.Error(t, err)
}
func TestNode_CopyFile_DirectorySource_Bad(t *testing.T) {
func TestNode_ExportFile_DirectorySource_Bad(t *testing.T) {
nodeTree := New()
nodeTree.AddData("bar/baz.txt", []byte("baz"))
destinationPath := core.Path(t.TempDir(), "test.txt")
err := nodeTree.CopyFile("bar", destinationPath, 0644)
err := nodeTree.ExportFile("bar", destinationPath, 0644)
assert.Error(t, err)
}

View file

@ -182,11 +182,28 @@ func (obfuscator *ShuffleMaskObfuscator) deriveMask(entropy []byte, length int)
// Example: &sigil.ShuffleMaskObfuscator{},
// Example: )
type ChaChaPolySigil struct {
Key []byte
Obfuscator PreObfuscator
key []byte
obfuscator PreObfuscator
randomReader goio.Reader
}
// Example: key := cipherSigil.Key()
func (s *ChaChaPolySigil) Key() []byte {
result := make([]byte, len(s.key))
copy(result, s.key)
return result
}
// Example: ob := cipherSigil.Obfuscator()
func (s *ChaChaPolySigil) Obfuscator() PreObfuscator {
return s.obfuscator
}
// Example: cipherSigil.SetObfuscator(nil)
func (s *ChaChaPolySigil) SetObfuscator(obfuscator PreObfuscator) {
s.obfuscator = obfuscator
}
// Example: cipherSigil, _ := sigil.NewChaChaPolySigil([]byte("0123456789abcdef0123456789abcdef"), nil)
// Example: ciphertext, _ := cipherSigil.In([]byte("payload"))
// Example: plaintext, _ := cipherSigil.Out(ciphertext)
@ -203,27 +220,27 @@ func NewChaChaPolySigil(key []byte, obfuscator PreObfuscator) (*ChaChaPolySigil,
}
return &ChaChaPolySigil{
Key: keyCopy,
Obfuscator: obfuscator,
key: keyCopy,
obfuscator: obfuscator,
randomReader: rand.Reader,
}, nil
}
func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
if sigil.Key == nil {
func (s *ChaChaPolySigil) In(data []byte) ([]byte, error) {
if s.key == nil {
return nil, NoKeyConfiguredError
}
if data == nil {
return nil, nil
}
aead, err := chacha20poly1305.NewX(sigil.Key)
aead, err := chacha20poly1305.NewX(s.key)
if err != nil {
return nil, core.E("sigil.ChaChaPolySigil.In", "create cipher", err)
}
nonce := make([]byte, aead.NonceSize())
reader := sigil.randomReader
reader := s.randomReader
if reader == nil {
reader = rand.Reader
}
@ -232,8 +249,8 @@ func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
}
obfuscated := data
if sigil.Obfuscator != nil {
obfuscated = sigil.Obfuscator.Obfuscate(data, nonce)
if s.obfuscator != nil {
obfuscated = s.obfuscator.Obfuscate(data, nonce)
}
ciphertext := aead.Seal(nonce, nonce, obfuscated, nil)
@ -241,15 +258,15 @@ func (sigil *ChaChaPolySigil) In(data []byte) ([]byte, error) {
return ciphertext, nil
}
func (sigil *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
if sigil.Key == nil {
func (s *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
if s.key == nil {
return nil, NoKeyConfiguredError
}
if data == nil {
return nil, nil
}
aead, err := chacha20poly1305.NewX(sigil.Key)
aead, err := chacha20poly1305.NewX(s.key)
if err != nil {
return nil, core.E("sigil.ChaChaPolySigil.Out", "create cipher", err)
}
@ -270,8 +287,8 @@ func (sigil *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
}
plaintext := obfuscated
if sigil.Obfuscator != nil {
plaintext = sigil.Obfuscator.Deobfuscate(obfuscated, nonce)
if s.obfuscator != nil {
plaintext = s.obfuscator.Deobfuscate(obfuscated, nonce)
}
if len(plaintext) == 0 {

View file

@ -145,8 +145,8 @@ func TestCryptoSigil_NewChaChaPolySigil_Good(t *testing.T) {
cipherSigil, err := NewChaChaPolySigil(key, nil)
require.NoError(t, err)
assert.NotNil(t, cipherSigil)
assert.Equal(t, key, cipherSigil.Key)
assert.NotNil(t, cipherSigil.Obfuscator)
assert.Equal(t, key, cipherSigil.Key())
assert.NotNil(t, cipherSigil.Obfuscator())
}
func TestCryptoSigil_NewChaChaPolySigil_KeyIsCopied_Good(t *testing.T) {
@ -159,7 +159,7 @@ func TestCryptoSigil_NewChaChaPolySigil_KeyIsCopied_Good(t *testing.T) {
require.NoError(t, err)
key[0] ^= 0xFF
assert.Equal(t, original, cipherSigil.Key)
assert.Equal(t, original, cipherSigil.Key())
}
func TestCryptoSigil_NewChaChaPolySigil_ShortKey_Bad(t *testing.T) {
@ -184,7 +184,7 @@ func TestCryptoSigil_NewChaChaPolySigil_CustomObfuscator_Good(t *testing.T) {
ob := &ShuffleMaskObfuscator{}
cipherSigil, err := NewChaChaPolySigil(key, ob)
require.NoError(t, err)
assert.Equal(t, ob, cipherSigil.Obfuscator)
assert.Equal(t, ob, cipherSigil.Obfuscator())
}
func TestCryptoSigil_NewChaChaPolySigil_CustomObfuscatorNil_Good(t *testing.T) {
@ -193,7 +193,7 @@ func TestCryptoSigil_NewChaChaPolySigil_CustomObfuscatorNil_Good(t *testing.T) {
cipherSigil, err := NewChaChaPolySigil(key, nil)
require.NoError(t, err)
assert.IsType(t, &XORObfuscator{}, cipherSigil.Obfuscator)
assert.IsType(t, &XORObfuscator{}, cipherSigil.Obfuscator())
}
func TestCryptoSigil_NewChaChaPolySigil_CustomObfuscator_InvalidKey_Bad(t *testing.T) {
@ -351,7 +351,7 @@ func TestCryptoSigil_ChaChaPolySigil_NoObfuscator_Good(t *testing.T) {
_, _ = rand.Read(key)
cipherSigil, _ := NewChaChaPolySigil(key, nil)
cipherSigil.Obfuscator = nil
cipherSigil.SetObfuscator(nil)
plaintext := []byte("raw encryption without pre-obfuscation")
ciphertext, err := cipherSigil.In(plaintext)

View file

@ -76,6 +76,8 @@ func (medium *Medium) Write(entryPath, content string) error {
}
// Example: _ = medium.WriteMode("app/theme", "midnight", 0600)
// Note: mode is not persisted — the SQLite store has no entry_mode column.
// Use Write when mode is irrelevant; WriteMode satisfies the Medium interface only.
func (medium *Medium) WriteMode(entryPath, content string, mode fs.FileMode) error {
return medium.Write(entryPath, content)
}
@ -144,23 +146,14 @@ func (medium *Medium) List(entryPath string) ([]fs.DirEntry, error) {
group, key := splitGroupKeyPath(entryPath)
if group == "" {
rows, err := medium.keyValueStore.database.Query("SELECT DISTINCT group_name FROM entries ORDER BY group_name")
groups, err := medium.keyValueStore.ListGroups()
if err != nil {
return nil, core.E("store.List", "query groups", err)
return nil, err
}
defer rows.Close()
var entries []fs.DirEntry
for rows.Next() {
var groupName string
if err := rows.Scan(&groupName); err != nil {
return nil, core.E("store.List", "scan", err)
}
entries := make([]fs.DirEntry, 0, len(groups))
for _, groupName := range groups {
entries = append(entries, &keyValueDirEntry{name: groupName, isDir: true})
}
if err := rows.Err(); err != nil {
return nil, core.E("store.List", "rows", err)
}
return entries, nil
}

View file

@ -112,6 +112,28 @@ func (keyValueStore *KeyValueStore) DeleteGroup(group string) error {
return nil
}
// Example: groups, _ := keyValueStore.ListGroups()
func (keyValueStore *KeyValueStore) ListGroups() ([]string, error) {
rows, err := keyValueStore.database.Query("SELECT DISTINCT group_name FROM entries ORDER BY group_name")
if err != nil {
return nil, core.E("store.ListGroups", "query groups", err)
}
defer rows.Close()
var groups []string
for rows.Next() {
var groupName string
if err := rows.Scan(&groupName); err != nil {
return nil, core.E("store.ListGroups", "scan", err)
}
groups = append(groups, groupName)
}
if err := rows.Err(); err != nil {
return nil, core.E("store.ListGroups", "rows", err)
}
return groups, nil
}
// Example: values, _ := keyValueStore.GetAll("app")
func (keyValueStore *KeyValueStore) GetAll(group string) (map[string]string, error) {
rows, err := keyValueStore.database.Query("SELECT entry_key, entry_value FROM entries WHERE group_name = ?", group)