448 lines
11 KiB
Go
448 lines
11 KiB
Go
package handlers
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"software-download-center/database"
|
||
"software-download-center/models"
|
||
"software-download-center/utils"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// LogEntry 日志条目
|
||
type LogEntry struct {
|
||
Time string `json:"time"`
|
||
Level string `json:"level"`
|
||
Message string `json:"message"`
|
||
}
|
||
|
||
var logBuffer []LogEntry
|
||
var maxLogEntries = 1000
|
||
|
||
// AddLog 添加日志到缓冲区
|
||
func AddLog(level, message string) {
|
||
entry := LogEntry{
|
||
Time: time.Now().Format("2006-01-02 15:04:05"),
|
||
Level: level,
|
||
Message: message,
|
||
}
|
||
|
||
logBuffer = append(logBuffer, entry)
|
||
|
||
// 同时输出到控制台(使用 utils.Logger)
|
||
logger := utils.NewLogger()
|
||
switch level {
|
||
case "ERROR":
|
||
logger.Error(message)
|
||
case "WARN":
|
||
logger.Warn(message)
|
||
case "INFO":
|
||
logger.Info(message)
|
||
default:
|
||
logger.System(message)
|
||
}
|
||
|
||
// 保持缓冲区大小
|
||
if len(logBuffer) > maxLogEntries {
|
||
logBuffer = logBuffer[len(logBuffer)-maxLogEntries:]
|
||
}
|
||
}
|
||
|
||
// GetLogBuffer 获取日志缓冲区(用于外部访问)
|
||
func GetLogBuffer() []LogEntry {
|
||
return logBuffer
|
||
}
|
||
|
||
// GetLogs 获取日志
|
||
func GetLogs(c *gin.Context) {
|
||
limit := 100
|
||
if limitStr := c.Query("limit"); limitStr != "" {
|
||
fmt.Sscanf(limitStr, "%d", &limit)
|
||
}
|
||
|
||
start := len(logBuffer) - limit
|
||
if start < 0 {
|
||
start = 0
|
||
}
|
||
|
||
logs := logBuffer[start:]
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"logs": logs,
|
||
"total": len(logBuffer),
|
||
})
|
||
}
|
||
|
||
// GetRoutes 获取所有路由
|
||
func GetRoutes(c *gin.Context) {
|
||
var routes []models.Route
|
||
if err := database.DB.Order("`order` ASC, id ASC").Find(&routes).Error; err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "获取路由失败",
|
||
})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"routes": routes,
|
||
})
|
||
}
|
||
|
||
// CreateRoute 创建路由
|
||
func CreateRoute(c *gin.Context) {
|
||
var route models.Route
|
||
if err := c.ShouldBindJSON(&route); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"error": "请求参数错误: " + err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
// 验证路径是否已存在
|
||
var existingRoute models.Route
|
||
if err := database.DB.Where("path = ? AND method = ?", route.Path, route.Method).First(&existingRoute).Error; err == nil {
|
||
AddLog("WARN", fmt.Sprintf("创建路由失败(已存在): %s %s (IP: %s)", route.Method, route.Path, c.ClientIP()))
|
||
c.JSON(http.StatusConflict, gin.H{
|
||
"error": "路由已存在",
|
||
})
|
||
return
|
||
}
|
||
|
||
if err := database.DB.Create(&route).Error; err != nil {
|
||
AddLog("ERROR", fmt.Sprintf("创建路由数据库错误: %s %s - %s (IP: %s)", route.Method, route.Path, err.Error(), c.ClientIP()))
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "创建路由失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
AddLog("INFO", fmt.Sprintf("创建路由成功: %s %s (类型: %s, IP: %s)", route.Method, route.Path, route.Type, c.ClientIP()))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "路由创建成功",
|
||
"route": route,
|
||
})
|
||
}
|
||
|
||
// UpdateRoute 更新路由
|
||
func UpdateRoute(c *gin.Context) {
|
||
id := c.Param("id")
|
||
var route models.Route
|
||
oldPath := route.Path
|
||
oldMethod := route.Method
|
||
|
||
if err := database.DB.First(&route, id).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
AddLog("WARN", fmt.Sprintf("更新路由失败(不存在): ID=%s (IP: %s)", id, c.ClientIP()))
|
||
c.JSON(http.StatusNotFound, gin.H{
|
||
"error": "路由不存在",
|
||
})
|
||
return
|
||
}
|
||
AddLog("ERROR", fmt.Sprintf("查询路由失败: ID=%s - %s (IP: %s)", id, err.Error(), c.ClientIP()))
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "查询路由失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
oldPath = route.Path
|
||
oldMethod = route.Method
|
||
|
||
if err := c.ShouldBindJSON(&route); err != nil {
|
||
AddLog("WARN", fmt.Sprintf("更新路由请求参数错误: ID=%s - %s (IP: %s)", id, err.Error(), c.ClientIP()))
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"error": "请求参数错误",
|
||
})
|
||
return
|
||
}
|
||
|
||
if err := database.DB.Save(&route).Error; err != nil {
|
||
AddLog("ERROR", fmt.Sprintf("更新路由数据库错误: %s %s - %s (IP: %s)", route.Method, route.Path, err.Error(), c.ClientIP()))
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "更新路由失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
AddLog("INFO", fmt.Sprintf("更新路由成功: %s %s -> %s %s (IP: %s)", oldMethod, oldPath, route.Method, route.Path, c.ClientIP()))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "路由更新成功",
|
||
"route": route,
|
||
})
|
||
}
|
||
|
||
// DeleteRoute 删除路由
|
||
func DeleteRoute(c *gin.Context) {
|
||
id := c.Param("id")
|
||
var route models.Route
|
||
if err := database.DB.First(&route, id).Error; err != nil {
|
||
if err == gorm.ErrRecordNotFound {
|
||
AddLog("WARN", fmt.Sprintf("删除路由失败(不存在): ID=%s (IP: %s)", id, c.ClientIP()))
|
||
c.JSON(http.StatusNotFound, gin.H{
|
||
"error": "路由不存在",
|
||
})
|
||
return
|
||
}
|
||
AddLog("ERROR", fmt.Sprintf("查询路由失败: ID=%s - %s (IP: %s)", id, err.Error(), c.ClientIP()))
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "查询路由失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
routeInfo := fmt.Sprintf("%s %s", route.Method, route.Path)
|
||
|
||
if err := database.DB.Delete(&route).Error; err != nil {
|
||
AddLog("ERROR", fmt.Sprintf("删除路由数据库错误: %s - %s (IP: %s)", routeInfo, err.Error(), c.ClientIP()))
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "删除路由失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
AddLog("INFO", fmt.Sprintf("删除路由成功: %s (IP: %s)", routeInfo, c.ClientIP()))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "路由删除成功",
|
||
})
|
||
}
|
||
|
||
// GetFiles 获取文件列表
|
||
func GetFiles(c *gin.Context) {
|
||
dir := c.Query("dir")
|
||
if dir == "" {
|
||
dir = "public/downloads"
|
||
}
|
||
|
||
// 安全检查:防止目录遍历
|
||
if strings.Contains(dir, "..") {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"error": "无效的目录路径",
|
||
})
|
||
return
|
||
}
|
||
|
||
files, err := os.ReadDir(dir)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "读取目录失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
var fileList []gin.H
|
||
for _, file := range files {
|
||
info, _ := file.Info()
|
||
fileList = append(fileList, gin.H{
|
||
"name": file.Name(),
|
||
"size": info.Size(),
|
||
"mod_time": info.ModTime().Format("2006-01-02 15:04:05"),
|
||
"is_dir": file.IsDir(),
|
||
})
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"files": fileList,
|
||
"path": dir,
|
||
})
|
||
}
|
||
|
||
// SaveFile 保存文件
|
||
func SaveFile(c *gin.Context) {
|
||
var req struct {
|
||
Path string `json:"path" binding:"required"`
|
||
Content string `json:"content" binding:"required"`
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"error": "请求参数错误",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 安全检查
|
||
if strings.Contains(req.Path, "..") {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"error": "无效的文件路径",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 确保目录存在
|
||
dir := filepath.Dir(req.Path)
|
||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "创建目录失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 保存文件
|
||
if err := os.WriteFile(req.Path, []byte(req.Content), 0644); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "保存文件失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
AddLog("INFO", fmt.Sprintf("保存文件: %s", req.Path))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "文件保存成功",
|
||
})
|
||
}
|
||
|
||
// ReadFile 读取文件
|
||
func ReadFile(c *gin.Context) {
|
||
path := c.Query("path")
|
||
if path == "" {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"error": "缺少文件路径参数",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 安全检查
|
||
if strings.Contains(path, "..") {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"error": "无效的文件路径",
|
||
})
|
||
return
|
||
}
|
||
|
||
content, err := os.ReadFile(path)
|
||
if err != nil {
|
||
AddLog("ERROR", fmt.Sprintf("读取文件失败: %s - %s (IP: %s)", path, err.Error(), c.ClientIP()))
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "读取文件失败",
|
||
})
|
||
return
|
||
}
|
||
|
||
AddLog("INFO", fmt.Sprintf("读取文件: %s (大小: %d 字节, IP: %s)", path, len(content), c.ClientIP()))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"content": string(content),
|
||
"path": path,
|
||
"size": len(content),
|
||
})
|
||
}
|
||
|
||
// ReloadRoutes 热重载路由和配置
|
||
func ReloadRoutes(c *gin.Context) {
|
||
var req struct {
|
||
Type string `json:"type"` // "config" 或 "all"
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
req.Type = "all"
|
||
}
|
||
|
||
if req.Type == "config" || req.Type == "all" {
|
||
// 重新加载所有配置文件
|
||
files := []string{"tool-status.json", "update-info.json", "media-types.json"}
|
||
successCount := 0
|
||
for _, file := range files {
|
||
if _, err := os.Stat(filepath.Join("public", file)); err == nil {
|
||
if err := utils.ReloadConfig(file); err == nil {
|
||
successCount++
|
||
}
|
||
}
|
||
}
|
||
AddLog("INFO", fmt.Sprintf("重新加载配置文件: %d/%d 成功 (IP: %s)", successCount, len(files), c.ClientIP()))
|
||
}
|
||
|
||
// 注意:Gin 不支持动态路由重载,路由更改需要重启服务器
|
||
if req.Type == "routes" || req.Type == "all" {
|
||
AddLog("INFO", fmt.Sprintf("路由热重载请求(需要重启服务器才能生效)(IP: %s)", c.ClientIP()))
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "配置已重新加载",
|
||
"note": "路由更改需要重启服务器才能生效,建议使用进程管理器(如 systemd、supervisor)",
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": "配置已重新加载",
|
||
})
|
||
}
|
||
|
||
// GetSystemInfo 获取系统信息
|
||
func GetSystemInfo(c *gin.Context) {
|
||
var userCount int64
|
||
var routeCount int64
|
||
database.DB.Model(&models.User{}).Count(&userCount)
|
||
database.DB.Model(&models.Route{}).Count(&routeCount)
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"users": userCount,
|
||
"routes": routeCount,
|
||
"logs": len(logBuffer),
|
||
"version": "1.0.0",
|
||
"server_time": time.Now().Format("2006-01-02 15:04:05"),
|
||
})
|
||
}
|
||
|
||
// UpdateJSONConfig 更新 JSON 配置文件
|
||
func UpdateJSONConfig(c *gin.Context) {
|
||
var req struct {
|
||
File string `json:"file" binding:"required"`
|
||
Content map[string]interface{} `json:"content" binding:"required"`
|
||
Reload bool `json:"reload"` // 是否立即重新加载
|
||
}
|
||
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"error": "请求参数错误",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 验证文件路径
|
||
allowedFiles := []string{"tool-status.json", "update-info.json", "media-types.json"}
|
||
allowed := false
|
||
for _, f := range allowedFiles {
|
||
if req.File == f {
|
||
allowed = true
|
||
break
|
||
}
|
||
}
|
||
|
||
if !allowed {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"error": "不允许修改此文件",
|
||
})
|
||
return
|
||
}
|
||
|
||
// 使用配置工具保存并更新缓存
|
||
if err := utils.SaveConfig(req.File, req.Content); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{
|
||
"error": "保存文件失败: " + err.Error(),
|
||
})
|
||
return
|
||
}
|
||
|
||
message := "配置文件更新成功"
|
||
if req.Reload {
|
||
// 重新加载配置到缓存
|
||
if err := utils.ReloadConfig(req.File); err != nil {
|
||
AddLog("WARN", fmt.Sprintf("配置文件已保存但重新加载失败: %s - %s", req.File, err.Error()))
|
||
message = "配置文件已保存,但重新加载时出现警告"
|
||
} else {
|
||
AddLog("INFO", fmt.Sprintf("配置文件已保存并立即加载: %s", req.File))
|
||
message = "配置文件已保存并立即生效"
|
||
}
|
||
} else {
|
||
AddLog("INFO", fmt.Sprintf("更新配置文件: %s", req.File))
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"message": message,
|
||
"file": req.File,
|
||
})
|
||
}
|