1、first commit。
This commit is contained in:
commit
5326172a99
28
config/config.yaml
Normal file
28
config/config.yaml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
base:
|
||||||
|
name: "avcnet_dash"
|
||||||
|
port: 8080
|
||||||
|
mode: "dev"
|
||||||
|
version: "v0.0.1"
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: "debug"
|
||||||
|
filename: "dash.log"
|
||||||
|
max_size: 2
|
||||||
|
max_age: 180
|
||||||
|
max_backups: 20
|
||||||
|
|
||||||
|
# 使用令牌桶限流
|
||||||
|
rate:
|
||||||
|
fill_interval: 10 # 填充速率 每(ms)填充一个
|
||||||
|
capacity: 10 #桶容量
|
||||||
|
#quantum: 1 # 指定每次填充的个数
|
||||||
|
#rate: 0.1 # 和capacity搭配使用,指定填充速率 rate = 0.1 capacity = 200 则表示每秒20填充20个令牌
|
||||||
|
max_wait: 2 # 最长等待时间(s)
|
||||||
|
|
||||||
|
snowflake:
|
||||||
|
start_time: "2025-05-20"
|
||||||
|
machine_id: 1
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
salt: "redhat"
|
||||||
|
expire: 86400
|
60
controller/author.go
Normal file
60
controller/author.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"dashboard/models"
|
||||||
|
"dashboard/pkg/jwt"
|
||||||
|
"dashboard/utils"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoginPage(c *gin.Context) {
|
||||||
|
log, _ := utils.GetLogFromContext(c)
|
||||||
|
|
||||||
|
log.Info("Serving login page")
|
||||||
|
|
||||||
|
c.File(models.FileLoginHtml)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserSignInHandler(jwtC *jwt.Jwt) func(c *gin.Context) error {
|
||||||
|
return func(c *gin.Context) error {
|
||||||
|
var user models.UserInfoParams
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&user); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Password != models.AdminPassword {
|
||||||
|
return models.ErrorPasswordErr
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenStr, err := jwtC.GenToken(map[string]interface{}{
|
||||||
|
"username": "admin",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(tokenStr)
|
||||||
|
|
||||||
|
token := fmt.Sprintf("%s%s", models.GinAuthorPrefixKey, tokenStr)
|
||||||
|
|
||||||
|
c.SetCookie(models.GinAuthorKey, token, 24*60*60, "/", "", false, true)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Login successful"})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserLogOutHandler(c *gin.Context) {
|
||||||
|
log, _ := utils.GetLogFromContext(c)
|
||||||
|
|
||||||
|
c.SetCookie(models.GinAuthorKey, "", -1, "/", "", false, true)
|
||||||
|
log.Sugar().Info("Logout successful")
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Logout successful"})
|
||||||
|
}
|
244
controller/files.go
Normal file
244
controller/files.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"dashboard/utils"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
IsDir bool `json:"is_dir"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
ModTime time.Time `json:"mod_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileUploadHandle(c *gin.Context) error {
|
||||||
|
log, _ := utils.GetLogFromContext(c)
|
||||||
|
|
||||||
|
file, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar().Errorf("Failed to get file: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetDir := c.PostForm("target_dir")
|
||||||
|
if targetDir == "" {
|
||||||
|
log.Sugar().Errorf("Target directory not specified")
|
||||||
|
return errors.New("Target directory not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止路径穿越攻击
|
||||||
|
cleanDir := filepath.Clean(targetDir)
|
||||||
|
if strings.Contains(cleanDir, "..") || !strings.HasPrefix(cleanDir, "/") {
|
||||||
|
log.Sugar().Errorf("Invalid target directory: %s", cleanDir)
|
||||||
|
return errors.New("Invalid target directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制文件大小(10MB)
|
||||||
|
const maxFileSize = 10 << 20 // 10MB
|
||||||
|
if file.Size > maxFileSize {
|
||||||
|
log.Sugar().Errorf("File too large: %d bytes", file.Size)
|
||||||
|
return errors.New("File size exceeds 10MB")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保目标目录存在
|
||||||
|
if err := os.MkdirAll(cleanDir, 0755); err != nil {
|
||||||
|
log.Sugar().Errorf("Failed to create directory %s: %v", cleanDir, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
targetPath := filepath.Join(cleanDir, file.Filename)
|
||||||
|
if err := c.SaveUploadedFile(file, targetPath); err != nil {
|
||||||
|
log.Sugar().Errorf("Failed to save file to %s: %v", targetPath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Sugar().Debug("File uploaded successfully to %s", targetPath)
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("File uploaded to %s", targetPath)})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileListHandle(c *gin.Context) error {
|
||||||
|
log, _ := utils.GetLogFromContext(c)
|
||||||
|
|
||||||
|
dirPath := c.Query("path")
|
||||||
|
if dirPath == "" {
|
||||||
|
log.Sugar().Errorf("Directory path not specified")
|
||||||
|
return errors.New("Directory path not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理路径并规范化
|
||||||
|
cleanPath := filepath.Clean(dirPath)
|
||||||
|
if !strings.HasPrefix(cleanPath, "/") {
|
||||||
|
log.Sugar().Errorf("Invalid directory path (not absolute): %s", cleanPath)
|
||||||
|
return errors.New("Invalid directory path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止路径穿越攻击
|
||||||
|
if strings.Contains(cleanPath, "/../") {
|
||||||
|
log.Sugar().Errorf("Invalid directory path (contains ..): %s", cleanPath)
|
||||||
|
return errors.New("Invalid directory path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取目录
|
||||||
|
entries, err := os.ReadDir(cleanPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar().Errorf("Failed to read directory %s: %v", cleanPath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var files []FileInfo
|
||||||
|
for _, entry := range entries {
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar().Errorf("Failed to get info for %s: %v", entry.Name(), err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
files = append(files, FileInfo{
|
||||||
|
Name: entry.Name(),
|
||||||
|
IsDir: entry.IsDir(),
|
||||||
|
Size: info.Size(),
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Sugar().Debug("Listed files in %s: %d entries", cleanPath, len(files))
|
||||||
|
c.JSON(http.StatusOK, gin.H{"files": files})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileDownloadHandle(c *gin.Context) error {
|
||||||
|
log, _ := utils.GetLogFromContext(c)
|
||||||
|
|
||||||
|
filePath := c.Query("path")
|
||||||
|
if filePath == "" {
|
||||||
|
log.Sugar().Errorf("File path not specified")
|
||||||
|
return errors.New("File path not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理路径并规范化
|
||||||
|
cleanPath := filepath.Clean(filePath)
|
||||||
|
if !strings.HasPrefix(cleanPath, "/") {
|
||||||
|
log.Sugar().Errorf("Invalid file path (not absolute): %s", cleanPath)
|
||||||
|
return errors.New("Invalid file path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 防止路径穿越攻击
|
||||||
|
if strings.Contains(cleanPath, "/../") {
|
||||||
|
log.Sugar().Errorf("Invalid file path (contains ..): %s", cleanPath)
|
||||||
|
return errors.New("Invalid file path")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查路径是否存在
|
||||||
|
fileInfo, err := os.Stat(cleanPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar().Errorf("Path not found %s: %v", cleanPath, err)
|
||||||
|
return errors.New("Path not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fileInfo.IsDir() {
|
||||||
|
// 处理文件下载
|
||||||
|
const maxFileSize = 10 << 20 // 10MB
|
||||||
|
if fileInfo.Size() > maxFileSize {
|
||||||
|
log.Sugar().Errorf("File too large: %d bytes", fileInfo.Size())
|
||||||
|
return errors.New("File size exceeds 10MB")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filepath.Base(cleanPath)))
|
||||||
|
c.Header("Content-Type", "application/octet-stream")
|
||||||
|
c.File(cleanPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理目录下载(创建ZIP)
|
||||||
|
tmpFile, err := os.CreateTemp("", "dir-*.zip")
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar().Errorf("Failed to create temp file: %v", err)
|
||||||
|
return errors.New("Failed to create zip file")
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile.Name()) // 清理临时文件
|
||||||
|
|
||||||
|
zipWriter := zip.NewWriter(tmpFile)
|
||||||
|
totalSize := int64(0)
|
||||||
|
const maxZipSize = 10 << 20 // 10MB
|
||||||
|
|
||||||
|
err = filepath.Walk(cleanPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建ZIP中的文件头
|
||||||
|
header, err := zip.FileInfoHeader(info)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置ZIP中的相对路径
|
||||||
|
relPath, err := filepath.Rel(cleanPath, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
header.Name = relPath
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
header.Name += "/"
|
||||||
|
} else {
|
||||||
|
header.Method = zip.Deflate
|
||||||
|
totalSize += info.Size()
|
||||||
|
if totalSize > maxZipSize {
|
||||||
|
return fmt.Errorf("directory size exceeds 10MB")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer, err := zipWriter.CreateHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
_, err = io.Copy(writer, file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
zipWriter.Close()
|
||||||
|
log.Sugar().Errorf("Failed to create zip for %s: %v", cleanPath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := zipWriter.Close(); err != nil {
|
||||||
|
log.Sugar().Errorf("Failed to close zip writer: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置下载头
|
||||||
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filepath.Base(cleanPath)+".zip"))
|
||||||
|
c.Header("Content-Type", "application/zip")
|
||||||
|
c.File(tmpFile.Name())
|
||||||
|
log.Sugar().Debug("Directory %s downloaded as zip", cleanPath)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
105
controller/sysinfo.go
Normal file
105
controller/sysinfo.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
|
"github.com/shirou/gopsutil/v4/host"
|
||||||
|
"github.com/shirou/gopsutil/v4/load"
|
||||||
|
"github.com/shirou/gopsutil/v4/mem"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SystemInfo struct {
|
||||||
|
CPUUsage float64 `json:"cpu_usage"`
|
||||||
|
MemoryUsage float64 `json:"memory_usage"`
|
||||||
|
DiskUsage float64 `json:"disk_usage"`
|
||||||
|
LoadAvg float64 `json:"load_avg"`
|
||||||
|
CPUCores int `json:"cpu_cores"`
|
||||||
|
CPUMHz float64 `json:"cpu_mhz"`
|
||||||
|
MemoryTotal uint64 `json:"memory_total"`
|
||||||
|
MemoryFree uint64 `json:"memory_free"`
|
||||||
|
DiskPartitions []DiskPartition `json:"disk_partitions"`
|
||||||
|
Uptime uint64 `json:"uptime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiskPartition struct {
|
||||||
|
Mountpoint string `json:"mountpoint"`
|
||||||
|
Total uint64 `json:"total"`
|
||||||
|
Used uint64 `json:"used"`
|
||||||
|
UsedPercent float64 `json:"used_percent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSystemInfo() (SystemInfo, error) {
|
||||||
|
var info SystemInfo
|
||||||
|
|
||||||
|
// CPU 使用率
|
||||||
|
cpuPercent, err := cpu.Percent(time.Second, false)
|
||||||
|
if err == nil && len(cpuPercent) > 0 {
|
||||||
|
info.CPUUsage = cpuPercent[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPU 详细信息
|
||||||
|
cpuInfo, err := cpu.Info()
|
||||||
|
if err == nil && len(cpuInfo) > 0 {
|
||||||
|
info.CPUCores = int(cpuInfo[0].Cores)
|
||||||
|
info.CPUMHz = cpuInfo[0].Mhz
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内存信息
|
||||||
|
vm, err := mem.VirtualMemory()
|
||||||
|
if err == nil {
|
||||||
|
info.MemoryUsage = vm.UsedPercent
|
||||||
|
info.MemoryTotal = vm.Total
|
||||||
|
info.MemoryFree = vm.Free
|
||||||
|
}
|
||||||
|
|
||||||
|
// 磁盘使用率(根目录)
|
||||||
|
diskUsage, err := disk.Usage("/")
|
||||||
|
if err == nil {
|
||||||
|
info.DiskUsage = diskUsage.UsedPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
// 磁盘分区信息
|
||||||
|
partitions, err := disk.Partitions(false)
|
||||||
|
if err == nil {
|
||||||
|
for _, p := range partitions {
|
||||||
|
usage, err := disk.Usage(p.Mountpoint)
|
||||||
|
if err == nil {
|
||||||
|
info.DiskPartitions = append(info.DiskPartitions, DiskPartition{
|
||||||
|
Mountpoint: p.Mountpoint,
|
||||||
|
Total: usage.Total,
|
||||||
|
Used: usage.Used,
|
||||||
|
UsedPercent: usage.UsedPercent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统负载
|
||||||
|
loadAvg, err := load.Avg()
|
||||||
|
if err == nil {
|
||||||
|
info.LoadAvg = loadAvg.Load1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 系统运行时间
|
||||||
|
uptime, err := host.Uptime()
|
||||||
|
if err == nil {
|
||||||
|
info.Uptime = uptime
|
||||||
|
}
|
||||||
|
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SystemInfoHandle(c *gin.Context) error {
|
||||||
|
info, err := getSystemInfo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, info)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
96
controller/websocket.go
Normal file
96
controller/websocket.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"dashboard/utils"
|
||||||
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/creack/pty"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TerminalHandle() func(c *gin.Context) error {
|
||||||
|
upgrader := websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true // 允许跨域
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return func(c *gin.Context) error {
|
||||||
|
log, _ := utils.GetLogFromContext(c)
|
||||||
|
|
||||||
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar().Errorf("WebSocket upgrade failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
cmd := exec.Command("bash")
|
||||||
|
ptyFile, err := pty.Start(cmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar().Errorf("Failed to start pty: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer ptyFile.Close()
|
||||||
|
|
||||||
|
wg, ctx := errgroup.WithContext(context.Background())
|
||||||
|
|
||||||
|
wg.Go(func() error {
|
||||||
|
defer log.Sugar().Errorf("task read over")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Sugar().Errorf("task canceld read")
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
_, msg, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar().Errorf("WebSocket read error: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = ptyFile.Write(msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar().Errorf("PTY write error: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
wg.Go(func() error {
|
||||||
|
defer log.Sugar().Errorf("task write over")
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
log.Sugar().Errorf("task canceld write")
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := ptyFile.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar().Errorf("PTY read error: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = conn.WriteMessage(websocket.BinaryMessage, buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar().Errorf("WebSocket write error: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wg.Wait(); err != nil {
|
||||||
|
log.Sugar().Errorf("WebSocket error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
67
go.mod
Normal file
67
go.mod
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
module dashboard
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.23.9
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bwmarrin/snowflake v0.3.0
|
||||||
|
github.com/creack/pty v1.1.24
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
|
github.com/gin-contrib/cors v1.7.5
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
github.com/juju/ratelimit v1.0.2
|
||||||
|
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.4
|
||||||
|
github.com/spf13/viper v1.20.1
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
golang.org/x/sync v0.12.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||||
|
github.com/bytedance/sonic v1.13.2 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.12.0 // indirect
|
||||||
|
github.com/spf13/cast v1.7.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
golang.org/x/arch v0.15.0 // indirect
|
||||||
|
golang.org/x/crypto v0.36.0 // indirect
|
||||||
|
golang.org/x/net v0.38.0 // indirect
|
||||||
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.6 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
161
go.sum
Normal file
161
go.sum
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
||||||
|
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||||
|
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||||
|
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||||
|
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk=
|
||||||
|
github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0=
|
||||||
|
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||||
|
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||||
|
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||||
|
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
|
||||||
|
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||||
|
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||||
|
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||||
|
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||||
|
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||||
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||||
|
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
|
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||||
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||||
|
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
|
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
|
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
96
logger/logger.go
Normal file
96
logger/logger.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/natefinch/lumberjack"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
opts options
|
||||||
|
*zap.Logger
|
||||||
|
level zap.AtomicLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts ...Option) (*Logger, error) {
|
||||||
|
logger := new(Logger)
|
||||||
|
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&logger.opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := logger.logInit()
|
||||||
|
|
||||||
|
return logger, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) SetLogLevel(levelStr string) error {
|
||||||
|
level := new(zapcore.Level)
|
||||||
|
if err := level.UnmarshalText([]byte(levelStr)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
l.level.SetLevel(*level)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) logWriter() zapcore.WriteSyncer {
|
||||||
|
var res io.Writer
|
||||||
|
res = os.Stdout
|
||||||
|
|
||||||
|
if l.opts.mode == "release" {
|
||||||
|
lj := &lumberjack.Logger{
|
||||||
|
Filename: l.opts.fileName,
|
||||||
|
MaxSize: l.opts.maxSize,
|
||||||
|
MaxBackups: l.opts.maxBackUp,
|
||||||
|
MaxAge: l.opts.maxAge,
|
||||||
|
}
|
||||||
|
|
||||||
|
res = io.MultiWriter(lj, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return zapcore.AddSync(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) logEncoder() zapcore.Encoder {
|
||||||
|
ec := zap.NewProductionEncoderConfig()
|
||||||
|
ec.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||||
|
ec.TimeKey = "time"
|
||||||
|
ec.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||||
|
ec.EncodeDuration = zapcore.SecondsDurationEncoder
|
||||||
|
ec.EncodeCaller = zapcore.ShortCallerEncoder
|
||||||
|
|
||||||
|
return zapcore.NewConsoleEncoder(ec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) logLevel() error {
|
||||||
|
level, err := zap.ParseAtomicLevel(l.opts.level)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
l.level = level
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) logInit() error {
|
||||||
|
if err := l.logLevel(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
writeSyncer := l.logWriter()
|
||||||
|
encoder := l.logEncoder()
|
||||||
|
|
||||||
|
core := zapcore.NewCore(encoder, writeSyncer, l.level)
|
||||||
|
|
||||||
|
l.Logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
|
||||||
|
|
||||||
|
zap.ReplaceGlobals(l.Logger)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
48
logger/option.go
Normal file
48
logger/option.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
maxSize int
|
||||||
|
maxAge int
|
||||||
|
maxBackUp int
|
||||||
|
fileName string
|
||||||
|
level string
|
||||||
|
mode string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(o *options)
|
||||||
|
|
||||||
|
func WithMaxSize(maxSize int) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.maxSize = maxSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMaxAge(maxAge int) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.maxAge = maxAge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMaxBackUp(maxBackUp int) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.maxBackUp = maxBackUp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithFileName(fileName string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.fileName = fileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLevel(levle string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.level = levle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMode(mode string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.mode = mode
|
||||||
|
}
|
||||||
|
}
|
82
main.go
Normal file
82
main.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"dashboard/logger"
|
||||||
|
"dashboard/routes"
|
||||||
|
"dashboard/settings"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var config = flag.String("f", "./config/config.yaml", "config file path")
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
sets := settings.New(settings.WithName(*config))
|
||||||
|
|
||||||
|
log, err := logger.New(
|
||||||
|
logger.WithFileName(sets.LogConfig.Filename),
|
||||||
|
logger.WithLevel(sets.LogConfig.Level),
|
||||||
|
logger.WithMaxAge(sets.LogConfig.MaxAge),
|
||||||
|
logger.WithMaxSize(sets.LogConfig.MaxSize),
|
||||||
|
logger.WithMaxBackUp(sets.LogConfig.MaxBackUps),
|
||||||
|
logger.WithMode(sets.BaseConfig.Mode),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = log.Sync()
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Info("Settings and log init ok")
|
||||||
|
|
||||||
|
r := routes.Setup(log, *sets.RateLimitConfig, *sets.JwtConfig)
|
||||||
|
|
||||||
|
listenAndServe(fmt.Sprintf(":%d", sets.BaseConfig.Port), r, log)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listenAndServe(addr string, handle http.Handler, log *logger.Logger) {
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: handle,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Sugar().Infof("Server listen on %s", addr)
|
||||||
|
go func() {
|
||||||
|
// 开启一个goroutine启动服务
|
||||||
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Error("listen: %s\n", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时
|
||||||
|
quit := make(chan os.Signal, 1) // 创建一个接收信号的通道
|
||||||
|
// kill 默认会发送 syscall.SIGTERM 信号
|
||||||
|
// kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号
|
||||||
|
// kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
|
||||||
|
// signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞
|
||||||
|
<-quit // 阻塞在此,当接收到上述两种信号时才会往下执行
|
||||||
|
log.Info("Shutdown Server ...")
|
||||||
|
// 创建一个5秒超时的context
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
// 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
log.Error("Server Shutdown: ", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Server exiting")
|
||||||
|
}
|
34
models/errorcode.go
Normal file
34
models/errorcode.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type resCode int
|
||||||
|
|
||||||
|
const codeBase = 1000
|
||||||
|
|
||||||
|
const (
|
||||||
|
CodeSuccess resCode = iota + codeBase
|
||||||
|
)
|
||||||
|
|
||||||
|
var codeMsg = map[resCode]string{
|
||||||
|
CodeSuccess:"success",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r resCode) String() string {
|
||||||
|
if res,ok:=codeMsg[r];ok{
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return codeMsg[CodeSuccess]
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseError struct {
|
||||||
|
Code resCode
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseError) Error() string {
|
||||||
|
if b.Msg != "" {
|
||||||
|
return b.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.Code.String()
|
||||||
|
}
|
5
models/params.go
Normal file
5
models/params.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type UserInfoParams struct {
|
||||||
|
Password string `json:"password" binding:"required"`
|
||||||
|
}
|
32
models/type.go
Normal file
32
models/type.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GinContxtLog = "log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
GinAuthorKey = "Authorization"
|
||||||
|
GinAuthorPrefixKey = "Bearer "
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrorInvalidData = errors.New("no such value")
|
||||||
|
ErrorPasswordErr = errors.New("user or password invalid")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FileLoginHtml = "./static/login.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AdminPassword = os.Getenv("ADMIN_PASSWORD")
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if AdminPassword == "" {
|
||||||
|
AdminPassword = "admin@123"
|
||||||
|
}
|
||||||
|
}
|
78
pkg/jwt/jwt.go
Normal file
78
pkg/jwt/jwt.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Jwt struct {
|
||||||
|
options
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts ...Option) *Jwt {
|
||||||
|
res := new(Jwt)
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&res.options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jwt) GenToken(mp map[string]interface{}) (string, error) {
|
||||||
|
claims := jwt.MapClaims{
|
||||||
|
"exp": time.Now().Add(j.expire).Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
maps.Copy(claims, mp)
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
|
||||||
|
return token.SignedString(j.salt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *Jwt) ParseToken(tokenStr string) (map[string]interface{}, error) {
|
||||||
|
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
|
return nil, jwt.ErrSignatureInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return j.salt, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorIsJwtExpired(err error) bool {
|
||||||
|
return errors.Is(err, jwt.ErrTokenExpired)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetValueStringFromToken(jwt map[string]interface{}, key string) (string, bool) {
|
||||||
|
if value, ok := jwt[key]; ok {
|
||||||
|
if res, ok := value.(string); ok {
|
||||||
|
return res, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetValueInt64FromToken(jwt map[string]interface{}, key string) (int64, bool) {
|
||||||
|
if value, ok := jwt[key]; ok {
|
||||||
|
if res, ok := value.(int64); ok {
|
||||||
|
return res, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
22
pkg/jwt/option.go
Normal file
22
pkg/jwt/option.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
expire time.Duration
|
||||||
|
salt []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
func WithSalt(salt string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.salt = []byte(salt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithExpire(expire time.Duration) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.expire = expire
|
||||||
|
}
|
||||||
|
}
|
36
pkg/rate/option.go
Normal file
36
pkg/rate/option.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package rate
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
fillInterval time.Duration
|
||||||
|
capacity int64
|
||||||
|
quantum int64
|
||||||
|
rate float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(o *options)
|
||||||
|
|
||||||
|
func WithFillInterval(fi time.Duration) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.fillInterval = fi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCapacity(cap int64) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.capacity = cap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithQuantum(qt int64) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.quantum = qt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRate(rate float64) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.rate = rate
|
||||||
|
}
|
||||||
|
}
|
39
pkg/rate/ratelimit.go
Normal file
39
pkg/rate/ratelimit.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package rate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/juju/ratelimit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Limit struct {
|
||||||
|
*ratelimit.Bucket
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts ...Option) (*Limit, error) {
|
||||||
|
opt := new(options)
|
||||||
|
for _, o := range opts {
|
||||||
|
o(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := new(Limit)
|
||||||
|
if opt.fillInterval > 0 && opt.capacity > 0 && opt.quantum > 0 {
|
||||||
|
res.Bucket = ratelimit.NewBucketWithQuantum(opt.fillInterval, opt.capacity, opt.quantum)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt.fillInterval > 0 && opt.capacity > 0 {
|
||||||
|
res.Bucket = ratelimit.NewBucket(opt.fillInterval, opt.capacity)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt.rate > 0 && opt.capacity > 0 {
|
||||||
|
res.Bucket = ratelimit.NewBucketWithRate(opt.rate, opt.capacity)
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("options error can not find func to init")
|
||||||
|
}
|
20
pkg/snowflake/option.go
Normal file
20
pkg/snowflake/option.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package snowflake
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
startTime string
|
||||||
|
machineId int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(o *options)
|
||||||
|
|
||||||
|
func WithStartTime(startTime string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.startTime = startTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMachineId(machineId int64) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.machineId = machineId
|
||||||
|
}
|
||||||
|
}
|
37
pkg/snowflake/snowflake.go
Normal file
37
pkg/snowflake/snowflake.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package snowflake
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bwmarrin/snowflake"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Snow struct {
|
||||||
|
*snowflake.Node
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts ...Option) (*Snow, error) {
|
||||||
|
opt := new(options)
|
||||||
|
for _, o := range opts {
|
||||||
|
o(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
res := new(Snow)
|
||||||
|
|
||||||
|
st, err := time.Parse("2006-01-02", opt.startTime)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
snowflake.Epoch = st.UnixNano() / 1000000
|
||||||
|
res.Node, err = snowflake.NewNode(opt.machineId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Snow) Gen() int64 {
|
||||||
|
return s.Generate().Int64()
|
||||||
|
}
|
155
routes/middleware.go
Normal file
155
routes/middleware.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"dashboard/logger"
|
||||||
|
"dashboard/models"
|
||||||
|
"dashboard/pkg/jwt"
|
||||||
|
"dashboard/pkg/rate"
|
||||||
|
"dashboard/settings"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GinLogger(log *logger.Logger) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
start := time.Now()
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
query := c.Request.URL.RawQuery
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
cost := time.Since(start)
|
||||||
|
|
||||||
|
log.Info(path,
|
||||||
|
zap.Int("status", c.Writer.Status()),
|
||||||
|
zap.String("method", c.Request.Method),
|
||||||
|
zap.String("path", path),
|
||||||
|
zap.String("query", query),
|
||||||
|
zap.String("ip", c.ClientIP()),
|
||||||
|
zap.String("user-agent", c.Request.UserAgent()),
|
||||||
|
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
|
||||||
|
zap.Duration("cost", cost),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GinRecovery(log *logger.Logger, stack bool) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
var brokenPipe bool
|
||||||
|
if ne, ok := err.(*net.OpError); ok {
|
||||||
|
if se, ok := ne.Err.(*os.SyscallError); ok {
|
||||||
|
expectStr := strings.ToLower(se.Error())
|
||||||
|
if strings.Contains(expectStr, "broken pipe") ||
|
||||||
|
strings.Contains(expectStr, "connection reset by peer") {
|
||||||
|
brokenPipe = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
httpRequest, _ := httputil.DumpRequest(c.Request, false)
|
||||||
|
if brokenPipe {
|
||||||
|
log.Error(c.Request.URL.Path,
|
||||||
|
zap.Any("error", err),
|
||||||
|
zap.String("request", string(httpRequest)),
|
||||||
|
)
|
||||||
|
|
||||||
|
c.Error(err.(error))
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if stack {
|
||||||
|
log.Error("[Recovery from panic]",
|
||||||
|
zap.Any("error", err),
|
||||||
|
zap.String("request", string(httpRequest)),
|
||||||
|
zap.String("stack", string(debug.Stack())),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
log.Error("[Recovery from panic]",
|
||||||
|
zap.Any("error", err),
|
||||||
|
zap.String("request", string(httpRequest)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.AbortWithStatus(http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GinLog(log *logger.Logger) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
c.Set(models.GinContxtLog, log)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GinJwtAuthor(jwtC *jwt.Jwt) appHandler {
|
||||||
|
return func(c *gin.Context) error {
|
||||||
|
authHeader, err := c.Cookie(models.GinAuthorKey)
|
||||||
|
if err != nil || !strings.HasPrefix(authHeader, models.GinAuthorPrefixKey) {
|
||||||
|
c.Abort()
|
||||||
|
return &models.BaseError{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
Msg: "Missing or invalid token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenStr := strings.TrimPrefix(authHeader, models.GinAuthorPrefixKey)
|
||||||
|
|
||||||
|
_, err = jwtC.ParseToken(tokenStr)
|
||||||
|
if err != nil {
|
||||||
|
c.Abort()
|
||||||
|
if jwt.ErrorIsJwtExpired(err) {
|
||||||
|
return &models.BaseError{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
Msg: "Expired token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.BaseError{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
Msg: "Invalid token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GinRateLimit(rateC settings.RateLimitConfig) appHandler {
|
||||||
|
lrate, err := rate.New(rate.WithCapacity(rateC.Capacity),
|
||||||
|
rate.WithFillInterval(time.Duration(rateC.FillInterval)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c *gin.Context) error {
|
||||||
|
if lrate.Available() ==0 {
|
||||||
|
return &models.BaseError{
|
||||||
|
Code: http.StatusServiceUnavailable,
|
||||||
|
Msg: "Exceeded rate limit",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
51
routes/routes.go
Normal file
51
routes/routes.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"dashboard/controller"
|
||||||
|
"dashboard/logger"
|
||||||
|
"dashboard/pkg/jwt"
|
||||||
|
"dashboard/settings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/cors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Setup(log *logger.Logger, rate settings.RateLimitConfig, jwtC settings.JwtConfig) *gin.Engine {
|
||||||
|
cjwt := jwt.New(jwt.WithSalt(jwtC.Salt), jwt.WithExpire(time.Duration(jwtC.Expire)*time.Second))
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
|
||||||
|
r.Use(cors.New(cors.Config{
|
||||||
|
AllowOrigins: []string{"*"},
|
||||||
|
AllowMethods: []string{"GET", "POST"},
|
||||||
|
AllowHeaders: []string{"Origin", "Content-Type"},
|
||||||
|
ExposeHeaders: []string{"Content-Length"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
MaxAge: 12 * time.Hour,
|
||||||
|
}))
|
||||||
|
|
||||||
|
r.Use(GinLogger(log), GinRecovery(log, true), GinLog(log), errWapper(GinRateLimit(rate)))
|
||||||
|
|
||||||
|
// 静态文件服务
|
||||||
|
r.Static("/static", "./static")
|
||||||
|
r.StaticFile("/", "./static/index.html")
|
||||||
|
|
||||||
|
r.GET("login", controller.LoginPage)
|
||||||
|
|
||||||
|
r.POST("/api/login", errWapper(controller.UserSignInHandler(cjwt)))
|
||||||
|
r.POST("/api/logout", controller.UserLogOutHandler)
|
||||||
|
|
||||||
|
g1 := r.Group("/api")
|
||||||
|
g1.Use(errWapper(GinJwtAuthor(cjwt)))
|
||||||
|
{
|
||||||
|
g1.GET("/system-info", errWapper(controller.SystemInfoHandle))
|
||||||
|
g1.POST("/upload", errWapper(controller.FileUploadHandle))
|
||||||
|
g1.GET("/files", errWapper(controller.FileListHandle))
|
||||||
|
g1.GET("/download", errWapper(controller.FileDownloadHandle))
|
||||||
|
}
|
||||||
|
|
||||||
|
r.GET("/ws/terminal", errWapper(GinJwtAuthor(cjwt)), errWapper(controller.TerminalHandle()))
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
37
routes/wrapperr.go
Normal file
37
routes/wrapperr.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"dashboard/models"
|
||||||
|
"dashboard/utils"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type appHandler func(*gin.Context) error
|
||||||
|
|
||||||
|
func errWapper(appH appHandler) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
log, _ := utils.GetLogFromContext(c)
|
||||||
|
|
||||||
|
err := appH(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
var baseErr *models.BaseError
|
||||||
|
if errors.As(err, &baseErr) {
|
||||||
|
log.Error("Base error", zap.Any("res", baseErr))
|
||||||
|
|
||||||
|
c.JSON(http.StatusBadRequest, baseErr)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error("Other error", zap.Error(err))
|
||||||
|
c.String(http.StatusBadGateway, err.Error())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
settings/option.go
Normal file
42
settings/option.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import "github.com/fsnotify/fsnotify"
|
||||||
|
|
||||||
|
type BackHandle func(fsnotify.Event)
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
name string
|
||||||
|
ctype string
|
||||||
|
path string
|
||||||
|
cb BackHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option func(*options)
|
||||||
|
|
||||||
|
func (o *options)repair(){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithName(name string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithType(ctype string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.ctype = ctype
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPath(path string) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.path = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCallBack(cb BackHandle) Option {
|
||||||
|
return func(o *options) {
|
||||||
|
o.cb = cb
|
||||||
|
}
|
||||||
|
}
|
59
settings/settings.go
Normal file
59
settings/settings.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
*AppConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opt ...Option) *Settings {
|
||||||
|
opts := new(options)
|
||||||
|
for _, o := range opt {
|
||||||
|
o(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.repair()
|
||||||
|
|
||||||
|
res := &Settings{
|
||||||
|
AppConfig: new(AppConfig),
|
||||||
|
}
|
||||||
|
|
||||||
|
mviper := viper.New()
|
||||||
|
if opts.name != "" && opts.path != "" && opts.ctype != "" {
|
||||||
|
mviper.SetConfigName(opts.name)
|
||||||
|
mviper.AddConfigPath(opts.path)
|
||||||
|
mviper.SetConfigType(opts.ctype)
|
||||||
|
} else {
|
||||||
|
mviper.SetConfigFile(opts.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mviper.ReadInConfig(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mviper.Unmarshal(&res.AppConfig); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mviper.WatchConfig()
|
||||||
|
mviper.OnConfigChange(func(in fsnotify.Event) {
|
||||||
|
if err := mviper.Unmarshal(res.AppConfig); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("config change")
|
||||||
|
|
||||||
|
if opts.cb != nil {
|
||||||
|
opts.cb(in)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
40
settings/type.go
Normal file
40
settings/type.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
type AppConfig struct {
|
||||||
|
*BaseConfig `mapstructure:"base"`
|
||||||
|
*LogConfig `mapstructure:"log"`
|
||||||
|
*SnowflakeConfig `mapstructure:"snowflake"`
|
||||||
|
*RateLimitConfig `mapstructure:"rate"`
|
||||||
|
*JwtConfig `mapstructure:"jwt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseConfig struct {
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
Name string `mapstructure:"name"`
|
||||||
|
Mode string `mapstructure:"mode"`
|
||||||
|
Version string `mapstructure:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogConfig struct {
|
||||||
|
Level string `mapstructure:"level"`
|
||||||
|
Filename string `mapstructure:"filename"`
|
||||||
|
MaxSize int `mapstructure:"max_size"`
|
||||||
|
MaxAge int `mapstructure:"max_age"`
|
||||||
|
MaxBackUps int `mapstructure:"max_backups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SnowflakeConfig struct {
|
||||||
|
StartTime string `mapstructure:"start_time"`
|
||||||
|
MachineId int64 `mapstructure:"machine_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RateLimitConfig struct {
|
||||||
|
FillInterval int64 `mapstructure:"fill_interval"`
|
||||||
|
Capacity int64 `mapstructure:"capacity"`
|
||||||
|
MaxWait int64 `mapstructure:"max_wait"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JwtConfig struct {
|
||||||
|
Salt string `mapstructure:"salt"`
|
||||||
|
Expire int64 `mapstructure:"expire"`
|
||||||
|
}
|
394
static/index.html
Executable file
394
static/index.html
Executable file
@ -0,0 +1,394 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Linux 系统运维监控</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://unpkg.com/xterm@5.3.0/lib/xterm.js"></script>
|
||||||
|
<link href="https://unpkg.com/xterm@5.3.0/css/xterm.css" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
#terminal { height: 400px; }
|
||||||
|
#file-list { max-height: 300px; overflow-y: auto; }
|
||||||
|
.tooltip {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: white;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 10;
|
||||||
|
max-width: 320px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
pointer-events: none; /* 防止提示框干扰鼠标事件 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 font-sans">
|
||||||
|
<div class="container mx-auto p-4">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Linux 系统运维监控</h1>
|
||||||
|
<button id="logout" class="bg-red-500 text-white px-4 py-2 rounded mb-4">登出</button>
|
||||||
|
|
||||||
|
<!-- 系统信息 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||||
|
<div class="bg-white p-4 rounded shadow relative" onmouseover="showTooltip('cpu-tooltip', event)" onmouseout="hideTooltip('cpu-tooltip')">
|
||||||
|
<h2 class="text-lg font-semibold">CPU 使用率</h2>
|
||||||
|
<p id="cpu-usage" class="text-xl">0%</p>
|
||||||
|
<div id="cpu-tooltip" class="tooltip"></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 rounded shadow relative" onmouseover="showTooltip('memory-tooltip', event)" onmouseout="hideTooltip('memory-tooltip')">
|
||||||
|
<h2 class="text-lg font-semibold">内存使用率</h2>
|
||||||
|
<p id="memory-usage" class="text-xl">0%</p>
|
||||||
|
<div id="memory-tooltip" class="tooltip"></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 rounded shadow relative" onmouseover="showTooltip('disk-tooltip', event)" onmouseout="hideTooltip('disk-tooltip')">
|
||||||
|
<h2 class="text-lg font-semibold">磁盘使用率</h2>
|
||||||
|
<p id="disk-usage" class="text-xl">0%</p>
|
||||||
|
<div id="disk-tooltip" class="tooltip"></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white p-4 rounded shadow relative" onmouseover="showTooltip('load-tooltip', event)" onmouseout="hideTooltip('load-tooltip')">
|
||||||
|
<h2 class="text-lg font-semibold">系统负载</h2>
|
||||||
|
<p id="load-avg" class="text-xl">0</p>
|
||||||
|
<div id="load-tooltip" class="tooltip"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件上传 -->
|
||||||
|
<div class="bg-white p-4 rounded shadow mb-8">
|
||||||
|
<h2 class="text-lg font-semibold mb-2">上传文件</h2>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-700 mb-2" for="target-dir">目标目录(绝对路径,如 /tmp/uploads)</label>
|
||||||
|
<input id="target-dir" type="text" class="w-full px-3 py-2 border rounded" placeholder="请输入目标目录">
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-700 mb-2" for="file">选择文件(最大10MB)</label>
|
||||||
|
<input id="file" type="file" class="w-full px-3 py-2 border rounded">
|
||||||
|
</div>
|
||||||
|
<button id="upload" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">上传</button>
|
||||||
|
<p id="upload-message" class="mt-4 text-green-500 hidden"></p>
|
||||||
|
<p id="upload-error" class="mt-4 text-red-500 hidden"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件浏览 -->
|
||||||
|
<div class="bg-white p-4 rounded shadow mb-8">
|
||||||
|
<h2 class="text-lg font-semibold mb-2">浏览文件</h2>
|
||||||
|
<div class="mb-4 flex items-center">
|
||||||
|
<label class="block text-gray-700 mr-2" for="browse-dir">当前路径:</label>
|
||||||
|
<input id="browse-dir" type="text" class="flex-grow px-3 py-2 border rounded" placeholder="请输入目录路径(如 /tmp)">
|
||||||
|
<button id="list-files" class="ml-2 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">刷新</button>
|
||||||
|
<button id="parent-dir" class="ml-2 bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">上一级</button>
|
||||||
|
</div>
|
||||||
|
<div id="file-list" class="border rounded p-2">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">名称</th>
|
||||||
|
<th class="text-left">类型</th>
|
||||||
|
<th class="text-left">大小</th>
|
||||||
|
<th class="text-left">修改时间</th>
|
||||||
|
<th class="text-left">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="file-table"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p id="file-error" class="mt-4 text-red-500 hidden"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 终端 -->
|
||||||
|
<div class="bg-white p-4 rounded shadow">
|
||||||
|
<h2 class="text-lg font-semibold mb-2">交互式终端</h2>
|
||||||
|
<div id="terminal"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 检查登录状态
|
||||||
|
async function checkLoginStatus() {
|
||||||
|
const response = await fetch('/api/system-info');
|
||||||
|
if (response.status === 401) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化字节为MB或GB
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes < 1024 * 1024) {
|
||||||
|
return (bytes / 1024).toFixed(2) + ' KB';
|
||||||
|
} else if (bytes < 1024 * 1024 * 1024) {
|
||||||
|
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
||||||
|
} else {
|
||||||
|
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化时间(秒)
|
||||||
|
function formatUptime(seconds) {
|
||||||
|
const days = Math.floor(seconds / (3600 * 24));
|
||||||
|
const hours = Math.floor((seconds % (3600 * 24)) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
return `${days}天 ${hours}小时 ${minutes}分钟`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示提示框
|
||||||
|
function showTooltip(tooltipId, event) {
|
||||||
|
const tooltip = document.getElementById(tooltipId);
|
||||||
|
const systemInfo = window.lastSystemInfo || {};
|
||||||
|
|
||||||
|
let content = '';
|
||||||
|
if (tooltipId === 'cpu-tooltip') {
|
||||||
|
content = `
|
||||||
|
使用率: ${systemInfo.cpu_usage ? systemInfo.cpu_usage.toFixed(2) : 0}% <br>
|
||||||
|
核心数: ${systemInfo.cpu_cores || '未知'} <br>
|
||||||
|
主频: ${systemInfo.cpu_mhz ? systemInfo.cpu_mhz.toFixed(2) : '未知'} MHz
|
||||||
|
`;
|
||||||
|
} else if (tooltipId === 'memory-tooltip') {
|
||||||
|
content = `
|
||||||
|
使用率: ${systemInfo.memory_usage ? systemInfo.memory_usage.toFixed(2) : 0}% <br>
|
||||||
|
总内存: ${systemInfo.memory_total ? formatBytes(systemInfo.memory_total) : '未知'} <br>
|
||||||
|
可用内存: ${systemInfo.memory_free ? formatBytes(systemInfo.memory_free) : '未知'}
|
||||||
|
`;
|
||||||
|
} else if (tooltipId === 'disk-tooltip') {
|
||||||
|
content = systemInfo.disk_partitions ? systemInfo.disk_partitions.map(p => `
|
||||||
|
挂载点: ${p.mountpoint} <br>
|
||||||
|
使用率: ${p.used_percent.toFixed(2)}% <br>
|
||||||
|
总空间: ${formatBytes(p.total)} <br>
|
||||||
|
已用: ${formatBytes(p.used)}
|
||||||
|
`).join('<hr>') : '无分区信息';
|
||||||
|
} else if (tooltipId === 'load-tooltip') {
|
||||||
|
content = `
|
||||||
|
1分钟负载: ${systemInfo.load_avg ? systemInfo.load_avg.toFixed(2) : 0} <br>
|
||||||
|
系统运行时间: ${systemInfo.uptime ? formatUptime(systemInfo.uptime) : '未知'}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip.innerHTML = content;
|
||||||
|
tooltip.style.display = 'block';
|
||||||
|
|
||||||
|
// 使用鼠标坐标定位
|
||||||
|
const offsetX = 10; // 水平偏移
|
||||||
|
const offsetY = 10; // 垂直偏移
|
||||||
|
let x = event.clientX + window.scrollX + offsetX;
|
||||||
|
let y = event.clientY + window.scrollY + offsetY;
|
||||||
|
|
||||||
|
// 防止提示框超出视口
|
||||||
|
const tooltipRect = tooltip.getBoundingClientRect();
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
// 如果右侧超出,移到鼠标左侧
|
||||||
|
if (x + tooltipRect.width > viewportWidth) {
|
||||||
|
x = event.clientX + window.scrollX - tooltipRect.width - offsetX;
|
||||||
|
}
|
||||||
|
// 如果下方超出,移到鼠标上方
|
||||||
|
if (y + tooltipRect.height > viewportHeight) {
|
||||||
|
y = event.clientY + window.scrollY - tooltipRect.height - offsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
tooltip.style.left = `${x}px`;
|
||||||
|
tooltip.style.top = `${y}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏提示框
|
||||||
|
function hideTooltip(tooltipId) {
|
||||||
|
const tooltip = document.getElementById(tooltipId);
|
||||||
|
tooltip.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新系统信息
|
||||||
|
async function updateSystemInfo() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/system-info');
|
||||||
|
if (response.status === 401) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
window.lastSystemInfo = data; // 缓存最新数据
|
||||||
|
document.getElementById('cpu-usage').textContent = data.cpu_usage.toFixed(2) + '%';
|
||||||
|
document.getElementById('memory-usage').textContent = data.memory_usage.toFixed(2) + '%';
|
||||||
|
document.getElementById('disk-usage').textContent = data.disk_usage.toFixed(2) + '%';
|
||||||
|
document.getElementById('load-avg').textContent = data.load_avg.toFixed(2);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch system info:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化终端
|
||||||
|
const term = new Terminal({
|
||||||
|
cursorBlink: true,
|
||||||
|
theme: {
|
||||||
|
background: '#1a1a1a',
|
||||||
|
foreground: '#ffffff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
term.open(document.getElementById('terminal'));
|
||||||
|
|
||||||
|
// 连接 WebSocket
|
||||||
|
const ws = new WebSocket('ws://' + window.location.host + '/ws/terminal');
|
||||||
|
ws.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket connected');
|
||||||
|
term.write('Connected to terminal\r\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = new Uint8Array(event.data);
|
||||||
|
term.write(new TextDecoder().decode(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (event) => {
|
||||||
|
term.write('\r\nWebSocket error');
|
||||||
|
console.error('WebSocket error:', event);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
term.write('\r\nWebSocket disconnected');
|
||||||
|
window.location.href = '/login';
|
||||||
|
};
|
||||||
|
|
||||||
|
term.onData(data => {
|
||||||
|
ws.send(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
document.getElementById('logout').addEventListener('click', async () => {
|
||||||
|
await fetch('/api/logout', { method: 'POST' });
|
||||||
|
window.location.href = '/login';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 文件上传
|
||||||
|
document.getElementById('upload').addEventListener('click', async () => {
|
||||||
|
const fileInput = document.getElementById('file');
|
||||||
|
const targetDir = document.getElementById('target-dir').value;
|
||||||
|
const messageEl = document.getElementById('upload-message');
|
||||||
|
const errorEl = document.getElementById('upload-error');
|
||||||
|
|
||||||
|
messageEl.classList.add('hidden');
|
||||||
|
errorEl.classList.add('hidden');
|
||||||
|
|
||||||
|
if (!fileInput.files[0]) {
|
||||||
|
errorEl.textContent = '请选择一个文件';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetDir) {
|
||||||
|
errorEl.textContent = '请输入目标目录';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', fileInput.files[0]);
|
||||||
|
formData.append('target_dir', targetDir);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
messageEl.textContent = data.message;
|
||||||
|
messageEl.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
errorEl.textContent = data.error || '上传失败';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Upload error:', error);
|
||||||
|
errorEl.textContent = '网络错误';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 规范化路径
|
||||||
|
function normalizePath(base, name) {
|
||||||
|
// 移除多余斜杠并拼接路径
|
||||||
|
let path = base.replace(/\/+$/, '') + '/' + name.replace(/^\/+/, '');
|
||||||
|
// 确保以单个斜杠开头
|
||||||
|
path = '/' + path.replace(/^\/+/, '');
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件浏览
|
||||||
|
async function listFiles(dirPath) {
|
||||||
|
const fileTable = document.getElementById('file-table');
|
||||||
|
const errorEl = document.getElementById('file-error');
|
||||||
|
const browseDirInput = document.getElementById('browse-dir');
|
||||||
|
|
||||||
|
fileTable.innerHTML = '';
|
||||||
|
errorEl.classList.add('hidden');
|
||||||
|
|
||||||
|
if (!dirPath) {
|
||||||
|
errorEl.textContent = '请输入目录路径';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 规范化路径
|
||||||
|
const normalizedPath = dirPath.replace(/\/+$/, '').replace(/^\/+/, '/');
|
||||||
|
console.log('Listing files for path:', normalizedPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/files?path=${encodeURIComponent(normalizedPath)}`);
|
||||||
|
if (response.status === 401) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
browseDirInput.value = normalizedPath; // 更新输入框路径
|
||||||
|
data.files.forEach(file => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
const nextPath = normalizePath(normalizedPath, file.name);
|
||||||
|
const nameCell = file.is_dir
|
||||||
|
? `<td><a href="#" class="text-blue-500 hover:underline" onclick="listFiles('${nextPath.replace(/'/g, "\\'")}')">${file.name}</a></td>`
|
||||||
|
: `<td>${file.name}</td>`;
|
||||||
|
row.innerHTML = `
|
||||||
|
${nameCell}
|
||||||
|
<td>${file.is_dir ? '目录' : '文件'}</td>
|
||||||
|
<td>${file.is_dir ? '-' : (file.size / 1024).toFixed(2) + ' KB'}</td>
|
||||||
|
<td>${new Date(file.mod_time).toLocaleString()}</td>
|
||||||
|
<td><a href="/api/download?path=${encodeURIComponent(nextPath)}" class="text-blue-500 hover:underline">下载</a></td>
|
||||||
|
`;
|
||||||
|
fileTable.appendChild(row);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
errorEl.textContent = data.error || '无法列出文件';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('List files error:', error);
|
||||||
|
errorEl.textContent = '网络错误';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新文件列表
|
||||||
|
document.getElementById('list-files').addEventListener('click', () => {
|
||||||
|
const dirPath = document.getElementById('browse-dir').value;
|
||||||
|
listFiles(dirPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 上一级目录
|
||||||
|
document.getElementById('parent-dir').addEventListener('click', () => {
|
||||||
|
const currentPath = document.getElementById('browse-dir').value;
|
||||||
|
if (currentPath === '/' || !currentPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parentPath = currentPath.substring(0, currentPath.lastIndexOf('/')) || '/';
|
||||||
|
listFiles(parentPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始检查登录状态并定期更新系统信息
|
||||||
|
checkLoginStatus();
|
||||||
|
updateSystemInfo();
|
||||||
|
setInterval(updateSystemInfo, 5000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
52
static/login.html
Executable file
52
static/login.html
Executable file
@ -0,0 +1,52 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>登录 - Linux 系统运维监控</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 font-sans flex items-center justify-center h-screen">
|
||||||
|
<div class="bg-white p-8 rounded shadow-md w-full max-w-md">
|
||||||
|
<h1 class="text-2xl font-bold mb-6 text-center">登录</h1>
|
||||||
|
<div>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label class="block text-gray-700 mb-2" for="password">密码</label>
|
||||||
|
<input id="password" type="password" class="w-full px-3 py-2 border rounded" placeholder="请输入密码">
|
||||||
|
</div>
|
||||||
|
<button id="login" class="w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">登录</button>
|
||||||
|
<p id="error" class="text-red-500 mt-4 hidden"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('login').addEventListener('click', async () => {
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
const errorEl = document.getElementById('error');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
console.log('Login successful, redirecting to /');
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
errorEl.textContent = data.error || '登录失败';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
errorEl.textContent = '网络错误';
|
||||||
|
errorEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
22
utils/gin.go
Normal file
22
utils/gin.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"dashboard/logger"
|
||||||
|
"dashboard/models"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetLogFromContext(c *gin.Context) (*logger.Logger, error) {
|
||||||
|
res, ok := c.Get(models.GinContxtLog)
|
||||||
|
if !ok {
|
||||||
|
return nil, models.ErrorInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
log, ok := res.(*logger.Logger)
|
||||||
|
if !ok {
|
||||||
|
return nil, models.ErrorInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
return log, nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user