Files
zkcoi e9ca2f7d70 fix: rename manager module to git.zkcoi.com/zkcoi/Meshray-Manager
- go.mod module path matches repo zkcoi/Meshray-Manager (case-distinct from zkcoi/Meshray/core)
- rewrite internal imports meshray/{internal,web,pkg} -> Meshray-Manager/... (core refs kept)
- sync README.md / install.sh repo URLs; add CHANGELOG entry
2026-07-15 16:25:46 +08:00

335 lines
8.2 KiB
Go

package handler
import (
"fmt"
"net/http"
"os"
"path/filepath"
"time"
"git.zkcoi.com/zkcoi/Meshray-Manager/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])
}