feat: Step 6 - Implement RestoreIncremental() for PostgreSQL
Implemented full incremental backup restoration: internal/backup/incremental_postgres.go: - RestoreIncremental() - main entry point - Validates incremental backup metadata (.meta.json) - Verifies base backup exists and is full backup - Verifies checksums match (BaseBackupID == base SHA256) - Extracts base backup to target directory first - Applies incremental on top (overwrites changed files) - Context cancellation support - Comprehensive error handling: - Missing base backup - Wrong backup type (not incremental) - Checksum mismatch - Missing metadata internal/backup/incremental_extract.go: - extractTarGz() - extracts tar.gz archives - Handles regular files, directories, symlinks - Preserves file permissions and timestamps - Progress logging every 100 files - Context-aware (cancellable) Restore Logic: 1. Load incremental metadata from .meta.json 2. Verify base backup exists and checksums match 3. Extract base backup (full restore) 4. Extract incremental backup (apply changed files) 5. Log completion with file counts Features: ✅ Validates backup chain integrity ✅ Checksum verification for safety ✅ Handles base backup path mismatch (warning) ✅ Creates target directory if missing ✅ Preserves file attributes (perms, mtime) ✅ Detailed logging at each step Status: READY FOR TESTING Next: Write integration test (Step 7)
This commit is contained in:
103
internal/backup/incremental_extract.go
Normal file
103
internal/backup/incremental_extract.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// extractTarGz extracts a tar.gz archive to the specified directory
|
||||
// Files are extracted with their original permissions and timestamps
|
||||
func (e *PostgresIncrementalEngine) extractTarGz(ctx context.Context, archivePath, targetDir string) error {
|
||||
// Open archive file
|
||||
archiveFile, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open archive: %w", err)
|
||||
}
|
||||
defer archiveFile.Close()
|
||||
|
||||
// Create gzip reader
|
||||
gzReader, err := gzip.NewReader(archiveFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
defer gzReader.Close()
|
||||
|
||||
// Create tar reader
|
||||
tarReader := tar.NewReader(gzReader)
|
||||
|
||||
// Extract each file
|
||||
fileCount := 0
|
||||
for {
|
||||
// Check context cancellation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break // End of archive
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read tar header: %w", err)
|
||||
}
|
||||
|
||||
// Build target path
|
||||
targetPath := filepath.Join(targetDir, header.Name)
|
||||
|
||||
// Ensure parent directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory for %s: %w", header.Name, err)
|
||||
}
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
// Create directory
|
||||
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
|
||||
return fmt.Errorf("failed to create directory %s: %w", header.Name, err)
|
||||
}
|
||||
|
||||
case tar.TypeReg:
|
||||
// Extract regular file
|
||||
outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file %s: %w", header.Name, err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||
outFile.Close()
|
||||
return fmt.Errorf("failed to write file %s: %w", header.Name, err)
|
||||
}
|
||||
outFile.Close()
|
||||
|
||||
// Preserve modification time
|
||||
if err := os.Chtimes(targetPath, header.ModTime, header.ModTime); err != nil {
|
||||
e.log.Warn("Failed to set file modification time", "file", header.Name, "error", err)
|
||||
}
|
||||
|
||||
fileCount++
|
||||
if fileCount%100 == 0 {
|
||||
e.log.Debug("Extraction progress", "files", fileCount)
|
||||
}
|
||||
|
||||
case tar.TypeSymlink:
|
||||
// Create symlink
|
||||
if err := os.Symlink(header.Linkname, targetPath); err != nil {
|
||||
// Don't fail on symlink errors - just warn
|
||||
e.log.Warn("Failed to create symlink", "source", header.Name, "target", header.Linkname, "error", err)
|
||||
}
|
||||
|
||||
default:
|
||||
e.log.Warn("Unsupported tar entry type", "type", header.Typeflag, "name", header.Name)
|
||||
}
|
||||
}
|
||||
|
||||
e.log.Info("Archive extracted", "files", fileCount, "archive", filepath.Base(archivePath))
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user