From 3a1661f8a575e22271abb7af0cbe1b2bcc6b24be Mon Sep 17 00:00:00 2001 From: tis24dev Date: Mon, 30 Mar 2026 09:20:34 +0200 Subject: [PATCH 1/2] Rename decryptCandidate to backupCandidate Rename the decryptCandidate type to backupCandidate across the orchestrator package and update all related function signatures and usages. Add a new backup_candidate_display.go with formatting helpers (created, hostname, mode, tool, target, compression, base, summary) and accompanying tests to centralize UI display logic. Update discovery, selection, preparation, integrity verification, restore, and UI functions to use backupCandidate and the new display helpers, and adjust tests accordingly to reflect the new type and output formatting. LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%) --- .../orchestrator/additional_helpers_test.go | 2 +- .../orchestrator/backup_candidate_display.go | 176 ++++++++++++++++++ .../backup_candidate_display_test.go | 119 ++++++++++++ internal/orchestrator/backup_sources.go | 18 +- internal/orchestrator/decrypt.go | 32 ++-- internal/orchestrator/decrypt_integrity.go | 4 +- .../orchestrator/decrypt_integrity_test.go | 6 +- .../orchestrator/decrypt_prepare_common.go | 2 +- internal/orchestrator/decrypt_test.go | 66 +++---- internal/orchestrator/decrypt_tui.go | 22 +-- internal/orchestrator/decrypt_tui_test.go | 6 +- .../orchestrator/decrypt_workflow_test.go | 2 +- internal/orchestrator/decrypt_workflow_ui.go | 6 +- .../orchestrator/decrypt_workflow_ui_test.go | 8 +- internal/orchestrator/restore.go | 4 +- .../restore_coverage_extra_test.go | 2 +- internal/orchestrator/restore_errors_test.go | 6 +- .../restore_workflow_abort_test.go | 4 +- .../restore_workflow_decision_test.go | 6 +- .../restore_workflow_errors_test.go | 4 +- .../restore_workflow_more_test.go | 16 +- .../orchestrator/restore_workflow_test.go | 8 +- internal/orchestrator/restore_workflow_ui.go | 4 +- .../restore_workflow_ui_helpers_test.go | 2 +- .../restore_workflow_warnings_test.go | 4 +- internal/orchestrator/workflow_ui.go | 2 +- internal/orchestrator/workflow_ui_cli.go | 2 +- .../orchestrator/workflow_ui_tui_decrypt.go | 103 +++------- 28 files changed, 435 insertions(+), 201 deletions(-) create mode 100644 internal/orchestrator/backup_candidate_display.go create mode 100644 internal/orchestrator/backup_candidate_display_test.go diff --git a/internal/orchestrator/additional_helpers_test.go b/internal/orchestrator/additional_helpers_test.go index c1b6caf6..0beb9628 100644 --- a/internal/orchestrator/additional_helpers_test.go +++ b/internal/orchestrator/additional_helpers_test.go @@ -962,7 +962,7 @@ func TestPromptClusterRestoreMode(t *testing.T) { } func TestConfirmRestoreAction(t *testing.T) { - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: &backup.Manifest{ CreatedAt: time.Now(), ArchivePath: "test.tar", diff --git a/internal/orchestrator/backup_candidate_display.go b/internal/orchestrator/backup_candidate_display.go new file mode 100644 index 00000000..379b2879 --- /dev/null +++ b/internal/orchestrator/backup_candidate_display.go @@ -0,0 +1,176 @@ +package orchestrator + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/tis24dev/proxsave/internal/backup" +) + +const ( + backupCandidateDisplayTimeLayout = "2006-01-02 15:04:05" + unknownBackupDateText = "unknown date" + unknownBackupHostText = "unknown host" + unknownBackupTargetText = "UNKNOWN" +) + +type backupCandidateDisplay struct { + Created string + Hostname string + Mode string + Tool string + Target string + Compression string + Base string + Summary string +} + +func describeBackupCandidate(cand *backupCandidate) backupCandidateDisplay { + display := backupCandidateDisplay{ + Created: formatBackupCandidateCreated(cand), + Hostname: formatBackupCandidateHostname(cand), + Mode: formatBackupCandidateMode(cand), + Tool: formatBackupCandidateTool(cand), + Target: formatBackupCandidateTarget(candidateManifest(cand)), + Compression: formatBackupCandidateCompression(cand), + Base: backupCandidateBaseName(cand), + } + display.Summary = formatBackupCandidateSummary(display) + return display +} + +func candidateManifest(cand *backupCandidate) *backup.Manifest { + if cand == nil { + return nil + } + return cand.Manifest +} + +func formatBackupCandidateCreated(cand *backupCandidate) string { + manifest := candidateManifest(cand) + if manifest == nil || manifest.CreatedAt.IsZero() { + return unknownBackupDateText + } + return manifest.CreatedAt.Format(backupCandidateDisplayTimeLayout) +} + +func formatBackupCandidateHostname(cand *backupCandidate) string { + manifest := candidateManifest(cand) + if manifest == nil { + return unknownBackupHostText + } + host := strings.TrimSpace(manifest.Hostname) + if host == "" { + return unknownBackupHostText + } + return host +} + +func formatBackupCandidateMode(cand *backupCandidate) string { + manifest := candidateManifest(cand) + if manifest == nil { + return "UNKNOWN" + } + mode := strings.ToUpper(statusFromManifest(manifest)) + if mode == "" { + return "UNKNOWN" + } + return mode +} + +func formatBackupCandidateTool(cand *backupCandidate) string { + manifest := candidateManifest(cand) + if manifest == nil { + return "Tool unknown" + } + version := strings.TrimSpace(manifest.ScriptVersion) + if version == "" { + return "Tool unknown" + } + if !strings.HasPrefix(strings.ToLower(version), "v") { + version = "v" + version + } + return "Tool " + version +} + +func formatBackupCandidateTarget(manifest *backup.Manifest) string { + if manifest == nil { + return unknownBackupTargetText + } + + targets := formatTargets(manifest) + targets = strings.TrimSpace(targets) + if targets == "" || targets == "unknown target" { + targets = unknownBackupTargetText + } else { + targets = strings.ToUpper(targets) + } + + version := normalizeProxmoxVersion(manifest.ProxmoxVersion) + if version != "" { + targets = fmt.Sprintf("%s %s", targets, version) + } + + if cluster := formatClusterMode(manifest.ClusterMode); cluster != "" { + targets = fmt.Sprintf("%s (%s)", targets, cluster) + } + + return targets +} + +func formatBackupCandidateCompression(cand *backupCandidate) string { + manifest := candidateManifest(cand) + if manifest == nil { + return "" + } + compression := strings.TrimSpace(manifest.CompressionType) + if compression == "" { + return "" + } + return strings.ToUpper(compression) +} + +func backupCandidateBaseName(cand *backupCandidate) string { + if cand == nil { + return "" + } + base := strings.TrimSpace(cand.DisplayBase) + if base != "" { + return base + } + + switch { + case strings.TrimSpace(cand.BundlePath) != "": + return filepath.Base(strings.TrimSpace(cand.BundlePath)) + case strings.TrimSpace(cand.RawArchivePath) != "": + return filepath.Base(strings.TrimSpace(cand.RawArchivePath)) + default: + return "" + } +} + +func formatBackupCandidateSummary(display backupCandidateDisplay) string { + parts := make([]string, 0, 2) + + if display.Hostname != "" && display.Hostname != unknownBackupHostText { + parts = append(parts, display.Hostname) + } + + switch { + case display.Base != "" && display.Created != "" && display.Created != unknownBackupDateText: + parts = append(parts, fmt.Sprintf("%s (%s)", display.Base, display.Created)) + case display.Base != "": + parts = append(parts, display.Base) + case display.Created != "" && display.Created != unknownBackupDateText: + parts = append(parts, display.Created) + } + + if len(parts) == 0 { + if display.Hostname != "" { + return display.Hostname + } + return display.Created + } + return strings.Join(parts, " • ") +} diff --git a/internal/orchestrator/backup_candidate_display_test.go b/internal/orchestrator/backup_candidate_display_test.go new file mode 100644 index 00000000..7f1dc78a --- /dev/null +++ b/internal/orchestrator/backup_candidate_display_test.go @@ -0,0 +1,119 @@ +package orchestrator + +import ( + "bufio" + "context" + "strings" + "testing" + "time" + + "github.com/tis24dev/proxsave/internal/backup" +) + +func TestDescribeBackupCandidate_Full(t *testing.T) { + created := time.Date(2026, time.March, 22, 12, 21, 22, 0, time.UTC) + cand := &backupCandidate{ + DisplayBase: "backup.tar.xz", + Manifest: &backup.Manifest{ + CreatedAt: created, + Hostname: "node1.example.com", + EncryptionMode: "age", + ScriptVersion: "1.2.3", + ProxmoxTargets: []string{"pve"}, + ProxmoxVersion: "8.0", + ClusterMode: "cluster", + CompressionType: "xz", + }, + } + + display := describeBackupCandidate(cand) + if display.Created != "2026-03-22 12:21:22" { + t.Fatalf("Created=%q", display.Created) + } + if display.Hostname != "node1.example.com" { + t.Fatalf("Hostname=%q", display.Hostname) + } + if display.Mode != "ENCRYPTED" { + t.Fatalf("Mode=%q", display.Mode) + } + if display.Tool != "Tool v1.2.3" { + t.Fatalf("Tool=%q", display.Tool) + } + if display.Target != "PVE v8.0 (cluster)" { + t.Fatalf("Target=%q", display.Target) + } + if display.Compression != "XZ" { + t.Fatalf("Compression=%q", display.Compression) + } + if display.Summary != "node1.example.com • backup.tar.xz (2026-03-22 12:21:22)" { + t.Fatalf("Summary=%q", display.Summary) + } +} + +func TestDescribeBackupCandidate_Fallbacks(t *testing.T) { + cand := &backupCandidate{ + RawArchivePath: "/tmp/archive.tar.xz", + Manifest: &backup.Manifest{}, + } + + display := describeBackupCandidate(cand) + if display.Created != unknownBackupDateText { + t.Fatalf("Created=%q, want %q", display.Created, unknownBackupDateText) + } + if display.Hostname != unknownBackupHostText { + t.Fatalf("Hostname=%q, want %q", display.Hostname, unknownBackupHostText) + } + if display.Mode != "PLAIN" { + t.Fatalf("Mode=%q, want %q", display.Mode, "PLAIN") + } + if display.Tool != "Tool unknown" { + t.Fatalf("Tool=%q, want %q", display.Tool, "Tool unknown") + } + if display.Target != unknownBackupTargetText { + t.Fatalf("Target=%q, want %q", display.Target, unknownBackupTargetText) + } + if display.Base != "archive.tar.xz" { + t.Fatalf("Base=%q, want %q", display.Base, "archive.tar.xz") + } + if display.Summary != "archive.tar.xz" { + t.Fatalf("Summary=%q, want %q", display.Summary, "archive.tar.xz") + } +} + +func TestBackupSummaryForUI_UsesSharedDisplayModel(t *testing.T) { + created := time.Date(2026, time.March, 22, 12, 21, 22, 0, time.UTC) + cand := &backupCandidate{ + DisplayBase: "backup.tar.xz", + Manifest: &backup.Manifest{ + CreatedAt: created, + Hostname: "node1.example.com", + }, + } + + if got := backupSummaryForUI(cand); got != "node1.example.com • backup.tar.xz (2026-03-22 12:21:22)" { + t.Fatalf("backupSummaryForUI()=%q", got) + } +} + +func TestPromptCandidateSelection_PrintsHostname(t *testing.T) { + reader := bufio.NewReader(strings.NewReader("0\n")) + candidates := []*backupCandidate{ + { + Manifest: &backup.Manifest{ + CreatedAt: time.Date(2026, time.March, 22, 12, 21, 22, 0, time.UTC), + Hostname: "node1.example.com", + EncryptionMode: "age", + ScriptVersion: "1.2.3", + ProxmoxType: "pve", + }, + }, + } + + stdout := captureCLIStdout(t, func() { + _, _ = promptCandidateSelection(context.Background(), reader, candidates) + }) + + if !strings.Contains(stdout, "Host node1.example.com") { + t.Fatalf("expected hostname in CLI output, got %q", stdout) + } +} diff --git a/internal/orchestrator/backup_sources.go b/internal/orchestrator/backup_sources.go index eb858720..86b5f2b6 100644 --- a/internal/orchestrator/backup_sources.go +++ b/internal/orchestrator/backup_sources.go @@ -89,8 +89,8 @@ func buildDecryptPathOptions(cfg *config.Config, logger *logging.Logger) (option } // discoverRcloneBackups lists backup candidates from an rclone remote and returns -// decrypt candidates backed by that remote (bundles and raw archives). -func discoverRcloneBackups(ctx context.Context, cfg *config.Config, remotePath string, logger *logging.Logger, report ProgressReporter) (candidates []*decryptCandidate, err error) { +// backup candidates backed by that remote (bundles and raw archives). +func discoverRcloneBackups(ctx context.Context, cfg *config.Config, remotePath string, logger *logging.Logger, report ProgressReporter) (candidates []*backupCandidate, err error) { done := logging.DebugStart(logger, "discover rclone backups", "remote=%s", remotePath) defer func() { done(err) }() start := time.Now() @@ -128,7 +128,7 @@ func discoverRcloneBackups(ctx context.Context, cfg *config.Config, remotePath s } logging.DebugStep(logger, "discover rclone backups", "rclone lsf output bytes=%d elapsed=%s", len(output), time.Since(lsfStart)) - candidates = make([]*decryptCandidate, 0) + candidates = make([]*backupCandidate, 0) lines := strings.Split(string(output), "\n") totalEntries := len(lines) @@ -238,7 +238,7 @@ func discoverRcloneBackups(ctx context.Context, cfg *config.Config, remotePath s if strings.TrimSpace(displayBase) == "" { displayBase = filepath.Base(item.filename) } - candidates = append(candidates, &decryptCandidate{ + candidates = append(candidates, &backupCandidate{ Manifest: manifest, Source: sourceBundle, BundlePath: item.remoteBundle, @@ -295,7 +295,7 @@ func discoverRcloneBackups(ctx context.Context, cfg *config.Config, remotePath s if strings.TrimSpace(displayBase) == "" { displayBase = filepath.Base(baseNameFromRemoteRef(item.remoteArchive)) } - candidates = append(candidates, &decryptCandidate{ + candidates = append(candidates, &backupCandidate{ Manifest: manifest, Source: sourceRaw, RawArchivePath: item.remoteArchive, @@ -353,7 +353,7 @@ func discoverRcloneBackups(ctx context.Context, cfg *config.Config, remotePath s // discoverBackupCandidates scans a local or mounted directory for backup // candidates (bundle or raw triplet: archive + metadata + checksum). -func discoverBackupCandidates(logger *logging.Logger, root string) (candidates []*decryptCandidate, err error) { +func discoverBackupCandidates(logger *logging.Logger, root string) (candidates []*backupCandidate, err error) { done := logging.DebugStart(logger, "discover backup candidates", "root=%s", root) defer func() { done(err) }() entries, err := restoreFS.ReadDir(root) @@ -362,7 +362,7 @@ func discoverBackupCandidates(logger *logging.Logger, root string) (candidates [ } logging.DebugStep(logger, "discover backup candidates", "entries=%d", len(entries)) - candidates = make([]*decryptCandidate, 0) + candidates = make([]*backupCandidate, 0) rawBases := make(map[string]struct{}) filesSeen := 0 dirsSkipped := 0 @@ -395,7 +395,7 @@ func discoverBackupCandidates(logger *logging.Logger, root string) (candidates [ continue } logging.DebugStep(logger, "discover backup candidates", "bundle accepted: %s created_at=%s", name, manifest.CreatedAt.Format(time.RFC3339)) - candidates = append(candidates, &decryptCandidate{ + candidates = append(candidates, &backupCandidate{ Manifest: manifest, Source: sourceBundle, BundlePath: fullPath, @@ -453,7 +453,7 @@ func discoverBackupCandidates(logger *logging.Logger, root string) (candidates [ } rawBases[baseName] = struct{}{} - candidates = append(candidates, &decryptCandidate{ + candidates = append(candidates, &backupCandidate{ Manifest: manifest, Source: sourceRaw, RawArchivePath: archivePath, diff --git a/internal/orchestrator/decrypt.go b/internal/orchestrator/decrypt.go index 83b44f7f..9ce590d5 100644 --- a/internal/orchestrator/decrypt.go +++ b/internal/orchestrator/decrypt.go @@ -36,7 +36,7 @@ const ( sourceRaw ) -type decryptCandidate struct { +type backupCandidate struct { Manifest *backup.Manifest Source decryptSourceType BundlePath string @@ -99,7 +99,7 @@ func RunDecryptWorkflow(ctx context.Context, cfg *config.Config, logger *logging return RunDecryptWorkflowWithDeps(ctx, &deps, version) } -func selectDecryptCandidate(ctx context.Context, reader *bufio.Reader, cfg *config.Config, logger *logging.Logger, requireEncrypted bool) (candidate *decryptCandidate, err error) { +func selectDecryptCandidate(ctx context.Context, reader *bufio.Reader, cfg *config.Config, logger *logging.Logger, requireEncrypted bool) (candidate *backupCandidate, err error) { done := logging.DebugStart(logger, "select backup candidate", "requireEncrypted=%v", requireEncrypted) defer func() { done(err) }() @@ -334,18 +334,20 @@ func inspectRcloneMetadataManifest(ctx context.Context, remoteMetadataPath, remo return legacy, nil } -func promptCandidateSelection(ctx context.Context, reader *bufio.Reader, candidates []*decryptCandidate) (*decryptCandidate, error) { +func promptCandidateSelection(ctx context.Context, reader *bufio.Reader, candidates []*backupCandidate) (*backupCandidate, error) { for { fmt.Println("\nAvailable backups:") for idx, cand := range candidates { - created := cand.Manifest.CreatedAt.Format("2006-01-02 15:04:05") - enc := strings.ToUpper(statusFromManifest(cand.Manifest)) - toolVersion := cand.Manifest.ScriptVersion - if toolVersion == "" { - toolVersion = "unknown" - } - targetSummary := formatTargetSummary(cand.Manifest) - fmt.Printf(" [%d] %s • %s • Tool v%s • %s\n", idx+1, created, enc, toolVersion, targetSummary) + display := describeBackupCandidate(cand) + fmt.Printf( + " [%d] %s • Host %s • %s • %s • %s\n", + idx+1, + display.Created, + display.Hostname, + display.Mode, + display.Tool, + display.Target, + ) } fmt.Println(" [0] Exit") @@ -429,12 +431,12 @@ func downloadRcloneBackup(ctx context.Context, remotePath string, logger *loggin return tmpPath, cleanup, nil } -func preparePlainBundle(ctx context.Context, reader *bufio.Reader, cand *decryptCandidate, version string, logger *logging.Logger) (bundle *preparedBundle, err error) { +func preparePlainBundle(ctx context.Context, reader *bufio.Reader, cand *backupCandidate, version string, logger *logging.Logger) (bundle *preparedBundle, err error) { ui := newCLIWorkflowUI(reader, logger) return preparePlainBundleWithUI(ctx, cand, version, logger, ui) } -func prepareDecryptedBackup(ctx context.Context, reader *bufio.Reader, cfg *config.Config, logger *logging.Logger, version string, requireEncrypted bool) (candidate *decryptCandidate, prepared *preparedBundle, err error) { +func prepareDecryptedBackup(ctx context.Context, reader *bufio.Reader, cfg *config.Config, logger *logging.Logger, version string, requireEncrypted bool) (candidate *backupCandidate, prepared *preparedBundle, err error) { done := logging.DebugStart(logger, "prepare decrypted backup", "requireEncrypted=%v", requireEncrypted) defer func() { done(err) }() candidate, err = selectDecryptCandidate(ctx, reader, cfg, logger, requireEncrypted) @@ -546,7 +548,7 @@ func extractBundleToWorkdirWithLogger(bundlePath, workDir string, logger *loggin return staged, nil } -func copyRawArtifactsToWorkdir(ctx context.Context, cand *decryptCandidate, workDir string) (staged stagedFiles, err error) { +func copyRawArtifactsToWorkdir(ctx context.Context, cand *backupCandidate, workDir string) (staged stagedFiles, err error) { return copyRawArtifactsToWorkdirWithLogger(ctx, cand, workDir, nil) } @@ -577,7 +579,7 @@ func rcloneCopyTo(ctx context.Context, remotePath, localPath string, showProgres return cmd.Run() } -func copyRawArtifactsToWorkdirWithLogger(ctx context.Context, cand *decryptCandidate, workDir string, logger *logging.Logger) (staged stagedFiles, err error) { +func copyRawArtifactsToWorkdirWithLogger(ctx context.Context, cand *backupCandidate, workDir string, logger *logging.Logger) (staged stagedFiles, err error) { done := logging.DebugStart(logger, "stage raw artifacts", "archive=%s workdir=%s rclone=%v", cand.RawArchivePath, workDir, cand.IsRclone) defer func() { done(err) }() if ctx == nil { diff --git a/internal/orchestrator/decrypt_integrity.go b/internal/orchestrator/decrypt_integrity.go index aba57845..ec1b3f38 100644 --- a/internal/orchestrator/decrypt_integrity.go +++ b/internal/orchestrator/decrypt_integrity.go @@ -57,7 +57,7 @@ func resolveStagedIntegrityExpectation(staged stagedFiles, manifest *backup.Mani return resolveIntegrityExpectationValues(checksumFromFile, checksumFromManifest) } -func resolveCandidateIntegrityExpectation(staged stagedFiles, cand *decryptCandidate) (*stagedIntegrityExpectation, error) { +func resolveCandidateIntegrityExpectation(staged stagedFiles, cand *backupCandidate) (*stagedIntegrityExpectation, error) { if cand != nil && cand.Integrity != nil && strings.TrimSpace(cand.Integrity.Checksum) != "" { normalized, err := backup.NormalizeChecksum(cand.Integrity.Checksum) if err != nil { @@ -95,7 +95,7 @@ func resolveCandidateIntegrityExpectation(staged stagedFiles, cand *decryptCandi return resolveStagedIntegrityExpectation(staged, manifest) } -func verifyStagedArchiveIntegrity(ctx context.Context, logger *logging.Logger, staged stagedFiles, cand *decryptCandidate) (string, error) { +func verifyStagedArchiveIntegrity(ctx context.Context, logger *logging.Logger, staged stagedFiles, cand *backupCandidate) (string, error) { if staged.ArchivePath == "" { return "", fmt.Errorf("staged archive path is empty") } diff --git a/internal/orchestrator/decrypt_integrity_test.go b/internal/orchestrator/decrypt_integrity_test.go index a77b6853..f34c99c1 100644 --- a/internal/orchestrator/decrypt_integrity_test.go +++ b/internal/orchestrator/decrypt_integrity_test.go @@ -60,7 +60,7 @@ func TestPreparePlainBundle_RejectsMissingChecksumVerification(t *testing.T) { t.Fatalf("write metadata: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: manifest, Source: sourceRaw, RawArchivePath: archivePath, @@ -109,7 +109,7 @@ func TestPreparePlainBundle_RejectsChecksumMismatch(t *testing.T) { t.Fatalf("write checksum: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: manifest, Source: sourceRaw, RawArchivePath: archivePath, @@ -144,7 +144,7 @@ func TestVerifyStagedArchiveIntegrity_UsesCandidateIntegrityExpectation(t *testi got, err := verifyStagedArchiveIntegrity(context.Background(), logging.New(types.LogLevelError, false), stagedFiles{ ArchivePath: archivePath, - }, &decryptCandidate{ + }, &backupCandidate{ Integrity: &stagedIntegrityExpectation{ Checksum: strings.ToUpper(checksumHexForBytes(archiveData)), Source: "checksum file", diff --git a/internal/orchestrator/decrypt_prepare_common.go b/internal/orchestrator/decrypt_prepare_common.go index 20b85ad6..f35813f3 100644 --- a/internal/orchestrator/decrypt_prepare_common.go +++ b/internal/orchestrator/decrypt_prepare_common.go @@ -62,7 +62,7 @@ func resolvePreparedArchivePath(workDir, stagedArchivePath, currentEncryption st return filepath.Join(workDir, archiveBase), nil } -func preparePlainBundleCommon(ctx context.Context, cand *decryptCandidate, version string, logger *logging.Logger, decryptArchive archiveDecryptFunc) (bundle *preparedBundle, err error) { +func preparePlainBundleCommon(ctx context.Context, cand *backupCandidate, version string, logger *logging.Logger, decryptArchive archiveDecryptFunc) (bundle *preparedBundle, err error) { if cand == nil || cand.Manifest == nil { return nil, fmt.Errorf("invalid backup candidate") } diff --git a/internal/orchestrator/decrypt_test.go b/internal/orchestrator/decrypt_test.go index e6efd30b..4932b1bf 100644 --- a/internal/orchestrator/decrypt_test.go +++ b/internal/orchestrator/decrypt_test.go @@ -1044,7 +1044,7 @@ func TestPromptPathSelection_InvalidIndexRetries(t *testing.T) { func TestPromptCandidateSelection_Abort(t *testing.T) { reader := bufio.NewReader(strings.NewReader("0\n")) - candidates := []*decryptCandidate{ + candidates := []*backupCandidate{ {Manifest: &backup.Manifest{EncryptionMode: "age"}}, } @@ -1057,7 +1057,7 @@ func TestPromptCandidateSelection_Abort(t *testing.T) { func TestPromptCandidateSelection_EmptyInputRetries(t *testing.T) { // Empty input, then valid selection reader := bufio.NewReader(strings.NewReader("\n\n1\n")) - candidates := []*decryptCandidate{ + candidates := []*backupCandidate{ {Manifest: &backup.Manifest{EncryptionMode: "age"}}, } @@ -1073,7 +1073,7 @@ func TestPromptCandidateSelection_EmptyInputRetries(t *testing.T) { func TestPromptCandidateSelection_InvalidIndexRetries(t *testing.T) { // Invalid index, then valid selection reader := bufio.NewReader(strings.NewReader("99\n1\n")) - candidates := []*decryptCandidate{ + candidates := []*backupCandidate{ {Manifest: &backup.Manifest{EncryptionMode: "age"}}, } @@ -1537,7 +1537,7 @@ func TestPreparePlainBundle_UnsupportedSource(t *testing.T) { restoreFS = osFS{} t.Cleanup(func() { restoreFS = origFS }) - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: &backup.Manifest{}, Source: decryptSourceType(99), // Invalid source } @@ -1573,7 +1573,7 @@ func TestPreparePlainBundle_SourceBundleSuccess(t *testing.T) { {name: "backup.sha256", data: checksumLineForBytes("archive.tar.xz", archiveData)}, }) - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: &backup.Manifest{ArchivePath: filepath.Join(dir, "archive.tar.xz"), EncryptionMode: "none"}, Source: sourceBundle, BundlePath: bundlePath, @@ -1605,7 +1605,7 @@ func TestPreparePlainBundle_ExtractError(t *testing.T) { restoreFS = osFS{} t.Cleanup(func() { restoreFS = origFS }) - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: &backup.Manifest{EncryptionMode: "none"}, Source: sourceBundle, BundlePath: "/nonexistent/bundle.tar", @@ -1937,7 +1937,7 @@ func TestCopyRawArtifactsToWorkdir_Success(t *testing.T) { t.Fatalf("write checksum: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ RawArchivePath: archivePath, RawMetadataPath: metadataPath, RawChecksumPath: checksumPath, @@ -1957,7 +1957,7 @@ func TestCopyRawArtifactsToWorkdir_ArchiveError(t *testing.T) { restoreFS = osFS{} t.Cleanup(func() { restoreFS = origFS }) - cand := &decryptCandidate{ + cand := &backupCandidate{ RawArchivePath: "/nonexistent/archive.tar.xz", RawMetadataPath: "/nonexistent/backup.metadata", RawChecksumPath: "/nonexistent/backup.sha256", @@ -1986,7 +1986,7 @@ func TestCopyRawArtifactsToWorkdir_MetadataError(t *testing.T) { t.Fatalf("write archive: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ RawArchivePath: archivePath, RawMetadataPath: "/nonexistent/backup.metadata", RawChecksumPath: "/nonexistent/backup.sha256", @@ -2019,7 +2019,7 @@ func TestCopyRawArtifactsToWorkdir_ChecksumError(t *testing.T) { t.Fatalf("write metadata: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ RawArchivePath: archivePath, RawMetadataPath: metadataPath, RawChecksumPath: "/nonexistent/backup.sha256", @@ -2085,7 +2085,7 @@ esac t.Setenv("METADATA_SRC", metadataSrc) t.Setenv("CHECKSUM_SRC", checksumSrc) - cand := &decryptCandidate{ + cand := &backupCandidate{ IsRclone: true, RawArchivePath: "gdrive:backup.tar.xz", RawMetadataPath: "gdrive:backup.tar.xz.metadata", @@ -2552,7 +2552,7 @@ func TestCopyRawArtifactsToWorkdir_ContextWorks(t *testing.T) { t.Fatalf("write metadata: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ RawArchivePath: archivePath, RawMetadataPath: metadataPath, RawChecksumPath: "", @@ -2575,7 +2575,7 @@ func TestCopyRawArtifactsToWorkdir_InvalidRclonePaths(t *testing.T) { workDir := t.TempDir() // Candidate with rclone but empty paths after colon - cand := &decryptCandidate{ + cand := &backupCandidate{ IsRclone: true, RawArchivePath: "gdrive:", // Empty path after colon RawMetadataPath: "gdrive:m", // Valid @@ -2765,7 +2765,7 @@ func TestPreparePlainBundle_CopyFileSamePath(t *testing.T) { t.Fatalf("write checksum: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: manifest, Source: sourceRaw, RawArchivePath: archivePath, @@ -2857,7 +2857,7 @@ esac return []byte(id.String()), nil } - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: manifest, Source: sourceBundle, BundlePath: "gdrive:backup.bundle.tar", @@ -2899,7 +2899,7 @@ func TestPreparePlainBundleCommon_TrimmedAgeEncryptionTriggersDecrypt(t *testing t.Fatalf("write checksum: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: &backup.Manifest{ ArchivePath: workArchive, EncryptionMode: " age ", @@ -2965,7 +2965,7 @@ func TestPreparePlainBundleCommon_AgeModeRequiresAgeSuffix(t *testing.T) { t.Fatalf("write checksum: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: &backup.Manifest{ ArchivePath: workArchive, EncryptionMode: "age", @@ -3016,7 +3016,7 @@ func TestPreparePlainBundleCommon_NonAgeRejectsAgeSuffix(t *testing.T) { t.Fatalf("write checksum: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: &backup.Manifest{ ArchivePath: workArchive, EncryptionMode: "none", @@ -3154,7 +3154,7 @@ func TestPreparePlainBundle_SourceBundleAdditional(t *testing.T) { tw.Close() f.Close() - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: manifest, Source: sourceBundle, BundlePath: bundlePath, @@ -3326,7 +3326,7 @@ func TestCopyRawArtifactsToWorkdir_WithChecksum(t *testing.T) { t.Fatalf("write checksum: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ RawArchivePath: archivePath, RawMetadataPath: metadataPath, RawChecksumPath: checksumPath, @@ -3418,7 +3418,7 @@ func TestPromptPathSelection_InvalidThenValid(t *testing.T) { func TestPromptCandidateSelection_Exit(t *testing.T) { now := time.Now() - cands := []*decryptCandidate{ + cands := []*backupCandidate{ { Manifest: &backup.Manifest{ CreatedAt: now, @@ -3444,7 +3444,7 @@ func TestPreparePlainBundle_MkdirAllError(t *testing.T) { restoreFS = fake defer func() { restoreFS = orig }() - cand := &decryptCandidate{ + cand := &backupCandidate{ Source: sourceBundle, BundlePath: "/bundle.tar", Manifest: &backup.Manifest{EncryptionMode: "none"}, @@ -3470,7 +3470,7 @@ func TestPreparePlainBundle_MkdirTempError(t *testing.T) { restoreFS = fake defer func() { restoreFS = orig }() - cand := &decryptCandidate{ + cand := &backupCandidate{ Source: sourceBundle, BundlePath: "/bundle.tar", Manifest: &backup.Manifest{EncryptionMode: "none"}, @@ -3627,7 +3627,7 @@ exit 0 t.Fatalf("mkdir: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ IsRclone: true, RawArchivePath: "remote:backup.tar.xz", RawMetadataPath: "remote:backup.metadata", @@ -3683,7 +3683,7 @@ fi t.Fatalf("mkdir: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ IsRclone: true, RawArchivePath: "remote:backup.tar.xz", RawMetadataPath: "remote:backup.metadata", @@ -3912,7 +3912,7 @@ func TestPreparePlainBundle_StatErrorAfterExtract(t *testing.T) { restoreFS = fake defer func() { restoreFS = orig }() - cand := &decryptCandidate{ + cand := &backupCandidate{ Source: sourceBundle, BundlePath: bundlePath, Manifest: &backup.Manifest{EncryptionMode: "none", Hostname: "test"}, @@ -3951,7 +3951,7 @@ exit 1 prependPathEnv(t, tmp) - cand := &decryptCandidate{ + cand := &backupCandidate{ Source: sourceBundle, BundlePath: "remote:backup.bundle.tar", IsRclone: true, @@ -4013,7 +4013,7 @@ exit 1 // Call preparePlainBundle with rclone candidate // It will first download (success), then try MkdirAll for tempRoot - cand := &decryptCandidate{ + cand := &backupCandidate{ Source: sourceBundle, BundlePath: "remote:backup.bundle.tar", IsRclone: true, @@ -4194,7 +4194,7 @@ func TestPreparePlainBundle_CopyFileError(t *testing.T) { restoreFS = fake defer func() { restoreFS = orig }() - cand := &decryptCandidate{ + cand := &backupCandidate{ Source: sourceBundle, BundlePath: bundlePath, Manifest: &backup.Manifest{EncryptionMode: "none", Hostname: "test"}, @@ -4295,7 +4295,7 @@ func TestPreparePlainBundle_StatErrorOnPlainArchive(t *testing.T) { restoreFS = fake defer func() { restoreFS = orig }() - cand := &decryptCandidate{ + cand := &backupCandidate{ Source: sourceBundle, BundlePath: bundlePath, Manifest: &backup.Manifest{EncryptionMode: "none", Hostname: "test"}, @@ -4361,7 +4361,7 @@ exit 0 restoreFS = osFS{} defer func() { restoreFS = orig }() - cand := &decryptCandidate{ + cand := &backupCandidate{ Source: sourceBundle, BundlePath: "remote:backup.bundle.tar", IsRclone: true, @@ -4429,7 +4429,7 @@ func TestPreparePlainBundle_GenerateChecksumErrorPath(t *testing.T) { restoreFS = fake defer func() { restoreFS = orig }() - cand := &decryptCandidate{ + cand := &backupCandidate{ Source: sourceBundle, BundlePath: bundlePath, Manifest: &backup.Manifest{EncryptionMode: "none", Hostname: "test"}, @@ -4507,7 +4507,7 @@ exit 0 restoreFS = fake defer func() { restoreFS = orig }() - cand := &decryptCandidate{ + cand := &backupCandidate{ Source: sourceBundle, BundlePath: "remote:backup.bundle.tar", IsRclone: true, diff --git a/internal/orchestrator/decrypt_tui.go b/internal/orchestrator/decrypt_tui.go index 04ea8aad..350a31cd 100644 --- a/internal/orchestrator/decrypt_tui.go +++ b/internal/orchestrator/decrypt_tui.go @@ -45,23 +45,7 @@ func RunDecryptWorkflowTUI(ctx context.Context, cfg *config.Config, logger *logg } func buildTargetInfo(manifest *backup.Manifest) string { - targets := formatTargets(manifest) - if targets == "" { - targets = "unknown" - } else { - targets = strings.ToUpper(targets) - } - - version := normalizeProxmoxVersion(manifest.ProxmoxVersion) - if version != "" { - targets = fmt.Sprintf("%s %s", targets, version) - } - - if cluster := formatClusterMode(manifest.ClusterMode); cluster != "" { - targets = fmt.Sprintf("%s (%s)", targets, cluster) - } - - return fmt.Sprintf("Targets: %s", targets) + return fmt.Sprintf("Targets: %s", formatBackupCandidateTarget(manifest)) } func normalizeProxmoxVersion(value string) string { @@ -75,11 +59,11 @@ func normalizeProxmoxVersion(value string) string { return version } -func filterEncryptedCandidates(candidates []*decryptCandidate) []*decryptCandidate { +func filterEncryptedCandidates(candidates []*backupCandidate) []*backupCandidate { if len(candidates) == 0 { return candidates } - filtered := make([]*decryptCandidate, 0, len(candidates)) + filtered := make([]*backupCandidate, 0, len(candidates)) for _, c := range candidates { if c == nil || c.Manifest == nil { continue diff --git a/internal/orchestrator/decrypt_tui_test.go b/internal/orchestrator/decrypt_tui_test.go index d94b78ea..2c82e217 100644 --- a/internal/orchestrator/decrypt_tui_test.go +++ b/internal/orchestrator/decrypt_tui_test.go @@ -51,10 +51,10 @@ func TestBuildTargetInfo(t *testing.T) { func TestFilterEncryptedCandidates(t *testing.T) { now := time.Now() - encrypted := &decryptCandidate{Manifest: &backup.Manifest{EncryptionMode: "age", CreatedAt: now}} - plain := &decryptCandidate{Manifest: &backup.Manifest{EncryptionMode: "none", CreatedAt: now}} + encrypted := &backupCandidate{Manifest: &backup.Manifest{EncryptionMode: "age", CreatedAt: now}} + plain := &backupCandidate{Manifest: &backup.Manifest{EncryptionMode: "none", CreatedAt: now}} - filtered := filterEncryptedCandidates([]*decryptCandidate{nil, encrypted, plain, {}}) + filtered := filterEncryptedCandidates([]*backupCandidate{nil, encrypted, plain, {}}) if len(filtered) != 1 || filtered[0] != encrypted { t.Fatalf("filterEncryptedCandidates returned %+v, want only encrypted candidate", filtered) } diff --git a/internal/orchestrator/decrypt_workflow_test.go b/internal/orchestrator/decrypt_workflow_test.go index e145b7dc..ed1644d2 100644 --- a/internal/orchestrator/decrypt_workflow_test.go +++ b/internal/orchestrator/decrypt_workflow_test.go @@ -89,7 +89,7 @@ func TestPreparePlainBundle_AllowsMissingRawChecksumSidecar(t *testing.T) { // No checksum sidecar: restore/decrypt should still proceed when the manifest // already carries the expected archive checksum. - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: manifest, Source: sourceRaw, RawArchivePath: archive, diff --git a/internal/orchestrator/decrypt_workflow_ui.go b/internal/orchestrator/decrypt_workflow_ui.go index 2cbd9882..aadc9719 100644 --- a/internal/orchestrator/decrypt_workflow_ui.go +++ b/internal/orchestrator/decrypt_workflow_ui.go @@ -30,7 +30,7 @@ func isNilInterface(v any) bool { } } -func selectBackupCandidateWithUI(ctx context.Context, ui BackupSelectionUI, cfg *config.Config, logger *logging.Logger, requireEncrypted bool) (candidate *decryptCandidate, err error) { +func selectBackupCandidateWithUI(ctx context.Context, ui BackupSelectionUI, cfg *config.Config, logger *logging.Logger, requireEncrypted bool) (candidate *backupCandidate, err error) { done := logging.DebugStart(logger, "select backup candidate (ui)", "requireEncrypted=%v", requireEncrypted) defer func() { done(err) }() @@ -51,7 +51,7 @@ func selectBackupCandidateWithUI(ctx context.Context, ui BackupSelectionUI, cfg logger.Info("Scanning %s for backups...", option.Path) - var candidates []*decryptCandidate + var candidates []*backupCandidate scanErr := ui.RunTask(ctx, "Scanning backups", "Scanning backup source...", func(scanCtx context.Context, report ProgressReporter) error { if option.IsRclone { found, err := discoverRcloneBackups(scanCtx, cfg, option.Path, logger, report) @@ -197,7 +197,7 @@ func decryptArchiveWithSecretPrompt(ctx context.Context, encryptedPath, outputPa } } -func preparePlainBundleWithUI(ctx context.Context, cand *decryptCandidate, version string, logger *logging.Logger, ui interface { +func preparePlainBundleWithUI(ctx context.Context, cand *backupCandidate, version string, logger *logging.Logger, ui interface { PromptDecryptSecret(ctx context.Context, displayName, previousError string) (string, error) }) (bundle *preparedBundle, err error) { if cand == nil || cand.Manifest == nil { diff --git a/internal/orchestrator/decrypt_workflow_ui_test.go b/internal/orchestrator/decrypt_workflow_ui_test.go index bea77a45..c37fccd1 100644 --- a/internal/orchestrator/decrypt_workflow_ui_test.go +++ b/internal/orchestrator/decrypt_workflow_ui_test.go @@ -34,7 +34,7 @@ func (f *fakeDecryptWorkflowUI) SelectBackupSource(ctx context.Context, options panic("unexpected SelectBackupSource call") } -func (f *fakeDecryptWorkflowUI) SelectBackupCandidate(ctx context.Context, candidates []*decryptCandidate) (*decryptCandidate, error) { +func (f *fakeDecryptWorkflowUI) SelectBackupCandidate(ctx context.Context, candidates []*backupCandidate) (*backupCandidate, error) { panic("unexpected SelectBackupCandidate call") } @@ -186,7 +186,7 @@ func TestPreparePlainBundleWithUICopiesRawArtifacts(t *testing.T) { t.Fatalf("write checksum: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: &backup.Manifest{ ArchivePath: rawArchive, EncryptionMode: "none", @@ -254,7 +254,7 @@ func TestPreparePlainBundleWithUIRejectsMissingUI(t *testing.T) { t.Fatalf("write checksum: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: &backup.Manifest{ ArchivePath: rawArchive, EncryptionMode: "none", @@ -319,7 +319,7 @@ func TestPreparePlainBundleWithUIRejectsTypedNilUI(t *testing.T) { t.Fatalf("write checksum: %v", err) } - cand := &decryptCandidate{ + cand := &backupCandidate{ Manifest: &backup.Manifest{ ArchivePath: rawArchive, EncryptionMode: "none", diff --git a/internal/orchestrator/restore.go b/internal/orchestrator/restore.go index c774821b..cf7db6a9 100644 --- a/internal/orchestrator/restore.go +++ b/internal/orchestrator/restore.go @@ -688,7 +688,7 @@ func exportDestRoot(baseDir string) string { } // runFullRestore performs a full restore without selective options (fallback) -func runFullRestore(ctx context.Context, reader *bufio.Reader, candidate *decryptCandidate, prepared *preparedBundle, destRoot string, logger *logging.Logger, dryRun bool) error { +func runFullRestore(ctx context.Context, reader *bufio.Reader, candidate *backupCandidate, prepared *preparedBundle, destRoot string, logger *logging.Logger, dryRun bool) error { if err := confirmRestoreAction(ctx, reader, candidate, destRoot); err != nil { return err } @@ -760,7 +760,7 @@ func runFullRestore(ctx context.Context, reader *bufio.Reader, candidate *decryp return nil } -func confirmRestoreAction(ctx context.Context, reader *bufio.Reader, cand *decryptCandidate, dest string) error { +func confirmRestoreAction(ctx context.Context, reader *bufio.Reader, cand *backupCandidate, dest string) error { manifest := cand.Manifest fmt.Println() fmt.Printf("Selected backup: %s (%s)\n", cand.DisplayBase, manifest.CreatedAt.Format("2006-01-02 15:04:05")) diff --git a/internal/orchestrator/restore_coverage_extra_test.go b/internal/orchestrator/restore_coverage_extra_test.go index 922a1a66..15bdd2b9 100644 --- a/internal/orchestrator/restore_coverage_extra_test.go +++ b/internal/orchestrator/restore_coverage_extra_test.go @@ -209,7 +209,7 @@ func TestRunFullRestore_ExtractsArchiveToDestination(t *testing.T) { } reader := bufio.NewReader(strings.NewReader("RESTORE\n")) - cand := &decryptCandidate{ + cand := &backupCandidate{ DisplayBase: "test", Manifest: &backup.Manifest{CreatedAt: time.Now()}, } diff --git a/internal/orchestrator/restore_errors_test.go b/internal/orchestrator/restore_errors_test.go index f8d7fb1c..313cfa91 100644 --- a/internal/orchestrator/restore_errors_test.go +++ b/internal/orchestrator/restore_errors_test.go @@ -1266,7 +1266,7 @@ func TestRunSafeClusterApply_NoVMConfigs(t *testing.T) { // -------------------------------------------------------------------------- func TestConfirmRestoreAction_InvalidInput(t *testing.T) { - cand := &decryptCandidate{ + cand := &backupCandidate{ DisplayBase: "test-backup", Manifest: &backup.Manifest{CreatedAt: time.Now()}, } @@ -1281,7 +1281,7 @@ func TestConfirmRestoreAction_InvalidInput(t *testing.T) { } func TestConfirmRestoreAction_Cancel(t *testing.T) { - cand := &decryptCandidate{ + cand := &backupCandidate{ DisplayBase: "test-backup", Manifest: &backup.Manifest{CreatedAt: time.Now()}, } @@ -1463,7 +1463,7 @@ func TestRunFullRestore_ExtractError(t *testing.T) { restoreFS = fakeFS - cand := &decryptCandidate{ + cand := &backupCandidate{ DisplayBase: "test-backup", Manifest: &backup.Manifest{CreatedAt: time.Now()}, } diff --git a/internal/orchestrator/restore_workflow_abort_test.go b/internal/orchestrator/restore_workflow_abort_test.go index 9dc5d313..032330a2 100644 --- a/internal/orchestrator/restore_workflow_abort_test.go +++ b/internal/orchestrator/restore_workflow_abort_test.go @@ -72,8 +72,8 @@ func TestRunRestoreWorkflow_FstabPromptInputAborted_AbortsWorkflow(t *testing.T) t.Fatalf("fakeFS.WriteFile(/bundle.tar): %v", err) } - prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*decryptCandidate, *preparedBundle, error) { - cand := &decryptCandidate{ + prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*backupCandidate, *preparedBundle, error) { + cand := &backupCandidate{ DisplayBase: "test", Manifest: &backup.Manifest{ CreatedAt: fakeNow.Now(), diff --git a/internal/orchestrator/restore_workflow_decision_test.go b/internal/orchestrator/restore_workflow_decision_test.go index 9f07c808..efa2f33f 100644 --- a/internal/orchestrator/restore_workflow_decision_test.go +++ b/internal/orchestrator/restore_workflow_decision_test.go @@ -14,9 +14,9 @@ import ( "github.com/tis24dev/proxsave/internal/types" ) -func stubPreparedRestoreBundle(archivePath string, manifest *backup.Manifest) func(context.Context, *config.Config, *logging.Logger, string, RestoreWorkflowUI) (*decryptCandidate, *preparedBundle, error) { - return func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*decryptCandidate, *preparedBundle, error) { - return &decryptCandidate{ +func stubPreparedRestoreBundle(archivePath string, manifest *backup.Manifest) func(context.Context, *config.Config, *logging.Logger, string, RestoreWorkflowUI) (*backupCandidate, *preparedBundle, error) { + return func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*backupCandidate, *preparedBundle, error) { + return &backupCandidate{ DisplayBase: "test", Manifest: manifest, }, &preparedBundle{ diff --git a/internal/orchestrator/restore_workflow_errors_test.go b/internal/orchestrator/restore_workflow_errors_test.go index f43aa9fa..ab91b8fd 100644 --- a/internal/orchestrator/restore_workflow_errors_test.go +++ b/internal/orchestrator/restore_workflow_errors_test.go @@ -12,7 +12,7 @@ import ( func TestConfirmRestoreAction_Abort(t *testing.T) { reader := bufio.NewReader(strings.NewReader("0\n")) - cand := &decryptCandidate{ + cand := &backupCandidate{ DisplayBase: "test", Manifest: &backup.Manifest{CreatedAt: time.Now()}, } @@ -25,7 +25,7 @@ func TestConfirmRestoreAction_Abort(t *testing.T) { func TestConfirmRestoreAction_Proceed(t *testing.T) { reader := bufio.NewReader(strings.NewReader("RESTORE\n")) - cand := &decryptCandidate{ + cand := &backupCandidate{ DisplayBase: "test", Manifest: &backup.Manifest{CreatedAt: time.Now()}, } diff --git a/internal/orchestrator/restore_workflow_more_test.go b/internal/orchestrator/restore_workflow_more_test.go index e60eb56c..dde8e1d8 100644 --- a/internal/orchestrator/restore_workflow_more_test.go +++ b/internal/orchestrator/restore_workflow_more_test.go @@ -81,8 +81,8 @@ func TestRunRestoreWorkflow_ClusterBackupSafeMode_ExportsClusterAndRestoresNetwo t.Fatalf("fakeFS.WriteFile: %v", err) } - prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*decryptCandidate, *preparedBundle, error) { - cand := &decryptCandidate{ + prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*backupCandidate, *preparedBundle, error) { + cand := &backupCandidate{ DisplayBase: "test", Manifest: &backup.Manifest{ CreatedAt: fakeNow.Now(), @@ -206,8 +206,8 @@ func TestRunRestoreWorkflow_PBSStopsServicesAndChecksZFSWhenSelected(t *testing. t.Fatalf("fakeFS.WriteFile: %v", err) } - prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*decryptCandidate, *preparedBundle, error) { - cand := &decryptCandidate{ + prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*backupCandidate, *preparedBundle, error) { + cand := &backupCandidate{ DisplayBase: "test", Manifest: &backup.Manifest{ CreatedAt: fakeNow.Now(), @@ -334,8 +334,8 @@ func TestRunRestoreWorkflow_IncompatibilityAndSafetyBackupFailureCanContinue(t * t.Fatalf("restoreSandbox.WriteFile: %v", err) } - prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*decryptCandidate, *preparedBundle, error) { - cand := &decryptCandidate{ + prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*backupCandidate, *preparedBundle, error) { + cand := &backupCandidate{ DisplayBase: "test", Manifest: &backup.Manifest{ CreatedAt: fakeNow.Now(), @@ -438,8 +438,8 @@ func TestRunRestoreWorkflow_ClusterRecoveryModeStopsAndRestartsServices(t *testi t.Fatalf("fakeFS.WriteFile: %v", err) } - prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*decryptCandidate, *preparedBundle, error) { - cand := &decryptCandidate{ + prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*backupCandidate, *preparedBundle, error) { + cand := &backupCandidate{ DisplayBase: "test", Manifest: &backup.Manifest{ CreatedAt: fakeNow.Now(), diff --git a/internal/orchestrator/restore_workflow_test.go b/internal/orchestrator/restore_workflow_test.go index e6d836f1..e22bbbf1 100644 --- a/internal/orchestrator/restore_workflow_test.go +++ b/internal/orchestrator/restore_workflow_test.go @@ -73,8 +73,8 @@ func TestRunRestoreWorkflow_CustomModeNoCategories_Succeeds(t *testing.T) { tmp := t.TempDir() archivePath := writeMinimalTar(t, tmp) - prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*decryptCandidate, *preparedBundle, error) { - cand := &decryptCandidate{ + prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*backupCandidate, *preparedBundle, error) { + cand := &backupCandidate{ DisplayBase: "test", Manifest: &backup.Manifest{ CreatedAt: time.Unix(1700000000, 0), @@ -124,8 +124,8 @@ func TestRunRestoreWorkflow_ConfirmFalseAborts(t *testing.T) { tmp := t.TempDir() archivePath := writeMinimalTar(t, tmp) - prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*decryptCandidate, *preparedBundle, error) { - cand := &decryptCandidate{ + prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*backupCandidate, *preparedBundle, error) { + cand := &backupCandidate{ DisplayBase: "test", Manifest: &backup.Manifest{ CreatedAt: time.Unix(1700000000, 0), diff --git a/internal/orchestrator/restore_workflow_ui.go b/internal/orchestrator/restore_workflow_ui.go index 48e5ab5b..a789de50 100644 --- a/internal/orchestrator/restore_workflow_ui.go +++ b/internal/orchestrator/restore_workflow_ui.go @@ -32,7 +32,7 @@ func fallbackRestoreDecisionInfoFromManifest(manifest *backup.Manifest) *Restore return info } -func prepareRestoreBundleWithUI(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*decryptCandidate, *preparedBundle, error) { +func prepareRestoreBundleWithUI(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*backupCandidate, *preparedBundle, error) { candidate, err := selectBackupCandidateWithUI(ctx, ui, cfg, logger, false) if err != nil { return nil, nil, err @@ -918,7 +918,7 @@ func dedupeCategoriesByID(categories []Category) []Category { return out } -func runFullRestoreWithUI(ctx context.Context, ui RestoreWorkflowUI, candidate *decryptCandidate, prepared *preparedBundle, destRoot string, logger *logging.Logger, dryRun bool) error { +func runFullRestoreWithUI(ctx context.Context, ui RestoreWorkflowUI, candidate *backupCandidate, prepared *preparedBundle, destRoot string, logger *logging.Logger, dryRun bool) error { if candidate == nil || prepared == nil || prepared.Manifest.ArchivePath == "" { return fmt.Errorf("invalid restore candidate") } diff --git a/internal/orchestrator/restore_workflow_ui_helpers_test.go b/internal/orchestrator/restore_workflow_ui_helpers_test.go index ca1a8fee..a3fa5ddc 100644 --- a/internal/orchestrator/restore_workflow_ui_helpers_test.go +++ b/internal/orchestrator/restore_workflow_ui_helpers_test.go @@ -56,7 +56,7 @@ func (f *fakeRestoreWorkflowUI) SelectBackupSource(ctx context.Context, options return decryptPathOption{}, fmt.Errorf("unexpected SelectBackupSource call") } -func (f *fakeRestoreWorkflowUI) SelectBackupCandidate(ctx context.Context, candidates []*decryptCandidate) (*decryptCandidate, error) { +func (f *fakeRestoreWorkflowUI) SelectBackupCandidate(ctx context.Context, candidates []*backupCandidate) (*backupCandidate, error) { return nil, fmt.Errorf("unexpected SelectBackupCandidate call") } diff --git a/internal/orchestrator/restore_workflow_warnings_test.go b/internal/orchestrator/restore_workflow_warnings_test.go index 3da4b6b7..154cbafc 100644 --- a/internal/orchestrator/restore_workflow_warnings_test.go +++ b/internal/orchestrator/restore_workflow_warnings_test.go @@ -89,8 +89,8 @@ func TestRunRestoreWorkflow_FstabMergeFails_ContinuesWithWarnings(t *testing.T) t.Fatalf("fakeFS.WriteFile(/bundle.tar): %v", err) } - prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*decryptCandidate, *preparedBundle, error) { - cand := &decryptCandidate{ + prepareRestoreBundleFunc = func(ctx context.Context, cfg *config.Config, logger *logging.Logger, version string, ui RestoreWorkflowUI) (*backupCandidate, *preparedBundle, error) { + cand := &backupCandidate{ DisplayBase: "test", Manifest: &backup.Manifest{ CreatedAt: fakeNow.Now(), diff --git a/internal/orchestrator/workflow_ui.go b/internal/orchestrator/workflow_ui.go index 03db940d..471ec901 100644 --- a/internal/orchestrator/workflow_ui.go +++ b/internal/orchestrator/workflow_ui.go @@ -29,7 +29,7 @@ type BackupSelectionUI interface { ShowMessage(ctx context.Context, title, message string) error ShowError(ctx context.Context, title, message string) error SelectBackupSource(ctx context.Context, options []decryptPathOption) (decryptPathOption, error) - SelectBackupCandidate(ctx context.Context, candidates []*decryptCandidate) (*decryptCandidate, error) + SelectBackupCandidate(ctx context.Context, candidates []*backupCandidate) (*backupCandidate, error) } // DecryptWorkflowUI groups prompts used by the decrypt workflow. diff --git a/internal/orchestrator/workflow_ui_cli.go b/internal/orchestrator/workflow_ui_cli.go index 9c069ec0..836f3050 100644 --- a/internal/orchestrator/workflow_ui_cli.go +++ b/internal/orchestrator/workflow_ui_cli.go @@ -81,7 +81,7 @@ func (u *cliWorkflowUI) SelectBackupSource(ctx context.Context, options []decryp return promptPathSelection(ctx, u.reader, options) } -func (u *cliWorkflowUI) SelectBackupCandidate(ctx context.Context, candidates []*decryptCandidate) (*decryptCandidate, error) { +func (u *cliWorkflowUI) SelectBackupCandidate(ctx context.Context, candidates []*backupCandidate) (*backupCandidate, error) { return promptCandidateSelection(ctx, u.reader, candidates) } diff --git a/internal/orchestrator/workflow_ui_tui_decrypt.go b/internal/orchestrator/workflow_ui_tui_decrypt.go index cae0c3f4..531571d5 100644 --- a/internal/orchestrator/workflow_ui_tui_decrypt.go +++ b/internal/orchestrator/workflow_ui_tui_decrypt.go @@ -209,10 +209,10 @@ func (u *tuiWorkflowUI) SelectBackupSource(ctx context.Context, options []decryp return selected, nil } -func (u *tuiWorkflowUI) SelectBackupCandidate(ctx context.Context, candidates []*decryptCandidate) (*decryptCandidate, error) { +func (u *tuiWorkflowUI) SelectBackupCandidate(ctx context.Context, candidates []*backupCandidate) (*backupCandidate, error) { app := newTUIApp() var ( - selected *decryptCandidate + selected *backupCandidate aborted bool ) @@ -223,76 +223,54 @@ func (u *tuiWorkflowUI) SelectBackupCandidate(ctx context.Context, candidates [] type row struct { created string + hostname string mode string tool string - targets string + target string compression string } rows := make([]row, len(candidates)) - var maxMode, maxTool, maxTargets, maxComp int + var maxHost, maxMode, maxTool, maxTarget, maxComp int for idx, cand := range candidates { - created := "" - if cand != nil && cand.Manifest != nil { - created = cand.Manifest.CreatedAt.Format("2006-01-02 15:04:05") - } - - mode := strings.ToUpper(statusFromManifest(cand.Manifest)) - if mode == "" { - mode = "UNKNOWN" - } - - toolVersion := "unknown" - if cand != nil && cand.Manifest != nil { - if v := strings.TrimSpace(cand.Manifest.ScriptVersion); v != "" { - toolVersion = v - } - } - tool := "Tool " + toolVersion - - targets := "Targets: unknown" - if cand != nil && cand.Manifest != nil { - targets = buildTargetInfo(cand.Manifest) - } - - comp := "" - if cand != nil && cand.Manifest != nil { - if c := strings.TrimSpace(cand.Manifest.CompressionType); c != "" { - comp = strings.ToUpper(c) - } - } + display := describeBackupCandidate(cand) rows[idx] = row{ - created: created, - mode: mode, - tool: tool, - targets: targets, - compression: comp, + created: display.Created, + hostname: display.Hostname, + mode: display.Mode, + tool: display.Tool, + target: display.Target, + compression: display.Compression, } - if len(mode) > maxMode { - maxMode = len(mode) + if len(display.Hostname) > maxHost { + maxHost = len(display.Hostname) + } + if len(display.Mode) > maxMode { + maxMode = len(display.Mode) } - if len(tool) > maxTool { - maxTool = len(tool) + if len(display.Tool) > maxTool { + maxTool = len(display.Tool) } - if len(targets) > maxTargets { - maxTargets = len(targets) + if len(display.Target) > maxTarget { + maxTarget = len(display.Target) } - if len(comp) > maxComp { - maxComp = len(comp) + if len(display.Compression) > maxComp { + maxComp = len(display.Compression) } } for idx, r := range rows { line := fmt.Sprintf( - "%2d) %s %-*s %-*s %-*s", + "%2d) %s %-*s %-*s %-*s %-*s", idx+1, r.created, + maxHost, r.hostname, maxMode, r.mode, maxTool, r.tool, - maxTargets, r.targets, + maxTarget, r.target, ) if maxComp > 0 { line = fmt.Sprintf("%s %-*s", line, maxComp, r.compression) @@ -407,31 +385,6 @@ func (u *tuiWorkflowUI) PromptDecryptSecret(ctx context.Context, displayName, pr return tuiPromptDecryptSecret(ctx, u.screenEnv(), displayName, previousError) } -func backupSummaryForUI(cand *decryptCandidate) string { - if cand == nil { - return "" - } - - base := strings.TrimSpace(cand.DisplayBase) - if base == "" { - switch { - case strings.TrimSpace(cand.BundlePath) != "": - base = filepath.Base(strings.TrimSpace(cand.BundlePath)) - case strings.TrimSpace(cand.RawArchivePath) != "": - base = filepath.Base(strings.TrimSpace(cand.RawArchivePath)) - } - } - - created := "" - if cand.Manifest != nil { - created = cand.Manifest.CreatedAt.Format("2006-01-02 15:04:05") - } - - if base == "" { - return created - } - if created == "" { - return base - } - return fmt.Sprintf("%s (%s)", base, created) +func backupSummaryForUI(cand *backupCandidate) string { + return describeBackupCandidate(cand).Summary } From 58c31385791e097e40665dc42168f0a4a99e061d Mon Sep 17 00:00:00 2001 From: tis24dev Date: Mon, 30 Mar 2026 10:07:04 +0200 Subject: [PATCH 2/2] Set coverage toolchain and make tests UTC Update CI to resolve a Go toolchain from go.mod and export it for coverage runs (adds a workflow step that sets COVER_GOTOOLCHAIN and uses it as GOTOOLCHAIN when running `go test`). Fix a timezone-dependent unit test by using time.Now().UTC() and explicit UTC dates for previous-month and previous-year backups to make timestamp calculations deterministic. Files changed: .github/workflows/codecov.yml, internal/orchestrator/additional_helpers_test.go. LiveReview Pre-Commit Check: skipped (iter:1, coverage:0%) --- .github/workflows/codecov.yml | 11 +++++++++++ internal/orchestrator/additional_helpers_test.go | 8 +++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 7be8d0de..bb3957bc 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -18,7 +18,18 @@ jobs: with: go-version-file: 'go.mod' + - name: Resolve coverage toolchain + run: | + TOOLCHAIN_FROM_MOD=$(awk '/^toolchain /{print $2}' go.mod) + if [ -n "$TOOLCHAIN_FROM_MOD" ]; then + echo "COVER_GOTOOLCHAIN=${TOOLCHAIN_FROM_MOD}+auto" >> "$GITHUB_ENV" + else + echo "COVER_GOTOOLCHAIN=auto" >> "$GITHUB_ENV" + fi + - name: Run tests with coverage + env: + GOTOOLCHAIN: ${{ env.COVER_GOTOOLCHAIN }} run: | go test $(go list ./... | grep -v -E '/cmd/|/pbs$|/bech32$|^github.com/tis24dev/proxsave$') -coverprofile=coverage.out diff --git a/internal/orchestrator/additional_helpers_test.go b/internal/orchestrator/additional_helpers_test.go index 0beb9628..f82dd386 100644 --- a/internal/orchestrator/additional_helpers_test.go +++ b/internal/orchestrator/additional_helpers_test.go @@ -198,12 +198,14 @@ func TestApplyStorageStatsSimplePrimary(t *testing.T) { } func TestApplyStorageStatsGFSPrimary(t *testing.T) { - now := time.Now() + now := time.Now().UTC() + previousMonthBackup := time.Date(now.Year(), now.Month(), 1, now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), time.UTC).Add(-24 * time.Hour) + previousYearBackup := time.Date(now.Year()-1, time.January, 15, now.Hour(), now.Minute(), now.Second(), now.Nanosecond(), time.UTC) backups := []*types.BackupMetadata{ {Timestamp: now}, // daily {Timestamp: now.AddDate(0, 0, -8)}, // weekly - {Timestamp: now.AddDate(0, -1, -1)}, // monthly - {Timestamp: now.AddDate(-1, 0, 0)}, // yearly + {Timestamp: previousMonthBackup}, // monthly + {Timestamp: previousYearBackup}, // yearly } adapter := &StorageAdapter{ backend: &stubStorage{loc: storage.LocationPrimary, list: backups},