Files
node/cmd/backup/main.go
T

275 lines
7.8 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Command backup provides CLI tools for database backup and restore operations.
//
// Usage:
//
// # Full backup with zstd compression
// luxd backup --output /path/to/backup.zst --since 0
//
// # Incremental backup (using version from last backup)
// luxd backup --output /path/to/backup-incr.zst --since 12345678
//
// # Restore from backup
// luxd restore --input /path/to/backup.zst
//
// # Get last backup metadata
// luxd backup-info --data-dir ~/.lux/mainnet
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/luxfi/database"
databasefactory "github.com/luxfi/database/factory"
"github.com/luxfi/log"
"github.com/luxfi/metric"
"github.com/luxfi/node/service/backup"
)
var (
// Flags
outputPath string
inputPath string
sinceVer uint64
dataDir string
dbType string
noCompress bool
forceRestore bool
)
func main() {
rootCmd := &cobra.Command{
Use: "backup",
Short: "Database backup and restore utilities",
Long: `Backup and restore utilities for Lux node database.
Supports:
- Full and incremental backups using ZapDB native backup
- Zstd compression (enabled by default for .zst extension)
- Works with both ZapDB and PebbleDB`,
}
// backup command
backupCmd := &cobra.Command{
Use: "backup",
Short: "Create a database backup",
Long: `Create a backup of the node database.
Examples:
# Full backup with zstd compression
luxd backup --output /backups/lux-backup.zst --data-dir ~/.lux/mainnet
# Incremental backup (pass version from previous backup)
luxd backup --output /backups/lux-backup-incr.zst --since 12345678 --data-dir ~/.lux/mainnet
# Backup without compression
luxd backup --output /backups/lux-backup.db --no-compress --data-dir ~/.lux/mainnet`,
RunE: runBackup,
}
backupCmd.Flags().StringVarP(&outputPath, "output", "o", "", "Output file path (required)")
backupCmd.Flags().Uint64VarP(&sinceVer, "since", "s", 0, "Version for incremental backup (0 for full)")
backupCmd.Flags().StringVarP(&dataDir, "data-dir", "d", "", "Node data directory (required)")
backupCmd.Flags().StringVar(&dbType, "db-type", "zapdb", "Database type (zapdb or pebbledb)")
backupCmd.Flags().BoolVar(&noCompress, "no-compress", false, "Disable zstd compression")
backupCmd.MarkFlagRequired("output")
backupCmd.MarkFlagRequired("data-dir")
// restore command
restoreCmd := &cobra.Command{
Use: "restore",
Short: "Restore database from backup",
Long: `Restore the node database from a backup file.
WARNING: This will overwrite the existing database!
Examples:
# Restore from compressed backup
luxd restore --input /backups/lux-backup.zst --data-dir ~/.lux/mainnet
# Force restore (skip confirmation)
luxd restore --input /backups/lux-backup.zst --data-dir ~/.lux/mainnet --force`,
RunE: runRestore,
}
restoreCmd.Flags().StringVarP(&inputPath, "input", "i", "", "Input backup file path (required)")
restoreCmd.Flags().StringVarP(&dataDir, "data-dir", "d", "", "Node data directory (required)")
restoreCmd.Flags().StringVar(&dbType, "db-type", "zapdb", "Database type (zapdb or pebbledb)")
restoreCmd.Flags().BoolVarP(&forceRestore, "force", "f", false, "Skip confirmation prompt")
restoreCmd.MarkFlagRequired("input")
restoreCmd.MarkFlagRequired("data-dir")
// info command
infoCmd := &cobra.Command{
Use: "backup-info",
Short: "Show backup metadata",
Long: `Display information about the last backup.
Examples:
luxd backup-info --data-dir ~/.lux/mainnet`,
RunE: runInfo,
}
infoCmd.Flags().StringVarP(&dataDir, "data-dir", "d", "", "Node data directory (required)")
infoCmd.MarkFlagRequired("data-dir")
rootCmd.AddCommand(backupCmd, restoreCmd, infoCmd)
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func runBackup(cmd *cobra.Command, args []string) error {
logger := log.New("cmd", "backup")
// Open database
dbPath := filepath.Join(dataDir, "db")
db, err := openDatabase(dbPath, dbType, logger)
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
defer db.Close()
// Create backup service
backupSvc, err := backup.New(backup.Config{
DB: db,
MetadataDir: filepath.Join(dataDir, "backup"),
Log: logger,
})
if err != nil {
return fmt.Errorf("failed to create backup service: %w", err)
}
// Determine compression
compress := !noCompress && (strings.HasSuffix(outputPath, ".zst") || strings.HasSuffix(outputPath, ".zstd"))
fmt.Printf("Starting backup...\n")
fmt.Printf(" Output: %s\n", outputPath)
fmt.Printf(" Since version: %d\n", sinceVer)
fmt.Printf(" Compression: %v\n", compress)
version, err := backupSvc.BackupToFile(outputPath, sinceVer, compress)
if err != nil {
return fmt.Errorf("backup failed: %w", err)
}
// Get file size
info, err := os.Stat(outputPath)
if err != nil {
return fmt.Errorf("failed to stat backup file: %w", err)
}
fmt.Printf("\nBackup completed successfully!\n")
fmt.Printf(" Version: %d\n", version)
fmt.Printf(" Size: %s\n", formatBytes(info.Size()))
fmt.Printf(" Incremental: %v\n", sinceVer > 0)
fmt.Printf("\nFor next incremental backup, use: --since %d\n", version)
return nil
}
func runRestore(cmd *cobra.Command, args []string) error {
logger := log.New("cmd", "restore")
// Check if backup file exists
if _, err := os.Stat(inputPath); os.IsNotExist(err) {
return fmt.Errorf("backup file not found: %s", inputPath)
}
// Confirm restore
if !forceRestore {
fmt.Printf("WARNING: This will overwrite the database at %s\n", dataDir)
fmt.Printf("Are you sure you want to continue? (yes/no): ")
var response string
fmt.Scanln(&response)
if response != "yes" {
fmt.Println("Restore cancelled.")
return nil
}
}
// Open database
dbPath := filepath.Join(dataDir, "db")
db, err := openDatabase(dbPath, dbType, logger)
if err != nil {
return fmt.Errorf("failed to open database: %w", err)
}
defer db.Close()
// Create backup service
backupSvc, err := backup.New(backup.Config{
DB: db,
MetadataDir: filepath.Join(dataDir, "backup"),
Log: logger,
})
if err != nil {
return fmt.Errorf("failed to create backup service: %w", err)
}
// Determine if compressed
compressed := strings.HasSuffix(inputPath, ".zst") || strings.HasSuffix(inputPath, ".zstd")
fmt.Printf("Starting restore...\n")
fmt.Printf(" Input: %s\n", inputPath)
fmt.Printf(" Compressed: %v\n", compressed)
if err := backupSvc.RestoreFromFile(inputPath, compressed); err != nil {
return fmt.Errorf("restore failed: %w", err)
}
fmt.Printf("\nRestore completed successfully!\n")
return nil
}
func runInfo(cmd *cobra.Command, args []string) error {
metadataPath := filepath.Join(dataDir, "backup", backup.MetadataFileName)
data, err := os.ReadFile(metadataPath)
if os.IsNotExist(err) {
fmt.Println("No backup metadata found.")
fmt.Printf("Metadata path: %s\n", metadataPath)
return nil
}
if err != nil {
return fmt.Errorf("failed to read metadata: %w", err)
}
fmt.Println("Backup Metadata:")
fmt.Println(string(data))
return nil
}
func openDatabase(path string, dbType string, logger log.Logger) (database.Database, error) {
// Ensure directory exists
if err := os.MkdirAll(path, 0755); err != nil {
return nil, err
}
gatherer := metric.NewRegistry()
db, err := databasefactory.New(dbType, path, false, nil, gatherer, logger, "backup", "db")
if err != nil {
return nil, err
}
return db, nil
}
func formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}