package handlers import ( "errors" "net/http" "strconv" "github.com/gin-gonic/gin" "git.netcell-it.de/projekte/edgeguard-native/internal/handlers/response" "git.netcell-it.de/projekte/edgeguard-native/internal/services/audit" "git.netcell-it.de/projekte/edgeguard-native/internal/services/backup" ) // BackupHandler exposes: // // GET /api/v1/backups — list // POST /api/v1/backups — trigger manual backup (sync) // GET /api/v1/backups/:id — single entry // GET /api/v1/backups/:id/download — sendfile tar.gz // DELETE /api/v1/backups/:id — delete tar.gz + row type BackupHandler struct { Service *backup.Service Audit *audit.Repo NodeID string Version string } func NewBackupHandler(s *backup.Service, a *audit.Repo, nodeID, version string) *BackupHandler { return &BackupHandler{Service: s, Audit: a, NodeID: nodeID, Version: version} } func (h *BackupHandler) Register(rg *gin.RouterGroup) { g := rg.Group("/backups") g.GET("", h.List) g.POST("", h.Trigger) g.GET("/:id", h.Get) g.GET("/:id/download", h.Download) g.POST("/:id/restore", h.Restore) g.DELETE("/:id", h.Delete) } // Restore startet einen Restore aus einem vorhandenen Backup. Endpoint // returnt sofort 202 Accepted — der eigentliche Restore läuft in einer // transient systemd-Unit; die UI pollt /healthz für die Restart- // Detection. Massive Audit-Trail, weil das ein destruktiver Eingriff // in den live-DB-State ist. func (h *BackupHandler) Restore(c *gin.Context) { id, ok := parseID(c) if !ok { return } e, err := h.Service.Restore(c.Request.Context(), id) if err != nil { response.Err(c, http.StatusInternalServerError, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "backup.restore", e.File, gin.H{"id": id, "sha256": e.SHA256}, h.NodeID) c.JSON(http.StatusAccepted, response.Envelope{ Data: gin.H{"status": "restoring", "file": e.File, "id": id}, Message: "Restore gestartet", }) } func (h *BackupHandler) List(c *gin.Context) { out, err := h.Service.List(c.Request.Context()) if err != nil { response.Internal(c, err) return } response.OK(c, gin.H{"backups": out}) } func (h *BackupHandler) Trigger(c *gin.Context) { // Manual backup läuft synchron — der Operator wartet vor dem // Knopf. Bei echten Multi-GB-DBs wäre async besser, aber unsere // edgeguard-DB ist klein (<50 MB typisch). res, err := h.Service.Run(c.Request.Context(), backup.KindManual, h.Version) if err != nil { response.Err(c, http.StatusInternalServerError, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "backup.create", res.File, gin.H{"size": res.SizeBytes, "sha256": res.SHA256}, h.NodeID) response.OK(c, gin.H{ "id": res.ID, "file": res.File, "size_bytes": res.SizeBytes, "sha256": res.SHA256, "db_dump_bytes": res.DBDumpBytes, "files_bytes": res.FilesBytes, "started_at": res.StartedAt, "finished_at": res.FinishedAt, }) } func (h *BackupHandler) Get(c *gin.Context) { id, ok := parseID(c) if !ok { return } e, _, err := h.Service.Get(c.Request.Context(), id) if err != nil { response.NotFound(c, err) return } response.OK(c, e) } func (h *BackupHandler) Download(c *gin.Context) { id, ok := parseID(c) if !ok { return } e, path, err := h.Service.Get(c.Request.Context(), id) if err != nil { response.NotFound(c, err) return } // gin.FileAttachment setzt Content-Disposition + sendet stream. c.FileAttachment(path, e.File) } func (h *BackupHandler) Delete(c *gin.Context) { id, ok := parseID(c) if !ok { return } if err := h.Service.Delete(c.Request.Context(), id); err != nil { response.Err(c, http.StatusInternalServerError, err) return } _ = h.Audit.Log(c.Request.Context(), actorOf(c), "backup.delete", strconv.FormatInt(id, 10), gin.H{"id": id}, h.NodeID) response.NoContent(c) } // Defensive — falls jemand den Pool fehlerhaft injected. var _ = errors.New