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>
223 lines
5.2 KiB
Go
223 lines
5.2 KiB
Go
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)
|
|
}
|