Files
dbbackup/internal/restore/formats.go
A. Renz 4be8a96699 fix: trust .dump extension when file doesn't exist in DetectArchiveFormat
The format detection now returns PostgreSQL Dump format for .dump files
when the file cannot be opened (e.g., when just checking filename pattern),
instead of falling back to SQL format.

This fixes the test that passes just a filename string without an actual file.
2025-12-11 17:39:19 +01:00

171 lines
4.8 KiB
Go
Executable File

package restore
import (
"compress/gzip"
"io"
"os"
"strings"
)
// 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"
)
// 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)
if strings.Contains(lower, "cluster") && strings.HasSuffix(lower, ".tar.gz") {
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") {
// 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") {
// 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 := gzip.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
func (f ArchiveFormat) IsClusterBackup() bool {
return f == FormatClusterTarGz
}
// 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"
}
}