package handlers import ( "backea/internal/backup" "context" "fmt" "io" "log" "net/http" "os" "os/exec" "path/filepath" "strings" "time" "github.com/labstack/echo/v4" ) // BackupActionsHandler handles backup-related actions like restore and download type BackupActionsHandler struct { backupFactory *backup.BackupFactory } // NewBackupActionsHandler creates a new backup actions handler func NewBackupActionsHandler(factory *backup.BackupFactory) *BackupActionsHandler { return &BackupActionsHandler{ backupFactory: factory, } } // RestoreRequest represents the request data for a backup restore type RestoreRequest struct { BackupID string `json:"backupID" form:"backupID"` GroupName string `json:"groupName" form:"groupName"` ServiceIndex string `json:"serviceIndex" form:"serviceIndex"` DestinationPath string `json:"destinationPath" form:"destinationPath"` } // RestoreBackup handles the request to restore a backup with destination path support func (h *BackupActionsHandler) RestoreBackup(c echo.Context) error { backupID := c.Param("backupID") if backupID == "" { return echo.NewHTTPError(http.StatusBadRequest, "backupID is required") } // Try to parse the group and service from query parameters groupName := c.QueryParam("groupName") serviceIndex := c.QueryParam("serviceIndex") destinationPath := c.QueryParam("destinationPath") // If not in query parameters, check form data if groupName == "" || serviceIndex == "" { var req RestoreRequest if err := c.Bind(&req); err == nil { groupName = req.GroupName serviceIndex = req.ServiceIndex destinationPath = req.DestinationPath } } if groupName == "" || serviceIndex == "" { return echo.NewHTTPError(http.StatusBadRequest, "groupName and serviceIndex are required either as query parameters or in the request body") } // Construct full service name serviceName := fmt.Sprintf("%s.%s", groupName, serviceIndex) log.Printf("Restoring backup ID=%s to service %s", backupID, serviceName) if destinationPath != "" { log.Printf("Restoring to custom destination: %s", destinationPath) } // Get the appropriate strategy for this service strategy, err := h.backupFactory.CreateBackupStrategyForService(groupName, serviceIndex) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to get backup strategy: %v", err)) } // If destination path is provided, use our custom restore function if destinationPath != "" { err = h.restoreBackupToCustomDestination(c.Request().Context(), strategy, backupID, serviceName, destinationPath) } else { // Otherwise use the regular restore err = strategy.RestoreBackup(c.Request().Context(), backupID, serviceName) } if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to restore backup: %v", err)) } return c.JSON(http.StatusOK, map[string]string{ "status": "success", "message": fmt.Sprintf("Backup %s restored successfully", backupID), }) } // RestoreBackupForm shows the restore form with directory input func (h *BackupActionsHandler) RestoreBackupForm(c echo.Context) error { backupID := c.QueryParam("backupID") groupName := c.QueryParam("groupName") serviceIndex := c.QueryParam("serviceIndex") if backupID == "" || groupName == "" || serviceIndex == "" { return echo.NewHTTPError(http.StatusBadRequest, "backupID, groupName, and serviceIndex are required") } // Build service name for lookup serviceName := fmt.Sprintf("%s.%s", groupName, serviceIndex) // Get backup info if available strategy, err := h.backupFactory.CreateBackupStrategyForService(groupName, serviceIndex) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to get backup strategy: %v", err)) } // Try to get backup info backupInfo, err := strategy.GetBackupInfo(c.Request().Context(), backupID, serviceName) var backupTimeStr string var backupSizeStr string var defaultPath string if err == nil && backupInfo != nil { backupTimeStr = backupInfo.CreationTime.Format("Jan 02, 2006 15:04:05") backupSizeStr = formatSize(backupInfo.Size) defaultPath = backupInfo.Source // Use original source as default path } else { backupTimeStr = "Unknown" backupSizeStr = "Unknown" defaultPath = "" } // TODO : create a real template return c.HTML(http.StatusOK, fmt.Sprintf(`

Restore Backup

Backup Date: %s

Size: %s

