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>
This commit is contained in:
222
cmd_export.go
Normal file
222
cmd_export.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExportEntry represents a single exported key-value pair
|
||||||
|
type ExportEntry struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportFile represents the export file format
|
||||||
|
type ExportFile struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
Entries []ExportEntry `json:"entries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleExport exports keys to a JSON file
|
||||||
|
func (c *KVSClient) handleExport(args []string) {
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Println(red("Usage: export <output-file> <key1> [key2] [key3] ..."))
|
||||||
|
fmt.Println("Example: export backup.json users/alice users/bob config/settings")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFile := args[0]
|
||||||
|
keys := args[1:]
|
||||||
|
|
||||||
|
exportFile := ExportFile{
|
||||||
|
Version: "1.0",
|
||||||
|
Entries: []ExportEntry{},
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(cyan("Exporting"), len(keys), "key(s)...")
|
||||||
|
|
||||||
|
successCount := 0
|
||||||
|
failCount := 0
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
respBody, status, err := c.doRequest("GET", "/kv/"+key, nil)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(red("✗")+" %s: %v\n", key, err)
|
||||||
|
failCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if status == 404 {
|
||||||
|
fmt.Printf(yellow("⊘")+" %s: not found\n", key)
|
||||||
|
failCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if status != 200 {
|
||||||
|
fmt.Printf(red("✗")+" %s: status %d\n", key, status)
|
||||||
|
failCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var stored StoredValue
|
||||||
|
if err := json.Unmarshal(respBody, &stored); err != nil {
|
||||||
|
fmt.Printf(red("✗")+" %s: parse error: %v\n", key, err)
|
||||||
|
failCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
exportFile.Entries = append(exportFile.Entries, ExportEntry{
|
||||||
|
Key: key,
|
||||||
|
UUID: stored.UUID,
|
||||||
|
Timestamp: stored.Timestamp,
|
||||||
|
Data: stored.Data,
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Printf(green("✓")+" %s\n", key)
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file
|
||||||
|
data, err := json.MarshalIndent(exportFile, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(red("Error marshaling export data:"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(outputFile, data, 0644); err != nil {
|
||||||
|
fmt.Println(red("Error writing file:"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(green("Export complete:"))
|
||||||
|
fmt.Printf(" Exported: %d\n", successCount)
|
||||||
|
if failCount > 0 {
|
||||||
|
fmt.Printf(" Failed: %s\n", red(fmt.Sprintf("%d", failCount)))
|
||||||
|
}
|
||||||
|
fmt.Printf(" File: %s\n", outputFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleImport imports keys from a JSON file
|
||||||
|
func (c *KVSClient) handleImport(args []string) {
|
||||||
|
if len(args) < 1 {
|
||||||
|
fmt.Println(red("Usage: import <input-file> [--skip-existing | --overwrite]"))
|
||||||
|
fmt.Println(" --skip-existing: Skip keys that already exist (default)")
|
||||||
|
fmt.Println(" --overwrite: Overwrite existing keys")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inputFile := args[0]
|
||||||
|
overwrite := false
|
||||||
|
|
||||||
|
// Parse flags
|
||||||
|
for _, arg := range args[1:] {
|
||||||
|
if arg == "--overwrite" {
|
||||||
|
overwrite = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file
|
||||||
|
data, err := os.ReadFile(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(red("Error reading file:"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var exportFile ExportFile
|
||||||
|
if err := json.Unmarshal(data, &exportFile); err != nil {
|
||||||
|
fmt.Println(red("Error parsing export file:"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(cyan("Importing"), len(exportFile.Entries), "key(s)...")
|
||||||
|
if exportFile.Version != "1.0" {
|
||||||
|
fmt.Println(yellow("Warning: Unknown export version"), exportFile.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
successCount := 0
|
||||||
|
skipCount := 0
|
||||||
|
failCount := 0
|
||||||
|
|
||||||
|
for _, entry := range exportFile.Entries {
|
||||||
|
// Check if key exists (unless we're overwriting)
|
||||||
|
if !overwrite {
|
||||||
|
_, status, err := c.doRequest("GET", "/kv/"+entry.Key, nil)
|
||||||
|
if err == nil && status == 200 {
|
||||||
|
fmt.Printf(yellow("⊘")+" %s: exists (skipped)\n", entry.Key)
|
||||||
|
skipCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import the key
|
||||||
|
respBody, status, err := c.doRequest("PUT", "/kv/"+entry.Key, entry.Data)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(red("✗")+" %s: %v\n", entry.Key, err)
|
||||||
|
failCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if status != 200 && status != 201 {
|
||||||
|
fmt.Printf(red("✗")+" %s: status %d: %s\n", entry.Key, status, string(respBody))
|
||||||
|
failCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf(green("✓")+" %s\n", entry.Key)
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(green("Import complete:"))
|
||||||
|
fmt.Printf(" Imported: %d\n", successCount)
|
||||||
|
if skipCount > 0 {
|
||||||
|
fmt.Printf(" Skipped: %d\n", skipCount)
|
||||||
|
}
|
||||||
|
if failCount > 0 {
|
||||||
|
fmt.Printf(" Failed: %s\n", red(fmt.Sprintf("%d", failCount)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleExportList reads keys from a file and exports them
|
||||||
|
func (c *KVSClient) handleExportList(args []string) {
|
||||||
|
if len(args) < 2 {
|
||||||
|
fmt.Println(red("Usage: export-list <key-list-file> <output-file>"))
|
||||||
|
fmt.Println("Example: export-list keys.txt backup.json")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("key-list-file should contain one key per line")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
keyListFile := args[0]
|
||||||
|
outputFile := args[1]
|
||||||
|
|
||||||
|
// Read keys from file
|
||||||
|
data, err := os.ReadFile(keyListFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(red("Error reading key list file:"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
keys := []string{}
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" && !strings.HasPrefix(line, "#") {
|
||||||
|
keys = append(keys, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(keys) == 0 {
|
||||||
|
fmt.Println(yellow("No keys found in file"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call export with the keys
|
||||||
|
exportArgs := append([]string{outputFile}, keys...)
|
||||||
|
c.handleExport(exportArgs)
|
||||||
|
}
|
@@ -60,6 +60,13 @@ Group Management:
|
|||||||
group add-member <uuid> <user-uuid> - Add member to group
|
group add-member <uuid> <user-uuid> - Add member to group
|
||||||
group remove-member <uuid> <user-uuid> - Remove member from group
|
group remove-member <uuid> <user-uuid> - Remove member from group
|
||||||
|
|
||||||
|
Export/Import:
|
||||||
|
export <file> <key1> ... - Export keys to JSON file
|
||||||
|
export-list <keyfile> <output> - Export keys listed in file
|
||||||
|
import <file> - Import keys from JSON file
|
||||||
|
--skip-existing - Skip existing keys (default)
|
||||||
|
--overwrite - Overwrite existing keys
|
||||||
|
|
||||||
Batch Operations:
|
Batch Operations:
|
||||||
batch <file> - Execute commands from file
|
batch <file> - Execute commands from file
|
||||||
source <file> - Alias for batch
|
source <file> - Alias for batch
|
||||||
|
6
main.go
6
main.go
@@ -60,6 +60,12 @@ func (c *KVSClient) executeCommand(line string) bool {
|
|||||||
}
|
}
|
||||||
case "group":
|
case "group":
|
||||||
c.handleGroup(args)
|
c.handleGroup(args)
|
||||||
|
case "export":
|
||||||
|
c.handleExport(args)
|
||||||
|
case "import":
|
||||||
|
c.handleImport(args)
|
||||||
|
case "export-list":
|
||||||
|
c.handleExportList(args)
|
||||||
case "batch", "source":
|
case "batch", "source":
|
||||||
c.handleBatch(args, c.executeCommand)
|
c.handleBatch(args, c.executeCommand)
|
||||||
default:
|
default:
|
||||||
|
3
utils.go
3
utils.go
@@ -127,6 +127,9 @@ var completer = readline.NewPrefixCompleter(
|
|||||||
readline.PcItem("add-member"),
|
readline.PcItem("add-member"),
|
||||||
readline.PcItem("remove-member"),
|
readline.PcItem("remove-member"),
|
||||||
),
|
),
|
||||||
|
readline.PcItem("export"),
|
||||||
|
readline.PcItem("import"),
|
||||||
|
readline.PcItem("export-list"),
|
||||||
readline.PcItem("batch"),
|
readline.PcItem("batch"),
|
||||||
readline.PcItem("source"),
|
readline.PcItem("source"),
|
||||||
readline.PcItem("help"),
|
readline.PcItem("help"),
|
||||||
|
Reference in New Issue
Block a user