1、first commit。

This commit is contained in:
redhat 2025-05-21 09:37:59 +08:00
commit 5326172a99
29 changed files with 2142 additions and 0 deletions

28
config/config.yaml Normal file
View 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
View 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
View 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
View 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
View 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
}
}

BIN
dash Executable file

Binary file not shown.

67
go.mod Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
package models
type UserInfoParams struct {
Password string `json:"password" binding:"required"`
}

32
models/type.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}