mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 15:02:13 +08:00
新增管理员专属的数据库备份与恢复功能: - 全量 PostgreSQL 备份(pg_dump),gzip 压缩后上传到 S3 兼容存储 - 支持手动备份和 cron 定时备份 - 支持从备份恢复(psql --single-transaction) - 备份文件自动过期清理(默认 14 天) - 前端完整管理页面(S3 配置、定时配置、备份列表、恢复/下载/删除) - 内置 Cloudflare R2 配置教程弹窗 - Dockerfile 从 postgres 镜像多阶段复制 pg_dump/psql,确保版本一致 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
171 lines
4.2 KiB
Go
171 lines
4.2 KiB
Go
package admin
|
||
|
||
import (
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
type BackupHandler struct {
|
||
backupService *service.BackupService
|
||
}
|
||
|
||
func NewBackupHandler(backupService *service.BackupService) *BackupHandler {
|
||
return &BackupHandler{backupService: backupService}
|
||
}
|
||
|
||
// ─── 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.CreateBackup(c.Request.Context(), "manual", expireDays)
|
||
if err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
response.Success(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})
|
||
}
|
||
|
||
// ─── 恢复操作 ───
|
||
|
||
func (h *BackupHandler) RestoreBackup(c *gin.Context) {
|
||
backupID := c.Param("id")
|
||
if backupID == "" {
|
||
response.BadRequest(c, "backup ID is required")
|
||
return
|
||
}
|
||
if err := h.backupService.RestoreBackup(c.Request.Context(), backupID); err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
response.Success(c, gin.H{"restored": true})
|
||
}
|