Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb6dd2855e | ||
|
|
64da3c7438 | ||
|
|
fd85f599ba | ||
|
|
cae443d5c8 | ||
|
|
0ec1bb2c54 | ||
|
|
47f8447a22 | ||
|
|
9c986d06cb | ||
|
|
e9428d5757 | ||
|
|
daab64d232 | ||
|
|
da8fb57268 | ||
|
|
fda0dad5f1 | ||
|
|
c6fb3196ea | ||
|
|
711d162a46 | ||
|
|
d667ce8afe | ||
|
|
6abc0fce51 | ||
|
|
b1d77a1d27 |
+16
-12
@@ -97,20 +97,24 @@ def sync_to_gitee(tag: str, body: str, files: slice):
|
||||
asset_api_uri = f"{release_api_uri}/{release_id}/attach_files"
|
||||
|
||||
for file_path in files:
|
||||
files = {
|
||||
'file': open(file_path, 'rb')
|
||||
}
|
||||
success = False
|
||||
|
||||
asset_api_response = requests.post(
|
||||
asset_api_uri, params={'access_token': access_token}, files=files)
|
||||
while not success:
|
||||
files = {
|
||||
'file': open(file_path, 'rb')
|
||||
}
|
||||
|
||||
if asset_api_response.status_code == 201:
|
||||
asset_info = asset_api_response.json()
|
||||
asset_name = asset_info.get('name')
|
||||
print(f"Successfully uploaded {asset_name}!")
|
||||
else:
|
||||
print(
|
||||
f"Request failed with status code {asset_api_response.status_code}")
|
||||
asset_api_response = requests.post(
|
||||
asset_api_uri, params={'access_token': access_token}, files=files)
|
||||
|
||||
if asset_api_response.status_code == 201:
|
||||
asset_info = asset_api_response.json()
|
||||
asset_name = asset_info.get('name')
|
||||
print(f"Successfully uploaded {asset_name}!")
|
||||
success = True
|
||||
else:
|
||||
print(
|
||||
f"Request failed with status code {asset_api_response.status_code}")
|
||||
|
||||
# 仅保留最新 Release 以防超出 Gitee 仓库配额
|
||||
try:
|
||||
|
||||
@@ -72,6 +72,7 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
||||
<a href="https://github.com/acgpiano" title="Acgpiano"><img src="https://avatars.githubusercontent.com/u/15900800?v=4" width="50;" alt="Acgpiano"/></a>
|
||||
<a href="https://github.com/eya46" title="eya46"><img src="https://avatars.githubusercontent.com/u/61458340?v=4" width="50;" alt="eya46"/></a>
|
||||
<a href="https://github.com/guoyongchang" title="guoyongchang"><img src="https://avatars.githubusercontent.com/u/10484506?v=4" width="50;" alt="guoyongchang"/></a>
|
||||
<a href="https://github.com/hiDandelion" title="hiDandelion"><img src="https://avatars.githubusercontent.com/u/77157418?v=4" width="50;" alt="hiDandelion"/></a>
|
||||
<a href="https://github.com/yuanweize" title="I"><img src="https://avatars.githubusercontent.com/u/30067203?v=4" width="50;" alt="I"/></a>
|
||||
<a href="https://github.com/lvyaoting" title="lvyaoting"><img src="https://avatars.githubusercontent.com/u/166296299?v=4" width="50;" alt="lvyaoting"/></a>
|
||||
<a href="https://github.com/unclezs" title="unclezs"><img src="https://avatars.githubusercontent.com/u/42318775?v=4" width="50;" alt="unclezs"/></a>
|
||||
@@ -90,7 +91,7 @@ You can change the dashboard language in the settings page (`/setting`) after th
|
||||
<a href="https://github.com/techotaku" title="Ian Li"><img src="https://avatars.githubusercontent.com/u/1948179?v=4" width="50;" alt="Ian Li"/></a>
|
||||
<a href="https://github.com/GreenTeodoro839" title="GreenTeodoro839"><img src="https://avatars.githubusercontent.com/u/77104800?v=4" width="50;" alt="GreenTeodoro839"/></a>
|
||||
<a href="https://github.com/Es-dese" title="Esdese"><img src="https://avatars.githubusercontent.com/u/71542548?v=4" width="50;" alt="Esdese"/></a>
|
||||
<a href="https://github.com/wwng2333" title="Crazy"><img src="https://avatars.githubusercontent.com/u/17147265?v=4" width="50;" alt="Crazy"/></a>
|
||||
<a href="https://github.com/wwng2333" title=":D"><img src="https://avatars.githubusercontent.com/u/17147265?v=4" width="50;" alt=":D"/></a>
|
||||
<a href="https://github.com/wellcoming" title="Coming"><img src="https://avatars.githubusercontent.com/u/74850890?v=4" width="50;" alt="Coming"/></a><!--GAMFC_DELIMITER_END-->
|
||||
|
||||
## Special Thanks
|
||||
|
||||
@@ -46,6 +46,8 @@ func (cp *commonPage) serve() {
|
||||
cr.GET("/network", cp.network)
|
||||
cr.GET("/ws", cp.ws)
|
||||
cr.POST("/terminal", cp.createTerminal)
|
||||
cr.GET("/file", cp.createFM)
|
||||
cr.GET("/file/:id", cp.fm)
|
||||
}
|
||||
|
||||
type viewPasswordForm struct {
|
||||
@@ -99,7 +101,6 @@ func (p *commonPage) service(c *gin.Context) {
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
|
||||
"Services": res.([]interface{})[0],
|
||||
"CycleTransferStats": res.([]interface{})[1],
|
||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -204,7 +205,6 @@ func (cp *commonPage) network(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/network"), mygin.CommonEnvironment(c, gin.H{
|
||||
"Servers": string(serversBytes),
|
||||
"MonitorInfos": string(monitorInfos),
|
||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||
"MaxTCPPingValue": singleton.Conf.MaxTCPPingValue,
|
||||
}))
|
||||
}
|
||||
@@ -254,14 +254,13 @@ func (cp *commonPage) home(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/home"), mygin.CommonEnvironment(c, gin.H{
|
||||
"Servers": string(stat),
|
||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||
"Servers": string(stat),
|
||||
}))
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 10240,
|
||||
WriteBufferSize: 10240,
|
||||
ReadBufferSize: 32768,
|
||||
WriteBufferSize: 32768,
|
||||
}
|
||||
|
||||
type Data struct {
|
||||
@@ -430,5 +429,130 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/terminal", mygin.CommonEnvironment(c, gin.H{
|
||||
"SessionID": streamId,
|
||||
"ServerName": server.Name,
|
||||
"ServerID": server.ID,
|
||||
}))
|
||||
}
|
||||
|
||||
func (cp *commonPage) fm(c *gin.Context) {
|
||||
streamId := c.Param("id")
|
||||
if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "无权访问",
|
||||
Msg: "FM会话不存在",
|
||||
Link: "/",
|
||||
Btn: "返回首页",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
||||
|
||||
wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusInternalServerError,
|
||||
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
||||
MessageID: "NetworkError",
|
||||
}),
|
||||
Msg: "Websocket协议切换失败",
|
||||
Link: "/",
|
||||
Btn: "返回首页",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
defer wsConn.Close()
|
||||
conn := websocketx.NewConn(wsConn)
|
||||
|
||||
go func() {
|
||||
// PING 保活
|
||||
for {
|
||||
if err = conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second * 10)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = rpc.NezhaHandlerSingleton.UserConnected(streamId, conn); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
|
||||
}
|
||||
|
||||
func (cp *commonPage) createFM(c *gin.Context) {
|
||||
IdString := c.Query("id")
|
||||
if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "无权访问",
|
||||
Msg: "用户未登录",
|
||||
Link: "/login",
|
||||
Btn: "去登录",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
streamId, err := uuid.GenerateUUID()
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusInternalServerError,
|
||||
Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
|
||||
MessageID: "SystemError",
|
||||
}),
|
||||
Msg: "生成会话ID失败",
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
rpc.NezhaHandlerSingleton.CreateStream(streamId)
|
||||
|
||||
serverId, err := strconv.Atoi(IdString)
|
||||
if err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "请求参数有误:" + err.Error(),
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
singleton.ServerLock.RLock()
|
||||
server := singleton.ServerList[uint64(serverId)]
|
||||
singleton.ServerLock.RUnlock()
|
||||
if server == nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "服务器不存在或处于离线状态",
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
fmData, _ := utils.Json.Marshal(&model.TaskFM{
|
||||
StreamID: streamId,
|
||||
})
|
||||
if err := server.TaskStream.Send(&proto.Task{
|
||||
Type: model.TaskTypeFM,
|
||||
Data: string(fmData),
|
||||
}); err != nil {
|
||||
mygin.ShowErrorPage(c, mygin.ErrInfo{
|
||||
Code: http.StatusForbidden,
|
||||
Title: "请求失败",
|
||||
Msg: "Agent信令下发失败",
|
||||
Link: "/server",
|
||||
Btn: "返回重试",
|
||||
}, true)
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/file", mygin.CommonEnvironment(c, gin.H{
|
||||
"SessionID": streamId,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
@@ -35,20 +34,27 @@ func ServeWeb(port uint) *http.Server {
|
||||
pprof.Register(r)
|
||||
}
|
||||
r.Use(natGateway)
|
||||
tmpl := template.New("").Funcs(funcMap)
|
||||
var err error
|
||||
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if os.Getenv("NZ_LOCAL_TEMPLATE") == "true" {
|
||||
r.SetFuncMap(funcMap)
|
||||
r.Use(mygin.RecordPath)
|
||||
r.Static("/static", "resource/static")
|
||||
r.LoadHTMLGlob("resource/template/**/*.html")
|
||||
} else {
|
||||
tmpl := template.New("").Funcs(funcMap)
|
||||
var err error
|
||||
tmpl, err = tmpl.ParseFS(resource.TemplateFS, "template/**/*.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tmpl = loadThirdPartyTemplates(tmpl)
|
||||
r.SetHTMLTemplate(tmpl)
|
||||
r.Use(mygin.RecordPath)
|
||||
staticFs, err := fs.Sub(resource.StaticFS, "static")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r.StaticFS("/static", http.FS(staticFs))
|
||||
}
|
||||
tmpl = loadThirdPartyTemplates(tmpl)
|
||||
r.SetHTMLTemplate(tmpl)
|
||||
r.Use(mygin.RecordPath)
|
||||
staticFs, err := fs.Sub(resource.StaticFS, "static")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r.StaticFS("/static", http.FS(staticFs))
|
||||
r.Static("/static-custom", "resource/static/custom")
|
||||
routers(r)
|
||||
page404 := func(c *gin.Context) {
|
||||
@@ -270,7 +276,7 @@ func natGateway(c *gin.Context) {
|
||||
rpc.NezhaHandlerSingleton.CreateStream(streamId)
|
||||
defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
|
||||
|
||||
taskData, err := json.Marshal(model.TaskNAT{
|
||||
taskData, err := utils.Json.Marshal(model.TaskNAT{
|
||||
StreamID: streamId,
|
||||
Host: natConfig.Host,
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -384,6 +385,7 @@ func (ma *memberAPI) addOrEditServer(c *gin.Context) {
|
||||
} else {
|
||||
s.Host = &model.Host{}
|
||||
s.State = &model.HostState{}
|
||||
s.TaskCloseLock = new(sync.Mutex)
|
||||
singleton.ServerLock.Lock()
|
||||
singleton.SecretToID[s.Secret] = s.ID
|
||||
singleton.ServerList[s.ID] = &s
|
||||
@@ -906,6 +908,7 @@ type settingForm struct {
|
||||
Theme string
|
||||
DashboardTheme string
|
||||
CustomCode string
|
||||
CustomCodeDashboard string
|
||||
ViewPassword string
|
||||
IgnoredIPNotification string
|
||||
IPChangeNotificationTag string // IP变更提醒的通知组
|
||||
@@ -971,6 +974,7 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
|
||||
singleton.Conf.Site.Theme = sf.Theme
|
||||
singleton.Conf.Site.DashboardTheme = sf.DashboardTheme
|
||||
singleton.Conf.Site.CustomCode = sf.CustomCode
|
||||
singleton.Conf.Site.CustomCodeDashboard = sf.CustomCodeDashboard
|
||||
singleton.Conf.Site.ViewPassword = sf.ViewPassword
|
||||
singleton.Conf.Oauth2.Admin = sf.Admin
|
||||
// 保证NotificationTag不为空
|
||||
|
||||
@@ -2,7 +2,6 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -199,7 +198,7 @@ func (oa *oauth2controller) callback(c *gin.Context) {
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
var cloudflareUserInfo *cloudflare.UserInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cloudflareUserInfo); err == nil {
|
||||
if err := utils.Json.NewDecoder(resp.Body).Decode(&cloudflareUserInfo); err == nil {
|
||||
user = cloudflareUserInfo.MapToNezhaUser()
|
||||
}
|
||||
}
|
||||
|
||||
+7
-6
@@ -80,12 +80,13 @@ type Config struct {
|
||||
Debug bool // debug模式开关
|
||||
Language string // 系统语言,默认 zh-CN
|
||||
Site struct {
|
||||
Brand string // 站点名称
|
||||
CookieName string // 浏览器 Cookie 名称
|
||||
Theme string
|
||||
DashboardTheme string
|
||||
CustomCode string
|
||||
ViewPassword string // 前台查看密码
|
||||
Brand string // 站点名称
|
||||
CookieName string // 浏览器 Cookie 名称
|
||||
Theme string
|
||||
DashboardTheme string
|
||||
CustomCode string
|
||||
CustomCodeDashboard string
|
||||
ViewPassword string // 前台查看密码
|
||||
}
|
||||
Oauth2 struct {
|
||||
Type string
|
||||
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
TaskTypeTerminalGRPC
|
||||
TaskTypeNAT
|
||||
TaskTypeReportHostInfo
|
||||
TaskTypeFM
|
||||
)
|
||||
|
||||
type TerminalTask struct {
|
||||
@@ -34,6 +35,10 @@ type TaskNAT struct {
|
||||
Host string
|
||||
}
|
||||
|
||||
type TaskFM struct {
|
||||
StreamID string
|
||||
}
|
||||
|
||||
const (
|
||||
MonitorCoverAll = iota
|
||||
MonitorCoverIgnoreAll
|
||||
|
||||
@@ -170,14 +170,23 @@ func (ns *NotificationServerBundle) replaceParamsInString(str string, message st
|
||||
|
||||
if ns.Server != nil {
|
||||
str = strings.ReplaceAll(str, "#SERVER.NAME#", mod(ns.Server.Name))
|
||||
str = strings.ReplaceAll(str, "#SERVER.ID#", mod(fmt.Sprintf("%d", ns.Server.ID)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.CPU#", mod(fmt.Sprintf("%f", ns.Server.State.CPU)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.MEM#", mod(fmt.Sprintf("%d", ns.Server.State.MemUsed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.SWAP#", mod(fmt.Sprintf("%d", ns.Server.State.SwapUsed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.DISK#", mod(fmt.Sprintf("%d", ns.Server.State.DiskUsed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.MEMUSED#", mod(fmt.Sprintf("%d", ns.Server.State.MemUsed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.SWAPUSED#", mod(fmt.Sprintf("%d", ns.Server.State.SwapUsed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.DISKUSED#", mod(fmt.Sprintf("%d", ns.Server.State.DiskUsed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.MEMTOTAL#", mod(fmt.Sprintf("%d", ns.Server.Host.MemTotal)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.SWAPTOTAL#", mod(fmt.Sprintf("%d", ns.Server.Host.SwapTotal)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.DISKTOTAL#", mod(fmt.Sprintf("%d", ns.Server.Host.DiskTotal)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.NETINSPEED#", mod(fmt.Sprintf("%d", ns.Server.State.NetInSpeed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.NETOUTSPEED#", mod(fmt.Sprintf("%d", ns.Server.State.NetOutSpeed)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.TRANSFERIN#", mod(fmt.Sprintf("%d", ns.Server.State.NetInTransfer)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.TRANSFEROUT#", mod(fmt.Sprintf("%d", ns.Server.State.NetOutTransfer)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.NETINTRANSFER#", mod(fmt.Sprintf("%d", ns.Server.State.NetInTransfer)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.NETOUTTRANSFER#", mod(fmt.Sprintf("%d", ns.Server.State.NetOutTransfer)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.LOAD1#", mod(fmt.Sprintf("%f", ns.Server.State.Load1)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.LOAD5#", mod(fmt.Sprintf("%f", ns.Server.State.Load5)))
|
||||
str = strings.ReplaceAll(str, "#SERVER.LOAD15#", mod(fmt.Sprintf("%f", ns.Server.State.Load15)))
|
||||
|
||||
@@ -72,11 +72,11 @@ func execCase(t *testing.T, item testSt) {
|
||||
UdpConnCount: 0,
|
||||
ProcessCount: 0,
|
||||
},
|
||||
LastActive: time.Time{},
|
||||
TaskClose: nil,
|
||||
TaskStream: nil,
|
||||
PrevHourlyTransferIn: 0,
|
||||
PrevHourlyTransferOut: 0,
|
||||
LastActive: time.Time{},
|
||||
TaskClose: nil,
|
||||
TaskStream: nil,
|
||||
PrevTransferInSnapshot: 0,
|
||||
PrevTransferOutSnapshot: 0,
|
||||
}
|
||||
ns := NotificationServerBundle{
|
||||
Notification: &n,
|
||||
|
||||
+7
-17
@@ -1,10 +1,13 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -42,19 +45,6 @@ func percentage(used, total uint64) float64 {
|
||||
return float64(used) * 100 / float64(total)
|
||||
}
|
||||
|
||||
func maxSliceValue(slice []float64) float64 {
|
||||
if len(slice) != 0 {
|
||||
max := slice[0]
|
||||
for _, val := range slice {
|
||||
if max < val {
|
||||
max = val
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Snapshot 未通过规则返回 struct{}{}, 通过返回 nil
|
||||
func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) interface{} {
|
||||
// 监控全部但是排除了此服务器
|
||||
@@ -103,21 +93,21 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
|
||||
src = float64(server.LastActive.Unix())
|
||||
}
|
||||
case "transfer_in_cycle":
|
||||
src = float64(server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn))
|
||||
src = float64(utils.Uint64SubInt64(server.State.NetInTransfer, server.PrevTransferInSnapshot))
|
||||
if u.CycleInterval != 0 {
|
||||
var res NResult
|
||||
db.Model(&Transfer{}).Select("SUM(`in`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
|
||||
src += float64(res.N)
|
||||
}
|
||||
case "transfer_out_cycle":
|
||||
src = float64(server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut))
|
||||
src = float64(utils.Uint64SubInt64(server.State.NetOutTransfer, server.PrevTransferOutSnapshot))
|
||||
if u.CycleInterval != 0 {
|
||||
var res NResult
|
||||
db.Model(&Transfer{}).Select("SUM(`out`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
|
||||
src += float64(res.N)
|
||||
}
|
||||
case "transfer_all_cycle":
|
||||
src = float64(server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut) + server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn))
|
||||
src = float64(utils.Uint64SubInt64(server.State.NetOutTransfer, server.PrevTransferOutSnapshot) + utils.Uint64SubInt64(server.State.NetInTransfer, server.PrevTransferInSnapshot))
|
||||
if u.CycleInterval != 0 {
|
||||
var res NResult
|
||||
db.Model(&Transfer{}).Select("SUM(`in`+`out`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
|
||||
@@ -143,7 +133,7 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
|
||||
temp = append(temp, tempStat.Temperature)
|
||||
}
|
||||
}
|
||||
src = maxSliceValue(temp)
|
||||
src = slices.Max(temp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-6
@@ -3,6 +3,7 @@ package model
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
@@ -27,11 +28,12 @@ type Server struct {
|
||||
State *HostState `gorm:"-"`
|
||||
LastActive time.Time `gorm:"-"`
|
||||
|
||||
TaskClose chan error `gorm:"-" json:"-"`
|
||||
TaskStream pb.NezhaService_RequestTaskServer `gorm:"-" json:"-"`
|
||||
TaskClose chan error `gorm:"-" json:"-"`
|
||||
TaskCloseLock *sync.Mutex `gorm:"-" json:"-"`
|
||||
TaskStream pb.NezhaService_RequestTaskServer `gorm:"-" json:"-"`
|
||||
|
||||
PrevHourlyTransferIn int64 `gorm:"-" json:"-"` // 上次数据点时的入站使用量
|
||||
PrevHourlyTransferOut int64 `gorm:"-" json:"-"` // 上次数据点时的出站使用量
|
||||
PrevTransferInSnapshot int64 `gorm:"-" json:"-"` // 上次数据点时的入站使用量
|
||||
PrevTransferOutSnapshot int64 `gorm:"-" json:"-"` // 上次数据点时的出站使用量
|
||||
}
|
||||
|
||||
func (s *Server) CopyFromRunningServer(old *Server) {
|
||||
@@ -39,9 +41,10 @@ func (s *Server) CopyFromRunningServer(old *Server) {
|
||||
s.State = old.State
|
||||
s.LastActive = old.LastActive
|
||||
s.TaskClose = old.TaskClose
|
||||
s.TaskCloseLock = old.TaskCloseLock
|
||||
s.TaskStream = old.TaskStream
|
||||
s.PrevHourlyTransferIn = old.PrevHourlyTransferIn
|
||||
s.PrevHourlyTransferOut = old.PrevHourlyTransferOut
|
||||
s.PrevTransferInSnapshot = old.PrevTransferInSnapshot
|
||||
s.PrevTransferOutSnapshot = old.PrevTransferOutSnapshot
|
||||
}
|
||||
|
||||
func boolToString(b bool) string {
|
||||
|
||||
+131
-83
@@ -2,169 +2,217 @@ package ddns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
const baseEndpoint = "https://api.cloudflare.com/client/v4/zones"
|
||||
|
||||
type ProviderCloudflare struct {
|
||||
Secret string
|
||||
secret string
|
||||
zoneId string
|
||||
recordId string
|
||||
domainConfig *DomainConfig
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
if domainConfig == nil {
|
||||
return false
|
||||
}
|
||||
type cfReq struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
Proxied bool `json:"proxied"`
|
||||
}
|
||||
|
||||
zoneID, err := provider.getZoneID(domainConfig.FullDomain)
|
||||
type cfResp struct {
|
||||
Result []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
func NewProviderCloudflare(s string) *ProviderCloudflare {
|
||||
return &ProviderCloudflare{
|
||||
secret: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) error {
|
||||
if domainConfig == nil {
|
||||
return fmt.Errorf("获取 DDNS 配置失败")
|
||||
}
|
||||
provider.domainConfig = domainConfig
|
||||
|
||||
err := provider.getZoneID()
|
||||
if err != nil {
|
||||
log.Printf("无法获取 zone ID: %s\n", err)
|
||||
return false
|
||||
return fmt.Errorf("无法获取 zone ID: %s", err)
|
||||
}
|
||||
|
||||
// 当IPv4和IPv6同时成功才算作成功
|
||||
var resultV4 = true
|
||||
var resultV6 = true
|
||||
if domainConfig.EnableIPv4 {
|
||||
if !provider.addDomainRecord(zoneID, domainConfig, true) {
|
||||
resultV4 = false
|
||||
if provider.domainConfig.EnableIPv4 {
|
||||
if err = provider.addDomainRecord(true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if domainConfig.EnableIpv6 {
|
||||
if !provider.addDomainRecord(zoneID, domainConfig, false) {
|
||||
resultV6 = false
|
||||
if provider.domainConfig.EnableIpv6 {
|
||||
if err = provider.addDomainRecord(false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return resultV4 && resultV6
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool {
|
||||
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain, isIpv4)
|
||||
func (provider *ProviderCloudflare) addDomainRecord(isIpv4 bool) error {
|
||||
err := provider.findDNSRecord(isIpv4)
|
||||
if err != nil {
|
||||
log.Printf("查找 DNS 记录时出错: %s\n", err)
|
||||
return false
|
||||
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
if provider.recordId == "" {
|
||||
// 添加 DNS 记录
|
||||
return provider.createDNSRecord(zoneID, domainConfig, isIpv4)
|
||||
return provider.createDNSRecord(isIpv4)
|
||||
} else {
|
||||
// 更新 DNS 记录
|
||||
return provider.updateDNSRecord(zoneID, record["id"].(string), domainConfig, isIpv4)
|
||||
return provider.updateDNSRecord(isIpv4)
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) getZoneID(domain string) (string, error) {
|
||||
_, realDomain := SplitDomain(domain)
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", realDomain)
|
||||
body, err := provider.sendRequest("GET", url, nil)
|
||||
func (provider *ProviderCloudflare) getZoneID() error {
|
||||
_, realDomain := splitDomain(provider.domainConfig.FullDomain)
|
||||
zu, _ := url.Parse(baseEndpoint)
|
||||
|
||||
q := zu.Query()
|
||||
q.Set("name", realDomain)
|
||||
zu.RawQuery = q.Encode()
|
||||
|
||||
body, err := provider.sendRequest("GET", zu.String(), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
var res map[string]interface{}
|
||||
err = json.Unmarshal(body, &res)
|
||||
res := &cfResp{}
|
||||
err = utils.Json.Unmarshal(body, res)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
result := res["result"].([]interface{})
|
||||
result := res.Result
|
||||
if len(result) > 0 {
|
||||
zoneID := result[0].(map[string]interface{})["id"].(string)
|
||||
return zoneID, nil
|
||||
provider.zoneId = result[0].ID
|
||||
return nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("找不到 Zone ID")
|
||||
return fmt.Errorf("找不到 Zone ID")
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
|
||||
var ipType = "A"
|
||||
if !isIPv4 {
|
||||
func (provider *ProviderCloudflare) findDNSRecord(isIPv4 bool) error {
|
||||
var ipType string
|
||||
if isIPv4 {
|
||||
ipType = "A"
|
||||
} else {
|
||||
ipType = "AAAA"
|
||||
}
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=%s&name=%s", zoneID, ipType, domain)
|
||||
body, err := provider.sendRequest("GET", url, nil)
|
||||
|
||||
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
|
||||
du, _ := url.Parse(de)
|
||||
|
||||
q := du.Query()
|
||||
q.Set("name", provider.domainConfig.FullDomain)
|
||||
q.Set("type", ipType)
|
||||
du.RawQuery = q.Encode()
|
||||
|
||||
body, err := provider.sendRequest("GET", du.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
var res map[string]interface{}
|
||||
err = json.Unmarshal(body, &res)
|
||||
res := &cfResp{}
|
||||
err = utils.Json.Unmarshal(body, res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
result := res["result"].([]interface{})
|
||||
result := res.Result
|
||||
if len(result) > 0 {
|
||||
return result[0].(map[string]interface{}), nil
|
||||
provider.recordId = result[0].ID
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil, nil // 没有找到 DNS 记录
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
var ipType = "A"
|
||||
var ipAddr = domainConfig.Ipv4Addr
|
||||
if !isIPv4 {
|
||||
func (provider *ProviderCloudflare) createDNSRecord(isIPv4 bool) error {
|
||||
var ipType, ipAddr string
|
||||
if isIPv4 {
|
||||
ipType = "A"
|
||||
ipAddr = provider.domainConfig.Ipv4Addr
|
||||
} else {
|
||||
ipType = "AAAA"
|
||||
ipAddr = domainConfig.Ipv6Addr
|
||||
ipAddr = provider.domainConfig.Ipv6Addr
|
||||
}
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID)
|
||||
data := map[string]interface{}{
|
||||
"type": ipType,
|
||||
"name": domainConfig.FullDomain,
|
||||
"content": ipAddr,
|
||||
"ttl": 60,
|
||||
"proxied": false,
|
||||
|
||||
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records")
|
||||
data := &cfReq{
|
||||
Name: provider.domainConfig.FullDomain,
|
||||
Type: ipType,
|
||||
Content: ipAddr,
|
||||
TTL: 60,
|
||||
Proxied: false,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
_, err := provider.sendRequest("POST", url, jsonData)
|
||||
return err == nil
|
||||
|
||||
jsonData, _ := utils.Json.Marshal(data)
|
||||
_, err := provider.sendRequest("POST", de, jsonData)
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
var ipType = "A"
|
||||
var ipAddr = domainConfig.Ipv4Addr
|
||||
if !isIPv4 {
|
||||
func (provider *ProviderCloudflare) updateDNSRecord(isIPv4 bool) error {
|
||||
var ipType, ipAddr string
|
||||
if isIPv4 {
|
||||
ipType = "A"
|
||||
ipAddr = provider.domainConfig.Ipv4Addr
|
||||
} else {
|
||||
ipType = "AAAA"
|
||||
ipAddr = domainConfig.Ipv6Addr
|
||||
ipAddr = provider.domainConfig.Ipv6Addr
|
||||
}
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, recordID)
|
||||
data := map[string]interface{}{
|
||||
"type": ipType,
|
||||
"name": domainConfig.FullDomain,
|
||||
"content": ipAddr,
|
||||
"ttl": 60,
|
||||
"proxied": false,
|
||||
|
||||
de, _ := url.JoinPath(baseEndpoint, provider.zoneId, "dns_records", provider.recordId)
|
||||
data := &cfReq{
|
||||
Name: provider.domainConfig.FullDomain,
|
||||
Type: ipType,
|
||||
Content: ipAddr,
|
||||
TTL: 60,
|
||||
Proxied: false,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
_, err := provider.sendRequest("PATCH", url, jsonData)
|
||||
return err == nil
|
||||
|
||||
jsonData, _ := utils.Json.Marshal(data)
|
||||
_, err := provider.sendRequest("PATCH", de, jsonData)
|
||||
return err
|
||||
}
|
||||
|
||||
// 以下为辅助方法,如发送 HTTP 请求等
|
||||
func (provider *ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.Secret))
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", provider.secret))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
resp, err := utils.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s\n", err.Error())
|
||||
log.Printf("NEZHA>> 无法关闭HTTP响应体流: %s", err.Error())
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
|
||||
+9
-1
@@ -1,5 +1,7 @@
|
||||
package ddns
|
||||
|
||||
import "golang.org/x/net/publicsuffix"
|
||||
|
||||
type DomainConfig struct {
|
||||
EnableIPv4 bool
|
||||
EnableIpv6 bool
|
||||
@@ -10,5 +12,11 @@ type DomainConfig struct {
|
||||
|
||||
type Provider interface {
|
||||
// UpdateDomain Return is updated
|
||||
UpdateDomain(domainConfig *DomainConfig) bool
|
||||
UpdateDomain(*DomainConfig) error
|
||||
}
|
||||
|
||||
func splitDomain(domain string) (prefix string, realDomain string) {
|
||||
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain)
|
||||
prefix = domain[:len(domain)-len(realDomain)-1]
|
||||
return prefix, realDomain
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,6 +2,6 @@ package ddns
|
||||
|
||||
type ProviderDummy struct{}
|
||||
|
||||
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
return false
|
||||
func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package ddns
|
||||
|
||||
import (
|
||||
"golang.org/x/net/publicsuffix"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (provider ProviderWebHook) FormatWebhookString(s string, config *DomainConfig, ipType string) string {
|
||||
if config == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
result := strings.TrimSpace(s)
|
||||
result = strings.Replace(s, "{ip}", config.Ipv4Addr, -1)
|
||||
result = strings.Replace(result, "{domain}", config.FullDomain, -1)
|
||||
result = strings.Replace(result, "{type}", ipType, -1)
|
||||
// remove \r
|
||||
result = strings.Replace(result, "\r", "", -1)
|
||||
return result
|
||||
}
|
||||
|
||||
func SetStringHeadersToRequest(req *http.Request, headers []string) {
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
for _, element := range headers {
|
||||
kv := strings.SplitN(element, ":", 2)
|
||||
if len(kv) == 2 {
|
||||
req.Header.Add(kv[0], kv[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SplitDomain 分割域名为前缀和一级域名
|
||||
func SplitDomain(domain string) (prefix string, realDomain string) {
|
||||
realDomain, _ = publicsuffix.EffectiveTLDPlusOne(domain)
|
||||
prefix = domain[:len(domain)-len(realDomain)-1]
|
||||
return prefix, realDomain
|
||||
}
|
||||
+119
-84
@@ -5,145 +5,180 @@ import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
url = "https://dnspod.tencentcloudapi.com"
|
||||
)
|
||||
const te = "https://dnspod.tencentcloudapi.com"
|
||||
|
||||
type ProviderTencentCloud struct {
|
||||
SecretID string
|
||||
SecretKey string
|
||||
secretID string
|
||||
secretKey string
|
||||
domainConfig *DomainConfig
|
||||
resp *tcResp
|
||||
}
|
||||
|
||||
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
if domainConfig == nil {
|
||||
return false
|
||||
type tcReq struct {
|
||||
RecordType string `json:"RecordType"`
|
||||
Domain string `json:"Domain"`
|
||||
RecordLine string `json:"RecordLine"`
|
||||
Subdomain string `json:"Subdomain,omitempty"`
|
||||
SubDomain string `json:"SubDomain,omitempty"` // As is
|
||||
Value string `json:"Value,omitempty"`
|
||||
TTL uint32 `json:"TTL,omitempty"`
|
||||
RecordId uint64 `json:"RecordId,omitempty"`
|
||||
}
|
||||
|
||||
type tcResp struct {
|
||||
Response struct {
|
||||
RecordList []struct {
|
||||
RecordId uint64
|
||||
Value string
|
||||
}
|
||||
Error struct {
|
||||
Code string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewProviderTencentCloud(id, key string) *ProviderTencentCloud {
|
||||
return &ProviderTencentCloud{
|
||||
secretID: id,
|
||||
secretKey: key,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) error {
|
||||
if domainConfig == nil {
|
||||
return fmt.Errorf("获取 DDNS 配置失败")
|
||||
}
|
||||
provider.domainConfig = domainConfig
|
||||
|
||||
// 当IPv4和IPv6同时成功才算作成功
|
||||
var resultV4 = true
|
||||
var resultV6 = true
|
||||
if domainConfig.EnableIPv4 {
|
||||
if !provider.addDomainRecord(domainConfig, true) {
|
||||
resultV4 = false
|
||||
var err error
|
||||
if provider.domainConfig.EnableIPv4 {
|
||||
if err = provider.addDomainRecord(true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if domainConfig.EnableIpv6 {
|
||||
if !provider.addDomainRecord(domainConfig, false) {
|
||||
resultV6 = false
|
||||
if provider.domainConfig.EnableIpv6 {
|
||||
if err = provider.addDomainRecord(false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return resultV4 && resultV6
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig, isIpv4 bool) bool {
|
||||
record, err := provider.findDNSRecord(domainConfig.FullDomain, isIpv4)
|
||||
func (provider *ProviderTencentCloud) addDomainRecord(isIpv4 bool) error {
|
||||
err := provider.findDNSRecord(isIpv4)
|
||||
if err != nil {
|
||||
log.Printf("查找 DNS 记录时出错: %s\n", err)
|
||||
return false
|
||||
return fmt.Errorf("查找 DNS 记录时出错: %s", err)
|
||||
}
|
||||
|
||||
if errResponse, ok := record["Error"].(map[string]interface{}); ok {
|
||||
if errCode, ok := errResponse["Code"].(string); ok && errCode == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
|
||||
// 添加 DNS 记录
|
||||
return provider.createDNSRecord(domainConfig.FullDomain, domainConfig, isIpv4)
|
||||
} else {
|
||||
log.Printf("查询 DNS 记录时出错,错误代码为: %s\n", errCode)
|
||||
}
|
||||
if provider.resp.Response.Error.Code == "ResourceNotFound.NoDataOfRecord" { // 没有找到 DNS 记录
|
||||
return provider.createDNSRecord(isIpv4)
|
||||
} else if provider.resp.Response.Error.Code != "" {
|
||||
return fmt.Errorf("查询 DNS 记录时出错,错误代码为: %s", provider.resp.Response.Error.Code)
|
||||
}
|
||||
|
||||
// 默认情况下更新 DNS 记录
|
||||
return provider.updateDNSRecord(domainConfig.FullDomain, record["RecordList"].([]interface{})[0].(map[string]interface{})["RecordId"].(float64), domainConfig, isIpv4)
|
||||
return provider.updateDNSRecord(isIpv4)
|
||||
}
|
||||
|
||||
func (provider *ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (map[string]interface{}, error) {
|
||||
var ipType = "A"
|
||||
if !isIPv4 {
|
||||
func (provider *ProviderTencentCloud) findDNSRecord(isIPv4 bool) error {
|
||||
var ipType string
|
||||
if isIPv4 {
|
||||
ipType = "A"
|
||||
} else {
|
||||
ipType = "AAAA"
|
||||
}
|
||||
_, realDomain := SplitDomain(domain)
|
||||
prefix, _ := SplitDomain(domain)
|
||||
data := map[string]interface{}{
|
||||
"RecordType": ipType,
|
||||
"Domain": realDomain,
|
||||
"RecordLine": "默认",
|
||||
"Subdomain": prefix,
|
||||
|
||||
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
|
||||
data := &tcReq{
|
||||
RecordType: ipType,
|
||||
Domain: realDomain,
|
||||
RecordLine: "默认",
|
||||
Subdomain: prefix,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
|
||||
jsonData, _ := utils.Json.Marshal(data)
|
||||
body, err := provider.sendRequest("DescribeRecordList", jsonData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
var res map[string]interface{}
|
||||
err = json.Unmarshal(body, &res)
|
||||
provider.resp = &tcResp{}
|
||||
err = utils.Json.Unmarshal(body, provider.resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
result := res["Response"].(map[string]interface{})
|
||||
return result, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *ProviderTencentCloud) createDNSRecord(domain string, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
var ipType = "A"
|
||||
var ipAddr = domainConfig.Ipv4Addr
|
||||
if !isIPv4 {
|
||||
func (provider *ProviderTencentCloud) createDNSRecord(isIPv4 bool) error {
|
||||
var ipType, ipAddr string
|
||||
if isIPv4 {
|
||||
ipType = "A"
|
||||
ipAddr = provider.domainConfig.Ipv4Addr
|
||||
} else {
|
||||
ipType = "AAAA"
|
||||
ipAddr = domainConfig.Ipv6Addr
|
||||
ipAddr = provider.domainConfig.Ipv6Addr
|
||||
}
|
||||
_, realDomain := SplitDomain(domain)
|
||||
prefix, _ := SplitDomain(domain)
|
||||
data := map[string]interface{}{
|
||||
"RecordType": ipType,
|
||||
"RecordLine": "默认",
|
||||
"Domain": realDomain,
|
||||
"SubDomain": prefix,
|
||||
"Value": ipAddr,
|
||||
"TTL": 600,
|
||||
|
||||
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
|
||||
data := &tcReq{
|
||||
RecordType: ipType,
|
||||
RecordLine: "默认",
|
||||
Domain: realDomain,
|
||||
SubDomain: prefix,
|
||||
Value: ipAddr,
|
||||
TTL: 600,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
|
||||
jsonData, _ := utils.Json.Marshal(data)
|
||||
_, err := provider.sendRequest("CreateRecord", jsonData)
|
||||
return err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (provider *ProviderTencentCloud) updateDNSRecord(domain string, recordID float64, domainConfig *DomainConfig, isIPv4 bool) bool {
|
||||
var ipType = "A"
|
||||
var ipAddr = domainConfig.Ipv4Addr
|
||||
if !isIPv4 {
|
||||
func (provider *ProviderTencentCloud) updateDNSRecord(isIPv4 bool) error {
|
||||
var ipType, ipAddr string
|
||||
if isIPv4 {
|
||||
ipType = "A"
|
||||
ipAddr = provider.domainConfig.Ipv4Addr
|
||||
} else {
|
||||
ipType = "AAAA"
|
||||
ipAddr = domainConfig.Ipv6Addr
|
||||
ipAddr = provider.domainConfig.Ipv6Addr
|
||||
}
|
||||
_, realDomain := SplitDomain(domain)
|
||||
prefix, _ := SplitDomain(domain)
|
||||
data := map[string]interface{}{
|
||||
"RecordType": ipType,
|
||||
"RecordLine": "默认",
|
||||
"Domain": realDomain,
|
||||
"SubDomain": prefix,
|
||||
"Value": ipAddr,
|
||||
"TTL": 600,
|
||||
"RecordId": recordID,
|
||||
|
||||
prefix, realDomain := splitDomain(provider.domainConfig.FullDomain)
|
||||
data := &tcReq{
|
||||
RecordType: ipType,
|
||||
RecordLine: "默认",
|
||||
Domain: realDomain,
|
||||
SubDomain: prefix,
|
||||
Value: ipAddr,
|
||||
TTL: 600,
|
||||
RecordId: provider.resp.Response.RecordList[0].RecordId,
|
||||
}
|
||||
jsonData, _ := json.Marshal(data)
|
||||
|
||||
jsonData, _ := utils.Json.Marshal(data)
|
||||
_, err := provider.sendRequest("ModifyRecord", jsonData)
|
||||
return err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
// 以下为辅助方法,如发送 HTTP 请求等
|
||||
func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
|
||||
req, err := http.NewRequest("POST", te, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -151,8 +186,8 @@ func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-TC-Version", "2021-03-23")
|
||||
|
||||
provider.signRequest(provider.SecretID, provider.SecretKey, req, action, string(data))
|
||||
resp, err := client.Do(req)
|
||||
provider.signRequest(provider.secretID, provider.secretKey, req, action, string(data))
|
||||
resp, err := utils.HttpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+91
-40
@@ -2,58 +2,109 @@ package ddns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/naiba/nezha/pkg/utils"
|
||||
)
|
||||
|
||||
type ProviderWebHook struct {
|
||||
URL string
|
||||
RequestMethod string
|
||||
RequestBody string
|
||||
RequestHeader string
|
||||
url string
|
||||
requestMethod string
|
||||
requestBody string
|
||||
requestHeader string
|
||||
domainConfig *DomainConfig
|
||||
}
|
||||
|
||||
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
|
||||
func NewProviderWebHook(s, rm, rb, rh string) *ProviderWebHook {
|
||||
return &ProviderWebHook{
|
||||
url: s,
|
||||
requestMethod: rm,
|
||||
requestBody: rb,
|
||||
requestHeader: rh,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) error {
|
||||
if domainConfig == nil {
|
||||
return false
|
||||
return fmt.Errorf("获取 DDNS 配置失败")
|
||||
}
|
||||
provider.domainConfig = domainConfig
|
||||
|
||||
if provider.domainConfig.FullDomain == "" {
|
||||
return fmt.Errorf("failed to update an empty domain")
|
||||
}
|
||||
|
||||
if domainConfig.FullDomain == "" {
|
||||
log.Println("NEZHA>> Failed to update an empty domain")
|
||||
return false
|
||||
}
|
||||
updated := false
|
||||
client := &http.Client{}
|
||||
if domainConfig.EnableIPv4 && domainConfig.Ipv4Addr != "" {
|
||||
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv4")
|
||||
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv4")
|
||||
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv4")
|
||||
headers := strings.Split(header, "\n")
|
||||
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
|
||||
if err == nil && req != nil {
|
||||
SetStringHeadersToRequest(req, headers)
|
||||
if _, err := client.Do(req); err != nil {
|
||||
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
|
||||
} else {
|
||||
updated = true
|
||||
}
|
||||
if provider.domainConfig.EnableIPv4 && provider.domainConfig.Ipv4Addr != "" {
|
||||
req, err := provider.prepareRequest(true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
|
||||
}
|
||||
if _, err := utils.HttpClient.Do(req); err != nil {
|
||||
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
|
||||
}
|
||||
}
|
||||
if domainConfig.EnableIpv6 && domainConfig.Ipv6Addr != "" {
|
||||
url := provider.FormatWebhookString(provider.URL, domainConfig, "ipv6")
|
||||
body := provider.FormatWebhookString(provider.RequestBody, domainConfig, "ipv6")
|
||||
header := provider.FormatWebhookString(provider.RequestHeader, domainConfig, "ipv6")
|
||||
headers := strings.Split(header, "\n")
|
||||
req, err := http.NewRequest(provider.RequestMethod, url, bytes.NewBufferString(body))
|
||||
if err == nil && req != nil {
|
||||
SetStringHeadersToRequest(req, headers)
|
||||
if _, err := client.Do(req); err != nil {
|
||||
log.Printf("NEZHA>> Failed to update a domain: %s. Cause by: %s\n", domainConfig.FullDomain, err.Error())
|
||||
} else {
|
||||
updated = true
|
||||
}
|
||||
|
||||
if provider.domainConfig.EnableIpv6 && provider.domainConfig.Ipv6Addr != "" {
|
||||
req, err := provider.prepareRequest(false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
|
||||
}
|
||||
if _, err := utils.HttpClient.Do(req); err != nil {
|
||||
return fmt.Errorf("failed to update a domain: %s. Cause by: %v", provider.domainConfig.FullDomain, err)
|
||||
}
|
||||
}
|
||||
return updated
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *ProviderWebHook) prepareRequest(isIPv4 bool) (*http.Request, error) {
|
||||
u, err := url.Parse(provider.url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed parsing url: %v", err)
|
||||
}
|
||||
|
||||
// Only handle queries here
|
||||
q := u.Query()
|
||||
for p, vals := range q {
|
||||
for n, v := range vals {
|
||||
vals[n] = provider.formatWebhookString(v, isIPv4)
|
||||
}
|
||||
q[p] = vals
|
||||
}
|
||||
|
||||
u.RawQuery = q.Encode()
|
||||
body := provider.formatWebhookString(provider.requestBody, isIPv4)
|
||||
header := provider.formatWebhookString(provider.requestHeader, isIPv4)
|
||||
headers := strings.Split(header, "\n")
|
||||
|
||||
req, err := http.NewRequest(provider.requestMethod, u.String(), bytes.NewBufferString(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed creating new request: %v", err)
|
||||
}
|
||||
|
||||
utils.SetStringHeadersToRequest(req, headers)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (provider *ProviderWebHook) formatWebhookString(s string, isIPv4 bool) string {
|
||||
var ipAddr, ipType string
|
||||
if isIPv4 {
|
||||
ipAddr = provider.domainConfig.Ipv4Addr
|
||||
ipType = "ipv4"
|
||||
} else {
|
||||
ipAddr = provider.domainConfig.Ipv6Addr
|
||||
ipType = "ipv6"
|
||||
}
|
||||
|
||||
r := strings.NewReplacer(
|
||||
"{ip}", ipAddr,
|
||||
"{domain}", provider.domainConfig.FullDomain,
|
||||
"{type}", ipType,
|
||||
"\r", "",
|
||||
)
|
||||
|
||||
result := r.Replace(strings.TrimSpace(s))
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H {
|
||||
data["Version"] = singleton.Version
|
||||
data["Conf"] = singleton.Conf
|
||||
data["Themes"] = model.Themes
|
||||
data["CustomCode"] = singleton.Conf.Site.CustomCode
|
||||
data["CustomCodeDashboard"] = singleton.Conf.Site.CustomCodeDashboard
|
||||
// 是否是管理页面
|
||||
data["IsAdminPage"] = adminPage[data["MatchedPath"].(string)]
|
||||
// 站点标题
|
||||
|
||||
@@ -37,8 +37,7 @@ func ValidateViewPassword(opt ValidateViewPasswordOption) gin.HandlerFunc {
|
||||
}
|
||||
if opt.IsPage {
|
||||
c.HTML(http.StatusOK, GetPreferredTheme(c, "/viewpassword"), CommonEnvironment(c, gin.H{
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
|
||||
"CustomCode": singleton.Conf.Site.CustomCode,
|
||||
"Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
|
||||
}))
|
||||
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ package utils
|
||||
import (
|
||||
"crypto/rand"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -76,3 +77,25 @@ func GenerateRandomString(n int) (string, error) {
|
||||
}
|
||||
return string(ret), nil
|
||||
}
|
||||
|
||||
func Uint64SubInt64(a uint64, b int64) uint64 {
|
||||
if b < 0 {
|
||||
return a + uint64(-b)
|
||||
}
|
||||
if a < uint64(b) {
|
||||
return 0
|
||||
}
|
||||
return a - uint64(b)
|
||||
}
|
||||
|
||||
func SetStringHeadersToRequest(req *http.Request, headers []string) {
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
for _, element := range headers {
|
||||
kv := strings.SplitN(element, ":", 2)
|
||||
if len(kv) == 2 {
|
||||
req.Header.Add(kv[0], kv[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+26
-2
@@ -331,6 +331,9 @@ other = "Frontend Theme"
|
||||
[CustomCodes]
|
||||
other = "Custom Codes (Style and Script)"
|
||||
|
||||
[CustomCodesDashboard]
|
||||
other = "Custom Codes for Dashboard"
|
||||
|
||||
[AccessPassword]
|
||||
other = "Frontend Access Password"
|
||||
|
||||
@@ -650,7 +653,28 @@ other = "Disable Switch Template in Frontend"
|
||||
other = "Servers On World Map"
|
||||
|
||||
[NAT]
|
||||
other = "NAT"
|
||||
other = "NAT Traversal"
|
||||
|
||||
[NetworkSpiterList]
|
||||
other = "Network Monitor"
|
||||
other = "Network Monitor"
|
||||
|
||||
[Refresh]
|
||||
other = "Refresh"
|
||||
|
||||
[CopyPath]
|
||||
other = "Copy Path"
|
||||
|
||||
[Goto]
|
||||
other = "Go to"
|
||||
|
||||
[GotoHeadline]
|
||||
other = "Go to a Folder"
|
||||
|
||||
[GotoGo]
|
||||
other = "Go"
|
||||
|
||||
[GotoClose]
|
||||
other = "Cancel"
|
||||
|
||||
[FMError]
|
||||
other = "Agent returned an error, please view the console for details. To open a new connection, reopen the FM again."
|
||||
|
||||
Vendored
+26
-2
@@ -331,6 +331,9 @@ other = "Tema de Frontend"
|
||||
[CustomCodes]
|
||||
other = "Códigos Personalizados (Incluye style y script)"
|
||||
|
||||
[CustomCodesDashboard]
|
||||
other = "Custom Codes for Dashboard"
|
||||
|
||||
[AccessPassword]
|
||||
other = "Contraseña de Acceso al Frontend"
|
||||
|
||||
@@ -650,7 +653,28 @@ other = "Deshabilitar Cambio de Plantilla en Frontend"
|
||||
other = "Servidores en el mapa mundial"
|
||||
|
||||
[NAT]
|
||||
other = "NAT"
|
||||
other = "NAT traversal"
|
||||
|
||||
[NetworkSpiterList]
|
||||
other = "Red Monitor"
|
||||
other = "Monitor de red"
|
||||
|
||||
[Refresh]
|
||||
other = "Actualizar"
|
||||
|
||||
[CopyPath]
|
||||
other = "Copiar ruta"
|
||||
|
||||
[Goto]
|
||||
other = "Ir a"
|
||||
|
||||
[GotoHeadline]
|
||||
other = "Ir a una carpeta"
|
||||
|
||||
[GotoGo]
|
||||
other = "Ir"
|
||||
|
||||
[GotoClose]
|
||||
other = "Cancelar"
|
||||
|
||||
[FMError]
|
||||
other = "Agent devolvió un error, consulte la consola para obtener más detalles. Para abrir una nueva conexión, vuelva a abrir el FM."
|
||||
|
||||
Vendored
+24
@@ -331,6 +331,9 @@ other = "前台界面主题"
|
||||
[CustomCodes]
|
||||
other = "自定义代码(包括 style 和 script)"
|
||||
|
||||
[CustomCodesDashboard]
|
||||
other = "Custom Codes for Dashboard"
|
||||
|
||||
[AccessPassword]
|
||||
other = "前台访问密码"
|
||||
|
||||
@@ -654,3 +657,24 @@ other = "内网穿透"
|
||||
|
||||
[NetworkSpiterList]
|
||||
other = "网络监控"
|
||||
|
||||
[Refresh]
|
||||
other = "刷新"
|
||||
|
||||
[CopyPath]
|
||||
other = "复制路径"
|
||||
|
||||
[Goto]
|
||||
other = "跳往"
|
||||
|
||||
[GotoHeadline]
|
||||
other = "跳往文件夹"
|
||||
|
||||
[GotoGo]
|
||||
other = "确认"
|
||||
|
||||
[GotoClose]
|
||||
other = "取消"
|
||||
|
||||
[FMError]
|
||||
other = "Agent 返回了错误,请查看控制台获取详细信息。要建立新连接,请重新打开 FM。"
|
||||
|
||||
Vendored
+43
-19
@@ -50,7 +50,7 @@ other = "新增計劃任務"
|
||||
other = "名稱"
|
||||
|
||||
[Scheduler]
|
||||
other = "計劃"
|
||||
other = "排程"
|
||||
|
||||
[BackUp]
|
||||
other = "備份"
|
||||
@@ -80,37 +80,37 @@ other = "特定伺服器"
|
||||
other = "輸入ID/名稱以搜尋"
|
||||
|
||||
[NotificationMethodGroup]
|
||||
other = "通知方式組"
|
||||
other = "通知群組"
|
||||
|
||||
[PushSuccessMessages]
|
||||
other = "推送成功的消息"
|
||||
other = "推送成功的訊息"
|
||||
|
||||
[TaskType]
|
||||
other = "任務類型"
|
||||
|
||||
[CronTask]
|
||||
other = "計劃任務"
|
||||
other = "排程任務"
|
||||
|
||||
[TriggerTask]
|
||||
other = "觸發任務"
|
||||
|
||||
[TheFormaOfTheScheduleIs]
|
||||
other = "計劃的格式為:"
|
||||
other = "排程的格式為:"
|
||||
|
||||
[SecondsMinutesHoursDaysMonthsWeeksSeeDetails]
|
||||
other = "秒 分 時 天 月 星期,詳情見"
|
||||
|
||||
[ScheduleExpressionFormat]
|
||||
other = "計劃表達式格式"
|
||||
other = "排程表達式格式"
|
||||
|
||||
[IntroductionOfCommands]
|
||||
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>&</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變量配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
|
||||
other = "命令說明:編寫命令時類似於 shell/bat 腳本。建議不要換行,多個命令可用 <code>&&</code> 或 <code>&</code> 連接,若出現命令無法找到的情況,可能是由於 <code>PATH</code> 環境變數配置問題。在 <code>Linux</code> 伺服器上,可在命令開頭加入 <code>source ~/.bashrc</code>,或使用命令的絕對路徑執行。"
|
||||
|
||||
[AddMonitor]
|
||||
other = "新增監控"
|
||||
|
||||
[Blog]
|
||||
other = "博客"
|
||||
other = "部落格"
|
||||
|
||||
[Target]
|
||||
other = "目標"
|
||||
@@ -158,7 +158,7 @@ other = "新增通知方式"
|
||||
other = "分組"
|
||||
|
||||
[DoNotSendTestMessages]
|
||||
other = "不發送測試信息"
|
||||
other = "不發送測試訊息"
|
||||
|
||||
[RequestMethod]
|
||||
other = "請求方式"
|
||||
@@ -221,7 +221,7 @@ other = "排序"
|
||||
other = "越大越靠前"
|
||||
|
||||
[Secret]
|
||||
other = "密鑰"
|
||||
other = "金鑰"
|
||||
|
||||
[Note]
|
||||
other = "備註"
|
||||
@@ -254,10 +254,10 @@ other = "忽略所有"
|
||||
other = "觸發執行"
|
||||
|
||||
[DeleteScheduledTask]
|
||||
other = "刪除計劃任務"
|
||||
other = "刪除排程任務"
|
||||
|
||||
[ConfirmToDeleteThisScheduledTask]
|
||||
other = "確認刪除此計劃任務?"
|
||||
other = "確認刪除此排程任務?"
|
||||
|
||||
[AccessDenied]
|
||||
other = "訪問被拒絕"
|
||||
@@ -331,6 +331,9 @@ other = "前台界面主題"
|
||||
[CustomCodes]
|
||||
other = "自定義代碼(包括 style 和 script)"
|
||||
|
||||
[CustomCodesDashboard]
|
||||
other = "Custom Codes for Dashboard"
|
||||
|
||||
[AccessPassword]
|
||||
other = "前台訪問密碼"
|
||||
|
||||
@@ -404,7 +407,7 @@ other = "流量"
|
||||
other = "負載"
|
||||
|
||||
[ProcessCount]
|
||||
other = "進程數"
|
||||
other = "行程數"
|
||||
|
||||
[ConnCount]
|
||||
other = "連接數"
|
||||
@@ -419,7 +422,7 @@ other = "活動"
|
||||
other = "版本"
|
||||
|
||||
[NetSpeed]
|
||||
other = "網絡"
|
||||
other = "網路"
|
||||
|
||||
[Uptime]
|
||||
other = "在線"
|
||||
@@ -455,7 +458,7 @@ other = "狀態"
|
||||
other = "可用性"
|
||||
|
||||
[AverageLatency]
|
||||
other = "平均響應時間"
|
||||
other = "平均回應時間"
|
||||
|
||||
[CycleTransferStats]
|
||||
other = "周期性流量統計"
|
||||
@@ -521,7 +524,7 @@ other = "發生錯誤"
|
||||
other = "系統錯誤"
|
||||
|
||||
[NetworkError]
|
||||
other = "網絡錯誤"
|
||||
other = "網路錯誤"
|
||||
|
||||
[ServicesStatus]
|
||||
other = "服務狀態"
|
||||
@@ -533,7 +536,7 @@ other = "伺服器管理"
|
||||
other = "服務監控"
|
||||
|
||||
[ScheduledTasks]
|
||||
other = "計劃任務"
|
||||
other = "排程任務"
|
||||
|
||||
[ApiManagement]
|
||||
other = "API 管理"
|
||||
@@ -611,7 +614,7 @@ other = "對遊客隱藏"
|
||||
other = "菜單"
|
||||
|
||||
[NetworkSpiter]
|
||||
other = "網絡"
|
||||
other = "網路"
|
||||
|
||||
[EnableShowInService]
|
||||
other = "在服務中顯示"
|
||||
@@ -653,4 +656,25 @@ other = "伺服器世界分布圖"
|
||||
other = "NAT"
|
||||
|
||||
[NetworkSpiterList]
|
||||
other = "網絡監控"
|
||||
other = "網路監控"
|
||||
|
||||
[Refresh]
|
||||
other = "重新整理"
|
||||
|
||||
[CopyPath]
|
||||
other = "複製路徑"
|
||||
|
||||
[Goto]
|
||||
other = "跳至"
|
||||
|
||||
[GotoHeadline]
|
||||
other = "跳至資料夾"
|
||||
|
||||
[GotoGo]
|
||||
other = "確定"
|
||||
|
||||
[GotoClose]
|
||||
other = "取消"
|
||||
|
||||
[FMError]
|
||||
other = "Agent 回傳了錯誤,請查看主控台獲取詳細資訊。要建立新連線,請重新開啟 FM。"
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
html[nz-theme='dark'] body {
|
||||
background-color: #121212 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.menu {
|
||||
background-color: #282828 !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.menu * {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .accordion {
|
||||
background-color: #282828 !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .accordion .title {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.card {
|
||||
background-color: #3f3f3f !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .header {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .description {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .icon {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.popup {
|
||||
background-color: #575757 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.table {
|
||||
background-color: #282828 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui thead th {
|
||||
background-color: #3f3f3f !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.buttons .button {
|
||||
background-color: #3f3f3f !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.modal {
|
||||
background-color: #282828 !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.modal * {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] textarea,
|
||||
html[nz-theme='dark'] input,
|
||||
html[nz-theme='dark'] select,
|
||||
html[nz-theme='dark'] .dropdown {
|
||||
background-color: #3f3f3f !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.message {
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.dropdown .menu {
|
||||
background-color: #575757 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.modal>.header {
|
||||
background-color: #3f3f3f !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.modal>.content {
|
||||
background-color: #282828 !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.modal>.actions {
|
||||
background-color: #3f3f3f !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] #alert {
|
||||
background-color: #3f3f3f !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.form .field>label {
|
||||
color: unset !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.segment {
|
||||
background-color: #3f3f3f !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.segment textarea,
|
||||
html[nz-theme='dark'] input,
|
||||
html[nz-theme='dark'] select,
|
||||
html[nz-theme='dark'] .dropdown {
|
||||
background-color: #575757 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] form label {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.inverted.segment {
|
||||
background-color: #121212 !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.inverted.segment * {
|
||||
color: #8b8b8b !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .menu .dropdown {
|
||||
background-color: #282828 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .ui.menu .ui.dropdown .menu>.item {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
html[nz-theme='dark'] .login .ui.message{
|
||||
color: #8b8b8b !important;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
let receivedLength = 0;
|
||||
let expectedLength = 0;
|
||||
let root;
|
||||
let draftHandle;
|
||||
let accessHandle;
|
||||
|
||||
const Operation = Object.freeze({
|
||||
WriteHeader: 1,
|
||||
WriteChunks: 2,
|
||||
DeleteFiles: 3
|
||||
});
|
||||
|
||||
onmessage = async function (event) {
|
||||
try {
|
||||
const { operation, arrayBuffer, fileName } = event.data;
|
||||
|
||||
switch (operation) {
|
||||
case Operation.WriteHeader: {
|
||||
const dataView = new DataView(arrayBuffer);
|
||||
expectedLength = Number(dataView.getBigUint64(4, false));
|
||||
receivedLength = 0;
|
||||
|
||||
// Create a new temporary file
|
||||
root = await navigator.storage.getDirectory();
|
||||
draftHandle = await root.getFileHandle(fileName, { create: true });
|
||||
accessHandle = await draftHandle.createSyncAccessHandle();
|
||||
|
||||
// Inform that file handle is created
|
||||
const dataChunk = arrayBuffer.slice(12);
|
||||
receivedLength += dataChunk.byteLength;
|
||||
accessHandle.write(dataChunk, { at: 0 });
|
||||
const progress = 'got handle';
|
||||
postMessage({ type: 'progress', progress: progress });
|
||||
break;
|
||||
}
|
||||
case Operation.WriteChunks: {
|
||||
if (!accessHandle) {
|
||||
throw new Error('accessHandle is undefined');
|
||||
}
|
||||
|
||||
const dataChunk = arrayBuffer;
|
||||
accessHandle.write(dataChunk, { at: receivedLength });
|
||||
receivedLength += dataChunk.byteLength;
|
||||
|
||||
if (receivedLength === expectedLength) {
|
||||
accessHandle.flush();
|
||||
accessHandle.close();
|
||||
|
||||
const fileBlob = await draftHandle.getFile();
|
||||
const blob = new Blob([fileBlob], { type: 'application/octet-stream' });
|
||||
|
||||
postMessage({ type: 'result', blob: blob, fileName: fileName });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Operation.DeleteFiles: {
|
||||
for await (const [name, handle] of root.entries()) {
|
||||
if (handle.kind === 'file') {
|
||||
await root.removeEntry(name);
|
||||
} else if (handle.kind === 'directory') {
|
||||
await root.removeEntry(name, { recursive: true });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
postMessage({ error: error.message });
|
||||
}
|
||||
};
|
||||
@@ -9,6 +9,11 @@ td {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.ui.container{
|
||||
width: 95vw !important;
|
||||
max-width: 1680px !important;
|
||||
}
|
||||
|
||||
.nb-container {
|
||||
padding-top: 75px;
|
||||
min-height: 100vh;
|
||||
|
||||
+55
-60
@@ -1,46 +1,35 @@
|
||||
/* 屏幕适配 */
|
||||
@media only screen and (min-width:1200px) {
|
||||
.ui.container {
|
||||
width:95% !important;
|
||||
font-size: 90% !important;
|
||||
max-width: 1300px !important;
|
||||
}
|
||||
}
|
||||
@media only screen and (max-width:767px) {
|
||||
.ui.card>.content>.header:not(.ui),.ui.cards>.card>.content>.header:not(.ui) {
|
||||
margin-top:0.4em !important;
|
||||
.ui.card>.content>.header:not(.ui),
|
||||
.ui.cards>.card>.content>.header:not(.ui) {
|
||||
margin-top: 0.4em !important;
|
||||
}
|
||||
.ui.menu .item>img:not(.ui){
|
||||
|
||||
.ui.menu .item>img:not(.ui) {
|
||||
width: 2.2rem;
|
||||
}
|
||||
.ui.menu .item:before{
|
||||
width:0.5px;
|
||||
|
||||
.ui.menu .item:before {
|
||||
width: 0.5px;
|
||||
}
|
||||
.ui.menu .item{
|
||||
|
||||
.ui.menu .item {
|
||||
padding: 0.9rem 0.55rem;
|
||||
}
|
||||
.ui.large.menu{
|
||||
|
||||
.ui.large.menu {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
i.icon {
|
||||
color:#000;
|
||||
width:1.2em !important;
|
||||
}
|
||||
i.fi {
|
||||
width:0.9em;
|
||||
margin:0px 6px 0px 2px;
|
||||
color: #000;
|
||||
width: 1.2em !important;
|
||||
}
|
||||
|
||||
body {
|
||||
content:" " !important;
|
||||
background:fixed !important;
|
||||
z-index:-1 !important;
|
||||
top:0 !important;
|
||||
right:0 !important;
|
||||
bottom:0 !important;
|
||||
left:0 !important;
|
||||
i.fi {
|
||||
width: 0.9em;
|
||||
margin: 0px 6px 0px 2px;
|
||||
}
|
||||
|
||||
td {
|
||||
@@ -55,6 +44,11 @@ td {
|
||||
margin-bottom: -47px;
|
||||
}
|
||||
|
||||
.ui.container {
|
||||
width: 95%;
|
||||
max-width: 1680px !important;
|
||||
}
|
||||
|
||||
#app .ui.fluid.accordion {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -65,79 +59,80 @@ td {
|
||||
padding-top: unset;
|
||||
}
|
||||
|
||||
.login.nb-container > .grid {
|
||||
.login.nb-container>.grid {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.login.nb-container > .grid .column {
|
||||
.login.nb-container>.grid .column {
|
||||
max-width: 450px;
|
||||
}
|
||||
|
||||
.ui.menu .item-right:before{
|
||||
width:0px;
|
||||
.ui.menu .item-right:before {
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.status.cards .flag {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.status.cards .header > .info.icon {
|
||||
.status.cards .header>.info.icon {
|
||||
float: right;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ui.grid {
|
||||
margin-bottom:-0.5em
|
||||
margin-bottom: -0.5em
|
||||
}
|
||||
|
||||
.ui.card>.content>.header:not(.ui), .ui.cards>.card>.content>.header:not(.ui){
|
||||
.ui.card>.content>.header:not(.ui),
|
||||
.ui.cards>.card>.content>.header:not(.ui) {
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
.status.cards .wide.column {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
height:2.3rem !important;
|
||||
height: 2.3rem !important;
|
||||
}
|
||||
|
||||
.status.cards .wide.column:nth-child(1) {
|
||||
margin-top:1.2rem !important;
|
||||
margin-top: 1.2rem !important;
|
||||
}
|
||||
|
||||
.status.cards .wide.column:nth-child(2) {
|
||||
margin-top:1.2rem !important;
|
||||
margin-top: 1.2rem !important;
|
||||
}
|
||||
|
||||
.status.cards .three.wide.column {
|
||||
text-align: center;
|
||||
width: 22%!important;
|
||||
width: 22% !important;
|
||||
}
|
||||
|
||||
.status.cards .thirteen.wide.column{
|
||||
width: 78%!important;
|
||||
padding-left:0;
|
||||
.status.cards .thirteen.wide.column {
|
||||
width: 78% !important;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.status.cards .description {
|
||||
padding-bottom:0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.status.cards .flag {
|
||||
margin-right:0.5rem !important;
|
||||
margin-right: 0.5rem !important;
|
||||
}
|
||||
|
||||
.status.cards .header > .info.icon {
|
||||
.status.cards .header>.info.icon {
|
||||
float: right;
|
||||
margin-right:0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
.ui.popup:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.closePopup{
|
||||
color:rgb(10, 148, 242) !important;
|
||||
.closePopup {
|
||||
color: rgb(10, 148, 242) !important;
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 10px;
|
||||
@@ -150,26 +145,26 @@ td {
|
||||
}
|
||||
|
||||
.ui.content {
|
||||
margin:0 !important;
|
||||
padding:1em !important;
|
||||
margin: 0 !important;
|
||||
padding: 1em !important;
|
||||
}
|
||||
|
||||
.status.cards .ui.content.popup {
|
||||
min-width:calc(100%)!important;
|
||||
line-height:2rem !important;
|
||||
border-radius:5px !important;
|
||||
border:1px solid transparent !important;
|
||||
font-family:Arial,Helvetica,sans-serif !important;
|
||||
min-width: calc(100%) !important;
|
||||
line-height: 2rem !important;
|
||||
border-radius: 5px !important;
|
||||
border: 1px solid transparent !important;
|
||||
font-family: Arial, Helvetica, sans-serif !important;
|
||||
}
|
||||
|
||||
.status.cards .outline.icon {
|
||||
margin-right:1px !important;
|
||||
margin-right: 1px !important;
|
||||
}
|
||||
|
||||
.ui.progress .bar {
|
||||
min-width:1.8em !important;
|
||||
border-radius:5px !important;
|
||||
line-height:1.65em !important;
|
||||
min-width: 1.8em !important;
|
||||
border-radius: 5px !important;
|
||||
line-height: 1.65em !important;
|
||||
text-align: right;
|
||||
padding-right: 0.4em;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
@@ -182,7 +177,7 @@ td {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.service-status .delay-today > i {
|
||||
.service-status .delay-today>i {
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
|
||||
+5
-2
@@ -9,6 +9,10 @@ body {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.container-fluid::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nezha {
|
||||
min-height: calc(100vh - 100px);
|
||||
}
|
||||
@@ -470,14 +474,13 @@ footer p{
|
||||
background-image: url(/static/theme-server-status/img/rb.png);
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
body {
|
||||
font-size: 10px !important;
|
||||
padding-top:60px !important;
|
||||
}
|
||||
.nezha {
|
||||
min-height: calc(100vh - 80px);
|
||||
min-height: calc(74.5vh);
|
||||
}
|
||||
.content {
|
||||
padding: 0;
|
||||
|
||||
Vendored
+1
@@ -17,5 +17,6 @@
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
||||
Vendored
+6
-1
@@ -11,7 +11,12 @@
|
||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/[email protected]/dist/semantic.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="https://unpkg.com/[email protected]/assets/font-logos.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/main.css?v2022042314">
|
||||
<link rel="stylesheet" type="text/css" href="/static/main.css?v20240813">
|
||||
<link rel="stylesheet" type="text/css" href="/static/darkmode.css?v20240813">
|
||||
<script>
|
||||
document.documentElement.setAttribute('nz-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
||||
</script>
|
||||
</head>
|
||||
{{if ts .CustomCodeDashboard}} {{.CustomCodeDashboard|safe}} {{end}}
|
||||
<body>
|
||||
{{end}}
|
||||
Vendored
+1
-1
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class=" actions">
|
||||
<div class="actions">
|
||||
<div class="ui negative button">{{tr "Cancel"}}</div>
|
||||
<button class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
|
||||
</button>
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" actions">
|
||||
<div class="actions">
|
||||
<div class="ui negative button">{{tr "Cancel"}}</div>
|
||||
<button class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
|
||||
</button>
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class=" actions">
|
||||
<div class="actions">
|
||||
<div class="ui negative button">{{tr "Cancel"}}</div>
|
||||
<button class="ui positive nezha-primary-btn right labeled icon button">{{tr "Confirm"}}<i class="checkmark icon"></i>
|
||||
</button>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{tr "Token"}}</th>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
|
||||
+494
@@ -0,0 +1,494 @@
|
||||
{{define "dashboard-default/file"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{.Conf.Language}}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>File List</title>
|
||||
<link rel="shortcut icon" type="image/png" href="/static/logo.svg?v20210804" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/mdui@2/mdui.css" />
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||
<script src="https://unpkg.com/mdui@2/mdui.global.js"></script>
|
||||
</head>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
margin: 5px 0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#top-app-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#current-directory {
|
||||
font-size: 1rem;
|
||||
font-weight: normal;
|
||||
margin: 0 15px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: calc(100% - 100px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<div id="top-app-bar">
|
||||
<mdui-dropdown>
|
||||
<mdui-button-icon slot="trigger" icon="menu"></mdui-button-icon>
|
||||
<mdui-menu>
|
||||
<mdui-menu-item id="refresh">{{tr "Refresh"}}</mdui-menu-item>
|
||||
<mdui-menu-item id="copy">{{tr "CopyPath"}}</mdui-menu-item>
|
||||
<mdui-menu-item id="goto">{{tr "Goto"}}</mdui-menu-item>
|
||||
</mdui-menu>
|
||||
</mdui-dropdown>
|
||||
<span id="current-directory"></span>
|
||||
<mdui-button-icon id="upload" icon="upload"></mdui-button-icon>
|
||||
</div>
|
||||
|
||||
<mdui-list id="file-list" class="file-list"></mdui-list>
|
||||
|
||||
<mdui-dialog id="error-dialog" headline="Error"
|
||||
description="{{tr "FMError"}}"></mdui-dialog>
|
||||
|
||||
<mdui-dialog id="upd-modal" class="modal">
|
||||
<mdui-linear-progress id="upd-progress"></mdui-linear-progress>
|
||||
</mdui-dialog>
|
||||
|
||||
<mdui-dialog id="goto-dialog" headline="{{tr "GotoHeadline"}}" close-on-overlay-click>
|
||||
<mdui-text-field id="goto-text" variant="outlined" value=""></mdui-text-field>
|
||||
<mdui-button id="goto-go" slot="action" variant="text">{{tr "GotoGo"}}</mdui-button>
|
||||
<mdui-button id="goto-close" slot="action" variant="tonal">{{tr "GotoClose"}}</mdui-button>
|
||||
</mdui-dialog>
|
||||
|
||||
<script>
|
||||
let currentPath = '/opt/nezha/';
|
||||
let fileName = '';
|
||||
let receivedBuffer = []; // 用于缓存数据块
|
||||
let expectedLength = 0;
|
||||
let receivedLength = 0;
|
||||
let isFirstChunk = true;
|
||||
let isUpCompleted = false;
|
||||
let handleReady = false;
|
||||
let worker;
|
||||
|
||||
function updateDirectoryTitle() {
|
||||
const directoryTitle = document.getElementById('current-directory');
|
||||
directoryTitle.textContent = `${currentPath}`;
|
||||
}
|
||||
|
||||
function updateFileList(items) {
|
||||
const fileListElement = document.getElementById('file-list');
|
||||
fileListElement.innerHTML = '';
|
||||
|
||||
if (currentPath !== '/') {
|
||||
const upItem = document.createElement('mdui-list-item');
|
||||
upItem.className = 'file-item up-directory';
|
||||
upItem.setAttribute('icon', 'arrow_back');
|
||||
upItem.textContent = "..";
|
||||
upItem.onclick = function () {
|
||||
const lastSlashIndex = currentPath.lastIndexOf('/', currentPath.length - 2);
|
||||
currentPath = currentPath.substring(0, lastSlashIndex + 1) || '/';
|
||||
listFile();
|
||||
};
|
||||
fileListElement.appendChild(upItem);
|
||||
}
|
||||
|
||||
items.sort((a, b) => {
|
||||
if (a.fileType === 'dir' && b.fileType !== 'dir') {
|
||||
return -1;
|
||||
}
|
||||
if (a.fileType !== 'dir' && b.fileType === 'dir') {
|
||||
return 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
items.forEach(item => {
|
||||
const listItem = document.createElement('mdui-list-item');
|
||||
listItem.className = `file-item ${item.fileType.toLowerCase()}`;
|
||||
listItem.setAttribute('nonclickable', 'true');
|
||||
listItem.setAttribute('icon', 'insert_drive_file');
|
||||
listItem.textContent = `${item.name}`;
|
||||
|
||||
if (item.fileType === 'dir') {
|
||||
listItem.setAttribute('nonclickable', 'false');
|
||||
listItem.setAttribute('icon', 'folder');
|
||||
listItem.style.cursor = 'pointer';
|
||||
listItem.onclick = function () {
|
||||
currentPath += `${item.name}/`;
|
||||
listFile();
|
||||
};
|
||||
} else {
|
||||
const downloadButton = document.createElement('mdui-button-icon');
|
||||
downloadButton.setAttribute('slot', 'end-icon');
|
||||
downloadButton.setAttribute('icon', 'download');
|
||||
downloadButton.onclick = function () {
|
||||
const filePath = currentPath + item.name;
|
||||
fileName = item.name;
|
||||
downloadFile(filePath);
|
||||
};
|
||||
listItem.appendChild(downloadButton);
|
||||
}
|
||||
|
||||
fileListElement.appendChild(listItem);
|
||||
});
|
||||
updateDirectoryTitle();
|
||||
}
|
||||
|
||||
function resetUpdState() {
|
||||
receivedBuffer = [];
|
||||
expectedLength = 0;
|
||||
receivedLength = 0;
|
||||
isFirstChunk = true;
|
||||
}
|
||||
|
||||
function downloadFile(filePath) {
|
||||
showUpdModal('d');
|
||||
|
||||
const prefix = new Int8Array([1]); // Request download
|
||||
const filePathMessage = new TextEncoder().encode(filePath);
|
||||
|
||||
const msg = new Int8Array(prefix.length + filePathMessage.length);
|
||||
msg.set(prefix);
|
||||
msg.set(filePathMessage, prefix.length);
|
||||
|
||||
socket.send(msg);
|
||||
}
|
||||
|
||||
function listFile() {
|
||||
const prefix = new Int8Array([0]);
|
||||
const resizeMessage = new TextEncoder().encode(currentPath);
|
||||
|
||||
const msg = new Int8Array(prefix.length + resizeMessage.length);
|
||||
msg.set(prefix);
|
||||
msg.set(resizeMessage, prefix.length);
|
||||
|
||||
socket.send(msg);
|
||||
}
|
||||
|
||||
async function uploadFile(file) {
|
||||
showUpdModal('u');
|
||||
|
||||
const chunkSize = 1048576; // 1MB chunk
|
||||
let offset = 0;
|
||||
|
||||
const filePath = `${currentPath}${file.name}`;
|
||||
const fileSize = file.size;
|
||||
const messageType = 2;
|
||||
|
||||
// Build header (type + file size + path)
|
||||
const filePathBytes = new TextEncoder().encode(filePath);
|
||||
const header = new ArrayBuffer(1 + 8 + filePathBytes.length);
|
||||
const headerView = new DataView(header);
|
||||
|
||||
headerView.setUint8(0, messageType);
|
||||
headerView.setBigUint64(1, BigInt(fileSize), false);
|
||||
|
||||
new Uint8Array(header, 9).set(filePathBytes);
|
||||
|
||||
// Send header
|
||||
socket.send(header);
|
||||
|
||||
// Send data chunks
|
||||
while (offset < fileSize) {
|
||||
const chunk = file.slice(offset, offset + chunkSize);
|
||||
const arrayBuffer = await readFileAsArrayBuffer(chunk);
|
||||
socket.send(arrayBuffer);
|
||||
offset += chunkSize;
|
||||
}
|
||||
|
||||
const checkCompletion = setInterval(() => {
|
||||
if (isUpCompleted) {
|
||||
clearInterval(checkCompletion); // 任务完成后停止检查
|
||||
hideUpdModal();
|
||||
resetUpdState();
|
||||
listFile();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
async function parseFileList(arrayBuffer) {
|
||||
const dataView = new DataView(arrayBuffer);
|
||||
const items = [];
|
||||
let offset = 4;
|
||||
|
||||
const pathLength = dataView.getUint32(offset);
|
||||
offset += 4;
|
||||
|
||||
const pathArray = new Uint8Array(arrayBuffer, offset, pathLength);
|
||||
currentPath = new TextDecoder('utf-8').decode(pathArray);
|
||||
offset += pathLength;
|
||||
|
||||
while (offset < dataView.byteLength) {
|
||||
const fileType = dataView.getUint8(offset);
|
||||
offset += 1;
|
||||
|
||||
const nameLength = dataView.getUint8(offset);
|
||||
offset += 1;
|
||||
|
||||
if (offset + nameLength > dataView.byteLength) {
|
||||
console.error('Error: Name length exceeds buffer size');
|
||||
break;
|
||||
}
|
||||
|
||||
const nameArray = new Uint8Array(arrayBuffer, offset, nameLength);
|
||||
const name = new TextDecoder('utf-8').decode(nameArray);
|
||||
offset += nameLength;
|
||||
|
||||
items.push({
|
||||
fileType: fileType === 0x01 ? 'dir' : 'f',
|
||||
name: name,
|
||||
});
|
||||
}
|
||||
|
||||
return { items };
|
||||
}
|
||||
|
||||
function readFileAsArrayBuffer(blob) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsArrayBuffer(blob);
|
||||
});
|
||||
}
|
||||
|
||||
function concatenateArrayBuffers(buffers) {
|
||||
let totalLength = 0;
|
||||
buffers.forEach(buf => totalLength += buf.byteLength);
|
||||
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
buffers.forEach(buf => {
|
||||
result.set(new Uint8Array(buf), offset);
|
||||
offset += buf.byteLength;
|
||||
});
|
||||
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
function arraysEqual(a, b) {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy text to clipboard: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleError(errMsg) {
|
||||
try {
|
||||
console.error('Received error: ', errMsg);
|
||||
hideUpdModal();
|
||||
const errorDialog = document.getElementById('error-dialog');
|
||||
errorDialog.open = true;
|
||||
if (socket.readyState === WebSocket.OPEN) {
|
||||
socket.close(1000, 'Closing due to error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while handling error and closing WebSocket:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function showUpdModal(operation) {
|
||||
const modal = document.getElementById('upd-modal');
|
||||
modal.open = true;
|
||||
if (operation === 'd') {
|
||||
modal.setAttribute('headline', 'Downloading...');
|
||||
} else if (operation === 'u') {
|
||||
modal.setAttribute('headline', 'Uploading...');
|
||||
}
|
||||
}
|
||||
|
||||
function hideUpdModal() {
|
||||
const modal = document.getElementById('upd-modal');
|
||||
modal.open = false;
|
||||
}
|
||||
|
||||
function waitForHandleReady() {
|
||||
return new Promise(resolve => {
|
||||
const checkReady = () => {
|
||||
if (handleReady) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(checkReady, 10);
|
||||
}
|
||||
};
|
||||
checkReady();
|
||||
});
|
||||
}
|
||||
|
||||
const socket = new WebSocket((window.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/file/' + '{{.SessionID}}');
|
||||
socket.binaryType = 'arraybuffer';
|
||||
|
||||
socket.onmessage = async function (event) {
|
||||
try {
|
||||
const arrayBuffer = event.data;
|
||||
|
||||
if (isFirstChunk) {
|
||||
const identifier = new Uint8Array(arrayBuffer, 0, 4);
|
||||
const fileIdentifier = new Uint8Array([0x4E, 0x5A, 0x54, 0x44]); // NZTD
|
||||
const fileNameIdentifier = new Uint8Array([0x4E, 0x5A, 0x46, 0x4E]); // NZFN
|
||||
const errorIdentifier = new Uint8Array([0x4E, 0x45, 0x52, 0x52]); // NERR
|
||||
const completeIdentifier = new Uint8Array([0x4E, 0x5A, 0x55, 0x50]); // NZUP
|
||||
|
||||
if (arraysEqual(identifier, fileIdentifier)) {
|
||||
worker = new Worker('/static/file.js');
|
||||
worker.onmessage = async function (event) {
|
||||
switch (event.data.type) {
|
||||
case 'error':
|
||||
console.error('Error from worker:', event.data.error);
|
||||
break;
|
||||
case 'progress':
|
||||
handleReady = true;
|
||||
break;
|
||||
case 'result':
|
||||
handleReady = false;
|
||||
const url = URL.createObjectURL(event.data.blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = event.data.fileName;
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// Delete the file in OPFS
|
||||
window.addEventListener('beforeunload', async () => {
|
||||
await worker.postMessage({ operation: 3, arrayBuffer: null, fileName: event.data.fileName });
|
||||
});
|
||||
|
||||
hideUpdModal();
|
||||
resetUpdState();
|
||||
break;
|
||||
}
|
||||
};
|
||||
await worker.postMessage({ operation: 1, arrayBuffer: arrayBuffer, fileName: fileName });
|
||||
isFirstChunk = false;
|
||||
} else if (arraysEqual(identifier, fileNameIdentifier)) {
|
||||
// List files
|
||||
const { items } = await parseFileList(arrayBuffer);
|
||||
updateFileList(items);
|
||||
return;
|
||||
} else if (arraysEqual(identifier, errorIdentifier)) {
|
||||
// Handle error
|
||||
const errBytes = arrayBuffer.slice(4);
|
||||
const errMsg = new TextDecoder('utf-8').decode(errBytes);
|
||||
await handleError(errMsg);
|
||||
return;
|
||||
} else if (arraysEqual(identifier, completeIdentifier)) {
|
||||
// Upload is completed
|
||||
isUpCompleted = true;
|
||||
return;
|
||||
} else {
|
||||
console.log('Unknown identifier');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await waitForHandleReady();
|
||||
await worker.postMessage({ operation: 2, arrayBuffer: arrayBuffer, fileName: fileName });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing received data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onopen = function (event) {
|
||||
listFile();
|
||||
};
|
||||
|
||||
socket.onerror = function (event) {
|
||||
console.error('WebSocket error:', event);
|
||||
};
|
||||
|
||||
socket.onclose = function (event) {
|
||||
console.log('WebSocket connection closed:', event);
|
||||
};
|
||||
|
||||
document.getElementById('refresh').addEventListener('click', listFile);
|
||||
|
||||
document.getElementById('copy').addEventListener('click', async () => {
|
||||
await copyTextToClipboard(currentPath);
|
||||
});
|
||||
|
||||
document.getElementById('goto').addEventListener('click', function () {
|
||||
const dialog = document.getElementById('goto-dialog');
|
||||
const textField = document.getElementById('goto-text');
|
||||
const goButton = document.getElementById('goto-go');
|
||||
const closeButton = document.getElementById('goto-close');
|
||||
|
||||
dialog.open = true;
|
||||
|
||||
// Ensure the path ends with a separator
|
||||
const updateText = function (event) {
|
||||
let text = event.target.value;
|
||||
if (!text.endsWith('/')) {
|
||||
text += '/';
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const handleGoClick = function () {
|
||||
let text = updateText({ target: textField });
|
||||
currentPath = text;
|
||||
listFile();
|
||||
dialog.open = false;
|
||||
};
|
||||
|
||||
textField.removeEventListener('change', updateText);
|
||||
textField.addEventListener('change', updateText);
|
||||
|
||||
goButton.removeEventListener('click', handleGoClick);
|
||||
goButton.addEventListener('click', handleGoClick);
|
||||
|
||||
closeButton.addEventListener("click", () => dialog.open = false);
|
||||
});
|
||||
|
||||
document.getElementById('upload').addEventListener('click', async function () {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.style.display = 'none';
|
||||
|
||||
fileInput.addEventListener('change', async function (event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
await uploadFile(file);
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(fileInput);
|
||||
fileInput.click();
|
||||
document.body.removeChild(fileInput);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
@@ -52,7 +52,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="ui very basic table">
|
||||
<table class="ui basic table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><button onclick="checkAllServer()" class="ui mini nezha-primary-btn button">{{tr "SelectAll"}}</button></th>
|
||||
|
||||
+7
-3
@@ -41,6 +41,10 @@
|
||||
<label>{{tr "CustomCodes"}}</label>
|
||||
<textarea name="CustomCode">{{.Conf.Site.CustomCode}}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "CustomCodesDashboard"}}</label>
|
||||
<textarea name="CustomCodeDashboard">{{.Conf.Site.CustomCodeDashboard}}</textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{tr "AccessPassword"}}</label>
|
||||
<input type="text" name="ViewPassword" placeholder="" value="{{.Conf.Site.ViewPassword}}">
|
||||
@@ -128,13 +132,13 @@
|
||||
})
|
||||
$('.checkbox').checkbox()
|
||||
$('#settingForm').find("select[name=Cover]").val({{.Conf.Cover }});
|
||||
{{if .Conf.EnableIPChangeNotification}}
|
||||
{{ if .Conf.EnableIPChangeNotification}}
|
||||
$('.checkbox.ip-change').checkbox('set checked')
|
||||
{{ end }}
|
||||
{{if .Conf.EnablePlainIPInNotification}}
|
||||
{{ if .Conf.EnablePlainIPInNotification}}
|
||||
$('.checkbox.plain-ip').checkbox('set checked')
|
||||
{{ end }}
|
||||
{{if .Conf.DisableSwitchTemplateInFrontend }}
|
||||
{{ if .Conf.DisableSwitchTemplateInFrontend }}
|
||||
$('.checkbox.disable-switch-template').checkbox('set checked')
|
||||
{{ end }}
|
||||
</script>
|
||||
|
||||
+64
-2
@@ -1,6 +1,7 @@
|
||||
{{define "dashboard-default/terminal"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{.Conf.Language}}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
@@ -22,17 +23,53 @@
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#file-list-iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: white;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#folder-button {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
font-size: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
z-index: 20;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body onresize="onResize()">
|
||||
<div id="terminal-container"></div>
|
||||
<iframe id="file-list-iframe" src=""></iframe>
|
||||
<button id="folder-button">📁</button>
|
||||
|
||||
<script src="https://unpkg.com/[email protected]/lib/xterm.js"></script>
|
||||
<script src="https://unpkg.com/@xterm/[email protected]/lib/addon-attach.js"></script>
|
||||
<script src="https://unpkg.com/@xterm/[email protected]/lib/addon-fit.js"></script>
|
||||
<script src="https://unpkg.com/@xterm/[email protected]/lib/addon-web-links.js"></script>
|
||||
<script src="https://unpkg.com/@xterm/[email protected]/lib/addon-attach.js"></script>
|
||||
<script>
|
||||
let sendResizing = false;
|
||||
|
||||
function doResize() {
|
||||
fitAddon.fit()
|
||||
|
||||
const w = fitAddon.proposeDimensions();
|
||||
const prefix = new Int8Array([1]);
|
||||
const resizeMessage = new TextEncoder().encode(JSON.stringify({
|
||||
@@ -70,10 +107,13 @@
|
||||
cursorBlink: true,
|
||||
});
|
||||
const socket = new WebSocket((window.location.protocol == 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/terminal/' + '{{.SessionID}}');
|
||||
|
||||
const attachAddon = new AttachAddon.AttachAddon(socket);
|
||||
const fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(attachAddon);
|
||||
|
||||
const fitAddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
|
||||
term.open(document.getElementById('terminal-container'));
|
||||
|
||||
socket.onopen = () => {
|
||||
@@ -88,7 +128,29 @@
|
||||
socket.onerror = () => {
|
||||
alert('{{tr "TerminalConnectionFailed"}}')
|
||||
}
|
||||
|
||||
// 处理文件夹按钮点击事件
|
||||
const folderButton = document.getElementById('folder-button');
|
||||
const fileListIframe = document.getElementById('file-list-iframe');
|
||||
let fileListVisible = false;
|
||||
|
||||
folderButton.addEventListener('click', () => {
|
||||
if (!fileListVisible) {
|
||||
// 显示文件列表
|
||||
const params = new URLSearchParams({
|
||||
id: "{{.ServerID}}"
|
||||
}).toString();
|
||||
fileListIframe.src = `/file?${params}`;
|
||||
fileListIframe.style.display = 'block';
|
||||
fileListVisible = true;
|
||||
} else {
|
||||
// 隐藏文件列表
|
||||
fileListIframe.style.display = 'none';
|
||||
fileListVisible = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
||||
+2
-4
@@ -10,14 +10,12 @@
|
||||
{{ if not .Conf.DisableSwitchTemplateInFrontend }}
|
||||
<script>
|
||||
function showSwitchTemplate(list, currentBackendTheme) {
|
||||
console.log(list, currentBackendTheme);
|
||||
console.log("currentBackendTheme:",currentBackendTheme);
|
||||
// console.log(list, currentBackendTheme);
|
||||
// console.log("currentBackendTheme:",currentBackendTheme);
|
||||
}
|
||||
showSwitchTemplate({{ .Themes }}, {{ .Conf.Site.Theme }})
|
||||
</script>
|
||||
{{ end }}
|
||||
<script>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
+8
-3
@@ -1,6 +1,7 @@
|
||||
{{define "theme-default/header"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{.Conf.Language}}">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -13,14 +14,18 @@
|
||||
<link rel="stylesheet" href="https://unpkg.com/[email protected]/font/bootstrap-icons.min.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/[email protected]/css/flag-icons.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/semantic-ui-alerts.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/theme-default/css/main.css?v20240616">
|
||||
<link rel="stylesheet" type="text/css" href="/static/theme-default/css/main.css?v20240816">
|
||||
<link rel="stylesheet" type="text/css" href="/static/darkmode.css?v20240816">
|
||||
<script src="https://unpkg.com/[email protected]/dist/jquery.min.js"></script>
|
||||
<script src="https://unpkg.com/[email protected]/dist/semantic.min.js"></script>
|
||||
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
|
||||
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
|
||||
<script src="/static/semantic-ui-alerts.min.js"></script>
|
||||
<script src="/static/theme-default/js/mixin.js?v20240302"></script>
|
||||
<script>
|
||||
document.documentElement.setAttribute('nz-theme', window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{end}}
|
||||
|
||||
{{end}}
|
||||
+2
-5
@@ -240,12 +240,12 @@
|
||||
const itemHeight = isMobile ? 10 : 10;
|
||||
const gridLeft = 25;
|
||||
const gridRight = 12;
|
||||
const fontColor = "rgba(0, 0, 0, 0.68)";
|
||||
const backgroundColor = '';
|
||||
const borderColor = "#ffffff";
|
||||
const chartData = this.chartDataList[id - 1];
|
||||
const chartContainer = this.$refs[`chart${id}`][0];
|
||||
const chart = echarts.init(chartContainer, null, {
|
||||
const chartTheme = $('html').attr('nz-theme') == "dark" ? "dark" : "";
|
||||
const chart = echarts.init(chartContainer, chartTheme, {
|
||||
renderer: 'canvas',
|
||||
useDirtyRect: false,
|
||||
width: 'auto',
|
||||
@@ -283,7 +283,6 @@
|
||||
trigger: 'axis',
|
||||
textStyle: {
|
||||
fontSize: fontSize,
|
||||
color: fontColor
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
@@ -292,7 +291,6 @@
|
||||
show: true,
|
||||
textStyle: {
|
||||
fontSize: fontSize,
|
||||
color: fontColor
|
||||
},
|
||||
lineStyle: {
|
||||
cap: 'butt'
|
||||
@@ -332,7 +330,6 @@
|
||||
series: seriesData,
|
||||
textStyle: {
|
||||
fontSize: fontSize,
|
||||
color: fontColor
|
||||
},
|
||||
grid: {
|
||||
top: '30',
|
||||
|
||||
+6
-5
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui container">
|
||||
<div ref="chartDom" style="border-radius: 28px; margin-top: 15px;height: 520px;max-width: 1400px;overflow: hidden"></div>
|
||||
<div ref="chartDom" style="margin-top: 15px;height: 520px;overflow: hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -65,10 +65,10 @@
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '8%',
|
||||
right: '8%',
|
||||
left: this.isMobile ? '8%' : '3.8%',
|
||||
right: this.isMobile ? '8%' : '3.8%',
|
||||
},
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.8)',
|
||||
backgroundColor: '',
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
@@ -243,7 +243,8 @@
|
||||
return str.includes('Windows')
|
||||
},
|
||||
renderChart() {
|
||||
this.myChart = echarts.init(this.$refs.chartDom);
|
||||
const chartTheme = $('html').attr('nz-theme') == "dark" ? "dark" : "";
|
||||
this.myChart = echarts.init(this.$refs.chartDom,chartTheme);
|
||||
this.myChart.setOption(this.option);
|
||||
},
|
||||
resizeHandle () {
|
||||
|
||||
+32
-32
@@ -1,36 +1,36 @@
|
||||
{{define "theme-server-status/footer"}}
|
||||
</div>
|
||||
<footer class="container-fluid">
|
||||
<p>{{ .Conf.Site.Brand }} | Theme ServerStatus | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}</p>
|
||||
</footer>
|
||||
<aside class="toolbox">
|
||||
<template v-if="showTools">
|
||||
<span v-if="page=='index' && countryMapChartData.length!=0" class="showMapChart">
|
||||
<i @click="showMapChart" data-toggle="modal" data-target="#mapChartBox" class="bi bi-geo-alt"></i>
|
||||
</span>
|
||||
<span class="toggleView">
|
||||
<i v-if="showGroup" @click="toggleShowGroup" class="show-nogroup bi bi-justify"></i>
|
||||
<i v-else @click="toggleShowGroup" class="show-group bi bi-view-stacked"></i>
|
||||
</span>
|
||||
<span class="toggleSemiTransparent" @click="toggleSemiTransparent">
|
||||
<i class="bi" :class="semiTransparent ? 'bi-droplet' : 'bi-droplet-half'"></i>
|
||||
</span>
|
||||
</template>
|
||||
<span v-if="!showTools">
|
||||
<i @click="toggleShowTools" class="bi bi-three-dots"></i>
|
||||
</span>
|
||||
<span class="setTheme">
|
||||
<i v-if="theme === 'light'" @click="setTheme('dark')" class="setTheme-dark bi bi-moon-fill"></i>
|
||||
<i v-else @click="setTheme('light')" class="setTheme-light bi bi-brightness-high-fill"></i>
|
||||
</span>
|
||||
<span v-if="showGoTop" class="showGoTop">
|
||||
<i @click="goTop" class="goTop bi bi-arrow-up"></i>
|
||||
</span>
|
||||
</aside>
|
||||
<template v-if="semiTransparent">
|
||||
<link rel="stylesheet" href="/static/theme-server-status/css/light.plus.css?v20240807">
|
||||
<link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20240807">
|
||||
</template>
|
||||
</div>
|
||||
<footer class="container-fluid">
|
||||
<p>{{ .Conf.Site.Brand }} | Theme ServerStatus | Powered by <a target="_blank" href="https://github.com/naiba/nezha">{{tr "NezhaMonitoring"}}</a> {{.Version}}</p>
|
||||
</footer>
|
||||
<aside class="toolbox">
|
||||
<template v-if="showTools">
|
||||
<span v-if="page=='index' && countryMapChartData.length!=0" class="showMapChart">
|
||||
<i @click="showMapChart" data-toggle="modal" data-target="#mapChartBox" class="bi bi-geo-alt"></i>
|
||||
</span>
|
||||
<span class="toggleView">
|
||||
<i v-if="showGroup" @click="toggleShowGroup" class="show-nogroup bi bi-justify"></i>
|
||||
<i v-else @click="toggleShowGroup" class="show-group bi bi-view-stacked"></i>
|
||||
</span>
|
||||
<span class="toggleSemiTransparent" @click="toggleSemiTransparent">
|
||||
<i class="bi" :class="semiTransparent ? 'bi-droplet' : 'bi-droplet-half'"></i>
|
||||
</span>
|
||||
</template>
|
||||
<span v-if="!showTools">
|
||||
<i @click="toggleShowTools" class="bi bi-three-dots"></i>
|
||||
</span>
|
||||
<span class="setTheme">
|
||||
<i v-if="theme === 'light'" @click="setTheme('dark')" class="setTheme-dark bi bi-moon-fill"></i>
|
||||
<i v-else @click="setTheme('light')" class="setTheme-light bi bi-brightness-high-fill"></i>
|
||||
</span>
|
||||
<span v-if="showGoTop" class="showGoTop">
|
||||
<i @click="goTop" class="goTop bi bi-arrow-up"></i>
|
||||
</span>
|
||||
</aside>
|
||||
<template v-if="semiTransparent">
|
||||
<link rel="stylesheet" href="/static/theme-server-status/css/light.plus.css?v20240807">
|
||||
<link rel="stylesheet" href="/static/theme-server-status/css/dark.plus.css?v20240807">
|
||||
</template>
|
||||
</div>
|
||||
{{if ts .CustomCode}}{{.CustomCode|safe}}{{end}}
|
||||
</body>
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
<script src="https://unpkg.com/[email protected]/dist/js/bootstrap.min.js"></script>
|
||||
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
|
||||
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v202408011">
|
||||
<link rel="stylesheet" href="/static/theme-server-status/css/main.css?v202408016">
|
||||
<link rel="stylesheet" href="/static/theme-server-status/css/dark.css?v202408011">
|
||||
<link rel="stylesheet" href="/static/theme-server-status/css/light.css?v20240811">
|
||||
<script src="/static/theme-server-status/js/mixin.js?v20240811"></script>
|
||||
|
||||
+5
-1
@@ -551,6 +551,10 @@
|
||||
symbol: 'none',
|
||||
data: data,
|
||||
connectNulls: true,
|
||||
legendHoverLink: false,
|
||||
emphasis: {
|
||||
disabled: true
|
||||
},
|
||||
lineStyle: {
|
||||
width: lineStyleWidth
|
||||
}
|
||||
@@ -560,7 +564,7 @@
|
||||
const legendData = chartData.map(item => item.monitor_name);
|
||||
const maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 3;
|
||||
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
|
||||
const autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28)
|
||||
const autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28);
|
||||
const height = 300 + autoIncrement;
|
||||
const gridTop = 40 + autoIncrement;
|
||||
const legendIcon = this.isMobile ? 'rect' : "";
|
||||
|
||||
+34
-36
@@ -12,7 +12,7 @@
|
||||
</li>
|
||||
<li class="dropdown-item" v-for="server in servers" @click="showCharts(server.ID)">
|
||||
<a><i :class="'fi fi-' + (server.Host.CountryCode || 'rb')"></i> @#server.Name#@ <i v-if="server.ID == currentServerId" class="check icon"></i></a>
|
||||
</li>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="chartTitle"><i class="chartCountryCode" :class="'fi fi-' + chartCountryCode"></i> @#chartTitle#@</div>
|
||||
@@ -44,7 +44,6 @@
|
||||
},
|
||||
methods: {
|
||||
showCharts(id) {
|
||||
const chartContainer = document.getElementById('chartbox');
|
||||
// 发起数据请求
|
||||
const url = `/api/v1/monitor/${id}`;
|
||||
fetch(url)
|
||||
@@ -64,17 +63,13 @@
|
||||
});
|
||||
},
|
||||
renderCharts(id, reload = false) {
|
||||
if (!this.chartDataList[id]) return;
|
||||
this.disposeCharts();
|
||||
if(!this.chartDataList[id]) return;
|
||||
if(this.chart) this.disposeCharts(this.chart);
|
||||
this.currentServerId = id;
|
||||
this.chartCountryCode = this.getServerCountryCode(id);
|
||||
this.chartTitle = this.chartDataList[id][0].server_name;
|
||||
const chartData = this.chartDataList[id];
|
||||
const chartContainer = document.getElementById('chartbox');
|
||||
this.chartTitle = chartData[0].server_name;
|
||||
if (reload) {
|
||||
const existingChart = echarts.getInstanceByDom(chartContainer);
|
||||
if (existingChart) existingChart.dispose();
|
||||
}
|
||||
// 定义图表参数值
|
||||
const MaxTCPPingValue = {{.Conf.MaxTCPPingValue}} ? {{.Conf.MaxTCPPingValue}} : 300;
|
||||
const autoheight = this.isMobile ? (window.innerHeight - 200) : (window.innerHeight - 250);
|
||||
@@ -111,34 +106,35 @@
|
||||
let data = { main: [], markLine: []};
|
||||
item.avg_delay.forEach((avgDelay, index) => {
|
||||
const threshold = 0.9 * MaxTCPPingValue; // 定义阀值,用于判断是否丢包
|
||||
const filterAvgDelay = item.avg_delay.filter(value => value !== 0 && value !== MaxTCPPingValue);
|
||||
const max = Math.max(...filterAvgDelay).toFixed(1);
|
||||
const autoAvgDelay = 1.05 * max > 0.91 * MaxTCPPingValue ? 1.05 * max : 0.91 * MaxTCPPingValue;
|
||||
// 定义丢包 1. avgDelay==0 2. avgDelay>=MaxTCPPingValue 3. avgDelay>=threshold
|
||||
if(avgDelay == 0 || avgDelay >= MaxTCPPingValue){ //绝对丢包
|
||||
loss += 1;
|
||||
const lossrate = 100 * loss / (index + 1);
|
||||
data['main'].push(
|
||||
[item.created_at[index], autoAvgDelay, lossrate]
|
||||
);
|
||||
data['markLine'].push({
|
||||
xAxis: item.created_at[index],
|
||||
label: { show: false },
|
||||
emphasis: { disabled: true },
|
||||
lineStyle: { type: "solid" }
|
||||
});
|
||||
if(lossrate != 100) {
|
||||
data['main'].push(
|
||||
[item.created_at[index], MaxTCPPingValue, lossrate]
|
||||
);
|
||||
data['markLine'].push({
|
||||
xAxis: item.created_at[index],
|
||||
label: { show: false },
|
||||
emphasis: { disabled: true },
|
||||
lineStyle: { type: "solid" }
|
||||
});
|
||||
}
|
||||
} else if (avgDelay >= threshold && avgDelay < MaxTCPPingValue){ // 相对丢包
|
||||
loss += 1;
|
||||
const lossrate = 100 * loss / (index + 1);
|
||||
data['main'].push(
|
||||
[item.created_at[index], avgDelay, lossrate]
|
||||
);
|
||||
data['markLine'].push({
|
||||
xAxis: item.created_at[index],
|
||||
label: { show: false },
|
||||
emphasis: { disabled: true },
|
||||
lineStyle: { type: "solid" }
|
||||
});
|
||||
if(lossrate != 100) {
|
||||
data['main'].push(
|
||||
[item.created_at[index], avgDelay, lossrate]
|
||||
);
|
||||
data['markLine'].push({
|
||||
xAxis: item.created_at[index],
|
||||
label: { show: false },
|
||||
emphasis: { disabled: true },
|
||||
lineStyle: { type: "solid" }
|
||||
});
|
||||
}
|
||||
} else { // 未丢包
|
||||
const lossrate = 100 * loss / (index + 1);
|
||||
data['main'].push(
|
||||
@@ -158,6 +154,10 @@
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
connectNulls: true,
|
||||
legendHoverLink: false,
|
||||
emphasis: {
|
||||
disabled: true
|
||||
},
|
||||
lineStyle: {
|
||||
width: lineStyleWidth
|
||||
},
|
||||
@@ -216,7 +216,7 @@
|
||||
});
|
||||
const maxLegendsPerRowMobile = localStorage.getItem("maxLegendsPerRowMobile") ? localStorage.getItem("maxLegendsPerRowMobile") : 3;
|
||||
const maxLegendsPerRowPc = localStorage.getItem("maxLegendsPerRowPc") ? localStorage.getItem("maxLegendsPerRowPc") : 6;
|
||||
const autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28)
|
||||
const autoIncrement = Math.floor((legendData.length - 1) / (this.isMobile ? maxLegendsPerRowMobile : maxLegendsPerRowPc)) * (this.isMobile ? 20 : 28);
|
||||
const height = autoheight + autoIncrement;
|
||||
const gridTop = 40 + autoIncrement;
|
||||
const legendIcon = this.isMobile ? 'rect' : "";
|
||||
@@ -333,11 +333,9 @@
|
||||
}
|
||||
});
|
||||
},
|
||||
disposeCharts(){
|
||||
if(this.chart) {
|
||||
this.chart.dispose();
|
||||
this.chart = null;
|
||||
}
|
||||
disposeCharts(chart){
|
||||
chart.dispose();
|
||||
chart = null;
|
||||
},
|
||||
getServerCountryCode(id){
|
||||
const result = this.servers.find(item => item.ID == id);
|
||||
|
||||
@@ -117,7 +117,7 @@ LOOP:
|
||||
endCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
_, innerErr := io.Copy(stream.userIo, stream.agentIo)
|
||||
_, innerErr := io.CopyBuffer(stream.userIo, stream.agentIo, make([]byte, 1048576))
|
||||
if innerErr != nil {
|
||||
err = innerErr
|
||||
}
|
||||
@@ -126,7 +126,7 @@ LOOP:
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
_, innerErr := io.Copy(stream.agentIo, stream.userIo)
|
||||
_, innerErr := io.CopyBuffer(stream.agentIo, stream.userIo, make([]byte, 1048576))
|
||||
if innerErr != nil {
|
||||
err = innerErr
|
||||
}
|
||||
|
||||
+17
-13
@@ -90,12 +90,14 @@ func (s *NezhaHandler) RequestTask(h *pb.Host, stream pb.NezhaService_RequestTas
|
||||
}
|
||||
closeCh := make(chan error)
|
||||
singleton.ServerLock.RLock()
|
||||
singleton.ServerList[clientID].TaskCloseLock.Lock()
|
||||
// 修复不断的请求 task 但是没有 return 导致内存泄漏
|
||||
if singleton.ServerList[clientID].TaskClose != nil {
|
||||
close(singleton.ServerList[clientID].TaskClose)
|
||||
}
|
||||
singleton.ServerList[clientID].TaskStream = stream
|
||||
singleton.ServerList[clientID].TaskClose = closeCh
|
||||
singleton.ServerList[clientID].TaskCloseLock.Unlock()
|
||||
singleton.ServerLock.RUnlock()
|
||||
return <-closeCh
|
||||
}
|
||||
@@ -112,10 +114,10 @@ func (s *NezhaHandler) ReportSystemState(c context.Context, r *pb.State) (*pb.Re
|
||||
singleton.ServerList[clientID].LastActive = time.Now()
|
||||
singleton.ServerList[clientID].State = &state
|
||||
|
||||
// 如果从未记录过,先打点,等到小时时间点时入库
|
||||
if singleton.ServerList[clientID].PrevHourlyTransferIn == 0 || singleton.ServerList[clientID].PrevHourlyTransferOut == 0 {
|
||||
singleton.ServerList[clientID].PrevHourlyTransferIn = int64(state.NetInTransfer)
|
||||
singleton.ServerList[clientID].PrevHourlyTransferOut = int64(state.NetOutTransfer)
|
||||
// 应对 dashboard 重启的情况,如果从未记录过,先打点,等到小时时间点时入库
|
||||
if singleton.ServerList[clientID].PrevTransferInSnapshot == 0 || singleton.ServerList[clientID].PrevTransferOutSnapshot == 0 {
|
||||
singleton.ServerList[clientID].PrevTransferInSnapshot = int64(state.NetInTransfer)
|
||||
singleton.ServerList[clientID].PrevTransferOutSnapshot = int64(state.NetOutTransfer)
|
||||
}
|
||||
|
||||
return &pb.Receipt{Proced: true}, nil
|
||||
@@ -135,9 +137,8 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
|
||||
// 检查并更新DDNS
|
||||
if singleton.Conf.DDNS.Enable &&
|
||||
singleton.ServerList[clientID].EnableDDNS &&
|
||||
singleton.ServerList[clientID].Host != nil &&
|
||||
host.IP != "" &&
|
||||
singleton.ServerList[clientID].Host.IP != host.IP {
|
||||
(singleton.ServerList[clientID].Host == nil || singleton.ServerList[clientID].Host.IP != host.IP) {
|
||||
serverDomain := singleton.ServerList[clientID].DDNSDomain
|
||||
if singleton.Conf.DDNS.Provider == "" {
|
||||
provider, err = singleton.GetDDNSProviderFromProfile(singleton.ServerList[clientID].DDNSProfile)
|
||||
@@ -158,16 +159,15 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
|
||||
|
||||
} else {
|
||||
// 虽然会在启动时panic, 可以断言不会走这个分支, 但是考虑到动态加载配置或者其它情况, 这里输出一下方便检查奇奇怪怪的BUG
|
||||
log.Printf("NEZHA>> 未找到对应的DDNS配置(%s), 或者是provider填写不正确, 请前往config.yml检查你的设置\n", singleton.ServerList[clientID].DDNSProfile)
|
||||
log.Printf("NEZHA>> 未找到对应的DDNS配置(%s), 或者是provider填写不正确, 请前往config.yml检查你的设置", singleton.ServerList[clientID].DDNSProfile)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 发送IP变动通知
|
||||
if singleton.Conf.EnableIPChangeNotification &&
|
||||
if singleton.ServerList[clientID].Host != nil && singleton.Conf.EnableIPChangeNotification &&
|
||||
((singleton.Conf.Cover == model.ConfigCoverAll && !singleton.Conf.IgnoredIPNotificationServerIDs[clientID]) ||
|
||||
(singleton.Conf.Cover == model.ConfigCoverIgnoreAll && singleton.Conf.IgnoredIPNotificationServerIDs[clientID])) &&
|
||||
singleton.ServerList[clientID].Host != nil &&
|
||||
singleton.ServerList[clientID].Host.IP != "" &&
|
||||
host.IP != "" &&
|
||||
singleton.ServerList[clientID].Host.IP != host.IP {
|
||||
@@ -184,10 +184,14 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
|
||||
nil)
|
||||
}
|
||||
|
||||
// 判断是否是机器重启,如果是机器重启要录入最后记录的流量里面
|
||||
if singleton.ServerList[clientID].Host.BootTime < host.BootTime {
|
||||
singleton.ServerList[clientID].PrevHourlyTransferIn = singleton.ServerList[clientID].PrevHourlyTransferIn - int64(singleton.ServerList[clientID].State.NetInTransfer)
|
||||
singleton.ServerList[clientID].PrevHourlyTransferOut = singleton.ServerList[clientID].PrevHourlyTransferOut - int64(singleton.ServerList[clientID].State.NetOutTransfer)
|
||||
/**
|
||||
* 这里的 singleton 中的数据都是关机前的旧数据
|
||||
* 当 agent 重启时,bootTime 变大,agent 端会先上报 host 信息,然后上报 state 信息
|
||||
* 这是可以借助上报顺序的空档,将停机前的流量统计数据标记下来,加到下一个小时的数据点上
|
||||
*/
|
||||
if singleton.ServerList[clientID].Host != nil && singleton.ServerList[clientID].Host.BootTime < host.BootTime {
|
||||
singleton.ServerList[clientID].PrevTransferInSnapshot = singleton.ServerList[clientID].PrevTransferInSnapshot - int64(singleton.ServerList[clientID].State.NetInTransfer)
|
||||
singleton.ServerList[clientID].PrevTransferOutSnapshot = singleton.ServerList[clientID].PrevTransferOutSnapshot - int64(singleton.ServerList[clientID].State.NetOutTransfer)
|
||||
}
|
||||
|
||||
// 不要冲掉国家码
|
||||
|
||||
+39
-53
@@ -3,86 +3,72 @@ package singleton
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
|
||||
ddns2 "github.com/naiba/nezha/pkg/ddns"
|
||||
)
|
||||
|
||||
func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig, maxRetries int) bool {
|
||||
if nil == config {
|
||||
return false
|
||||
const (
|
||||
ProviderWebHook = "webhook"
|
||||
ProviderCloudflare = "cloudflare"
|
||||
ProviderTencentCloud = "tencentcloud"
|
||||
)
|
||||
|
||||
type ProviderFunc func(*ddns2.DomainConfig) ddns2.Provider
|
||||
|
||||
func RetryableUpdateDomain(provider ddns2.Provider, domainConfig *ddns2.DomainConfig, maxRetries int) {
|
||||
if domainConfig == nil {
|
||||
return
|
||||
}
|
||||
for retries := 0; retries < maxRetries; retries++ {
|
||||
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)\n", config.FullDomain, retries+1, maxRetries)
|
||||
if provider.UpdateDomain(config) {
|
||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功\n", config.FullDomain)
|
||||
return true
|
||||
log.Printf("NEZHA>> 正在尝试更新域名(%s)DDNS(%d/%d)", domainConfig.FullDomain, retries+1, maxRetries)
|
||||
if err := provider.UpdateDomain(domainConfig); err != nil {
|
||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败: %v", domainConfig.FullDomain, err)
|
||||
} else {
|
||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS成功", domainConfig.FullDomain)
|
||||
break
|
||||
}
|
||||
}
|
||||
log.Printf("NEZHA>> 尝试更新域名(%s)DDNS失败\n", config.FullDomain)
|
||||
return false
|
||||
}
|
||||
|
||||
// Deprecated
|
||||
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) {
|
||||
switch provider {
|
||||
case "webhook":
|
||||
return &ddns2.ProviderWebHook{
|
||||
URL: Conf.DDNS.WebhookURL,
|
||||
RequestMethod: Conf.DDNS.WebhookMethod,
|
||||
RequestBody: Conf.DDNS.WebhookRequestBody,
|
||||
RequestHeader: Conf.DDNS.WebhookHeaders,
|
||||
}, nil
|
||||
case "dummy":
|
||||
return &ddns2.ProviderDummy{}, nil
|
||||
case "cloudflare":
|
||||
return &ddns2.ProviderCloudflare{
|
||||
Secret: Conf.DDNS.AccessSecret,
|
||||
}, nil
|
||||
case "tencentcloud":
|
||||
return &ddns2.ProviderTencentCloud{
|
||||
SecretID: Conf.DDNS.AccessID,
|
||||
SecretKey: Conf.DDNS.AccessSecret,
|
||||
}, nil
|
||||
case ProviderWebHook:
|
||||
return ddns2.NewProviderWebHook(Conf.DDNS.WebhookURL, Conf.DDNS.WebhookMethod, Conf.DDNS.WebhookRequestBody, Conf.DDNS.WebhookHeaders), nil
|
||||
case ProviderCloudflare:
|
||||
return ddns2.NewProviderCloudflare(Conf.DDNS.AccessSecret), nil
|
||||
case ProviderTencentCloud:
|
||||
return ddns2.NewProviderTencentCloud(Conf.DDNS.AccessID, Conf.DDNS.AccessSecret), nil
|
||||
default:
|
||||
return new(ddns2.ProviderDummy), fmt.Errorf("无法找到配置的DDNS提供者 %s", provider)
|
||||
}
|
||||
return &ddns2.ProviderDummy{}, fmt.Errorf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider)
|
||||
}
|
||||
|
||||
func GetDDNSProviderFromProfile(profileName string) (ddns2.Provider, error) {
|
||||
profile, ok := Conf.DDNS.Profiles[profileName]
|
||||
if !ok {
|
||||
return &ddns2.ProviderDummy{}, fmt.Errorf("未找到配置项 %s", profileName)
|
||||
return new(ddns2.ProviderDummy), fmt.Errorf("未找到配置项 %s", profileName)
|
||||
}
|
||||
|
||||
switch profile.Provider {
|
||||
case "webhook":
|
||||
return &ddns2.ProviderWebHook{
|
||||
URL: profile.WebhookURL,
|
||||
RequestMethod: profile.WebhookMethod,
|
||||
RequestBody: profile.WebhookRequestBody,
|
||||
RequestHeader: profile.WebhookHeaders,
|
||||
}, nil
|
||||
case "dummy":
|
||||
return &ddns2.ProviderDummy{}, nil
|
||||
case "cloudflare":
|
||||
return &ddns2.ProviderCloudflare{
|
||||
Secret: profile.AccessSecret,
|
||||
}, nil
|
||||
case "tencentcloud":
|
||||
return &ddns2.ProviderTencentCloud{
|
||||
SecretID: profile.AccessID,
|
||||
SecretKey: profile.AccessSecret,
|
||||
}, nil
|
||||
case ProviderWebHook:
|
||||
return ddns2.NewProviderWebHook(profile.WebhookURL, profile.WebhookMethod, profile.WebhookRequestBody, profile.WebhookHeaders), nil
|
||||
case ProviderCloudflare:
|
||||
return ddns2.NewProviderCloudflare(profile.AccessSecret), nil
|
||||
case ProviderTencentCloud:
|
||||
return ddns2.NewProviderTencentCloud(profile.AccessID, profile.AccessSecret), nil
|
||||
default:
|
||||
return new(ddns2.ProviderDummy), fmt.Errorf("无法找到配置的DDNS提供者 %s", profile.Provider)
|
||||
}
|
||||
return &ddns2.ProviderDummy{}, fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
|
||||
}
|
||||
|
||||
func ValidateDDNSProvidersFromProfiles() error {
|
||||
validProviders := map[string]bool{"webhook": true, "dummy": true, "cloudflare": true, "tencentcloud": true}
|
||||
providers := make(map[string]string)
|
||||
for profileName, profile := range Conf.DDNS.Profiles {
|
||||
if _, ok := validProviders[profile.Provider]; !ok {
|
||||
validProviders := []string{ProviderWebHook, ProviderCloudflare, ProviderTencentCloud}
|
||||
for _, profile := range Conf.DDNS.Profiles {
|
||||
if ok := slices.Contains(validProviders, profile.Provider); !ok {
|
||||
return fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
|
||||
}
|
||||
providers[profileName] = profile.Provider
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ func loadServers() {
|
||||
innerS := s
|
||||
innerS.Host = &model.Host{}
|
||||
innerS.State = &model.HostState{}
|
||||
innerS.TaskCloseLock = new(sync.Mutex)
|
||||
ServerList[innerS.ID] = &innerS
|
||||
SecretToID[innerS.Secret] = innerS.ID
|
||||
ServerTagToIDList[innerS.Tag] = append(ServerTagToIDList[innerS.Tag], innerS.ID)
|
||||
|
||||
@@ -100,14 +100,14 @@ func RecordTransferHourlyUsage() {
|
||||
for id, server := range ServerList {
|
||||
tx := model.Transfer{
|
||||
ServerID: id,
|
||||
In: server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn),
|
||||
Out: server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut),
|
||||
In: utils.Uint64SubInt64(server.State.NetInTransfer, server.PrevTransferInSnapshot),
|
||||
Out: utils.Uint64SubInt64(server.State.NetOutTransfer, server.PrevTransferOutSnapshot),
|
||||
}
|
||||
if tx.In == 0 && tx.Out == 0 {
|
||||
continue
|
||||
}
|
||||
server.PrevHourlyTransferIn = int64(server.State.NetInTransfer)
|
||||
server.PrevHourlyTransferOut = int64(server.State.NetOutTransfer)
|
||||
server.PrevTransferInSnapshot = int64(server.State.NetInTransfer)
|
||||
server.PrevTransferOutSnapshot = int64(server.State.NetOutTransfer)
|
||||
tx.CreatedAt = nowTrimSeconds
|
||||
txs = append(txs, tx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user