package swap import ( "fmt" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "dbbackup/internal/logger" ) // Manager handles temporary swap file creation and cleanup type Manager struct { swapPath string sizeGB int isActive bool wasCreated bool log logger.Logger } // NewManager creates a new swap file manager func NewManager(swapPath string, sizeGB int, log logger.Logger) *Manager { return &Manager{ swapPath: swapPath, sizeGB: sizeGB, log: log, } } // IsSupported checks if swap file management is supported on this platform func (m *Manager) IsSupported() bool { // Only supported on Linux return runtime.GOOS == "linux" } // NeedsRoot checks if we need root privileges for swap operations func (m *Manager) NeedsRoot() bool { // On Linux, swap operations require root return runtime.GOOS == "linux" && os.Geteuid() != 0 } // GetCurrentSwap returns current swap usage info func (m *Manager) GetCurrentSwap() (totalMB, usedMB, freeMB int64, err error) { // Read /proc/meminfo for swap info data, err := os.ReadFile("/proc/meminfo") if err != nil { return 0, 0, 0, err } lines := strings.Split(string(data), "\n") for _, line := range lines { fields := strings.Fields(line) if len(fields) < 2 { continue } switch fields[0] { case "SwapTotal:": if val, err := strconv.ParseInt(fields[1], 10, 64); err == nil { totalMB = val / 1024 } case "SwapFree:": if val, err := strconv.ParseInt(fields[1], 10, 64); err == nil { freeMB = val / 1024 } } } usedMB = totalMB - freeMB return } // IsSwapFileActive checks if our specific swap file is currently active func (m *Manager) IsSwapFileActive() (bool, error) { // Read /proc/swaps to see active swap devices data, err := os.ReadFile("/proc/swaps") if err != nil { return false, err } absPath, _ := filepath.Abs(m.swapPath) lines := strings.Split(string(data), "\n") for _, line := range lines { if strings.Contains(line, absPath) || strings.Contains(line, m.swapPath) { return true, nil } } return false, nil } // Setup creates and enables the swap file if needed func (m *Manager) Setup() error { if !m.IsSupported() { return fmt.Errorf("swap file management not supported on %s", runtime.GOOS) } if m.sizeGB <= 0 { return fmt.Errorf("swap size must be > 0 GB") } if m.NeedsRoot() { m.log.Warn("Swap file creation requires root privileges, skipping automatic swap setup") return fmt.Errorf("swap file operations require root privileges (current euid: %d)", os.Geteuid()) } m.log.Info("Setting up temporary swap file", "path", m.swapPath, "size_gb", m.sizeGB) // Check if swap file already exists and is active if active, _ := m.IsSwapFileActive(); active { m.log.Info("Swap file already active", "path", m.swapPath) m.isActive = true return nil } // Check if file exists but is not active if _, err := os.Stat(m.swapPath); err == nil { m.log.Warn("Swap file exists but is not active, removing", "path", m.swapPath) if err := os.Remove(m.swapPath); err != nil { return fmt.Errorf("failed to remove existing swap file: %w", err) } } // Create swap file directory if needed swapDir := filepath.Dir(m.swapPath) if err := os.MkdirAll(swapDir, 0755); err != nil { return fmt.Errorf("failed to create swap directory: %w", err) } // Calculate size in bytes sizeBytes := int64(m.sizeGB) * 1024 * 1024 * 1024 m.log.Info("Creating swap file", "size_bytes", sizeBytes) // Use fallocate for fast file creation cmd := exec.Command("fallocate", "-l", fmt.Sprintf("%d", sizeBytes), m.swapPath) if output, err := cmd.CombinedOutput(); err != nil { // Fallback to dd if fallocate fails m.log.Warn("fallocate failed, using dd (slower)", "error", err, "output", string(output)) cmd = exec.Command("dd", "if=/dev/zero", "of="+m.swapPath, "bs=1M", fmt.Sprintf("count=%d", m.sizeGB*1024)) if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to create swap file with dd: %w (output: %s)", err, string(output)) } } m.wasCreated = true // Set correct permissions (600 for security) if err := os.Chmod(m.swapPath, 0600); err != nil { m.cleanup() return fmt.Errorf("failed to set swap file permissions: %w", err) } // Format as swap m.log.Info("Formatting swap file") cmd = exec.Command("mkswap", m.swapPath) if output, err := cmd.CombinedOutput(); err != nil { m.cleanup() return fmt.Errorf("failed to format swap file: %w (output: %s)", err, string(output)) } // Enable swap m.log.Info("Enabling swap file") cmd = exec.Command("swapon", m.swapPath) if output, err := cmd.CombinedOutput(); err != nil { m.cleanup() return fmt.Errorf("failed to enable swap file: %w (output: %s)", err, string(output)) } m.isActive = true // Log current swap status if total, used, free, err := m.GetCurrentSwap(); err == nil { m.log.Info("Swap status after setup", "total_mb", total, "used_mb", used, "free_mb", free, "added_gb", m.sizeGB) } return nil } // cleanup removes the swap file (internal helper) func (m *Manager) cleanup() { if m.wasCreated { os.Remove(m.swapPath) m.wasCreated = false } } // Cleanup disables and removes the swap file func (m *Manager) Cleanup() error { if !m.isActive { return nil } m.log.Info("Cleaning up swap file", "path", m.swapPath) // Check if still active if active, _ := m.IsSwapFileActive(); active { // Disable swap m.log.Info("Disabling swap file") cmd := exec.Command("swapoff", m.swapPath) if output, err := cmd.CombinedOutput(); err != nil { m.log.Warn("Failed to disable swap file", "error", err, "output", string(output)) // Continue anyway } } // Remove file if we created it if m.wasCreated { m.log.Info("Removing swap file", "path", m.swapPath) if err := os.Remove(m.swapPath); err != nil { m.log.Warn("Failed to remove swap file", "error", err) return fmt.Errorf("failed to remove swap file: %w", err) } m.wasCreated = false } m.isActive = false return nil } // IsActive returns whether the swap file is currently active func (m *Manager) IsActive() bool { return m.isActive } // WasCreated returns whether this manager created the swap file func (m *Manager) WasCreated() bool { return m.wasCreated }