Initial commit: Disaster recovery CLI tool
A Go-based CLI tool for recovering servers from backups to new cloud VMs. Features: - Multi-cloud support: Exoscale, Cloudscale, Hetzner Cloud - Backup sources: Local filesystem, Hetzner Storage Box - 6-stage restore pipeline with /etc whitelist protection - DNS migration with safety checks and auto-rollback - Dry-run by default, requires --yes to execute - Cloud-init for SSH key injection Packages: - cmd/recover-server: CLI commands (recover, migrate-dns, list, cleanup) - internal/providers: Cloud provider implementations - internal/backup: Backup source implementations - internal/restore: 6-stage restore pipeline - internal/dns: Exoscale DNS management - internal/ui: Prompts, progress, dry-run display - internal/config: Environment and host configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
111
internal/ui/progress.go
Normal file
111
internal/ui/progress.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Progress tracks and displays progress
|
||||
type Progress struct {
|
||||
Total int
|
||||
Current int
|
||||
StartTime time.Time
|
||||
Message string
|
||||
}
|
||||
|
||||
// NewProgress creates a new progress tracker
|
||||
func NewProgress(total int, message string) *Progress {
|
||||
return &Progress{
|
||||
Total: total,
|
||||
Current: 0,
|
||||
StartTime: time.Now(),
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// Increment advances progress by one
|
||||
func (p *Progress) Increment() {
|
||||
p.Current++
|
||||
p.Print()
|
||||
}
|
||||
|
||||
// SetMessage updates the current message
|
||||
func (p *Progress) SetMessage(msg string) {
|
||||
p.Message = msg
|
||||
}
|
||||
|
||||
// Print displays current progress
|
||||
func (p *Progress) Print() {
|
||||
elapsed := time.Since(p.StartTime)
|
||||
percent := float64(p.Current) / float64(p.Total) * 100
|
||||
|
||||
bar := p.bar(20)
|
||||
|
||||
fmt.Printf("\r[%s] %.1f%% (%d/%d) %s - %s ",
|
||||
bar, percent, p.Current, p.Total, elapsed.Round(time.Second), p.Message)
|
||||
}
|
||||
|
||||
// Complete marks progress as done
|
||||
func (p *Progress) Complete() {
|
||||
p.Current = p.Total
|
||||
elapsed := time.Since(p.StartTime)
|
||||
fmt.Printf("\r[%s] 100%% (%d/%d) %s - Complete\n",
|
||||
p.bar(20), p.Total, p.Total, elapsed.Round(time.Second))
|
||||
}
|
||||
|
||||
func (p *Progress) bar(width int) string {
|
||||
filled := int(float64(p.Current) / float64(p.Total) * float64(width))
|
||||
empty := width - filled
|
||||
|
||||
return strings.Repeat("█", filled) + strings.Repeat("░", empty)
|
||||
}
|
||||
|
||||
// Spinner displays a spinning indicator
|
||||
type Spinner struct {
|
||||
Message string
|
||||
stop chan bool
|
||||
done chan bool
|
||||
}
|
||||
|
||||
// NewSpinner creates a new spinner
|
||||
func NewSpinner(message string) *Spinner {
|
||||
return &Spinner{
|
||||
Message: message,
|
||||
stop: make(chan bool),
|
||||
done: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the spinner animation
|
||||
func (s *Spinner) Start() {
|
||||
go func() {
|
||||
frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||
i := 0
|
||||
for {
|
||||
select {
|
||||
case <-s.stop:
|
||||
s.done <- true
|
||||
return
|
||||
default:
|
||||
fmt.Printf("\r%s %s", frames[i%len(frames)], s.Message)
|
||||
i++
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop stops the spinner
|
||||
func (s *Spinner) Stop() {
|
||||
s.stop <- true
|
||||
<-s.done
|
||||
fmt.Printf("\r%s\n", strings.Repeat(" ", len(s.Message)+5))
|
||||
}
|
||||
|
||||
// StopWithMessage stops spinner with a final message
|
||||
func (s *Spinner) StopWithMessage(msg string) {
|
||||
s.stop <- true
|
||||
<-s.done
|
||||
fmt.Printf("\r%s\n", msg)
|
||||
}
|
||||
Reference in New Issue
Block a user