backup_handlers.go 5.7 KB

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