package api import ( "encoding/base64" "encoding/json" "fmt" "log" "net/http" "strconv" "strings" "time" "github.com/cynfo/rmm-backend/ws" ) // POST /api/v1/agents/{id}/backup — Backup ausloesen (via WebSocket an Agent) func (h *Handler) triggerBackup(hub *ws.Hub) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { agentID := r.PathValue("id") if !hub.IsAgentConnected(agentID) { writeError(w, http.StatusServiceUnavailable, "Agent nicht verbunden") return } cmdID := generateID() cmd := map[string]interface{}{ "type": "command", "id": cmdID, "command": "backup", "params": map[string]interface{}{}, } // Response-Channel registrieren responseCh := hub.RegisterResponseWaiter(cmdID) defer hub.UnregisterResponseWaiter(cmdID) cmdJSON, _ := json.Marshal(cmd) if err := hub.SendToAgent(agentID, cmdJSON); err != nil { log.Printf("Backup-Command senden fehlgeschlagen: %v", err) writeError(w, http.StatusInternalServerError, "Command senden fehlgeschlagen") return } log.Printf("Backup-Command (ID: %s) an Agent %s gesendet", cmdID, agentID) // Auf Response warten (config.xml kann gross sein, 60s Timeout) select { case resp := <-responseCh: status, _ := resp["status"].(string) if status != "ok" { errMsg, _ := resp["error"].(string) writeError(w, http.StatusInternalServerError, fmt.Sprintf("Backup fehlgeschlagen: %s", errMsg)) return } data, ok := resp["data"].(map[string]interface{}) if !ok { writeError(w, http.StatusInternalServerError, "Ungueltiges Response-Format") return } // Config aus Base64 dekodieren configB64, _ := data["config"].(string) hash, _ := data["hash"].(string) sizeFloat, _ := data["size"].(float64) size := int(sizeFloat) configXML, err := base64.StdEncoding.DecodeString(configB64) if err != nil { writeError(w, http.StatusInternalServerError, "Base64-Dekodierung fehlgeschlagen") return } // In DB speichern (dedupliziert) backupID, isNew, err := h.db.SaveConfigBackup(agentID, hash, size, string(configXML)) if err != nil { log.Printf("Backup speichern fehlgeschlagen: %v", err) writeError(w, http.StatusInternalServerError, "Backup speichern fehlgeschlagen") return } msg := "Backup gespeichert" if !isNew { msg = "Config unveraendert (Hash identisch)" } writeJSON(w, http.StatusOK, map[string]interface{}{ "backup_id": backupID, "hash": hash, "size": size, "new": isNew, "message": msg, }) case <-time.After(60 * time.Second): writeError(w, http.StatusGatewayTimeout, "Timeout: Agent hat nicht rechtzeitig geantwortet") } } } // GET /api/v1/agents/{id}/backups — Alle Backups auflisten func (h *Handler) listBackups() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { agentID := r.PathValue("id") limit := 50 if l := r.URL.Query().Get("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed } } backups, err := h.db.GetConfigBackups(agentID, limit) if err != nil { log.Printf("Backups laden fehlgeschlagen: %v", err) writeError(w, http.StatusInternalServerError, "Fehler beim Laden") return } writeJSON(w, http.StatusOK, backups) } } // GET /api/v1/agents/{id}/backups/{backup_id} — Einzelnes Backup herunterladen func (h *Handler) getBackup() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { agentID := r.PathValue("id") backupIDStr := r.PathValue("backup_id") // "latest" als Spezialwert if backupIDStr == "latest" { backup, err := h.db.GetLatestConfigBackup(agentID) if err != nil { writeError(w, http.StatusInternalServerError, "Fehler beim Laden") return } if backup == nil { writeError(w, http.StatusNotFound, "Kein Backup vorhanden") return } // Format: raw XML oder JSON? if r.URL.Query().Get("format") == "xml" { w.Header().Set("Content-Type", "application/xml") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=config_%s.xml", agentID[:8])) w.Write([]byte(backup["config_xml"].(string))) return } writeJSON(w, http.StatusOK, backup) return } backupID, err := strconv.ParseInt(backupIDStr, 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "Ungueltige Backup-ID") return } backup, err := h.db.GetConfigBackup(agentID, backupID) if err != nil { writeError(w, http.StatusInternalServerError, "Fehler beim Laden") return } if backup == nil { writeError(w, http.StatusNotFound, "Backup nicht gefunden") return } // Raw XML Download if r.URL.Query().Get("format") == "xml" { w.Header().Set("Content-Type", "application/xml") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=config_%s_%d.xml", agentID[:8], backupID)) w.Write([]byte(backup["config_xml"].(string))) return } writeJSON(w, http.StatusOK, backup) } } // DELETE /api/v1/agents/{id}/backups/{backup_id} — Backup loeschen func (h *Handler) deleteBackup() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { agentID := r.PathValue("id") backupIDStr := r.PathValue("backup_id") backupID, err := strconv.ParseInt(backupIDStr, 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "Ungueltige Backup-ID") return } deleted, err := h.db.DeleteConfigBackup(agentID, backupID) if err != nil { writeError(w, http.StatusInternalServerError, "Loeschen fehlgeschlagen") return } if !deleted { writeError(w, http.StatusNotFound, "Backup nicht gefunden") return } writeJSON(w, http.StatusOK, map[string]string{"message": "Backup geloescht"}) } } // GET /api/v1/agents/{id}/backups/diff?a={id1}&b={id2} — Diff zwischen zwei Backups func (h *Handler) diffBackups() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { agentID := r.PathValue("id") aStr := r.URL.Query().Get("a") bStr := r.URL.Query().Get("b") if aStr == "" || bStr == "" { writeError(w, http.StatusBadRequest, "Parameter 'a' und 'b' (Backup-IDs) erforderlich") return } aID, err := strconv.ParseInt(aStr, 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "Ungueltige Backup-ID 'a'") return } bID, err := strconv.ParseInt(bStr, 10, 64) if err != nil { writeError(w, http.StatusBadRequest, "Ungueltige Backup-ID 'b'") return } backupA, err := h.db.GetConfigBackup(agentID, aID) if err != nil || backupA == nil { writeError(w, http.StatusNotFound, fmt.Sprintf("Backup %d nicht gefunden", aID)) return } backupB, err := h.db.GetConfigBackup(agentID, bID) if err != nil || backupB == nil { writeError(w, http.StatusNotFound, fmt.Sprintf("Backup %d nicht gefunden", bID)) return } xmlA := backupA["config_xml"].(string) xmlB := backupB["config_xml"].(string) if backupA["hash"] == backupB["hash"] { writeJSON(w, http.StatusOK, map[string]interface{}{ "identical": true, "a": map[string]interface{}{"id": aID, "hash": backupA["hash"], "created_at": backupA["created_at"]}, "b": map[string]interface{}{"id": bID, "hash": backupB["hash"], "created_at": backupB["created_at"]}, "diff": "", }) return } // Zeilenweises Diff berechnen diff := computeLineDiff(xmlA, xmlB) writeJSON(w, http.StatusOK, map[string]interface{}{ "identical": false, "a": map[string]interface{}{"id": aID, "hash": backupA["hash"], "created_at": backupA["created_at"]}, "b": map[string]interface{}{"id": bID, "hash": backupB["hash"], "created_at": backupB["created_at"]}, "diff": diff, }) } } // computeLineDiff erzeugt ein einfaches zeilenweises Diff (unified-style) func computeLineDiff(a, b string) string { linesA := strings.Split(a, "\n") linesB := strings.Split(b, "\n") var result []string maxLen := len(linesA) if len(linesB) > maxLen { maxLen = len(linesB) } // Einfaches sequenzielles Diff — fuer XML config.xml voellig ausreichend i, j := 0, 0 for i < len(linesA) || j < len(linesB) { if i < len(linesA) && j < len(linesB) && linesA[i] == linesB[j] { // Gleiche Zeile — uebergehen (nur Context bei Aenderungen zeigen) i++ j++ continue } // Vorausschauen ob die Zeile weiter unten in der anderen Datei existiert foundInB := -1 if i < len(linesA) { for k := j; k < len(linesB) && k < j+10; k++ { if linesA[i] == linesB[k] { foundInB = k break } } } foundInA := -1 if j < len(linesB) { for k := i; k < len(linesA) && k < i+10; k++ { if linesB[j] == linesA[k] { foundInA = k break } } } if foundInB >= 0 && (foundInA < 0 || foundInB-j <= foundInA-i) { // Zeilen in B eingefuegt for j < foundInB { result = append(result, fmt.Sprintf("+ %s", linesB[j])) j++ } } else if foundInA >= 0 { // Zeilen in A entfernt for i < foundInA { result = append(result, fmt.Sprintf("- %s", linesA[i])) i++ } } else { // Zeile geaendert if i < len(linesA) { result = append(result, fmt.Sprintf("- %s", linesA[i])) i++ } if j < len(linesB) { result = append(result, fmt.Sprintf("+ %s", linesB[j])) j++ } } } return strings.Join(result, "\n") }