mirror of
https://github.com/luxfi/node.git
synced 2026-07-27 03:39:39 +00:00
275 lines
7.8 KiB
Go
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])
|
|
}
|