Files
kvs-sh/utils.go
ryyst bd73a1c477 feat: add export/import functionality for backup and migration
Implemented roadmap #4: Export/import functionality.

Features:
- export <file> <key1> [key2...] - Export specified keys to JSON
- export-list <keyfile> <output> - Bulk export from key list file
- import <file> - Import keys from JSON file
  --skip-existing (default) - Skip keys that already exist
  --overwrite - Overwrite existing keys

Export file format (v1.0):
{
  "version": "1.0",
  "entries": [
    {
      "key": "users/alice",
      "uuid": "...",
      "timestamp": 1234567890,
      "data": {...}
    }
  ]
}

The format preserves UUIDs and timestamps for audit trails.
Export shows progress with ✓/✗/⊘ symbols for success/fail/not-found.
Import checks for existing keys and respects skip/overwrite flags.

Use cases:
- Backup: export-list keys.txt backup.json
- Migration: import backup.json --overwrite
- Selective sync: export dest.json key1 key2

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 23:57:25 +03:00

140 lines
4.0 KiB
Go

package main
import (
"fmt"
"strings"
"github.com/chzyer/readline"
"github.com/fatih/color"
)
// Color helpers
var (
cyan = color.New(color.FgCyan).SprintFunc()
green = color.New(color.FgGreen).SprintFunc()
yellow = color.New(color.FgYellow).SprintFunc()
red = color.New(color.FgRed).SprintFunc()
magenta = color.New(color.FgMagenta).SprintFunc()
)
// parseCommand parses command line respecting quotes
func parseCommand(line string) []string {
var parts []string
var current strings.Builder
var quoteChar rune = 0 // Use a rune to store the active quote character (' or ")
for _, ch := range line {
switch {
// If we see a quote character
case ch == '\'' || ch == '"':
if quoteChar == 0 {
// Not currently inside a quoted string, so start one
quoteChar = ch
} else if quoteChar == ch {
// Inside a quoted string and found the matching quote, so end it
quoteChar = 0
} else {
// Inside a quoted string but found the *other* type of quote, treat it as a literal character
current.WriteRune(ch)
}
// If we see a space or tab and are NOT inside a quoted string
case (ch == ' ' || ch == '\t') && quoteChar == 0:
if current.Len() > 0 {
parts = append(parts, current.String())
current.Reset()
}
// Any other character
default:
current.WriteRune(ch)
}
}
// Add the last part if it exists
if current.Len() > 0 {
parts = append(parts, current.String())
}
return parts
}
// printJSONError displays a user-friendly JSON parsing error with helpful hints
func printJSONError(input string, err error) {
fmt.Println(red("❌ Invalid JSON:"), err)
fmt.Println()
// Show what was received
if len(input) > 100 {
fmt.Println(yellow("Received:"), input[:100]+"...")
} else {
fmt.Println(yellow("Received:"), input)
}
fmt.Println()
// Provide helpful hints based on common errors
errMsg := err.Error()
if strings.Contains(errMsg, "looking for beginning of") ||
strings.Contains(errMsg, "invalid character") {
fmt.Println(cyan("💡 Tip:"), "JSON with spaces needs quotes:")
fmt.Println(green(" ✓ Correct:"), "put key '{\"hello\":\"world\"}'")
fmt.Println(green(" ✓ Correct:"), "put key '{\"a\":1,\"b\":2}'")
fmt.Println(green(" ✓ Compact:"), "put key {\"a\":1,\"b\":2} (no spaces)")
fmt.Println(red(" ✗ Wrong: "), "put key {\"a\": 1, \"b\": 2} (spaces without quotes)")
} else if strings.Contains(errMsg, "unexpected end of JSON") {
fmt.Println(cyan("💡 Tip:"), "JSON appears incomplete or cut off")
fmt.Println(green(" Check:"), "Are all brackets/braces matched? {}, []")
fmt.Println(green(" Check:"), "Did spaces split your JSON into multiple arguments?")
} else {
fmt.Println(cyan("💡 Tip:"), "Wrap JSON in quotes for complex values:")
fmt.Println(green(" Single quotes:"), "put key '{\"your\":\"json\"}'")
fmt.Println(green(" Double quotes:"), "put key \"{\\\"your\\\":\\\"json\\\"}\"")
}
}
// Auto-completion setup
var completer = readline.NewPrefixCompleter(
readline.PcItem("connect"),
readline.PcItem("auth"),
readline.PcItem("profile",
readline.PcItem("add"),
readline.PcItem("use"),
readline.PcItem("remove"),
readline.PcItem("save"),
readline.PcItem("load"),
),
readline.PcItem("get"),
readline.PcItem("put"),
readline.PcItem("delete"),
readline.PcItem("meta",
readline.PcItem("get"),
readline.PcItem("set",
readline.PcItem("--owner"),
readline.PcItem("--group"),
readline.PcItem("--permissions"),
),
),
readline.PcItem("members"),
readline.PcItem("health"),
readline.PcItem("user",
readline.PcItem("get"),
readline.PcItem("create"),
),
readline.PcItem("group",
readline.PcItem("create"),
readline.PcItem("get"),
readline.PcItem("update"),
readline.PcItem("delete"),
readline.PcItem("add-member"),
readline.PcItem("remove-member"),
),
readline.PcItem("export"),
readline.PcItem("import"),
readline.PcItem("export-list"),
readline.PcItem("batch"),
readline.PcItem("source"),
readline.PcItem("help"),
readline.PcItem("exit"),
readline.PcItem("quit"),
readline.PcItem("clear"),
)