Implemented full incremental backup creation: internal/backup/incremental_postgres.go: - CreateIncrementalBackup() - main entry point - Validates base backup exists and is full backup - Loads base backup metadata (.meta.json) - Uses FindChangedFiles() to detect modifications - Creates tar.gz with ONLY changed files - Generates incremental metadata with: - Base backup ID (SHA-256) - Backup chain (base -> incr1 -> incr2...) - Changed file count and total size - Saves .meta.json with full incremental metadata - Calculates SHA-256 checksum of archive internal/backup/incremental_tar.go: - createTarGz() - creates compressed archive - addFileToTar() - adds individual files to tar - Handles context cancellation - Progress logging for each file - Preserves file permissions and timestamps Helper Functions: - loadBackupInfo() - loads BackupMetadata from .meta.json - buildBackupChain() - constructs restore chain - CalculateFileChecksum() - SHA-256 for archive Features: ✅ Creates tar.gz with ONLY changed files ✅ Much smaller than full backup ✅ Links to base backup via SHA-256 ✅ Tracks complete restore chain ✅ Full metadata for restore validation ✅ Context-aware (cancellable) Status: READY FOR TESTING Next: Wire into backup engine, test with real PostgreSQL data
96 lines
2.4 KiB
Go
96 lines
2.4 KiB
Go
package backup
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
)
|
|
|
|
// createTarGz creates a tar.gz archive with the specified changed files
|
|
func (e *PostgresIncrementalEngine) createTarGz(ctx context.Context, outputFile string, changedFiles []ChangedFile, config *IncrementalBackupConfig) error {
|
|
// Create output file
|
|
outFile, err := os.Create(outputFile)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create output file: %w", err)
|
|
}
|
|
defer outFile.Close()
|
|
|
|
// Create gzip writer
|
|
gzWriter, err := gzip.NewWriterLevel(outFile, config.CompressionLevel)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create gzip writer: %w", err)
|
|
}
|
|
defer gzWriter.Close()
|
|
|
|
// Create tar writer
|
|
tarWriter := tar.NewWriter(gzWriter)
|
|
defer tarWriter.Close()
|
|
|
|
// Add each changed file to archive
|
|
for i, changedFile := range changedFiles {
|
|
// Check context cancellation
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
e.log.Debug("Adding file to archive",
|
|
"file", changedFile.RelativePath,
|
|
"progress", fmt.Sprintf("%d/%d", i+1, len(changedFiles)))
|
|
|
|
if err := e.addFileToTar(tarWriter, changedFile); err != nil {
|
|
return fmt.Errorf("failed to add file %s: %w", changedFile.RelativePath, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// addFileToTar adds a single file to the tar archive
|
|
func (e *PostgresIncrementalEngine) addFileToTar(tarWriter *tar.Writer, changedFile ChangedFile) error {
|
|
// Open the file
|
|
file, err := os.Open(changedFile.AbsolutePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Get file info
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to stat file: %w", err)
|
|
}
|
|
|
|
// Skip if file has been deleted/changed since scan
|
|
if info.Size() != changedFile.Size {
|
|
e.log.Warn("File size changed since scan, using current size",
|
|
"file", changedFile.RelativePath,
|
|
"old_size", changedFile.Size,
|
|
"new_size", info.Size())
|
|
}
|
|
|
|
// Create tar header
|
|
header := &tar.Header{
|
|
Name: changedFile.RelativePath,
|
|
Size: info.Size(),
|
|
Mode: int64(info.Mode()),
|
|
ModTime: info.ModTime(),
|
|
}
|
|
|
|
// Write header
|
|
if err := tarWriter.WriteHeader(header); err != nil {
|
|
return fmt.Errorf("failed to write tar header: %w", err)
|
|
}
|
|
|
|
// Copy file content
|
|
if _, err := io.Copy(tarWriter, file); err != nil {
|
|
return fmt.Errorf("failed to copy file content: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|