| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- package api
- import (
- "bytes"
- "errors"
- "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(w, r)
- if err != nil {
- writeBackupUploadError(w, err)
- 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(w, r)
- if err != nil {
- writeBackupUploadError(w, err)
- 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)
- }
- const maxBackupUploadBytes int64 = backup.MaxArchiveBytes + (1 << 20)
- func writeBackupUploadError(w http.ResponseWriter, err error) {
- var maxErr *http.MaxBytesError
- if errors.As(err, &maxErr) {
- writeError(w, http.StatusRequestEntityTooLarge, "Backup upload exceeds the size limit")
- return
- }
- writeError(w, http.StatusBadRequest, err.Error())
- }
- func readUploadedBackup(w http.ResponseWriter, r *http.Request) ([]byte, error) {
- return readUploadedBackupLimit(w, r, maxBackupUploadBytes)
- }
- func readUploadedBackupLimit(w http.ResponseWriter, r *http.Request, limit int64) ([]byte, error) {
- if r.Body == nil {
- return nil, fmt.Errorf("empty request body")
- }
- if r.ContentLength > limit {
- return nil, &http.MaxBytesError{Limit: limit}
- }
- r.Body = http.MaxBytesReader(w, r.Body, limit)
- defer r.Body.Close()
- contentType := r.Header.Get("Content-Type")
- if strings.Contains(contentType, "multipart/form-data") {
- if err := r.ParseMultipartForm(32 << 20); err != nil {
- return nil, fmt.Errorf("failed parsing upload: %w", err)
- }
- if r.MultipartForm != nil {
- defer r.MultipartForm.RemoveAll()
- }
- f, _, err := r.FormFile("backup")
- if err != nil {
- f, _, err = r.FormFile("file")
- }
- if err != nil {
- return nil, fmt.Errorf("no 'backup' or 'file' uploaded")
- }
- defer f.Close()
- return io.ReadAll(f)
- }
- return io.ReadAll(r.Body)
- }
|