backup_handlers.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. package api
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "net/http"
  7. "os"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "debtik/internal/auth"
  12. "debtik/internal/backup"
  13. )
  14. func (s *Server) requireAdminRole(w http.ResponseWriter, r *http.Request) bool {
  15. sess := getSessionFromContext(r.Context())
  16. if sess != nil {
  17. if sess.Role != auth.RoleFull {
  18. writeError(w, http.StatusForbidden, "Administrator privileges required")
  19. return false
  20. }
  21. return true
  22. }
  23. if s.authMgr != nil {
  24. writeError(w, http.StatusUnauthorized, "Authentication required")
  25. return false
  26. }
  27. return true
  28. }
  29. func (s *Server) handleBackupExport(w http.ResponseWriter, r *http.Request) {
  30. if !s.requireAdminRole(w, r) {
  31. return
  32. }
  33. if s.backupMgr == nil {
  34. s.backupMgr = backup.NewManager("", "", "", nil)
  35. }
  36. comment := r.URL.Query().Get("comment")
  37. zipBytes, manifest, err := s.backupMgr.CreateBackup(comment)
  38. if err != nil {
  39. writeError(w, http.StatusInternalServerError, "Failed to create backup: "+err.Error())
  40. return
  41. }
  42. filename := fmt.Sprintf("debtik-backup-%s-%s.zip", manifest.Hostname, manifest.CreatedAt.Format("20060102-150405"))
  43. w.Header().Set("Content-Type", "application/zip")
  44. w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
  45. w.Header().Set("Content-Length", strconv.Itoa(len(zipBytes)))
  46. w.WriteHeader(http.StatusOK)
  47. _, _ = w.Write(zipBytes)
  48. }
  49. func (s *Server) handleBackupInspect(w http.ResponseWriter, r *http.Request) {
  50. if !s.requireAdminRole(w, r) {
  51. return
  52. }
  53. if s.backupMgr == nil {
  54. s.backupMgr = backup.NewManager("", "", "", nil)
  55. }
  56. buf, err := readUploadedBackup(r)
  57. if err != nil {
  58. writeError(w, http.StatusBadRequest, err.Error())
  59. return
  60. }
  61. manifest, err := s.backupMgr.InspectBackup(bytes.NewReader(buf), int64(len(buf)))
  62. if err != nil {
  63. writeError(w, http.StatusBadRequest, "Invalid backup archive: "+err.Error())
  64. return
  65. }
  66. writeJSON(w, http.StatusOK, map[string]any{
  67. "success": true,
  68. "manifest": manifest,
  69. })
  70. }
  71. func (s *Server) handleBackupRestore(w http.ResponseWriter, r *http.Request) {
  72. if !s.requireAdminRole(w, r) {
  73. return
  74. }
  75. if s.backupMgr == nil {
  76. s.backupMgr = backup.NewManager("", "", "", nil)
  77. }
  78. buf, err := readUploadedBackup(r)
  79. if err != nil {
  80. writeError(w, http.StatusBadRequest, err.Error())
  81. return
  82. }
  83. res, err := s.backupMgr.RestoreBackup(bytes.NewReader(buf), int64(len(buf)))
  84. if err != nil {
  85. writeError(w, http.StatusBadRequest, "Restore failed: "+err.Error())
  86. return
  87. }
  88. // Trigger asynchronous restart so DebTik reloads cleanly
  89. s.backupMgr.TriggerRestart(1 * time.Second)
  90. writeJSON(w, http.StatusOK, res)
  91. }
  92. func (s *Server) handleBackupList(w http.ResponseWriter, r *http.Request) {
  93. if !s.requireAdminRole(w, r) {
  94. return
  95. }
  96. if s.backupMgr == nil {
  97. s.backupMgr = backup.NewManager("", "", "", nil)
  98. }
  99. list, err := s.backupMgr.ListLocalBackups()
  100. if err != nil {
  101. writeError(w, http.StatusInternalServerError, "Failed to list backups: "+err.Error())
  102. return
  103. }
  104. writeJSON(w, http.StatusOK, list)
  105. }
  106. func (s *Server) handleBackupDownloadLocal(w http.ResponseWriter, r *http.Request) {
  107. if !s.requireAdminRole(w, r) {
  108. return
  109. }
  110. if s.backupMgr == nil {
  111. s.backupMgr = backup.NewManager("", "", "", nil)
  112. }
  113. filename := r.PathValue("filename")
  114. fullPath, err := s.backupMgr.GetLocalBackup(filename)
  115. if err != nil {
  116. writeError(w, http.StatusNotFound, err.Error())
  117. return
  118. }
  119. w.Header().Set("Content-Type", "application/zip")
  120. w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
  121. http.ServeFile(w, r, fullPath)
  122. }
  123. func (s *Server) handleBackupRestoreLocal(w http.ResponseWriter, r *http.Request) {
  124. if !s.requireAdminRole(w, r) {
  125. return
  126. }
  127. if s.backupMgr == nil {
  128. s.backupMgr = backup.NewManager("", "", "", nil)
  129. }
  130. filename := r.PathValue("filename")
  131. fullPath, err := s.backupMgr.GetLocalBackup(filename)
  132. if err != nil {
  133. writeError(w, http.StatusNotFound, err.Error())
  134. return
  135. }
  136. f, err := os.Open(fullPath)
  137. if err != nil {
  138. writeError(w, http.StatusInternalServerError, "Cannot open backup file: "+err.Error())
  139. return
  140. }
  141. defer f.Close()
  142. fi, err := f.Stat()
  143. if err != nil {
  144. writeError(w, http.StatusInternalServerError, "Cannot stat backup file: "+err.Error())
  145. return
  146. }
  147. res, err := s.backupMgr.RestoreBackup(f, fi.Size())
  148. if err != nil {
  149. writeError(w, http.StatusBadRequest, "Restore failed: "+err.Error())
  150. return
  151. }
  152. s.backupMgr.TriggerRestart(1 * time.Second)
  153. writeJSON(w, http.StatusOK, res)
  154. }
  155. func readUploadedBackup(r *http.Request) ([]byte, error) {
  156. contentType := r.Header.Get("Content-Type")
  157. // Multipart form upload
  158. if strings.Contains(contentType, "multipart/form-data") {
  159. if err := r.ParseMultipartForm(32 << 20); err != nil {
  160. return nil, fmt.Errorf("failed parsing upload: %w", err)
  161. }
  162. var file io.Reader
  163. f, _, err := r.FormFile("backup")
  164. if err == nil {
  165. defer f.Close()
  166. file = f
  167. } else {
  168. f, _, err2 := r.FormFile("file")
  169. if err2 == nil {
  170. defer f.Close()
  171. file = f
  172. } else {
  173. return nil, fmt.Errorf("no 'backup' or 'file' uploaded")
  174. }
  175. }
  176. return io.ReadAll(file)
  177. }
  178. // Raw binary zip stream
  179. if r.Body == nil {
  180. return nil, fmt.Errorf("empty request body")
  181. }
  182. defer r.Body.Close()
  183. return io.ReadAll(r.Body)
  184. }