v3.42.18: Unify archive verification - backup manager uses same checks as restore
- verifyArchiveCmd now uses restore.Safety and restore.Diagnoser - Same validation logic in backup manager verify and restore safety checks - No more discrepancy between verify showing valid and restore failing
This commit is contained in:
@@ -4,8 +4,8 @@ This directory contains pre-compiled binaries for the DB Backup Tool across mult
|
|||||||
|
|
||||||
## Build Information
|
## Build Information
|
||||||
- **Version**: 3.42.10
|
- **Version**: 3.42.10
|
||||||
- **Build Time**: 2026-01-08_10:18:23_UTC
|
- **Build Time**: 2026-01-08_10:59:00_UTC
|
||||||
- **Git Commit**: 682510d
|
- **Git Commit**: 92402f0
|
||||||
|
|
||||||
## Recent Updates (v1.1.0)
|
## Recent Updates (v1.1.0)
|
||||||
- ✅ Fixed TUI progress display with line-by-line output
|
- ✅ Fixed TUI progress display with line-by-line output
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"dbbackup/internal/config"
|
"dbbackup/internal/config"
|
||||||
"dbbackup/internal/logger"
|
"dbbackup/internal/logger"
|
||||||
|
"dbbackup/internal/restore"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OperationState represents the current operation state
|
// OperationState represents the current operation state
|
||||||
@@ -349,85 +349,70 @@ func (m BackupManagerModel) View() string {
|
|||||||
return s.String()
|
return s.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyArchiveCmd runs actual archive verification
|
// verifyArchiveCmd runs the SAME verification as restore safety checks
|
||||||
|
// This ensures consistency between backup manager verify and restore preview
|
||||||
func verifyArchiveCmd(archive ArchiveInfo) tea.Cmd {
|
func verifyArchiveCmd(archive ArchiveInfo) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
// Determine verification method based on format
|
var issues []string
|
||||||
var valid bool
|
|
||||||
var details string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch {
|
// 1. Run the same archive integrity check as restore
|
||||||
case strings.HasSuffix(archive.Path, ".tar.gz") || strings.HasSuffix(archive.Path, ".tgz"):
|
safety := restore.NewSafety(nil, nil) // Doesn't need config/log for validation
|
||||||
// Verify tar.gz archive
|
if err := safety.ValidateArchive(archive.Path); err != nil {
|
||||||
cmd := exec.Command("tar", "-tzf", archive.Path)
|
return verifyResultMsg{
|
||||||
output, cmdErr := cmd.CombinedOutput()
|
archive: archive.Name,
|
||||||
if cmdErr != nil {
|
valid: false,
|
||||||
return verifyResultMsg{archive: archive.Name, valid: false, err: nil, details: "Archive corrupt or incomplete"}
|
err: nil,
|
||||||
|
details: fmt.Sprintf("Archive integrity: %v", err),
|
||||||
}
|
}
|
||||||
lines := strings.Split(string(output), "\n")
|
|
||||||
fileCount := 0
|
|
||||||
for _, l := range lines {
|
|
||||||
if l != "" {
|
|
||||||
fileCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
valid = true
|
|
||||||
details = fmt.Sprintf("%d files in archive", fileCount)
|
|
||||||
|
|
||||||
case strings.HasSuffix(archive.Path, ".dump") || strings.HasSuffix(archive.Path, ".sql"):
|
|
||||||
// Verify PostgreSQL dump with pg_restore --list
|
|
||||||
cmd := exec.Command("pg_restore", "--list", archive.Path)
|
|
||||||
output, cmdErr := cmd.CombinedOutput()
|
|
||||||
if cmdErr != nil {
|
|
||||||
// Try as plain SQL
|
|
||||||
if strings.HasSuffix(archive.Path, ".sql") {
|
|
||||||
// Just check file is readable and has content
|
|
||||||
fi, statErr := os.Stat(archive.Path)
|
|
||||||
if statErr == nil && fi.Size() > 0 {
|
|
||||||
valid = true
|
|
||||||
details = "Plain SQL file readable"
|
|
||||||
} else {
|
|
||||||
return verifyResultMsg{archive: archive.Name, valid: false, err: nil, details: "File empty or unreadable"}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return verifyResultMsg{archive: archive.Name, valid: false, err: nil, details: "pg_restore cannot read dump"}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lines := strings.Split(string(output), "\n")
|
|
||||||
objectCount := 0
|
|
||||||
for _, l := range lines {
|
|
||||||
if l != "" && !strings.HasPrefix(l, ";") {
|
|
||||||
objectCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
valid = true
|
|
||||||
details = fmt.Sprintf("%d objects in dump", objectCount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case strings.HasSuffix(archive.Path, ".sql.gz"):
|
// 2. Run the same deep diagnosis as restore
|
||||||
// Verify gzipped SQL
|
diagnoser := restore.NewDiagnoser(nil, false)
|
||||||
cmd := exec.Command("gzip", "-t", archive.Path)
|
diagResult, diagErr := diagnoser.DiagnoseFile(archive.Path)
|
||||||
if cmdErr := cmd.Run(); cmdErr != nil {
|
if diagErr != nil {
|
||||||
return verifyResultMsg{archive: archive.Name, valid: false, err: nil, details: "Gzip archive corrupt"}
|
return verifyResultMsg{
|
||||||
|
archive: archive.Name,
|
||||||
|
valid: false,
|
||||||
|
err: diagErr,
|
||||||
|
details: "Cannot diagnose archive",
|
||||||
}
|
}
|
||||||
valid = true
|
|
||||||
details = "Gzip integrity OK"
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Unknown format - just check file exists and has size
|
|
||||||
fi, statErr := os.Stat(archive.Path)
|
|
||||||
if statErr != nil {
|
|
||||||
return verifyResultMsg{archive: archive.Name, valid: false, err: statErr, details: "Cannot access file"}
|
|
||||||
}
|
|
||||||
if fi.Size() == 0 {
|
|
||||||
return verifyResultMsg{archive: archive.Name, valid: false, err: nil, details: "File is empty"}
|
|
||||||
}
|
|
||||||
valid = true
|
|
||||||
details = "File exists and has content"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return verifyResultMsg{archive: archive.Name, valid: valid, err: err, details: details}
|
if !diagResult.IsValid {
|
||||||
|
// Collect error details
|
||||||
|
if diagResult.IsTruncated {
|
||||||
|
issues = append(issues, "TRUNCATED")
|
||||||
|
}
|
||||||
|
if diagResult.IsCorrupted {
|
||||||
|
issues = append(issues, "CORRUPTED")
|
||||||
|
}
|
||||||
|
if len(diagResult.Errors) > 0 {
|
||||||
|
issues = append(issues, diagResult.Errors[0])
|
||||||
|
}
|
||||||
|
return verifyResultMsg{
|
||||||
|
archive: archive.Name,
|
||||||
|
valid: false,
|
||||||
|
err: nil,
|
||||||
|
details: strings.Join(issues, "; "),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build success details
|
||||||
|
details := "Verified"
|
||||||
|
if diagResult.Details != nil {
|
||||||
|
if diagResult.Details.TableCount > 0 {
|
||||||
|
details = fmt.Sprintf("%d databases in archive", diagResult.Details.TableCount)
|
||||||
|
} else if diagResult.Details.PgRestoreListable {
|
||||||
|
details = "pg_restore verified"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any warnings
|
||||||
|
if len(diagResult.Warnings) > 0 {
|
||||||
|
details += fmt.Sprintf(" [%d warnings]", len(diagResult.Warnings))
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifyResultMsg{archive: archive.Name, valid: true, err: nil, details: details}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user