Files
dbbackup/internal/backup/incremental_extract.go
Renz b18e9e9ec9 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)
2025-11-26 07:04:34 +00:00

104 lines
2.7 KiB
Go

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
}