| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- 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)
- }
|