This is a critical bugfix release addressing multiple hardcoded temporary directory paths that prevented proper use of the WorkDir configuration option. PROBLEM: Users configuring WorkDir (e.g., /u01/dba/tmp) for systems with small root filesystems still experienced failures because critical operations hardcoded /tmp instead of respecting the configured WorkDir. This made the WorkDir option essentially non-functional. FIXED LOCATIONS: 1. internal/restore/engine.go:632 - CRITICAL: Used BackupDir instead of WorkDir for extraction 2. cmd/restore.go:354,834 - CLI restore/diagnose commands ignored WorkDir 3. cmd/migrate.go:208,347 - Migration commands hardcoded /tmp 4. internal/migrate/engine.go:120 - Migration engine ignored WorkDir 5. internal/config/config.go:224 - SwapFilePath hardcoded /tmp 6. internal/config/config.go:519 - Backup directory fallback hardcoded /tmp 7. internal/tui/restore_exec.go:161 - Debug logs hardcoded /tmp 8. internal/tui/settings.go:805 - Directory browser default hardcoded /tmp 9. internal/tui/restore_preview.go:474 - Display message hardcoded /tmp NEW FEATURES: - Added Config.GetEffectiveWorkDir() helper method - WorkDir now respects WORK_DIR environment variable - All temp operations now consistently use configured WorkDir with /tmp fallback IMPACT: - Restores on systems with small root disks now work properly with WorkDir configured - Admins can control disk space usage for all temporary operations - Debug logs, extraction dirs, swap files all respect WorkDir setting Version: 3.42.1 (Critical Fix Release)
213 lines
6.2 KiB
Go
213 lines
6.2 KiB
Go
package restore
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"dbbackup/internal/cloud"
|
|
"dbbackup/internal/logger"
|
|
"dbbackup/internal/metadata"
|
|
)
|
|
|
|
// CloudDownloader handles downloading backups from cloud storage
|
|
type CloudDownloader struct {
|
|
backend cloud.Backend
|
|
log logger.Logger
|
|
}
|
|
|
|
// NewCloudDownloader creates a new cloud downloader
|
|
func NewCloudDownloader(backend cloud.Backend, log logger.Logger) *CloudDownloader {
|
|
return &CloudDownloader{
|
|
backend: backend,
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
// DownloadOptions contains options for downloading from cloud
|
|
type DownloadOptions struct {
|
|
VerifyChecksum bool // Verify SHA-256 checksum after download
|
|
KeepLocal bool // Keep downloaded file (don't delete temp)
|
|
TempDir string // Temp directory (default: os.TempDir())
|
|
}
|
|
|
|
// DownloadResult contains information about a downloaded backup
|
|
type DownloadResult struct {
|
|
LocalPath string // Path to downloaded file
|
|
RemotePath string // Original remote path
|
|
Size int64 // File size in bytes
|
|
SHA256 string // SHA-256 checksum (if verified)
|
|
MetadataPath string // Path to downloaded metadata (if exists)
|
|
IsTempFile bool // Whether the file is in a temp directory
|
|
}
|
|
|
|
// Download downloads a backup from cloud storage
|
|
func (d *CloudDownloader) Download(ctx context.Context, remotePath string, opts DownloadOptions) (*DownloadResult, error) {
|
|
// Determine temp directory (use from opts, or from config's WorkDir, or fallback to system temp)
|
|
tempDir := opts.TempDir
|
|
if tempDir == "" {
|
|
// Try to get from config if available (passed via opts.TempDir)
|
|
tempDir = os.TempDir()
|
|
}
|
|
|
|
// Create unique temp subdirectory
|
|
tempSubDir := filepath.Join(tempDir, fmt.Sprintf("dbbackup-download-%d", os.Getpid()))
|
|
if err := os.MkdirAll(tempSubDir, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create temp directory: %w", err)
|
|
}
|
|
|
|
// Extract filename from remote path
|
|
filename := filepath.Base(remotePath)
|
|
localPath := filepath.Join(tempSubDir, filename)
|
|
|
|
d.log.Info("Downloading backup from cloud", "remote", remotePath, "local", localPath)
|
|
|
|
// Get file size for progress tracking
|
|
size, err := d.backend.GetSize(ctx, remotePath)
|
|
if err != nil {
|
|
d.log.Warn("Could not get remote file size", "error", err)
|
|
size = 0 // Continue anyway
|
|
}
|
|
|
|
// Progress callback
|
|
var lastPercent int
|
|
progressCallback := func(transferred, total int64) {
|
|
if total > 0 {
|
|
percent := int(float64(transferred) / float64(total) * 100)
|
|
if percent != lastPercent && percent%10 == 0 {
|
|
d.log.Info("Download progress", "percent", percent, "transferred", cloud.FormatSize(transferred), "total", cloud.FormatSize(total))
|
|
lastPercent = percent
|
|
}
|
|
}
|
|
}
|
|
|
|
// Download file
|
|
if err := d.backend.Download(ctx, remotePath, localPath, progressCallback); err != nil {
|
|
// Cleanup on failure
|
|
os.RemoveAll(tempSubDir)
|
|
return nil, fmt.Errorf("download failed: %w", err)
|
|
}
|
|
|
|
result := &DownloadResult{
|
|
LocalPath: localPath,
|
|
RemotePath: remotePath,
|
|
Size: size,
|
|
IsTempFile: !opts.KeepLocal,
|
|
}
|
|
|
|
// Try to download metadata file
|
|
metaRemotePath := remotePath + ".meta.json"
|
|
exists, err := d.backend.Exists(ctx, metaRemotePath)
|
|
if err == nil && exists {
|
|
metaLocalPath := localPath + ".meta.json"
|
|
if err := d.backend.Download(ctx, metaRemotePath, metaLocalPath, nil); err != nil {
|
|
d.log.Warn("Failed to download metadata", "error", err)
|
|
} else {
|
|
result.MetadataPath = metaLocalPath
|
|
d.log.Debug("Downloaded metadata", "path", metaLocalPath)
|
|
}
|
|
}
|
|
|
|
// Verify checksum if requested
|
|
if opts.VerifyChecksum {
|
|
d.log.Info("Verifying checksum...")
|
|
checksum, err := calculateSHA256(localPath)
|
|
if err != nil {
|
|
// Cleanup on verification failure
|
|
os.RemoveAll(tempSubDir)
|
|
return nil, fmt.Errorf("checksum calculation failed: %w", err)
|
|
}
|
|
result.SHA256 = checksum
|
|
|
|
// Check against metadata if available
|
|
if result.MetadataPath != "" {
|
|
meta, err := metadata.Load(result.MetadataPath)
|
|
if err != nil {
|
|
d.log.Warn("Failed to load metadata for verification", "error", err)
|
|
} else if meta.SHA256 != "" && meta.SHA256 != checksum {
|
|
// Cleanup on verification failure
|
|
os.RemoveAll(tempSubDir)
|
|
return nil, fmt.Errorf("checksum mismatch: expected %s, got %s", meta.SHA256, checksum)
|
|
} else if meta.SHA256 == checksum {
|
|
d.log.Info("Checksum verified successfully", "sha256", checksum)
|
|
}
|
|
}
|
|
}
|
|
|
|
d.log.Info("Download completed", "path", localPath, "size", cloud.FormatSize(result.Size))
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// DownloadFromURI downloads a backup using a cloud URI
|
|
func (d *CloudDownloader) DownloadFromURI(ctx context.Context, uri string, opts DownloadOptions) (*DownloadResult, error) {
|
|
// Parse URI
|
|
cloudURI, err := cloud.ParseCloudURI(uri)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid cloud URI: %w", err)
|
|
}
|
|
|
|
// Download using the path from URI
|
|
return d.Download(ctx, cloudURI.Path, opts)
|
|
}
|
|
|
|
// Cleanup removes downloaded temp files
|
|
func (r *DownloadResult) Cleanup() error {
|
|
if !r.IsTempFile {
|
|
return nil // Don't delete non-temp files
|
|
}
|
|
|
|
// Remove the entire temp directory
|
|
tempDir := filepath.Dir(r.LocalPath)
|
|
if err := os.RemoveAll(tempDir); err != nil {
|
|
return fmt.Errorf("failed to cleanup temp files: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// calculateSHA256 calculates the SHA-256 checksum of a file
|
|
func calculateSHA256(filePath string) (string, error) {
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer file.Close()
|
|
|
|
hash := sha256.New()
|
|
if _, err := io.Copy(hash, file); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
// DownloadFromCloudURI is a convenience function to download from a cloud URI
|
|
func DownloadFromCloudURI(ctx context.Context, uri string, opts DownloadOptions) (*DownloadResult, error) {
|
|
// Parse URI
|
|
cloudURI, err := cloud.ParseCloudURI(uri)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid cloud URI: %w", err)
|
|
}
|
|
|
|
// Create config from URI
|
|
cfg := cloudURI.ToConfig()
|
|
|
|
// Create backend
|
|
backend, err := cloud.NewBackend(cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create cloud backend: %w", err)
|
|
}
|
|
|
|
// Create downloader
|
|
log := logger.New("info", "text")
|
|
downloader := NewCloudDownloader(backend, log)
|
|
|
|
// Download
|
|
return downloader.Download(ctx, cloudURI.Path, opts)
|
|
}
|