Initial commit
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user