Adopt sneak.berlin/go/vaultik vanity import path, README overhaul
Module path changed from git.eeqj.de/sneak/vaultik to sneak.berlin/go/vaultik (vanity redirect). All imports, ldflags, Dockerfile, goreleaser config, and docs updated. App data/config directories now use plain "vaultik" instead of the reverse-DNS name. README: - New copy-pasteable quickstart at top: go install, config init, age keypair, config set for key + file:// destination, home backup - All command names in command details are code-quoted - config set/get gained sequence index support (age_recipients.0) so lists are settable from the CLI - Dockerfile build is CGO_ENABLED=0 to match the pure-Go build
This commit is contained in:
@@ -10,16 +10,16 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/vaultik/internal/config"
|
||||
"git.eeqj.de/sneak/vaultik/internal/database"
|
||||
"git.eeqj.de/sneak/vaultik/internal/globals"
|
||||
"git.eeqj.de/sneak/vaultik/internal/log"
|
||||
"git.eeqj.de/sneak/vaultik/internal/pidlock"
|
||||
"git.eeqj.de/sneak/vaultik/internal/snapshot"
|
||||
"git.eeqj.de/sneak/vaultik/internal/storage"
|
||||
"git.eeqj.de/sneak/vaultik/internal/vaultik"
|
||||
"github.com/adrg/xdg"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/vaultik/internal/config"
|
||||
"sneak.berlin/go/vaultik/internal/database"
|
||||
"sneak.berlin/go/vaultik/internal/globals"
|
||||
"sneak.berlin/go/vaultik/internal/log"
|
||||
"sneak.berlin/go/vaultik/internal/pidlock"
|
||||
"sneak.berlin/go/vaultik/internal/snapshot"
|
||||
"sneak.berlin/go/vaultik/internal/storage"
|
||||
"sneak.berlin/go/vaultik/internal/vaultik"
|
||||
)
|
||||
|
||||
// AppOptions contains common options for creating the fx application.
|
||||
@@ -125,7 +125,7 @@ func RunApp(ctx context.Context, app *fx.App) error {
|
||||
// It acquires a PID lock before starting to prevent concurrent instances.
|
||||
func RunWithApp(ctx context.Context, opts AppOptions) error {
|
||||
// Acquire PID lock to prevent concurrent instances
|
||||
lockDir := filepath.Join(xdg.DataHome, "berlin.sneak.app.vaultik")
|
||||
lockDir := filepath.Join(xdg.DataHome, "vaultik")
|
||||
lock, err := pidlock.Acquire(lockDir)
|
||||
if err != nil {
|
||||
if errors.Is(err, pidlock.ErrAlreadyRunning) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
const defaultConfigTemplate = `# vaultik configuration
|
||||
# Documentation: https://git.eeqj.de/sneak/vaultik
|
||||
# Documentation: https://sneak.berlin/go/vaultik
|
||||
|
||||
# ─── REQUIRED ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -195,7 +196,9 @@ storage_url: ""
|
||||
# hostname: myserver
|
||||
|
||||
# Path to the local SQLite index database.
|
||||
# Default: ~/.local/share/berlin.sneak.app.vaultik/index.sqlite
|
||||
# Default: the platform data directory, e.g.
|
||||
# macOS: ~/Library/Application Support/vaultik/index.sqlite
|
||||
# Linux: ~/.local/share/vaultik/index.sqlite
|
||||
# index_path: /path/to/index.sqlite
|
||||
`
|
||||
|
||||
@@ -387,8 +390,9 @@ func loadYAMLFile(path string) (*yaml.Node, error) {
|
||||
return &root, nil
|
||||
}
|
||||
|
||||
// yamlPathGet navigates a dotted key path through mapping nodes and
|
||||
// returns the value node.
|
||||
// yamlPathGet navigates a dotted key path through mapping and sequence
|
||||
// nodes and returns the value node. Numeric path components index into
|
||||
// sequences (e.g. "age_recipients.0").
|
||||
func yamlPathGet(root *yaml.Node, keys []string) (*yaml.Node, error) {
|
||||
node := root
|
||||
if node.Kind == yaml.DocumentNode {
|
||||
@@ -399,19 +403,30 @@ func yamlPathGet(root *yaml.Node, keys []string) (*yaml.Node, error) {
|
||||
}
|
||||
|
||||
for i, key := range keys {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return nil, fmt.Errorf("key %q is not a map", strings.Join(keys[:i], "."))
|
||||
}
|
||||
found := false
|
||||
for j := 0; j+1 < len(node.Content); j += 2 {
|
||||
if node.Content[j].Value == key {
|
||||
node = node.Content[j+1]
|
||||
found = true
|
||||
break
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
found := false
|
||||
for j := 0; j+1 < len(node.Content); j += 2 {
|
||||
if node.Content[j].Value == key {
|
||||
node = node.Content[j+1]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("key not found: %s", strings.Join(keys[:i+1], "."))
|
||||
if !found {
|
||||
return nil, fmt.Errorf("key not found: %s", strings.Join(keys[:i+1], "."))
|
||||
}
|
||||
case yaml.SequenceNode:
|
||||
idx, err := strconv.Atoi(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("key %q is a list; use a numeric index", strings.Join(keys[:i], "."))
|
||||
}
|
||||
if idx < 0 || idx >= len(node.Content) {
|
||||
return nil, fmt.Errorf("index %d out of range for %s (len %d)", idx, strings.Join(keys[:i], "."), len(node.Content))
|
||||
}
|
||||
node = node.Content[idx]
|
||||
default:
|
||||
return nil, fmt.Errorf("key %q is not a map or list", strings.Join(keys[:i], "."))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +434,9 @@ func yamlPathGet(root *yaml.Node, keys []string) (*yaml.Node, error) {
|
||||
}
|
||||
|
||||
// yamlPathSet navigates a dotted key path, creating intermediate maps as
|
||||
// needed, and sets the final key to the given scalar value.
|
||||
// needed, and sets the final key to the given scalar value. Numeric path
|
||||
// components index into sequences; an index equal to the sequence length
|
||||
// appends a new element (e.g. "age_recipients.1" on a 1-element list).
|
||||
func yamlPathSet(root *yaml.Node, keys []string, value string) error {
|
||||
node := root
|
||||
if node.Kind == yaml.DocumentNode {
|
||||
@@ -430,41 +447,67 @@ func yamlPathSet(root *yaml.Node, keys []string, value string) error {
|
||||
}
|
||||
|
||||
for i, key := range keys {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return fmt.Errorf("key %q is not a map", strings.Join(keys[:i], "."))
|
||||
}
|
||||
|
||||
last := i == len(keys)-1
|
||||
|
||||
var valueNode *yaml.Node
|
||||
for j := 0; j+1 < len(node.Content); j += 2 {
|
||||
if node.Content[j].Value == key {
|
||||
valueNode = node.Content[j+1]
|
||||
break
|
||||
switch node.Kind {
|
||||
case yaml.MappingNode:
|
||||
var valueNode *yaml.Node
|
||||
for j := 0; j+1 < len(node.Content); j += 2 {
|
||||
if node.Content[j].Value == key {
|
||||
valueNode = node.Content[j+1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if valueNode == nil {
|
||||
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: key}
|
||||
valueNode = &yaml.Node{Kind: yaml.MappingNode}
|
||||
if last {
|
||||
valueNode = &yaml.Node{Kind: yaml.ScalarNode, Value: value}
|
||||
if valueNode == nil {
|
||||
keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: key}
|
||||
valueNode = &yaml.Node{Kind: yaml.MappingNode}
|
||||
if last {
|
||||
valueNode = &yaml.Node{Kind: yaml.ScalarNode, Value: value}
|
||||
}
|
||||
node.Content = append(node.Content, keyNode, valueNode)
|
||||
} else if last {
|
||||
setScalar(valueNode, value)
|
||||
}
|
||||
node.Content = append(node.Content, keyNode, valueNode)
|
||||
} else if last {
|
||||
valueNode.Kind = yaml.ScalarNode
|
||||
valueNode.Tag = ""
|
||||
valueNode.Value = value
|
||||
valueNode.Content = nil
|
||||
valueNode.Style = 0
|
||||
}
|
||||
|
||||
node = valueNode
|
||||
node = valueNode
|
||||
|
||||
case yaml.SequenceNode:
|
||||
idx, err := strconv.Atoi(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("key %q is a list; use a numeric index", strings.Join(keys[:i], "."))
|
||||
}
|
||||
if idx < 0 || idx > len(node.Content) {
|
||||
return fmt.Errorf("index %d out of range for %s (len %d)", idx, strings.Join(keys[:i], "."), len(node.Content))
|
||||
}
|
||||
if idx == len(node.Content) {
|
||||
newNode := &yaml.Node{Kind: yaml.MappingNode}
|
||||
if last {
|
||||
newNode = &yaml.Node{Kind: yaml.ScalarNode, Value: value}
|
||||
}
|
||||
node.Content = append(node.Content, newNode)
|
||||
} else if last {
|
||||
setScalar(node.Content[idx], value)
|
||||
}
|
||||
node = node.Content[idx]
|
||||
|
||||
default:
|
||||
return fmt.Errorf("key %q is not a map or list", strings.Join(keys[:i], "."))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setScalar overwrites a node in place with a plain scalar value.
|
||||
func setScalar(n *yaml.Node, value string) {
|
||||
n.Kind = yaml.ScalarNode
|
||||
n.Tag = ""
|
||||
n.Value = value
|
||||
n.Content = nil
|
||||
n.Style = 0
|
||||
}
|
||||
|
||||
// configPathForInit returns the config path to write, checking --config flag,
|
||||
// VAULTIK_CONFIG env, and the platform default.
|
||||
func configPathForInit() string {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/vaultik/internal/config"
|
||||
"gopkg.in/yaml.v3"
|
||||
"sneak.berlin/go/vaultik/internal/config"
|
||||
)
|
||||
|
||||
// TestDefaultConfigTemplateParses ensures the init template is valid YAML
|
||||
@@ -45,6 +45,8 @@ func TestDefaultConfigTemplateParses(t *testing.T) {
|
||||
|
||||
const testYAML = `# top comment
|
||||
compression_level: 3
|
||||
age_recipients:
|
||||
- age1aaa
|
||||
s3:
|
||||
bucket: oldbucket # inline comment
|
||||
region: us-east-1
|
||||
@@ -74,6 +76,9 @@ func TestYAMLPathGet(t *testing.T) {
|
||||
{"compression_level", "3", false},
|
||||
{"s3.bucket", "oldbucket", false},
|
||||
{"s3.region", "us-east-1", false},
|
||||
{"age_recipients.0", "age1aaa", false},
|
||||
{"age_recipients.5", "", true},
|
||||
{"age_recipients.notanumber", "", true},
|
||||
{"s3.nonexistent", "", true},
|
||||
{"nonexistent", "", true},
|
||||
{"compression_level.sub", "", true},
|
||||
@@ -114,6 +119,17 @@ func TestYAMLPathSet(t *testing.T) {
|
||||
t.Fatalf("set newmap.newkey: %v", err)
|
||||
}
|
||||
|
||||
// Overwrite a sequence element and append a new one
|
||||
if err := yamlPathSet(root, splitPath("age_recipients.0"), "age1bbb"); err != nil {
|
||||
t.Fatalf("set age_recipients.0: %v", err)
|
||||
}
|
||||
if err := yamlPathSet(root, splitPath("age_recipients.1"), "age1ccc"); err != nil {
|
||||
t.Fatalf("append age_recipients.1: %v", err)
|
||||
}
|
||||
if err := yamlPathSet(root, splitPath("age_recipients.5"), "age1ddd"); err == nil {
|
||||
t.Error("expected out-of-range append to fail")
|
||||
}
|
||||
|
||||
// Round-trip and verify values + comment preservation
|
||||
out, err := yaml.Marshal(root)
|
||||
if err != nil {
|
||||
@@ -121,7 +137,7 @@ func TestYAMLPathSet(t *testing.T) {
|
||||
}
|
||||
text := string(out)
|
||||
|
||||
for _, want := range []string{"newbucket", "s3.example.com", "newkey: val", "# top comment", "# inline comment"} {
|
||||
for _, want := range []string{"newbucket", "s3.example.com", "newkey: val", "# top comment", "# inline comment", "age1bbb", "age1ccc"} {
|
||||
if !contains(text, want) {
|
||||
t.Errorf("round-tripped YAML missing %q:\n%s", want, text)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.eeqj.de/sneak/vaultik/internal/config"
|
||||
"git.eeqj.de/sneak/vaultik/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
"sneak.berlin/go/vaultik/internal/config"
|
||||
"sneak.berlin/go/vaultik/internal/log"
|
||||
)
|
||||
|
||||
// NewDatabaseCommand creates the database command group
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"git.eeqj.de/sneak/vaultik/internal/log"
|
||||
"git.eeqj.de/sneak/vaultik/internal/vaultik"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/vaultik/internal/log"
|
||||
"sneak.berlin/go/vaultik/internal/vaultik"
|
||||
)
|
||||
|
||||
// NewInfoCommand creates the info command
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"git.eeqj.de/sneak/vaultik/internal/log"
|
||||
"git.eeqj.de/sneak/vaultik/internal/vaultik"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/vaultik/internal/log"
|
||||
"sneak.berlin/go/vaultik/internal/vaultik"
|
||||
)
|
||||
|
||||
// NewPruneCommand creates the prune command
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"git.eeqj.de/sneak/vaultik/internal/log"
|
||||
"git.eeqj.de/sneak/vaultik/internal/vaultik"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/vaultik/internal/log"
|
||||
"sneak.berlin/go/vaultik/internal/vaultik"
|
||||
)
|
||||
|
||||
// NewRemoteCommand creates the remote command and subcommands
|
||||
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"git.eeqj.de/sneak/vaultik/internal/config"
|
||||
"git.eeqj.de/sneak/vaultik/internal/globals"
|
||||
"git.eeqj.de/sneak/vaultik/internal/log"
|
||||
"git.eeqj.de/sneak/vaultik/internal/storage"
|
||||
"git.eeqj.de/sneak/vaultik/internal/vaultik"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/vaultik/internal/config"
|
||||
"sneak.berlin/go/vaultik/internal/globals"
|
||||
"sneak.berlin/go/vaultik/internal/log"
|
||||
"sneak.berlin/go/vaultik/internal/storage"
|
||||
"sneak.berlin/go/vaultik/internal/vaultik"
|
||||
)
|
||||
|
||||
// RestoreOptions contains options for the restore command
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"git.eeqj.de/sneak/vaultik/internal/log"
|
||||
"git.eeqj.de/sneak/vaultik/internal/vaultik"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/vaultik/internal/log"
|
||||
"sneak.berlin/go/vaultik/internal/vaultik"
|
||||
)
|
||||
|
||||
// NewSnapshotCommand creates the snapshot command and subcommands
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.eeqj.de/sneak/vaultik/internal/log"
|
||||
"git.eeqj.de/sneak/vaultik/internal/storage"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/vaultik/internal/log"
|
||||
"sneak.berlin/go/vaultik/internal/storage"
|
||||
)
|
||||
|
||||
// StoreApp contains dependencies for store commands
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"git.eeqj.de/sneak/vaultik/internal/globals"
|
||||
"github.com/spf13/cobra"
|
||||
"sneak.berlin/go/vaultik/internal/globals"
|
||||
)
|
||||
|
||||
// NewVersionCommand creates the version command
|
||||
|
||||
Reference in New Issue
Block a user