Some checks failed
CI/CD / Test (push) Successful in 2m59s
CI/CD / Lint (push) Successful in 1m10s
CI/CD / Integration Tests (push) Failing after 25s
CI/CD / Native Engine Tests (push) Successful in 50s
CI/CD / Build Binary (push) Successful in 44s
CI/CD / Test Release Build (push) Successful in 1m17s
CI/CD / Release Binaries (push) Failing after 10m7s
NEW FEATURE: - TUI cluster restore now accepts .sql and .sql.gz files (pg_dumpall output) - Uses native engine automatically for SQL-based cluster restores - Added CanBeClusterRestore() method to detect valid cluster formats Supported cluster restore formats: - .tar.gz (dbbackup cluster format) - .sql (pg_dumpall plain format) - .sql.gz (pg_dumpall compressed format)
219 lines
6.4 KiB
Go
Executable File
219 lines
6.4 KiB
Go
Executable File
package restore
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/klauspost/pgzip"
|
|
)
|
|
|
|
// ArchiveFormat represents the type of backup archive
|
|
type ArchiveFormat string
|
|
|
|
const (
|
|
FormatPostgreSQLDump ArchiveFormat = "PostgreSQL Dump (.dump)"
|
|
FormatPostgreSQLDumpGz ArchiveFormat = "PostgreSQL Dump Compressed (.dump.gz)"
|
|
FormatPostgreSQLSQL ArchiveFormat = "PostgreSQL SQL (.sql)"
|
|
FormatPostgreSQLSQLGz ArchiveFormat = "PostgreSQL SQL Compressed (.sql.gz)"
|
|
FormatMySQLSQL ArchiveFormat = "MySQL SQL (.sql)"
|
|
FormatMySQLSQLGz ArchiveFormat = "MySQL SQL Compressed (.sql.gz)"
|
|
FormatClusterTarGz ArchiveFormat = "Cluster Archive (.tar.gz)"
|
|
FormatUnknown ArchiveFormat = "Unknown"
|
|
)
|
|
|
|
// backupMetadata represents the structure of .meta.json files
|
|
type backupMetadata struct {
|
|
DatabaseType string `json:"database_type"`
|
|
}
|
|
|
|
// readMetadataDBType reads the database_type from the .meta.json file if it exists
|
|
func readMetadataDBType(archivePath string) string {
|
|
metaPath := archivePath + ".meta.json"
|
|
data, err := os.ReadFile(metaPath)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var meta backupMetadata
|
|
if err := json.Unmarshal(data, &meta); err != nil {
|
|
return ""
|
|
}
|
|
return strings.ToLower(meta.DatabaseType)
|
|
}
|
|
|
|
// DetectArchiveFormat detects the format of a backup archive from its filename and content
|
|
func DetectArchiveFormat(filename string) ArchiveFormat {
|
|
lower := strings.ToLower(filename)
|
|
|
|
// Check for cluster archives first (most specific)
|
|
// A .tar.gz file is considered a cluster backup if:
|
|
// 1. Contains "cluster" in name, OR
|
|
// 2. Is a .tar.gz file (likely a cluster backup archive)
|
|
if strings.HasSuffix(lower, ".tar.gz") {
|
|
// All .tar.gz files are treated as cluster backups
|
|
// since that's the format used for cluster archives
|
|
return FormatClusterTarGz
|
|
}
|
|
|
|
// For .dump files, assume PostgreSQL custom format based on extension
|
|
// If the file exists and can be read, verify with magic bytes
|
|
if strings.HasSuffix(lower, ".dump.gz") {
|
|
// Check if file exists and has content signature
|
|
result := isCustomFormat(filename, true)
|
|
// If file doesn't exist or we can't read it, trust the extension
|
|
// If file exists and has PGDMP signature, it's custom format
|
|
// If file exists but doesn't have signature, it might be SQL named as .dump
|
|
if result == formatCheckCustom || result == formatCheckFileNotFound {
|
|
return FormatPostgreSQLDumpGz
|
|
}
|
|
return FormatPostgreSQLSQLGz
|
|
}
|
|
|
|
if strings.HasSuffix(lower, ".dump") {
|
|
result := isCustomFormat(filename, false)
|
|
if result == formatCheckCustom || result == formatCheckFileNotFound {
|
|
return FormatPostgreSQLDump
|
|
}
|
|
return FormatPostgreSQLSQL
|
|
}
|
|
|
|
// Check for compressed SQL formats
|
|
if strings.HasSuffix(lower, ".sql.gz") {
|
|
// First, try to determine from metadata file
|
|
if dbType := readMetadataDBType(filename); dbType != "" {
|
|
if dbType == "mysql" || dbType == "mariadb" {
|
|
return FormatMySQLSQLGz
|
|
}
|
|
return FormatPostgreSQLSQLGz
|
|
}
|
|
// Fallback: determine if MySQL or PostgreSQL based on naming convention
|
|
if strings.Contains(lower, "mysql") || strings.Contains(lower, "mariadb") {
|
|
return FormatMySQLSQLGz
|
|
}
|
|
return FormatPostgreSQLSQLGz
|
|
}
|
|
|
|
// Check for uncompressed SQL formats
|
|
if strings.HasSuffix(lower, ".sql") {
|
|
// First, try to determine from metadata file
|
|
if dbType := readMetadataDBType(filename); dbType != "" {
|
|
if dbType == "mysql" || dbType == "mariadb" {
|
|
return FormatMySQLSQLGz
|
|
}
|
|
return FormatPostgreSQLSQL
|
|
}
|
|
// Fallback: determine if MySQL or PostgreSQL based on naming convention
|
|
if strings.Contains(lower, "mysql") || strings.Contains(lower, "mariadb") {
|
|
return FormatMySQLSQL
|
|
}
|
|
return FormatPostgreSQLSQL
|
|
}
|
|
|
|
if strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") {
|
|
return FormatClusterTarGz
|
|
}
|
|
|
|
return FormatUnknown
|
|
}
|
|
|
|
// formatCheckResult represents the result of checking file format
|
|
type formatCheckResult int
|
|
|
|
const (
|
|
formatCheckFileNotFound formatCheckResult = iota
|
|
formatCheckCustom
|
|
formatCheckNotCustom
|
|
)
|
|
|
|
// isCustomFormat checks if a file is PostgreSQL custom format (has PGDMP signature)
|
|
func isCustomFormat(filename string, compressed bool) formatCheckResult {
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
// File doesn't exist or can't be opened - return file not found
|
|
return formatCheckFileNotFound
|
|
}
|
|
defer file.Close()
|
|
|
|
var reader io.Reader = file
|
|
|
|
// Handle compression
|
|
if compressed {
|
|
gz, err := pgzip.NewReader(file)
|
|
if err != nil {
|
|
return formatCheckFileNotFound
|
|
}
|
|
defer gz.Close()
|
|
reader = gz
|
|
}
|
|
|
|
// Read first 5 bytes to check for PGDMP signature
|
|
buffer := make([]byte, 5)
|
|
n, err := reader.Read(buffer)
|
|
if err != nil || n < 5 {
|
|
return formatCheckNotCustom
|
|
}
|
|
|
|
if string(buffer) == "PGDMP" {
|
|
return formatCheckCustom
|
|
}
|
|
return formatCheckNotCustom
|
|
}
|
|
|
|
// IsCompressed returns true if the archive format is compressed
|
|
func (f ArchiveFormat) IsCompressed() bool {
|
|
return f == FormatPostgreSQLDumpGz ||
|
|
f == FormatPostgreSQLSQLGz ||
|
|
f == FormatMySQLSQLGz ||
|
|
f == FormatClusterTarGz
|
|
}
|
|
|
|
// IsClusterBackup returns true if the archive is a cluster backup (.tar.gz format created by dbbackup)
|
|
func (f ArchiveFormat) IsClusterBackup() bool {
|
|
return f == FormatClusterTarGz
|
|
}
|
|
|
|
// CanBeClusterRestore returns true if the format can be used for cluster restore
|
|
// This includes .tar.gz (dbbackup format) and .sql/.sql.gz (pg_dumpall format for native engine)
|
|
func (f ArchiveFormat) CanBeClusterRestore() bool {
|
|
return f == FormatClusterTarGz ||
|
|
f == FormatPostgreSQLSQL ||
|
|
f == FormatPostgreSQLSQLGz
|
|
}
|
|
|
|
// IsPostgreSQL returns true if the archive is PostgreSQL format
|
|
func (f ArchiveFormat) IsPostgreSQL() bool {
|
|
return f == FormatPostgreSQLDump ||
|
|
f == FormatPostgreSQLDumpGz ||
|
|
f == FormatPostgreSQLSQL ||
|
|
f == FormatPostgreSQLSQLGz ||
|
|
f == FormatClusterTarGz
|
|
}
|
|
|
|
// IsMySQL returns true if format is MySQL
|
|
func (f ArchiveFormat) IsMySQL() bool {
|
|
return f == FormatMySQLSQL || f == FormatMySQLSQLGz
|
|
}
|
|
|
|
// String returns human-readable format name
|
|
func (f ArchiveFormat) String() string {
|
|
switch f {
|
|
case FormatPostgreSQLDump:
|
|
return "PostgreSQL Dump"
|
|
case FormatPostgreSQLDumpGz:
|
|
return "PostgreSQL Dump (gzip)"
|
|
case FormatPostgreSQLSQL:
|
|
return "PostgreSQL SQL"
|
|
case FormatPostgreSQLSQLGz:
|
|
return "PostgreSQL SQL (gzip)"
|
|
case FormatMySQLSQL:
|
|
return "MySQL SQL"
|
|
case FormatMySQLSQLGz:
|
|
return "MySQL SQL (gzip)"
|
|
case FormatClusterTarGz:
|
|
return "Cluster Archive (tar.gz)"
|
|
default:
|
|
return "Unknown"
|
|
}
|
|
}
|