Files
kvs-sh/cmd_export.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

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)
}