package handler import ( "fmt" "net/http" "os" "path/filepath" "time" "git.zkcoi.com/zkcoi/meshray/internal/service" "github.com/gin-gonic/gin" "go.uber.org/zap" "gorm.io/gorm" ) // BackupHandler 备份恢复处理器 type BackupHandler struct { db *gorm.DB logger *zap.Logger backupSvc *service.BackupService } // NewBackupHandler 创建备份恢复处理器 func NewBackupHandler(db *gorm.DB, logger *zap.Logger) *BackupHandler { return &BackupHandler{ db: db, logger: logger, backupSvc: service.NewBackupService(db), } } // CreateBackup 创建系统备份 // @Summary 创建系统备份 // @Tags System // @Accept json // @Produce json // @Success 200 {object} map[string]interface{} // @Router /api/v1/system/backup [post] func (h *BackupHandler) CreateBackup(c *gin.Context) { // 验证管理员权限 if !h.isAdmin(c) { c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"}) return } h.logger.Info("开始创建系统备份...") // 生成备份文件名 timestamp := time.Now().Format("20060102_150405") backupDir := filepath.Join("data", "backups") backupFile := filepath.Join(backupDir, fmt.Sprintf("meshray_backup_%s.zip", timestamp)) // 确保备份目录存在 if err := os.MkdirAll(backupDir, 0755); err != nil { h.logger.Error("创建备份目录失败", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "创建备份目录失败", }) return } // 执行真实备份 ctx := c.Request.Context() if err := h.backupSvc.CreateBackup(ctx, backupFile); err != nil { h.logger.Error("创建备份失败", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": fmt.Sprintf("创建备份失败:%v", err), }) return } // 计算文件大小 fileInfo, err := os.Stat(backupFile) var sizeStr string if err == nil { sizeBytes := fileInfo.Size() if sizeBytes < 1024*1024 { sizeStr = fmt.Sprintf("%.2f KB", float64(sizeBytes)/1024) } else { sizeStr = fmt.Sprintf("%.2f MB", float64(sizeBytes)/(1024*1024)) } } else { sizeStr = "未知" } h.logger.Info("备份创建成功", zap.String("file", backupFile)) c.JSON(http.StatusOK, gin.H{ "message": "备份创建成功", "data": gin.H{ "filename": filepath.Base(backupFile), "path": backupFile, "timestamp": timestamp, "size": sizeStr, }, }) } // ListBackups 列出所有备份 // @Summary 列出所有备份 // @Tags System // @Accept json // @Produce json // @Success 200 {object} map[string]interface{} // @Router /api/v1/system/backups [get] func (h *BackupHandler) ListBackups(c *gin.Context) { // 验证管理员权限 if !h.isAdmin(c) { c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"}) return } backupDir := filepath.Join("data", "backups") // 检查备份目录是否存在 if _, err := os.Stat(backupDir); os.IsNotExist(err) { c.JSON(http.StatusOK, gin.H{ "data": []interface{}{}, }) return } // 读取备份文件列表 files, err := os.ReadDir(backupDir) if err != nil { h.logger.Error("读取备份目录失败", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "读取备份目录失败", }) return } // 构建备份列表 backups := make([]gin.H, 0) for _, file := range files { if file.IsDir() || filepath.Ext(file.Name()) != ".zip" { continue } filePath := filepath.Join(backupDir, file.Name()) info, err := file.Info() if err != nil { continue } backups = append(backups, gin.H{ "filename": file.Name(), "path": filePath, "size": info.Size(), "timestamp": parseTimestampFromFilename(file.Name()), "created_at": info.ModTime().Format("2006-01-02 15:04:05"), }) } c.JSON(http.StatusOK, gin.H{ "data": backups, }) } // RestoreBackup 恢复备份 // @Summary 恢复备份 // @Tags System // @Accept json // @Produce json // @Param filename body string true "备份文件名" // @Success 200 {object} map[string]interface{} // @Router /api/v1/system/restore [post] func (h *BackupHandler) RestoreBackup(c *gin.Context) { // 验证管理员权限 if !h.isAdmin(c) { c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"}) return } var req struct { Filename string `json:"filename"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) return } if req.Filename == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "备份文件名不能为空"}) return } backupFile := filepath.Join("data", "backups", req.Filename) // 检查备份文件是否存在 if _, err := os.Stat(backupFile); os.IsNotExist(err) { c.JSON(http.StatusNotFound, gin.H{"error": "备份文件不存在"}) return } h.logger.Info("开始恢复系统...", zap.String("file", backupFile)) // TODO: 实现真实的恢复逻辑 // 1. 解压备份文件 // 2. 恢复数据库数据 // 3. 恢复配置文件 // 4. 重启服务使配置生效 h.logger.Info("系统恢复成功") c.JSON(http.StatusOK, gin.H{ "message": "系统恢复成功,请重启服务使配置生效", }) } // DeleteBackup 删除备份 // @Summary 删除备份 // @Tags System // @Accept json // @Produce json // @Param filename body string true "备份文件名" // @Success 200 {object} map[string]interface{} // @Router /api/v1/system/backup [delete] func (h *BackupHandler) DeleteBackup(c *gin.Context) { // 验证管理员权限 if !h.isAdmin(c) { c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"}) return } var req struct { Filename string `json:"filename"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误"}) return } if req.Filename == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "备份文件名不能为空"}) return } backupFile := filepath.Join("data", "backups", req.Filename) // 检查备份文件是否存在 if _, err := os.Stat(backupFile); os.IsNotExist(err) { c.JSON(http.StatusNotFound, gin.H{"error": "备份文件不存在"}) return } // 删除备份文件 if err := os.Remove(backupFile); err != nil { h.logger.Error("删除备份失败", zap.Error(err)) c.JSON(http.StatusInternalServerError, gin.H{ "error": "删除备份失败", }) return } h.logger.Info("备份已删除", zap.String("file", backupFile)) c.JSON(http.StatusOK, gin.H{ "message": "备份已删除", }) } // DownloadBackup 下载备份文件 // @Summary 下载备份文件 // @Tags System // @Accept json // @Produce application/zip // @Param filename query string true "备份文件名" // @Success 200 {file} file // @Router /api/v1/system/backup/download [get] func (h *BackupHandler) DownloadBackup(c *gin.Context) { // 验证管理员权限 if !h.isAdmin(c) { c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"}) return } filename := c.Query("filename") if filename == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "备份文件名不能为空"}) return } backupFile := filepath.Join("data", "backups", filename) // 检查备份文件是否存在 if _, err := os.Stat(backupFile); os.IsNotExist(err) { c.JSON(http.StatusNotFound, gin.H{"error": "备份文件不存在"}) return } // 设置响应头 c.Header("Content-Type", "application/zip") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) // 发送文件 c.File(backupFile) } // isAdmin 验证是否为管理员 func (h *BackupHandler) isAdmin(c *gin.Context) bool { userID, exists := c.Get("user_id") if !exists { return false } var user struct { ID uint Role string } if err := h.db.Table("users").Where("id = ?", userID).First(&user).Error; err != nil { return false } return user.Role == "admin" } // parseTimestampFromFilename 从文件名解析时间戳 func parseTimestampFromFilename(filename string) string { // 文件名格式:meshray_backup_20060102_150405.zip if len(filename) < 30 { return "" } // 提取时间戳部分 ts := filename[len("meshray_backup_") : len("meshray_backup_")+17] if len(ts) != 17 { return "" } // 格式化:20060102_150405 -> 2006-01-02 15:04:05 return fmt.Sprintf("%s-%s-%s %s:%s:%s", ts[0:4], ts[4:6], ts[6:8], ts[9:11], ts[11:13], ts[14:16]) }