package api import ( "bytes" "fmt" "io" "net/http" "os" "strconv" "strings" "time" "debtik/internal/auth" "debtik/internal/backup" ) func (s *Server) requireAdminRole(w http.ResponseWriter, r *http.Request) bool { sess := getSessionFromContext(r.Context()) if sess != nil { if sess.Role != auth.RoleFull { writeError(w, http.StatusForbidden, "Administrator privileges required") return false } return true } if s.authMgr != nil { writeError(w, http.StatusUnauthorized, "Authentication required") return false } return true } func (s *Server) handleBackupExport(w http.ResponseWriter, r *http.Request) { if !s.requireAdminRole(w, r) { return } if s.backupMgr == nil { s.backupMgr = backup.NewManager("", "", "", nil) } comment := r.URL.Query().Get("comment") zipBytes, manifest, err := s.backupMgr.CreateBackup(comment) if err != nil { writeError(w, http.StatusInternalServerError, "Failed to create backup: "+err.Error()) return } filename := fmt.Sprintf("debtik-backup-%s-%s.zip", manifest.Hostname, manifest.CreatedAt.Format("20060102-150405")) w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) w.Header().Set("Content-Length", strconv.Itoa(len(zipBytes))) w.WriteHeader(http.StatusOK) _, _ = w.Write(zipBytes) } func (s *Server) handleBackupInspect(w http.ResponseWriter, r *http.Request) { if !s.requireAdminRole(w, r) { return } if s.backupMgr == nil { s.backupMgr = backup.NewManager("", "", "", nil) } buf, err := readUploadedBackup(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } manifest, err := s.backupMgr.InspectBackup(bytes.NewReader(buf), int64(len(buf))) if err != nil { writeError(w, http.StatusBadRequest, "Invalid backup archive: "+err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{ "success": true, "manifest": manifest, }) } func (s *Server) handleBackupRestore(w http.ResponseWriter, r *http.Request) { if !s.requireAdminRole(w, r) { return } if s.backupMgr == nil { s.backupMgr = backup.NewManager("", "", "", nil) } buf, err := readUploadedBackup(r) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } res, err := s.backupMgr.RestoreBackup(bytes.NewReader(buf), int64(len(buf))) if err != nil { writeError(w, http.StatusBadRequest, "Restore failed: "+err.Error()) return } // Trigger asynchronous restart so DebTik reloads cleanly s.backupMgr.TriggerRestart(1 * time.Second) writeJSON(w, http.StatusOK, res) } func (s *Server) handleBackupList(w http.ResponseWriter, r *http.Request) { if !s.requireAdminRole(w, r) { return } if s.backupMgr == nil { s.backupMgr = backup.NewManager("", "", "", nil) } list, err := s.backupMgr.ListLocalBackups() if err != nil { writeError(w, http.StatusInternalServerError, "Failed to list backups: "+err.Error()) return } writeJSON(w, http.StatusOK, list) } func (s *Server) handleBackupDownloadLocal(w http.ResponseWriter, r *http.Request) { if !s.requireAdminRole(w, r) { return } if s.backupMgr == nil { s.backupMgr = backup.NewManager("", "", "", nil) } filename := r.PathValue("filename") fullPath, err := s.backupMgr.GetLocalBackup(filename) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) http.ServeFile(w, r, fullPath) } func (s *Server) handleBackupRestoreLocal(w http.ResponseWriter, r *http.Request) { if !s.requireAdminRole(w, r) { return } if s.backupMgr == nil { s.backupMgr = backup.NewManager("", "", "", nil) } filename := r.PathValue("filename") fullPath, err := s.backupMgr.GetLocalBackup(filename) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } f, err := os.Open(fullPath) if err != nil { writeError(w, http.StatusInternalServerError, "Cannot open backup file: "+err.Error()) return } defer f.Close() fi, err := f.Stat() if err != nil { writeError(w, http.StatusInternalServerError, "Cannot stat backup file: "+err.Error()) return } res, err := s.backupMgr.RestoreBackup(f, fi.Size()) if err != nil { writeError(w, http.StatusBadRequest, "Restore failed: "+err.Error()) return } s.backupMgr.TriggerRestart(1 * time.Second) writeJSON(w, http.StatusOK, res) } func readUploadedBackup(r *http.Request) ([]byte, error) { contentType := r.Header.Get("Content-Type") // Multipart form upload if strings.Contains(contentType, "multipart/form-data") { if err := r.ParseMultipartForm(32 << 20); err != nil { return nil, fmt.Errorf("failed parsing upload: %w", err) } var file io.Reader f, _, err := r.FormFile("backup") if err == nil { defer f.Close() file = f } else { f, _, err2 := r.FormFile("file") if err2 == nil { defer f.Close() file = f } else { return nil, fmt.Errorf("no 'backup' or 'file' uploaded") } } return io.ReadAll(file) } // Raw binary zip stream if r.Body == nil { return nil, fmt.Errorf("empty request body") } defer r.Body.Close() return io.ReadAll(r.Body) }