- Add .golangci.yml with minimal linters (govet, ineffassign) - Run gofmt -s and goimports on all files to fix formatting - Disable fieldalignment and copylocks checks in govet
185 lines
5.9 KiB
Go
185 lines
5.9 KiB
Go
package metadata
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// BackupMetadata contains comprehensive information about a backup
|
|
type BackupMetadata struct {
|
|
Version string `json:"version"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Database string `json:"database"`
|
|
DatabaseType string `json:"database_type"` // postgresql, mysql, mariadb
|
|
DatabaseVersion string `json:"database_version"` // e.g., "PostgreSQL 15.3"
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
User string `json:"user"`
|
|
BackupFile string `json:"backup_file"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
SHA256 string `json:"sha256"`
|
|
Compression string `json:"compression"` // none, gzip, pigz
|
|
BackupType string `json:"backup_type"` // full, incremental (for v2.2)
|
|
BaseBackup string `json:"base_backup,omitempty"`
|
|
Duration float64 `json:"duration_seconds"`
|
|
ExtraInfo map[string]string `json:"extra_info,omitempty"`
|
|
|
|
// Encryption fields (v2.3+)
|
|
Encrypted bool `json:"encrypted"` // Whether backup is encrypted
|
|
EncryptionAlgorithm string `json:"encryption_algorithm,omitempty"` // e.g., "aes-256-gcm"
|
|
|
|
// Incremental backup fields (v2.2+)
|
|
Incremental *IncrementalMetadata `json:"incremental,omitempty"` // Only present for incremental backups
|
|
}
|
|
|
|
// IncrementalMetadata contains metadata specific to incremental backups
|
|
type IncrementalMetadata struct {
|
|
BaseBackupID string `json:"base_backup_id"` // SHA-256 of base backup
|
|
BaseBackupPath string `json:"base_backup_path"` // Filename of base backup
|
|
BaseBackupTimestamp time.Time `json:"base_backup_timestamp"` // When base backup was created
|
|
IncrementalFiles int `json:"incremental_files"` // Number of changed files
|
|
TotalSize int64 `json:"total_size"` // Total size of changed files (bytes)
|
|
BackupChain []string `json:"backup_chain"` // Chain: [base, incr1, incr2, ...]
|
|
}
|
|
|
|
// ClusterMetadata contains metadata for cluster backups
|
|
type ClusterMetadata struct {
|
|
Version string `json:"version"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
ClusterName string `json:"cluster_name"`
|
|
DatabaseType string `json:"database_type"`
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
Databases []BackupMetadata `json:"databases"`
|
|
TotalSize int64 `json:"total_size_bytes"`
|
|
Duration float64 `json:"duration_seconds"`
|
|
ExtraInfo map[string]string `json:"extra_info,omitempty"`
|
|
}
|
|
|
|
// CalculateSHA256 computes the SHA-256 checksum of a file
|
|
func CalculateSHA256(filePath string) (string, error) {
|
|
f, err := os.Open(filePath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
hasher := sha256.New()
|
|
if _, err := io.Copy(hasher, f); err != nil {
|
|
return "", fmt.Errorf("failed to calculate checksum: %w", err)
|
|
}
|
|
|
|
return hex.EncodeToString(hasher.Sum(nil)), nil
|
|
}
|
|
|
|
// Save writes metadata to a .meta.json file
|
|
func (m *BackupMetadata) Save() error {
|
|
metaPath := m.BackupFile + ".meta.json"
|
|
|
|
data, err := json.MarshalIndent(m, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal metadata: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(metaPath, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write metadata file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Load reads metadata from a .meta.json file
|
|
func Load(backupFile string) (*BackupMetadata, error) {
|
|
metaPath := backupFile + ".meta.json"
|
|
|
|
data, err := os.ReadFile(metaPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read metadata file: %w", err)
|
|
}
|
|
|
|
var meta BackupMetadata
|
|
if err := json.Unmarshal(data, &meta); err != nil {
|
|
return nil, fmt.Errorf("failed to parse metadata: %w", err)
|
|
}
|
|
|
|
return &meta, nil
|
|
}
|
|
|
|
// SaveCluster writes cluster metadata to a .meta.json file
|
|
func (m *ClusterMetadata) Save(targetFile string) error {
|
|
metaPath := targetFile + ".meta.json"
|
|
|
|
data, err := json.MarshalIndent(m, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal cluster metadata: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(metaPath, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write cluster metadata file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadCluster reads cluster metadata from a .meta.json file
|
|
func LoadCluster(targetFile string) (*ClusterMetadata, error) {
|
|
metaPath := targetFile + ".meta.json"
|
|
|
|
data, err := os.ReadFile(metaPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read cluster metadata file: %w", err)
|
|
}
|
|
|
|
var meta ClusterMetadata
|
|
if err := json.Unmarshal(data, &meta); err != nil {
|
|
return nil, fmt.Errorf("failed to parse cluster metadata: %w", err)
|
|
}
|
|
|
|
return &meta, nil
|
|
}
|
|
|
|
// ListBackups scans a directory for backup files and returns their metadata
|
|
func ListBackups(dir string) ([]*BackupMetadata, error) {
|
|
pattern := filepath.Join(dir, "*.meta.json")
|
|
matches, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan directory: %w", err)
|
|
}
|
|
|
|
var backups []*BackupMetadata
|
|
for _, metaFile := range matches {
|
|
// Extract backup file path (remove .meta.json suffix)
|
|
backupFile := metaFile[:len(metaFile)-len(".meta.json")]
|
|
|
|
meta, err := Load(backupFile)
|
|
if err != nil {
|
|
// Skip invalid metadata files
|
|
continue
|
|
}
|
|
|
|
backups = append(backups, meta)
|
|
}
|
|
|
|
return backups, nil
|
|
}
|
|
|
|
// FormatSize returns human-readable size
|
|
func FormatSize(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])
|
|
}
|