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 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 <file> - Execute commands from file
|
||||
source <file> - Alias for batch
|
||||
|
6
main.go
6
main.go
@@ -60,6 +60,12 @@ func (c *KVSClient) executeCommand(line string) bool {
|
||||
}
|
||||
case "group":
|
||||
c.handleGroup(args)
|
||||
case "export":
|
||||
c.handleExport(args)
|
||||
case "import":
|
||||
c.handleImport(args)
|
||||
case "export-list":
|
||||
c.handleExportList(args)
|
||||
case "batch", "source":
|
||||
c.handleBatch(args, c.executeCommand)
|
||||
default:
|
||||
|
3
utils.go
3
utils.go
@@ -127,6 +127,9 @@ var completer = readline.NewPrefixCompleter(
|
||||
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"),
|
||||
|
Reference in New Issue
Block a user