Compare commits

...

21 Commits

Author SHA1 Message Date
耗子 a5e1fbc232 feat: update v2.0.10 2023-07-22 16:44:41 +08:00
耗子 1689ef4e4a feat: add task check in update 2023-07-22 03:15:48 +08:00
耗子 fb383f9f90 feat: update v2.0.9 2023-07-22 03:09:16 +08:00
耗子 624fc9c77c feat(cmd): update cleanTask 2023-07-22 03:02:09 +08:00
耗子 793b2ee989 fix: missing file 2023-07-22 02:58:39 +08:00
耗子 e56cf991ae fix: dos2unix 2023-07-22 02:50:45 +08:00
耗子 1daaab1e93 feat: update v2.0.7 2023-07-22 02:47:56 +08:00
耗子 b9f75534ef feat: update v2.0.7 2023-07-22 02:36:08 +08:00
耗子 594dfea986 feat: update v2.0.7 2023-07-22 02:31:07 +08:00
耗子 915403e3c0 feat: update 2023-07-21 15:27:52 +08:00
耗子 0715a4e497 feat: update 2023-07-21 15:27:42 +08:00
耗子 0ab4738e1d feat: update 2023-07-21 04:32:41 +08:00
耗子 0d4f8bbea1 feat: update 2023-07-21 02:26:12 +08:00
耗子 ea59149175 fix: remove file menu 2023-07-21 00:34:42 +08:00
耗子 87a7350f9a fix: some bugs 2023-07-21 00:28:44 +08:00
耗子 59243d5203 fix(cmd): optimize getInfo 2023-07-20 15:39:19 +08:00
耗子 83e2526ce8 fix(plugin controller): script path 2023-07-20 13:39:43 +08:00
耗子 1f5718cd67 fix(cmd): conf path 2023-07-20 01:46:05 +08:00
耗子 3d8f2948be fix(conf): disable debug 2023-07-20 01:45:12 +08:00
耗子 6ef713ab3d fix(install script): oom 2023-07-20 01:30:35 +08:00
耗子 feb377a8f1 fix(install script): run migrate 2023-07-20 01:21:20 +08:00
82 changed files with 8317 additions and 1349 deletions
+1
View File
@@ -23,6 +23,7 @@ _cgo_export.*
*.exe
*.o
*.so
panel
# 压缩包 #
# Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit
+2 -2
View File
@@ -32,13 +32,13 @@
```shell
TODO
bash <(curl -sSL https://dl.cdn.haozi.net/panel/install_panel.sh)
```
## 日常维护
```shell
TODO
panel xxx
```
## 问题反馈
+4 -4
View File
@@ -8,8 +8,10 @@ import (
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
@@ -36,10 +38,8 @@ func (receiver *Monitoring) Extend() command.Extend {
// Handle Execute the console command.
func (receiver *Monitoring) Handle(ctx console.Context) error {
var setting models.Setting
if err := facades.Orm().Query().Where("key", "monitor").First(&setting); err != nil {
return nil
}
if setting.Value == "0" || len(setting.Value) == 0 {
monitor := services.NewSettingImpl().Get(models.SettingKeyMonitor)
if !cast.ToBool(monitor) {
return nil
}
+25 -14
View File
@@ -4,11 +4,10 @@ import (
"os"
"github.com/gookit/color"
"github.com/spf13/cast"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
@@ -50,7 +49,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
return nil
}
settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子Linux面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyPanelEntrance, Value: "/"}}
settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子Linux面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyEntrance, Value: "/"}}
err = facades.Orm().Query().Create(&settings)
if err != nil {
color.Redln("初始化失败")
@@ -73,7 +72,19 @@ func (receiver *Panel) Handle(ctx console.Context) error {
color.Greenln("初始化成功")
case "update":
err := tools.UpdatePanel()
var task models.Task
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
if err == nil {
color.Redln("当前有任务正在执行,禁止更新")
return nil
}
input := arg1
proxy := false
if input == "y" || input == "Y" || input == "yes" || input == "Yes" {
proxy = true
}
err = tools.UpdatePanel(cast.ToBool(proxy))
if err != nil {
color.Redln("更新失败: " + err.Error())
return nil
@@ -105,19 +116,19 @@ func (receiver *Panel) Handle(ctx console.Context) error {
return nil
}
port := tools.ExecShell("cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}'")
port := tools.ExecShell(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
color.Greenln("用户名: " + user.Username)
color.Greenln("密码: " + password)
color.Greenln("面板端口: " + port)
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyPanelEntrance, "/"))
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
case "getPort":
port := tools.ExecShell("cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}'")
color.Greenln("面板端口: " + port)
case "getEntrance":
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyPanelEntrance, "/"))
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
case "writePlugin":
slug := arg1
@@ -177,14 +188,14 @@ func (receiver *Panel) Handle(ctx console.Context) error {
color.Greenln("写入MySQL root密码成功")
case "cleanRunningTask":
_, err := facades.Orm().Query().Model(&models.Task{}).Where("status", models.TaskStatusRunning).Update("status", models.TaskStatusFailed)
case "cleanTask":
_, err := facades.Orm().Query().Model(&models.Task{}).Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).Update("status", models.TaskStatusFailed)
if err != nil {
color.Redln("清理正在运行的任务失败")
color.Redln("清理任务失败")
return nil
}
color.Greenln("清理正在运行的任务成功")
color.Greenln("清理任务成功")
case "backup":
@@ -279,15 +290,15 @@ func (receiver *Panel) Handle(ctx console.Context) error {
default:
color.Yellowln(facades.Config().GetString("panel.name") + "命令行工具 - " + facades.Config().GetString("panel.version"))
color.Greenln("请使用以下命令:")
color.Greenln("panel update 更新/修复面板到最新版本")
color.Greenln("panel update {proxy} 更新/修复面板到最新版本")
color.Greenln("panel getInfo 重新初始化面板账号信息")
color.Greenln("panel getPort 获取面板访问端口")
color.Greenln("panel getEntrance 获取面板访问入口")
color.Greenln("panel cleanRunningTask 强制清理面板正在运行的任务")
color.Greenln("panel cleanTask 清理面板运行中和等待中的任务")
color.Greenln("panel backup {website/mysql/postgresql} {name} {path} 备份网站/MySQL数据库/PostgreSQL数据库到指定目录")
color.Redln("以下命令请在开发者指导下使用:")
color.Yellowln("panel init 初始化面板")
color.Yellowln("panel writePlugin {slug} 写入插件安装状态")
color.Yellowln("panel writePlugin {slug} {version} 写入插件安装状态")
color.Yellowln("panel deletePlugin {slug} 移除插件安装状态")
color.Yellowln("panel writeMysqlPassword {password} 写入MySQL root密码")
color.Yellowln("panel writeSite {name} {status} {path} {php} {ssl} 写入网站数据到面板")
+4 -1
View File
@@ -3,6 +3,7 @@ package console
import (
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/schedule"
"github.com/goravel/framework/facades"
"panel/app/console/commands"
)
@@ -11,7 +12,9 @@ type Kernel struct {
}
func (kernel *Kernel) Schedule() []schedule.Event {
return []schedule.Event{}
return []schedule.Event{
facades.Schedule().Command("panel:monitoring").EveryMinute().SkipIfStillRunning(),
}
}
func (kernel *Kernel) Commands() []console.Command {
+53 -41
View File
@@ -1,6 +1,7 @@
package controllers
import (
"regexp"
"strconv"
"github.com/goravel/framework/contracts/http"
@@ -44,7 +45,7 @@ func (r *CronController) List(ctx http.Context) {
func (r *CronController) Add(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
"time": "required|regex:^((\\*|\\d+|\\d+-\\d+|\\d+\\/\\d+|\\d+-\\d+\\/\\d+|\\*\\/\\d+)(\\,(\\*|\\d+|\\d+-\\d+|\\d+\\/\\d+|\\d+-\\d+\\/\\d+|\\*\\/\\d+))*\\s?){5}$",
"time": "required",
"script": "required",
})
if err != nil {
@@ -52,7 +53,13 @@ func (r *CronController) Add(ctx http.Context) {
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().All())
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
// 单独验证时间格式
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) {
Error(ctx, http.StatusBadRequest, "时间格式错误")
return
}
@@ -70,7 +77,7 @@ func (r *CronController) Add(ctx http.Context) {
return
}
shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + tools.RandomString(16)
if !tools.WriteFile(shellDir+shellFile+".sh", ctx.Request().Input("script"), 0644) {
if !tools.WriteFile(shellDir+shellFile+".sh", ctx.Request().Input("script"), 0700) {
facades.Log().Error("[面板][CronController] 创建计划任务脚本失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
@@ -99,11 +106,22 @@ func (r *CronController) Add(ctx http.Context) {
})
}
// Script 获取脚本内容
func (r *CronController) Script(ctx http.Context) {
var cron models.Cron
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
Error(ctx, http.StatusBadRequest, "计划任务不存在")
return
}
Success(ctx, tools.ReadFile(cron.Shell))
}
func (r *CronController) Update(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"id": "required|int",
"name": "required|min_len:1|max_len:255",
"time": "required|regex:^((\\*|\\d+|\\d+-\\d+|\\d+\\/\\d+|\\d+-\\d+\\/\\d+|\\*\\/\\d+)(\\,(\\*|\\d+|\\d+-\\d+|\\d+\\/\\d+|\\d+-\\d+\\/\\d+|\\*\\/\\d+))*\\s?){5}$",
"time": "required",
"script": "required",
})
if err != nil {
@@ -111,7 +129,13 @@ func (r *CronController) Update(ctx http.Context) {
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().All())
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
// 单独验证时间格式
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) {
Error(ctx, http.StatusBadRequest, "时间格式错误")
return
}
@@ -122,6 +146,20 @@ func (r *CronController) Update(ctx http.Context) {
return
}
if !cron.Status {
Error(ctx, http.StatusBadRequest, "计划任务已禁用")
return
}
cron.Time = ctx.Request().Input("time")
cron.Name = ctx.Request().Input("name")
err = facades.Orm().Query().Save(&cron)
if err != nil {
facades.Log().Error("[面板][CronController] 更新计划任务失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if !tools.WriteFile(cron.Shell, ctx.Request().Input("script"), 0644) {
facades.Log().Error("[面板][CronController] 更新计划任务脚本失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
@@ -138,25 +176,16 @@ func (r *CronController) Update(ctx http.Context) {
}
func (r *CronController) Delete(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"id": "required|int",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().All())
return
}
var cron models.Cron
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
Error(ctx, http.StatusBadRequest, "计划任务不存在")
return
}
r.cron.DeleteFromSystem(cron)
tools.RemoveFile(cron.Shell)
_, err = facades.Orm().Query().Delete(&cron)
if err != nil {
facades.Log().Error("[面板][CronController] 删除计划任务失败 ", err)
@@ -164,22 +193,19 @@ func (r *CronController) Delete(ctx http.Context) {
return
}
r.cron.DeleteFromSystem(cron)
Success(ctx, nil)
}
func (r *CronController) Status(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"id": "required|int",
"status": "required|in:true,false",
"status": "bool",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().All())
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
@@ -191,7 +217,7 @@ func (r *CronController) Status(ctx http.Context) {
}
cron.Status = ctx.Request().InputBool("status")
_, err = facades.Orm().Query().Update(&cron)
err = facades.Orm().Query().Save(&cron)
if err != nil {
facades.Log().Error("[面板][CronController] 更新计划任务状态失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
@@ -207,20 +233,8 @@ func (r *CronController) Status(ctx http.Context) {
}
func (r *CronController) Log(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"id": "required|int",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().All())
return
}
var cron models.Cron
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
Error(ctx, http.StatusBadRequest, "计划任务不存在")
return
@@ -231,7 +245,5 @@ func (r *CronController) Log(ctx http.Context) {
return
}
Success(ctx, http.Json{
"log": tools.ReadFile(cron.Log),
})
Success(ctx, tools.ReadFile(cron.Log))
}
+56 -1
View File
@@ -1,6 +1,13 @@
package controllers
import "github.com/goravel/framework/contracts/http"
import (
"sync"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/services"
)
func Success(ctx http.Context, data any) {
ctx.Response().Success().Json(http.Json{
@@ -16,3 +23,51 @@ func Error(ctx http.Context, code int, message any) {
"message": message,
})
}
// Check 检查插件是否可用
func Check(ctx http.Context, slug string) bool {
plugin := services.NewPluginImpl().GetBySlug(slug)
installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug)
installedPlugins, err := services.NewPluginImpl().AllInstalled()
if err != nil {
facades.Log().Error("[面板][插件] 获取已安装插件失败")
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return false
}
if installedPlugin.Version != plugin.Version || installedPlugin.Slug != plugin.Slug {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要更新至 "+plugin.Version+" 版本")
return false
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
return false
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
return false
}
}
return true
}
+68 -10
View File
@@ -2,6 +2,7 @@ package controllers
import (
"fmt"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
@@ -19,16 +20,16 @@ type MenuItem struct {
}
type InfoController struct {
// Dependent services
plugin services.Plugin
}
func NewInfoController() *InfoController {
return &InfoController{
// Inject services
plugin: services.NewPluginImpl(),
}
}
func (r *InfoController) Name(ctx http.Context) {
func (c *InfoController) Name(ctx http.Context) {
var setting models.Setting
err := facades.Orm().Query().Where("key", "name").First(&setting)
if err != nil {
@@ -42,20 +43,20 @@ func (r *InfoController) Name(ctx http.Context) {
})
}
func (r *InfoController) Menu(ctx http.Context) {
func (c *InfoController) Menu(ctx http.Context) {
Success(ctx, []MenuItem{
{Name: "home", Title: "主页", Icon: "layui-icon-home", Jump: "/"},
{Name: "website", Title: "网站管理", Icon: "layui-icon-website", Jump: "website/list"},
{Name: "monitor", Title: "资源监控", Icon: "layui-icon-chart-screen", Jump: "monitor"},
{Name: "safe", Title: "系统安全", Icon: "layui-icon-auz", Jump: "safe"},
{Name: "file", Title: "文件管理", Icon: "layui-icon-file", Jump: "file"},
/*{Name: "file", Title: "文件管理", Icon: "layui-icon-file", Jump: "file"},*/
{Name: "cron", Title: "计划任务", Icon: "layui-icon-date", Jump: "cron"},
{Name: "plugin", Title: "插件中心", Icon: "layui-icon-app", Jump: "plugin"},
{Name: "setting", Title: "面板设置", Icon: "layui-icon-set", Jump: "setting"},
})
}
func (r *InfoController) HomePlugins(ctx http.Context) {
func (c *InfoController) HomePlugins(ctx http.Context) {
var plugins []models.Plugin
err := facades.Orm().Query().Where("show", 1).Find(&plugins)
if err != nil {
@@ -80,11 +81,11 @@ func (r *InfoController) HomePlugins(ctx http.Context) {
Success(ctx, pluginsJson)
}
func (r *InfoController) NowMonitor(ctx http.Context) {
func (c *InfoController) NowMonitor(ctx http.Context) {
Success(ctx, tools.GetMonitoringInfo())
}
func (r *InfoController) SystemInfo(ctx http.Context) {
func (c *InfoController) SystemInfo(ctx http.Context) {
monitorInfo := tools.GetMonitoringInfo()
Success(ctx, http.Json{
@@ -94,7 +95,7 @@ func (r *InfoController) SystemInfo(ctx http.Context) {
})
}
func (r *InfoController) InstalledDbAndPhp(ctx http.Context) {
func (c *InfoController) InstalledDbAndPhp(ctx http.Context) {
var php []models.Plugin
err := facades.Orm().Query().Where("slug like ?", "php%").Find(&php)
if err != nil {
@@ -116,9 +117,66 @@ func (r *InfoController) InstalledDbAndPhp(ctx http.Context) {
postgresqlInstalled = false
}
type data struct {
Slug string `json:"slug"`
Name string `json:"name"`
}
var phpData []data
phpData = append(phpData, data{Slug: "0", Name: "不使用"})
for _, p := range php {
phpData = append(phpData, data{Slug: strings.ReplaceAll(p.Slug, "php", ""), Name: c.plugin.GetBySlug(p.Slug).Name})
}
Success(ctx, http.Json{
"php": php,
"php": phpData,
"mysql": mysqlInstalled,
"postgresql": postgresqlInstalled,
})
}
func (c *InfoController) CheckUpdate(ctx http.Context) {
version := facades.Config().GetString("panel.version")
remote, err := tools.GetLatestPanelVersion()
if err != nil {
Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
return
}
if version == remote.Version {
Success(ctx, http.Json{
"update": false,
"version": remote.Version,
"name": remote.Name,
"body": remote.Body,
"date": remote.Date,
})
return
}
Success(ctx, http.Json{
"update": true,
"version": remote.Version,
"name": remote.Name,
"body": remote.Body,
"date": remote.Date,
})
}
func (c *InfoController) Update(ctx http.Context) {
var task models.Task
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
if err == nil {
Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止更新")
return
}
proxy := ctx.Request().InputBool("proxy")
err = tools.UpdatePanel(proxy)
if err != nil {
facades.Log().Error("[面板][InfoController] 更新面板失败 ", err.Error())
Error(ctx, http.StatusInternalServerError, "更新失败: "+err.Error())
return
}
Success(ctx, nil)
}
+99 -8
View File
@@ -1,12 +1,13 @@
package controllers
import (
"fmt"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
)
@@ -46,9 +47,20 @@ func (r *MonitorController) SaveDays(ctx http.Context) {
Success(ctx, nil)
}
// SwitchAndDays 监控开关和监控天数
func (r *MonitorController) SwitchAndDays(ctx http.Context) {
monitor := r.setting.Get(models.SettingKeyMonitor)
monitorDays := r.setting.Get(models.SettingKeyMonitorDays)
Success(ctx, http.Json{
"switch": cast.ToBool(monitor),
"days": cast.ToInt(monitorDays),
})
}
// Clear 清空监控数据
func (r *MonitorController) Clear(ctx http.Context) {
_, err := facades.Orm().Query().Delete(&models.Monitor{})
_, err := facades.Orm().Query().Where("1 = 1").Delete(&models.Monitor{})
if err != nil {
facades.Log().Error("[面板][MonitorController] 清空监控数据失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
@@ -60,18 +72,97 @@ func (r *MonitorController) Clear(ctx http.Context) {
// List 监控数据列表
func (r *MonitorController) List(ctx http.Context) {
start := ctx.Request().Input("start")
end := ctx.Request().Input("end")
startTime := carbon.Parse(start)
endTime := carbon.Parse(end)
start := ctx.Request().InputInt64("start")
end := ctx.Request().InputInt64("end")
startTime := carbon.FromTimestampMilli(start)
endTime := carbon.FromTimestampMilli(end)
var monitors []models.Monitor
err := facades.Orm().Query().Where("created_at", ">=", startTime).Where("created_at", "<=", endTime).Get(&monitors)
err := facades.Orm().Query().Where("created_at >= ?", startTime.ToDateTimeString()).Where("created_at <= ?", endTime.ToDateTimeString()).Get(&monitors)
if err != nil {
facades.Log().Error("[面板][MonitorController] 查询监控数据失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, monitors)
if len(monitors) == 0 {
Error(ctx, http.StatusNotFound, "监控数据为空")
return
}
type load struct {
Load1 []float64 `json:"load1"`
Load5 []float64 `json:"load5"`
Load15 []float64 `json:"load15"`
}
type cpu struct {
Percent []string `json:"percent"`
}
type mem struct {
Total string `json:"total"`
Available []string `json:"available"`
Used []string `json:"used"`
}
type swap struct {
Total string `json:"total"`
Used []string `json:"used"`
Free []string `json:"free"`
}
type network struct {
Sent []string `json:"sent"`
Recv []string `json:"recv"`
Tx []string `json:"tx"`
Rx []string `json:"rx"`
}
type monitorData struct {
Times []string `json:"times"`
Load load `json:"load"`
Cpu cpu `json:"cpu"`
Mem mem `json:"mem"`
Swap swap `json:"swap"`
Net network `json:"net"`
}
var data monitorData
var bytesSent uint64
var bytesRecv uint64
var bytesSent2 uint64
var bytesRecv2 uint64
for _, net := range monitors[0].Info.Net {
bytesSent += net.BytesSent
bytesRecv += net.BytesRecv
}
for i, monitor := range monitors {
// 跳过第一条数据,因为第一条数据的流量为 0
if i == 0 {
// MB
data.Mem.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Total)/1024/1024)
data.Swap.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Total)/1024/1024)
continue
}
for _, net := range monitor.Info.Net {
bytesSent2 += net.BytesSent
bytesRecv2 += net.BytesRecv
}
data.Times = append(data.Times, monitor.CreatedAt.ToDateTimeString())
data.Load.Load1 = append(data.Load.Load1, monitor.Info.Load.Load1)
data.Load.Load5 = append(data.Load.Load5, monitor.Info.Load.Load5)
data.Load.Load15 = append(data.Load.Load15, monitor.Info.Load.Load15)
data.Cpu.Percent = append(data.Cpu.Percent, fmt.Sprintf("%.2f", monitor.Info.Percent[0]))
data.Mem.Available = append(data.Mem.Available, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Available)/1024/1024))
data.Mem.Used = append(data.Mem.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Used)/1024/1024))
data.Swap.Used = append(data.Swap.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024))
data.Swap.Free = append(data.Swap.Free, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Free)/1024/1024))
data.Net.Sent = append(data.Net.Sent, fmt.Sprintf("%.2f", float64(bytesSent2/1024)))
data.Net.Recv = append(data.Net.Recv, fmt.Sprintf("%.2f", float64(bytesRecv2/1024)))
// 监控频率为 1 分钟,所以这里除以 60 即可得到每秒的流量
data.Net.Tx = append(data.Net.Tx, fmt.Sprintf("%.2f", float64(bytesSent2-bytesSent)/60/1024))
data.Net.Rx = append(data.Net.Rx, fmt.Sprintf("%.2f", float64(bytesRecv2-bytesRecv)/60/1024))
bytesSent = bytesSent2
bytesRecv = bytesRecv2
}
Success(ctx, data)
}
+111 -57
View File
@@ -80,35 +80,53 @@ func (r *PluginController) List(ctx http.Context) {
// Install 安装插件
func (r *PluginController) Install(ctx http.Context) {
slug := ctx.Request().Input("slug")
plugins := r.plugin.All()
var plugin services.PanelPlugin
check := false
for _, item := range plugins {
if item.Slug == slug {
check = true
plugin = item
break
}
}
if !check {
Error(ctx, http.StatusBadRequest, "插件不存在")
return
}
var installedPlugin models.Plugin
if err := facades.Orm().Query().Where("slug", slug).First(&installedPlugin); err != nil {
plugin := r.plugin.GetBySlug(slug)
installedPlugin := r.plugin.GetInstalledBySlug(slug)
installedPlugins, err := r.plugin.AllInstalled()
if err != nil {
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if installedPlugin.ID != 0 {
Error(ctx, http.StatusBadRequest, "插件已安装")
return
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
return
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
return
}
}
var task models.Task
task.Name = "安装插件 " + plugin.Name
task.Status = models.TaskStatusWaiting
task.Shell = "bash scripts/plugins/" + plugin.Slug + "/install.sh >> /tmp/" + plugin.Slug + ".log 2>&1"
task.Shell = plugin.Install + " >> /tmp/" + plugin.Slug + ".log 2>&1"
task.Log = "/tmp/" + plugin.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[面板][PluginController] 创建任务失败: " + err.Error())
@@ -123,35 +141,53 @@ func (r *PluginController) Install(ctx http.Context) {
// Uninstall 卸载插件
func (r *PluginController) Uninstall(ctx http.Context) {
slug := ctx.Request().Input("slug")
plugins := r.plugin.All()
var plugin services.PanelPlugin
check := false
for _, item := range plugins {
if item.Slug == slug {
check = true
plugin = item
break
}
}
if !check {
Error(ctx, http.StatusBadRequest, "插件不存在")
return
}
var installedPlugin models.Plugin
if err := facades.Orm().Query().Where("slug", slug).First(&installedPlugin); err != nil {
plugin := r.plugin.GetBySlug(slug)
installedPlugin := r.plugin.GetInstalledBySlug(slug)
installedPlugins, err := r.plugin.AllInstalled()
if err != nil {
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if installedPlugin.ID == 0 {
Error(ctx, http.StatusBadRequest, "插件未安装")
return
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
return
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
return
}
}
var task models.Task
task.Name = "卸载插件 " + plugin.Name
task.Status = models.TaskStatusWaiting
task.Shell = "bash scripts/plugins/" + plugin.Slug + "/uninstall.sh >> /tmp/" + plugin.Slug + ".log 2>&1"
task.Shell = plugin.Uninstall + " >> /tmp/" + plugin.Slug + ".log 2>&1"
task.Log = "/tmp/" + plugin.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[面板][PluginController] 创建任务失败: " + err.Error())
@@ -166,35 +202,53 @@ func (r *PluginController) Uninstall(ctx http.Context) {
// Update 更新插件
func (r *PluginController) Update(ctx http.Context) {
slug := ctx.Request().Input("slug")
plugins := r.plugin.All()
var plugin services.PanelPlugin
check := false
for _, item := range plugins {
if item.Slug == slug {
check = true
plugin = item
break
}
}
if !check {
Error(ctx, http.StatusBadRequest, "插件不存在")
return
}
var installedPlugin models.Plugin
if err := facades.Orm().Query().Where("slug", slug).First(&installedPlugin); err != nil {
plugin := r.plugin.GetBySlug(slug)
installedPlugin := r.plugin.GetInstalledBySlug(slug)
installedPlugins, err := r.plugin.AllInstalled()
if err != nil {
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if installedPlugin.ID == 0 {
Error(ctx, http.StatusBadRequest, "插件未安装")
return
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
return
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
return
}
}
var task models.Task
task.Name = "更新插件 " + plugin.Name
task.Status = models.TaskStatusWaiting
task.Shell = "bash scripts/plugins/" + plugin.Slug + "/update.sh >> /tmp/" + plugin.Slug + ".log 2>&1"
task.Shell = plugin.Update + " >> /tmp/" + plugin.Slug + ".log 2>&1"
task.Log = "/tmp/" + plugin.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[面板][PluginController] 创建任务失败: " + err.Error())
@@ -0,0 +1,825 @@
package mysql57
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Mysql57Controller struct {
setting services.Setting
}
func NewMysql57Controller() *Mysql57Controller {
return &Mysql57Controller{
setting: services.NewSettingImpl(),
}
}
// Status 获取运行状态
func (c *Mysql57Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Reload 重载配置
func (c *Mysql57Controller) Reload(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
tools.ExecShell("systemctl reload mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Restart 重启服务
func (c *Mysql57Controller) Restart(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
tools.ExecShell("systemctl restart mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Start 启动服务
func (c *Mysql57Controller) Start(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
tools.ExecShell("systemctl start mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Stop 停止服务
func (c *Mysql57Controller) Stop(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
tools.ExecShell("systemctl stop mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status != "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// GetConfig 获取配置
func (c *Mysql57Controller) GetConfig(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
// 获取配置
config := tools.ReadFile("/www/server/mysql/conf/my.cnf")
if len(config) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
return
}
controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (c *Mysql57Controller) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
config := ctx.Request().Input("config")
if len(config) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
return
}
if !tools.WriteFile("/www/server/mysql/conf/my.cnf", config, 0644) {
controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
return
}
controllers.Success(ctx, "保存MySQL配置成功")
}
// Load 获取负载
func (c *Mysql57Controller) Load(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
return
}
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if status != "active" {
controllers.Error(ctx, http.StatusInternalServerError, "MySQL 已停止运行")
return
}
raw := tools.ExecShell("mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
if strings.Contains(raw, "Access denied for user") {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码错误")
return
}
if !strings.Contains(raw, "Uptime") {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
return
}
data := make(map[int]map[string]string)
expressions := []struct {
regex string
name string
}{
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
}
for i, expression := range expressions {
re := regexp.MustCompile(expression.regex)
matches := re.FindStringSubmatch(raw)
if len(matches) > 1 {
data[i] = make(map[string]string)
data[i] = map[string]string{"name": expression.name, "value": matches[1]}
if expression.name == "发送" || expression.name == "接收" {
data[i]["value"] = tools.FormatBytes(cast.ToFloat64(matches[1]))
}
}
}
// 索引命中率
readRequests := cast.ToFloat64(data[9]["value"])
reads := cast.ToFloat64(data[10]["value"])
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
// Innodb 索引命中率
bufferPoolReads := cast.ToFloat64(data[11]["value"])
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
controllers.Success(ctx, data)
}
// ErrorLog 获取错误日志
func (c *Mysql57Controller) ErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
log := tools.ExecShell("tail -n 100 /www/server/mysql/mysql-error.log")
controllers.Success(ctx, log)
}
// ClearErrorLog 清空错误日志
func (c *Mysql57Controller) ClearErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
tools.ExecShell("echo '' > /www/server/mysql/mysql-error.log")
controllers.Success(ctx, "清空错误日志成功")
}
// SlowLog 获取慢查询日志
func (c *Mysql57Controller) SlowLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
log := tools.ExecShell("tail -n 100 /www/server/mysql/mysql-slow.log")
controllers.Success(ctx, log)
}
// ClearSlowLog 清空慢查询日志
func (c *Mysql57Controller) ClearSlowLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
tools.ExecShell("echo '' > /www/server/mysql/mysql-slow.log")
controllers.Success(ctx, "清空慢查询日志成功")
}
// GetRootPassword 获取root密码
func (c *Mysql57Controller) GetRootPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
return
}
controllers.Success(ctx, rootPassword)
}
// SetRootPassword 设置root密码
func (c *Mysql57Controller) SetRootPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status != "active" {
controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
return
}
rootPassword := ctx.Request().Input(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码不能为空")
return
}
oldRootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
if oldRootPassword != rootPassword {
tools.ExecShell("mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\"")
tools.ExecShell("mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\"")
err := c.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
if err != nil {
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
return
}
}
controllers.Success(ctx, "设置root密码成功")
}
// DatabaseList 获取数据库列表
func (c *Mysql57Controller) DatabaseList(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
type database struct {
Name string `json:"name"`
}
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
facades.Log().Error("[MySQL57] 连接数据库失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
return
}
defer db.Close()
rows, err := db.Query("SHOW DATABASES")
if err != nil {
facades.Log().Error("[MySQL57] 获取数据库列表失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
return
}
defer rows.Close()
var databases []database
for rows.Next() {
var d database
err := rows.Scan(&d.Name)
if err != nil {
continue
}
databases = append(databases, d)
}
if err := rows.Err(); err != nil {
facades.Log().Error("[MySQL57] 获取数据库列表失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
return
}
controllers.Success(ctx, databases)
}
// AddDatabase 添加数据库
func (c *Mysql57Controller) AddDatabase(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "添加数据库成功")
}
// DeleteDatabase 删除数据库
func (c *Mysql57Controller) DeleteDatabase(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\"")
controllers.Success(ctx, "删除数据库成功")
}
// BackupList 获取备份列表
func (c *Mysql57Controller) BackupList(ctx http.Context) {
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
files, err := os.ReadDir(backupPath)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
return
}
var backupFiles []map[string]string
for _, file := range files {
if file.IsDir() {
continue
}
info, err := file.Info()
if err != nil {
continue
}
backupFiles = append(backupFiles, map[string]string{
"file": file.Name(),
"size": tools.FormatBytes(float64(info.Size())),
})
}
controllers.Success(ctx, backupFiles)
}
// UploadBackup 上传备份
func (c *Mysql57Controller) UploadBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
// TODO 框架 Bug ? 下面会 panic
controllers.Error(ctx, http.StatusBadRequest, "暂不支持")
return
file, err := ctx.Request().File("file")
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
name := file.GetClientOriginalName()
extension := file.GetClientOriginalExtension()
_, err = file.Store(backupPath + "/" + name + "." + extension)
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
controllers.Success(ctx, "上传文件成功")
}
// CreateBackup 创建备份
func (c *Mysql57Controller) CreateBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
backupFile := database + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
err = os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
facades.Log().Error("[MySQL57] 设置环境变量 MYSQL_PWD 失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "备份失败")
return
}
tools.ExecShell("mysqldump -uroot " + database + " > " + backupPath + "/" + backupFile)
tools.ExecShell("cd " + backupPath + " && zip -r " + backupPath + "/" + backupFile + ".zip " + backupFile)
tools.RemoveFile(backupPath + "/" + backupFile)
_ = os.Unsetenv("MYSQL_PWD")
controllers.Success(ctx, "备份成功")
}
// DeleteBackup 删除备份
func (c *Mysql57Controller) DeleteBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"file": "required|min_len:1|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
file := ctx.Request().Input("file")
tools.RemoveFile(backupPath + "/" + file)
controllers.Success(ctx, "删除备份成功")
}
// RestoreBackup 还原备份
func (c *Mysql57Controller) RestoreBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"file": "required|min_len:1|max_len:255",
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
file := ctx.Request().Input("file")
backupFile := backupPath + "/" + file
if !tools.Exists(backupFile) {
controllers.Error(ctx, http.StatusBadRequest, "备份文件不存在")
return
}
err = os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
facades.Log().Error("[MYSQL57] 设置环境变量 MYSQL_PWD 失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "还原失败")
return
}
// 获取文件拓展名
ext := filepath.Ext(file)
switch ext {
case ".zip":
tools.ExecShell("unzip -o " + backupFile + " -d " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".gz":
if strings.HasSuffix(file, ".tar.gz") {
// 解压.tar.gz文件
tools.ExecShell("tar -zxvf " + backupFile + " -C " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ".tar.gz")
} else {
// 解压.gz文件
tools.ExecShell("gzip -d " + backupFile)
backupFile = strings.TrimSuffix(backupFile, ext)
}
case ".bz2":
tools.ExecShell("bzip2 -d " + backupFile)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".tar":
tools.ExecShell("tar -xvf " + backupFile + " -C " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".rar":
tools.ExecShell("unrar x " + backupFile + " " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
}
if !tools.Exists(backupFile) {
controllers.Error(ctx, http.StatusBadRequest, "自动解压备份文件失败,请手动解压")
return
}
tools.ExecShell("mysql -uroot " + ctx.Request().Input("database") + " < " + backupFile)
_ = os.Unsetenv("MYSQL_PWD")
controllers.Success(ctx, "还原成功")
}
// UserList 用户列表
func (c *Mysql57Controller) UserList(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
type user struct {
User string `json:"user"`
Host string `json:"host"`
Grants []string `json:"grants"`
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
facades.Log().Error("[MYSQL57] 连接数据库失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
}
defer db.Close()
rows, err := db.Query("SELECT user, host FROM mysql.user")
if err != nil {
facades.Log().Error("[MYSQL57] 查询数据库失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "查询数据库失败")
}
defer rows.Close()
var userGrants []user
for rows.Next() {
var u user
err := rows.Scan(&u.User, &u.Host)
if err != nil {
continue
}
// 查询用户权限
grantsRows, err := db.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", u.User, u.Host))
if err != nil {
continue
}
defer grantsRows.Close()
for grantsRows.Next() {
var grant string
err := grantsRows.Scan(&grant)
if err != nil {
continue
}
u.Grants = append(u.Grants, grant)
}
if err := grantsRows.Err(); err != nil {
continue
}
userGrants = append(userGrants, u)
}
if err := rows.Err(); err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
return
}
controllers.Success(ctx, userGrants)
}
// AddUser 添加用户
func (c *Mysql57Controller) AddUser(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "添加成功")
}
// DeleteUser 删除用户
func (c *Mysql57Controller) DeleteUser(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\"")
controllers.Success(ctx, "删除成功")
}
// SetUserPassword 设置用户密码
func (c *Mysql57Controller) SetUserPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "修改成功")
}
// SetUserPrivileges 设置用户权限
func (c *Mysql57Controller) SetUserPrivileges(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"database": "required|min_len:1|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
database := ctx.Request().Input("database")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "修改成功")
}
@@ -1,6 +1,7 @@
package mysql80
import (
"database/sql"
"fmt"
"os"
"path/filepath"
@@ -11,11 +12,8 @@ import (
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"golang.org/x/exp/slices"
"panel/app/models"
"panel/app/http/controllers"
"panel/app/http/controllers/plugins"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
@@ -31,13 +29,12 @@ func NewMysql80Controller() *Mysql80Controller {
}
// Status 获取运行状态
func (r *Mysql80Controller) Status(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
out := tools.ExecShell("systemctl status mysql | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
@@ -51,14 +48,13 @@ func (r *Mysql80Controller) Status(ctx http.Context) {
}
// Reload 重载配置
func (r *Mysql80Controller) Reload(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) Reload(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
tools.ExecShell("systemctl reload mysql")
out := tools.ExecShell("systemctl status mysql | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
tools.ExecShell("systemctl reload mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
@@ -72,14 +68,13 @@ func (r *Mysql80Controller) Reload(ctx http.Context) {
}
// Restart 重启服务
func (r *Mysql80Controller) Restart(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) Restart(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
tools.ExecShell("systemctl restart mysql")
out := tools.ExecShell("systemctl status mysql | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
tools.ExecShell("systemctl restart mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
@@ -93,14 +88,13 @@ func (r *Mysql80Controller) Restart(ctx http.Context) {
}
// Start 启动服务
func (r *Mysql80Controller) Start(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) Start(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
tools.ExecShell("systemctl start mysql")
out := tools.ExecShell("systemctl status mysql | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
tools.ExecShell("systemctl start mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
@@ -114,14 +108,13 @@ func (r *Mysql80Controller) Start(ctx http.Context) {
}
// Stop 停止服务
func (r *Mysql80Controller) Stop(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) Stop(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
tools.ExecShell("systemctl stop mysql")
out := tools.ExecShell("systemctl status mysql | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
tools.ExecShell("systemctl stop mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
@@ -135,13 +128,13 @@ func (r *Mysql80Controller) Stop(ctx http.Context) {
}
// GetConfig 获取配置
func (r *Mysql80Controller) GetConfig(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) GetConfig(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
// 获取配置
config := tools.ReadFile("mysql80")
config := tools.ReadFile("/www/server/mysql/conf/my.cnf")
if len(config) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
return
@@ -151,8 +144,8 @@ func (r *Mysql80Controller) GetConfig(ctx http.Context) {
}
// SaveConfig 保存配置
func (r *Mysql80Controller) SaveConfig(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
@@ -162,7 +155,7 @@ func (r *Mysql80Controller) SaveConfig(ctx http.Context) {
return
}
if !tools.WriteFile("mysql80", config, 0644) {
if !tools.WriteFile("/www/server/mysql/conf/my.cnf", config, 0644) {
controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
return
}
@@ -171,19 +164,19 @@ func (r *Mysql80Controller) SaveConfig(ctx http.Context) {
}
// Load 获取负载
func (r *Mysql80Controller) Load(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) Load(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
return
}
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if strings.TrimSpace(status) != "active" {
if status != "active" {
controllers.Error(ctx, http.StatusInternalServerError, "MySQL 已停止运行")
return
}
@@ -203,24 +196,24 @@ func (r *Mysql80Controller) Load(ctx http.Context) {
regex string
name string
}{
{`Uptime\s+\|\s+(\d+)\s+\|`, "总查询次数"},
{`Queries\s+\|\s+(\d+)\s+\|`, "总连接次数"},
{`Connections\s+\|\s+(\d+)\s+\|`, "每秒事务"},
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "发送"},
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "接收"},
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "活动连接数"},
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "索引命中率"},
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
{`Open_tables\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "锁表次数"},
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, ""},
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
}
for i, expression := range expressions {
@@ -240,7 +233,7 @@ func (r *Mysql80Controller) Load(ctx http.Context) {
readRequests := cast.ToFloat64(data[9]["value"])
reads := cast.ToFloat64(data[10]["value"])
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
// Innodb索引命中率
// Innodb 索引命中率
bufferPoolReads := cast.ToFloat64(data[11]["value"])
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
@@ -249,8 +242,8 @@ func (r *Mysql80Controller) Load(ctx http.Context) {
}
// ErrorLog 获取错误日志
func (r *Mysql80Controller) ErrorLog(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) ErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
@@ -259,8 +252,8 @@ func (r *Mysql80Controller) ErrorLog(ctx http.Context) {
}
// ClearErrorLog 清空错误日志
func (r *Mysql80Controller) ClearErrorLog(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) ClearErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
@@ -269,8 +262,8 @@ func (r *Mysql80Controller) ClearErrorLog(ctx http.Context) {
}
// SlowLog 获取慢查询日志
func (r *Mysql80Controller) SlowLog(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) SlowLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
@@ -279,8 +272,8 @@ func (r *Mysql80Controller) SlowLog(ctx http.Context) {
}
// ClearSlowLog 清空慢查询日志
func (r *Mysql80Controller) ClearSlowLog(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) ClearSlowLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
@@ -289,12 +282,12 @@ func (r *Mysql80Controller) ClearSlowLog(ctx http.Context) {
}
// GetRootPassword 获取root密码
func (r *Mysql80Controller) GetRootPassword(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) GetRootPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
return
@@ -304,13 +297,12 @@ func (r *Mysql80Controller) GetRootPassword(ctx http.Context) {
}
// SetRootPassword 设置root密码
func (r *Mysql80Controller) SetRootPassword(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) SetRootPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
out := tools.ExecShell("systemctl status mysql | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
@@ -326,11 +318,11 @@ func (r *Mysql80Controller) SetRootPassword(ctx http.Context) {
return
}
oldRootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
oldRootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
if oldRootPassword != rootPassword {
tools.ExecShell("mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\"")
tools.ExecShell("mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\"")
err := r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
err := c.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
if err != nil {
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
@@ -343,57 +335,73 @@ func (r *Mysql80Controller) SetRootPassword(ctx http.Context) {
}
// DatabaseList 获取数据库列表
func (r *Mysql80Controller) DatabaseList(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) DatabaseList(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
out := tools.ExecShell("mysql -uroot -p" + r.setting.Get(models.SettingKeyMysqlRootPassword) + " -e \"show databases;\"")
databases := strings.Split(out, "\n")
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
type database struct {
Name string `json:"name"`
}
databases = databases[1 : len(databases)-1]
systemDatabases := []string{"information_schema", "mysql", "performance_schema", "sys"}
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
facades.Log().Error("[MySQL80] 连接数据库失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
return
}
defer db.Close()
var userDatabases []string
for _, db := range databases {
if !slices.Contains(systemDatabases, db) {
userDatabases = append(userDatabases, db)
rows, err := db.Query("SHOW DATABASES")
if err != nil {
facades.Log().Error("[MySQL80] 获取数据库列表失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
return
}
defer rows.Close()
var databases []database
for rows.Next() {
var d database
err := rows.Scan(&d.Name)
if err != nil {
continue
}
databases = append(databases, d)
}
type Database struct {
Name string
if err := rows.Err(); err != nil {
facades.Log().Error("[MySQL80] 获取数据库列表失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
return
}
var dbStructs []Database
for _, db := range userDatabases {
dbStructs = append(dbStructs, Database{Name: db})
}
controllers.Success(ctx, dbStructs)
controllers.Success(ctx, databases)
}
// AddDatabase 添加数据库
func (r *Mysql80Controller) AddDatabase(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) AddDatabase(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255|regex:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*(_|[^\\w])).+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
@@ -407,8 +415,8 @@ func (r *Mysql80Controller) AddDatabase(ctx http.Context) {
}
// DeleteDatabase 删除数据库
func (r *Mysql80Controller) DeleteDatabase(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) DeleteDatabase(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
@@ -420,11 +428,11 @@ func (r *Mysql80Controller) DeleteDatabase(ctx http.Context) {
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\"")
@@ -432,8 +440,8 @@ func (r *Mysql80Controller) DeleteDatabase(ctx http.Context) {
}
// BackupList 获取备份列表
func (r *Mysql80Controller) BackupList(ctx http.Context) {
backupPath := "/www/backup/mysql"
func (c *Mysql80Controller) BackupList(ctx http.Context) {
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
@@ -465,9 +473,41 @@ func (r *Mysql80Controller) BackupList(ctx http.Context) {
controllers.Success(ctx, backupFiles)
}
// UploadBackup 上传备份
func (c *Mysql80Controller) UploadBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
// TODO 框架 Bug ? 下面会 panic
controllers.Error(ctx, http.StatusBadRequest, "暂不支持")
return
file, err := ctx.Request().File("file")
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
name := file.GetClientOriginalName()
extension := file.GetClientOriginalExtension()
_, err = file.Store(backupPath + "/" + name + "." + extension)
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
controllers.Success(ctx, "上传文件成功")
}
// CreateBackup 创建备份
func (r *Mysql80Controller) CreateBackup(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) CreateBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
@@ -479,14 +519,14 @@ func (r *Mysql80Controller) CreateBackup(ctx http.Context) {
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
backupPath := "/www/backup/mysql"
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
backupFile := backupPath + "/" + database + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
backupFile := database + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
@@ -497,17 +537,17 @@ func (r *Mysql80Controller) CreateBackup(ctx http.Context) {
return
}
tools.ExecShell("mysqldump -uroot " + database + " > " + backupFile)
tools.ExecShell("zip -r " + backupFile + ".zip " + backupFile)
tools.RemoveFile(backupFile)
tools.ExecShell("mysqldump -uroot " + database + " > " + backupPath + "/" + backupFile)
tools.ExecShell("cd " + backupPath + " && zip -r " + backupPath + "/" + backupFile + ".zip " + backupFile)
tools.RemoveFile(backupPath + "/" + backupFile)
_ = os.Unsetenv("MYSQL_PWD")
controllers.Success(ctx, "备份成功")
}
// DeleteBackup 删除备份
func (r *Mysql80Controller) DeleteBackup(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) DeleteBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
@@ -519,11 +559,11 @@ func (r *Mysql80Controller) DeleteBackup(ctx http.Context) {
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
backupPath := "/www/backup/mysql"
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
file := ctx.Request().Input("file")
tools.RemoveFile(backupPath + "/" + file)
@@ -531,8 +571,8 @@ func (r *Mysql80Controller) DeleteBackup(ctx http.Context) {
}
// RestoreBackup 还原备份
func (r *Mysql80Controller) RestoreBackup(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) RestoreBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
@@ -545,12 +585,12 @@ func (r *Mysql80Controller) RestoreBackup(ctx http.Context) {
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
backupPath := "/www/backup/mysql"
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
file := ctx.Request().Input("file")
backupFile := backupPath + "/" + file
if !tools.Exists(backupFile) {
@@ -560,7 +600,7 @@ func (r *Mysql80Controller) RestoreBackup(ctx http.Context) {
err = os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
facades.Log().Error("[MySQL80] 设置环境变量 MYSQL_PWD 失败:" + err.Error())
facades.Log().Error("[MYSQL80] 设置环境变量 MYSQL_PWD 失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "还原失败")
return
}
@@ -604,64 +644,94 @@ func (r *Mysql80Controller) RestoreBackup(ctx http.Context) {
}
// UserList 用户列表
func (r *Mysql80Controller) UserList(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) UserList(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
type User struct {
Username string `json:"username"`
Host string `json:"host"`
Privileges string `json:"privileges"`
type user struct {
User string `json:"user"`
Host string `json:"host"`
Grants []string `json:"grants"`
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
out := tools.ExecShell("mysql -uroot -p" + rootPassword + " -e 'select user,host from mysql.user'")
rawUsers := strings.Split(out, "\n")
users := make([]User, 0)
for _, rawUser := range rawUsers {
user := strings.Split(rawUser, "\t")
if user[0] == "root" || user[0] == "mysql.sys" || user[0] == "mysql.infoschema" || user[0] == "mysql.session" {
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
facades.Log().Error("[MYSQL80] 连接数据库失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
}
defer db.Close()
rows, err := db.Query("SELECT user, host FROM mysql.user")
if err != nil {
facades.Log().Error("[MYSQL80] 查询数据库失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "查询数据库失败")
}
defer rows.Close()
var userGrants []user
for rows.Next() {
var u user
err := rows.Scan(&u.User, &u.Host)
if err != nil {
continue
}
out := tools.ExecShell("mysql -uroot -p" + rootPassword + " -e 'show grants for " + user[0] + "@" + user[1] + "'")
rawPrivileges := strings.Split(out, "\n")
privileges := make([]string, 0)
for _, rawPrivilege := range rawPrivileges {
if rawPrivilege == "" {
// 查询用户权限
grantsRows, err := db.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", u.User, u.Host))
if err != nil {
continue
}
defer grantsRows.Close()
for grantsRows.Next() {
var grant string
err := grantsRows.Scan(&grant)
if err != nil {
continue
}
privilege := rawPrivilege[6:strings.Index(rawPrivilege, " TO")]
privileges = append(privileges, privilege)
u.Grants = append(u.Grants, grant)
}
users = append(users, User{Username: user[0], Host: user[1], Privileges: strings.Join(privileges, " | ")})
if err := grantsRows.Err(); err != nil {
continue
}
userGrants = append(userGrants, u)
}
controllers.Success(ctx, users)
if err := rows.Err(); err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
return
}
controllers.Success(ctx, userGrants)
}
// AddUser 添加用户
func (r *Mysql80Controller) AddUser(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) AddUser(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255|regex:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*(_|[^\\w])).+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
@@ -673,8 +743,8 @@ func (r *Mysql80Controller) AddUser(ctx http.Context) {
}
// DeleteUser 删除用户
func (r *Mysql80Controller) DeleteUser(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) DeleteUser(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
@@ -686,11 +756,11 @@ func (r *Mysql80Controller) DeleteUser(ctx http.Context) {
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\"")
@@ -698,25 +768,25 @@ func (r *Mysql80Controller) DeleteUser(ctx http.Context) {
}
// SetUserPassword 设置用户密码
func (r *Mysql80Controller) SetUserPassword(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) SetUserPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255|regex:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*(_|[^\\w])).+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
@@ -726,8 +796,8 @@ func (r *Mysql80Controller) SetUserPassword(ctx http.Context) {
}
// SetUserPrivileges 设置用户权限
func (r *Mysql80Controller) SetUserPrivileges(ctx http.Context) {
if !plugins.Check(ctx, "mysql80") {
func (c *Mysql80Controller) SetUserPrivileges(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
@@ -740,11 +810,11 @@ func (r *Mysql80Controller) SetUserPrivileges(ctx http.Context) {
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
database := ctx.Request().Input("database")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\"")
@@ -2,7 +2,6 @@ package openresty
import (
"regexp"
"strings"
"time"
"github.com/goravel/framework/contracts/http"
@@ -11,7 +10,6 @@ import (
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/http/controllers/plugins"
"panel/pkg/tools"
)
@@ -27,12 +25,11 @@ func NewOpenrestyController() *OpenRestyController {
// Status 获取运行状态
func (r *OpenRestyController) Status(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
out := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
status := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
@@ -47,13 +44,12 @@ func (r *OpenRestyController) Status(ctx http.Context) {
// Reload 重载配置
func (r *OpenRestyController) Reload(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
tools.ExecShell("systemctl reload openresty")
out := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
status := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
@@ -62,19 +58,18 @@ func (r *OpenRestyController) Reload(ctx http.Context) {
if status == "active" {
controllers.Success(ctx, "重载OpenResty成功")
} else {
controllers.Error(ctx, 1, "重载OpenResty失败: "+string(out))
controllers.Error(ctx, 1, "重载OpenResty失败: "+status)
}
}
// Start 启动OpenResty
func (r *OpenRestyController) Start(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
tools.ExecShell("systemctl start openresty")
out := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
status := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
@@ -83,19 +78,18 @@ func (r *OpenRestyController) Start(ctx http.Context) {
if status == "active" {
controllers.Success(ctx, "启动OpenResty成功")
} else {
controllers.Error(ctx, 1, "启动OpenResty失败: "+string(out))
controllers.Error(ctx, 1, "启动OpenResty失败: "+status)
}
}
// Stop 停止OpenResty
func (r *OpenRestyController) Stop(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
tools.ExecShell("systemctl stop openresty")
out := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
status := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
@@ -104,19 +98,18 @@ func (r *OpenRestyController) Stop(ctx http.Context) {
if status != "active" {
controllers.Success(ctx, "停止OpenResty成功")
} else {
controllers.Error(ctx, 1, "停止OpenResty失败: "+string(out))
controllers.Error(ctx, 1, "停止OpenResty失败: "+status)
}
}
// Restart 重启OpenResty
func (r *OpenRestyController) Restart(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
tools.ExecShell("systemctl restart openresty")
out := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
status := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
@@ -125,13 +118,13 @@ func (r *OpenRestyController) Restart(ctx http.Context) {
if status == "active" {
controllers.Success(ctx, "重启OpenResty成功")
} else {
controllers.Error(ctx, 1, "重启OpenResty失败: "+string(out))
controllers.Error(ctx, 1, "重启OpenResty失败: "+status)
}
}
// GetConfig 获取配置
func (r *OpenRestyController) GetConfig(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
@@ -146,7 +139,7 @@ func (r *OpenRestyController) GetConfig(ctx http.Context) {
// SaveConfig 保存配置
func (r *OpenRestyController) SaveConfig(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
@@ -166,7 +159,12 @@ func (r *OpenRestyController) SaveConfig(ctx http.Context) {
// ErrorLog 获取错误日志
func (r *OpenRestyController) ErrorLog(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
if !tools.Exists("/www/wwwlogs/nginx_error.log") {
controllers.Success(ctx, "")
return
}
@@ -176,17 +174,17 @@ func (r *OpenRestyController) ErrorLog(ctx http.Context) {
// ClearErrorLog 清空错误日志
func (r *OpenRestyController) ClearErrorLog(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
_ = tools.ExecShell("echo '' > /www/wwwlogs/nginx_error.log")
tools.ExecShell("echo '' > /www/wwwlogs/nginx_error.log")
controllers.Success(ctx, "清空OpenResty错误日志成功")
}
// Load 获取负载
func (r *OpenRestyController) Load(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
@@ -199,42 +197,63 @@ func (r *OpenRestyController) Load(ctx http.Context) {
}
raw := resp.String()
var data map[int]map[string]any
type nginxStatus struct {
Name string `json:"name"`
Value string `json:"value"`
}
var data []nginxStatus
out := tools.ExecShell("ps aux | grep nginx | grep 'worker process' | wc -l")
workers := strings.TrimSpace(out)
data[0]["name"] = "工作进程"
data[0]["value"] = workers
workers := tools.ExecShell("ps aux | grep nginx | grep 'worker process' | wc -l")
data = append(data, nginxStatus{
Name: "工作进程",
Value: workers,
})
out = tools.ExecShell("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
mem := tools.FormatBytes(cast.ToFloat64(strings.TrimSpace(out)))
data[1]["name"] = "内存占用"
data[1]["value"] = mem
out := tools.ExecShell("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
mem := tools.FormatBytes(cast.ToFloat64(out))
data = append(data, nginxStatus{
Name: "内存占用",
Value: mem,
})
match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 2 {
data[2]["name"] = "活跃连接数"
data[2]["value"] = match[1]
data = append(data, nginxStatus{
Name: "活跃连接数",
Value: match[1],
})
}
match = regexp.MustCompile(`server accepts handled requests\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 4 {
data[3]["name"] = "总连接次数"
data[3]["value"] = match[1]
data[4]["name"] = "总握手次数"
data[4]["value"] = match[2]
data[5]["name"] = "总请求次数"
data[5]["value"] = match[3]
data = append(data, nginxStatus{
Name: "总连接次数",
Value: match[1],
})
data = append(data, nginxStatus{
Name: "总握手次数",
Value: match[2],
})
data = append(data, nginxStatus{
Name: "总请求次数",
Value: match[3],
})
}
match = regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 4 {
data[6]["name"] = "请求数"
data[6]["value"] = match[1]
data[7]["name"] = "响应数"
data[7]["value"] = match[2]
data[8]["name"] = "驻留进程"
data[8]["value"] = match[3]
data = append(data, nginxStatus{
Name: "请求数",
Value: match[1],
})
data = append(data, nginxStatus{
Name: "响应数",
Value: match[2],
})
data = append(data, nginxStatus{
Name: "驻留进程",
Value: match[3],
})
}
controllers.Success(ctx, data)
@@ -11,7 +11,6 @@ import (
"github.com/imroc/req/v3"
"panel/app/http/controllers"
"panel/app/http/controllers/plugins"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
@@ -32,7 +31,7 @@ func NewPhp74Controller() *Php74Controller {
}
func (c *Php74Controller) Status(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -51,7 +50,7 @@ func (c *Php74Controller) Status(ctx http.Context) {
}
func (c *Php74Controller) Reload(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -71,7 +70,7 @@ func (c *Php74Controller) Reload(ctx http.Context) {
}
func (c *Php74Controller) Start(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -91,7 +90,7 @@ func (c *Php74Controller) Start(ctx http.Context) {
}
func (c *Php74Controller) Stop(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -111,7 +110,7 @@ func (c *Php74Controller) Stop(ctx http.Context) {
}
func (c *Php74Controller) Restart(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -131,7 +130,7 @@ func (c *Php74Controller) Restart(ctx http.Context) {
}
func (c *Php74Controller) GetConfig(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -140,7 +139,7 @@ func (c *Php74Controller) GetConfig(ctx http.Context) {
}
func (c *Php74Controller) SaveConfig(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -150,12 +149,12 @@ func (c *Php74Controller) SaveConfig(ctx http.Context) {
}
func (c *Php74Controller) Load(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
client := req.C().SetTimeout(10 * time.Second)
resp, err := client.R().Get("http://127.0.0.1/phpfpm_" + c.version + "_status")
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + c.version)
if err != nil || !resp.IsSuccessState() {
facades.Log().Error("获取PHP-" + c.version + "运行状态失败")
controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
@@ -186,7 +185,7 @@ func (c *Php74Controller) Load(ctx http.Context) {
}
func (c *Php74Controller) ErrorLog(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -195,7 +194,7 @@ func (c *Php74Controller) ErrorLog(ctx http.Context) {
}
func (c *Php74Controller) SlowLog(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -204,7 +203,7 @@ func (c *Php74Controller) SlowLog(ctx http.Context) {
}
func (c *Php74Controller) ClearErrorLog(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -213,7 +212,7 @@ func (c *Php74Controller) ClearErrorLog(ctx http.Context) {
}
func (c *Php74Controller) ClearSlowLog(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -229,7 +228,7 @@ type Extension struct {
}
func (c *Php74Controller) GetExtensionList(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -238,7 +237,7 @@ func (c *Php74Controller) GetExtensionList(ctx http.Context) {
}
func (c *Php74Controller) InstallExtension(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -259,7 +258,7 @@ func (c *Php74Controller) InstallExtension(ctx http.Context) {
var task models.Task
task.Name = "安装PHP-" + c.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = "bash scripts/php_extensions/" + item.Slug + ".sh install " + c.version + ">> /tmp/" + item.Slug + ".log 2>&1"
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[PHP-" + c.version + "] 创建安装拓展任务失败:" + err.Error())
@@ -278,7 +277,7 @@ func (c *Php74Controller) InstallExtension(ctx http.Context) {
}
func (c *Php74Controller) UninstallExtension(ctx http.Context) {
if !plugins.Check(ctx, "php"+c.version) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
@@ -299,7 +298,7 @@ func (c *Php74Controller) UninstallExtension(ctx http.Context) {
var task models.Task
task.Name = "卸载PHP-" + c.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = "bash scripts/php_extensions/" + item.Slug + ".sh uninstall " + c.version + ">> /tmp/" + item.Slug + ".log 2>&1"
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[PHP-" + c.version + "] 创建卸载拓展任务失败:" + err.Error())
@@ -362,7 +361,7 @@ func (c *Php74Controller) GetExtensions() []Extension {
for _, item := range rawExtensionList {
if !strings.Contains(item, "[") && item != "" {
for i := range extensions {
if extensions[i].Name == item {
if extensions[i].Slug == item {
extensions[i].Installed = true
}
}
@@ -0,0 +1,372 @@
package php80
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/imroc/req/v3"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Php80Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp80Controller() *Php80Controller {
return &Php80Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "80",
}
}
func (c *Php80Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php80Controller) Reload(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl reload php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php80Controller) Start(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl start php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php80Controller) Stop(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl stop php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status != "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php80Controller) Restart(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl restart php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php80Controller) GetConfig(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
config := tools.ReadFile("/www/server/php/" + c.version + "/etc/php.ini")
controllers.Success(ctx, config)
}
func (c *Php80Controller) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
config := ctx.Request().Input("config")
tools.WriteFile("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
c.Reload(ctx)
}
func (c *Php80Controller) Load(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
client := req.C().SetTimeout(10 * time.Second)
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + c.version)
if err != nil || !resp.IsSuccessState() {
facades.Log().Error("获取PHP-" + c.version + "运行状态失败")
controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
return
}
raw := resp.String()
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
type Data struct {
Name string `json:"name"`
Value string `json:"value"`
}
data := make([]Data, len(dataKeys))
for i := range dataKeys {
data[i].Name = dataKeys[i]
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
match := r.FindStringSubmatch(raw)
if len(match) > 1 {
data[i].Value = strings.TrimSpace(match[1])
}
}
controllers.Success(ctx, data)
}
func (c *Php80Controller) ErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
log := tools.ExecShell("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log")
controllers.Success(ctx, log)
}
func (c *Php80Controller) SlowLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
log := tools.ExecShell("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log")
controllers.Success(ctx, log)
}
func (c *Php80Controller) ClearErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
controllers.Success(ctx, true)
}
func (c *Php80Controller) ClearSlowLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
controllers.Success(ctx, true)
}
type Extension struct {
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Installed bool `json:"installed"`
}
func (c *Php80Controller) GetExtensionList(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
extensions := c.GetExtensions()
controllers.Success(ctx, extensions)
}
func (c *Php80Controller) InstallExtension(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "参数错误")
return
}
extensions := c.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if item.Installed {
controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
return
}
var task models.Task
task.Name = "安装PHP-" + c.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[PHP-" + c.version + "] 创建安装拓展任务失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
c.task.Process(task.ID)
controllers.Success(ctx, true)
return
}
}
controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
}
func (c *Php80Controller) UninstallExtension(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "参数错误")
return
}
extensions := c.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if !item.Installed {
controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
return
}
var task models.Task
task.Name = "卸载PHP-" + c.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[PHP-" + c.version + "] 创建卸载拓展任务失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
c.task.Process(task.ID)
controllers.Success(ctx, true)
return
}
}
controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
}
func (c *Php80Controller) GetExtensions() []Extension {
var extensions []Extension
extensions = append(extensions, Extension{
Name: "OPcache",
Slug: "Zend OPcache",
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "PhpRedis",
Slug: "redis",
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "ImageMagick",
Slug: "imagick",
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "Exif",
Slug: "exif",
Description: "通过 exif 扩展,你可以操作图像元数据。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "pdo_pgsql",
Slug: "pdo_pgsql",
Description: "(需先安装PostgreSQLpdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "ionCube",
Slug: "ionCube Loader",
Description: "ionCube 是一个专业级的PHP加密解密工具。",
Installed: false,
})
raw := tools.ExecShell("/www/server/php/" + c.version + "/bin/php -m")
rawExtensionList := strings.Split(raw, "\n")
for _, item := range rawExtensionList {
if !strings.Contains(item, "[") && item != "" {
for i := range extensions {
if extensions[i].Slug == item {
extensions[i].Installed = true
}
}
}
}
return extensions
}
-59
View File
@@ -1,59 +0,0 @@
package plugins
import (
"sync"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/http/controllers"
"panel/app/services"
)
// Check 检查插件是否可用
func Check(ctx http.Context, slug string) bool {
plugin := services.NewPluginImpl().GetBySlug(slug)
installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug)
installedPlugins, err := services.NewPluginImpl().AllInstalled()
if err != nil {
facades.Log().Error("[面板][插件] 获取已安装插件失败")
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
return false
}
if installedPlugin.Version != plugin.Version || installedPlugin.Slug != plugin.Slug {
controllers.Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要更新至 "+plugin.Version+" 版本")
return false
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
controllers.Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
return false
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
controllers.Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
return false
}
}
return true
}
+33 -20
View File
@@ -26,7 +26,7 @@ func (r *SafeController) GetFirewallStatus(ctx http.Context) {
func (r *SafeController) SetFirewallStatus(ctx http.Context) {
var out string
if ctx.Request().QueryBool("status") {
if ctx.Request().InputBool("status") {
if tools.IsRHEL() {
out = tools.ExecShell("systemctl start firewalld")
} else {
@@ -45,7 +45,7 @@ func (r *SafeController) SetFirewallStatus(ctx http.Context) {
func (r *SafeController) GetFirewallRules(ctx http.Context) {
if !r.firewallStatus() {
Error(ctx, http.StatusBadRequest, "防火墙未启动")
Success(ctx, nil)
return
}
@@ -68,16 +68,13 @@ func (r *SafeController) GetFirewallRules(ctx http.Context) {
Success(ctx, rules)
} else {
out := tools.ExecShell("ufw status numbered | grep ALLOW | awk '{print $2}'")
out := tools.ExecShell("ufw status | grep -v '(v6)' | grep ALLOW | awk '{print $1}'")
if len(out) == 0 {
Success(ctx, nil)
return
}
var rules []map[string]string
for _, port := range strings.Split(out, "\n") {
if strings.Contains(port, "]") {
continue
}
rule := strings.Split(port, "/")
rules = append(rules, map[string]string{
"port": rule[0],
@@ -162,7 +159,13 @@ func (r *SafeController) firewallStatus() bool {
}
func (r *SafeController) GetSshStatus(ctx http.Context) {
out := tools.ExecShell("systemctl status sshd | grep Active | awk '{print $3}'")
var out string
if tools.IsRHEL() {
out = tools.ExecShell("systemctl status sshd | grep Active | awk '{print $3}'")
} else {
out = tools.ExecShell("systemctl status ssh | grep Active | awk '{print $3}'")
}
running := false
if out == "(running)" {
running = true
@@ -172,19 +175,29 @@ func (r *SafeController) GetSshStatus(ctx http.Context) {
}
func (r *SafeController) SetSshStatus(ctx http.Context) {
if ctx.Request().QueryBool("status") {
tools.ExecShell("systemctl enable sshd")
tools.ExecShell("systemctl start sshd")
if ctx.Request().InputBool("status") {
if tools.IsRHEL() {
tools.ExecShell("systemctl enable sshd")
tools.ExecShell("systemctl start sshd")
} else {
tools.ExecShell("systemctl enable ssh")
tools.ExecShell("systemctl start ssh")
}
} else {
tools.ExecShell("systemctl stop sshd")
tools.ExecShell("systemctl disable sshd")
if tools.IsRHEL() {
tools.ExecShell("systemctl stop sshd")
tools.ExecShell("systemctl disable sshd")
} else {
tools.ExecShell("systemctl stop ssh")
tools.ExecShell("systemctl disable ssh")
}
}
Success(ctx, nil)
}
func (r *SafeController) GetSshPort(ctx http.Context) {
out := tools.ExecShell("cat /etc/ssh/sshd_config | grep Port | awk '{print $2}'")
out := tools.ExecShell("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
Success(ctx, out)
}
@@ -195,7 +208,7 @@ func (r *SafeController) SetSshPort(ctx http.Context) {
return
}
oldPort := tools.ExecShell("cat /etc/ssh/sshd_config | grep Port | awk '{print $2}'")
oldPort := tools.ExecShell("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
tools.ExecShell("sed -i 's/#Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
tools.ExecShell("sed -i 's/Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
@@ -226,17 +239,17 @@ func (r *SafeController) GetPingStatus(ctx http.Context) {
func (r *SafeController) SetPingStatus(ctx http.Context) {
if tools.IsRHEL() {
if ctx.Request().QueryBool("status") {
tools.ExecShell("firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'")
} else {
if ctx.Request().InputBool("status") {
tools.ExecShell("firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'")
} else {
tools.ExecShell("firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'")
}
tools.ExecShell("firewall-cmd --reload")
} else {
if ctx.Request().QueryBool("status") {
tools.ExecShell("sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules")
} else {
if ctx.Request().InputBool("status") {
tools.ExecShell("sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/g' /etc/ufw/before.rules")
} else {
tools.ExecShell("sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules")
}
tools.ExecShell("ufw reload")
}
+18 -6
View File
@@ -46,10 +46,11 @@ func (r *SettingController) List(ctx http.Context) {
return
}
result["username"] = user.Username
result["email"] = user.Email
result["port"] = tools.ExecShell(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
Success(ctx, result)
}
@@ -58,7 +59,7 @@ func (r *SettingController) Save(ctx http.Context) {
port := ctx.Request().Input("port")
backupPath := ctx.Request().Input("backup_path")
websitePath := ctx.Request().Input("website_path")
panelEntrance := ctx.Request().Input("panel_entrance")
entrance := ctx.Request().Input("entrance")
username := ctx.Request().Input("username")
email := ctx.Request().Input("email")
password := ctx.Request().Input("password")
@@ -70,10 +71,13 @@ func (r *SettingController) Save(ctx http.Context) {
return
}
oldPort := tools.ExecShell("cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}'")
oldPort := tools.ExecShell(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
if oldPort != port {
tools.ExecShell("sed -i 's/APP_PORT=" + oldPort + "/APP_PORT=" + port + "/g' /www/panel/panel.conf")
}
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
err = r.setting.Set(models.SettingKeyBackupPath, backupPath)
if err != nil {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
@@ -81,6 +85,10 @@ func (r *SettingController) Save(ctx http.Context) {
return
}
if !tools.Exists(websitePath) {
tools.Mkdir(websitePath, 0755)
tools.Chown(websitePath, "www", "www")
}
err = r.setting.Set(models.SettingKeyWebsitePath, websitePath)
if err != nil {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
@@ -88,7 +96,7 @@ func (r *SettingController) Save(ctx http.Context) {
return
}
err = r.setting.Set(models.SettingKeyPanelEntrance, panelEntrance)
err = r.setting.Set(models.SettingKeyEntrance, entrance)
if err != nil {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
@@ -105,8 +113,12 @@ func (r *SettingController) Save(ctx http.Context) {
return
}
user.Username = username
user.Email = email
if len(username) > 0 {
user.Username = username
}
if len(email) > 0 {
user.Email = email
}
if len(password) > 0 {
hash, err := facades.Hash().Make(password)
if err != nil {
+1 -1
View File
@@ -69,7 +69,7 @@ func (r *TaskController) Log(ctx http.Context) {
func (r *TaskController) Delete(ctx http.Context) {
var task models.Task
_, err := facades.Orm().Query().Where("id", ctx.Request().QueryInt("id")).Delete(&task)
_, err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).Delete(&task)
if err != nil {
facades.Log().Error("[面板][TaskController] 删除任务失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
+86 -64
View File
@@ -8,10 +8,10 @@ import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/models"
"panel/pkg/tools"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type WebsiteController struct {
@@ -46,31 +46,37 @@ func (c *WebsiteController) List(ctx http.Context) {
}
func (c *WebsiteController) Add(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$",
"domain": "required",
"php": "required",
"db": "required",
"db_type": "required_if:db,1",
"db_name": "required_if:db,1",
"db_user": "required_if:db,1",
"db_password": "required_if:db,1",
"db_type": "required_if:db,true",
"db_name": "required_if:db,true",
"db_user": "required_if:db,true",
"db_password": "required_if:db,true",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().All())
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
var website services.PanelWebsite
err = ctx.Request().Bind(&website)
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
website.Name = ctx.Request().Input("name")
website.Domain = ctx.Request().Input("domain")
website.Php = ctx.Request().InputInt("php")
website.Db = ctx.Request().InputBool("db")
website.DbType = ctx.Request().Input("db_type")
website.DbName = ctx.Request().Input("db_name")
website.DbUser = ctx.Request().Input("db_user")
website.DbPassword = ctx.Request().Input("db_password")
newSite, err := c.website.Add(website)
if err != nil {
@@ -83,23 +89,14 @@ func (c *WebsiteController) Add(ctx http.Context) {
}
func (c *WebsiteController) Delete(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"id": "required|int",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
if !Check(ctx, "openresty") {
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().All())
return
}
id := ctx.Request().InputInt("id")
err = c.website.Delete(id)
err := c.website.Delete(id)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 删除网站失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
Error(ctx, http.StatusInternalServerError, "删除网站失败: "+err.Error())
return
}
@@ -107,6 +104,9 @@ func (c *WebsiteController) Delete(ctx http.Context) {
}
func (c *WebsiteController) GetDefaultConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
index := tools.ReadFile("/www/server/openresty/html/index.html")
stop := tools.ReadFile("/www/server/openresty/html/stop.html")
@@ -117,6 +117,9 @@ func (c *WebsiteController) GetDefaultConfig(ctx http.Context) {
}
func (c *WebsiteController) SaveDefaultConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
index := ctx.Request().Input("index")
stop := ctx.Request().Input("stop")
@@ -136,6 +139,9 @@ func (c *WebsiteController) SaveDefaultConfig(ctx http.Context) {
}
func (c *WebsiteController) GetConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
@@ -153,21 +159,40 @@ func (c *WebsiteController) GetConfig(ctx http.Context) {
}
func (c *WebsiteController) SaveConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"id": "required|int",
"id": "required",
"domains": "required",
"ports": "required",
"hsts": "bool",
"ssl": "bool",
"http_redirect": "bool",
"open_basedir": "bool",
"waf": "required",
"waf_cache": "required",
"waf_mode": "required",
"waf_cc_deny": "required",
"index": "required",
"path": "required",
"root": "required",
"raw": "required",
"php": "required",
"ssl_certificate": "required_if:ssl,true",
"ssl_certificate_key": "required_if:ssl,true",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().All())
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
id := ctx.Request().InputInt("id")
var website models.Website
if facades.Orm().Query().Where("id", id).FirstOrFail(&website) != nil {
if facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&website) != nil {
Error(ctx, http.StatusBadRequest, "网站不存在")
return
}
@@ -178,9 +203,9 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
}
// 原文
raw := tools.ReadFile("/www/server/panel/vhost/openresty/" + website.Name + ".conf")
raw := tools.ReadFile("/www/server/vhost/" + website.Name + ".conf")
if strings.TrimSpace(raw) != strings.TrimSpace(ctx.Request().Input("raw")) {
tools.WriteFile("/www/server/panel/vhost/openresty/"+website.Name+".conf", ctx.Request().Input("raw"), 0644)
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", ctx.Request().Input("raw"), 0644)
Success(ctx, nil)
return
}
@@ -195,7 +220,7 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
// 域名
domain := "server_name"
domains := strings.Split(ctx.Request().Input("domain"), "\n")
domains := strings.Split(ctx.Request().Input("domains"), "\n")
if len(domains) == 0 {
Error(ctx, http.StatusBadRequest, "域名不能为空")
return
@@ -216,7 +241,7 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
// 端口
var port strings.Builder
ports := strings.Split(ctx.Request().Input("port"), "\n")
ports := strings.Split(ctx.Request().Input("ports"), "\n")
if len(ports) == 0 {
Error(ctx, http.StatusBadRequest, "端口不能为空")
return
@@ -287,15 +312,13 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
wafMode := ctx.Request().Input("waf_mode", "DYNAMIC")
wafCcDeny := ctx.Request().Input("waf_cc_deny", "rate=1000r/m duration=60m")
wafCache := ctx.Request().Input("waf_cache", "capacity=50")
wafConfig := `
# waf标记位开始
wafConfig := `# waf标记位开始
waf ` + waf + `;
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
waf_mode ` + wafMode + `;
waf_cc_deny ` + wafCcDeny + `;
waf_cache ` + wafCache + `;
`
`
wafConfigOld := tools.Cut(raw, "# waf标记位开始", "# waf标记位结束")
if len(strings.TrimSpace(wafConfigOld)) != 0 {
raw = strings.Replace(raw, wafConfigOld, "", -1)
@@ -308,8 +331,7 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
if ssl {
tools.WriteFile("/www/server/vhost/ssl/"+website.Name+".pem", ctx.Request().Input("ssl_certificate"), 0644)
tools.WriteFile("/www/server/vhost/ssl/"+website.Name+".key", ctx.Request().Input("ssl_certificate_key"), 0644)
sslConfig := `
# ssl标记位开始
sslConfig := `# ssl标记位开始
ssl_certificate /www/server/vhost/ssl/` + website.Name + `.pem;
ssl_certificate_key /www/server/vhost/ssl/` + website.Name + `.key;
ssl_session_timeout 1d;
@@ -318,28 +340,21 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
`
`
if ctx.Request().InputBool("http_redirect") {
sslConfig += `
# http重定向标记位开始
sslConfig += `# http重定向标记位开始
if (\$server_port !~ 443){
return 301 https://\$host\$request_uri;
}
error_page 497 https://\$host\$request_uri;
# http重定向标记位结束
`
`
}
if ctx.Request().InputBool("hsts") {
sslConfig += `
# hsts标记位开始
sslConfig += `# hsts标记位开始
add_header Strict-Transport-Security "max-age=63072000" always;
# hsts标记位结束
`
`
}
sslConfigOld := tools.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
if len(strings.TrimSpace(sslConfigOld)) != 0 {
@@ -349,7 +364,7 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
} else {
sslConfigOld := tools.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
if len(strings.TrimSpace(sslConfigOld)) != 0 {
raw = strings.Replace(raw, sslConfigOld, "", -1)
raw = strings.Replace(raw, sslConfigOld, "\n ", -1)
}
}
@@ -357,9 +372,8 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
website.Php = ctx.Request().InputInt("php")
phpConfigOld := tools.Cut(raw, "# php标记位开始", "# php标记位结束")
phpConfig := `
include enable-php` + strconv.Itoa(website.Php) + `.conf;
`
include enable-php-` + strconv.Itoa(website.Php) + `.conf;
`
if len(strings.TrimSpace(phpConfigOld)) != 0 {
raw = strings.Replace(raw, phpConfigOld, phpConfig, -1)
}
@@ -379,7 +393,10 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
Success(ctx, nil)
}
func (c *WebsiteController) ClearSiteLpg(ctx http.Context) {
func (c *WebsiteController) ClearLog(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
@@ -426,7 +443,6 @@ func (c *WebsiteController) UpdateRemark(ctx http.Context) {
}
func (c *WebsiteController) BackupList(ctx http.Context) {
backupList, err := c.backup.WebsiteList()
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站备份列表失败 ", err)
@@ -463,6 +479,9 @@ func (c *WebsiteController) CreateBackup(ctx http.Context) {
}
func (c *WebsiteController) ResetConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
@@ -511,7 +530,7 @@ server
# waf标记位开始
waf on;
waf_rule_path /www/server/nginx/ngx_waf/assets/rules/;
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
waf_mode DYNAMIC;
waf_cc_deny rate=1000r/m duration=60m;
waf_cache capacity=50;
@@ -550,7 +569,10 @@ server
Success(ctx, nil)
}
func (c *WebsiteController) SetStatus(ctx http.Context) {
func (c *WebsiteController) Status(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
@@ -574,26 +596,26 @@ func (c *WebsiteController) SetStatus(ctx http.Context) {
raw := tools.ReadFile("/www/server/vhost/" + website.Name + ".conf")
// 运行目录
rootConfig := tools.Cut(raw, "# root标记位开始", "# root标记位结束")
rootConfig := tools.Cut(raw, "# root标记位开始\n", "# root标记位结束")
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig)
if len(match) == 2 {
if website.Status {
root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig)
raw = strings.ReplaceAll(raw, rootConfig, "root "+root[1]+";")
raw = strings.ReplaceAll(raw, rootConfig, " root "+root[1]+";\n ")
} else {
raw = strings.ReplaceAll(raw, rootConfig, "root /www/server/openresty/html;\n# root "+match[1]+";\n")
raw = strings.ReplaceAll(raw, rootConfig, " root /www/server/openresty/html;\n # root "+match[1]+";\n ")
}
}
// 默认文件
indexConfig := tools.Cut(raw, "# index标记位开始", "# index标记位结束")
indexConfig := tools.Cut(raw, "# index标记位开始\n", "# index标记位结束")
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig)
if len(match) == 2 {
if website.Status {
index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig)
raw = strings.ReplaceAll(raw, indexConfig, "index "+index[1]+";")
raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ")
} else {
raw = strings.ReplaceAll(raw, indexConfig, "index stop.html;\n# index "+match[1]+";\n")
raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ")
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
func Static() contractshttp.Middleware {
return func(ctx contractshttp.Context) {
static.Serve(services.NewSettingImpl().Get("panel_entrance", "/"), static.LocalFile("./public", false))(ctx.(*frameworkhttp.GinContext).Instance())
static.Serve(services.NewSettingImpl().Get("entrance", "/"), static.LocalFile("/www/panel/public", false))(ctx.(*frameworkhttp.GinContext).Instance())
ctx.Request().Next()
}
+1 -1
View File
@@ -8,7 +8,7 @@ const (
SettingKeyMonitorDays = "monitor_days"
SettingKeyBackupPath = "backup_path"
SettingKeyWebsitePath = "website_path"
SettingKeyPanelEntrance = "panel_entrance"
SettingKeyEntrance = "entrance"
SettingKeyMysqlRootPassword = "mysql_root_password"
)
+3
View File
@@ -8,4 +8,7 @@ var (
Version = "5.7.42"
Requires = []string{}
Excludes = []string{"mysql80"}
Install = `bash /www/panel/scripts/mysql/install.sh 57`
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 57`
Update = `echo "not support now"`
)
+4 -1
View File
@@ -3,9 +3,12 @@ package mysql80
var (
Name = "MySQL-8.0"
Author = "耗子"
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。"
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。(内存 < 4G 无法安装)"
Slug = "mysql80"
Version = "8.0.33"
Requires = []string{}
Excludes = []string{"mysql57"}
Install = `bash /www/panel/scripts/mysql/install.sh 80`
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 80`
Update = `echo "not support now"`
)
+3
View File
@@ -8,4 +8,7 @@ var (
Version = "1.21.4.1"
Requires = []string{}
Excludes = []string{}
Install = "bash /www/panel/scripts/openresty/install.sh"
Uninstall = "bash /www/panel/scripts/openresty/uninstall.sh"
Update = "bash /www/panel/scripts/openresty/install.sh"
)
+3
View File
@@ -8,4 +8,7 @@ var (
Version = "7.4.33"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/php/install.sh 74`
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 74`
Update = `bash /www/panel/scripts/php/install.sh 74`
)
+3
View File
@@ -8,4 +8,7 @@ var (
Version = "8.0.29"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/php/install.sh 80`
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 80`
Update = `bash /www/panel/scripts/php/install.sh 80`
)
+4 -4
View File
@@ -1,6 +1,8 @@
package services
import (
"strings"
"panel/app/models"
"panel/pkg/tools"
)
@@ -24,17 +26,15 @@ func (r *CronImpl) AddToSystem(cron models.Cron) {
} else {
tools.ExecShell("echo \"" + cron.Time + " " + cron.Shell + " >> " + cron.Log + " 2>&1\" >> /var/spool/cron/crontabs/root")
}
tools.ExecShell("systemctl restart crond")
}
// DeleteFromSystem 从系统中删除
func (r *CronImpl) DeleteFromSystem(cron models.Cron) {
// 需要转义Shell路径的/为\/
cron.Shell = strings.ReplaceAll(cron.Shell, "/", "\\/")
if tools.IsRHEL() {
tools.ExecShell("sed -i '/" + cron.Shell + "/d' /var/spool/cron/root")
} else {
tools.ExecShell("sed -i '/" + cron.Shell + "/d' /var/spool/cron/crontabs/root")
}
tools.ExecShell("systemctl restart crond")
}
+40
View File
@@ -5,9 +5,11 @@ import (
"github.com/goravel/framework/facades"
"panel/app/models"
"panel/app/plugins/mysql57"
"panel/app/plugins/mysql80"
"panel/app/plugins/openresty"
"panel/app/plugins/php74"
"panel/app/plugins/php80"
)
// PanelPlugin 插件元数据结构
@@ -19,11 +21,16 @@ type PanelPlugin struct {
Version string
Requires []string
Excludes []string
Install string
Uninstall string
Update string
}
type Plugin interface {
AllInstalled() ([]models.Plugin, error)
All() []PanelPlugin
GetBySlug(slug string) PanelPlugin
GetInstalledBySlug(slug string) models.Plugin
}
type PluginImpl struct {
@@ -55,6 +62,21 @@ func (r *PluginImpl) All() []PanelPlugin {
Version: openresty.Version,
Requires: openresty.Requires,
Excludes: openresty.Excludes,
Install: openresty.Install,
Uninstall: openresty.Uninstall,
Update: openresty.Update,
})
p = append(p, PanelPlugin{
Name: mysql57.Name,
Author: mysql57.Author,
Description: mysql57.Description,
Slug: mysql57.Slug,
Version: mysql57.Version,
Requires: mysql57.Requires,
Excludes: mysql57.Excludes,
Install: mysql57.Install,
Uninstall: mysql57.Uninstall,
Update: mysql57.Update,
})
p = append(p, PanelPlugin{
Name: mysql80.Name,
@@ -64,6 +86,9 @@ func (r *PluginImpl) All() []PanelPlugin {
Version: mysql80.Version,
Requires: mysql80.Requires,
Excludes: mysql80.Excludes,
Install: mysql80.Install,
Uninstall: mysql80.Uninstall,
Update: mysql80.Update,
})
p = append(p, PanelPlugin{
Name: php74.Name,
@@ -73,6 +98,21 @@ func (r *PluginImpl) All() []PanelPlugin {
Version: php74.Version,
Requires: php74.Requires,
Excludes: php74.Excludes,
Install: php74.Install,
Uninstall: php74.Uninstall,
Update: php74.Update,
})
p = append(p, PanelPlugin{
Name: php80.Name,
Author: php80.Author,
Description: php80.Description,
Slug: php80.Slug,
Version: php80.Version,
Requires: php80.Requires,
Excludes: php80.Excludes,
Install: php80.Install,
Uninstall: php80.Uninstall,
Update: php80.Update,
})
return p
+21 -20
View File
@@ -61,10 +61,13 @@ type WebsiteSetting struct {
}
type WebsiteImpl struct {
setting Setting
}
func NewWebsiteImpl() *WebsiteImpl {
return &WebsiteImpl{}
return &WebsiteImpl{
setting: NewSettingImpl(),
}
}
// List 列出网站
@@ -88,7 +91,7 @@ func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) {
// path为空时,设置默认值
if len(website.Path) == 0 {
website.Path = "/www/wwwroot/" + website.Name
website.Path = r.setting.Get(models.SettingKeyWebsitePath) + "/" + website.Name
}
// path不为/开头时,返回错误
if website.Path[0] != '/' {
@@ -113,8 +116,7 @@ func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) {
tools.Mkdir(website.Path, 0755)
index := `
<!DOCTYPE html>
index := `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
@@ -159,8 +161,7 @@ func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) {
}
}
nginxConf := fmt.Sprintf(`
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
server
{
@@ -186,7 +187,7 @@ server
# waf标记位开始
waf on;
waf_rule_path /www/server/nginx/ngx_waf/assets/rules/;
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
waf_mode DYNAMIC;
waf_cc_deny rate=1000r/m duration=60m;
waf_cache capacity=50;
@@ -218,12 +219,12 @@ server
`, portList, domainList, website.Path, website.Php, website.Name, website.Name, website.Name)
tools.WriteFile("/www/server/panel/vhost/openresty/"+website.Name+".conf", nginxConf, 0644)
tools.WriteFile("/www/server/panel/vhost/openresty/rewrite/"+website.Name+".conf", "", 0644)
tools.WriteFile("/www/server/panel/vhost/openresty/ssl/"+website.Name+".pem", "", 0644)
tools.WriteFile("/www/server/panel/vhost/openresty/ssl/"+website.Name+".key", "", 0644)
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", nginxConf, 0644)
tools.WriteFile("/www/server/vhost/rewrite/"+website.Name+".conf", "", 0644)
tools.WriteFile("/www/server/vhost/ssl/"+website.Name+".pem", "", 0644)
tools.WriteFile("/www/server/vhost/ssl/"+website.Name+".key", "", 0644)
tools.ExecShellAsync("systemctl reload openresty")
tools.ExecShell("systemctl reload openresty")
// TODO 创建数据库
@@ -233,7 +234,7 @@ server
// Delete 删除网站
func (r *WebsiteImpl) Delete(id int) error {
var website models.Website
if err := facades.Orm().Query().Where("id", id).First(&website); err != nil {
if err := facades.Orm().Query().Where("id", id).FirstOrFail(&website); err != nil {
return err
}
@@ -241,13 +242,13 @@ func (r *WebsiteImpl) Delete(id int) error {
return err
}
tools.RemoveFile("/www/server/panel/vhost/openresty/" + website.Name + ".conf")
tools.RemoveFile("/www/server/panel/vhost/openresty/rewrite/" + website.Name + ".conf")
tools.RemoveFile("/www/server/panel/vhost/openresty/ssl/" + website.Name + ".pem")
tools.RemoveFile("/www/server/panel/vhost/openresty/ssl/" + website.Name + ".key")
tools.RemoveFile("/www/server/vhost/" + website.Name + ".conf")
tools.RemoveFile("/www/server/vhost/rewrite/" + website.Name + ".conf")
tools.RemoveFile("/www/server/vhost/ssl/" + website.Name + ".pem")
tools.RemoveFile("/www/server/vhost/ssl/" + website.Name + ".key")
tools.RemoveFile(website.Path)
tools.ExecShellAsync("systemctl reload openresty")
tools.ExecShell("systemctl reload openresty")
// TODO 删除数据库
@@ -261,7 +262,7 @@ func (r *WebsiteImpl) GetConfig(id int) (WebsiteSetting, error) {
return WebsiteSetting{}, err
}
config := tools.ReadFile("/www/server/panel/vhost/openresty/" + website.Name + ".conf")
config := tools.ReadFile("/www/server/vhost/" + website.Name + ".conf")
var setting WebsiteSetting
setting.Name = website.Name
@@ -339,7 +340,7 @@ func (r *WebsiteImpl) GetConfig(id int) (WebsiteSetting, error) {
setting.WafCache = match[1]
}
setting.Rewrite = tools.ReadFile("/www/server/panel/vhost/openresty/rewrite/" + website.Name + ".conf")
setting.Rewrite = tools.ReadFile("/www/server/vhost/rewrite/" + website.Name + ".conf")
setting.Log = tools.ExecShell("tail -n 100 /www/wwwlogs/" + website.Name + ".log")
return setting, nil
+6 -2
View File
@@ -2,6 +2,7 @@ package bootstrap
import (
"github.com/goravel/framework/foundation"
"github.com/goravel/framework/support/carbon"
"panel/config"
)
@@ -9,9 +10,12 @@ import (
func Boot() {
app := foundation.NewApplication()
//Bootstrap the application
// Bootstrap the application
app.Boot()
//Bootstrap the config.
// Bootstrap the config.
config.Boot()
// 设置 Carbon 时区
carbon.SetTimezone(carbon.PRC)
}
+1 -1
View File
@@ -14,7 +14,7 @@ func init() {
"connections": map[string]any{
"panel": map[string]any{
"driver": "sqlite",
"database": "database/panel.db",
"database": "/www/panel/database/panel.db",
"prefix": "",
"singular": false, // Table name is singular
},
+1 -1
View File
@@ -24,7 +24,7 @@ func init() {
"disks": map[string]any{
"local": map[string]any{
"driver": "local",
"root": "/",
"root": "storage/app",
"url": "http://localhost/",
},
},
+1 -1
View File
@@ -8,6 +8,6 @@ func init() {
config := facades.Config()
config.Add("panel", map[string]any{
"name": "耗子面板",
"version": "2.0.0",
"version": "v2.0.10",
})
}
+2 -2
View File
@@ -5,8 +5,8 @@ go 1.18
require (
github.com/gertd/go-pluralize v0.2.1
github.com/gin-contrib/static v0.0.1
github.com/gookit/color v1.5.3
github.com/goravel/framework v1.12.1-0.20230717105343-6ce864794883
github.com/gookit/color v1.5.4
github.com/goravel/framework v1.12.1-0.20230721095426-6f24ecdaf6a7
github.com/iancoleman/strcase v0.2.0
github.com/imroc/req/v3 v3.37.2
github.com/mojocn/base64Captcha v1.3.5
+5 -8
View File
@@ -85,6 +85,7 @@ github.com/aws/aws-sdk-go v1.37.16 h1:Q4YOP2s00NpB9wfmTDZArdcLRuG9ijbnoAwTW3ivle
github.com/aws/aws-sdk-go v1.37.16/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/brianvoe/gofakeit/v6 v6.23.0 h1:pgVhyWpYq4e0GEVCh2gdZnS/nBX+8SnyTBliHg5xjks=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM=
github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
@@ -169,8 +170,6 @@ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.8.0 h1:02X12E2I/4C1n+v90yTqrjRa8yuo7c3KeHI3FRznCvc=
github.com/glebarez/sqlite v1.8.0/go.mod h1:bpET16h1za2KOOMb8+jCp6UBP/iahDpfPQqSaYLTLx8=
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -345,8 +344,8 @@ github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK6
github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gookit/color v1.5.2/go.mod h1:w8h4bGiHeeBpvQVePTutdbERIUf3oJE5lZ8HM0UgXyg=
github.com/gookit/color v1.5.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE=
github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gookit/filter v1.1.4 h1:SXd6PEumiP/0jtF2crQRaz1wmKwHbW9xg5Ds6/ZP16w=
github.com/gookit/filter v1.1.4/go.mod h1:0CEPQvudso375RitQf9X8HerUg9cz8N7c/yn6b1RMzM=
github.com/gookit/goutil v0.5.12/go.mod h1:6vhWm/bSYXGE8poqFbFz6IGM7jV2r6qVhyK567SX/AI=
@@ -358,10 +357,8 @@ github.com/goravel/file-rotatelogs v0.0.0-20211215053220-2ab31dd9575c h1:obhFK91
github.com/goravel/file-rotatelogs v0.0.0-20211215053220-2ab31dd9575c/go.mod h1:YSWsLXlG16u5CWFaXNZHhEQD10+NwF3xfgDV816OwLE=
github.com/goravel/file-rotatelogs/v2 v2.4.1 h1:ogkeIFcTHSBRUBpZYiyJbpul8hkVXxHPuDbOaP78O1M=
github.com/goravel/file-rotatelogs/v2 v2.4.1/go.mod h1:euk9qr52WrzM8ICs1hecFcR4CZ/ZZOPdacHfvHgbOf0=
github.com/goravel/framework v1.12.1-0.20230710102458-737cce0f687c h1:KPggCIxAZghopvPXC0g0jCmFNbC+r5EawjHCcbfBOhQ=
github.com/goravel/framework v1.12.1-0.20230710102458-737cce0f687c/go.mod h1:SBsBTY8KTqSDipGPEZXnRRC1mWzKmL55Eo1P5aHNDgY=
github.com/goravel/framework v1.12.1-0.20230717105343-6ce864794883 h1:8+87CCYqt5O6GqAYmGWz/W6SXGKp6xyn+McMbtWgyug=
github.com/goravel/framework v1.12.1-0.20230717105343-6ce864794883/go.mod h1:quBqUHAyZutEs/TFfm9b/caI+BtyF82njGhNIL1QJM0=
github.com/goravel/framework v1.12.1-0.20230721095426-6f24ecdaf6a7 h1:2rkC/6M7tLx/Ya1TulO2GjtVRIGXTmpp0XppzZRloYA=
github.com/goravel/framework v1.12.1-0.20230721095426-6f24ecdaf6a7/go.mod h1:1lxwtCXMkotSmt0YWRg/s7EnMUFfmne9L1a50X9J0fA=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
+3
View File
@@ -32,5 +32,8 @@ func main() {
}
}()
// 启动计划任务
go facades.Schedule().Run()
select {}
}
+1 -1
View File
@@ -1,6 +1,6 @@
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_DEBUG=false
APP_HOST=0.0.0.0
APP_PORT=8888
+5 -4
View File
@@ -4,6 +4,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/goravel/framework/facades"
)
@@ -51,11 +52,11 @@ func ExecShell(shell string) string {
output, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error())
facades.Log().Errorf("[面板][Helpers] 执行命令 %s 失败: %s", shell, err.Error())
return ""
}
return string(output)
return strings.TrimSpace(string(output))
}
// ExecShellAsync 异步执行 shell 命令
@@ -64,13 +65,13 @@ func ExecShellAsync(shell string) {
err := cmd.Start()
if err != nil {
facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error())
facades.Log().Errorf("[面板][Helpers] 执行命令 %s 失败: %s", shell, err.Error())
}
go func() {
err := cmd.Wait()
if err != nil {
facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error())
facades.Log().Errorf("[面板][Helpers] 执行命令 %s 失败: %s", shell, err.Error())
}
}()
}
+1 -1
View File
@@ -48,7 +48,7 @@ func (s *SystemHelperTestSuite) TestRemoveFile() {
}
func (s *SystemHelperTestSuite) TestExecShell() {
s.Equal("test\n", ExecShell("echo 'test'"))
s.Equal("test", ExecShell("echo 'test'"))
}
func (s *SystemHelperTestSuite) TestExecShellAsync() {
+42 -5
View File
@@ -5,9 +5,11 @@ import (
"errors"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/gookit/color"
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/host"
@@ -114,17 +116,52 @@ func GetLatestPanelVersion() (PanelInfo, error) {
}
// UpdatePanel 更新面板
func UpdatePanel() error {
func UpdatePanel(proxy bool) error {
panelInfo, err := GetLatestPanelVersion()
if err != nil {
return err
}
cmd := exec.Command("/bin/bash", "-c", "wget -O panel.tar.gz "+panelInfo.DownloadUrl+" && tar -zxvf panel.tar.gz && rm -rf panel.tar.gz && chmod +x panel && ./panel artisan migrate")
_, err = cmd.Output()
if err != nil {
return errors.New("更新面板失败")
color.Greenln("最新版本: " + panelInfo.Version)
color.Greenln("下载链接: " + panelInfo.DownloadUrl)
color.Greenln("使用代理: " + strconv.FormatBool(proxy))
color.Greenln("备份面板配置...")
ExecShell("cp -f /www/panel/database/panel.db /tmp/panel.db.bak")
ExecShell("cp -f /www/panel/panel.conf /tmp/panel.conf.bak")
if !Exists("/tmp/panel.db.bak") || !Exists("/tmp/panel.conf.bak") {
return errors.New("备份面板配置失败")
}
color.Greenln("备份完成")
color.Greenln("清理旧版本...")
ExecShell("rm -rf /www/panel/*")
color.Greenln("清理完成")
color.Greenln("正在下载...")
if proxy {
ExecShell("wget -O /www/panel/panel.zip https://ghproxy.com/" + panelInfo.DownloadUrl)
} else {
ExecShell("wget -O /www/panel/panel.zip " + panelInfo.DownloadUrl)
}
color.Greenln("下载完成")
color.Greenln("更新新版本...")
ExecShell("cd /www/panel && unzip -o panel.zip && rm -rf panel.zip && chmod 700 panel")
color.Greenln("更新完成")
color.Greenln("恢复面板配置...")
ExecShell("cp -f /tmp/panel.db.bak /www/panel/database/panel.db")
ExecShell("cp -f /tmp/panel.conf.bak /www/panel/panel.conf")
if !Exists("/www/panel/database/panel.db") || !Exists("/www/panel/panel.conf") {
return errors.New("恢复面板配置失败")
}
ExecShell("/www/panel/panel --env=panel.conf artisan migrate")
color.Greenln("恢复完成")
color.Greenln("重启面板...")
ExecShell("systemctl restart panel")
color.Greenln("重启完成")
return nil
}
-30
View File
@@ -1,30 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env
pnpm-lock.yaml
+194
View File
@@ -0,0 +1,194 @@
/* 样式加载完毕的标识 */
html #layuicss-cron {
display: none;
position: absolute;
width: 1989px;
}
/* 主体结构 */
.layui-cron {
width: 550px;
position: absolute;
z-index: 99999999;
margin: 5px 0;
border-radius: 2px;
font-size: 14px;
-webkit-animation-duration: 0.3s;
animation-duration: 0.3s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
background-color: white;
display: flex;
flex-direction: column;
box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 5px 0px;
-webkit-animation-name: cron-upbit;
animation-name: cron-upbit;
border: 1px solid #e6e6e6;
}
.layui-cron-main ul {
padding-left: 10px;
}
@-webkit-keyframes cron-upbit {
/* 微微往上滑入 */
from {
-webkit-transform: translate3d(0, 20px, 0);
opacity: 0.3;
}
to {
-webkit-transform: translate3d(0, 0, 0);
opacity: 1;
}
}
@keyframes cron-upbit {
from {
transform: translate3d(0, 20px, 0);
opacity: 0.3;
}
to {
transform: translate3d(0, 0, 0);
opacity: 1;
}
}
/* tabs */
.layui-cron>.layui-tab {
margin: 0;
box-shadow: none;
border: none;
}
/* 行 */
.cron-row {
padding-left: 13px;
}
/* 格 */
.cron-grid {
padding-left: 15px;
}
/* 表达式 */
.cron-title {
font-weight: 700;
font-size: 14px;
margin: 10px;
margin-bottom: 0;
}
.cron-box {
margin: 10px;
}
.cron-box+.cron-box {
margin-top: 0;
}
/* 按钮 */
.cron-footer-btns {
text-align: right;
margin-right: 10px;
margin-bottom: 10px;
}
.cron-footer-btns span {
height: 26px;
line-height: 26px;
margin: 0 0 0 -1px;
padding: 0 10px;
border: 1px solid #C9C9C9;
background-color: #fff;
white-space: nowrap;
vertical-align: top;
border-radius: 2px;
display: inline-block;
cursor: pointer;
font-size: 12px;
box-sizing: border-box;
color: #666;
}
.cron-footer-btns span:hover {
color: #5FB878;
}
/* 表单 */
.layui-cron .layui-form-radio {
margin-right: 0;
}
.cron-form {
line-height: 28px;
font-size: 14px;
}
.cron-input-mid {
display: inline-block;
vertical-align: middle;
margin-top: 6px;
background-color: #e5e5e5;
padding: 0 12px;
height: 28px;
line-height: 28px;
border: 1px solid #ccc;
box-sizing: border-box;
}
.cron-input {
display: inline-block;
vertical-align: middle;
margin-top: 6px;
padding: 0 8px;
background-color: #fff;
border: 1px solid #ccc;
height: 28px;
line-height: 28px;
box-sizing: border-box;
width: 80px;
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
}
.cron-input:focus {
outline: 0;
border: 1px solid #01AAED;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 4px 0px #01AAED;
translate: 1s;
}
.layui-cron .layui-form-checkbox[lay-skin="primary"] span {
padding-right: 10px;
min-width: 16px;
}
.layui-cron .layui-form-checkbox[lay-skin="primary"] {
padding-left: 22px;
margin-top: 5px;
}
.layui-cron input[type=number] {
-moz-appearance:textfield;
}
.layui-cron input[type=number]::-webkit-inner-spin-button,
.layui-cron input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.cron-tips{
color: grey;
line-height: 28px;
height: 28px;
display: inline-block;
vertical-align: middle;
margin-top: 8px;
margin-left: 5px;
}
+970
View File
@@ -0,0 +1,970 @@
/**
@ Namelayui.cron Cron表达式解析器
@ Author:贝哥哥
@ LicenseMIT
*/
layui.define(['lay', 'element', 'form'], function (exports) { //假如该组件依赖 layui.form
var $ = layui.$, layer = layui.layer, lay = layui.lay, element = layui.element, form = layui.form
//字符常量
, MOD_NAME = 'cron', ELEM = '.layui-cron', THIS = 'layui-this', SHOW = 'layui-show', HIDE = 'layui-hide'
, ELEM_STATIC = 'layui-cron-static', ELEM_FOOTER = 'layui-cron-footer', ELEM_CONFIRM = '.cron-btns-confirm',
ELEM_HINT = 'layui-cron-hint'
, ELEM_RUN_HINT = 'layui-cron-run-hint'
//外部接口
, cron = {
v: '2.0.0' // cron 组件当前版本
, index: layui.cron ? (layui.cron.index + 10000) : 0 // corn 实例标识
//设置全局项
, set: function (options) {
var that = this;
that.config = $.extend({}, that.config, options);
return that;
}
//事件监听
, on: function (events, callback) {
return layui.onevent.call(this, MOD_NAME, events, callback);
}
//主体CSS等待事件
, ready: function (fn) {
var cssPath = layui.cache.base + "cron.css?v=" + cron.v;
layui.link(cssPath, fn, "cron"); //此处的“cron”要对应 cron.css 中的样式: html #layuicss-cron{}
return this;
}
}
//操作当前实例
, thisIns = function () {
var that = this, options = that.config, id = options.id || options.index;
return {
//提示框
hint: function (content) {
that.hint.call(that, content);
}, config: options
}
}
//构造器,创建实例
, Class = function (options) {
var that = this;
that.index = ++cron.index;
that.config = $.extend({}, that.config, cron.config, options);
cron.ready(function () {
that.init();
});
};
//默认配置
Class.prototype.config = {
value: null // 当前表达式值,每秒执行一次
,
isInitValue: true //用于控制是否自动向元素填充初始值(需配合 value 参数使用)
,
lang: "cn" //语言,只支持cn/en,即中文和英文
,
tabs: [{key: 'minutes', range: '0-59'}, {
key: 'hours', range: '0-23'
}, {key: 'days', range: '1-31'}, {key: 'months', range: '1-12'}, {key: 'weeks', range: '1-7'}],
defaultCron: {minutes: "*", hours: "*", days: "*", months: "*", weeks: "*"},
trigger: "click" //呼出控件的事件
,
btns: ['run', 'confirm'] //右下角显示的按钮,会按照数组顺序排列
,
position: null //控件定位方式定位, 默认absolute,支持:fixed/absolute/static
,
zIndex: null //控件层叠顺序
,
show: false //是否直接显示,如果设置 true,则默认直接显示控件
,
showBottom: true //是否显示底部栏
,
done: null //控件选择完毕后的回调,点击运行/确定也均会触发
,
run: null // 最近运行时间接口
};
//多语言
Class.prototype.lang = function () {
var that = this, options = that.config, text = {
cn: {
tabs: [{title: "分"}, {title: "时"}, {title: "日"}, {title: "月"}, {
title: "周",
rateBegin: "第",
rateMid: "周的星期",
rateEnd: ""
}],
every: "每",
unspecified: "不指定",
period: "周期",
periodFrom: "从",
rate: "按照",
rateBegin: "从",
rateMid: "开始,每",
rateEnd: "执行一次",
weekday: "工作日",
weekdayPrefix: "每月",
weekdaySuffix: "号最近的那个工作日",
lastday: "本月最后一日",
lastweek: "本月最后一个星期",
custom: "指定",
tools: {
confirm: '确定', run: '运行'
},
formatError: ['Cron格式不合法', '<br>已为你重置']
}, en: {
tabs: [{title: "Minutes"}, {title: "Hours"}, {title: "Days"}, {title: "Months"}, {title: "Weeks"}],
every: "Every ",
unspecified: "Unspecified",
period: "Period",
periodFrom: "From",
rate: "According to",
rateBegin: "begin at",
rateMid: ", every",
rateEnd: " execute once",
weekday: "Weekday",
weekdayPrefix: "Every month at ",
weekdaySuffix: "号最近的那个工作日",
lastday: "Last day of the month",
lastweek: "本月最后一个星期",
custom: "Custom",
tools: {
confirm: 'Confirm', run: 'Run'
},
formatError: ['The cron format error', '<br>It has been reset']
}
};
return text[options.lang] || text['cn'];
};
//初始准备
Class.prototype.init = function () {
var that = this, options = that.config, isStatic = options.position === 'static';
options.elem = lay(options.elem);
options.eventElem = lay(options.eventElem);
if (!options.elem[0]) return;
//如果不是input|textarea元素,则默认采用click事件
if (!that.isInput(options.elem[0])) {
if (options.trigger === 'focus') {
options.trigger = 'click';
}
}
// 设置渲染所绑定元素的唯一KEY
if (!options.elem.attr('lay-key')) {
options.elem.attr('lay-key', that.index);
options.eventElem.attr('lay-key', that.index);
}
// 当前实例主面板ID
that.elemID = 'layui-icon' + options.elem.attr('lay-key');
//默认赋值
if (options.value && options.isInitValue) {
that.setValue(options.value);
}
if (!options.value) {
options.value = options.elem[0].value || '';
}
var cronArr = options.value.split(' ');
if (cronArr.length >= 6) {
options.cron = {
minutes: cronArr[0],
hours: cronArr[1],
days: cronArr[2],
months: cronArr[3],
weeks: cronArr[4],
};
} else {
options.cron = lay.extend({}, options.defaultCron);
}
if (options.show || isStatic) that.render();
isStatic || that.events();
};
// 控件主体渲染
Class.prototype.render = function () {
var that = this, options = that.config, lang = that.lang(), isStatic = options.position === 'static',
tabFilter = 'cron-tab' + options.elem.attr('lay-key')
//主面板
, elem = that.elem = lay.elem('div', {
id: that.elemID, 'class': ['layui-cron', isStatic ? (' ' + ELEM_STATIC) : ''].join('')
})
// tab 内容区域
, elemTab = that.elemTab = lay.elem('div', {
'class': 'layui-tab layui-tab-card', 'lay-filter': tabFilter
}), tabHead = lay.elem('ul', {
'class': 'layui-tab-title'
}), tabContent = lay.elem('div', {
'class': 'layui-tab-content'
})
//底部区域
, divFooter = that.footer = lay.elem('div', {
'class': ELEM_FOOTER
});
if (options.zIndex) elem.style.zIndex = options.zIndex;
// 生成tab 内容区域
elemTab.appendChild(tabHead);
elemTab.appendChild(tabContent);
lay.each(lang.tabs, function (i, item) {
// 表头
var li = lay.elem('li', {
'class': i === 0 ? THIS : "", 'lay-id': i
});
li.innerHTML = item.title;
tabHead.appendChild(li);
// 表体
tabContent.appendChild(that.getTabContentChildElem(i));
});
// 主区域
elemMain = that.elemMain = lay.elem('div', {
'class': 'layui-cron-main'
});
elemMain.appendChild(elemTab);
//生成底部栏
lay(divFooter).html(function () {
var html = [], btns = [];
lay.each(options.btns, function (i, item) {
var title = lang.tools[item] || 'btn';
btns.push('<span lay-type="' + item + '" class="cron-btns-' + item + '">' + title + '</span>');
});
html.push('<div class="cron-footer-btns">' + btns.join('') + '</div>');
return html.join('');
}());
//插入到主区域
elem.appendChild(elemMain);
options.showBottom && elem.appendChild(divFooter);
//移除上一个控件
that.remove(Class.thisElemCron);
//如果是静态定位,则插入到指定的容器中,否则,插入到body
isStatic ? options.elem.append(elem) : (document.body.appendChild(elem)
, that.position());
that.checkCron();
that.elemEvent(); // 主面板事件
Class.thisElemCron = that.elemID;
form.render();
}
// 渲染 tab 子控件
Class.prototype.getTabContentChildElem = function (index) {
var that = this, options = that.config, tabItem = options.tabs[index], tabItemKey = tabItem.key,
lang = that.lang(), tabItemLang = lang.tabs[index], cron = options.cron,
formFilter = 'cronForm' + tabItemKey + options.elem.attr('lay-key'), data = function () {
if (cron[tabItemKey].indexOf('-') != -1) {
// 周期数据
var arr = cron[tabItemKey].split('-');
return {
type: 'range', start: arr[0], end: arr[1]
};
}
if (cron[tabItemKey].indexOf('/') != -1) {
// 频率数据
var arr = cron[tabItemKey].split('/');
return {
type: 'rate', begin: arr[0], rate: arr[1]
};
}
if (cron[tabItemKey].indexOf(',') != -1 || /^\+?[0-9][0-9]*$/.test(cron[tabItemKey])) {
// 按照指定执行
var arr = cron[tabItemKey].split(',').map(Number);
return {
type: 'custom', values: arr
};
}
if (cron[tabItemKey].indexOf('W') != -1) {
// 最近的工作日
var value = cron[tabItemKey].replace('W', '');
return {
type: 'weekday', value: value
};
}
if (index === 2 && cron[tabItemKey] === 'L') {
// 本月最后一日
return {
type: 'lastday', value: 'L'
};
}
if (index === 4 && cron[tabItemKey].indexOf('L') != -1) {
// 本月最后一个周 value
var value = cron[tabItemKey].replace('L', '');
return {
type: 'lastweek', value: value
};
}
if (cron[tabItemKey] === '*') {
// 每次
return {
type: 'every', value: '*'
};
}
if (cron[tabItemKey] === '?' || cron[tabItemKey] === undefined || cron[tabItemKey] === '') {
// 不指定
return {
//type: 'unspecified', value: cron[tabItemKey]
type: 'every', value: '*'
};
}
}(), rangeData = function () {
if (tabItem.range) {
var arr = tabItem.range.split('-');
return {
min: parseInt(arr[0]), max: parseInt(arr[1])
};
}
}();
var elem = lay.elem('div', {
'class': 'layui-tab-item layui-form ' + (index === 0 ? SHOW : ""), 'lay-filter': formFilter
});
// 每次
elem.appendChild(function () {
var everyRadio = lay.elem('input', {
'name': tabItemKey + '[type]',
'type': 'radio',
'value': 'every',
'title': lang.every + tabItemLang.title
});
if (data.type === 'every') {
lay(everyRadio).attr('checked', true);
}
var everyDiv = lay.elem('div', {
'class': 'cron-row'
});
everyDiv.appendChild(everyRadio);
return everyDiv;
}());
// 不指定,从日开始
/*if (index >= 2) {
elem.appendChild(function () {
var unspecifiedRadio = lay.elem('input', {
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'unspecified', 'title': lang.unspecified
});
if (data.type === 'unspecified') {
lay(unspecifiedRadio).attr('checked', true);
}
var unspecifiedDiv = lay.elem('div', {
'class': 'cron-row'
});
unspecifiedDiv.appendChild(unspecifiedRadio);
return unspecifiedDiv;
}());
}*/
// 周期
var rangeChild = [function () {
var rangeRadio = lay.elem('input', {
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'range', 'title': lang.period
});
if (data.type === 'range') {
lay(rangeRadio).attr('checked', true);
}
return rangeRadio;
}(), function () {
var elem = lay.elem('div', {
'class': 'cron-input-mid'
});
elem.innerHTML = lang.periodFrom;
return elem;
}(), function () {
var elem = lay.elem('input', {
'class': 'cron-input', 'type': 'number', 'name': 'rangeStart', 'value': data.start || ''
});
return elem;
}(), function () {
var elem = lay.elem('div', {
'class': 'cron-input-mid'
});
elem.innerHTML = '-';
return elem;
}(), function () {
var elem = lay.elem('input', {
'class': 'cron-input', 'type': 'number', 'name': 'rangeEnd', 'value': data.end || ''
});
return elem;
}(), function () {
var elem = lay.elem('div', {
'class': 'cron-input-mid'
});
elem.innerHTML = tabItemLang.title;
return elem;
}()]
, rangeDiv = lay.elem('div', {
'class': 'cron-row'
});
lay.each(rangeChild, function (i, item) {
rangeDiv.appendChild(item);
});
if (tabItem.range) {
var rangeTip = lay.elem('div', {
'class': 'cron-tips'
});
rangeTip.innerHTML = ['(', tabItem.range, ')'].join('');
rangeDiv.appendChild(rangeTip);
}
elem.appendChild(rangeDiv);
// 频率,年没有
if (index < 6) {
var rateChild = [function () {
var rateRadio = lay.elem('input', {
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'rate', 'title': lang.rate
});
if (data.type === 'rate') {
lay(rateRadio).attr('checked', true);
}
return rateRadio;
}(), function () {
var elem = lay.elem('div', {
'class': 'cron-input-mid'
});
elem.innerHTML = tabItemLang.rateBegin || lang.rateBegin;
return elem;
}(), function () {
var elem = lay.elem('input', {
'class': 'cron-input', 'type': 'number', 'name': 'begin', 'value': data.begin || ''
});
return elem;
}(), function () {
var elem = lay.elem('div', {
'class': 'cron-input-mid'
});
elem.innerHTML = tabItemLang.rateMid || (tabItemLang.title + lang.rateMid);
return elem;
}(), function () {
var elem = lay.elem('input', {
'class': 'cron-input', 'type': 'number', 'name': 'rate', 'value': data.rate || ''
});
return elem;
}(), function () {
var elem = lay.elem('div', {
'class': 'cron-input-mid'
});
elem.innerHTML = undefined != tabItemLang.rateEnd ? tabItemLang.rateEnd : (tabItemLang.title + lang.rateEnd);
if (undefined != tabItemLang.rateEnd && tabItemLang.rateEnd === '') {
lay(elem).addClass(HIDE);
}
return elem;
}()]
, rateDiv = lay.elem('div', {
'class': 'cron-row'
});
lay.each(rateChild, function (i, item) {
rateDiv.appendChild(item);
});
if (tabItem.range) {
var rateTip = lay.elem('div', {
'class': 'cron-tips'
});
if (index === 4) {
// 周
rateTip.innerHTML = '(1-4/1-7)';
} else {
rateTip.innerHTML = ['(', rangeData.min, '/', (rangeData.max + (index <= 1 ? 1 : 0)), ')'].join('');
}
rateDiv.appendChild(rateTip);
}
elem.appendChild(rateDiv);
}
// 特殊:日(最近的工作日、最后一日),周(最后一周)
/*if (index === 2) {
// 日
// 最近的工作日
var weekChild = [function () {
var weekRadio = lay.elem('input', {
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'weekday', 'title': lang.weekday
});
if (data.type === 'weekday') {
lay(weekRadio).attr('checked', true);
}
return weekRadio;
}(), function () {
var elem = lay.elem('div', {
'class': 'cron-input-mid'
});
elem.innerHTML = lang.weekdayPrefix;
return elem;
}(), function () {
var elem = lay.elem('input', {
'class': 'cron-input', 'type': 'number', 'name': 'weekday', 'value': data.value || ''
});
return elem;
}(), function () {
var elem = lay.elem('div', {
'class': 'cron-input-mid'
});
elem.innerHTML = lang.weekdaySuffix;
return elem;
}(), function () {
var elem = lay.elem('div', {
'class': 'cron-tips'
});
elem.innerHTML = ['(', tabItem.range, ')'].join('');
return elem;
}()]
, weekDiv = lay.elem('div', {
'class': 'cron-row'
});
lay.each(weekChild, function (i, item) {
weekDiv.appendChild(item);
});
elem.appendChild(weekDiv);
// 本月最后一日
elem.appendChild(function () {
var lastRadio = lay.elem('input', {
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'lastday', 'title': lang.lastday
});
if (data.type === 'lastday') {
lay(lastRadio).attr('checked', true);
}
var lastDiv = lay.elem('div', {
'class': 'cron-row'
});
lastDiv.appendChild(lastRadio);
return lastDiv;
}());
}
if (index === 4) {
// 本月最后一个周几
var lastWeekChild = [function () {
var lastWeekRadio = lay.elem('input', {
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'lastweek', 'title': lang.lastweek
});
if (data.type === 'lastweek') {
lay(lastWeekRadio).attr('checked', true);
}
return lastWeekRadio;
}(), function () {
var elem = lay.elem('input', {
'class': 'cron-input', 'type': 'number', 'name': 'lastweek', 'value': data.value || ''
});
return elem;
}(), function () {
var elem = lay.elem('div', {
'class': 'cron-tips'
});
elem.innerHTML = ['(', tabItem.range, ')'].join('');
return elem;
}()]
, lastWeekDiv = lay.elem('div', {
'class': 'cron-row'
});
lay.each(lastWeekChild, function (i, item) {
lastWeekDiv.appendChild(item);
});
elem.appendChild(lastWeekDiv);
}*/
// 指定
if (index <= 4) {
elem.appendChild(function () {
var customRadio = lay.elem('input', {
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'custom', 'title': lang.custom
});
if (data.type === 'custom') {
lay(customRadio).attr('checked', true);
}
var customDiv = lay.elem('div', {
'class': 'cron-row'
});
customDiv.appendChild(customRadio);
return customDiv;
}());
// 指定数值,时分秒显示两位数,自动补零
elem.appendChild(function () {
var customGrid = lay.elem('div', {
'class': 'cron-grid'
});
var i = rangeData.min;
while (i <= rangeData.max) {
// 时分秒显示两位数,自动补零
var gridItemValue = index <= 1 ? lay.digit(i, 2) : i;
var gridItem = lay.elem('input', {
'type': 'checkbox',
'title': gridItemValue,
'lay-skin': 'primary',
'name': tabItemKey + '[custom]',
'value': i
});
if (data.values && data.values.includes(i)) {
lay(gridItem).attr('checked', true);
}
customGrid.appendChild(gridItem);
i++;
}
return customGrid;
}());
}
return elem;
}
//是否输入框
Class.prototype.isInput = function (elem) {
return /input|textarea/.test(elem.tagName.toLocaleLowerCase());
};
// 绑定的元素事件处理
Class.prototype.events = function () {
var that = this, options = that.config
//绑定呼出控件事件
, showEvent = function (elem, bind) {
elem.on(options.trigger, function () {
bind && (that.bindElem = this);
that.render();
});
};
if (!options.elem[0] || options.elem[0].eventHandler) return;
showEvent(options.elem, 'bind');
showEvent(options.eventElem);
//绑定关闭控件事件
lay(document).on('click', function (e) {
if (e.target === options.elem[0] || e.target === options.eventElem[0] || e.target === lay(options.closeStop)[0]) {
return;
}
that.remove();
}).on('keydown', function (e) {
if (e.keyCode === 13) {
if (lay('#' + that.elemID)[0] && that.elemID === Class.thisElemDate) {
e.preventDefault();
lay(that.footer).find(ELEM_CONFIRM)[0].click();
}
}
});
//自适应定位
lay(window).on('resize', function () {
if (!that.elem || !lay(ELEM)[0]) {
return false;
}
that.position();
});
options.elem[0].eventHandler = true;
};
// 主面板事件
Class.prototype.elemEvent = function () {
var that = this, options = that.config, tabFilter = 'cron-tab' + options.elem.attr('lay-key');
// 阻止主面板点击冒泡,避免因触发文档事件而关闭主面
lay(that.elem).on('click', function (e) {
lay.stope(e);
});
// tab选项卡切换
var lis = lay(that.elemTab).find('li');
lis.on('click', function () {
var layid = lay(this).attr('lay-id');
if (undefined === layid) {
return;
}
element.tabChange(tabFilter, layid);
});
// cron选项点击
form.on('radio', function (data) {
var $parent = data.othis.parent();
var formFilter = $parent.parent().attr('lay-filter');
var formData = form.val(formFilter);
var radioType = data.value;
if ('range' === radioType) {
// 范围
form.val(formFilter, {
rangeStart: formData.rangeStart || 0, rangeEnd: formData.rangeEnd || 2
});
}
if ('rate' === radioType) {
// 频率
form.val(formFilter, {
begin: formData.begin || 0, rate: formData.rate || 2
});
}
if ('custom' === radioType) {
// custom
var $grid = $parent.next();
if ($grid.find(':checkbox:checked').length <= 0) {
$grid.children(':checkbox:first').next().click()
}
}
if ('weekday' === radioType) {
// weekday
form.val(formFilter, {
weekday: formData.weekday || 1
});
}
if ('lastweek' === radioType) {
// lastweek
form.val(formFilter, {
lastweek: formData.lastweek || 1
});
}
});
//gird checkbox点击时,自动选中radio
form.on('checkbox', function (data) {
//触发选项的点击事件
var $parent = data.othis.parent().parent();
//循环父级,找到子级的子级找到value="custom"的radio
var $radio = $parent.children().find('input[value="custom"]');
//触发radio的点击事件
$radio.next().click();
});
//点击底部按钮
lay(that.footer).find('span').on('click', function () {
var type = lay(this).attr('lay-type');
that.tool(this, type);
});
};
//底部按钮点击事件
Class.prototype.tool = function (btn, type) {
var that = this, options = that.config, lang = that.lang(), isStatic = options.position === 'static', active = {
//运行
run: function () {
var value = that.parse();
var loading = layer.load();
$.get(options.run, {cron: value}, function (res) {
layer.close(loading);
if (res.code !== 0) {
return that.hint(res.msg);
}
that.runHint(res.data);
}, 'json').fail(function () {
layer.close(loading);
that.hint('服务器异常!');
});
}
//确定
, confirm: function () {
var value = that.parse();
that.done([value]);
that.setValue(value).remove()
}
};
active[type] && active[type]();
};
//执行 done/change 回调
Class.prototype.done = function (param, type) {
var that = this, options = that.config;
param = param || [that.parse()];
typeof options[type || 'done'] === 'function' && options[type || 'done'].apply(options, param);
return that;
};
// 解析cron表达式
Class.prototype.parse = function () {
var that = this, options = that.config, valueArr = [];
lay.each(options.tabs, function (index, item) {
var key = item.key;
var formFilter = 'cronForm' + key + options.elem.attr('lay-key');
var formData = form.val(formFilter);
var radioType = (key + '[type]');
var current = "";
if (formData[radioType] === 'every') {
// 每次
current = "*";
}
if (formData[radioType] === 'range') {
// 范围
current = formData.rangeStart + "-" + formData.rangeEnd;
}
if (formData[radioType] === 'rate') {
// 频率
current = formData.begin + "/" + formData.rate;
}
if (formData[radioType] === 'custom') {
// 指定
var checkboxName = (item.key + '[custom]');
var customArr = [];
$('input[name="' + checkboxName + '"]:checked').each(function () {
customArr.push($(this).val());
});
current = customArr.join(',');
}
if (formData[radioType] === 'weekday') {
// 每月 formData.weekday 号最近的那个工作日
current = formData.weekday + "W";
}
if (formData[radioType] === 'lastday') {
// 本月最后一日
current = "L";
}
if (formData[radioType] === 'lastweek') {
// 本月最后星期
current = formData.lastweek + "L";
}
if (formData[radioType] === 'unspecified' && index != 6) {
// 不指定
current = "?";
}
if (current !== "") {
valueArr.push(current);
options.cron[key] = current;
}
});
return valueArr.join(' ');
};
//控件移除
Class.prototype.remove = function (prev) {
var that = this, options = that.config, elem = lay('#' + (prev || that.elemID));
if (!elem[0]) return that;
if (!elem.hasClass(ELEM_STATIC)) {
that.checkCron(function () {
elem.remove();
});
}
return that;
};
//定位算法
Class.prototype.position = function () {
var that = this, options = that.config;
lay.position(that.bindElem || options.elem[0], that.elem, {
position: options.position
});
return that;
};
//提示
Class.prototype.hint = function (content) {
var that = this, options = that.config, div = lay.elem('div', {
'class': ELEM_HINT
});
if (!that.elem) return;
div.innerHTML = content || '';
lay(that.elem).find('.' + ELEM_HINT).remove();
that.elem.appendChild(div);
clearTimeout(that.hinTimer);
that.hinTimer = setTimeout(function () {
lay(that.elem).find('.' + ELEM_HINT).remove();
}, 3000);
};
//运行提示
Class.prototype.runHint = function (runList) {
var that = this, options = that.config, div = lay.elem('div', {
'class': ELEM_RUN_HINT
});
// debugger;
if (!that.elem || !runList || !runList.length) return;
lay(div).html(function () {
var html = [];
lay.each(runList, function (i, item) {
html.push('<div class="cron-run-list">' + item + '</div>');
});
return html.join('');
}());
lay(that.elem).find('.' + ELEM_RUN_HINT).remove();
that.elem.appendChild(div);
};
//赋值
Class.prototype.setValue = function (value = '') {
var that = this, options = that.config, elem = that.bindElem || options.elem[0],
valType = that.isInput(elem) ? 'val' : 'html'
options.position === 'static' || lay(elem)[valType](value || '');
elem.textContent = '生成';
return this;
};
//cron校验
Class.prototype.checkCron = function (fn) {
var that = this, options = that.config, lang = that.lang(), elem = that.bindElem || options.elem[0],
value = that.isInput(elem) ? elem.value : (options.position === 'static' ? '' : elem.innerHTML)
, checkValid = function (value = "") {
};
// cron 值,多个空格替换为一个空格,去掉首尾空格
value = value || options.value;
if (typeof value === 'string') {
value = value.replace(/\s+/g, ' ').replace(/^\s|\s$/g, '');
}
if (fn === 'init') return checkValid(value), that;
value = that.parse();
if (value) {
that.setValue(value);
}
fn && fn();
return that;
};
//核心入口
cron.render = function (options) {
var ins = new Class(options);
return thisIns.call(ins);
};
exports('cron', cron);
});
+285
View File
@@ -0,0 +1,285 @@
<!--
Name: 计划任务
Author: 耗子
Date: 2023-07-21
-->
<title>计划任务</title>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">添加计划任务</div>
<div class="layui-card-body">
<form class="layui-form" action="" lay-filter="cron-add-form">
<div class="layui-form-item">
<label class="layui-form-label">任务名</label>
<div class="layui-input-inline">
<input type="text" name="name" lay-verify="required" placeholder="请输入任务名称"
autocomplete="off" class="layui-input">
</div>
<div class="layui-form-mid layui-word-aux">请填写任务名称</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">执行周期</label>
<div class="layui-input-inline">
<input type="text" name="time" id="cron-add-time"
lay-verify="required" placeholder="请选择或输入cron表达式" class="layui-input">
</div>
<div class="layui-form-mid layui-word-aux">请务必正确填写执行周期</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">脚本内容</label>
<div class="layui-input-block">
<div id="cron-add-script-editor"
style="height: 250px;"># 在此输入你要执行的脚本内容</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit="" lay-filter="cron-add-submit">立即提交</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">计划任务列表</div>
<div class="layui-card-body">
<table id="panel-cron" lay-filter="panel-cron"></table>
<!-- 操作按钮模板 -->
<script type="text/html" id="cron-table-edit">
<a class="layui-btn layui-btn-xs" lay-event="log">日志</a>
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="del">删除</a>
</script>
<!-- 运行开关 -->
<script type="text/html" id="cron-table-status">
<input type="checkbox" name="cron-status" lay-skin="switch" lay-text="ON|OFF"
lay-filter="cron-status"
value="{{ d.status }}" data-id="{{ d.id }}"
{{ d.status==
1 ? 'checked' : '' }}>
</script>
</div>
</div>
</div>
<script>
var cronAddScriptEditor = ace.edit("cron-add-script-editor", {
mode: "ace/mode/sh",
selectionStyle: "text"
});
var cronEditScriptEditor;
layui.use(['admin', 'table', 'jquery', 'cron'], function () {
var $ = layui.$
, form = layui.form
, table = layui.table
, admin = layui.admin
, cron = layui.cron;
cron.render({
elem: "#cron-add-time",
btns: ['confirm'],
show: false,
done: function (value) {
$('#cron-add-time').val(value);
}
});
form.render();
table.render({
elem: '#panel-cron'
, url: '/api/panel/cron/list'
, cols: [[
{field: 'id', hide: true, title: 'ID'}
, {field: 'name', width: 150, title: '任务名', sort: true}
, {field: 'type', width: 150, title: '任务类型', sort: true}
, {field: 'status', title: '启用', width: 100, templet: '#cron-table-status', unresize: true}
, {field: 'time', width: 200, title: '任务周期(cron表达式)'}
, {field: 'updated_at', title: '上次运行时间'}
, {
field: 'edit',
width: 180,
title: '操作',
templet: '#cron-table-edit',
fixed: 'right',
align: 'left'
}
]]
, page: true
, text: {
none: '暂无数据'
}
, parseData: function (res) {
return {
"code": res.code,
"msg": res.message,
"count": res.data.total,
"data": res.data.items
};
}
});
// 工具条
table.on('tool(panel-cron)', function (obj) {
let data = obj.data;
if (obj.event === 'log') {
// 打开日志弹窗
admin.popup({
title: '日志'
,
area: ['80%', '80%']
,
id: 'cron-log'
,
content: '<pre id="cron-log-view" style="overflow: auto; height: 95%;border: 0 none; line-height:23px; padding: 15px; margin: 0; white-space: pre-wrap; background-color: rgb(51,51,51); color:#f1f1f1; border-radius:0;"></pre>'
,
success: function (layero, index) {
admin.req({
url: '/api/panel/cron/log?id=' + data.id
, type: 'GET'
, success: function (res) {
if (res.code === 0) {
$('#cron-log-view').html(res.data);
} else {
layer.msg(res.message, {icon: 2, time: 1000});
}
}
});
}
});
} else if (obj.event === 'edit') {
// 打开编辑弹窗
admin.req({
url: '/api/panel/cron/script?id=' + data.id
, type: 'GET'
, success: function (res) {
if (res.code === 0) {
admin.popup({
title: '编辑'
,
area: ['80%', '80%']
,
id: 'cron-log'
,
content: '任务名&nbsp;&nbsp;&nbsp;&nbsp;<div class="layui-input-inline" style="width: 190px;"><input type="text" name="cron-edit-name" placeholder="请输入任务名称" autocomplete="off" class="layui-input" value="' + data.name + '"></div>&nbsp;&nbsp;&nbsp;&nbsp;执行周期&nbsp;&nbsp;&nbsp;&nbsp;<div class="layui-input-inline" style="width: 190px;"><input id="cron-edit-time-' + data.id + '" type="text" name="cron-edit-time" placeholder="请输入执行周期" autocomplete="off" class="layui-input" value="' + data.time + '"/></div><hr><div id="cron-edit-script-editor" style="height: 80%;">' + res.data + '</div><br><button id="cron-edit-' + data.id + '" class="layui-btn">保存</button>'
,
success: function (layero, index) {
cronEditScriptEditor = ace.edit("cron-edit-script-editor", {
mode: "ace/mode/sh",
selectionStyle: "text"
});
cron.render({
elem: "#cron-edit-time-" + data.id,
btns: ['confirm'],
show: false,
done: function (value) {
$('#cron-add-time').val(value);
}
});
$('#cron-edit-' + data.id).click(function () {
admin.req({
url: '/api/panel/cron/update'
, type: 'POST'
, data: {
id: data.id,
name: $('input[name="cron-edit-name"]').val(),
time: $('input[name="cron-edit-time"]').val(),
script: cronEditScriptEditor.getValue()
}
, success: function (res) {
if (res.code === 0) {
layer.msg('保存成功', {icon: 1, time: 1000});
table.reload('panel-cron');
layer.close(index);
} else {
layer.msg(res.message, {icon: 2, time: 1000});
}
}
});
});
}
});
} else {
layer.msg(res.message, {icon: 2, time: 1000});
}
}
});
} else if (obj.event === 'del') {
layer.confirm('确定删除计划任务' + data.name + '吗?', function (index) {
layer.close(index);
admin.req({
url: '/api/panel/cron/delete',
type: 'POST',
data: {
id: data.id
}
, success: function (res) {
if (res.code === 0) {
table.reload('panel-cron');
layer.msg('计划任务:' + data.name + ' 已删除', {
icon: 1,
time: 1000
});
} else {
layer.msg(res.message, {icon: 2, time: 1000});
}
}
});
});
}
});
form.on('switch(cron-status)', function (obj) {
let $ = layui.$;
let id = $(this).data('id');
let status = obj.elem.checked;
admin.req({
url: '/api/panel/cron/status',
type: 'POST',
data: {
id: id,
status: status
}
, success: function (res) {
if (res.code === 0) {
layer.msg('设置成功', {icon: 1, time: 1000});
} else {
layer.msg(res.message, {icon: 2, time: 1000});
}
}
});
});
form.on('submit(cron-add-submit)', function (data) {
data.field.script = cronAddScriptEditor.getValue();
admin.req({
url: "/api/panel/cron/add"
, type: 'post'
, data: data.field
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:计划任务添加失败,接口返回' + result);
layer.msg('计划任务添加失败!')
return false;
}
table.reload('panel-cron');
layer.alert('计划任务添加成功!', {
icon: 1
, title: '提示'
, btn: ['确定']
, yes: function (index) {
layer.closeAll();
//location.reload();
}
});
}
});
return false;
});
});
</script>
+31 -19
View File
@@ -209,7 +209,7 @@ Date: 2023-06-22
let cpu_info
admin.req({
url: '/api/panel/info/nowMonitor'
, method: 'get'
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
layer.msg('系统资源获取失败,请刷新重试!')
@@ -276,7 +276,7 @@ Date: 2023-06-22
})
admin.req({
url: '/api/panel/info/systemInfo'
, method: 'get'
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:系统信息获取失败,接口返回' + result)
@@ -313,29 +313,41 @@ Date: 2023-06-22
admin.req(
{
url: '/api/panel/info/checkUpdate'
, method: 'get'
, type: 'get'
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
layer.msg('获取版本信息失败,请刷新重试!')
return false
}
if (result.data.version) {
admin.popup({
title: '提示'
,
shade: 0
,
anim: -1
,
area: ['400px', '200px']
,
id: 'layadmin-layer-skin-update-panel'
,
skin: 'layui-anim layui-anim-upbit'
,
content: '最新版本:' + result.data.version + '<br><br>更新日志:' + result.data.describe + '<br><br>请在SSH执行<span class="layui-badge-rim">panel update</span>以更新面板!'
})
if (result.data.update) {
layer.confirm('更新日期: <br>'+new Date(result.data.date).toLocaleString()+'<br>更新日志: <pre>'+result.data.body+'</pre>', {
title: '最新版本: '+result.data.version+' ,是否更新?',
btn: ['更新', '取消']
}, function () {
let proxy = false
layer.confirm('对于大陆服务器,建议使用代理进行更新', {
title: '是否使用代理更新?',
btn: ['', '']
}, function () {
proxy = true
index = layer.msg('正在更新,稍后请手动刷新...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/panel/info/update'
, type: 'post'
, data: { proxy: proxy }
})
}, function(){
proxy = false
index = layer.msg('正在更新,稍后请手动刷新...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/panel/info/update'
, type: 'post'
, data: { proxy: proxy }
})
})
})
} else {
layer.msg('当前已是最新版本!')
}
+54 -49
View File
@@ -1,5 +1,5 @@
<script type="text/html" template>
<link rel="stylesheet" href="/panel/adminui/src/css/login.css?v={{ layui.admin.v }}" media="all">
<link rel="stylesheet" href="panel/adminui/src/css/login.css?v={{ layui.admin.v }}" media="all">
</script>
@@ -24,7 +24,8 @@
placeholder="密码" class="layui-input">
</div>
<div class="layui-form-item" style="margin-bottom: 20px;">
<a target="_blank" href="https://jq.qq.com/?_wv=1027&amp;k=I1oJKSTH" class="layadmin-user-jump-change layadmin-link"
<a target="_blank" href="https://jq.qq.com/?_wv=1027&amp;k=I1oJKSTH"
class="layadmin-user-jump-change layadmin-link"
style="margin-top: 7px;">忘记密码?</a>
</div>
<div class="layui-form-item">
@@ -41,55 +42,59 @@
</div>
<script>
layui.use(['admin', 'form'], function () {
let $ = layui.$
, setter = layui.setter
, admin = layui.admin
, form = layui.form
, router = layui.router()
, search = router.search;
layui.use(['admin', 'form'], function () {
let $ = layui.$
, setter = layui.setter
, admin = layui.admin
, form = layui.form
, router = layui.router()
, search = router.search;
// 设置面板名称
$('#panel-login-name').text(setter.name);
form.render();
//提交
form.on('submit(panel-login-submit)', function (obj) {
// 判断obj.field.remember是否存在
if (obj.field.remember) {
obj.field.remember = 1;
} else {
obj.field.remember = 0;
}
admin.req({
url: '/api/panel/user/login'
, data: obj.field
, method: 'post'
, done: function (res) {
// 请求成功后,写入 access_token
layui.data(setter.tableName, {
key: setter.request.tokenName
, value: res.data.access_token
});
// 登入成功的提示与跳转
layer.msg('登录成功', {
offset: '15px'
, icon: 1
, time: 1000
}, function () {
/**
* 可能是LayuiAdmin的bug,如果直接跳转到主页,会出现表格无法获取access_token的问题
* 所以这里强制刷新到主页
* location.hash = search.redirect ? decodeURIComponent(search.redirect) : '/';
*/
location.href = '/';
});
// 判断是否已登录
if (layui.data(setter.tableName)[setter.request.tokenName]) {
location.hash = search.redirect ? decodeURIComponent(search.redirect) : '/';
}
});
// 设置面板名称
$('#panel-login-name').text(setter.name);
form.render();
//提交
form.on('submit(panel-login-submit)', function (obj) {
// 判断obj.field.remember是否存在
if (obj.field.remember) {
obj.field.remember = 1;
} else {
obj.field.remember = 0;
}
admin.req({
url: '/api/panel/user/login'
, data: obj.field
, type: 'post'
, done: function (res) {
// 请求成功后,写入 access_token
layui.data(setter.tableName, {
key: setter.request.tokenName
, value: res.data.access_token
});
// 登入成功的提示与跳转
layer.msg('登录成功', {
offset: '15px'
, icon: 1
, time: 1000
}, function () {
/**
* 可能是LayuiAdmin的bug,如果直接跳转到主页,会出现表格无法获取access_token的问题
* 所以这里强制刷新一次页面
*/
location.reload();
});
}
});
});
});
});
</script>
+8 -8
View File
@@ -1,5 +1,5 @@
<script type="text/html" template>
<link rel="stylesheet" href="/panel/adminui/src/css/login.css?v={{ layui.admin.v }}" media="all">
<link rel="stylesheet" href="panel/adminui/src/css/login.css?v={{ layui.admin.v }}" media="all">
</script>
@@ -19,11 +19,11 @@
</div>
<script>
layui.use(['admin'], function () {
let admin = layui.admin
setTimeout(function () {
admin.exit()
window.location = '/'
}, 3000)
})
layui.use(['admin'], function () {
let admin = layui.admin
setTimeout(function () {
admin.exit()
location.hash = '/login'
}, 3000)
})
</script>
+413
View File
@@ -0,0 +1,413 @@
<!--
Name: 资源监控
Author: 耗子
Date: 2023-07-21
-->
<title>资源监控</title>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-form layui-card-body" style="overflow: hidden;">
<div class="layui-inline">
<span style="margin-right: 10px;">开启监控</span><input type="checkbox" id="monitor-switch"
lay-filter="monitor" lay-skin="switch"
lay-text="ON|OFF">
<span style="margin-left: 40px; margin-right: 10px;">保存天数</span>
<div class="layui-input-inline"><input type="number" name="monitor-save-days" class="layui-input"
style="height: 30px; margin-top: 5px;" min=0 max=30 disabled>
</div>
<div class="layui-input-inline">
<button id="save_monitor_date" class="layui-btn layui-btn-sm" style="margin-left: 10px;">确定
</button>
</div>
</div>
<div class="layui-inline" style="float: right;">
<span style="margin-right: 10px;">时间选择</span>
<div id="monitor-date" class="layui-input-inline">
<div class="layui-input-inline">
<input type="text" autocomplete="off" id="monitor-startDate"
class="layui-input"
placeholder="开始时间">
</div>
-
<div class="layui-input-inline">
<input type="text" autocomplete="off" id="monitor-endDate"
class="layui-input"
placeholder="结束时间">
</div>
</div>
<span style="margin-left: 40px;"></span>
<button id="clear_monitor_record" class="layui-btn layui-btn-sm layui-btn-danger">清空监控记录
</button>
</div>
</div>
</div>
<div class="layui-row layui-col-space10">
<div class="layui-col-xs12 layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
<span>负载</span>
</div>
<div class="layui-card-body">
<div id="load_monitor" style="width: 100%;height: 400px;"></div>
</div>
</div>
</div>
<div class="layui-col-xs12 layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
<span>CPU</span>
</div>
<div class="layui-card-body">
<div id="cpu_monitor" style="width: 100%;height: 400px;"></div>
</div>
</div>
</div>
</div>
<div class="layui-row layui-col-space10">
<div class="layui-col-xs12 layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
<span>内存</span>
</div>
<div class="layui-card-body">
<div id="memory_monitor" style="width: 100%;height: 400px;"></div>
</div>
</div>
</div>
<div class="layui-col-xs12 layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">
<span>网络</span>
</div>
<div class="layui-card-body">
<div id="network_monitor" style="width: 100%;height: 400px;"></div>
</div>
</div>
</div>
</div>
</div>
<script>
layui.use(['admin', 'view', 'form', 'element', 'carousel'], function () {
var admin = layui.admin;
var view = layui.view;
var $ = layui.jquery;
var form = layui.form;
var laydate = layui.laydate;
// 渲染时间选择器
// 默认开始时间为今天0点,结束时间为当前时间
var startDate = new Date(new Date().setHours(0, 0, 0, 0));
var endDate = new Date();
// 拼接时间字符串
var startStr = startDate.getFullYear() + '-' + (startDate.getMonth() + 1) + '-' + startDate.getDate() + ' ' + startDate.getHours() + ':' + startDate.getMinutes() + ':' + startDate.getSeconds();
var endStr = endDate.getFullYear() + '-' + (endDate.getMonth() + 1) + '-' + endDate.getDate() + ' ' + endDate.getHours() + ':' + endDate.getMinutes() + ':' + endDate.getSeconds();
laydate.render({
elem: '#monitor-date'
, type: 'datetime'
, range: ['#monitor-startDate', '#monitor-endDate']
, value: startStr + ' - ' + endStr
, calendar: true
, done: function (value, date, endDate) {
let start_date,
end_date;
// 初始化时间日期对象
start_date = new Date(date.year, date.month, date.date, date.hours, date.minutes, date.seconds);
end_date = new Date(endDate.year, endDate.month, endDate.date, endDate.hours, endDate.minutes, endDate.seconds);
getMonitorData(start_date.getTime(), end_date.getTime());
}
});
// 获取监控开关和保存天数
admin.req({
url: '/api/panel/monitor/switchAndDays',
type: 'get',
dataType: 'json',
success: function (res) {
if (res.code === 0) {
if (res.data.switch) {
$('#monitor-switch').attr('checked', true);
} else {
$('#monitor-switch').attr('checked', false);
}
$('input[name="monitor-save-days"]').val(res.data.days);
$('input[name="monitor-save-days"]').removeAttr('disabled');
form.render();
}
}
});
// 监听switch开关:是否开启监控
form.on('switch(monitor)', function (data) {
admin.req({
url: '/api/panel/monitor/switch',
type: 'post',
dataType: 'json',
data: {switch: data.elem.checked},
success: function (res) {
if (res.code === 0) {
layer.msg('修改成功', {icon: 1});
} else {
layer.msg(res.message, {icon: 2});
}
}
});
});
// 监听保存天数按钮
$('#save_monitor_date').click(function () {
var days = $('input[name="monitor-save-days"]').val();
if (days == '') {
layer.msg('请输入保存天数', {icon: 2});
return false;
}
admin.req({
url: '/api/panel/monitor/saveDays',
type: 'post',
dataType: 'json',
data: {days: days},
success: function (res) {
if (res.code === 0) {
layer.msg('修改成功', {icon: 1});
} else {
layer.msg(res.message, {icon: 2});
}
}
});
});
// 监听清除监控数据按钮
$('#clear_monitor_record').click(function () {
layer.confirm('确定要清除监控数据吗?', function (index) {
admin.req({
url: '/api/panel/monitor/clear',
type: 'post',
dataType: 'json',
success: function (res) {
if (res.code === 0) {
layer.msg('清除成功', {icon: 1});
setTimeout(function () {
admin.render();
}, 1000);
} else {
layer.msg(res.message, {icon: 2});
}
}
});
layer.close(index);
});
});
getMonitorData(startDate.getTime(), endDate.getTime());
});
// 获取监控数据
function getMonitorData(startDate, endDate) {
layui.use(['admin'], function () {
let admin = layui.admin;
// 获取监控数据
admin.req({
url: '/api/panel/monitor/list?start=' + startDate + '&end=' + endDate,
type: 'get',
dataType: 'json',
success: function (res) {
if (res.code !== 0) {
layer.msg(res.message, {icon: 2, shade: 0.3});
return false;
}
let loadChart = renderEcharts('load_monitor', '负载', {
x: 'left',
data: ["1分钟", "5分钟", "15分钟"]
}, res.data.times, [{
name: '1分钟',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.load.load1,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}, {
name: '5分钟',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.load.load5,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}, {
name: '15分钟',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.load.load15,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}], [{
type: 'value',
}]);
let cpuChart = renderEcharts('cpu_monitor', 'CPU', undefined, res.data.times, [{
name: '使用率',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.cpu.percent,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}], [{
name: '单位 %',
min: 0,
max: 100,
type: 'value',
axisLabel: {
formatter: '{value} %'
}
}]);
let memoryChart = renderEcharts('memory_monitor', '内存', {
x: 'left',
data: ["内存", "Swap"]
}, res.data.times, [{
name: '内存',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.mem.used,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}, {
name: 'Swap',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.swap.used,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}], [{
name: '单位 MB',
min: 0,
max: res.data.mem.total,
type: 'value',
axisLabel: {
formatter: '{value} M'
}
}]);
let networkChart = renderEcharts('network_monitor', '网络', {
x: 'left',
data: ["总计出", "总计入", "每秒出", "每秒入"]
}, res.data.times, [{
name: '总计出',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.net.sent,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}, {
name: '总计入',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.net.recv,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}, {
name: '每秒出',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.net.tx,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}, {
name: '每秒入',
type: 'line',
smooth: true,
itemStyle: {normal: {areaStyle: {type: 'default'}}},
data: res.data.net.rx,
markPoint: {
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
},
markLine: {
data: [{type: 'average', name: '平均值'}]
}
}], [{
name: '单位 Kb/s',
type: 'value',
axisLabel: {
formatter: '{value} Kb'
}
}]);
// 在窗口大小改变时,重置图表大小
window.addEventListener("resize", function () {
loadChart.resize();
cpuChart.resize();
memoryChart.resize();
networkChart.resize();
});
}, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error);
}
});
});
}
// 渲染图表
function renderEcharts(element_id, title, legend = undefined, data_xAxis, series, yAxis = undefined) {
var Chart = echarts.init(document.getElementById(element_id), layui.echartsTheme);
var option = {
title: {text: title, x: 'center', textStyle: {fontSize: 20}},
tooltip: {trigger: 'axis'},
legend: legend,
xAxis: [{type: 'category', boundaryGap: false, data: data_xAxis}],
yAxis: yAxis,
dataZoom: {
show: true,
realtime: true,
start: 0,
end: 100
},
series: series
};
Chart.setOption(option);
return Chart;
}
</script>
+139 -137
View File
@@ -35,150 +35,152 @@
</div>
<script>
layui.use(['admin', 'table', 'jquery'], function () {
var $ = layui.$
, form = layui.form
, table = layui.table
, admin = layui.admin
layui.use(['admin', 'table', 'jquery'], function () {
var $ = layui.$
, form = layui.form
, table = layui.table
, admin = layui.admin
table.render({
elem: '#panel-plugin'
, url: '/api/panel/plugin/list'
, cols: [[
{ field: 'slug', hide: true, title: 'Slug' }
, { field: 'name', width: 150, title: '插件名', sort: true }
, { field: 'description', title: '描述' }
, { field: 'author', width: 100, title: '作者' }
, { field: 'installed_version', width: 140, title: '已装版本' }
, { field: 'version', width: 140, title: '最新版本' }
, { field: 'show', title: '首页显示', width: 90, templet: '#plugin-show', unresize: true }
, {
field: 'control',
width: 180,
title: '操作',
templet: '#panel-plugin-control',
fixed: 'right',
align: 'left'
}
]]
, page: false
, text: '耗子Linux面板:数据加载出现异常!'
, done: function () {
//element.render('progress');
}
})
// 工具条
table.on('tool(panel-plugin)', function (obj) {
let data = obj.data
if (obj.event === 'open') {
location.hash = '/plugins/' + data.slug
} else if (obj.event === 'install') {
layer.confirm('确定安装该插件吗?', function (index) {
layer.close(index)
admin.req({
url: '/api/panel/plugin/install',
type: 'POST',
data: {
slug: data.slug
table.render({
elem: '#panel-plugin'
, url: '/api/panel/plugin/list'
, cols: [[
{field: 'slug', hide: true, title: 'Slug'}
, {field: 'name', width: 150, title: '插件名', sort: true}
, {field: 'description', title: '描述'}
, {field: 'author', width: 100, title: '作者'}
, {field: 'installed_version', width: 140, title: '已装版本'}
, {field: 'version', width: 140, title: '最新版本'}
, {field: 'show', title: '首页显示', width: 90, templet: '#plugin-show', unresize: true}
, {
field: 'control',
width: 180,
title: '操作',
templet: '#panel-plugin-control',
fixed: 'right',
align: 'left'
}
]]
, page: false
, text: {
none: '无数据'
}
, success: function (res) {
if (res.code === 0) {
table.reload('panel-plugin')
layer.msg('安装:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
})
} else {
layer.msg(res.msg, { icon: 2, time: 1000 })
}
, done: function () {
//element.render('progress');
}
})
})
} else if (obj.event === 'uninstall') {
layer.confirm('确定卸载该插件吗?', function (index) {
layer.close(index)
admin.req({
url: '/api/panel/plugin/uninstall',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code === 0) {
table.reload('panel-plugin')
layer.msg('卸载:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
// 工具条
table.on('tool(panel-plugin)', function (obj) {
let data = obj.data
if (obj.event === 'open') {
location.hash = '/plugins/' + data.slug
} else if (obj.event === 'install') {
layer.confirm('确定安装该插件吗?', function (index) {
layer.close(index)
admin.req({
url: '/api/panel/plugin/install',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code === 0) {
table.reload('panel-plugin')
layer.msg('安装:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
})
} else {
layer.msg(res.message, {icon: 2, time: 1000})
}
}
})
})
} else {
layer.msg(res.msg, { icon: 2, time: 1000 })
}
}
})
})
} else if (obj.event === 'update') {
layer.confirm('确定升级该插件吗?', function (index) {
layer.close(index)
admin.req({
url: '/api/panel/plugin/update',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code === 0) {
table.reload('panel-plugin')
layer.msg('升级:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
} else if (obj.event === 'uninstall') {
layer.confirm('确定卸载该插件吗?', function (index) {
layer.close(index)
admin.req({
url: '/api/panel/plugin/uninstall',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code === 0) {
table.reload('panel-plugin')
layer.msg('卸载:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
})
} else {
layer.msg(res.message, {icon: 2, time: 1000})
}
}
})
})
} else if (obj.event === 'update') {
layer.confirm('确定升级该插件吗?', function (index) {
layer.close(index)
admin.req({
url: '/api/panel/plugin/update',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code === 0) {
table.reload('panel-plugin')
layer.msg('升级:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
})
} else {
layer.msg(res.message, {icon: 2, time: 1000})
}
}
})
})
} else {
layer.msg(res.msg, { icon: 2, time: 1000 })
}
}
})
})
}
form.on('switch(plugin-show-home)', function (obj) {
let $ = layui.$
let plugin_slug = $(this).data('plugin-slug')
let show = obj.elem.checked ? 1 : 0
admin.req({
url: '/api/panel/plugin/updateShow',
type: 'POST',
data: {
slug: plugin_slug,
show: show
}
, success: function (res) {
if (res.code === 0) {
layer.msg('设置成功', {icon: 1, time: 1000})
} else {
// 还原开关状态
obj.elem.checked = !obj.elem.checked
form.render('checkbox')
layer.msg(res.message, {icon: 2, time: 1000})
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error)
}
})
})
/*form.render(null, 'plugin-form');
//搜索
form.on('submit(plugin-search-submit)', function (data) {
var field = data.field;
//执行重载
table.reload('plugin-search-submit', {
where: field
});
});*/
})
form.on('switch(plugin-show-home)', function (obj) {
let $ = layui.$
let plugin_slug = $(this).data('plugin-slug')
let show = obj.elem.checked ? 1 : 0
admin.req({
url: '/api/panel/plugin/updateShow',
type: 'POST',
data: {
slug: plugin_slug,
show: show
}
, success: function (res) {
if (res.code === 0) {
layer.msg('设置成功', { icon: 1, time: 1000 })
} else {
// 还原开关状态
obj.elem.checked = !obj.elem.checked
form.render('checkbox')
layer.msg(res.msg, { icon: 2, time: 1000 })
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error)
}
})
})
/*form.render(null, 'plugin-form');
//搜索
form.on('submit(plugin-search-submit)', function (data) {
var field = data.field;
//执行重载
table.reload('plugin-search-submit', {
where: field
});
});*/
})
</script>
+558
View File
@@ -0,0 +1,558 @@
<!--
Name: MySQL管理器
Author: 耗子
Date: 2022-07-22
-->
<title>MySQL</title>
<div class="layui-fluid" id="component-tabs">
<div class="layui-row">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header">MySQL管理</div>
<div class="layui-card-body">
<div class="layui-tab">
<ul class="layui-tab-title">
<li class="layui-this">基本信息</li>
<li>管理</li>
<li>配置修改</li>
<li>负载状态</li>
<li>错误日志</li>
<li>慢查询日志</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend>运行状态</legend>
</fieldset>
<blockquote id="mysql-status" class="layui-elem-quote layui-quote-nm">当前状态:<span
class="layui-badge layui-bg-black">获取中</span></blockquote>
<div class="layui-btn-container" style="padding-top: 30px;">
<button id="mysql-start" class="layui-btn">启动</button>
<button id="mysql-stop" class="layui-btn layui-btn-danger">停止</button>
<button id="mysql-restart" class="layui-btn layui-btn-warm">重启</button>
<button id="mysql-reload" class="layui-btn layui-btn-normal">重载</button>
</div>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend>基本设置</legend>
</fieldset>
<div class="layui-form" lay-filter="mysql_setting">
<div class="layui-form-item">
<label class="layui-form-label" style="font-size: 13px;">root 密码</label>
<div class="layui-input-inline">
<input type="text" name="mysql_root_password" value="获取中ing..."
class="layui-input" disabled>
</div>
<div class="layui-form-mid layui-word-aux">查看/修改MySQL的root密码</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn layui-btn-sm" lay-submit
lay-filter="mysql_setting_submit">确认修改
</button>
</div>
</div>
</div>
</div>
<div class="layui-tab-item">
<blockquote class="layui-elem-quote">面板仅集成了部分常用功能,如需更多功能,建议安装
phpMyAdmin 使用。
</blockquote>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend>数据库列表</legend>
</fieldset>
<table class="layui-hide" id="mysql-database-list"
lay-filter="mysql-database-list"></table>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend>用户列表</legend>
</fieldset>
<table class="layui-hide" id="mysql-user-list" lay-filter="mysql-user-list"></table>
<!-- 数据库顶部工具栏 -->
<script type="text/html" id="mysql-database-list-bar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="add_database">新建数据库
</button>
</div>
</script>
<!-- 用户顶部工具栏 -->
<script type="text/html" id="mysql-user-list-bar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="add_user">新建用户</button>
</div>
</script>
<!-- 数据库右侧管理 -->
<script type="text/html" id="mysql-database-list-control">
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="backup">备份</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
<!-- 用户右侧管理 -->
<script type="text/html" id="mysql-user-list-control">
<a class="layui-btn layui-btn-normal layui-btn-xs"
lay-event="change_password">改密</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
</div>
<div class="layui-tab-item">
<blockquote class="layui-elem-quote">此处修改的是MySQL主配置文件,如果你不了解各参数的含义,请不要随意修改!<br>
提示:Ctrl+F 搜索关键字,Ctrl+S 保存,Ctrl+H 查找替换!
</blockquote>
<div id="mysql-config-editor"
style="height: 600px;"></div>
<div class="layui-btn-container" style="padding-top: 30px;">
<button id="mysql-config-save" class="layui-btn">保存</button>
</div>
</div>
<div class="layui-tab-item">
<table class="layui-hide" id="mysql-load-status"></table>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="mysql-clean-error-log" class="layui-btn">清空日志</button>
</div>
<pre id="mysql-error-log" class="layui-code">
获取中...
</pre>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="mysql-clean-slow-log" class="layui-btn">清空日志</button>
</div>
<pre id="mysql-slow-log" class="layui-code">
获取中...
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let mysql_config_editor;// 定义mysql配置编辑器的全局变量
layui.use(['index', 'code', 'table'], function () {
let $ = layui.$
, admin = layui.admin
, element = layui.element
, code = layui.code
, table = layui.table
, form = layui.form
, view = layui.view;
// 渲染表单
form.render();
admin.req({
url: "/api/plugins/mysql57/rootPassword"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:系统信息获取失败,接口返回' + result);
return false;
}
form.val("mysql_setting", {
"mysql_root_password": result.data
});
$('input').attr('disabled', false);
}
});
// 提交修改
form.on('submit(mysql_setting_submit)', function (data) {
index = layer.msg('请稍候...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql57/rootPassword"
, type: 'post'
, data: data.field
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL设置保存失败,接口返回' + result);
return false;
}
layer.msg('修改成功!')
}
});
return false;
});
// 获取mysql运行状态并渲染
admin.req({
url: "/api/plugins/mysql57/status"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL运行状态获取失败,接口返回' + result);
return false;
}
if (result.data) {
$('#mysql-status').html('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
} else {
$('#mysql-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
}
}
});
// 获取数据库列表
table.render({
elem: '#mysql-database-list'
, url: '/api/plugins/mysql57/database'
, toolbar: '#mysql-database-list-bar'
, title: '数据库列表'
, cols: [[
{field: 'name', title: '库名', fixed: 'left', unresize: true, sort: true}
, {fixed: 'right', title: '操作', toolbar: '#mysql-database-list-control', width: 150}
]]
});
// 头工具栏事件
table.on('toolbar(mysql-database-list)', function (obj) {
if (obj.event === 'add_database') {
admin.popup({
title: '新建数据库'
, area: ['600px', '300px']
, id: 'LAY-popup-mysql-database-add'
, success: function (layer, index) {
view(this.id).render('plugins/mysql57/add_database', {}).done(function () {
form.render(null, 'LAY-popup-mysql-database-add');
});
}
});
}
});
// 行工具事件
table.on('tool(mysql-database-list)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('高风险操作,确定要删除数据库 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
index = layer.msg('请稍候...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/mysql57/deleteDatabase"
, type: 'post'
, data: {
database: data.name
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:数据库删除失败,接口返回' + result);
return false;
}
obj.del();
layer.alert('数据库' + data.name + '删除成功!');
}
});
layer.close(index);
});
} else if (obj.event === 'backup') {
// 打开备份页面
admin.popup({
title: '备份管理 - ' + data.name
, area: ['70%', '80%']
, id: 'LAY-popup-mysql-backup'
, success: function (layero, index) {
view(this.id).render('plugins/mysql57/backup', {
data: data
}).done(function () {
form.render(null, 'LAY-popup-mysql-backup');
});
}
});
}
});
// 获取数据库用户列表
table.render({
elem: '#mysql-user-list'
, url: '/api/plugins/mysql57/user'
, toolbar: '#mysql-user-list-bar'
, title: '用户列表'
, cols: [[
{field: 'user', title: '用户名', fixed: 'left', width: 300, sort: true}
, {field: 'host', title: '主机', width: 250, sort: true}
, {field: 'grants', title: '权限'}
, {fixed: 'right', title: '操作', toolbar: '#mysql-user-list-control', width: 150}
]]
});
// 头工具栏事件
table.on('toolbar(mysql-user-list)', function (obj) {
if (obj.event === 'add_user') {
admin.popup({
title: '新建用户'
, area: ['600px', '300px']
, id: 'LAY-popup-mysql-user-add'
, success: function (layer, index) {
view(this.id).render('plugins/mysql57/add_user', {}).done(function () {
form.render(null, 'LAY-popup-mysql-user-add');
});
}
});
}
});
// 行工具事件
table.on('tool(mysql-user-list)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('高风险操作,确定要删除用户 <b style="color: red;">' + data.user + '</b> 吗?', function (index) {
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/mysql57/deleteUser"
, type: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:用户删除失败,接口返回' + result);
return false;
}
obj.del();
layer.alert('用户' + data.user + '删除成功!');
}
});
layer.close(index);
});
} else if (obj.event === 'change_password') {
// 弹出输入密码框
layer.prompt({
formType: 1
, title: '请输入新密码(8位以上大小写数字特殊符号混合)'
}, function (value, index) {
layer.close(index);
index = layer.msg('正在提交...', {icon: 16, time: 0});
// 发送请求
admin.req({
url: "/api/plugins/mysql57/userPassword"
, type: 'post'
, data: {
user: data.user,
password: value
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:密码修改失败,接口返回' + result);
return false;
}
layer.alert('用户' + data.user + '密码修改成功!');
}
});
});
}
});
// 获取mysql错误日志并渲染
admin.req({
url: "/api/plugins/mysql57/errorLog"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL错误日志获取失败,接口返回' + result);
$('#mysql-error-log').text('MySQL错误日志获取失败,请刷新重试!');
code({
elem: '#mysql-error-log'
, title: 'error.log'
, encode: true
, about: false
});
return false;
}
$('#mysql-error-log').text(result.data);
code({
elem: '#mysql-error-log'
, title: 'error.log'
, encode: true
, about: false
});
}
});
// 获取mysql慢查询日志并渲染
admin.req({
url: "/api/plugins/mysql57/slowLog"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL慢查询日志获取失败,接口返回' + result);
$('#mysql-slow-log').text('MySQL慢查询日志获取失败,请刷新重试!');
code({
elem: '#mysql-slow-log'
, title: 'slow.log'
, encode: true
, about: false
});
return false;
}
$('#mysql-slow-log').text(result.data);
code({
elem: '#mysql-slow-log'
, title: 'slow.log'
, encode: true
, about: false
});
}
});
// 获取mysql配置并渲染
admin.req({
url: "/api/plugins/mysql57/config"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL主配置获取失败,接口返回' + result);
return false;
}
$('#mysql-config-editor').text(result.data);
mysql_config_editor = ace.edit("mysql-config-editor", {
mode: "ace/mode/ini",
selectionStyle: "text"
});
}
});
// 获取mysql负载状态并渲染
table.render({
elem: '#mysql-load-status'
, url: '/api/plugins/mysql57/load'
, cols: [[
{field: 'name', width: '80%', title: '属性',}
, {field: 'value', width: '20%', title: '当前值'}
]]
});
element.render();
// 事件监听
$('#mysql-start').click(function () {
layer.confirm('确定要启动MySQL吗?', {
btn: ['启动', '取消']
}, function () {
index = layer.msg('正在启动MySQL...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql57/start"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL启动失败,接口返回' + result);
return false;
}
admin.events.refresh();
layer.alert('MySQL启动成功!');
}
});
});
});
$('#mysql-stop').click(function () {
layer.confirm('停止MySQL将导致使用MySQL的网站无法访问,是否继续停止?', {
btn: ['停止', '取消']
}, function () {
index = layer.msg('正在停止MySQL...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql57/stop"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL停止失败,接口返回' + result);
return false;
}
admin.events.refresh();
layer.alert('MySQL停止成功!');
}
});
});
});
$('#mysql-restart').click(function () {
layer.confirm('重启MySQL将导致使用MySQL的网站短时间无法访问,是否继续重启?', {
btn: ['重启', '取消']
}, function () {
index = layer.msg('正在重启MySQL...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql57/restart"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL重启失败,接口返回' + result);
return false;
}
admin.events.refresh();
layer.alert('MySQL重启成功!');
}
});
});
});
$('#mysql-reload').click(function () {
index = layer.msg('正在重载MySQL...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql57/reload"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL重载失败,接口返回' + result);
return false;
}
admin.events.refresh();
layer.alert('MySQL重载成功!');
}
});
});
$('#mysql-config-save').click(function () {
index = layer.msg('正在保存配置...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql57/config"
, type: 'post'
, data: {
config: mysql_config_editor.getValue()
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL配置保存失败,接口返回' + result);
return false;
}
layer.alert('MySQL配置保存成功!');
}
});
});
$('#mysql-clean-error-log').click(function () {
index = layer.msg('正在清空错误日志...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql57/clearErrorLog"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL错误日志清空失败,接口返回' + result);
return false;
}
layer.msg('MySQL错误日志已清空!');
setTimeout(function () {
admin.events.refresh();
}, 1000);
}
});
});
$('#mysql-clean-slow-log').click(function () {
index = layer.msg('正在清空慢查询日志...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql57/clearSlowLog"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL慢查询日志清空失败,接口返回' + result);
return false;
}
layer.msg('MySQL慢查询日志已清空!');
setTimeout(function () {
admin.events.refresh();
}, 1000);
}
});
});
});
</script>
@@ -0,0 +1,80 @@
<!--
Name: MySQL管理器 - 添加数据库
Author: 耗子
Date: 2023-07-22
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<form class="layui-form" action="" lay-filter="add-mysql-database-form">
<div class="layui-form-item">
<label class="layui-form-label">数据库名</label>
<div class="layui-input-block">
<input type="text" name="database" lay-verify="required" placeholder="请输入数据库名"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="user" lay-verify="required" placeholder="请输入用户名"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="text" name="password" lay-verify="required" placeholder="请输入密码(8位以上大小写数字特殊符号混合)"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<div class="layui-footer">
<button class="layui-btn" lay-submit="" lay-filter="add-mysql-database-submit">立即提交</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</div>
</form>
</script>
<script>
layui.data.sendParams = function (params) {
layui.use(['admin', 'form'], function () {
var $ = layui.$
, admin = layui.admin
, layer = layui.layer
, form = layui.form
, table = layui.table
form.render();
// 提交
form.on('submit(add-mysql-database-submit)', function (data) {
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/mysql57/addDatabase"
, type: 'post'
, data: data.field
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:数据库添加失败,接口返回' + result);
layer.msg('数据库添加失败,请刷新重试!')
return false;
}
table.reload('mysql-database-list');
table.reload('mysql-user-list');
layer.alert('数据库添加成功!', {
icon: 1
, title: '提示'
, btn: ['确定']
, yes: function (index) {
layer.closeAll();
}
});
}
});
return false;
});
});
};
</script>
@@ -0,0 +1,80 @@
<!--
Name: MySQL管理器 - 添加用户
Author: 耗子
Date: 2023-07-22
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<form class="layui-form" action="" lay-filter="add-mysql-user-form">
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="user" lay-verify="required" placeholder="请输入用户名"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="text" name="password" lay-verify="required" placeholder="请输入密码(8位以上大小写数字特殊符号混合)"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">数据库</label>
<div class="layui-input-block">
<input type="text" name="database" lay-verify="required" placeholder="输入授权给该用户的数据库名"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<div class="layui-footer">
<button class="layui-btn" lay-submit="" lay-filter="add-mysql-user-submit">立即提交</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</div>
</form>
</script>
<script>
layui.data.sendParams = function (params) {
layui.use(['admin', 'form'], function () {
var $ = layui.$
, admin = layui.admin
, layer = layui.layer
, form = layui.form
, table = layui.table
form.render();
// 提交
form.on('submit(add-mysql-user-submit)', function (data) {
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/mysql57/addUser"
, type: 'post'
, data: data.field
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:用户添加失败,接口返回' + result);
layer.msg('用户添加失败,请刷新重试!')
return false;
}
table.reload('mysql-database-list');
table.reload('mysql-user-list');
layer.alert('用户添加成功!', {
icon: 1
, title: '提示'
, btn: ['确定']
, yes: function (index) {
layer.closeAll();
}
});
}
});
return false;
});
});
};
</script>
@@ -0,0 +1,145 @@
<!--
Name: MySQL管理器 - 数据库备份
Author: 耗子
Date: 2023-07-22
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<div class="layui-row">
<div class="layui-col-xs12 layui-col-sm12 layui-col-md12">
<table class="layui-hide" id="mysql-backup-list" lay-filter="mysql-backup-list"></table>
</div>
</div>
</script>
<!-- 备份顶部工具栏 -->
<script type="text/html" id="mysql-database-backup-bar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="backup_database">备份数据库</button>
<button class="layui-btn layui-btn-sm" id="upload_mysql_backup">上传备份</button>
</div>
</script>
<!-- 备份右侧管理 -->
<script type="text/html" id="mysql-database-backup-control">
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="restore">恢复</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
<script>
layui.data.sendParams = function (params) {
layui.use(['admin', 'form', 'laydate', 'code'], function () {
var $ = layui.$
, admin = layui.admin
, layer = layui.layer
, table = layui.table
, upload = layui.upload;
// 渲染表格
table.render({
elem: '#mysql-backup-list'
, url: '/api/plugins/mysql57/backup'
, toolbar: '#mysql-database-backup-bar'
, title: '备份列表'
, cols: [[
{field: 'file', title: '备份名称', width: 500}
, {field: 'size', title: '文件大小'}
, {field: 'right', title: '操作', width: 150, toolbar: '#mysql-database-backup-control'}
]]
, text: {
none: '无备份数据'
}
, done: function (res, curr, count) {
upload.render({
elem: '#upload_mysql_backup'
, url: '/api/plugins/mysql57/uploadBackup'
, accept: 'file'
, ext: 'sql|zip|rar|tar|gz|bz2'
, before: function (obj) {
index = layer.msg('正在上传备份文件,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
});
}
, done: function (res) {
layer.close(index);
layer.msg('上传成功!', {icon: 1});
table.reload('mysql-backup-list');
}
});
}
});
// 头工具栏事件
table.on('toolbar(mysql-backup-list)', function (obj) {
if (obj.event === 'backup_database') {
index = layer.msg('正在备份数据库,请稍等...', {
icon: 16
, time: 0
});
admin.req({
url: '/api/plugins/mysql57/createBackup'
, type: 'post'
, data: {
database: params.data.name
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:备份数据库失败,接口返回' + result);
layer.alert('备份失败!');
return false;
}
table.reload('mysql-backup-list');
layer.msg('备份成功!', {icon: 1});
}
});
}
});
// 行工具事件
table.on('tool(mysql-backup-list)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('确定要删除数据库备份 <b style="color: red;">' + data.file + '</b> 吗?', function (index) {
index = layer.msg('正在删除数据库备份,请稍等...', {
icon: 16
, time: 0
});
admin.req({
url: "/api/plugins/mysql57/deleteBackup"
, type: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:数据库备份删除失败,接口返回' + result);
layer.msg('数据库备份删除失败,请刷新重试!')
return false;
}
obj.del();
layer.alert('数据库备份' + data.file + '删除成功!');
}
});
});
} else if (obj.event === 'restore') {
layer.confirm('高风险操作,确定要恢复数据库备份 <b style="color: red;">' + data.file + '</b> 吗?', function (index) {
index = layer.msg('正在恢复数据库备份,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
});
data.database = params.data.name;
admin.req({
url: "/api/plugins/mysql57/restoreBackup"
, type: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:数据库恢复失败,接口返回' + result);
layer.msg('数据库备份恢复失败,请刷新重试!')
return false;
}
layer.alert('数据库备份' + data.file + '恢复成功!');
}
});
});
}
});
});
};
</script>
+558
View File
@@ -0,0 +1,558 @@
<!--
Name: MySQL管理器
Author: 耗子
Date: 2022-07-22
-->
<title>MySQL</title>
<div class="layui-fluid" id="component-tabs">
<div class="layui-row">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header">MySQL管理</div>
<div class="layui-card-body">
<div class="layui-tab">
<ul class="layui-tab-title">
<li class="layui-this">基本信息</li>
<li>管理</li>
<li>配置修改</li>
<li>负载状态</li>
<li>错误日志</li>
<li>慢查询日志</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend>运行状态</legend>
</fieldset>
<blockquote id="mysql-status" class="layui-elem-quote layui-quote-nm">当前状态:<span
class="layui-badge layui-bg-black">获取中</span></blockquote>
<div class="layui-btn-container" style="padding-top: 30px;">
<button id="mysql-start" class="layui-btn">启动</button>
<button id="mysql-stop" class="layui-btn layui-btn-danger">停止</button>
<button id="mysql-restart" class="layui-btn layui-btn-warm">重启</button>
<button id="mysql-reload" class="layui-btn layui-btn-normal">重载</button>
</div>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend>基本设置</legend>
</fieldset>
<div class="layui-form" lay-filter="mysql_setting">
<div class="layui-form-item">
<label class="layui-form-label" style="font-size: 13px;">root 密码</label>
<div class="layui-input-inline">
<input type="text" name="mysql_root_password" value="获取中ing..."
class="layui-input" disabled>
</div>
<div class="layui-form-mid layui-word-aux">查看/修改MySQL的root密码</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn layui-btn-sm" lay-submit
lay-filter="mysql_setting_submit">确认修改
</button>
</div>
</div>
</div>
</div>
<div class="layui-tab-item">
<blockquote class="layui-elem-quote">面板仅集成了部分常用功能,如需更多功能,建议安装
phpMyAdmin 使用。
</blockquote>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend>数据库列表</legend>
</fieldset>
<table class="layui-hide" id="mysql-database-list"
lay-filter="mysql-database-list"></table>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend>用户列表</legend>
</fieldset>
<table class="layui-hide" id="mysql-user-list" lay-filter="mysql-user-list"></table>
<!-- 数据库顶部工具栏 -->
<script type="text/html" id="mysql-database-list-bar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="add_database">新建数据库
</button>
</div>
</script>
<!-- 用户顶部工具栏 -->
<script type="text/html" id="mysql-user-list-bar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="add_user">新建用户</button>
</div>
</script>
<!-- 数据库右侧管理 -->
<script type="text/html" id="mysql-database-list-control">
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="backup">备份</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
<!-- 用户右侧管理 -->
<script type="text/html" id="mysql-user-list-control">
<a class="layui-btn layui-btn-normal layui-btn-xs"
lay-event="change_password">改密</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
</div>
<div class="layui-tab-item">
<blockquote class="layui-elem-quote">此处修改的是MySQL主配置文件,如果你不了解各参数的含义,请不要随意修改!<br>
提示:Ctrl+F 搜索关键字,Ctrl+S 保存,Ctrl+H 查找替换!
</blockquote>
<div id="mysql-config-editor"
style="height: 600px;"></div>
<div class="layui-btn-container" style="padding-top: 30px;">
<button id="mysql-config-save" class="layui-btn">保存</button>
</div>
</div>
<div class="layui-tab-item">
<table class="layui-hide" id="mysql-load-status"></table>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="mysql-clean-error-log" class="layui-btn">清空日志</button>
</div>
<pre id="mysql-error-log" class="layui-code">
获取中...
</pre>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="mysql-clean-slow-log" class="layui-btn">清空日志</button>
</div>
<pre id="mysql-slow-log" class="layui-code">
获取中...
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let mysql_config_editor;// 定义mysql配置编辑器的全局变量
layui.use(['index', 'code', 'table'], function () {
let $ = layui.$
, admin = layui.admin
, element = layui.element
, code = layui.code
, table = layui.table
, form = layui.form
, view = layui.view;
// 渲染表单
form.render();
admin.req({
url: "/api/plugins/mysql80/rootPassword"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:系统信息获取失败,接口返回' + result);
return false;
}
form.val("mysql_setting", {
"mysql_root_password": result.data
});
$('input').attr('disabled', false);
}
});
// 提交修改
form.on('submit(mysql_setting_submit)', function (data) {
index = layer.msg('请稍候...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql80/rootPassword"
, type: 'post'
, data: data.field
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL设置保存失败,接口返回' + result);
return false;
}
layer.msg('修改成功!')
}
});
return false;
});
// 获取mysql运行状态并渲染
admin.req({
url: "/api/plugins/mysql80/status"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL运行状态获取失败,接口返回' + result);
return false;
}
if (result.data) {
$('#mysql-status').html('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
} else {
$('#mysql-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
}
}
});
// 获取数据库列表
table.render({
elem: '#mysql-database-list'
, url: '/api/plugins/mysql80/database'
, toolbar: '#mysql-database-list-bar'
, title: '数据库列表'
, cols: [[
{field: 'name', title: '库名', fixed: 'left', unresize: true, sort: true}
, {fixed: 'right', title: '操作', toolbar: '#mysql-database-list-control', width: 150}
]]
});
// 头工具栏事件
table.on('toolbar(mysql-database-list)', function (obj) {
if (obj.event === 'add_database') {
admin.popup({
title: '新建数据库'
, area: ['600px', '300px']
, id: 'LAY-popup-mysql-database-add'
, success: function (layer, index) {
view(this.id).render('plugins/mysql80/add_database', {}).done(function () {
form.render(null, 'LAY-popup-mysql-database-add');
});
}
});
}
});
// 行工具事件
table.on('tool(mysql-database-list)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('高风险操作,确定要删除数据库 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
index = layer.msg('请稍候...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/mysql80/deleteDatabase"
, type: 'post'
, data: {
database: data.name
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:数据库删除失败,接口返回' + result);
return false;
}
obj.del();
layer.alert('数据库' + data.name + '删除成功!');
}
});
layer.close(index);
});
} else if (obj.event === 'backup') {
// 打开备份页面
admin.popup({
title: '备份管理 - ' + data.name
, area: ['70%', '80%']
, id: 'LAY-popup-mysql-backup'
, success: function (layero, index) {
view(this.id).render('plugins/mysql80/backup', {
data: data
}).done(function () {
form.render(null, 'LAY-popup-mysql-backup');
});
}
});
}
});
// 获取数据库用户列表
table.render({
elem: '#mysql-user-list'
, url: '/api/plugins/mysql80/user'
, toolbar: '#mysql-user-list-bar'
, title: '用户列表'
, cols: [[
{field: 'user', title: '用户名', fixed: 'left', width: 300, sort: true}
, {field: 'host', title: '主机', width: 250, sort: true}
, {field: 'grants', title: '权限'}
, {fixed: 'right', title: '操作', toolbar: '#mysql-user-list-control', width: 150}
]]
});
// 头工具栏事件
table.on('toolbar(mysql-user-list)', function (obj) {
if (obj.event === 'add_user') {
admin.popup({
title: '新建用户'
, area: ['600px', '300px']
, id: 'LAY-popup-mysql-user-add'
, success: function (layer, index) {
view(this.id).render('plugins/mysql80/add_user', {}).done(function () {
form.render(null, 'LAY-popup-mysql-user-add');
});
}
});
}
});
// 行工具事件
table.on('tool(mysql-user-list)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('高风险操作,确定要删除用户 <b style="color: red;">' + data.user + '</b> 吗?', function (index) {
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/mysql80/deleteUser"
, type: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:用户删除失败,接口返回' + result);
return false;
}
obj.del();
layer.alert('用户' + data.user + '删除成功!');
}
});
layer.close(index);
});
} else if (obj.event === 'change_password') {
// 弹出输入密码框
layer.prompt({
formType: 1
, title: '请输入新密码(8位以上大小写数字特殊符号混合)'
}, function (value, index) {
layer.close(index);
index = layer.msg('正在提交...', {icon: 16, time: 0});
// 发送请求
admin.req({
url: "/api/plugins/mysql80/userPassword"
, type: 'post'
, data: {
user: data.user,
password: value
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:密码修改失败,接口返回' + result);
return false;
}
layer.alert('用户' + data.user + '密码修改成功!');
}
});
});
}
});
// 获取mysql错误日志并渲染
admin.req({
url: "/api/plugins/mysql80/errorLog"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL错误日志获取失败,接口返回' + result);
$('#mysql-error-log').text('MySQL错误日志获取失败,请刷新重试!');
code({
elem: '#mysql-error-log'
, title: 'error.log'
, encode: true
, about: false
});
return false;
}
$('#mysql-error-log').text(result.data);
code({
elem: '#mysql-error-log'
, title: 'error.log'
, encode: true
, about: false
});
}
});
// 获取mysql慢查询日志并渲染
admin.req({
url: "/api/plugins/mysql80/slowLog"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL慢查询日志获取失败,接口返回' + result);
$('#mysql-slow-log').text('MySQL慢查询日志获取失败,请刷新重试!');
code({
elem: '#mysql-slow-log'
, title: 'slow.log'
, encode: true
, about: false
});
return false;
}
$('#mysql-slow-log').text(result.data);
code({
elem: '#mysql-slow-log'
, title: 'slow.log'
, encode: true
, about: false
});
}
});
// 获取mysql配置并渲染
admin.req({
url: "/api/plugins/mysql80/config"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL主配置获取失败,接口返回' + result);
return false;
}
$('#mysql-config-editor').text(result.data);
mysql_config_editor = ace.edit("mysql-config-editor", {
mode: "ace/mode/ini",
selectionStyle: "text"
});
}
});
// 获取mysql负载状态并渲染
table.render({
elem: '#mysql-load-status'
, url: '/api/plugins/mysql80/load'
, cols: [[
{field: 'name', width: '80%', title: '属性',}
, {field: 'value', width: '20%', title: '当前值'}
]]
});
element.render();
// 事件监听
$('#mysql-start').click(function () {
layer.confirm('确定要启动MySQL吗?', {
btn: ['启动', '取消']
}, function () {
index = layer.msg('正在启动MySQL...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql80/start"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL启动失败,接口返回' + result);
return false;
}
admin.events.refresh();
layer.alert('MySQL启动成功!');
}
});
});
});
$('#mysql-stop').click(function () {
layer.confirm('停止MySQL将导致使用MySQL的网站无法访问,是否继续停止?', {
btn: ['停止', '取消']
}, function () {
index = layer.msg('正在停止MySQL...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql80/stop"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL停止失败,接口返回' + result);
return false;
}
admin.events.refresh();
layer.alert('MySQL停止成功!');
}
});
});
});
$('#mysql-restart').click(function () {
layer.confirm('重启MySQL将导致使用MySQL的网站短时间无法访问,是否继续重启?', {
btn: ['重启', '取消']
}, function () {
index = layer.msg('正在重启MySQL...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql80/restart"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL重启失败,接口返回' + result);
return false;
}
admin.events.refresh();
layer.alert('MySQL重启成功!');
}
});
});
});
$('#mysql-reload').click(function () {
index = layer.msg('正在重载MySQL...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql80/reload"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL重载失败,接口返回' + result);
return false;
}
admin.events.refresh();
layer.alert('MySQL重载成功!');
}
});
});
$('#mysql-config-save').click(function () {
index = layer.msg('正在保存配置...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql80/config"
, type: 'post'
, data: {
config: mysql_config_editor.getValue()
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL配置保存失败,接口返回' + result);
return false;
}
layer.alert('MySQL配置保存成功!');
}
});
});
$('#mysql-clean-error-log').click(function () {
index = layer.msg('正在清空错误日志...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql80/clearErrorLog"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL错误日志清空失败,接口返回' + result);
return false;
}
layer.msg('MySQL错误日志已清空!');
setTimeout(function () {
admin.events.refresh();
}, 1000);
}
});
});
$('#mysql-clean-slow-log').click(function () {
index = layer.msg('正在清空慢查询日志...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/mysql80/clearSlowLog"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:MySQL慢查询日志清空失败,接口返回' + result);
return false;
}
layer.msg('MySQL慢查询日志已清空!');
setTimeout(function () {
admin.events.refresh();
}, 1000);
}
});
});
});
</script>
@@ -0,0 +1,80 @@
<!--
Name: MySQL管理器 - 添加数据库
Author: 耗子
Date: 2023-07-22
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<form class="layui-form" action="" lay-filter="add-mysql-database-form">
<div class="layui-form-item">
<label class="layui-form-label">数据库名</label>
<div class="layui-input-block">
<input type="text" name="database" lay-verify="required" placeholder="请输入数据库名"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="user" lay-verify="required" placeholder="请输入用户名"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="text" name="password" lay-verify="required" placeholder="请输入密码(8位以上大小写数字特殊符号混合)"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<div class="layui-footer">
<button class="layui-btn" lay-submit="" lay-filter="add-mysql-database-submit">立即提交</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</div>
</form>
</script>
<script>
layui.data.sendParams = function (params) {
layui.use(['admin', 'form'], function () {
var $ = layui.$
, admin = layui.admin
, layer = layui.layer
, form = layui.form
, table = layui.table
form.render();
// 提交
form.on('submit(add-mysql-database-submit)', function (data) {
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/mysql80/addDatabase"
, type: 'post'
, data: data.field
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:数据库添加失败,接口返回' + result);
layer.msg('数据库添加失败,请刷新重试!')
return false;
}
table.reload('mysql-database-list');
table.reload('mysql-user-list');
layer.alert('数据库添加成功!', {
icon: 1
, title: '提示'
, btn: ['确定']
, yes: function (index) {
layer.closeAll();
}
});
}
});
return false;
});
});
};
</script>
@@ -0,0 +1,80 @@
<!--
Name: MySQL管理器 - 添加用户
Author: 耗子
Date: 2023-07-22
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<form class="layui-form" action="" lay-filter="add-mysql-user-form">
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="user" lay-verify="required" placeholder="请输入用户名"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">密码</label>
<div class="layui-input-block">
<input type="text" name="password" lay-verify="required" placeholder="请输入密码(8位以上大小写数字特殊符号混合)"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">数据库</label>
<div class="layui-input-block">
<input type="text" name="database" lay-verify="required" placeholder="输入授权给该用户的数据库名"
autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<div class="layui-footer">
<button class="layui-btn" lay-submit="" lay-filter="add-mysql-user-submit">立即提交</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</div>
</form>
</script>
<script>
layui.data.sendParams = function (params) {
layui.use(['admin', 'form'], function () {
var $ = layui.$
, admin = layui.admin
, layer = layui.layer
, form = layui.form
, table = layui.table
form.render();
// 提交
form.on('submit(add-mysql-user-submit)', function (data) {
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/mysql80/addUser"
, type: 'post'
, data: data.field
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:用户添加失败,接口返回' + result);
layer.msg('用户添加失败,请刷新重试!')
return false;
}
table.reload('mysql-database-list');
table.reload('mysql-user-list');
layer.alert('用户添加成功!', {
icon: 1
, title: '提示'
, btn: ['确定']
, yes: function (index) {
layer.closeAll();
}
});
}
});
return false;
});
});
};
</script>
@@ -0,0 +1,145 @@
<!--
Name: MySQL管理器 - 数据库备份
Author: 耗子
Date: 2023-07-22
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<div class="layui-row">
<div class="layui-col-xs12 layui-col-sm12 layui-col-md12">
<table class="layui-hide" id="mysql-backup-list" lay-filter="mysql-backup-list"></table>
</div>
</div>
</script>
<!-- 备份顶部工具栏 -->
<script type="text/html" id="mysql-database-backup-bar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="backup_database">备份数据库</button>
<button class="layui-btn layui-btn-sm" id="upload_mysql_backup">上传备份</button>
</div>
</script>
<!-- 备份右侧管理 -->
<script type="text/html" id="mysql-database-backup-control">
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="restore">恢复</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
<script>
layui.data.sendParams = function (params) {
layui.use(['admin', 'form', 'laydate', 'code'], function () {
var $ = layui.$
, admin = layui.admin
, layer = layui.layer
, table = layui.table
, upload = layui.upload;
// 渲染表格
table.render({
elem: '#mysql-backup-list'
, url: '/api/plugins/mysql80/backup'
, toolbar: '#mysql-database-backup-bar'
, title: '备份列表'
, cols: [[
{field: 'file', title: '备份名称', width: 500}
, {field: 'size', title: '文件大小'}
, {field: 'right', title: '操作', width: 150, toolbar: '#mysql-database-backup-control'}
]]
, text: {
none: '无备份数据'
}
, done: function (res, curr, count) {
upload.render({
elem: '#upload_mysql_backup'
, url: '/api/plugins/mysql80/uploadBackup'
, accept: 'file'
, ext: 'sql|zip|rar|tar|gz|bz2'
, before: function (obj) {
index = layer.msg('正在上传备份文件,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
});
}
, done: function (res) {
layer.close(index);
layer.msg('上传成功!', {icon: 1});
table.reload('mysql-backup-list');
}
});
}
});
// 头工具栏事件
table.on('toolbar(mysql-backup-list)', function (obj) {
if (obj.event === 'backup_database') {
index = layer.msg('正在备份数据库,请稍等...', {
icon: 16
, time: 0
});
admin.req({
url: '/api/plugins/mysql80/createBackup'
, type: 'post'
, data: {
database: params.data.name
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:备份数据库失败,接口返回' + result);
layer.alert('备份失败!');
return false;
}
table.reload('mysql-backup-list');
layer.msg('备份成功!', {icon: 1});
}
});
}
});
// 行工具事件
table.on('tool(mysql-backup-list)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('确定要删除数据库备份 <b style="color: red;">' + data.file + '</b> 吗?', function (index) {
index = layer.msg('正在删除数据库备份,请稍等...', {
icon: 16
, time: 0
});
admin.req({
url: "/api/plugins/mysql80/deleteBackup"
, type: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:数据库备份删除失败,接口返回' + result);
layer.msg('数据库备份删除失败,请刷新重试!')
return false;
}
obj.del();
layer.alert('数据库备份' + data.file + '删除成功!');
}
});
});
} else if (obj.event === 'restore') {
layer.confirm('高风险操作,确定要恢复数据库备份 <b style="color: red;">' + data.file + '</b> 吗?', function (index) {
index = layer.msg('正在恢复数据库备份,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
});
data.database = params.data.name;
admin.req({
url: "/api/plugins/mysql80/restoreBackup"
, type: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:数据库恢复失败,接口返回' + result);
layer.msg('数据库备份恢复失败,请刷新重试!')
return false;
}
layer.alert('数据库备份' + data.file + '恢复成功!');
}
});
});
}
});
});
};
</script>
+46 -51
View File
@@ -43,7 +43,7 @@ Date: 2023-06-24
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="openresty-clean-error-log" class="layui-btn">清空日志</button>
<button id="openresty-clear-error-log" class="layui-btn">清空日志</button>
</div>
<pre id="openresty-error-log" class="layui-code">
获取中...
@@ -68,7 +68,7 @@ Date: 2023-06-24
admin.req({
url: '/api/plugins/openresty/status'
, method: 'get'
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:OpenResty运行状态获取失败,接口返回' + result)
@@ -86,7 +86,7 @@ Date: 2023-06-24
// 获取openresty错误日志并渲染
admin.req({
url: '/api/plugins/openresty/errorLog'
, method: 'get'
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:OpenResty错误日志获取失败,接口返回' + result)
@@ -114,7 +114,7 @@ Date: 2023-06-24
// 获取openresty配置并渲染
admin.req({
url: '/api/plugins/openresty/config'
, method: 'get'
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:OpenResty主配置获取失败,接口返回' + result)
@@ -141,54 +141,50 @@ Date: 2023-06-24
// 事件监听
$('#openresty-start').click(function () {
admin.popup({
title: '<span style="color: red;">警告</span>'
,
shade: 0
,
anim: -1
,
area: ['300px', '200px']
,
id: 'layadmin-layer-skin-openresty-start'
,
skin: 'layui-anim layui-anim-upbit'
,
content: '面板的正常访问依赖OpenResty,因此不支持在面板启动OpenResty,如您确需操作,请在SSH执行<span class="layui-badge-rim">systemctl start nginx</span>以启动OpenResty'
})
index = layer.msg('正在启动OpenResty...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/plugins/openresty/start'
, type: 'post'
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
return false
}
admin.events.refresh()
layer.alert('OpenResty启动成功!')
}
})
})
$('#openresty-stop').click(function () {
admin.popup({
title: '<span style="color: red;">警告</span>'
,
shade: 0
,
anim: -1
,
area: ['300px', '200px']
,
id: 'layadmin-layer-skin-openresty-stop'
,
skin: 'layui-anim layui-anim-upbit'
,
content: '面板的正常访问依赖OpenResty,因此不支持在面板停止OpenResty,如您确需操作,请在SSH执行<span class="layui-badge-rim">systemctl stop nginx</span>以停止OpenResty'
})
layer.confirm('停止OpenResty将导致网站无法访问,是否继续停止?', {
btn: ['停止', '取消']
}, function () {
index = layer.msg('正在停止OpenResty...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/plugins/openresty/stop'
, type: 'post'
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
return false
}
admin.events.refresh()
layer.alert('OpenResty停止成功!')
}
})
})
})
$('#openresty-restart').click(function () {
layer.confirm('重启OpenResty有可能导致面板短时间无法访问,是否继续重启?', {
layer.confirm('重启OpenResty将导致网站短时间无法访问,是否继续重启?', {
btn: ['重启', '取消']
}, function () {
index = layer.msg('正在重启OpenResty...', { icon: 16, time: 0 })
index = layer.msg('正在重启OpenResty...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/plugins/openresty/restart'
, method: 'post'
, beforeSend: function () {
layer.msg('已发送重启请求,请稍后刷新确认重启状态。')
}
, type: 'post'
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
console.log('耗子Linux面板:OpenResty重启失败,接口返回' + result)
return false
}
admin.events.refresh()
@@ -198,43 +194,42 @@ Date: 2023-06-24
})
})
$('#openresty-reload').click(function () {
index = layer.msg('正在重载OpenResty...', { icon: 16, time: 0 })
index = layer.msg('正在重载OpenResty...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/plugins/openresty/reload'
, method: 'post'
, type: 'post'
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
console.log('耗子Linux面板:OpenResty重载失败,接口返回' + result)
return false
}
admin.events.refresh()
layer.alert('OpenResty重载成功!')
}
})
})
$('#openresty-config-save').click(function () {
index = layer.msg('正在保存OpenResty主配置...', { icon: 16, time: 0 })
index = layer.msg('正在保存OpenResty主配置...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/plugins/openresty/config'
, method: 'post'
, type: 'post'
, data: {
config: openresty_config_editor.getValue()
}
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
console.log('耗子Linux面板:OpenResty配置保存失败,接口返回' + result)
return false
}
layer.alert('OpenResty配置保存成功!')
}
})
})
$('#openresty-clean-error-log').click(function () {
index = layer.msg('正在清空OpenResty错误日志...', { icon: 16, time: 0 })
$('#openresty-clear-error-log').click(function () {
index = layer.msg('正在清空OpenResty错误日志...', { icon: 16, time: 0, shade: 0.3 })
admin.req({
url: '/api/plugins/openresty/cleanErrorLog'
, method: 'post'
url: '/api/plugins/openresty/clearErrorLog'
, type: 'post'
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
+381
View File
@@ -0,0 +1,381 @@
<!--
Name: PHP管理器
Author: 耗子
Date: 2023-07-22
-->
<title>PHP-7.4</title>
<div class="layui-fluid" id="component-tabs">
<div class="layui-row">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header">PHP-7.4管理</div>
<div class="layui-card-body">
<div class="layui-tab">
<ul class="layui-tab-title">
<li class="layui-this">运行状态</li>
<li>拓展管理</li>
<li>配置修改</li>
<li>负载状态</li>
<li>运行日志</li>
<li>慢日志</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<blockquote id="php74-status" class="layui-elem-quote layui-quote-nm">当前状态:<span
class="layui-badge layui-bg-black">获取中</span></blockquote>
<div class="layui-btn-container" style="padding-top: 30px;">
<button id="php74-start" class="layui-btn">启动</button>
<button id="php74-stop" class="layui-btn layui-btn-danger">停止</button>
<button id="php74-restart" class="layui-btn layui-btn-warm">重启</button>
<button id="php74-reload" class="layui-btn layui-btn-normal">重载</button>
</div>
</div>
<div class="layui-tab-item">
<table id="php74-extension" lay-filter="php74-extension"></table>
<!-- 操作按钮模板 -->
<script type="text/html" id="php74-extension-control">
{{# if(d.installed == true){ }}
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="uninstall">卸载</a>
{{# } else{ }}
<a class="layui-btn layui-btn-xs" lay-event="install">安装</a>
{{# } }}
</script>
</div>
<div class="layui-tab-item">
<blockquote class="layui-elem-quote">此处修改的是PHP主配置文件,如果你不了解各参数的含义,请不要随意修改!<br>
提示:Ctrl+F 搜索关键字,Ctrl+S 保存,Ctrl+H 查找替换!
</blockquote>
<div id="php74-config-editor"
style="height: 600px;"></div>
<div class="layui-btn-container" style="padding-top: 30px;">
<button id="php74-config-save" class="layui-btn">保存</button>
</div>
</div>
<div class="layui-tab-item">
<table class="layui-hide" id="php74-load-status"></table>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="php74-clean-error-log" class="layui-btn">清空日志</button>
</div>
<pre id="php74-error-log" class="layui-code">
获取中...
</pre>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="php74-clean-slow-log" class="layui-btn">清空日志</button>
</div>
<pre id="php74-slow-log" class="layui-code">
获取中...
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let php74_config_editor;
layui.use(['index', 'code', 'table'], function () {
let $ = layui.$
, admin = layui.admin
, element = layui.element
, code = layui.code
, table = layui.table;
// 获取php74运行状态并渲染
admin.req({
url: "/api/plugins/php74/status"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:PHP运行状态获取失败,接口返回' + result);
return false;
}
if (result.data) {
$('#php74-status').html('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
} else {
$('#php74-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
}
}
});
// 获取php74错误日志并渲染
admin.req({
url: "/api/plugins/php74/errorLog"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
$('#php74-error-log').text('PHP日志获取失败,请刷新重试!');
code({
elem: '#php74-error-log'
, title: 'php-fpm.log'
, encode: true
, about: false
});
return false;
}
$('#php74-error-log').text(result.data);
code({
elem: '#php74-error-log'
, title: 'php-fpm.log'
, encode: true
, about: false
});
}
});
// 获取php74慢日志并渲染
admin.req({
url: "/api/plugins/php74/slowLog"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
$('#php74-slow-log').text('PHP慢日志获取失败,请刷新重试!');
code({
elem: '#php74-slow-log'
, title: 'slow.log'
, encode: true
, about: false
});
return false;
}
$('#php74-slow-log').text(result.data);
code({
elem: '#php74-slow-log'
, title: 'slow.log'
, encode: true
, about: false
});
}
});
// 获取php74配置并渲染
admin.req({
url: "/api/plugins/php74/config"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:PHP主配置获取失败,接口返回' + result);
return false;
}
$('#php74-config-editor').text(result.data);
php74_config_editor = ace.edit("php74-config-editor", {
mode: "ace/mode/ini",
selectionStyle: "text"
});
}
});
// 获取php74负载状态并渲染
table.render({
elem: '#php74-load-status'
, url: '/api/plugins/php74/load'
, cols: [[
{field: 'name', width: '80%', title: '属性',}
, {field: 'value', width: '20%', title: '当前值'}
]]
});
element.render();
// 获取php74扩展并渲染
table.render({
elem: '#php74-extension'
, url: '/api/plugins/php74/extensions'
, cols: [[
{field: 'slug', hide: true, title: 'Slug', sort: true}
, {field: 'name', width: '20%', title: '拓展名'}
, {field: 'description', width: '70%', title: '描述'}
, {
field: 'control',
title: '操作',
templet: '#php74-extension-control',
fixed: 'right',
align: 'left'
}
]]
, page: false
, text: {
none: '暂无拓展'
}
});
// 工具条
table.on('tool(php74-extension)', function (obj) {
let data = obj.data;
if (obj.event === 'install') {
layer.confirm('确定安装该拓展吗?', function (index) {
layer.close(index);
index = layer.msg('请稍后...', {icon: 16, time: 0});
admin.req({
url: '/api/plugins/php74/installExtension',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code === 0) {
layer.close(index);
table.reload('php74-extension');
layer.msg('安装:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
});
} else {
layer.msg(res.msg, {icon: 2, time: 1000});
}
}
});
});
} else if (obj.event === 'uninstall') {
layer.confirm('确定卸载该拓展吗?', function (index) {
layer.close(index);
index = layer.msg('请稍后...', {icon: 16, time: 0});
admin.req({
url: '/api/plugins/php74/uninstallExtension',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
layer.close(index);
if (res.code === 0) {
table.reload('php74-extension');
layer.msg('卸载:' + data.name + ' 成功加入任务队列', {icon: 1, time: 1000});
} else {
layer.msg(res.msg, {icon: 2, time: 1000});
}
}
});
});
}
});
// 事件监听
$('#php74-start').click(function () {
index = layer.msg('正在启动PHP,请稍后...', {icon: 16, time: 0});
layer.confirm('确定要启动PHP吗?', {
btn: ['启动', '取消']
}, function () {
admin.req({
url: "/api/plugins/php74/start"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('PHP启动成功!');
}
});
});
});
$('#php74-stop').click(function () {
layer.confirm('停止PHP将导致使用PHP的网站无法访问,是否继续停止?', {
btn: ['停止', '取消']
}, function () {
index = layer.msg('正在停止PHP,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php74/stop"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('PHP停止成功!');
}
});
});
});
$('#php74-restart').click(function () {
layer.confirm('重启PHP将导致使用PHP的网站短时间无法访问,是否继续重启?', {
btn: ['重启', '取消']
}, function () {
index = layer.msg('正在重启PHP,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php74/restart"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('PHP重启成功!');
}
});
});
});
$('#php74-reload').click(function () {
index = layer.msg('正在重载PHP,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php74/reload"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
layer.alert('PHP重载成功!');
}
});
});
$('#php74-config-save').click(function () {
index = layer.msg('正在保存配置,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php74/config"
, type: 'post'
, data: {
config: php74_config_editor.getValue()
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
layer.alert('PHP配置保存成功!');
}
});
});
$('#php74-clean-error-log').click(function () {
index = layer.msg('正在清空错误日志,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php74/clearErrorLog"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.msg('PHP日志已清空!');
}
});
});
$('#php74-clean-slow-log').click(function () {
index = layer.msg('正在清空慢日志,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php74/clearSlowLog"
, type: 'post'
, success: function (result) {
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.msg('PHP慢日志已清空!');
}
});
});
});
</script>
+381
View File
@@ -0,0 +1,381 @@
<!--
Name: PHP管理器
Author: 耗子
Date: 2023-07-22
-->
<title>PHP-8.0</title>
<div class="layui-fluid" id="component-tabs">
<div class="layui-row">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header">PHP-8.0管理</div>
<div class="layui-card-body">
<div class="layui-tab">
<ul class="layui-tab-title">
<li class="layui-this">运行状态</li>
<li>拓展管理</li>
<li>配置修改</li>
<li>负载状态</li>
<li>运行日志</li>
<li>慢日志</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<blockquote id="php80-status" class="layui-elem-quote layui-quote-nm">当前状态:<span
class="layui-badge layui-bg-black">获取中</span></blockquote>
<div class="layui-btn-container" style="padding-top: 30px;">
<button id="php80-start" class="layui-btn">启动</button>
<button id="php80-stop" class="layui-btn layui-btn-danger">停止</button>
<button id="php80-restart" class="layui-btn layui-btn-warm">重启</button>
<button id="php80-reload" class="layui-btn layui-btn-normal">重载</button>
</div>
</div>
<div class="layui-tab-item">
<table id="php80-extension" lay-filter="php80-extension"></table>
<!-- 操作按钮模板 -->
<script type="text/html" id="php80-extension-control">
{{# if(d.installed == true){ }}
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="uninstall">卸载</a>
{{# } else{ }}
<a class="layui-btn layui-btn-xs" lay-event="install">安装</a>
{{# } }}
</script>
</div>
<div class="layui-tab-item">
<blockquote class="layui-elem-quote">此处修改的是PHP主配置文件,如果你不了解各参数的含义,请不要随意修改!<br>
提示:Ctrl+F 搜索关键字,Ctrl+S 保存,Ctrl+H 查找替换!
</blockquote>
<div id="php80-config-editor"
style="height: 600px;"></div>
<div class="layui-btn-container" style="padding-top: 30px;">
<button id="php80-config-save" class="layui-btn">保存</button>
</div>
</div>
<div class="layui-tab-item">
<table class="layui-hide" id="php80-load-status"></table>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="php80-clean-error-log" class="layui-btn">清空日志</button>
</div>
<pre id="php80-error-log" class="layui-code">
获取中...
</pre>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="php80-clean-slow-log" class="layui-btn">清空日志</button>
</div>
<pre id="php80-slow-log" class="layui-code">
获取中...
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let php80_config_editor;
layui.use(['index', 'code', 'table'], function () {
let $ = layui.$
, admin = layui.admin
, element = layui.element
, code = layui.code
, table = layui.table;
// 获取php80运行状态并渲染
admin.req({
url: "/api/plugins/php80/status"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:PHP运行状态获取失败,接口返回' + result);
return false;
}
if (result.data) {
$('#php80-status').html('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
} else {
$('#php80-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
}
}
});
// 获取php80错误日志并渲染
admin.req({
url: "/api/plugins/php80/errorLog"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
$('#php80-error-log').text('PHP日志获取失败,请刷新重试!');
code({
elem: '#php80-error-log'
, title: 'php-fpm.log'
, encode: true
, about: false
});
return false;
}
$('#php80-error-log').text(result.data);
code({
elem: '#php80-error-log'
, title: 'php-fpm.log'
, encode: true
, about: false
});
}
});
// 获取php80慢日志并渲染
admin.req({
url: "/api/plugins/php80/slowLog"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
$('#php80-slow-log').text('PHP慢日志获取失败,请刷新重试!');
code({
elem: '#php80-slow-log'
, title: 'slow.log'
, encode: true
, about: false
});
return false;
}
$('#php80-slow-log').text(result.data);
code({
elem: '#php80-slow-log'
, title: 'slow.log'
, encode: true
, about: false
});
}
});
// 获取php80配置并渲染
admin.req({
url: "/api/plugins/php80/config"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:PHP主配置获取失败,接口返回' + result);
return false;
}
$('#php80-config-editor').text(result.data);
php80_config_editor = ace.edit("php80-config-editor", {
mode: "ace/mode/ini",
selectionStyle: "text"
});
}
});
// 获取php80负载状态并渲染
table.render({
elem: '#php80-load-status'
, url: '/api/plugins/php80/load'
, cols: [[
{field: 'name', width: '80%', title: '属性',}
, {field: 'value', width: '20%', title: '当前值'}
]]
});
element.render();
// 获取php80扩展并渲染
table.render({
elem: '#php80-extension'
, url: '/api/plugins/php80/extensions'
, cols: [[
{field: 'slug', hide: true, title: 'Slug', sort: true}
, {field: 'name', width: '20%', title: '拓展名'}
, {field: 'description', width: '70%', title: '描述'}
, {
field: 'control',
title: '操作',
templet: '#php80-extension-control',
fixed: 'right',
align: 'left'
}
]]
, page: false
, text: {
none: '暂无拓展'
}
});
// 工具条
table.on('tool(php80-extension)', function (obj) {
let data = obj.data;
if (obj.event === 'install') {
layer.confirm('确定安装该拓展吗?', function (index) {
layer.close(index);
index = layer.msg('请稍后...', {icon: 16, time: 0});
admin.req({
url: '/api/plugins/php80/installExtension',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code === 0) {
layer.close(index);
table.reload('php80-extension');
layer.msg('安装:' + data.name + ' 成功加入任务队列', {
icon: 1,
time: 1000
});
} else {
layer.msg(res.msg, {icon: 2, time: 1000});
}
}
});
});
} else if (obj.event === 'uninstall') {
layer.confirm('确定卸载该拓展吗?', function (index) {
layer.close(index);
index = layer.msg('请稍后...', {icon: 16, time: 0});
admin.req({
url: '/api/plugins/php80/uninstallExtension',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
layer.close(index);
if (res.code === 0) {
table.reload('php80-extension');
layer.msg('卸载:' + data.name + ' 成功加入任务队列', {icon: 1, time: 1000});
} else {
layer.msg(res.msg, {icon: 2, time: 1000});
}
}
});
});
}
});
// 事件监听
$('#php80-start').click(function () {
index = layer.msg('正在启动PHP,请稍后...', {icon: 16, time: 0});
layer.confirm('确定要启动PHP吗?', {
btn: ['启动', '取消']
}, function () {
admin.req({
url: "/api/plugins/php80/start"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('PHP启动成功!');
}
});
});
});
$('#php80-stop').click(function () {
layer.confirm('停止PHP将导致使用PHP的网站无法访问,是否继续停止?', {
btn: ['停止', '取消']
}, function () {
index = layer.msg('正在停止PHP,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php80/stop"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('PHP停止成功!');
}
});
});
});
$('#php80-restart').click(function () {
layer.confirm('重启PHP将导致使用PHP的网站短时间无法访问,是否继续重启?', {
btn: ['重启', '取消']
}, function () {
index = layer.msg('正在重启PHP,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php80/restart"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('PHP重启成功!');
}
});
});
});
$('#php80-reload').click(function () {
index = layer.msg('正在重载PHP,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php80/reload"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
layer.alert('PHP重载成功!');
}
});
});
$('#php80-config-save').click(function () {
index = layer.msg('正在保存配置,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php80/config"
, type: 'post'
, data: {
config: php80_config_editor.getValue()
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
layer.alert('PHP配置保存成功!');
}
});
});
$('#php80-clean-error-log').click(function () {
index = layer.msg('正在清空错误日志,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php80/clearErrorLog"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.msg('PHP日志已清空!');
}
});
});
$('#php80-clean-slow-log').click(function () {
index = layer.msg('正在清空慢日志,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php80/clearSlowLog"
, type: 'post'
, success: function (result) {
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.msg('PHP慢日志已清空!');
}
});
});
});
</script>
+300
View File
@@ -0,0 +1,300 @@
<!--
Name: 系统安全
Author: 耗子
Date: 2023-07-21
-->
<title>系统安全</title>
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-form layui-card-header layuiadmin-card-header-auto">
<div class="layui-inline">
<span style="margin-right: 10px;">防火墙</span>
<input type="checkbox" id="safe_firewall" lay-filter="safe_firewall" lay-skin="switch"
lay-text="ON|OFF">
<span style="margin: 0px 10px;">启用SSH</span>
<input type="checkbox" id="safe_ssh" lay-filter="safe_ssh" lay-skin="switch" lay-text="ON|OFF">
<span style="margin: 0px 10px 0px 20px;">SSH端口</span>
<div class="layui-input-inline" style="width: 80px;">
<input type="number" id="safe_ssh_port" class="layui-input" style="height: 30px; margin-top: 5px;"
min=1
max=65535 disabled>
</div>
<div class="layui-input-inline">
<button id="safe_ssh_port_save" class="layui-btn layui-btn-sm layui-btn-primary">确定
</button>
</div>
<span style="margin: 0px 10px 0px 20px;">允许Ping</span>
<input type="checkbox" id="switch_ping" lay-filter="safe_ping" lay-skin="switch" lay-text="ON|OFF">
</div>
<div class="layui-inline" style="float: right;">
<button class="layui-btn layui-btn-sm layui-btn-danger">清空 OpenResty 日志
</button>
</div>
</div>
</div>
<div id="vm_security">
<div class="layui-card">
<div class="layui-form layui-card-header layuiadmin-card-header-auto">
<div class="layui-inline">
<span style="margin-right: 10px;">端口控制</span>
<div class="layui-input-inline">
<input id="safe_add_firewall_rule_port" type="text" name="safe_add_firewall_rule_port"
class="layui-input"
placeholder="例如:3306、1000-2000">
</div>
<div class="layui-input-inline">
<select id="safe_add_firewall_rule_protocol" lay-filter="safe_add_firewall_rule_protocol"
style="height: 30px; margin-top: 5px;">
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
</select>
</div>
<div class="layui-input-inline">
<button id="safe_add_firewall_rule" class="layui-btn layui-btn-sm" style="margin-top: -4px;">放行
</button>
</div>
</div>
</div>
<div class="layui-card-body">
<table class="layui-hide" id="safe-port" lay-filter="safe-port"></table>
<!-- 右侧删除端口 -->
<script type="text/html" id="safe-port-setting">
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
</div>
</div>
</div>
</div>
<script>
layui.use(['layer', 'admin', 'form', 'laypage', 'table'], function () {
var $ = layui.$;
var admin = layui.admin;
var table = layui.table;
var form = layui.form;
var layer = layui.layer;
// 获取防火墙状态
admin.req({
url: '/api/panel/safe/firewallStatus'
, type: 'get'
, dataType: 'json'
, success: function (res) {
if (res.code === 0) {
// 防火墙
if (res.data) {
$('#safe_firewall').attr('checked', true);
} else {
$('#safe_firewall').attr('checked', false);
}
form.render();
} else {
layer.msg(res.message, {icon: 2, shade: 0.1});
}
}
});
// 获取SSH状态
admin.req({
url: '/api/panel/safe/sshStatus'
, type: 'get'
, dataType: 'json'
, success: function (res) {
if (res.code === 0) {
// SSH
if (res.data) {
$('#safe_ssh').attr('checked', true);
} else {
$('#safe_ssh').attr('checked', false);
}
form.render();
} else {
layer.msg(res.message, {icon: 2, shade: 0.1});
}
}
});
// 获取SSH端口
admin.req({
url: '/api/panel/safe/sshPort'
, type: 'get'
, dataType: 'json'
, success: function (res) {
if (res.code === 0) {
// SSH端口
$('#safe_ssh_port').val(res.data);
$('#safe_ssh_port').attr('disabled', false);
form.render();
} else {
layer.msg(res.message, {icon: 2, shade: 0.1});
}
}
});
// 获取ping状态
admin.req({
url: '/api/panel/safe/pingStatus'
, type: 'get'
, dataType: 'json'
, success: function (res) {
if (res.code === 0) {
// ping
if (res.data) {
$('#switch_ping').attr('checked', true);
} else {
$('#switch_ping').attr('checked', false);
}
form.render();
} else {
layer.msg(res.message, {icon: 2, shade: 0.1});
}
}
});
// 设置防火墙开关
form.on('switch(safe_firewall)', function (data) {
admin.req({
url: '/api/panel/safe/firewallStatus'
, type: 'post'
, dataType: 'json'
, data: {
status: data.elem.checked
}
, success: function (res) {
if (res.code === 0) {
layer.msg('设置成功', {icon: 1, shade: 0.1});
} else {
layer.msg(res.message, {icon: 2, shade: 0.1});
}
}
});
});
// 设置SSH开关
form.on('switch(safe_ssh)', function (data) {
admin.req({
url: '/api/panel/safe/sshStatus'
, type: 'post'
, dataType: 'json'
, data: {
status: data.elem.checked
}
, success: function (res) {
if (res.code === 0) {
layer.msg('设置成功', {icon: 1, shade: 0.1});
} else {
layer.msg(res.message, {icon: 2, shade: 0.1});
}
}
});
});
// 设置ping开关
form.on('switch(safe_ping)', function (data) {
admin.req({
url: '/api/panel/safe/pingStatus'
, type: 'post'
, dataType: 'json'
, data: {
status: data.elem.checked
}
, success: function (res) {
if (res.code === 0) {
layer.msg('设置成功', {icon: 1, shade: 0.1});
} else {
layer.msg(res.message, {icon: 2, shade: 0.1});
}
}
});
});
table.render({
elem: '#safe-port'
, url: '/api/panel/safe/firewallRules'
, title: '防火墙'
, cols: [[
{field: 'port', title: '端口', width: 100, sort: true}
, {field: 'protocol', title: '协议', sort: true}
, {fixed: 'right', title: '操作', toolbar: '#safe-port-setting', width: 150}
]]
});
table.on('tool(safe-port)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('确定要删除 <b style="color: red;">' + data.protocol + '</b> 端口 <b style="color: red;">' + data.port + '</b> 吗?', function (index) {
admin.req({
url: "/api/panel/safe/deleteFirewallRule"
, type: 'post'
, data: data
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:端口删除失败,接口返回' + result);
layer.msg('网站删除失败,请刷新重试!')
return false;
}
obj.del();
layer.alert('<b style="color: red;">' + data.protocol + '</b> 端口 <b style="color: red;">' + data.port + '</b> 删除成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error);
}
});
layer.close(index);
});
}
});
// 监听ssh端口保存
$('#safe_ssh_port_save').click(function () {
var port = Number($('#safe_ssh_port').val());
// 判断端口是否合法
if (isNaN(port) || port < 1 || port > 65535) {
layer.msg('端口号不合法', {icon: 2, shade: 0.1});
return false;
}
var index = layer.load();
admin.req({
url: '/api/panel/safe/sshPort'
, type: 'post'
, dataType: 'json'
, data: {
port: port
}
, success: function (res) {
layer.close(index);
if (res.code === 0) {
layer.msg('设置成功', {icon: 1, shade: 0.1});
} else {
layer.msg(res.message, {icon: 2, shade: 0.1});
}
}
});
});
// 监听添加端口保存
$('#safe_add_firewall_rule').click(function () {
var port = $('#safe_add_firewall_rule_port').val();
var protocol = $('#safe_add_firewall_rule_protocol').val();
var index = layer.load();
admin.req({
url: '/api/panel/safe/addFirewallRule'
, type: 'post'
, dataType: 'json'
, data: {
port: port,
protocol: protocol
}
, success: function (res) {
layer.close(index);
if (res.code === 0) {
layer.msg('设置成功', {icon: 1, shade: 0.1});
table.reload('safe-port');
} else {
layer.msg(res.message, {icon: 2, shade: 0.1});
}
}
});
});
});
</script>
+178
View File
@@ -0,0 +1,178 @@
<!--
Name: 面板设置
Author: 耗子
Date: 2023-07-21
-->
<title>面板设置</title>
<div class="layui-fluid">
<div class="layui-row layui-col-space15">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header">设置</div>
<div class="layui-card-body">
<div class="layui-form" lay-filter="panel_setting">
<!--<div class="layui-form-item">
<label class="layui-form-label">API 开关</label>
<div class="layui-input-inline">
<input type="checkbox" name="api" lay-skin="switch" lay-text="ON|OFF">
</div>
<div class="layui-form-mid layui-word-aux">开启后将提供面板API接口的访问支持</div>
</div>
<div id="setting-api-token" class="layui-form-item">
<label class="layui-form-label">API Token</label>
<div class="layui-input-inline">
<input type="text" name="api_token" value="获取中ing..." class="layui-input" disabled>
</div>
<div class="layui-form-mid layui-word-aux">API Token,用于携带访问面板接口</div>
</div>-->
<!--<div class="layui-form-item">
<label class="layui-form-label">多设备登录</label>
<div class="layui-input-inline">
<input type="checkbox" name="multi_login" lay-skin="switch" lay-text="ON|OFF">
</div>
<div class="layui-form-mid layui-word-aux">
开启后将允许多设备同时登录面板,可能具有一定安全隐患
</div>
</div>-->
<div class="layui-form-item">
<label class="layui-form-label">面板名称</label>
<div class="layui-input-inline">
<input type="text" name="name" value="获取中ing..." class="layui-input" disabled>
</div>
<div class="layui-form-mid layui-word-aux">修改面板的显示名称</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">面板用户名</label>
<div class="layui-input-inline">
<input type="text" name="username" value="获取中ing..." class="layui-input" disabled>
</div>
<div class="layui-form-mid layui-word-aux">修改面板的登录用户名</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">面板密码</label>
<div class="layui-input-inline">
<input type="password" name="password" value="" class="layui-input" disabled>
</div>
<div class="layui-form-mid layui-word-aux">修改面板的登录密码(留空不修改)</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">面板邮箱</label>
<div class="layui-input-inline">
<input type="text" name="email" value="获取中ing..." class="layui-input" disabled>
</div>
<div class="layui-form-mid layui-word-aux">修改面板账号的邮箱,目前用于签发免费SSL证书</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">面板端口</label>
<div class="layui-input-inline">
<input type="text" name="port" value="" class="layui-input" disabled>
</div>
<div class="layui-form-mid layui-word-aux">修改面板的访问端口(<b style="color: red;">保存后需要重启面板并修改浏览器地址栏的端口为新端口以访问面板</b>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">面板入口</label>
<div class="layui-input-inline">
<input type="text" name="entrance" value="" class="layui-input" disabled>
</div>
<div class="layui-form-mid layui-word-aux">修改面板的访问入口(<b style="color: red;">保存后需要重启面板并修改浏览器地址栏的入口为新入口以访问面板</b>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">建站目录</label>
<div class="layui-input-inline">
<input type="text" name="website_path" value="获取中ing..." class="layui-input" disabled>
</div>
<div class="layui-form-mid layui-word-aux">修改面板的默认建站目录</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">备份目录</label>
<div class="layui-input-inline">
<input type="text" name="backup_path" value="获取中ing..." class="layui-input" disabled>
</div>
<div class="layui-form-mid layui-word-aux">修改面板的默认备份目录</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="panel_setting_submit">确认修改</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
layui.define(['form', 'upload'], function () {
var $ = layui.$
, layer = layui.layer
, admin = layui.admin
, form = layui.form;
// 渲染表单
form.render();
$('#setting-api-token').hide();
admin.req({
url: "/api/panel/setting/list"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:系统信息获取失败,接口返回' + result);
layer.msg('系统信息获取失败,请刷新重试!')
return false;
}
form.val("panel_setting",
result.data
);
$('input').attr('disabled', false);
if (result.data.api === 1) {
$('#setting-api-token').show();
$('#setting-api-token input').attr('readonly', true);
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error);
}
});
// 面板设置
form.on('submit(panel_setting_submit)', function (obj) {
// 面板API
if (obj.field.api === "on") {
obj.field.api = 1;
} else {
obj.field.api = 0;
}
// 多设备登录
if (obj.field.multi_login === "on") {
obj.field.multi_login = 1;
} else {
obj.field.multi_login = 0;
}
// 提交修改
admin.req({
url: "/api/panel/setting/save"
, type: 'post'
, data: obj.field
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:设置保存失败,接口返回' + result);
layer.msg('面板设置保存失败,请刷新重试!')
return false;
}
admin.render();
layer.msg('面板设置保存成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error);
}
});
return false;
});
});
</script>
+162
View File
@@ -0,0 +1,162 @@
<!--
Name: 任务中心
Author: 耗子
Date: 2023-07-22
-->
<title>任务中心</title>
<div class="layui-fluid" id="component-tabs">
<div class="layui-row">
<div class="layui-col-md12">
<div class="layui-card">
<div class="layui-card-header">任务列表</div>
<div class="layui-card-body">
<div class="layui-tab">
<ul class="layui-tab-title">
<li class="layui-this">进行中</li>
<li>等待中</li>
<li>已完成</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<script type="text/html" template
lay-url="/api/panel/task/list?status=running&page=1&limit=10"
lay-done="layui.data.done(d);">
{{# if(d.data.total != 0){ }}
<blockquote class="layui-elem-quote">{{ d.data.items[0].name }}</blockquote>
<pre id="plugin-install-log" class="layui-code">
日志获取中...
</pre>
{{# } else { }}
<blockquote class="layui-elem-quote">暂无任务</blockquote>
{{# } }}
</script>
</div>
<div class="layui-tab-item">
<table id="panel-task-waiting" lay-filter="panel-task-waiting"></table>
</div>
<div class="layui-tab-item">
<table id="panel-task-finished" lay-filter="panel-task-finished"></table>
<script type="text/html" id="panel-task-finished-control-tpl">
<a class="layui-btn layui-btn-xs" lay-event="remove">移除</a>
</script>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function render_plugin_install_log(d) {
layui.use(['admin', 'jquery'], function () {
let admin = layui.admin
, $ = layui.jquery;
admin.req({
url: "/api/panel/task/log?id=" + d.data.items[0].id
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
$('#plugin-install-log').html('实时安装日志获取失败,请刷新重试!');
return false;
}
$('#plugin-install-log').html(result.data);
}
})
});
}
layui.data.done = function (d) {
if (d.data.items[0] !== undefined) {
render_plugin_install_log(d);
setInterval(function () {
render_plugin_install_log(d);
}, 2000);
}
};
layui.use(['admin', 'table', 'jquery'], function () {
var table = layui.table
, admin = layui.admin;
table.render({
elem: '#panel-task-waiting'
, url: '/api/panel/task/list?status=waiting'
, cols: [[
{field: 'id', hide: true, title: 'ID', sort: true}
, {field: 'name', width: '100%', title: '任务名'}
]]
, page: true
, text: {
none: '无数据'
}
, parseData: function (res) {
return {
"code": res.code,
"msg": res.message,
"count": res.data.total,
"data": res.data.items
};
}
});
table.render({
elem: '#panel-task-finished'
, id: 'panel-task-finished-table'
, url: '/api/panel/task/list?status=finished'
, cols: [[
{field: 'id', hide: true, title: 'ID', sort: true}
, {field: 'name', width: '80%', title: '任务名'}
, {
field: 'control',
title: '操作',
templet: '#panel-task-finished-control-tpl',
fixed: 'right',
align: 'center'
}
]]
, page: true
, text: {
none: '无数据'
}
, parseData: function (res) {
return {
"code": res.code,
"msg": res.message,
"count": res.data.total,
"data": res.data.items
};
}
});
// 工具条
table.on('tool(panel-task-finished)', function (obj) {
let data = obj.data;
if (obj.event === 'remove') {
layer.confirm('确定移除该记录吗?', function (index) {
layer.close(index);
admin.req({
url: '/api/panel/task/delete',
type: 'post',
data: {
id: data.id
}
, success: function (res) {
if (res.code == 0) {
layer.msg('移除任务:' + data.name + ' 成功!', {icon: 1, time: 1000}, function () {
// 重载表格
table.reload('panel-task-finished-table');
});
} else {
layer.msg(res.message, {icon: 2, time: 1000});
}
}
});
});
}
});
});
</script>
+6 -7
View File
@@ -25,9 +25,8 @@ Date: 2023-06-24
<label class="layui-form-label">PHP版本</label>
<div class="layui-input-block">
<select name="php" lay-filter="add-website-php">
<option value="00" selected="">不使用</option>
{{# layui.each(d.params.php, function(index, item){ }}
<option value="{{ item }}">{{ item }}</option>
<option value="{{ item.slug }}">{{ item.name }}</option>
{{# }); }}
</select>
</div>
@@ -57,7 +56,7 @@ Date: 2023-06-24
<div class="layui-inline">
<label class="layui-form-label">数据库用户</label>
<div class="layui-input-inline">
<input type="text" name="db_username" autocomplete="off" class="layui-input">
<input type="text" name="db_user" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-inline">
@@ -73,7 +72,7 @@ Date: 2023-06-24
<label class="layui-form-label">目录</label>
<div class="layui-input-block">
<input type="text" name="path"
placeholder="请输入网站根目录(不填默认为/www/wwwroot/网站名)"
placeholder="请输入网站根目录(不填默认为建站目录/网站名)"
autocomplete="off" class="layui-input">
</div>
</div>
@@ -122,11 +121,11 @@ Date: 2023-06-24
if (data.value === 'mysql') {
$('#add-website-db-info').show()
$('input[name="db_name"]').val($('input[name="name"]').val() + '_mysql')
$('input[name="db_username"]').val($('input[name="name"]').val() + '_mysql')
$('input[name="db_user"]').val($('input[name="name"]').val() + '_mysql')
} else if (data.value === 'postgresql') {
$('#add-website-db-info').show()
$('input[name="db_name"]').val($('input[name="name"]').val() + '_postgresql')
$('input[name="db_username"]').val($('input[name="name"]').val() + '_postgresql')
$('input[name="db_user"]').val($('input[name="name"]').val() + '_postgresql')
}
})
// 提交
@@ -139,7 +138,7 @@ Date: 2023-06-24
}
admin.req({
url: '/api/panel/website/add'
, method: 'post'
, type: 'post'
, data: data.field
, success: function (result) {
if (result.code !== 0) {
+158
View File
@@ -0,0 +1,158 @@
<!--
Name: 网站 - 备份
Author: 耗子
Date: 2023-07-21
-->
<h1>这里正在装修,下个版本再来看看吧!</h1>
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<div class="layui-row">
<div class="layui-col-xs12 layui-col-sm12 layui-col-md12">
<table class="layui-hide" id="website-backup-list" lay-filter="website-backup-list"></table>
</div>
</div>
</script>
<!-- 备份顶部工具栏 -->
<script type="text/html" id="website-backup-bar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="backup_website">备份网站</button>
<button class="layui-btn layui-btn-sm" id="upload_website_backup">上传备份</button>
</div>
</script>
<!-- 备份右侧管理 -->
<script type="text/html" id="website-backup-control">
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="restore">恢复</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
</script>
<script>
layui.data.sendParams = function (params) {
layui.use(['admin', 'form', 'laydate', 'code'], function () {
var $ = layui.$
, admin = layui.admin
, layer = layui.layer
, table = layui.table
, upload = layui.upload;
// 渲染表格
table.render({
elem: '#website-backup-list'
, url: '/api/panel/website/backupList'
, toolbar: '#website-backup-bar'
, title: '备份列表'
, cols: [[
{field: 'backup', title: '备份名称', width: 500}
, {field: 'size', title: '文件大小'}
, {field: 'right', title: '操作', width: 150, toolbar: '#website-backup-control'}
]]
, text: {
none: '无备份数据'
}
, done: function (res, curr, count) {
upload.render({
elem: '#upload_website_backup'
, url: '/api/panel/website/uploadBackup'
, accept: 'file'
, exts: 'zip'
, before: function (obj) {
index = layer.msg('正在上传备份文件,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
});
}
, done: function (res) {
layer.close(index);
layer.msg('上传成功!', {icon: 1});
table.reload('website-backup-list');
}
, error: function (res) {
layer.msg('上传失败:' + res.msg, {icon: 2});
}
});
}
});
// 头工具栏事件
table.on('toolbar(website-backup-list)', function (obj) {
if (obj.event === 'backup_website') {
index = layer.msg('正在备份网站,请稍等...', {
icon: 16
, time: 0
});
admin.req({
url: '/api/panel/website/createBackup'
, type: 'post'
, data: {
name: params.data.name
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:网站失败,接口返回' + result);
layer.alert('备份失败!');
return false;
}
table.reload('website-backup-list');
layer.msg('备份成功!', {icon: 1});
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error);
}
});
}
});
// 行工具事件
table.on('tool(website-backup-list)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('确定要删除网站备份 <b style="color: red;">' + data.backup + '</b> 吗?', function (index) {
index = layer.msg('正在删除网站备份,请稍等...', {
icon: 16
, time: 0
});
admin.req({
url: "/api/panel/website/deleteBackup"
, method: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:网站备份删除失败,接口返回' + result);
layer.msg('网站备份删除失败,请刷新重试!')
return false;
}
obj.del();
layer.alert('网站备份' + data.backup + '删除成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error);
}
});
});
} else if (obj.event === 'restore') {
layer.confirm('高风险操作,确定要恢复网站备份 <b style="color: red;">' + data.backup + '</b> 吗?', function (index) {
index = layer.msg('正在恢复网站备份,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
});
data.name = params.data.name;
admin.req({
url: "/api/panel/website/restoreBackup"
, method: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:网站备份恢复失败,接口返回' + result);
layer.msg('网站备份恢复失败,请刷新重试!')
return false;
}
layer.alert('网站备份' + data.backup + '恢复成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error);
}
});
});
}
});
});
};
</script>
@@ -1,9 +1,9 @@
<!--
Name: 网站 - 全局设置
Author: 耗子
Date: 2023-06-24
Date: 2023-07-21
-->
<script type="text/html" template lay-url="/api/panel/website/getDefaultSettings"
<script type="text/html" template lay-url="/api/panel/website/defaultConfig"
lay-done="layui.data.sendParams(d.params)">
<div class="layui-tab">
<ul class="layui-tab-title">
@@ -38,10 +38,7 @@ Date: 2023-06-24
layui.use(['admin', 'form', 'laydate', 'code'], function () {
var $ = layui.$
, admin = layui.admin
, element = layui.element
, layer = layui.layer
, laydate = layui.laydate
, code = layui.code;
, layer = layui.layer;
indexEditor = ace.edit("index-editor", {
mode: "ace/mode/html",
selectionStyle: "text"
@@ -54,7 +51,7 @@ Date: 2023-06-24
$('#save-website-default-settings').click(function () {
layer.load();
admin.req({
url: '/api/panel/website/saveDefaultSettings'
url: '/api/panel/website/defaultConfig'
, type: 'post'
, data: {
index: indexEditor.getValue(),
@@ -63,17 +60,14 @@ Date: 2023-06-24
, success: function (res) {
layer.closeAll('loading');
if (res.code === 0) {
layer.msg('保存成功', {icon: 1});
layer.msg('保存成功', {icon: 1, shade: 0.3});
setTimeout(function () {
admin.render();
}, 1000);
} else {
layer.msg(res.msg, {icon: 2});
layer.msg(res.message, {icon: 2, shade: 0.3});
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error);
}
});
});
+257 -183
View File
@@ -1,7 +1,7 @@
<!--
Name: 网站 - 编辑
Author: 耗子
Date: 2023-06-24
Date: 2023-07-21
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<div class="layui-tab" lay-filter="website-edit-tab">
@@ -17,22 +17,62 @@ Date: 2023-06-24
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<!-- 域名绑定 -->
<div class="layui-form layui-form-pane">
<div class="layui-form">
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">域名</label>
<div class="layui-input-block">
<textarea name="domain" lay-verify="required"
placeholder="请输入域名,一行一个支持泛域名"
class="layui-textarea">{{ d.params.config.domain }}</textarea>
<div class="layui-input-inline" id="domains">
{{# layui.each(d.params.config.domains, function(index, item){ }}
<div class="layui-form-item">
<div class="layui-input-inline">
<input type="text" name="domains[]" lay-verify="required" value="{{ item }}"
class="layui-input" style="width: 200%;">
</div>
<div class="layui-input-block" style="margin-left: 480px">
<button type="button"
class="layui-btn layui-btn-danger layui-btn-sm removeclass"><i
class="layui-icon">&#xe67e;</i></button>
</div>
</div>
{{# }); }}
</div>
<div class="layui-form-item">
<div class="layui-input-block" style="margin-left: 480px">
<button id="add-domain" type="button"
class="layui-btn layui-btn-warm layui-btn-sm">
<i class="layui-icon">&#xe654;</i>
</button>
</div>
</div>
</div>
</div>
<hr>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">端口</label>
<div class="layui-input-block">
<textarea name="port" lay-verify="required"
placeholder="请输入访问端口,一行一个"
class="layui-textarea">{{ d.params.config.port }}</textarea>
<div class="layui-input-inline" id="ports">
{{# layui.each(d.params.config.ports, function(index, item){ }}
<div class="layui-form-item">
<div class="layui-input-inline">
<input type="text" name="ports[]" lay-verify="required" value="{{ item }}"
class="layui-input" style="width: 200%;">
</div>
<div class="layui-input-block" style="margin-left: 480px">
<button type="button"
class="layui-btn layui-btn-danger layui-btn-sm removeclass"><i
class="layui-icon">&#xe67e;</i></button>
</div>
</div>
{{# }); }}
</div>
<div class="layui-form-item">
<div class="layui-input-block" style="margin-left: 480px">
<button id="add-port" type="button"
class="layui-btn layui-btn-warm layui-btn-sm">
<i class="layui-icon">&#xe654;</i>
</button>
</div>
</div>
</div>
</div>
</div>
@@ -66,11 +106,11 @@ Date: 2023-06-24
<label class="layui-form-label">PHP版本</label>
<div class="layui-input-block">
<select name="php" lay-filter="website-php">
{{# layui.each(d.params.php_version, function(index, item){ }}
{{# if(item == d.params.config.php){ }}
<option value="{{ item }}" selected="">{{ item }}</option>
{{# layui.each(d.params.php, function(index, item){ }}
{{# if(item.slug == d.params.config.php){ }}
<option value="{{ item.slug }}" selected="">{{ item.name }}</option>
{{# }else{ }}
<option value="{{ item }}">{{ item }}</option>
<option value="{{ item.slug }}">{{ item.name }}</option>
{{# } }}
{{# }); }}
</select>
@@ -80,7 +120,7 @@ Date: 2023-06-24
<label class="layui-form-label">防跨站攻击</label>
<div class="layui-input-block">
<input type="checkbox" name="open_basedir" lay-skin="switch" lay-text="ON|OFF"
{{ d.params.config.open_basedir== 1 ? 'checked' : '' }}>
{{ d.params.config.open_basedir ? 'checked' : '' }}>
</div>
</div>
</div>
@@ -97,7 +137,7 @@ Date: 2023-06-24
<label class="layui-form-label">总开关</label>
<div class="layui-input-inline">
<input type="checkbox" name="waf" lay-skin="switch" lay-text="ON|OFF"
{{ d.params.config.waf== 1 ? 'checked' : '' }}>
{{ d.params.config.waf ? 'checked' : '' }}>
</div>
<div class="layui-form-mid layui-word-aux">只有打开了总开关下面的设置才会生效</div>
</div>
@@ -132,7 +172,7 @@ Date: 2023-06-24
<label class="layui-form-label">总开关</label>
<div class="layui-input-inline">
<input type="checkbox" name="ssl" lay-skin="switch" lay-text="ON|OFF"
{{ d.params.config.ssl== 1 ? 'checked' : '' }}>
{{ d.params.config.ssl ? 'checked' : '' }}>
</div>
<div class="layui-form-mid layui-word-aux">只有打开了总开关下面的设置才会生效</div>
</div>
@@ -142,25 +182,25 @@ Date: 2023-06-24
<label class="layui-form-label">HTTP跳转</label>
<div class="layui-input-block">
<input type="checkbox" name="http_redirect" lay-skin="switch" lay-text="ON|OFF"
{{ d.params.config.http_redirect== 1 ? 'checked' : '' }}>
{{ d.params.config.http_redirect ? 'checked' : '' }}>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">HSTS</label>
<div class="layui-input-inline">
<input type="checkbox" name="hsts" lay-skin="switch" lay-text="ON|OFF"
{{ d.params.config.hsts== 1 ? 'checked' : '' }}>
{{ d.params.config.hsts ? 'checked' : '' }}>
</div>
</div>
<div class="layui-inline">
<!--<div class="layui-inline">
<div class="layui-input-inline">
<button id="issue-ssl" class="layui-btn layui-btn-sm">签发免费SSL证书</button>
</div>
</div>
</div>-->
</div>
<div class="layui-form-item layui-form-text">
{{# if(d.params.config.ssl == 1){ }}
{{# if(d.params.config.ssl){ }}
<label class="layui-form-label">证书 <span style="color: red; float: right;">剩余有效期{{ d.params.config.ssl_date }}</span></label>
{{# }else{ }}
<label class="layui-form-label">证书</label>
@@ -195,7 +235,7 @@ Date: 2023-06-24
<br>
如果你修改了原文那么点击保存后其余的修改将不会生效
</blockquote>
<div id="config-editor" style="height: 400px;">{{ d.params.config.config_raw }}</div>
<div id="config-editor" style="height: 400px;">{{ d.params.config.raw }}</div>
</div>
<div class="layui-tab-item">
<!-- 访问日志 -->
@@ -209,171 +249,205 @@ Date: 2023-06-24
</div>
</script>
<script>
let rewriteEditor = ''
let configEditor = ''
layui.data.sendParams = function (params) {
layui.use(['admin', 'form', 'laydate', 'code'], function () {
var $ = layui.$
, admin = layui.admin
, layer = layui.layer
, code = layui.code
, form = layui.form
, element = layui.element
form.render()
element.render()
element.on('tab(website-edit-tab)', function (data) {
if (data.index === 6) {
// 隐藏保存按钮
$('.layui-footer').hide()
} else {
$('.layui-footer').show()
}
})
rewriteEditor = ace.edit('rewrite-editor', {
mode: 'ace/mode/nginx',
selectionStyle: 'text'
})
configEditor = ace.edit('config-editor', {
mode: 'ace/mode/nginx',
selectionStyle: 'text'
})
code({
elem: '#website-log'
, encode: true
, about: false
let rewriteEditor = ''
let configEditor = ''
layui.data.sendParams = function (params) {
layui.use(['admin', 'form', 'laydate', 'code'], function () {
var $ = layui.$
, admin = layui.admin
, layer = layui.layer
, code = layui.code
, form = layui.form
, element = layui.element
form.render()
element.render()
element.on('tab(website-edit-tab)', function (data) {
if (data.index === 6) {
// 隐藏保存按钮
$('.layui-footer').hide()
} else {
$('.layui-footer').show()
}
})
rewriteEditor = ace.edit('rewrite-editor', {
mode: 'ace/mode/nginx',
selectionStyle: 'text'
})
configEditor = ace.edit('config-editor', {
mode: 'ace/mode/nginx',
selectionStyle: 'text'
})
code({
elem: '#website-log'
, encode: true
, about: false
})
})
$('#clean-site-log').click(function () {
layer.confirm('确定要清空日志吗?', function (index) {
layer.close(index)
layer.load()
admin.req({
url: '/api/panel/website/clearSiteLog'
, type: 'post'
, data: { name: params.config.name }
, success: function (res) {
layer.closeAll('loading')
if (res.code === 0) {
layer.msg('已清空', { icon: 1 })
setTimeout(function () {
admin.render()
}, 1000)
} else {
layer.msg(res.msg, { icon: 2 })
}
}
})
})
})
$('#save-site-config').click(function () {
layer.load()
var port = $('textarea[name="port"]').val()
var reg = new RegExp(/\n443.*\n?/)
// 如果开启了https,就自动添加443端口
if ($('input[name="ssl"]').prop('checked') && !reg.test(port)) {
port = port + '\n443'
}
// 如果关闭了https,就自动删除443端口
if (!$('input[name="ssl"]').prop('checked') && reg.test(port)) {
// 正则替换
port = port.replace(/443.*\n?/, '')
}
admin.req({
url: '/api/panel/website/saveSiteSettings'
, type: 'post'
, data: {
name: params.config.name,
config: {
domain: $('textarea[name="domain"]').val(),
port: port,
ssl: $('input[name="ssl"]').prop('checked') ? 1 : 0,
http_redirect: $('input[name="http_redirect"]').prop('checked') ? 1 : 0,
hsts: $('input[name="hsts"]').prop('checked') ? 1 : 0,
ssl_certificate: $('textarea[name="ssl_certificate"]').val(),
ssl_certificate_key: $('textarea[name="ssl_certificate_key"]').val(),
path: $('input[name="path"]').val(),
root: $('input[name="root"]').val(),
index: $('input[name="index"]').val(),
php: $('select[name="php"]').val(),
open_basedir: $('input[name="open_basedir"]').prop('checked') ? 1 : 0,
waf: $('input[name="waf"]').prop('checked') ? 1 : 0,
waf_mode: $('input[name="waf_mode"]').val(),
waf_cc_deny: $('input[name="waf_cc_deny"]').val(),
waf_cache: $('input[name="waf_cache"]').val(),
rewrite: rewriteEditor.getValue(),
config_raw: configEditor.getValue()
}
}
, success: function (res) {
layer.closeAll('loading')
if (res.code === 0) {
layer.msg('保存成功', { icon: 1 })
setTimeout(function () {
admin.render()
}, 1000)
} else {
layer.msg(res.msg, { icon: 2 })
}
}
})
})
// 重置配置
$('#site-config-restore').click(function () {
layer.confirm('高风险操作,网站配置重置后所有配置均需重新设置,确定要重置配置吗?', function (index) {
index = layer.msg('重置网站配置', {
icon: 16
, time: 0
})
admin.req({
url: '/api/panel/website/resetSiteConfig'
, type: 'post'
, data: { name: params.config.name }
, success: function (res) {
layer.close(index)
if (res.code === 0) {
layer.alert('重置成功,你需要重新添加域名/端口绑定,设置各配置参数!', function (index) {
admin.render()
layer.close(index)
$('#clean-site-log').click(function () {
layer.confirm('确定要清空日志吗?', function (index) {
layer.close(index)
layer.load()
admin.req({
url: '/api/panel/website/clearSiteLog'
, type: 'post'
, data: {name: params.config.name}
, success: function (res) {
layer.closeAll('loading')
if (res.code === 0) {
layer.msg('已清空', {icon: 1})
setTimeout(function () {
admin.render()
}, 1000)
} else {
layer.msg(res.message, {icon: 2})
}
}
})
})
} else {
layer.msg(res.msg, { icon: 2 })
}
}
})
})
})
})
// 监听签发证书按钮
$('#issue-ssl').click(function () {
layer.confirm('确定要申请签发免费SSL证书吗?', function (index) {
index = layer.msg('正在签发证书,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
})
admin.req({
url: '/api/panel/website/issueSsl'
, type: 'post'
, data: {
name: params.config.name
, type: 'lets'
}
, success: function (res) {
layer.close(index)
if (res.code === 0) {
layer.msg('签发成功', { icon: 1 })
setTimeout(function () {
admin.render()
}, 1000)
} else {
layer.alert(res.msg, { icon: 2 })
}
}
})
$('#save-site-config').click(function () {
layer.load()
let ports = $("input[name='ports[]']").map(function () {
return $(this).val();
}).get()
let domains = $("input[name='domains[]']").map(function () {
return $(this).val();
}).get()
admin.req({
url: '/api/panel/website/config'
, type: 'post'
, data: {
id: params.id,
domains: domains.join('\n'),
ports: ports.join('\n'),
ssl: $('input[name="ssl"]').prop('checked'),
http_redirect: $('input[name="http_redirect"]').prop('checked'),
hsts: $('input[name="hsts"]').prop('checked'),
ssl_certificate: $('textarea[name="ssl_certificate"]').val(),
ssl_certificate_key: $('textarea[name="ssl_certificate_key"]').val(),
path: $('input[name="path"]').val(),
root: $('input[name="root"]').val(),
index: $('input[name="index"]').val(),
php: $('select[name="php"]').val(),
open_basedir: $('input[name="open_basedir"]').prop('checked'),
waf: $('input[name="waf"]').prop('checked') ? 'on' : 'off',
waf_mode: $('input[name="waf_mode"]').val(),
waf_cc_deny: $('input[name="waf_cc_deny"]').val(),
waf_cache: $('input[name="waf_cache"]').val(),
rewrite: rewriteEditor.getValue(),
raw: configEditor.getValue()
}
, success: function (res) {
layer.closeAll('loading')
if (res.code === 0) {
layer.msg('保存成功', {icon: 1})
setTimeout(function () {
admin.render()
}, 1000)
} else {
layer.msg(res.message, {icon: 2})
}
}
})
})
// 重置配置
$('#site-config-restore').click(function () {
layer.confirm('高风险操作,网站配置重置后所有配置均需重新设置,确定要重置配置吗?', function (index) {
index = layer.msg('重置网站配置', {
icon: 16
, time: 0
})
admin.req({
url: '/api/panel/website/resetConfig'
, type: 'post'
, data: {id: params.id}
, success: function (res) {
layer.close(index)
if (res.code === 0) {
layer.alert('重置成功,你需要重新添加域名/端口绑定,设置各配置参数!', function (index) {
admin.render()
layer.close(index)
})
} else {
layer.msg(res.message, {icon: 2})
}
}
})
})
})
// 监听签发证书按钮
$('#issue-ssl').click(function () {
layer.confirm('确定要申请签发免费SSL证书吗?', function (index) {
index = layer.msg('正在签发证书,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
})
admin.req({
url: '/api/panel/website/issueSsl'
, type: 'post'
, data: {
name: params.config.name
, type: 'lets'
}
, success: function (res) {
layer.close(index)
if (res.code === 0) {
layer.msg('签发成功', {icon: 1})
setTimeout(function () {
admin.render()
}, 1000)
} else {
layer.alert(res.message, {icon: 2})
}
}
})
})
})
// 动态添加input输入框
$("#add-domain").click(function () {
var str = '<div class="layui-form-item">' +
'<div class="layui-input-inline">' +
'<input type="text" name="domains[]" placeholder="example.com" lay-verify="required"' +
'class="layui-input" style="width: 200%;">' +
'</div>' +
'<div class="layui-input-block" style="margin-left: 480px">' +
'<button type="button" class="layui-btn layui-btn-danger layui-btn-sm removeclass"><i class="layui-icon">&#xe67e;</i></button>' +
'</div>' +
'</div>';
$("#domains").append(str);
form.render();
});
$("#add-port").click(function () {
var str = '<div class="layui-form-item">' +
'<div class="layui-input-inline">' +
'<input type="text" name="ports[]" placeholder="443 ssl" lay-verify="required"' +
'class="layui-input" style="width: 200%;">' +
'</div>' +
'<div class="layui-input-block" style="margin-left: 480px">' +
'<button type="button" class="layui-btn layui-btn-danger layui-btn-sm removeclass"><i class="layui-icon">&#xe67e;</i></button>' +
'</div>' +
'</div>';
$("#ports").append(str);
form.render();
});
//删除动态添加的input输入框
$("body").on('click', ".removeclass", function () {
let parentEle = $(this).parent().parent();
parentEle.remove();
form.render();
});
})
})
})
}
}
</script>
+198 -192
View File
@@ -1,7 +1,7 @@
<!--
Name: 网站 - 列表
Author: 耗子
Date: 2023-06-24
Date: 2023-07-71
-->
<title>网站</title>
<div class="layui-fluid">
@@ -16,7 +16,7 @@ Date: 2023-06-24
<script type="text/html" id="website-list-bar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="website_add">添加网站</button>
<button class="layui-btn layui-btn-sm" lay-event="website_default_settings">全局设置
<button class="layui-btn layui-btn-sm" lay-event="website_default_config">全局设置
</button>
</div>
</script>
@@ -30,12 +30,16 @@ Date: 2023-06-24
<script type="text/html" id="website-run">
<input type="checkbox" name="run" lay-skin="switch" lay-text="ON|OFF"
lay-filter="website-run-checkbox"
value="{{ d.status }}" data-website-name="{{ d.name }}"
{{ d.status== 1 ? 'checked' : '' }}>
value="{{ d.status }}" data-website-id="{{ d.id }}"
{{ d.status ? 'checked' : '' }}>
</script>
<!-- 网站SSL状态 -->
<script type="text/html" id="website-ssl">
{{ d.ssl == 1 ? '已开启' : '未开启' }}
{{ d.ssl ? '已开启' : '未开启' }}
</script>
<!-- 网站PHP版本 -->
<script type="text/html" id="website-php">
{{ d.php == '0' ? '不使用' : d.php }}
</script>
</div>
</div>
@@ -44,198 +48,200 @@ Date: 2023-06-24
</div>
<script>
let php, mysql, postgresql
layui.use(['admin', 'table', 'form', 'view'], function () {
var admin = layui.admin
, table = layui.table
, form = layui.form
, view = layui.view
// 获取已安装的PHP和DB版本
admin.req({
url: '/api/panel/info/installedDbAndPhp'
, method: 'get'
, success: function (result) {
php = result.data.php
mysql = result.data.mysql
postgresql = result.data.postgresql
}
})
table.render({
elem: '#website-list'
, url: '/api/panel/website/list'
, toolbar: '#website-list-bar'
, title: '网站列表'
, cols: [[
{ field: 'name', title: '网站名', width: 150, unresize: true, sort: true }
, { field: 'run', title: '运行', width: 100, templet: '#website-run', unresize: true }
, { field: 'path', title: '目录', width: 250 }
, { field: 'php', title: 'PHP', width: 60 }
, { field: 'ssl', title: 'SSL', width: 110, templet: '#website-ssl' }
, { field: 'note', title: '备注', edit: 'textarea' }
, { fixed: 'right', title: '操作', unresize: true, toolbar: '#website-control', width: 180 }
]]
, parseData: function (res) {
return {
'code': res.code,
'msg': res.message,
'count': res.data.total,
'data': res.data.items
}
}
, page: true
})
// 头工具栏事件
table.on('toolbar(website-list)', function (obj) {
if (obj.event === 'website_add') {
admin.popup({
title: '添加网站'
, area: ['80%', '60%']
, id: 'LAY-popup-website-add'
, success: function (layer, index) {
view(this.id).render('website/add', {
php: php,
mysql: mysql,
postgresql: postgresql
}).done(function () {
form.render(null, 'LAY-popup-website-add')
})
}
})
} else if (obj.event === 'website_default_settings') {
admin.popup({
title: '全局设置'
, area: ['80%', '80%']
, id: 'LAY-popup-website-add'
, success: function (layer, index) {
view(this.id).render('website/default_settings', {}).done(function () {
form.render(null, 'LAY-popup-website-default-settings')
})
}
})
}
})
// 行工具事件
table.on('tool(website-list)', function (obj) {
let data = obj.data
if (obj.event === 'del') {
layer.confirm('删除网站将一并删除站点目录(不包括数据库),是否继续?', function (index) {
admin.req({
url: '/api/panel/website/delete'
, method: 'post'
, data: data
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:网站删除失败,接口返回' + result)
layer.msg('网站删除失败,请刷新重试!')
return false
}
obj.del()
layer.alert('网站' + data.name + '删除成功!')
}
})
layer.close(index)
})
} else if (obj.event === 'edit') {
let config
let php, mysql, postgresql
layui.use(['admin', 'table', 'form', 'view'], function () {
var admin = layui.admin
, table = layui.table
, form = layui.form
, view = layui.view
// 获取已安装的PHP和DB版本
admin.req({
url: '/api/panel/website/getSiteSettings?name=' + data.name
, method: 'get'
, beforeSend: function (request) {
layer.load()
}
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:网站设置获取失败,接口返回' + result)
layer.alert('网站设置获取失败!')
return false
url: '/api/panel/info/installedDbAndPhp'
, type: 'get'
, success: function (result) {
php = result.data.php
mysql = result.data.mysql
postgresql = result.data.postgresql
}
config = result.data
layer.closeAll('loading')
// 打开编辑网站页面
admin.popup({
title: '编辑网站 - ' + data.name
, area: ['80%', '80%']
, id: 'LAY-popup-website-edit'
, success: function (layero, index) {
view(this.id).render('website/edit', {
php: php,
mysql: mysql,
postgresql: postgresql,
data: data,
config: config
}).done(function () {
form.render(null, 'LAY-popup-website-edit')
})
table.render({
elem: '#website-list'
, url: '/api/panel/website/list'
, toolbar: '#website-list-bar'
, title: '网站列表'
, cols: [[
{field: 'id', hide: true, title: 'ID', sort: true}
, {field: 'name', title: '网站名', width: 150, unresize: true, sort: true}
, {field: 'run', title: '运行', width: 100, templet: '#website-run', unresize: true}
, {field: 'path', title: '目录', width: 250}
, {field: 'php', title: 'PHP', width: 100, templet: '#website-php'}
, {field: 'ssl', title: 'SSL', width: 100, templet: '#website-ssl'}
, {field: 'remark', title: '备注', edit: 'textarea'}
, {fixed: 'right', title: '操作', unresize: true, toolbar: '#website-control', width: 180}
]]
, parseData: function (res) {
return {
'code': res.code,
'msg': res.message,
'count': res.data.total,
'data': res.data.items
}
}
, page: true
})
// 头工具栏事件
table.on('toolbar(website-list)', function (obj) {
if (obj.event === 'website_add') {
admin.popup({
title: '添加网站'
, area: ['80%', '60%']
, id: 'LAY-popup-website-add'
, success: function (layer, index) {
view(this.id).render('website/add', {
php: php,
mysql: mysql,
postgresql: postgresql
}).done(function () {
form.render(null, 'LAY-popup-website-add')
})
}
})
}
})
}
} else if (obj.event === 'website_default_config') {
admin.popup({
title: '全局设置'
, area: ['80%', '80%']
, id: 'LAY-popup-website-add'
, success: function (layer, index) {
view(this.id).render('website/default_config', {}).done(function () {
form.render(null, 'LAY-popup-website-default-config')
})
}
})
}
})
} else if (obj.event === 'backup') {
// 打开备份页面
admin.popup({
title: '备份管理 - ' + data.name
, area: ['70%', '80%']
, id: 'LAY-popup-website-backup'
, success: function (layero, index) {
view(this.id).render('website/backup', {
data: data
}).done(function () {
form.render(null, 'LAY-popup-website-backup')
})
}
// 行工具事件
table.on('tool(website-list)', function (obj) {
let data = obj.data
if (obj.event === 'del') {
layer.confirm('删除网站将一并删除站点目录(不包括数据库),是否继续?', function (index) {
admin.req({
url: '/api/panel/website/delete'
, type: 'post'
, data: data
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:网站删除失败,接口返回' + result)
layer.msg('网站删除失败,请刷新重试!')
return false
}
obj.del()
layer.alert('网站' + data.name + '删除成功!')
}
})
layer.close(index)
})
} else if (obj.event === 'edit') {
let config
admin.req({
url: '/api/panel/website/config?id=' + data.id
, type: 'get'
, beforeSend: function (request) {
layer.load()
}
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:网站设置获取失败,接口返回' + result)
layer.alert('网站设置获取失败!')
return false
}
config = result.data
layer.closeAll('loading')
// 打开编辑网站页面
admin.popup({
title: '编辑网站 - ' + data.name
, area: ['80%', '80%']
, id: 'LAY-popup-website-edit'
, success: function (layero, index) {
view(this.id).render('website/edit', {
id : data.id,
php: php,
mysql: mysql,
postgresql: postgresql,
data: data,
config: config
}).done(function () {
form.render(null, 'LAY-popup-website-edit')
})
}
})
}
})
} else if (obj.event === 'backup') {
// 打开备份页面
admin.popup({
title: '备份管理 - ' + data.name
, area: ['70%', '80%']
, id: 'LAY-popup-website-backup'
, success: function (layero, index) {
view(this.id).render('website/backup', {
data: data
}).done(function () {
form.render(null, 'LAY-popup-website-backup')
})
}
})
}
})
}
// 网站备注编辑
table.on('edit(website-list)', function (obj) {
let value = obj.value // 得到修改后的值
, data = obj.data // 得到行数据
admin.req({
url: '/api/panel/website/updateRemark'
, type: 'post'
, data: {
id: data.id,
remark: value
}
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:网站备注更新失败,接口返回' + result)
layer.msg('网站备注更新失败,请刷新重试!')
return false
}
layer.alert('网站 ' + data.name + ' 备注更新成功!')
}
})
})
// 网站运行状态操作
form.on('switch(website-run-checkbox)', function (obj) {
let $ = layui.$
let website_id = $(this).data('website-id')
let status = obj.elem.checked
admin.req({
url: '/api/panel/website/status'
, type: 'post'
, data: {
id: website_id,
status: status
}
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:网站运行状态设置失败,接口返回' + result)
layer.msg('网站运行状态设置失败,请刷新重试!')
return false
}
layer.alert('运行状态修改成功')
}
})
})
})
// 网站备注编辑
table.on('edit(website-list)', function (obj) {
let value = obj.value // 得到修改后的值
, data = obj.data // 得到行数据
admin.req({
url: '/api/panel/website/updateSiteNote'
, method: 'post'
, data: {
name: data.name,
note: value
}
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:网站备注更新失败,接口返回' + result)
layer.msg('网站备注更新失败,请刷新重试!')
return false
}
layer.alert('网站 ' + data.name + ' 备注更新成功!')
}
})
})
// 网站运行状态操作
form.on('switch(website-run-checkbox)', function (obj) {
let $ = layui.$
let website_name = $(this).data('website-name')
let status = obj.elem.checked ? 1 : 0
admin.req({
url: '/api/panel/website/setSiteStatus'
, method: 'post'
, data: {
name: website_name,
status: status
}
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:网站运行状态设置失败,接口返回' + result)
layer.msg('网站运行状态设置失败,请刷新重试!')
return false
}
layer.alert('网站 ' + website_name + ' 运行状态设置成功!')
}
})
})
})
</script>
+58 -7
View File
@@ -3,6 +3,8 @@ package routes
import (
"github.com/goravel/framework/contracts/route"
"github.com/goravel/framework/facades"
"panel/app/http/controllers/plugins/mysql57"
"panel/app/http/controllers/plugins/php80"
"panel/app/http/controllers/plugins/mysql80"
"panel/app/http/controllers/plugins/openresty"
@@ -23,7 +25,37 @@ func Plugin() {
route.Get("config", openRestyController.GetConfig)
route.Post("config", openRestyController.SaveConfig)
route.Get("errorLog", openRestyController.ErrorLog)
route.Get("clearErrorLog", openRestyController.ClearErrorLog)
route.Post("clearErrorLog", openRestyController.ClearErrorLog)
})
facades.Route().Prefix("api/plugins/mysql57").Middleware(middleware.Jwt()).Group(func(route route.Route) {
mysql57Controller := mysql57.NewMysql57Controller()
route.Get("status", mysql57Controller.Status)
route.Post("reload", mysql57Controller.Reload)
route.Post("start", mysql57Controller.Start)
route.Post("stop", mysql57Controller.Stop)
route.Post("restart", mysql57Controller.Restart)
route.Get("load", mysql57Controller.Load)
route.Get("config", mysql57Controller.GetConfig)
route.Post("config", mysql57Controller.SaveConfig)
route.Get("errorLog", mysql57Controller.ErrorLog)
route.Post("clearErrorLog", mysql57Controller.ClearErrorLog)
route.Get("slowLog", mysql57Controller.SlowLog)
route.Post("clearSlowLog", mysql57Controller.ClearSlowLog)
route.Get("rootPassword", mysql57Controller.GetRootPassword)
route.Post("rootPassword", mysql57Controller.SetRootPassword)
route.Get("database", mysql57Controller.DatabaseList)
route.Post("addDatabase", mysql57Controller.AddDatabase)
route.Post("deleteDatabase", mysql57Controller.DeleteDatabase)
route.Get("backup", mysql57Controller.BackupList)
route.Post("createBackup", mysql57Controller.CreateBackup)
route.Post("uploadBackup", mysql57Controller.UploadBackup)
route.Post("deleteBackup", mysql57Controller.DeleteBackup)
route.Post("restoreBackup", mysql57Controller.RestoreBackup)
route.Get("user", mysql57Controller.UserList)
route.Post("addUser", mysql57Controller.AddUser)
route.Post("deleteUser", mysql57Controller.DeleteUser)
route.Post("userPassword", mysql57Controller.SetUserPassword)
route.Post("userPrivileges", mysql57Controller.SetUserPrivileges)
})
facades.Route().Prefix("api/plugins/mysql80").Middleware(middleware.Jwt()).Group(func(route route.Route) {
mysql80Controller := mysql80.NewMysql80Controller()
@@ -36,9 +68,9 @@ func Plugin() {
route.Get("config", mysql80Controller.GetConfig)
route.Post("config", mysql80Controller.SaveConfig)
route.Get("errorLog", mysql80Controller.ErrorLog)
route.Get("clearErrorLog", mysql80Controller.ClearErrorLog)
route.Post("clearErrorLog", mysql80Controller.ClearErrorLog)
route.Get("slowLog", mysql80Controller.SlowLog)
route.Get("clearSlowLog", mysql80Controller.ClearSlowLog)
route.Post("clearSlowLog", mysql80Controller.ClearSlowLog)
route.Get("rootPassword", mysql80Controller.GetRootPassword)
route.Post("rootPassword", mysql80Controller.SetRootPassword)
route.Get("database", mysql80Controller.DatabaseList)
@@ -46,13 +78,14 @@ func Plugin() {
route.Post("deleteDatabase", mysql80Controller.DeleteDatabase)
route.Get("backup", mysql80Controller.BackupList)
route.Post("createBackup", mysql80Controller.CreateBackup)
route.Post("uploadBackup", mysql80Controller.UploadBackup)
route.Post("deleteBackup", mysql80Controller.DeleteBackup)
route.Post("restoreBackup", mysql80Controller.RestoreBackup)
route.Get("user", mysql80Controller.UserList)
route.Post("addUser", mysql80Controller.AddUser)
route.Post("deleteUser", mysql80Controller.DeleteUser)
route.Post("setUserPassword", mysql80Controller.SetUserPassword)
route.Post("setUserPrivileges", mysql80Controller.SetUserPrivileges)
route.Post("userPassword", mysql80Controller.SetUserPassword)
route.Post("userPrivileges", mysql80Controller.SetUserPrivileges)
})
facades.Route().Prefix("api/plugins/php74").Middleware(middleware.Jwt()).Group(func(route route.Route) {
php74Controller := php74.NewPhp74Controller()
@@ -66,10 +99,28 @@ func Plugin() {
route.Post("config", php74Controller.SaveConfig)
route.Get("errorLog", php74Controller.ErrorLog)
route.Get("slowLog", php74Controller.SlowLog)
route.Get("clearErrorLog", php74Controller.ClearErrorLog)
route.Get("clearSlowLog", php74Controller.ClearSlowLog)
route.Post("clearErrorLog", php74Controller.ClearErrorLog)
route.Post("clearSlowLog", php74Controller.ClearSlowLog)
route.Get("extensions", php74Controller.GetExtensionList)
route.Post("installExtension", php74Controller.InstallExtension)
route.Post("uninstallExtension", php74Controller.UninstallExtension)
})
facades.Route().Prefix("api/plugins/php80").Middleware(middleware.Jwt()).Group(func(route route.Route) {
php80Controller := php80.NewPhp80Controller()
route.Get("status", php80Controller.Status)
route.Post("reload", php80Controller.Reload)
route.Post("start", php80Controller.Start)
route.Post("stop", php80Controller.Stop)
route.Post("restart", php80Controller.Restart)
route.Get("load", php80Controller.Load)
route.Get("config", php80Controller.GetConfig)
route.Post("config", php80Controller.SaveConfig)
route.Get("errorLog", php80Controller.ErrorLog)
route.Get("slowLog", php80Controller.SlowLog)
route.Post("clearErrorLog", php80Controller.ClearErrorLog)
route.Post("clearSlowLog", php80Controller.ClearSlowLog)
route.Get("extensions", php80Controller.GetExtensionList)
route.Post("installExtension", php80Controller.InstallExtension)
route.Post("uninstallExtension", php80Controller.UninstallExtension)
})
}
+19 -2
View File
@@ -10,6 +10,7 @@ import (
)
func Web() {
facades.Route().StaticFile("favicon.ico", "/www/panel/public/favicon.ico")
facades.Route().Prefix("api/panel").Group(func(r route.Route) {
r.Prefix("info").Group(func(r route.Route) {
infoController := controllers.NewInfoController()
@@ -19,6 +20,8 @@ func Web() {
r.Middleware(middleware.Jwt()).Get("nowMonitor", infoController.NowMonitor)
r.Middleware(middleware.Jwt()).Get("systemInfo", infoController.SystemInfo)
r.Middleware(middleware.Jwt()).Get("installedDbAndPhp", infoController.InstalledDbAndPhp)
r.Middleware(middleware.Jwt()).Get("checkUpdate", infoController.CheckUpdate)
r.Middleware(middleware.Jwt()).Post("update", infoController.Update)
})
r.Prefix("user").Group(func(r route.Route) {
userController := controllers.NewUserController()
@@ -35,6 +38,18 @@ func Web() {
r.Prefix("website").Middleware(middleware.Jwt()).Group(func(r route.Route) {
websiteController := controllers.NewWebsiteController()
r.Get("list", websiteController.List)
r.Post("add", websiteController.Add)
r.Post("delete", websiteController.Delete)
r.Get("defaultConfig", websiteController.GetDefaultConfig)
r.Post("defaultConfig", websiteController.SaveDefaultConfig)
r.Get("config", websiteController.GetConfig)
r.Post("config", websiteController.SaveConfig)
r.Get("clearLog", websiteController.ClearLog)
r.Post("updateRemark", websiteController.UpdateRemark)
r.Get("backupList", websiteController.BackupList)
r.Post("createBackup", websiteController.CreateBackup)
r.Post("resetConfig", websiteController.ResetConfig)
r.Post("status", websiteController.Status)
})
r.Prefix("plugin").Middleware(middleware.Jwt()).Group(func(r route.Route) {
pluginController := controllers.NewPluginController()
@@ -47,6 +62,7 @@ func Web() {
r.Prefix("cron").Middleware(middleware.Jwt()).Group(func(r route.Route) {
cronController := controllers.NewCronController()
r.Get("list", cronController.List)
r.Get("script", cronController.Script)
r.Post("add", cronController.Add)
r.Post("update", cronController.Update)
r.Post("delete", cronController.Delete)
@@ -58,8 +74,8 @@ func Web() {
r.Get("firewallStatus", safeController.GetFirewallStatus)
r.Post("firewallStatus", safeController.SetFirewallStatus)
r.Get("firewallRules", safeController.GetFirewallRules)
r.Post("addFirewallRules", safeController.AddFirewallRule)
r.Post("deleteFirewallRules", safeController.DeleteFirewallRule)
r.Post("addFirewallRule", safeController.AddFirewallRule)
r.Post("deleteFirewallRule", safeController.DeleteFirewallRule)
r.Get("sshStatus", safeController.GetSshStatus)
r.Post("sshStatus", safeController.SetSshStatus)
r.Get("sshPort", safeController.GetSshPort)
@@ -73,6 +89,7 @@ func Web() {
r.Post("saveDays", monitorController.SaveDays)
r.Post("clear", monitorController.Clear)
r.Get("list", monitorController.List)
r.Get("switchAndDays", monitorController.SwitchAndDays)
})
r.Prefix("setting").Middleware(middleware.Jwt()).Group(func(r route.Route) {
settingController := controllers.NewSettingController()
+48 -31
View File
@@ -58,8 +58,7 @@ Prepare_system() {
exit 1
fi
wwwUserCheck=$(cat /etc/passwd | grep www)
if [ "${wwwUserCheck}" == "" ]; then
if ! id -u "www" > /dev/null 2>&1; then
groupadd www
useradd -s /sbin/nologin -g www www
fi
@@ -68,29 +67,29 @@ Prepare_system() {
ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
[ -s /etc/selinux/config ] && sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config
setenforce 0 >/dev/null 2>&1
setenforce 0 > /dev/null 2>&1
ulimit -n 204800
echo 6553560 >/proc/sys/fs/file-max
echo 6553560 > /proc/sys/fs/file-max
checkSoftNofile=$(cat /etc/security/limits.conf | grep '^* soft nofile .*$')
checkHardNofile=$(cat /etc/security/limits.conf | grep '^* hard nofile .*$')
checkSoftNproc=$(cat /etc/security/limits.conf | grep '^* soft nproc .*$')
checkHardNproc=$(cat /etc/security/limits.conf | grep '^* hard nproc .*$')
checkFsFileMax=$(cat /etc/sysctl.conf | grep '^fs.file-max.*$')
if [ "${checkSoftNofile}" == "" ]; then
echo "* soft nofile 204800" >>/etc/security/limits.conf
echo "* soft nofile 204800" >> /etc/security/limits.conf
fi
if [ "${checkHardNofile}" == "" ]; then
echo "* hard nofile 204800" >>/etc/security/limits.conf
echo "* hard nofile 204800" >> /etc/security/limits.conf
fi
if [ "${checkSoftNproc}" == "" ]; then
echo "* soft nproc 204800" >>/etc/security/limits.conf
echo "* soft nproc 204800" >> /etc/security/limits.conf
fi
if [ "${checkHardNproc}" == "" ]; then
echo "* hard nproc 204800 " >>/etc/security/limits.conf
echo "* hard nproc 204800 " >> /etc/security/limits.conf
fi
if [ "${checkFsFileMax}" == "" ]; then
echo fs.file-max = 6553560 >>/etc/sysctl.conf
echo fs.file-max = 6553560 >> /etc/sysctl.conf
fi
if [ "${OS}" == "centos" ]; then
@@ -121,19 +120,25 @@ Prepare_system() {
dnf config-manager --set-enabled crb
/usr/bin/crb enable
dnf makecache
dnf install -y curl wget zip unzip tar git jq git-core
dnf install -y curl wget zip unzip tar git jq git-core dos2unix
elif [ "${OS}" == "debian" ]; then
if [[ ${ipLocation} =~ "中国" ]]; then
sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list
sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list
fi
apt update
apt install -y curl wget zip unzip tar git jq git
apt install -y curl wget zip unzip tar git jq git dos2unix
else
echo -e $HR
echo "错误:该系统不支持安装耗子面板,请更换Debian12/RHEL9安装。"
exit 1
fi
if [ "$?" != "0" ]; then
echo -e $HR
echo "错误:安装面板依赖软件失败,请截图错误信息寻求帮助。"
exit 1
fi
}
Auto_Swap() {
@@ -153,14 +158,14 @@ Auto_Swap() {
chmod 600 $swapFile
mkswap -f $swapFile
swapon $swapFile
echo "$swapFile swap swap defaults 0 0" >>/etc/fstab
echo "$swapFile swap swap defaults 0 0" >> /etc/fstab
}
Init_Panel() {
mkdir ${setup_Path}/server
mkdir ${setup_Path}/server/cron
mkdir ${setup_Path}/server/cron/logs
chmod -R 644 ${setup_Path}/server/cron
chmod -R 644 ${setup_Path}/server/cron/logs
chmod -R 755 ${setup_Path}/server
mkdir ${setup_Path}/panel
rm -rf ${setup_Path}/panel/*
# 下载面板zip包并解压
@@ -173,6 +178,11 @@ Init_Panel() {
echo "错误:该系统架构不支持安装耗子面板,请更换x86_64/aarch64架构安装。"
exit 1
fi
if [ "$?" != "0" ] || [ "${panelZip}" == "" ]; then
echo -e $HR
echo "错误:获取面板下载链接失败,请截图错误信息寻求帮助。"
exit 1
fi
wget -O ${setup_Path}/panel/panel.zip "${download_Url}${panelZip}"
cd ${setup_Path}/panel
unzip -o panel.zip
@@ -180,6 +190,7 @@ Init_Panel() {
cp panel-example.conf panel.conf
${setup_Path}/panel/panel --env="panel.conf" artisan key:generate
${setup_Path}/panel/panel --env="panel.conf" artisan jwt:secret
${setup_Path}/panel/panel --env="panel.conf" artisan migrate
chmod -R 700 ${setup_Path}/panel
cp scripts/panel.sh /usr/bin/panel
chmod -R 700 /usr/bin/panel
@@ -188,24 +199,30 @@ Init_Panel() {
yum install firewalld -y
systemctl enable firewalld
systemctl start firewalld
firewall-cmd --set-default-zone=public >/dev/null 2>&1
firewall-cmd --permanent --zone=public --add-port=22/tcp >/dev/null 2>&1
firewall-cmd --permanent --zone=public --add-port=80/tcp >/dev/null 2>&1
firewall-cmd --permanent --zone=public --add-port=443/tcp >/dev/null 2>&1
firewall-cmd --permanent --zone=public --add-port=8888/tcp >/dev/null 2>&1
firewall-cmd --permanent --zone=public --add-port=${sshPort}/tcp >/dev/null 2>&1
firewall-cmd --set-default-zone=public > /dev/null 2>&1
firewall-cmd --permanent --zone=public --add-port=22/tcp > /dev/null 2>&1
firewall-cmd --permanent --zone=public --add-port=80/tcp > /dev/null 2>&1
firewall-cmd --permanent --zone=public --add-port=443/tcp > /dev/null 2>&1
firewall-cmd --permanent --zone=public --add-port=8888/tcp > /dev/null 2>&1
firewall-cmd --permanent --zone=public --add-port=${sshPort}/tcp > /dev/null 2>&1
firewall-cmd --reload
elif [ "${OS}" == "debian" ]; then
apt install ufw -y
sudo ufw enable
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8888/tcp
sudo ufw allow ${sshPort}/tcp
echo y | ufw enable
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 8888/tcp
ufw allow ${sshPort}/tcp
ufw reload
fi
if [ "$?" != "0" ]; then
echo -e $HR
echo "错误:防火墙放行失败,请截图错误信息寻求帮助。"
exit 1
fi
# 写入服务文件
cat >/etc/systemd/system/panel.service <<EOF
cat > /etc/systemd/system/panel.service << EOF
[Unit]
Description=HaoZi Panel
After=syslog.target network.target
@@ -214,7 +231,7 @@ Wants=network.target
[Service]
Type=simple
WorkingDirectory=${setup_Path}/panel/
ExecStart=./panel --env="panel.conf"
ExecStart=/www/panel/panel --env="/www/panel/panel.conf"
ExecReload=kill -s HUP \$MAINPID
ExecStop=kill -s QUIT \$MAINPID
User=root
@@ -239,21 +256,21 @@ clear
echo -e $LOGO
# 安装确认
read -p "面板将安装至${setup_Path}目录,请输入 y 并回车以开始安装:" install
read -p "面板将安装至 ${setup_Path} 目录,请输入 y 并回车以开始安装:" install
if [ "$install" != 'y' ]; then
echo "输入不正确,已退出安装。"
exit
fi
#代理设置
read -p "是否使用GitHub代理安装?(y/n)" proxy
read -p "是否使用GitHub代理安装(建议大陆机器使用)(y/n)" proxy
if [ "$proxy" == 'y' ]; then
download_Url="https://ghproxy.com/"
fi
clear
echo -e $LOGO
echo '安装面板依赖软件(如报错请检查 Dnf/Yum 源是否正常)'
echo '安装面板依赖软件(如报错请检查 APT/Yum 源是否正常)'
echo -e $HR
sleep 3s
Prepare_system
+20 -15
View File
@@ -26,10 +26,11 @@ setupPath="/www"
mysqlPath="${setupPath}/server/mysql"
mysqlVersion=""
mysqlPassword=$(cat /dev/urandom | head -n 16 | md5sum | head -c 16)
cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l)
if [[ "${1}" == "8.0" ]]; then
if [[ "${1}" == "80" ]]; then
mysqlVersion="8.0.33"
elif [[ "${1}" == "5.7" ]]; then
elif [[ "${1}" == "57" ]]; then
mysqlVersion="5.7.42"
else
echo -e $HR
@@ -37,7 +38,7 @@ else
exit 1
fi
if [[ "${memTotal}" -lt "4096" ]] && [[ "${1}" == "8.0" ]]; then
if [[ "${memTotal}" -lt "4096" ]] && [[ "${1}" == "80" ]]; then
echo -e $HR
echo "错误:这点内存(${memTotal}M)还想装 MySQL 8.0?洗洗睡吧!"
exit 1
@@ -102,7 +103,12 @@ cd src
mkdir build
cd build
cmake .. -DCMAKE_INSTALL_PREFIX=${mysqlPath} -DMYSQL_DATADIR=${mysqlPath}/data -DSYSCONFDIR=${mysqlPath}/conf -DWITH_MYISAM_STORAGE_ENGINE=1 -DWITH_INNOBASE_STORAGE_ENGINE=1 -DWITH_PARTITION_STORAGE_ENGINE=1 -DWITH_ARCHIVE_STORAGE_ENGINE=1 -DWITH_FEDERATED_STORAGE_ENGINE=1 -DWITH_BLACKHOLE_STORAGE_ENGINE=1 -DWITH_EXTRA_CHARSETS=all -DEXTRA_CHARSETS=all -DDEFAULT_CHARSET=utf8mb4 -DDEFAULT_COLLATION=utf8mb4_general_ci -DENABLED_LOCAL_INFILE=1 -DWITH_SYSTEMD=1 -DSYSTEMD_PID_DIR=${mysqlPath} -DWITH_SSL=/usr/local/openssl-1.1 -DWITH_BOOST=../boost
make -j$(nproc)
if [[ "${cpuCore}" -gt "1" ]]; then
make -j2
else
make
fi
# 安装
make install
@@ -175,14 +181,13 @@ interactive-timeout
EOF
# 根据CPU核心数确定写入线程数
cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l)
sed -i 's/innodb_write_io_threads = 4/innodb_write_io_threads = '${cpuCore}'/g' ${mysqlPath}/conf/my.cnf
sed -i 's/innodb_read_io_threads = 4/innodb_read_io_threads = '${cpuCore}'/g' ${mysqlPath}/conf/my.cnf
if [[ "${1}" == "8.0" ]]; then
if [[ "${1}" == "80" ]]; then
sed -i '/query_cache_size/d' ${mysqlPath}/conf/my.cnf
fi
if [[ "${1}" == "5.7" ]]; then
if [[ "${1}" == "57" ]]; then
sed -i '/innodb_redo_log_capacity/d' ${mysqlPath}/conf/my.cnf
fi
@@ -261,16 +266,13 @@ elif [[ ${memTotal} -ge 32768 ]]; then
sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 512M#" ${mysqlPath}/conf/my.cnf
fi
chmod 644 ${mysqlPath}/conf/my.cnf
chown -R mysql:mysql ${mysqlPath}
chmod -R 755 ${mysqlPath}
# 初始化
rm -rf ${mysqlPath}/src
rm -rf ${mysqlPath}/data
mkdir -p ${mysqlPath}/data
chown -R mysql:mysql ${mysqlPath}/data
chmod -R 755 ${mysqlPath}/data
chown -R mysql:mysql ${mysqlPath}
chmod -R 755 ${mysqlPath}
chmod 644 ${mysqlPath}/conf/my.cnf
${mysqlPath}/bin/mysqld --initialize-insecure --user=mysql --basedir=${mysqlPath} --datadir=${mysqlPath}/data
@@ -279,7 +281,7 @@ source /etc/profile
# 启动
cp ${mysqlPath}/lib/systemd/system/mysqld.service /etc/systemd/system/mysqld.service
sed -i "#ExecStartPre#d" /etc/systemd/system/mysqld.service
sed -i '/ExecStartPre/d' /etc/systemd/system/mysqld.service
systemctl daemon-reload
systemctl enable mysqld
@@ -287,4 +289,7 @@ systemctl start mysqld
${mysqlPath}/bin/mysqladmin -u root password ${mysqlPassword}
echo "mysql root password: ${mysqlPassword}"
panel writePlugin mysql${1} ${mysqlVersion}
panel writeMysqlPassword ${mysqlPassword}
echo -e "${HR}\nMySQL-${1} 安装完成\n${HR}"
+2 -2
View File
@@ -36,6 +36,6 @@ groupdel mysql
sed -i '#export PATH=/www/server/mysql#d' /etc/profile
source /etc/profile
panel deletePlugin mysql
panel deletePlugin mysql${1}
echo -e "${HR}\nMySQL uninstall completed.\n${HR}"
echo -e "${HR}\nMySQL-${1} 卸载完成\n${HR}"
+19 -18
View File
@@ -24,6 +24,7 @@ downloadUrl="https://dl.cdn.haozi.net/panel/openresty"
setupPath="/www"
openrestyPath="${setupPath}/server/openresty"
openrestyVersion="1.21.4.1"
cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l)
# 安装依赖
if [ "${OS}" == "centos" ]; then
@@ -38,10 +39,10 @@ if [ "${OS}" == "centos" ]; then
/usr/bin/crb enable
dnf makecache
dnf groupinstall "Development Tools" -y
dnf install tar unzip gd gd-devel git-core flex perl perl-CPAN oniguruma oniguruma-devel libsodium-devel libxml2-devel libxslt-devel GeoIP-devel bison yajl yajl-devel curl curl-devel libtermcap-devel ncurses-devel libevent-devel readline-devel libuuid-devel brotli-devel icu libicu libicu-devel openssl openssl-devel -y
dnf install tar unzip gd gd-devel git-core flex perl oniguruma oniguruma-devel libsodium-devel libxml2-devel libxslt-devel GeoIP-devel bison yajl yajl-devel curl curl-devel libtermcap-devel ncurses-devel libevent-devel readline-devel libuuid-devel brotli-devel icu libicu libicu-devel openssl openssl-devel -y
elif [ "${OS}" == "debian" ]; then
apt update
apt install build-essential tar unzip libgd3 libgd-dev git flex perl perl-modules libonig-dev libsodium-dev libxml2-dev libxslt1-dev libgeoip-dev bison libyajl-dev curl libcurl4-openssl-dev libncurses5-dev libevent-dev libreadline-dev uuid-dev libbrotli-dev icu-devtools libicu-dev openssl libssl-dev -y
apt install build-essential tar unzip libgd3 libgd-dev git flex perl libonig-dev libsodium-dev libxml2-dev libxslt1-dev libgeoip-dev bison libyajl-dev curl libcurl4-openssl-dev libncurses5-dev libevent-dev libreadline-dev uuid-dev libbrotli-dev icu-devtools libicu-dev openssl libssl-dev -y
else
echo -e $HR
echo "错误:耗子Linux面板不支持该系统"
@@ -93,14 +94,15 @@ rm -f nginx-dav-ext-module-3.0.0.tar.gz
mv nginx-dav-ext-module-3.0.0 nginx-dav-ext-module
# waf
wget -T 20 -O ngx_waf.zip ${downloadUrl}/modules/ngx_waf-6.1.9.zip
unzip -o ngx_waf.zip
mv ngx_waf-6.1.9 ngx_waf
rm -f ngx_waf.zip
wget -T 60 -O uthash.zip ${downloadUrl}/modules/uthash-2.3.0.zip
unzip -o uthash.zip
mv uthash-2.3.0 uthash
rm -f uthash.zip
cd ../
wget -T 20 -O ngx_waf.zip ${downloadUrl}/modules/ngx_waf-6.1.9.zip
unzip -o ngx_waf.zip
mv ngx_waf-6.1.9 ngx_waf
rm -f ngx_waf.zip
cd ngx_waf/inc
wget -T 60 -O libinjection.zip ${downloadUrl}/modules/libinjection-3.10.0.zip
unzip -o libinjection.zip
@@ -133,18 +135,17 @@ cd ${openrestyPath}/src
export LD_LIBRARY_PATH=/usr/local/lib/:$LD_LIBRARY_PATH
export LIB_UTHASH=${openrestyPath}/src/uthash
./configure --user=www --group=www --prefix=${openrestyPath} --with-luajit --add-module=${openrestyPath}/src/ngx_cache_purge --add-module=${openrestyPath}/src/nginx-sticky-module --with-openssl=${openrestyPath}/src/openssl --with-pcre=${openrestyPath}/src/pcre --with-http_v2_module --with-http_slice_module --with-threads --with-stream --with-stream_ssl_module --with-stream_realip_module --with-stream_geoip_module --with-stream_ssl_preread_module --with-http_stub_status_module --with-http_ssl_module --with-http_image_filter_module --with-http_gzip_static_module --with-http_gunzip_module --with-ipv6 --with-http_geoip_module --with-http_sub_module --with-http_flv_module --with-http_addition_module --with-http_realip_module --with-http_mp4_module --with-ld-opt="-Wl,-E" --with-cc-opt="-O2 -std=gnu99" --with-cpu-opt="amd64" --with-http_dav_module --add-module=${openrestyPath}/src/nginx-dav-ext-module --add-module=${openrestyPath}/src/ngx_brotli --add-module=${openrestyPath}/src/ngx_waf
make -j$(nproc)
./configure --user=www --group=www --prefix=${openrestyPath} --with-luajit --add-module=${openrestyPath}/src/ngx_cache_purge --add-module=${openrestyPath}/src/nginx-sticky-module --with-openssl=${openrestyPath}/src/openssl --with-pcre=${openrestyPath}/src/pcre --with-http_v2_module --with-http_slice_module --with-threads --with-stream --with-stream_ssl_module --with-stream_realip_module --with-stream_geoip_module --with-stream_ssl_preread_module --with-http_stub_status_module --with-http_ssl_module --with-http_image_filter_module --with-http_gzip_static_module --with-http_gunzip_module --with-ipv6 --with-http_geoip_module --with-http_sub_module --with-http_flv_module --with-http_addition_module --with-http_realip_module --with-http_mp4_module --with-ld-opt="-Wl,-E" --with-cc-opt="-O2 -std=gnu99" --with-cpu-opt="amd64" --with-http_dav_module --add-module=${openrestyPath}/src/nginx-dav-ext-module --add-module=${openrestyPath}/src/ngx_brotli --add-module=${openrestyPath}/ngx_waf
if [[ "${cpuCore}" -gt "1" ]]; then
make -j2
else
make
fi
if [ "$?" != "0" ]; then
echo -e $HR
echo "提示OpenResty多线程编译失败,尝试单线程编译..."
make
if [ "$?" != "0" ]; then
echo -e $HR
echo "错误:OpenResty编译失败,请截图错误信息寻求帮助。"
rm -rf ${openrestyPath}
exit 1
fi
echo "错误OpenResty编译失败,请截图错误信息寻求帮助。"
rm -rf ${openrestyPath}
exit 1
fi
make install
if [ ! -f "${openrestyPath}/nginx/sbin/nginx" ]; then
@@ -362,6 +363,6 @@ systemctl daemon-reload
systemctl enable openresty.service
systemctl start openresty.service
panel writePlugin openresty
panel writePlugin openresty ${openrestyVersion}
echo -e "${HR}\nOpenResty install completed.\n${HR}"
echo -e "${HR}\nOpenResty 安装完成\n${HR}"
+1 -1
View File
@@ -28,4 +28,4 @@ rm -rf /www/server/openresty
panel deletePlugin openresty
echo -e "${HR}\nOpenResty uninstall completed.\n${HR}"
echo -e "${HR}\nOpenResty 卸载完成\n${HR}"
+1 -1
View File
@@ -16,4 +16,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
'
/www/panel/panel --env="panel.conf" artisan panel "$@"
/www/panel/panel --env="/www/panel/panel.conf" artisan panel "$@"
+15 -8
View File
@@ -23,7 +23,9 @@ OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } ||
downloadUrl="https://dl.cdn.haozi.net/panel/php"
setupPath="/www"
phpVersion="${1}"
phpVersionCode=""
phpPath="${setupPath}/server/php/${phpVersion}"
cpuCore=$(cat /proc/cpuinfo | grep "processor" | wc -l)
# 安装依赖
if [ "${OS}" == "centos" ]; then
@@ -55,19 +57,20 @@ cd ${phpPath}
# 下载源码
if [ "${phpVersion}" == "74" ]; then
wget -O ${phpPath}/php-${phpVersion}.tar.gz ${downloadUrl}/php-7.4.33.tar.gz
phpVersionCode="7.4.33"
elif [ "${phpVersion}" == "80" ]; then
wget -O ${phpPath}/php-${phpVersion}.tar.gz ${downloadUrl}/php-8.0.29.tar.gz
phpVersionCode="8.0.29"
elif [ "${phpVersion}" == "81" ]; then
wget -O ${phpPath}/php-${phpVersion}.tar.gz ${downloadUrl}/php-8.1.21.tar.gz
phpVersionCode="8.1.21"
elif [ "${phpVersion}" == "82" ]; then
wget -O ${phpPath}/php-${phpVersion}.tar.gz ${downloadUrl}/php-8.2.8.tar.gz
phpVersionCode="8.2.8"
else
echo -e $HR
echo "错误:PHP-${phpVersion}不支持,请检查版本号是否正确。"
exit 1
fi
wget -O ${phpPath}/php-${phpVersion}.tar.gz ${downloadUrl}/php-${phpVersionCode}.tar.gz
if [ "$?" != "0" ]; then
echo -e $HR
echo "错误:PHP-${phpVersion}下载失败,请检查网络是否正常。"
@@ -101,7 +104,11 @@ cd src
./configure --prefix=${phpPath} --with-config-file-path=${phpPath}/etc --enable-fpm --with-fpm-user=www --with-fpm-group=www --enable-mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-freetype --with-jpeg --with-zlib --with-libxml-dir=/usr --enable-xml --disable-rpath --enable-bcmath --enable-shmop --enable-sysvsem --enable-inline-optimization --with-curl --enable-mbregex --enable-mbstring --enable-intl --enable-pcntl --enable-ftp --enable-gd --with-openssl --with-mhash --enable-pcntl --enable-sockets --with-xmlrpc --enable-soap --with-gettext --enable-fileinfo --enable-opcache --with-sodium --with-webp
# 编译安装
make -j${nproc}
if [[ "${cpuCore}" -gt "1" ]]; then
make -j2
else
make
fi
make install
if [ ! -f "${phpPath}/bin/php" ]; then
echo -e $HR
@@ -167,7 +174,7 @@ pm.min_spare_servers = 5
pm.max_spare_servers = 10
request_terminate_timeout = 100
request_slowlog_timeout = 30
pm.status_path = /phpfpm_${phpVersion}_status
pm.status_path = /phpfpm_status/${phpVersion}
slowlog = var/log/slow.log
EOF
@@ -242,6 +249,6 @@ systemctl daemon-reload
systemctl enable php-fpm-${phpVersion}.service
systemctl start php-fpm-${phpVersion}.service
panel writePlugin php${phpVersion}
panel writePlugin php${phpVersion} ${phpVersionCode}
echo -e "${HR}\nPHP-${phpVersion} 安装成功!\n${HR}"
echo -e "${HR}\nPHP-${phpVersion} 安装成\n${HR}"
+2 -2
View File
@@ -30,6 +30,6 @@ systemctl daemon-reload
rm -rf ${phpPath}
rm -f /usr/bin/php-${phpVersion}
panel deletePlugin openresty
panel deletePlugin php${phpVersion}
echo -e "${HR}\nPHP uninstall completed.\n${HR}"
echo -e "${HR}\nPHP-${phpVersion} 卸载完成\n${HR}"
@@ -18,6 +18,7 @@ limitations under the License.
'
HR="+----------------------------------------------------"
OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown")
downloadUrl="https://dl.cdn.haozi.net/panel/php_extensions"
action="$1"
@@ -34,7 +35,15 @@ Install() {
fi
# 安装依赖
dnf install ImageMagick ImageMagick-devel -y
if [ "${OS}" == "centos" ]; then
dnf install ImageMagick ImageMagick-devel -y
elif [ "${OS}" == "debian" ]; then
apt install imagemagick libmagickwand-dev
else
echo -e $HR
echo "错误:耗子Linux面板不支持该系统"
exit 1
fi
cd /www/server/php/${phpVersion}/src/ext
rm -rf imagick