Add WireGuard TUI implementation
- Add Go TUI with bubbletea for WireGuard management - Implement client CRUD operations with QR code generation - Add configuration and validation modules - Install/update scripts for client setup - Update Makefile to build binaries to bin/ directory - Add .gitignore for Go projects
This commit is contained in:
333
internal/tui/screens/restore.go
Normal file
333
internal/tui/screens/restore.go
Normal file
@@ -0,0 +1,333 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/calmcacil/wg-admin/internal/backup"
|
||||
"github.com/calmcacil/wg-admin/internal/tui/components"
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// RestoreScreen displays a list of available backups for restoration
|
||||
type RestoreScreen struct {
|
||||
table table.Model
|
||||
backups []backup.Backup
|
||||
selectedBackup *backup.Backup
|
||||
confirmModal *components.ConfirmModel
|
||||
showConfirm bool
|
||||
isRestoring bool
|
||||
restoreError error
|
||||
restoreSuccess bool
|
||||
message string
|
||||
}
|
||||
|
||||
// Styles
|
||||
var (
|
||||
restoreTitleStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("62")).
|
||||
Bold(true)
|
||||
restoreHelpStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("63")).
|
||||
MarginTop(1)
|
||||
restoreSuccessStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("46")).
|
||||
Bold(true)
|
||||
restoreErrorStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("196")).
|
||||
Bold(true)
|
||||
restoreInfoStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("241")).
|
||||
MarginTop(1)
|
||||
)
|
||||
|
||||
// NewRestoreScreen creates a new restore screen
|
||||
func NewRestoreScreen() *RestoreScreen {
|
||||
return &RestoreScreen{
|
||||
showConfirm: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the restore screen
|
||||
func (s *RestoreScreen) Init() tea.Cmd {
|
||||
return s.loadBackups
|
||||
}
|
||||
|
||||
// Update handles messages for the restore screen
|
||||
func (s *RestoreScreen) Update(msg tea.Msg) (Screen, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
|
||||
// Handle confirmation modal
|
||||
if s.showConfirm && s.confirmModal != nil {
|
||||
_, cmd = s.confirmModal.Update(msg)
|
||||
|
||||
// Handle confirmation result
|
||||
if !s.confirmModal.Visible {
|
||||
if s.confirmModal.IsConfirmed() && s.selectedBackup != nil {
|
||||
// User confirmed restore
|
||||
s.isRestoring = true
|
||||
s.showConfirm = false
|
||||
return s, s.performRestore()
|
||||
}
|
||||
// User cancelled - close modal
|
||||
s.showConfirm = false
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Handle Enter key to confirm
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
if msg.String() == "enter" && s.confirmModal.IsConfirmed() && s.selectedBackup != nil {
|
||||
s.isRestoring = true
|
||||
s.showConfirm = false
|
||||
return s, s.performRestore()
|
||||
}
|
||||
}
|
||||
|
||||
return s, cmd
|
||||
}
|
||||
|
||||
// Handle normal screen messages
|
||||
switch msg := msg.(type) {
|
||||
case backupsLoadedMsg:
|
||||
s.backups = msg.backups
|
||||
s.buildTable()
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "q", "esc":
|
||||
// Return to list screen - signal parent to switch screens
|
||||
return s, nil
|
||||
case "enter":
|
||||
// Show confirmation for selected backup
|
||||
if len(s.table.Rows()) > 0 {
|
||||
selected := s.table.SelectedRow()
|
||||
if len(selected) > 0 {
|
||||
// Find the backup by name
|
||||
for _, b := range s.backups {
|
||||
if b.Name == selected[0] {
|
||||
s.selectedBackup = &b
|
||||
s.confirmModal = components.NewConfirm(
|
||||
fmt.Sprintf(
|
||||
"Are you sure you want to restore from backup '%s'?\n\nOperation: %s\nDate: %s\n\nThis will replace current WireGuard configuration.\nA safety backup will be created first.",
|
||||
b.Name,
|
||||
b.Operation,
|
||||
b.Timestamp.Format("2006-01-02 15:04:05"),
|
||||
),
|
||||
80,
|
||||
24,
|
||||
)
|
||||
s.showConfirm = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case restoreCompletedMsg:
|
||||
s.isRestoring = false
|
||||
if msg.err != nil {
|
||||
s.restoreError = msg.err
|
||||
s.message = fmt.Sprintf("Restore failed: %v", msg.err)
|
||||
} else {
|
||||
s.restoreSuccess = true
|
||||
s.message = fmt.Sprintf("Restore successful! Safety backup created at: %s", msg.safetyBackupPath)
|
||||
}
|
||||
}
|
||||
|
||||
if !s.showConfirm && s.confirmModal != nil {
|
||||
s.table, cmd = s.table.Update(msg)
|
||||
}
|
||||
return s, cmd
|
||||
}
|
||||
|
||||
// View renders the restore screen
|
||||
func (s *RestoreScreen) View() string {
|
||||
if s.showConfirm && s.confirmModal != nil {
|
||||
// Render underlying content dimmed
|
||||
content := s.renderContent()
|
||||
dimmedContent := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("244")).
|
||||
Render(content)
|
||||
|
||||
// Overlay confirmation modal
|
||||
return lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
dimmedContent,
|
||||
s.confirmModal.View(),
|
||||
)
|
||||
}
|
||||
|
||||
return s.renderContent()
|
||||
}
|
||||
|
||||
// renderContent renders the main restore screen content
|
||||
func (s *RestoreScreen) renderContent() string {
|
||||
var content strings.Builder
|
||||
|
||||
content.WriteString(restoreTitleStyle.Render("Restore WireGuard Configuration"))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
if len(s.backups) == 0 && !s.isRestoring && s.message == "" {
|
||||
content.WriteString("No backups found. Press 'q' to return.")
|
||||
return content.String()
|
||||
}
|
||||
|
||||
if s.isRestoring {
|
||||
content.WriteString("Restoring from backup, please wait...")
|
||||
return content.String()
|
||||
}
|
||||
|
||||
if s.restoreSuccess {
|
||||
content.WriteString(restoreSuccessStyle.Render("✓ " + s.message))
|
||||
content.WriteString("\n\n")
|
||||
content.WriteString(restoreInfoStyle.Render("Press 'q' to return to client list."))
|
||||
return content.String()
|
||||
}
|
||||
|
||||
if s.restoreError != nil {
|
||||
content.WriteString(restoreErrorStyle.Render("✗ " + s.message))
|
||||
content.WriteString("\n\n")
|
||||
content.WriteString(s.table.View())
|
||||
content.WriteString("\n\n")
|
||||
content.WriteString(restoreHelpStyle.Render("Actions: [Enter] Restore Selected • [↑/↓] Navigate • [q] Back"))
|
||||
return content.String()
|
||||
}
|
||||
|
||||
// Show backup list
|
||||
content.WriteString(s.table.View())
|
||||
content.WriteString("\n\n")
|
||||
|
||||
// Show selected backup details
|
||||
if len(s.table.Rows()) > 0 && s.selectedBackup != nil {
|
||||
content.WriteString(restoreInfoStyle.Render(
|
||||
fmt.Sprintf(
|
||||
"Selected: %s (%s) - %s\nSize: %s",
|
||||
s.selectedBackup.Operation,
|
||||
s.selectedBackup.Timestamp.Format("2006-01-02 15:04:05"),
|
||||
s.selectedBackup.Name,
|
||||
formatBytes(s.selectedBackup.Size),
|
||||
),
|
||||
))
|
||||
content.WriteString("\n")
|
||||
}
|
||||
|
||||
content.WriteString(restoreHelpStyle.Render("Actions: [Enter] Restore Selected • [↑/↓] Navigate • [q] Back"))
|
||||
|
||||
return content.String()
|
||||
}
|
||||
|
||||
// loadBackups loads the list of available backups
|
||||
func (s *RestoreScreen) loadBackups() tea.Msg {
|
||||
backups, err := backup.ListBackups()
|
||||
if err != nil {
|
||||
return errMsg{err: err}
|
||||
}
|
||||
return backupsLoadedMsg{backups: backups}
|
||||
}
|
||||
|
||||
// buildTable creates and configures the backup list table
|
||||
func (s *RestoreScreen) buildTable() {
|
||||
columns := []table.Column{
|
||||
{Title: "Name", Width: 40},
|
||||
{Title: "Operation", Width: 15},
|
||||
{Title: "Date", Width: 20},
|
||||
{Title: "Size", Width: 12},
|
||||
}
|
||||
|
||||
var rows []table.Row
|
||||
for _, b := range s.backups {
|
||||
row := table.Row{
|
||||
b.Name,
|
||||
b.Operation,
|
||||
b.Timestamp.Format("2006-01-02 15:04"),
|
||||
formatBytes(b.Size),
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
|
||||
s.table = table.New(
|
||||
table.WithColumns(columns),
|
||||
table.WithRows(rows),
|
||||
table.WithFocused(true),
|
||||
table.WithHeight(len(rows)+2), // Header + rows
|
||||
)
|
||||
|
||||
// Apply styles
|
||||
s.setTableStyles()
|
||||
}
|
||||
|
||||
// setTableStyles applies styling to the table
|
||||
func (s *RestoreScreen) setTableStyles() {
|
||||
styles := table.DefaultStyles()
|
||||
styles.Header = styles.Header.
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("240")).
|
||||
BorderBottom(true).
|
||||
Bold(true)
|
||||
styles.Selected = styles.Selected.
|
||||
Foreground(lipgloss.Color("229")).
|
||||
Background(lipgloss.Color("57")).
|
||||
Bold(false)
|
||||
s.table.SetStyles(styles)
|
||||
}
|
||||
|
||||
// performRestore performs the restore operation
|
||||
func (s *RestoreScreen) performRestore() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
if s.selectedBackup == nil {
|
||||
return restoreCompletedMsg{
|
||||
err: fmt.Errorf("no backup selected"),
|
||||
}
|
||||
}
|
||||
|
||||
// Get safety backup path from backup.BackupConfig
|
||||
safetyBackupPath, err := backup.BackupConfig(fmt.Sprintf("pre-restore-from-%s", s.selectedBackup.Name))
|
||||
if err != nil {
|
||||
return restoreCompletedMsg{
|
||||
err: fmt.Errorf("failed to create safety backup: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
// Perform restore
|
||||
if err := backup.RestoreBackup(s.selectedBackup.Name); err != nil {
|
||||
return restoreCompletedMsg{
|
||||
err: err,
|
||||
safetyBackupPath: safetyBackupPath,
|
||||
}
|
||||
}
|
||||
|
||||
// Restore succeeded - trigger client list refresh
|
||||
return restoreCompletedMsg{
|
||||
safetyBackupPath: safetyBackupPath,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// formatBytes formats a byte count into human-readable format
|
||||
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 %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
// Messages
|
||||
|
||||
// backupsLoadedMsg is sent when backups are loaded
|
||||
type backupsLoadedMsg struct {
|
||||
backups []backup.Backup
|
||||
}
|
||||
|
||||
// restoreCompletedMsg is sent when a restore operation completes
|
||||
type restoreCompletedMsg struct {
|
||||
err error
|
||||
safetyBackupPath string
|
||||
}
|
||||
Reference in New Issue
Block a user