`, backupTimeStr, backupSizeStr, backupID, backupID, groupName, serviceIndex, defaultPath)) } // restoreBackupToCustomDestination handles restoring a backup to a custom directory func (h *BackupActionsHandler) restoreBackupToCustomDestination(ctx context.Context, strategy backup.Strategy, backupID, serviceName, destinationPath string) error { // Validate destination path destinationPath = filepath.Clean(destinationPath) // Check if destination exists stat, err := os.Stat(destinationPath) if err != nil { if os.IsNotExist(err) { // Create directory if it doesn't exist if err := os.MkdirAll(destinationPath, 0755); err != nil { return fmt.Errorf("failed to create destination directory: %w", err) } } else { return fmt.Errorf("failed to check destination directory: %w", err) } } else if !stat.IsDir() { return fmt.Errorf("destination path is not a directory") } // Create a temporary directory to store the downloaded backup tempDir, err := os.MkdirTemp("", "backea-restore-download-*") if err != nil { return fmt.Errorf("failed to create temporary download directory: %w", err) } defer os.RemoveAll(tempDir) // Create a temporary directory for extraction extractDir, err := os.MkdirTemp("", "backea-restore-extract-*") if err != nil { return fmt.Errorf("failed to create temporary extraction directory: %w", err) } defer os.RemoveAll(extractDir) // Download the backup to a temporary file log.Printf("Downloading backup %s to temporary location for service %s", backupID, serviceName) reader, err := strategy.DownloadBackup(ctx, backupID, serviceName) if err != nil { return fmt.Errorf("failed to download backup: %w", err) } defer reader.Close() // Create a temporary file to store the zip zipFile, err := os.CreateTemp(tempDir, "backup-*.zip") if err != nil { return fmt.Errorf("failed to create temporary file: %w", err) } zipPath := zipFile.Name() defer os.Remove(zipPath) // Copy the zip content to the temp file log.Printf("Writing backup data to temporary file: %s", zipPath) _, err = io.Copy(zipFile, reader) if err != nil { zipFile.Close() return fmt.Errorf("failed to write backup to disk: %w", err) } // Flush to disk and close the file zipFile.Sync() zipFile.Close() // Extract the zip to the extraction directory log.Printf("Extracting backup to %s", extractDir) if err := extractZip(zipPath, extractDir); err != nil { return fmt.Errorf("failed to extract backup: %w", err) } // Sync extracted files to the destination using rsync log.Printf("Syncing files from %s to %s", extractDir, destinationPath) if err := syncDirectories(extractDir, destinationPath); err != nil { return fmt.Errorf("failed to sync files to destination: %w", err) } log.Printf("Backup successfully restored to %s", destinationPath) return nil } // extractZip extracts a zip file to a destination directory func extractZip(zipFile, destDir string) error { // Use unzip command for extraction cmd := exec.Command("unzip", "-o", "-q", zipFile, "-d", destDir) output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to extract zip: %w, output: %s", err, string(output)) } return nil } // syncDirectories syncs files from source to destination using rsync func syncDirectories(src, dest string) error { // Ensure source path ends with a slash to copy contents, not the directory itself if !strings.HasSuffix(src, "/") { src = src + "/" } cmd := exec.Command("rsync", "-av", "--delete", src, dest) output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to sync directories: %w, output: %s", err, string(output)) } return nil } // Helper function to format file size func formatSize(size int64) string { const unit = 1024 if size < unit { return fmt.Sprintf("%d B", size) } div, exp := int64(unit), 0 for n := size / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.2f %cB", float64(size)/float64(div), "KMGTPE"[exp]) } // DownloadBackup handles the backup download request func (h *BackupActionsHandler) DownloadBackup(c echo.Context) error { // Get query parameters backupID := c.QueryParam("backupID") groupName := c.QueryParam("groupName") serviceIndex := c.QueryParam("serviceIndex") // Validate required parameters if backupID == "" || groupName == "" || serviceIndex == "" { return echo.NewHTTPError(http.StatusBadRequest, "backupID, groupName, and serviceIndex are required") } // Create service name for logging serviceName := fmt.Sprintf("%s.%s", groupName, serviceIndex) log.Printf("Downloading backup: ID=%s, Service=%s", backupID, serviceName) // Get the appropriate strategy for this service strategy, err := h.backupFactory.CreateBackupStrategyForService(groupName, serviceIndex) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to get backup strategy: %v", err)) } // Get all backups for this service backups, err := strategy.ListBackups(c.Request().Context(), serviceName) if err != nil { log.Printf("Warning: Failed to list backups: %v. Will try with provided ID directly.", err) } else { // Look for a matching backup with the proper snapshot ID found := false for _, backup := range backups { // If we find an exact match, or if the backup ID contains our ID (partial match) if backup.ID == backupID { found = true break } } if !found { log.Printf("Warning: Backup ID %s not found in service backups. Will try direct download anyway.", backupID) } } // Attempt to download the backup using the provided ID backupReader, err := strategy.DownloadBackup(c.Request().Context(), backupID, serviceName) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to download backup: %v", err)) } defer backupReader.Close() // Set filename for download filename := fmt.Sprintf("%s_%s_%s.zip", groupName, serviceIndex, time.Now().Format("20060102_150405"), ) // Set response headers for zip download c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) c.Response().Header().Set("Content-Type", "application/zip") // Stream the file to the client log.Printf("Streaming backup as %s", filename) _, err = io.Copy(c.Response().Writer, backupReader) if err != nil { log.Printf("Error streaming backup: %v", err) return err } log.Printf("Backup download for %s completed successfully", backupID) return nil }