mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 23:12:14 +08:00
POST /backups 和 POST /backups/:id/restore 改为异步:立即返回 HTTP 202, 后台 goroutine 独立执行 pg_dump → gzip → S3 上传,前端每 2s 轮询状态。 后端: - 新增 StartBackup/StartRestore 方法,后台 goroutine 不依赖 HTTP 连接 - Graceful shutdown 等待活跃操作完成,启动时清理孤立 running 记录 - BackupRecord 新增 progress/restore_status 字段支持进度和恢复状态追踪 前端: - 创建备份/恢复后轮询 GET /backups/:id 直到完成或失败 - 标签页切换暂停/恢复轮询,组件卸载清理定时器 - 正确处理 409(备份进行中)和轮询超时 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
206 lines
5.0 KiB
Go
206 lines
5.0 KiB
Go
package admin
|
||
|
||
import (
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||
"github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
type BackupHandler struct {
|
||
backupService *service.BackupService
|
||
userService *service.UserService
|
||
}
|
||
|
||
func NewBackupHandler(backupService *service.BackupService, userService *service.UserService) *BackupHandler {
|
||
return &BackupHandler{
|
||
backupService: backupService,
|
||
userService: userService,
|
||
}
|
||
}
|
||
|
||
// ─── S3 配置 ───
|
||
|
||
func (h *BackupHandler) GetS3Config(c *gin.Context) {
|
||
cfg, err := h.backupService.GetS3Config(c.Request.Context())
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
response.Success(c, cfg)
|
||
}
|
||
|
||
func (h *BackupHandler) UpdateS3Config(c *gin.Context) {
|
||
var req service.BackupS3Config
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||
return
|
||
}
|
||
cfg, err := h.backupService.UpdateS3Config(c.Request.Context(), req)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
response.Success(c, cfg)
|
||
}
|
||
|
||
func (h *BackupHandler) TestS3Connection(c *gin.Context) {
|
||
var req service.BackupS3Config
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||
return
|
||
}
|
||
err := h.backupService.TestS3Connection(c.Request.Context(), req)
|
||
if err != nil {
|
||
response.Success(c, gin.H{"ok": false, "message": err.Error()})
|
||
return
|
||
}
|
||
response.Success(c, gin.H{"ok": true, "message": "connection successful"})
|
||
}
|
||
|
||
// ─── 定时备份 ───
|
||
|
||
func (h *BackupHandler) GetSchedule(c *gin.Context) {
|
||
cfg, err := h.backupService.GetSchedule(c.Request.Context())
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
response.Success(c, cfg)
|
||
}
|
||
|
||
func (h *BackupHandler) UpdateSchedule(c *gin.Context) {
|
||
var req service.BackupScheduleConfig
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "Invalid request: "+err.Error())
|
||
return
|
||
}
|
||
cfg, err := h.backupService.UpdateSchedule(c.Request.Context(), req)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
response.Success(c, cfg)
|
||
}
|
||
|
||
// ─── 备份操作 ───
|
||
|
||
type CreateBackupRequest struct {
|
||
ExpireDays *int `json:"expire_days"` // nil=使用默认值14,0=永不过期
|
||
}
|
||
|
||
func (h *BackupHandler) CreateBackup(c *gin.Context) {
|
||
var req CreateBackupRequest
|
||
_ = c.ShouldBindJSON(&req) // 允许空 body
|
||
|
||
expireDays := 14 // 默认14天过期
|
||
if req.ExpireDays != nil {
|
||
expireDays = *req.ExpireDays
|
||
}
|
||
|
||
record, err := h.backupService.StartBackup(c.Request.Context(), "manual", expireDays)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
response.Accepted(c, record)
|
||
}
|
||
|
||
func (h *BackupHandler) ListBackups(c *gin.Context) {
|
||
records, err := h.backupService.ListBackups(c.Request.Context())
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
if records == nil {
|
||
records = []service.BackupRecord{}
|
||
}
|
||
response.Success(c, gin.H{"items": records})
|
||
}
|
||
|
||
func (h *BackupHandler) GetBackup(c *gin.Context) {
|
||
backupID := c.Param("id")
|
||
if backupID == "" {
|
||
response.BadRequest(c, "backup ID is required")
|
||
return
|
||
}
|
||
record, err := h.backupService.GetBackupRecord(c.Request.Context(), backupID)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
response.Success(c, record)
|
||
}
|
||
|
||
func (h *BackupHandler) DeleteBackup(c *gin.Context) {
|
||
backupID := c.Param("id")
|
||
if backupID == "" {
|
||
response.BadRequest(c, "backup ID is required")
|
||
return
|
||
}
|
||
if err := h.backupService.DeleteBackup(c.Request.Context(), backupID); err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
response.Success(c, gin.H{"deleted": true})
|
||
}
|
||
|
||
func (h *BackupHandler) GetDownloadURL(c *gin.Context) {
|
||
backupID := c.Param("id")
|
||
if backupID == "" {
|
||
response.BadRequest(c, "backup ID is required")
|
||
return
|
||
}
|
||
url, err := h.backupService.GetBackupDownloadURL(c.Request.Context(), backupID)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
response.Success(c, gin.H{"url": url})
|
||
}
|
||
|
||
// ─── 恢复操作(需要重新输入管理员密码) ───
|
||
|
||
type RestoreBackupRequest struct {
|
||
Password string `json:"password" binding:"required"`
|
||
}
|
||
|
||
func (h *BackupHandler) RestoreBackup(c *gin.Context) {
|
||
backupID := c.Param("id")
|
||
if backupID == "" {
|
||
response.BadRequest(c, "backup ID is required")
|
||
return
|
||
}
|
||
|
||
var req RestoreBackupRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
response.BadRequest(c, "password is required for restore operation")
|
||
return
|
||
}
|
||
|
||
// 从上下文获取当前管理员用户 ID
|
||
sub, ok := middleware.GetAuthSubjectFromContext(c)
|
||
if !ok {
|
||
response.Unauthorized(c, "unauthorized")
|
||
return
|
||
}
|
||
|
||
// 获取管理员用户并验证密码
|
||
user, err := h.userService.GetByID(c.Request.Context(), sub.UserID)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
if !user.CheckPassword(req.Password) {
|
||
response.BadRequest(c, "incorrect admin password")
|
||
return
|
||
}
|
||
|
||
record, err := h.backupService.StartRestore(c.Request.Context(), backupID)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
response.Accepted(c, record)
|
||
}
|