mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-03 15:02:13 +08:00
1. S3 凭证加密存储:使用 SecretEncryptor (AES-256-GCM) 加密 SecretAccessKey, 防止备份文件中泄露 S3 凭证,兼容旧的未加密数据 2. 修复 saveRecord 竞态条件:添加 recordsMu 互斥锁保护 records 的 load/save 3. 恢复操作增加服务端验证:handler 层要求重新输入管理员密码,通过 bcrypt 校验,前端弹出密码输入框 4. pg_dump/psql/S3 操作抽象为接口:定义 DBDumper 和 BackupObjectStore 接口, 实现放入 repository 层,遵循项目依赖注入架构规范 5. 改为流式处理避免大数据库 OOM:备份时 pg_dump stdout -> gzip -> io.Pipe -> S3 upload;恢复时 S3 download -> gzip reader -> psql stdin,不再全量加载 6. loadRecords 区分"无数据"和"数据损坏"场景:JSON 解析失败返回明确错误 7. 添加 18 个核心逻辑单元测试:覆盖加密、并发、流式备份/恢复、错误处理等 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
205 lines
5.0 KiB
Go
205 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.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})
|
||
}
|
||
|
||
// ─── 恢复操作(需要重新输入管理员密码) ───
|
||
|
||
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
|
||
}
|
||
|
||
if err := h.backupService.RestoreBackup(c.Request.Context(), backupID); err != nil {
|
||
response.ErrorFrom(c, err)
|
||
return
|
||
}
|
||
response.Success(c, gin.H{"restored": true})
|
||
}
|