package handlers import ( "encoding/json" "fmt" "net/http" "time" "github.com/go-chi/chi/v5" "sneak.berlin/go/upaas/internal/models" "sneak.berlin/go/upaas/internal/service/app" "sneak.berlin/go/upaas/templates" ) // importMaxBodyBytes is the maximum allowed request body size for backup import (10 MB). const importMaxBodyBytes = 10 << 20 // HandleExportApp exports a single app's configuration as a JSON download. func (h *Handlers) HandleExportApp() http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { appID := chi.URLParam(request, "id") application, findErr := models.FindApp(request.Context(), h.db, appID) if findErr != nil || application == nil { http.NotFound(writer, request) return } bundle, exportErr := h.appService.ExportApp(request.Context(), application) if exportErr != nil { h.log.Error("failed to export app", "error", exportErr, "app", application.Name) http.Error(writer, "Internal Server Error", http.StatusInternalServerError) return } filename := fmt.Sprintf("upaas-backup-%s-%s.json", application.Name, time.Now().UTC().Format("20060102-150405"), ) writer.Header().Set("Content-Type", "application/json") writer.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`) encoder := json.NewEncoder(writer) encoder.SetIndent("", " ") err := encoder.Encode(bundle) if err != nil { h.log.Error("failed to encode backup", "error", err) } } } // HandleExportAllApps exports all app configurations as a JSON download. func (h *Handlers) HandleExportAllApps() http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { bundle, exportErr := h.appService.ExportAllApps(request.Context()) if exportErr != nil { h.log.Error("failed to export all apps", "error", exportErr) http.Error(writer, "Internal Server Error", http.StatusInternalServerError) return } filename := fmt.Sprintf("upaas-backup-all-%s.json", time.Now().UTC().Format("20060102-150405"), ) writer.Header().Set("Content-Type", "application/json") writer.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`) encoder := json.NewEncoder(writer) encoder.SetIndent("", " ") err := encoder.Encode(bundle) if err != nil { h.log.Error("failed to encode backup", "error", err) } } } // HandleImportPage renders the import/restore page. func (h *Handlers) HandleImportPage() http.HandlerFunc { tmpl := templates.GetParsed() return func(writer http.ResponseWriter, request *http.Request) { data := h.addGlobals(map[string]any{ "Success": request.URL.Query().Get("success"), }, request) h.renderTemplate(writer, tmpl, "backup_import.html", data) } } // HandleImportApps processes an uploaded backup JSON file and imports apps. func (h *Handlers) HandleImportApps() http.HandlerFunc { tmpl := templates.GetParsed() return func(writer http.ResponseWriter, request *http.Request) { bundle, parseErr := h.parseBackupUpload(request) if parseErr != "" { data := h.addGlobals(map[string]any{"Error": parseErr}, request) h.renderTemplate(writer, tmpl, "backup_import.html", data) return } imported, skipped, importErr := h.appService.ImportApps( request.Context(), bundle, ) if importErr != nil { h.log.Error("failed to import apps", "error", importErr) data := h.addGlobals(map[string]any{ "Error": "Import failed: " + importErr.Error(), }, request) h.renderTemplate(writer, tmpl, "backup_import.html", data) return } successMsg := fmt.Sprintf("Imported %d app(s)", len(imported)) if len(skipped) > 0 { successMsg += fmt.Sprintf(", skipped %d (name conflict)", len(skipped)) } http.Redirect(writer, request, "/backup/import?success="+successMsg, http.StatusSeeOther, ) } } // parseBackupUpload extracts and validates a BackupBundle from a multipart upload. // Returns the bundle and an empty string on success, or nil and an error message. func (h *Handlers) parseBackupUpload( request *http.Request, ) (*app.BackupBundle, string) { request.Body = http.MaxBytesReader(nil, request.Body, importMaxBodyBytes) parseErr := request.ParseMultipartForm(importMaxBodyBytes) if parseErr != nil { return nil, "Failed to parse upload: " + parseErr.Error() } file, _, openErr := request.FormFile("backup_file") if openErr != nil { return nil, "Please select a backup file to import" } defer func() { _ = file.Close() }() var bundle app.BackupBundle decodeErr := json.NewDecoder(file).Decode(&bundle) if decodeErr != nil { return nil, "Invalid backup file: " + decodeErr.Error() } if bundle.Version != 1 { return nil, fmt.Sprintf( "Unsupported backup version: %d (expected 1)", bundle.Version, ) } if len(bundle.Apps) == 0 { return nil, "Backup file contains no apps" } return &bundle, "" } // HandleAPIExportApp exports a single app's configuration as JSON via API. func (h *Handlers) HandleAPIExportApp() http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { appID := chi.URLParam(request, "id") application, err := h.appService.GetApp(request.Context(), appID) if err != nil { h.respondJSON(writer, request, map[string]string{"error": "internal server error"}, http.StatusInternalServerError) return } if application == nil { h.respondJSON(writer, request, map[string]string{"error": "app not found"}, http.StatusNotFound) return } bundle, exportErr := h.appService.ExportApp(request.Context(), application) if exportErr != nil { h.log.Error("failed to export app", "error", exportErr) h.respondJSON(writer, request, map[string]string{"error": "failed to export app"}, http.StatusInternalServerError) return } h.respondJSON(writer, request, bundle, http.StatusOK) } } // HandleAPIExportAllApps exports all app configurations as JSON via API. func (h *Handlers) HandleAPIExportAllApps() http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { bundle, exportErr := h.appService.ExportAllApps(request.Context()) if exportErr != nil { h.log.Error("failed to export all apps", "error", exportErr) h.respondJSON(writer, request, map[string]string{"error": "failed to export apps"}, http.StatusInternalServerError) return } h.respondJSON(writer, request, bundle, http.StatusOK) } } // HandleAPIImportApps imports app configurations from a JSON request body via API. func (h *Handlers) HandleAPIImportApps() http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { request.Body = http.MaxBytesReader(writer, request.Body, importMaxBodyBytes) var bundle app.BackupBundle decodeErr := json.NewDecoder(request.Body).Decode(&bundle) if decodeErr != nil { h.respondJSON(writer, request, map[string]string{"error": "invalid request body"}, http.StatusBadRequest) return } if bundle.Version != 1 { h.respondJSON(writer, request, map[string]string{"error": fmt.Sprintf( "unsupported backup version: %d", bundle.Version, )}, http.StatusBadRequest) return } if len(bundle.Apps) == 0 { h.respondJSON(writer, request, map[string]string{"error": "backup contains no apps"}, http.StatusBadRequest) return } imported, skipped, importErr := h.appService.ImportApps( request.Context(), &bundle, ) if importErr != nil { h.log.Error("api: failed to import apps", "error", importErr) h.respondJSON(writer, request, map[string]string{"error": "import failed: " + importErr.Error()}, http.StatusInternalServerError) return } h.respondJSON(writer, request, map[string]any{ "imported": imported, "skipped": skipped, }, http.StatusOK) } }