Compare commits

...

61 Commits

Author SHA1 Message Date
耗子 d9b4162e63 feat: 更新版本号 2023-07-24 21:25:05 +08:00
耗子 a15ee96156 refactor: database backup and restore 2023-07-24 19:21:15 +08:00
耗子 4e71e8884a fix(monitor): net count error 2023-07-24 18:56:49 +08:00
耗子 346bb2d2b9 fix(monitor): net count error 2023-07-24 18:55:28 +08:00
耗子 0b476c2764 fix(monitor): net count error 2023-07-24 18:53:50 +08:00
耗子 bd5854cef3 feat: website backup and restore 2023-07-24 18:46:38 +08:00
耗子 5d7ae8d632 fix: file upload 2023-07-24 18:19:13 +08:00
耗子 adbca77687 feat: release v2.0.12 2023-07-23 04:28:44 +08:00
耗子 6377a2111c feat(tools): swap add to 4G 2023-07-23 04:27:50 +08:00
耗子 c9cc9f1fa7 feat(tools): jump /var 2023-07-23 04:04:04 +08:00
耗子 49e32beb8a fix: rhel9 build 2023-07-23 04:02:24 +08:00
耗子 f6f10aca26 feat: release v2.0.11 2023-07-22 18:02:11 +08:00
耗子 cb5d9acf3f feat: add phpmyadmin 2023-07-22 18:01:16 +08:00
耗子 903218dabb feat: auto create php conf for openresty 2023-07-22 17:39:41 +08:00
耗子 71f0e5d7cd feat: auto setting website path user 2023-07-22 17:26:05 +08:00
耗子 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
耗子 bce2f27500 feat(website controller): optimize ssl config 2023-07-20 01:10:27 +08:00
耗子 5ce65653ad refactor: packages to pkg 2023-07-20 01:06:49 +08:00
耗子 ec7138ae2a feat: website controller 2023-07-20 01:03:05 +08:00
耗子 46644c352d feat: optimize code 2023-07-19 01:23:31 +08:00
耗子 2a3119fe9a feat: cron controller 2023-07-19 00:24:41 +08:00
耗子 6119626f50 feat: monitor controller 2023-07-18 14:38:13 +08:00
耗子 c004acbfa0 feat: safe controller 2023-07-18 00:42:37 +08:00
耗子 da58071a0a feat: task controller 2023-07-17 23:18:02 +08:00
耗子 ab599ef0f7 feat: php74 2023-07-16 17:22:49 +08:00
耗子 03f531eff4 feat: fail2ban script 2023-07-15 04:25:39 +08:00
耗子 09d4d15f72 feat: update scripts 2023-07-15 04:09:46 +08:00
耗子 a4cc6570f4 fix: lint 2023-07-11 20:08:47 +08:00
耗子 9556aac923 fix: lint 2023-07-11 20:03:08 +08:00
耗子 8ef737253c fix: lint 2023-07-11 19:55:05 +08:00
耗子 047a743aed fix: lint 2023-07-11 19:52:24 +08:00
耗子 ab78122036 feat: mysql80 plugin 2023-07-11 19:52:08 +08:00
耗子 ce918533ab feat: mysql80 plugin 2023-07-11 19:49:27 +08:00
耗子 e5226c4bf3 fix: tests 2023-07-11 01:46:28 +08:00
耗子 98c4764b95 fix: tests 2023-07-11 01:40:27 +08:00
耗子 8b31415f5c fix: tests 2023-07-11 01:37:18 +08:00
耗子 b606431305 feat: system helpers and website service 2023-07-11 01:26:04 +08:00
耗子 e2c2717852 feat: update framework 2023-07-10 18:25:47 +08:00
耗子 4ff6561942 feat: update framework 2023-07-10 18:05:15 +08:00
耗子 f5529b732f feat: update framework 2023-07-10 17:47:15 +08:00
耗子 397273012c feat: update framework 2023-07-10 17:29:58 +08:00
117 changed files with 13318 additions and 1183 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: ✏️ Feature
if: github.event.label.name == '✏️ Feature'
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-tools@v3
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
@@ -30,7 +30,7 @@ jobs:
"Talk is cheap, Show me the Code." - Linus Torvalds
- name: ☢️ Bug
if: github.event.label.name == '☢️ Bug'
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-tools@v3
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: needs more info
uses: actions-cool/issues-helper@v3
uses: actions-cool/issues-tools@v3
with:
actions: 'close-issues'
labels: 'question'
+1
View File
@@ -23,6 +23,7 @@ _cgo_export.*
*.exe
*.o
*.so
panel
# 压缩包 #
# Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit
+3 -3
View File
@@ -32,18 +32,18 @@
```shell
TODO
bash <(curl -sSL https://dl.cdn.haozi.net/panel/install_panel.sh)
```
## 日常维护
```shell
TODO
panel xxx
```
## 问题反馈
使用类问题,可在 [WePublish社区论坛](https://wepublish.cn/forum) 提问寻求帮助。
使用类问题,可在 [WePublish社区论坛](https://wepublish.cn/forums) 提问寻求帮助。
对于面板自身问题,可在 GitHub 的`Issues`
页面提交问题反馈,注意[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。
+6 -7
View File
@@ -4,14 +4,15 @@ import (
"strconv"
"github.com/gookit/color"
"github.com/goravel/framework/contracts/console"
"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/packages/helpers"
"panel/app/services"
"panel/pkg/tools"
)
type Monitoring struct {
@@ -37,14 +38,12 @@ 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
}
info := helpers.GetMonitoringInfo()
info := tools.GetMonitoringInfo()
err := facades.Orm().Query().Create(&models.Monitor{
Info: info,
+33 -33
View File
@@ -2,18 +2,16 @@ package commands
import (
"os"
"regexp"
"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"
"panel/packages/helpers"
"panel/pkg/tools"
)
type Panel struct {
@@ -51,14 +49,14 @@ func (receiver *Panel) Handle(ctx console.Context) error {
return nil
}
settings := []models.Setting{{Key: "name", Value: "耗子Linux面板"}, {Key: "monitor", Value: "1"}, {Key: "monitor_days", Value: "30"}, {Key: "backup_path", Value: "/www/backup"}, {Key: "website_path", Value: "/www/wwwroot"}, {Key: "panel_entrance", 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("初始化失败")
return nil
}
hash, err := facades.Hash().Make(helpers.RandomString(32))
hash, err := facades.Hash().Make(tools.RandomString(32))
if err != nil {
color.Redln("初始化失败")
return nil
@@ -74,7 +72,19 @@ func (receiver *Panel) Handle(ctx console.Context) error {
color.Greenln("初始化成功")
case "update":
err := helpers.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
@@ -90,14 +100,14 @@ func (receiver *Panel) Handle(ctx console.Context) error {
return nil
}
password := helpers.RandomString(16)
password := tools.RandomString(16)
hash, err := facades.Hash().Make(password)
if err != nil {
color.Redln("生成密码失败")
return nil
}
user.Username = helpers.RandomString(8)
user.Username = tools.RandomString(8)
user.Password = hash
err = facades.Orm().Query().Save(&user)
@@ -106,29 +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}' | tr -d '\n'`)
color.Greenln("用户名: " + user.Username)
color.Greenln("密码: " + password)
// color.Greenln("面板端口: " + port)
color.Greenln("面板入口: " + services.NewSettingImpl().Get("panel_entrance", "/"))
color.Greenln("面板端口: " + port)
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
case "getPort":
nginxConf, err := os.ReadFile("/www/server/nginx/conf/nginx.conf")
if err != nil {
color.Redln("获取面板端口失败,请检查Nginx主配置文件")
return nil
}
match := regexp.MustCompile(`listen\s+(\d+)`).FindStringSubmatch(string(nginxConf))
if len(match) < 2 {
color.Redln("获取面板端口失败,请检查Nginx主配置文件")
return nil
}
port := match[1]
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("panel_entrance", "/"))
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
case "writePlugin":
slug := arg1
@@ -176,7 +176,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
var setting models.Setting
err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{
Key: "mysql_root_password",
Key: models.SettingKeyMysqlRootPassword,
}, models.Setting{
Value: password,
})
@@ -188,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":
@@ -290,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 {
+249
View File
@@ -0,0 +1,249 @@
package controllers
import (
"regexp"
"strconv"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type CronController struct {
cron services.Cron
}
func NewCronController() *CronController {
return &CronController{
cron: services.NewCronImpl(),
}
}
func (r *CronController) List(ctx http.Context) {
limit := ctx.Request().QueryInt("limit")
page := ctx.Request().QueryInt("page")
var crons []models.Cron
var total int64
err := facades.Orm().Query().Paginate(page, limit, &crons, &total)
if err != nil {
facades.Log().Error("[面板][CronController] 查询计划任务列表失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, http.Json{
"total": total,
"items": crons,
})
}
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",
"script": "required",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
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
}
// 写入shell
shellDir := "/www/server/cron/"
shellLogDir := "/www/server/cron/logs/"
if !tools.Exists(shellDir) {
facades.Log().Error("[面板][CronController] 计划任务目录不存在")
Error(ctx, http.StatusInternalServerError, "计划任务目录不存在")
return
}
if !tools.Exists(shellLogDir) {
facades.Log().Error("[面板][CronController] 计划任务日志目录不存在")
Error(ctx, http.StatusInternalServerError, "计划任务日志目录不存在")
return
}
shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + tools.RandomString(16)
if !tools.WriteFile(shellDir+shellFile+".sh", ctx.Request().Input("script"), 0700) {
facades.Log().Error("[面板][CronController] 创建计划任务脚本失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
tools.ExecShell("dos2unix " + shellDir + shellFile + ".sh")
var cron models.Cron
cron.Name = ctx.Request().Input("name")
cron.Type = "shell"
cron.Status = true
cron.Time = ctx.Request().Input("time")
cron.Shell = shellDir + shellFile + ".sh"
cron.Log = shellLogDir + shellFile + ".log"
err = facades.Orm().Query().Create(&cron)
if err != nil {
facades.Log().Error("[面板][CronController] 创建计划任务失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
r.cron.AddToSystem(cron)
Success(ctx, http.Json{
"id": cron.ID,
})
}
// 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{
"name": "required|min_len:1|max_len:255",
"time": "required",
"script": "required",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
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
}
var cron models.Cron
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
Error(ctx, http.StatusBadRequest, "计划任务不存在")
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, "系统内部错误")
return
}
tools.ExecShell("dos2unix " + cron.Shell)
r.cron.DeleteFromSystem(cron)
if cron.Status {
r.cron.AddToSystem(cron)
}
Success(ctx, nil)
}
func (r *CronController) Delete(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
}
r.cron.DeleteFromSystem(cron)
tools.RemoveFile(cron.Shell)
_, err = facades.Orm().Query().Delete(&cron)
if err != nil {
facades.Log().Error("[面板][CronController] 删除计划任务失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
func (r *CronController) Status(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"status": "bool",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
var cron models.Cron
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
Error(ctx, http.StatusBadRequest, "计划任务不存在")
return
}
cron.Status = ctx.Request().InputBool("status")
err = facades.Orm().Query().Save(&cron)
if err != nil {
facades.Log().Error("[面板][CronController] 更新计划任务状态失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
r.cron.DeleteFromSystem(cron)
if cron.Status {
r.cron.AddToSystem(cron)
}
Success(ctx, nil)
}
func (r *CronController) Log(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
}
if !tools.Exists(cron.Log) {
Error(ctx, http.StatusBadRequest, "日志文件不存在")
return
}
Success(ctx, tools.ReadFile(cron.Log))
}
+18
View File
@@ -0,0 +1,18 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
)
type FileController struct {
// Dependent services
}
func NewFileController() *FileController {
return &FileController{
// Inject services
}
}
func (r *FileController) Index(ctx http.Context) {
}
+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
}
+71 -13
View File
@@ -2,13 +2,14 @@ package controllers
import (
"fmt"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/models"
"panel/app/services"
"panel/packages/helpers"
"panel/pkg/tools"
)
type MenuItem struct {
@@ -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,12 +81,12 @@ func (r *InfoController) HomePlugins(ctx http.Context) {
Success(ctx, pluginsJson)
}
func (r *InfoController) NowMonitor(ctx http.Context) {
Success(ctx, helpers.GetMonitoringInfo())
func (c *InfoController) NowMonitor(ctx http.Context) {
Success(ctx, tools.GetMonitoringInfo())
}
func (r *InfoController) SystemInfo(ctx http.Context) {
monitorInfo := helpers.GetMonitoringInfo()
func (c *InfoController) SystemInfo(ctx http.Context) {
monitorInfo := tools.GetMonitoringInfo()
Success(ctx, http.Json{
"os_name": monitorInfo.Host.Platform + " " + monitorInfo.Host.PlatformVersion,
@@ -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)
}
+176
View File
@@ -0,0 +1,176 @@
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"
)
type MonitorController struct {
setting services.Setting
}
func NewMonitorController() *MonitorController {
return &MonitorController{
setting: services.NewSettingImpl(),
}
}
// Switch 监控开关
func (r *MonitorController) Switch(ctx http.Context) {
value := ctx.Request().InputBool("switch")
err := r.setting.Set(models.SettingKeyMonitor, cast.ToString(value))
if err != nil {
facades.Log().Error("[面板][MonitorController] 更新监控开关失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
// SaveDays 保存监控天数
func (r *MonitorController) SaveDays(ctx http.Context) {
days := ctx.Request().Input("days")
err := r.setting.Set(models.SettingKeyMonitorDays, days)
if err != nil {
facades.Log().Error("[面板][MonitorController] 更新监控天数失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
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().Where("1 = 1").Delete(&models.Monitor{})
if err != nil {
facades.Log().Error("[面板][MonitorController] 清空监控数据失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
// List 监控数据列表
func (r *MonitorController) List(ctx http.Context) {
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.ToDateTimeString()).Where("created_at <= ?", endTime.ToDateTimeString()).Get(&monitors)
if err != nil {
facades.Log().Error("[面板][MonitorController] 查询监控数据失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
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 {
if net.Name == "lo" {
continue
}
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 {
if net.Name == "lo" {
continue
}
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/1024)))
data.Net.Recv = append(data.Net.Recv, fmt.Sprintf("%.2f", float64(bytesRecv2/1024/1024)))
// 监控频率为 1 分钟,所以这里除以 60 即可得到每秒的流量
data.Net.Tx = append(data.Net.Tx, fmt.Sprintf("%.2f", float64(bytesSent2-bytesSent)/60/1024/1024))
data.Net.Rx = append(data.Net.Rx, fmt.Sprintf("%.2f", float64(bytesRecv2-bytesRecv)/60/1024/1024))
bytesSent = bytesSent2
bytesRecv = bytesRecv2
bytesSent2 = 0
bytesRecv2 = 0
}
Success(ctx, data)
}
+117 -76
View File
@@ -1,12 +1,10 @@
package controllers
import (
"github.com/goravel/framework/contracts/queue"
"github.com/goravel/framework/facades"
"panel/app/jobs"
"sync"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/models"
"panel/app/services"
@@ -14,11 +12,13 @@ import (
type PluginController struct {
plugin services.Plugin
task services.Task
}
func NewPluginController() *PluginController {
return &PluginController{
plugin: services.NewPluginImpl(),
task: services.NewTaskImpl(),
}
}
@@ -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())
@@ -116,42 +134,60 @@ func (r *PluginController) Install(ctx http.Context) {
return
}
processTask(task.ID)
r.task.Process(task.ID)
Success(ctx, "任务已提交")
}
// 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())
@@ -159,42 +195,60 @@ func (r *PluginController) Uninstall(ctx http.Context) {
return
}
processTask(task.ID)
r.task.Process(task.ID)
Success(ctx, "任务已提交")
}
// 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())
@@ -202,7 +256,7 @@ func (r *PluginController) Update(ctx http.Context) {
return
}
processTask(task.ID)
r.task.Process(task.ID)
Success(ctx, "任务已提交")
}
@@ -231,16 +285,3 @@ func (r *PluginController) UpdateShow(ctx http.Context) {
Success(ctx, "操作成功")
}
// processTask 处理任务
func processTask(taskID uint) {
go func() {
err := facades.Queue().Job(&jobs.ProcessTask{}, []queue.Arg{
{Type: "uint", Value: taskID},
}).Dispatch()
if err != nil {
facades.Log().Error("[面板][PluginController] 运行任务失败: " + err.Error())
return
}
}()
}
@@ -0,0 +1,743 @@
package mysql57
import (
"database/sql"
"fmt"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Mysql57Controller struct {
setting services.Setting
backup services.Backup
}
func NewMysql57Controller() *Mysql57Controller {
return &Mysql57Controller{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// 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) {
backupList, err := c.backup.MysqlList()
if err != nil {
facades.Log().Error("[MySQL57] 获取备份列表失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
return
}
controllers.Success(ctx, backupList)
}
// UploadBackup 上传备份
func (c *Mysql57Controller) UploadBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
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()
_, err = file.StoreAs(backupPath, name)
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
}
database := ctx.Request().Input("database")
err = c.backup.MysqlBackup(database)
if err != nil {
facades.Log().Error("[MYSQL57] 创建备份失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
return
}
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{
"name": "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"
fileName := ctx.Request().Input("name")
tools.RemoveFile(backupPath + "/" + fileName)
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{
"name": "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
}
err = c.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
if err != nil {
facades.Log().Error("[MYSQL57] 还原失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
return
}
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, "修改成功")
}
@@ -0,0 +1,743 @@
package mysql80
import (
"database/sql"
"fmt"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Mysql80Controller struct {
setting services.Setting
backup services.Backup
}
func NewMysql80Controller() *Mysql80Controller {
return &Mysql80Controller{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// Status 获取运行状态
func (c *Mysql80Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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 *Mysql80Controller) Reload(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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 *Mysql80Controller) Restart(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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 *Mysql80Controller) Start(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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 *Mysql80Controller) Stop(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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 *Mysql80Controller) GetConfig(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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 *Mysql80Controller) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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 *Mysql80Controller) Load(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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 *Mysql80Controller) ErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
log := tools.ExecShell("tail -n 100 /www/server/mysql/mysql-error.log")
controllers.Success(ctx, log)
}
// ClearErrorLog 清空错误日志
func (c *Mysql80Controller) ClearErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
tools.ExecShell("echo '' > /www/server/mysql/mysql-error.log")
controllers.Success(ctx, "清空错误日志成功")
}
// SlowLog 获取慢查询日志
func (c *Mysql80Controller) SlowLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
log := tools.ExecShell("tail -n 100 /www/server/mysql/mysql-slow.log")
controllers.Success(ctx, log)
}
// ClearSlowLog 清空慢查询日志
func (c *Mysql80Controller) ClearSlowLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
tools.ExecShell("echo '' > /www/server/mysql/mysql-slow.log")
controllers.Success(ctx, "清空慢查询日志成功")
}
// GetRootPassword 获取root密码
func (c *Mysql80Controller) GetRootPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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 *Mysql80Controller) SetRootPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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 *Mysql80Controller) DatabaseList(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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("[MySQL80] 连接数据库失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
return
}
defer db.Close()
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)
}
if err := rows.Err(); err != nil {
facades.Log().Error("[MySQL80] 获取数据库列表失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
return
}
controllers.Success(ctx, databases)
}
// AddDatabase 添加数据库
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",
})
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 *Mysql80Controller) DeleteDatabase(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_]+$|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 *Mysql80Controller) BackupList(ctx http.Context) {
backupList, err := c.backup.MysqlList()
if err != nil {
facades.Log().Error("[MySQL80] 获取备份列表失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
return
}
controllers.Success(ctx, backupList)
}
// UploadBackup 上传备份
func (c *Mysql80Controller) UploadBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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()
_, err = file.StoreAs(backupPath, name)
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
controllers.Success(ctx, "上传文件成功")
}
// CreateBackup 创建备份
func (c *Mysql80Controller) CreateBackup(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_]+$|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
}
database := ctx.Request().Input("database")
err = c.backup.MysqlBackup(database)
if err != nil {
facades.Log().Error("[MYSQL80] 创建备份失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
return
}
controllers.Success(ctx, "备份成功")
}
// DeleteBackup 删除备份
func (c *Mysql80Controller) DeleteBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"name": "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"
fileName := ctx.Request().Input("name")
tools.RemoveFile(backupPath + "/" + fileName)
controllers.Success(ctx, "删除备份成功")
}
// RestoreBackup 还原备份
func (c *Mysql80Controller) RestoreBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"name": "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
}
err = c.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
if err != nil {
facades.Log().Error("[MYSQL80] 还原失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
return
}
controllers.Success(ctx, "还原成功")
}
// UserList 用户列表
func (c *Mysql80Controller) UserList(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
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("[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
}
// 查询用户权限
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 *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",
})
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 *Mysql80Controller) DeleteUser(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_]+$",
})
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 *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",
})
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 *Mysql80Controller) SetUserPrivileges(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_]+$",
"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,21 +1,16 @@
package openresty
import (
"os"
"os/exec"
"regexp"
"strings"
"time"
"github.com/imroc/req/v3"
"github.com/spf13/cast"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/imroc/req/v3"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/http/controllers/plugins"
"panel/packages/helpers"
"panel/pkg/tools"
)
type OpenRestyController struct {
@@ -30,18 +25,11 @@ func NewOpenrestyController() *OpenRestyController {
// Status 获取运行状态
func (r *OpenRestyController) Status(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
cmd := exec.Command("bash", "-c", "systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
out, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 获取OpenResty状态失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
status := strings.TrimSpace(string(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
@@ -56,27 +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
}
cmd := exec.Command("bash", "-c", "systemctl reload openresty")
_, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 重载OpenResty失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "重载OpenResty失败")
return
}
cmd = exec.Command("bash", "-c", "systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
out, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 获取OpenResty状态失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
}
status := strings.TrimSpace(string(out))
tools.ExecShell("systemctl reload openresty")
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
@@ -85,33 +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
}
cmd := exec.Command("bash", "-c", "systemctl start openresty")
_, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 启动OpenResty失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "启动OpenResty失败")
return
}
cmd = exec.Command("bash", "-c", "systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
out, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 获取OpenResty状态失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
}
status := strings.TrimSpace(string(out))
tools.ExecShell("systemctl start openresty")
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
@@ -120,68 +78,38 @@ 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
}
cmd := exec.Command("bash", "-c", "systemctl stop openresty")
_, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 停止OpenResty失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "停止OpenResty失败")
return
}
cmd = exec.Command("bash", "-c", "systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
out, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 获取OpenResty状态失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
}
status := strings.TrimSpace(string(out))
tools.ExecShell("systemctl stop openresty")
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
}
if status == "active" {
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
}
cmd := exec.Command("bash", "-c", "systemctl restart openresty")
_, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 重启OpenResty失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "重启OpenResty失败")
return
}
cmd = exec.Command("bash", "-c", "systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
out, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 获取OpenResty状态失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
}
status := strings.TrimSpace(string(out))
tools.ExecShell("systemctl restart openresty")
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
@@ -190,28 +118,28 @@ 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
}
config, err := os.ReadFile("/www/server/openresty/conf/nginx.conf")
if err != nil {
config := tools.ReadFile("/www/server/openresty/conf/nginx.conf")
if len(config) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty配置失败")
return
}
controllers.Success(ctx, string(config))
controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *OpenRestyController) SaveConfig(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
@@ -221,86 +149,42 @@ func (r *OpenRestyController) SaveConfig(ctx http.Context) {
return
}
oldConfig, err := os.ReadFile("/www/server/openresty/conf/nginx.conf")
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty配置失败")
return
}
err = os.WriteFile("/www/server/openresty/conf/nginx.conf", []byte(config), 0644)
if err != nil {
facades.Log().Error("[OpenResty] 保存OpenResty配置失败: " + err.Error())
if !tools.WriteFile("/www/server/openresty/conf/nginx.conf", config, 0644) {
controllers.Error(ctx, http.StatusInternalServerError, "保存OpenResty配置失败")
return
}
cmd := exec.Command("bash", "-c", "openresty -t 2>&1")
out, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 检查OpenResty配置失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "检查OpenResty配置失败")
return
}
if !strings.Contains(string(out), "test is successful") {
err = os.WriteFile("/www/server/openresty/conf/nginx.conf", oldConfig, 0644)
if err != nil {
facades.Log().Error("[OpenResty] 保存OpenResty配置失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "保存OpenResty配置失败")
return
}
controllers.Error(ctx, http.StatusForbidden, "OpenResty配置有误,请修正后再保存: "+string(out))
}
cmd = exec.Command("bash", "-c", "systemctl reload openresty")
_, err = cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 重载OpenResty配置失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "重载OpenResty配置失败")
return
}
controllers.Success(ctx, "保存OpenResty配置成功")
}
// ErrorLog 获取错误日志
func (r *OpenRestyController) ErrorLog(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
if !controllers.Check(ctx, "openresty") {
return
}
cmd := exec.Command("bash", "-c", "tail -n 100 /www/wwwlogs/nginx_error.log")
out, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 获取OpenResty错误日志失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty错误日志失败")
if !tools.Exists("/www/wwwlogs/nginx_error.log") {
controllers.Success(ctx, "")
return
}
controllers.Success(ctx, string(out))
out := tools.ExecShell("tail -n 100 /www/wwwlogs/nginx_error.log")
controllers.Success(ctx, out)
}
// ClearErrorLog 清空错误日志
func (r *OpenRestyController) ClearErrorLog(ctx http.Context) {
if !plugins.Check(ctx, "openresty") {
return
}
cmd := exec.Command("bash", "-c", "echo '' > /www/wwwlogs/nginx_error.log")
_, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 清空OpenResty错误日志失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "清空OpenResty错误日志失败")
if !controllers.Check(ctx, "openresty") {
return
}
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
}
@@ -313,54 +197,63 @@ func (r *OpenRestyController) Load(ctx http.Context) {
}
raw := resp.String()
var data map[int]map[string]any
cmd := exec.Command("bash", "-c", "ps aux | grep nginx | grep 'worker process' | wc -l")
out, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 获取OpenResty工作进程失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty工作进程失败")
return
type nginxStatus struct {
Name string `json:"name"`
Value string `json:"value"`
}
workers := strings.TrimSpace(string(out))
data[0]["name"] = "工作进程"
data[0]["value"] = workers
var data []nginxStatus
cmd = exec.Command("bash", "-c", "ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
out, err = cmd.CombinedOutput()
if err != nil {
facades.Log().Error("[OpenResty] 获取OpenResty内存占用失败: " + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty内存占用失败")
return
}
mem := helpers.FormatBytes(cast.ToFloat64(strings.TrimSpace(string(out))))
data[1]["name"] = "内存占用"
data[1]["value"] = mem
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(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)
@@ -0,0 +1,372 @@
package php74
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 Php74Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp74Controller() *Php74Controller {
return &Php74Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "74",
}
}
func (c *Php74Controller) 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 *Php74Controller) 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 *Php74Controller) 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 *Php74Controller) 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 *Php74Controller) 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 *Php74Controller) 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 *Php74Controller) 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 *Php74Controller) 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 *Php74Controller) 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 *Php74Controller) 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 *Php74Controller) 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 *Php74Controller) 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 *Php74Controller) GetExtensionList(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
extensions := c.GetExtensions()
controllers.Success(ctx, extensions)
}
func (c *Php74Controller) 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 *Php74Controller) 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 *Php74Controller) 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
}
@@ -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
}
@@ -0,0 +1,81 @@
package phpmyadmin
import (
"os"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"panel/app/http/controllers"
"panel/pkg/tools"
)
type PhpMyAdminController struct {
}
func NewPhpMyAdminController() *PhpMyAdminController {
return &PhpMyAdminController{}
}
func (c *PhpMyAdminController) Info(ctx http.Context) {
if !controllers.Check(ctx, "phpmyadmin") {
return
}
files, err := os.ReadDir("/www/server/phpmyadmin")
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 目录")
return
}
var phpmyadmin string
for _, f := range files {
if strings.HasPrefix(f.Name(), "phpmyadmin_") {
phpmyadmin = f.Name()
}
}
if len(phpmyadmin) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 目录")
return
}
conf := tools.ReadFile("/www/server/vhost/phpmyadmin.conf")
match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf)
if len(match) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 端口")
return
}
controllers.Success(ctx, http.Json{
"phpmyadmin": phpmyadmin,
"port": match[1],
})
}
func (c *PhpMyAdminController) SetPort(ctx http.Context) {
if !controllers.Check(ctx, "phpmyadmin") {
return
}
port := ctx.Request().Input("port")
if len(port) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "端口不能为空")
return
}
conf := tools.ReadFile("/www/server/vhost/phpmyadmin.conf")
conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+port+";")
tools.WriteFile("/www/server/vhost/phpmyadmin.conf", conf, 0644)
if tools.IsRHEL() {
tools.ExecShell("firewall-cmd --zone=public --add-port=" + port + "/tcp --permanent")
tools.ExecShell("firewall-cmd --reload")
} else {
tools.ExecShell("ufw allow " + port + "/tcp")
tools.ExecShell("ufw reload")
}
tools.ExecShell("systemctl reload openresty")
controllers.Success(ctx, nil)
}
-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
}
+258
View File
@@ -0,0 +1,258 @@
package controllers
import (
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
"panel/pkg/tools"
)
type SafeController struct {
// Dependent services
}
func NewSafeController() *SafeController {
return &SafeController{
// Inject services
}
}
func (r *SafeController) GetFirewallStatus(ctx http.Context) {
Success(ctx, r.firewallStatus())
}
func (r *SafeController) SetFirewallStatus(ctx http.Context) {
var out string
if ctx.Request().InputBool("status") {
if tools.IsRHEL() {
out = tools.ExecShell("systemctl start firewalld")
} else {
out = tools.ExecShell("echo y | ufw enable")
}
} else {
if tools.IsRHEL() {
out = tools.ExecShell("systemctl stop firewalld")
} else {
out = tools.ExecShell("ufw disable")
}
}
Success(ctx, out)
}
func (r *SafeController) GetFirewallRules(ctx http.Context) {
if !r.firewallStatus() {
Success(ctx, nil)
return
}
if tools.IsRHEL() {
out := tools.ExecShell("firewall-cmd --list-all 2>&1")
match := regexp.MustCompile(`ports: (.*)`).FindStringSubmatch(out)
if len(match) == 0 {
Success(ctx, nil)
return
}
ports := strings.Split(match[1], " ")
var rules []map[string]string
for _, port := range ports {
rule := strings.Split(port, "/")
rules = append(rules, map[string]string{
"port": rule[0],
"protocol": rule[1],
})
}
Success(ctx, rules)
} else {
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") {
rule := strings.Split(port, "/")
rules = append(rules, map[string]string{
"port": rule[0],
"protocol": rule[1],
})
}
Success(ctx, rules)
}
}
func (r *SafeController) AddFirewallRule(ctx http.Context) {
if !r.firewallStatus() {
Error(ctx, http.StatusBadRequest, "防火墙未启动")
return
}
port := ctx.Request().InputInt("port", 0)
protocol := ctx.Request().Input("protocol", "")
if port == 0 || protocol == "" {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
if tools.IsRHEL() {
tools.ExecShell("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
tools.ExecShell("firewall-cmd --add-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
tools.ExecShell("firewall-cmd --reload")
} else {
tools.ExecShell("ufw delete allow " + cast.ToString(port) + "/" + protocol)
tools.ExecShell("ufw allow " + cast.ToString(port) + "/" + protocol)
tools.ExecShell("ufw reload")
}
Success(ctx, nil)
}
func (r *SafeController) DeleteFirewallRule(ctx http.Context) {
if !r.firewallStatus() {
Error(ctx, http.StatusBadRequest, "防火墙未启动")
return
}
port := ctx.Request().InputInt("port", 0)
protocol := ctx.Request().Input("protocol", "")
if port == 0 || protocol == "" {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
if tools.IsRHEL() {
tools.ExecShell("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
tools.ExecShell("firewall-cmd --reload")
} else {
tools.ExecShell("ufw delete allow " + cast.ToString(port) + "/" + protocol)
tools.ExecShell("ufw reload")
}
Success(ctx, nil)
}
func (r *SafeController) firewallStatus() bool {
var out string
var running bool
if tools.IsRHEL() {
out = tools.ExecShell("systemctl status firewalld | grep Active | awk '{print $3}'")
if out == "(running)" {
running = true
} else {
running = false
}
} else {
out = tools.ExecShell("ufw status | grep Status | awk '{print $2}'")
if out == "active" {
running = true
} else {
running = false
}
}
return running
}
func (r *SafeController) GetSshStatus(ctx http.Context) {
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
}
Success(ctx, running)
}
func (r *SafeController) SetSshStatus(ctx http.Context) {
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 {
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}'")
Success(ctx, out)
}
func (r *SafeController) SetSshPort(ctx http.Context) {
port := ctx.Request().InputInt("port", 0)
if port == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
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")
if status := tools.ExecShell("systemctl status sshd | grep Active | awk '{print $3}'"); status == "(running)" {
tools.ExecShell("systemctl restart sshd")
}
Success(ctx, nil)
}
func (r *SafeController) GetPingStatus(ctx http.Context) {
if tools.IsRHEL() {
out := tools.ExecShell("firewall-cmd --query-rich-rule='rule protocol value=icmp drop' 2>&1")
if out == "no" {
Success(ctx, true)
} else {
Success(ctx, false)
}
} else {
config := tools.ReadFile("/etc/ufw/before.rules")
if strings.Contains(config, "-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT") {
Success(ctx, true)
} else {
Success(ctx, false)
}
}
}
func (r *SafeController) SetPingStatus(ctx http.Context) {
if tools.IsRHEL() {
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().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")
}
Success(ctx, nil)
}
+141
View File
@@ -0,0 +1,141 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/pkg/tools"
"panel/app/models"
"panel/app/services"
)
type SettingController struct {
setting services.Setting
}
func NewSettingController() *SettingController {
return &SettingController{
setting: services.NewSettingImpl(),
}
}
func (r *SettingController) List(ctx http.Context) {
var settings []models.Setting
err := facades.Orm().Query().Get(&settings)
if err != nil {
facades.Log().Error("[面板][SettingController] 查询设置列表失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
var result = make(map[string]string)
for _, setting := range settings {
if setting.Key == models.SettingKeyMysqlRootPassword {
continue
}
result[setting.Key] = setting.Value
}
var user models.User
err = facades.Auth().User(ctx, &user)
if err != nil {
facades.Log().Error("[面板][SettingController] 获取用户失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
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)
}
func (r *SettingController) Save(ctx http.Context) {
name := ctx.Request().Input("name")
port := ctx.Request().Input("port")
backupPath := ctx.Request().Input("backup_path")
websitePath := ctx.Request().Input("website_path")
entrance := ctx.Request().Input("entrance")
username := ctx.Request().Input("username")
email := ctx.Request().Input("email")
password := ctx.Request().Input("password")
err := r.setting.Set(models.SettingKeyName, name)
if err != nil {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
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)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
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)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
err = r.setting.Set(models.SettingKeyEntrance, entrance)
if err != nil {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
var user models.User
err = facades.Auth().User(ctx, &user)
if err != nil {
facades.Log().Error("[面板][SettingController] 获取用户失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
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 {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
user.Password = hash
}
if err = facades.Orm().Query().Save(&user); err != nil {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
+50 -2
View File
@@ -4,15 +4,16 @@ import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/models"
"panel/pkg/tools"
)
type TaskController struct {
//Dependent services
// Dependent services
}
func NewTaskController() *TaskController {
return &TaskController{
//Inject services
// Inject services
}
}
@@ -30,3 +31,50 @@ func (r *TaskController) Status(ctx http.Context) {
"task": false,
})
}
func (r *TaskController) List(ctx http.Context) {
status := ctx.Request().Query("status")
if len(status) == 0 {
status = models.TaskStatusWaiting
}
var tasks []models.Task
var total int64
err := facades.Orm().Query().Where("status", status).Paginate(ctx.Request().QueryInt("page"), ctx.Request().QueryInt("limit"), &tasks, &total)
if err != nil {
facades.Log().Error("[面板][TaskController] 查询任务列表失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, http.Json{
"total": total,
"items": tasks,
})
}
func (r *TaskController) Log(ctx http.Context) {
var task models.Task
err := facades.Orm().Query().Where("id", ctx.Request().QueryInt("id")).FirstOrFail(&task)
if err != nil {
facades.Log().Error("[面板][TaskController] 查询任务失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
log := tools.ExecShell("tail -n 30 " + task.Log)
Success(ctx, log)
}
func (r *TaskController) Delete(ctx http.Context) {
var task models.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, "系统内部错误")
return
}
Success(ctx, nil)
}
+2 -2
View File
@@ -9,12 +9,12 @@ import (
)
type UserController struct {
//Dependent services
// Dependent services
}
func NewUserController() *UserController {
return &UserController{
//Inject services
// Inject services
}
}
+685 -7
View File
@@ -1,30 +1,41 @@
package controllers
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type WebsiteController struct {
//Dependent services
website services.Website
setting services.Setting
backup services.Backup
}
func NewWebsiteController() *WebsiteController {
return &WebsiteController{
//Inject services
website: services.NewWebsiteImpl(),
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
func (r *WebsiteController) List(ctx http.Context) {
// List 网站列表
func (c *WebsiteController) List(ctx http.Context) {
limit := ctx.Request().QueryInt("limit")
page := ctx.Request().QueryInt("page")
var websites []models.Website
var total int64
err := facades.Orm().Query().Paginate(page, limit, &websites, &total)
total, websites, err := c.website.List(page, limit)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 查询网站列表失败 ", err)
facades.Log().Error("[面板][WebsiteController] 获取网站列表失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
@@ -34,3 +45,670 @@ func (r *WebsiteController) List(ctx http.Context) {
"items": websites,
})
}
// Add 添加网站
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,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().One())
return
}
var website services.PanelWebsite
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 {
facades.Log().Error("[面板][WebsiteController] 添加网站失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, newSite)
}
// Delete 删除网站
func (c *WebsiteController) Delete(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
err := c.website.Delete(id)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 删除网站失败 ", err)
Error(ctx, http.StatusInternalServerError, "删除网站失败: "+err.Error())
return
}
Success(ctx, nil)
}
// GetDefaultConfig 获取默认配置
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")
Success(ctx, http.Json{
"index": index,
"stop": stop,
})
}
// SaveDefaultConfig 保存默认配置
func (c *WebsiteController) SaveDefaultConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
index := ctx.Request().Input("index")
stop := ctx.Request().Input("stop")
if !tools.WriteFile("/www/server/openresty/html/index.html", index, 0644) {
facades.Log().Error("[面板][WebsiteController] 保存默认配置失败")
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if !tools.WriteFile("/www/server/openresty/html/stop.html", stop, 0644) {
facades.Log().Error("[面板][WebsiteController] 保存默认配置失败")
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
// GetConfig 获取配置
func (c *WebsiteController) GetConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
config, err := c.website.GetConfig(id)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, config)
}
// SaveConfig 保存配置
func (c *WebsiteController) SaveConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"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().One())
return
}
var website models.Website
if facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&website) != nil {
Error(ctx, http.StatusBadRequest, "网站不存在")
return
}
if !website.Status {
Error(ctx, http.StatusBadRequest, "网站已停用,请先启用")
return
}
// 原文
raw := tools.ReadFile("/www/server/vhost/" + website.Name + ".conf")
if strings.TrimSpace(raw) != strings.TrimSpace(ctx.Request().Input("raw")) {
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", ctx.Request().Input("raw"), 0644)
Success(ctx, nil)
return
}
// 目录
path := ctx.Request().Input("path")
if !tools.Exists(path) {
Error(ctx, http.StatusBadRequest, "网站目录不存在")
return
}
website.Path = path
// 域名
domain := "server_name"
domains := strings.Split(ctx.Request().Input("domains"), "\n")
if len(domains) == 0 {
Error(ctx, http.StatusBadRequest, "域名不能为空")
return
}
for _, v := range domains {
if v == "" {
continue
}
domain += " " + v
}
domain += ";"
domainConfigOld := tools.Cut(raw, "# server_name标记位开始", "# server_name标记位结束")
if len(strings.TrimSpace(domainConfigOld)) == 0 {
Error(ctx, http.StatusBadRequest, "配置文件中缺少server_name标记位")
return
}
raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1)
// 端口
var port strings.Builder
ports := strings.Split(ctx.Request().Input("ports"), "\n")
if len(ports) == 0 {
Error(ctx, http.StatusBadRequest, "端口不能为空")
return
}
for i, v := range ports {
if _, err := strconv.Atoi(v); err != nil && v != "443 ssl http2" {
Error(ctx, http.StatusBadRequest, "端口格式错误")
return
}
if v == "443" && ctx.Request().InputBool("ssl") {
v = "443 ssl http2"
}
if i != len(ports)-1 {
port.WriteString(" listen " + v + ";\n")
} else {
port.WriteString(" listen " + v + ";")
}
}
portConfigOld := tools.Cut(raw, "# port标记位开始", "# port标记位结束")
if len(strings.TrimSpace(portConfigOld)) == 0 {
Error(ctx, http.StatusBadRequest, "配置文件中缺少port标记位")
return
}
raw = strings.Replace(raw, portConfigOld, "\n"+port.String()+"\n ", -1)
// 运行目录
root := tools.Cut(raw, "# root标记位开始", "# root标记位结束")
if len(strings.TrimSpace(root)) == 0 {
Error(ctx, http.StatusBadRequest, "配置文件中缺少root标记位")
return
}
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root)
if len(match) != 2 {
Error(ctx, http.StatusBadRequest, "配置文件中root标记位格式错误")
return
}
rootNew := strings.Replace(root, match[1], path, -1)
raw = strings.Replace(raw, root, rootNew, -1)
// 默认文件
index := tools.Cut(raw, "# index标记位开始", "# index标记位结束")
if len(strings.TrimSpace(index)) == 0 {
Error(ctx, http.StatusBadRequest, "配置文件中缺少index标记位")
return
}
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index)
if len(match) != 2 {
Error(ctx, http.StatusBadRequest, "配置文件中index标记位格式错误")
return
}
indexNew := strings.Replace(index, match[1], ctx.Request().Input("index"), -1)
raw = strings.Replace(raw, index, indexNew, -1)
// 防跨站
if !strings.HasSuffix(path, "/") {
path += "/"
}
if ctx.Request().InputBool("open_basedir") {
tools.WriteFile(path+".user.ini", "open_basedir="+path+":/tmp/", 0644)
} else {
if tools.Exists(path + ".user.ini") {
tools.RemoveFile(path + ".user.ini")
}
}
// WAF
waf := ctx.Request().Input("waf")
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标记位开始
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)
}
raw = strings.Replace(raw, "# waf标记位开始", wafConfig, -1)
// SSL
ssl := ctx.Request().InputBool("ssl")
website.Ssl = ssl
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标记位开始
ssl_certificate /www/server/vhost/ssl/` + website.Name + `.pem;
ssl_certificate_key /www/server/vhost/ssl/` + website.Name + `.key;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
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重定向标记位开始
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标记位开始
add_header Strict-Transport-Security "max-age=63072000" always;
# hsts标记位结束
`
}
sslConfigOld := tools.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
if len(strings.TrimSpace(sslConfigOld)) != 0 {
raw = strings.Replace(raw, sslConfigOld, "", -1)
}
raw = strings.Replace(raw, "# ssl标记位开始", sslConfig, -1)
} else {
sslConfigOld := tools.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
if len(strings.TrimSpace(sslConfigOld)) != 0 {
raw = strings.Replace(raw, sslConfigOld, "\n ", -1)
}
}
if website.Php != ctx.Request().InputInt("php") {
website.Php = ctx.Request().InputInt("php")
phpConfigOld := tools.Cut(raw, "# php标记位开始", "# php标记位结束")
phpConfig := `
include enable-php-` + strconv.Itoa(website.Php) + `.conf;
`
if len(strings.TrimSpace(phpConfigOld)) != 0 {
raw = strings.Replace(raw, phpConfigOld, phpConfig, -1)
}
}
err = facades.Orm().Query().Save(&website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 保存网站配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", raw, 0644)
tools.WriteFile("/www/server/vhost/rewrite/"+website.Name+".conf", ctx.Request().Input("rewrite"), 0644)
tools.ExecShell("systemctl reload openresty")
Success(ctx, nil)
}
// ClearLog 清空日志
func (c *WebsiteController) ClearLog(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
website := models.Website{}
err := facades.Orm().Query().Where("id", id).Get(&website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
tools.RemoveFile("/www/wwwlogs/" + website.Name + ".log")
Success(ctx, nil)
}
// UpdateRemark 更新备注
func (c *WebsiteController) UpdateRemark(ctx http.Context) {
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
website := models.Website{}
err := facades.Orm().Query().Where("id", id).Get(&website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
website.Remark = ctx.Request().Input("remark")
err = facades.Orm().Query().Save(&website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 保存网站备注失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
// BackupList 备份列表
func (c *WebsiteController) BackupList(ctx http.Context) {
backupList, err := c.backup.WebsiteList()
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站备份列表失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, backupList)
}
// CreateBackup 创建备份
func (c *WebsiteController) CreateBackup(ctx http.Context) {
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
website := models.Website{}
err := facades.Orm().Query().Where("id", id).Get(&website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
Error(ctx, http.StatusInternalServerError, "获取网站信息失败: "+err.Error())
return
}
err = c.backup.WebSiteBackup(website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 备份网站失败 ", err)
Error(ctx, http.StatusInternalServerError, "备份网站失败: "+err.Error())
return
}
Success(ctx, nil)
}
// UploadBackup 上传备份
func (c *WebsiteController) UploadBackup(ctx http.Context) {
file, err := ctx.Request().File("file")
if err != nil {
Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/website"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
Success(ctx, "上传文件成功")
}
// RestoreBackup 还原备份
func (c *WebsiteController) RestoreBackup(ctx http.Context) {
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
fileName := ctx.Request().Input("name")
if len(fileName) == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
website := models.Website{}
err := facades.Orm().Query().Where("id", id).Get(&website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
Error(ctx, http.StatusInternalServerError, "获取网站信息失败: "+err.Error())
return
}
err = c.backup.WebsiteRestore(website, fileName)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 还原网站失败 ", err)
Error(ctx, http.StatusInternalServerError, "还原网站失败: "+err.Error())
return
}
Success(ctx, nil)
}
// DeleteBackup 删除备份
func (c *WebsiteController) DeleteBackup(ctx http.Context) {
fileName := ctx.Request().Input("name")
if len(fileName) == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/website"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
if !tools.RemoveFile(backupPath + "/" + fileName) {
Error(ctx, http.StatusInternalServerError, "删除备份失败")
return
}
Success(ctx, nil)
}
// ResetConfig 重置配置
func (c *WebsiteController) ResetConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
website := models.Website{}
if err := facades.Orm().Query().Where("id", id).Get(&website); err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
website.Status = true
website.Ssl = false
if err := facades.Orm().Query().Save(&website); err != nil {
facades.Log().Error("[面板][WebsiteController] 保存网站配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
raw := fmt.Sprintf(`
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
server
{
# port标记位开始
listen 80;
# port标记位结束
# server_name标记位开始
server_name localhost;
# server_name标记位结束
# index标记位开始
index index.php index.html;
# index标记位结束
# root标记位开始
root %s;
# root标记位结束
# ssl标记位开始
# ssl标记位结束
# php标记位开始
include enable-php-%d.conf;
# php标记位结束
# waf标记位开始
waf on;
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;
# waf标记位结束
# 错误页配置,可自行设置
#error_page 404 /404.html;
#error_page 502 /502.html;
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
include /www/server/vhost/rewrite/%s.conf;
# 面板默认禁止访问部分敏感目录,可自行修改
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
{
return 404;
}
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
location ~ .*\.(js|css)$
{
expires 1h;
error_log /dev/null;
access_log /dev/null;
}
access_log /www/wwwlogs/%s.log;
error_log /www/wwwlogs/%s.log;
}
`, website.Path, website.Php, website.Name, website.Name, website.Name)
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", raw, 0644)
tools.WriteFile("/www/server/vhost/rewrite"+website.Name+".conf", "", 0644)
tools.ExecShell("systemctl reload openresty")
Success(ctx, nil)
}
// Status 网站状态
func (c *WebsiteController) Status(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
website := models.Website{}
if err := facades.Orm().Query().Where("id", id).Get(&website); err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
website.Status = ctx.Request().InputBool("status")
if err := facades.Orm().Query().Save(&website); err != nil {
facades.Log().Error("[面板][WebsiteController] 保存网站配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
raw := tools.ReadFile("/www/server/vhost/" + website.Name + ".conf")
// 运行目录
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]+";\n ")
} else {
raw = strings.ReplaceAll(raw, rootConfig, " root /www/server/openresty/html;\n # root "+match[1]+";\n ")
}
}
// 默认文件
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]+";\n ")
} else {
raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ")
}
}
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", raw, 0644)
tools.ExecShell("systemctl reload openresty")
Success(ctx, nil)
}
+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()
}
+6 -5
View File
@@ -2,12 +2,13 @@ package models
import (
"github.com/goravel/framework/support/carbon"
"panel/packages/helpers"
"panel/pkg/tools"
)
type Monitor struct {
ID uint `gorm:"primaryKey" json:"id"`
Info helpers.MonitoringInfo `gorm:"type:json;serializer:json" json:"info"`
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
ID uint `gorm:"primaryKey" json:"id"`
Info tools.MonitoringInfo `gorm:"type:json;serializer:json" json:"info"`
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
}
+10
View File
@@ -2,6 +2,16 @@ package models
import "github.com/goravel/framework/support/carbon"
const (
SettingKeyName = "name"
SettingKeyMonitor = "monitor"
SettingKeyMonitorDays = "monitor_days"
SettingKeyBackupPath = "backup_path"
SettingKeyWebsitePath = "website_path"
SettingKeyEntrance = "entrance"
SettingKeyMysqlRootPassword = "mysql_root_password"
)
type Setting struct {
ID uint `gorm:"primaryKey" json:"id"`
Key string `gorm:"unique;not null" json:"key"`
+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"
)
+14
View File
@@ -0,0 +1,14 @@
package php74
var (
Name = "PHP-7.4"
Author = "耗子"
Description = "PHP 是世界上最好的语言!"
Slug = "php74"
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`
)
+14
View File
@@ -0,0 +1,14 @@
package phpmyadmin
var (
Name = "phpMyAdmin"
Author = "耗子"
Description = "phpMyAdmin 是一个以 PHP 为基础,以 Web-Base 方式架构在网站主机上的 MySQL 数据库管理工具。"
Slug = "phpmyadmin"
Version = "5.2.1"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/phpmyadmin/install.sh`
Uninstall = `bash /www/panel/scripts/phpmyadmin/uninstall.sh`
Update = `bash /www/panel/scripts/phpmyadmin/uninstall.sh && bash /www/panel/scripts/phpmyadmin/install.sh`
)
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"github.com/goravel/framework/contracts/validation"
"panel/packages/captcha"
"panel/pkg/captcha"
)
type Captcha struct {
+209 -2
View File
@@ -1,7 +1,214 @@
// Package services 备份服务
package services
import (
"errors"
"os"
"path/filepath"
"strings"
"github.com/goravel/framework/support/carbon"
"panel/app/models"
"panel/pkg/tools"
)
type Backup interface {
BackupWebSite(string) error
BackupDatabase(string, string) error
WebsiteList() ([]BackupFile, error)
WebSiteBackup(website models.Website) error
WebsiteRestore(website models.Website, backupFile string) error
MysqlList() ([]BackupFile, error)
MysqlBackup(database string) error
MysqlRestore(database string, backupFile string) error
}
type BackupFile struct {
Name string `json:"name"`
Size string `json:"size"`
}
type BackupImpl struct {
setting Setting
}
func NewBackupImpl() *BackupImpl {
return &BackupImpl{
setting: NewSettingImpl(),
}
}
// WebsiteList 网站备份列表
func (s *BackupImpl) WebsiteList() ([]BackupFile, error) {
backupPath := s.setting.Get(models.SettingKeyBackupPath)
if len(backupPath) == 0 {
return []BackupFile{}, nil
}
backupPath += "/website"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
files, err := os.ReadDir(backupPath)
if err != nil {
return []BackupFile{}, err
}
var backupList []BackupFile
for _, file := range files {
info, err := file.Info()
if err != nil {
continue
}
backupList = append(backupList, BackupFile{
Name: file.Name(),
Size: tools.FormatBytes(float64(info.Size())),
})
}
return backupList, nil
}
// WebSiteBackup 网站备份
func (s *BackupImpl) WebSiteBackup(website models.Website) error {
backupPath := s.setting.Get(models.SettingKeyBackupPath)
if len(backupPath) == 0 {
return errors.New("未正确配置备份路径")
}
backupPath += "/website"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
backupFile := backupPath + "/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".zip"
tools.ExecShell(`cd '` + website.Path + `' && zip -r '` + backupFile + `' .`)
return nil
}
// WebsiteRestore 网站恢复
func (s *BackupImpl) WebsiteRestore(website models.Website, backupFile string) error {
backupPath := s.setting.Get(models.SettingKeyBackupPath)
if len(backupPath) == 0 {
return errors.New("未正确配置备份路径")
}
backupPath += "/website"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
backupFile = backupPath + "/" + backupFile
if !tools.Exists(backupFile) {
return errors.New("备份文件不存在")
}
tools.ExecShell(`rm -rf '` + website.Path + `/*'`)
tools.ExecShell(`unzip -o '` + backupFile + `' -d '` + website.Path + `' 2>&1`)
tools.Chmod(website.Path, 0755)
tools.Chown(website.Path, "www", "www")
return nil
}
// MysqlList MySQL备份列表
func (s *BackupImpl) MysqlList() ([]BackupFile, error) {
backupPath := s.setting.Get(models.SettingKeyBackupPath)
if len(backupPath) == 0 {
return []BackupFile{}, nil
}
backupPath += "/mysql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
files, err := os.ReadDir(backupPath)
if err != nil {
return []BackupFile{}, err
}
var backupList []BackupFile
for _, file := range files {
info, err := file.Info()
if err != nil {
continue
}
backupList = append(backupList, BackupFile{
Name: file.Name(),
Size: tools.FormatBytes(float64(info.Size())),
})
}
return backupList, nil
}
// MysqlBackup MySQL备份
func (s *BackupImpl) MysqlBackup(database string) error {
backupPath := s.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := s.setting.Get(models.SettingKeyMysqlRootPassword)
backupFile := database + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
err := os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
return err
}
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")
return nil
}
// MysqlRestore MySQL恢复
func (s *BackupImpl) MysqlRestore(database string, backupFile string) error {
backupPath := s.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := s.setting.Get(models.SettingKeyMysqlRootPassword)
ext := filepath.Ext(backupFile)
backupFile = backupPath + "/" + backupFile
if !tools.Exists(backupFile) {
return errors.New("备份文件不存在")
}
err := os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
return err
}
switch ext {
case ".zip":
tools.ExecShell("unzip -o " + backupFile + " -d " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".gz":
if strings.HasSuffix(backupFile, ".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) {
return errors.New("自动解压失败,请手动解压")
}
tools.ExecShell("mysql -uroot " + database + " < " + backupFile)
_ = os.Unsetenv("MYSQL_PWD")
return nil
}
+40
View File
@@ -0,0 +1,40 @@
package services
import (
"strings"
"panel/app/models"
"panel/pkg/tools"
)
type Cron interface {
AddToSystem(cron models.Cron)
DeleteFromSystem(cron models.Cron)
}
type CronImpl struct {
}
func NewCronImpl() *CronImpl {
return &CronImpl{}
}
// AddToSystem 添加到系统
func (r *CronImpl) AddToSystem(cron models.Cron) {
if tools.IsRHEL() {
tools.ExecShell("echo \"" + cron.Time + " " + cron.Shell + " >> " + cron.Log + " 2>&1\" >> /var/spool/cron/root")
} else {
tools.ExecShell("echo \"" + cron.Time + " " + cron.Shell + " >> " + cron.Log + " 2>&1\" >> /var/spool/cron/crontabs/root")
}
}
// 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")
}
}
+73
View File
@@ -5,7 +5,12 @@ 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"
"panel/app/plugins/phpmyadmin"
)
// PanelPlugin 插件元数据结构
@@ -17,11 +22,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 {
@@ -53,6 +63,69 @@ 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,
Author: mysql80.Author,
Description: mysql80.Description,
Slug: mysql80.Slug,
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,
Author: php74.Author,
Description: php74.Description,
Slug: php74.Slug,
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,
})
p = append(p, PanelPlugin{
Name: phpmyadmin.Name,
Author: phpmyadmin.Author,
Description: phpmyadmin.Description,
Slug: phpmyadmin.Slug,
Version: phpmyadmin.Version,
Requires: phpmyadmin.Requires,
Excludes: phpmyadmin.Excludes,
Install: phpmyadmin.Install,
Uninstall: phpmyadmin.Uninstall,
Update: phpmyadmin.Update,
})
return p
+31
View File
@@ -0,0 +1,31 @@
package services
import (
"github.com/goravel/framework/contracts/queue"
"github.com/goravel/framework/facades"
"panel/app/jobs"
)
type Task interface {
Process(taskID uint)
}
type TaskImpl struct {
}
func NewTaskImpl() *TaskImpl {
return &TaskImpl{}
}
func (r *TaskImpl) Process(taskID uint) {
go func() {
err := facades.Queue().Job(&jobs.ProcessTask{}, []queue.Arg{
{Type: "uint", Value: taskID},
}).Dispatch()
if err != nil {
facades.Log().Error("[面板][TaskService] 运行任务失败: " + err.Error())
return
}
}()
}
+313 -4
View File
@@ -1,18 +1,33 @@
// Package services 网站服务
package services
import "panel/app/models"
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/goravel/framework/facades"
"golang.org/x/exp/slices"
"panel/app/models"
"panel/pkg/tools"
)
type Website interface {
List() ([]models.Website, error)
List(page int, limit int) (int64, []models.Website, error)
Add(website PanelWebsite) (models.Website, error)
Delete(id int) error
GetConfig(id int) (WebsiteSetting, error)
}
type PanelWebsite struct {
Name string `json:"name"`
Status bool `json:"status"`
Domain string `json:"domain"`
Path string `json:"path"`
Php string `json:"php"`
Ssl string `json:"ssl"`
Php int `json:"php"`
Ssl bool `json:"ssl"`
Remark string `json:"remark"`
Db bool `json:"db"`
DbType string `json:"db_type"`
@@ -29,6 +44,7 @@ type WebsiteSetting struct {
Root string `json:"root"`
Path string `json:"path"`
Index string `json:"index"`
Php int `json:"php"`
OpenBasedir bool `json:"open_basedir"`
Ssl bool `json:"ssl"`
SslCertificate string `json:"ssl_certificate"`
@@ -43,3 +59,296 @@ type WebsiteSetting struct {
Raw string `json:"raw"`
Log string `json:"log"`
}
type WebsiteImpl struct {
setting Setting
}
func NewWebsiteImpl() *WebsiteImpl {
return &WebsiteImpl{
setting: NewSettingImpl(),
}
}
// List 列出网站
func (r *WebsiteImpl) List(page, limit int) (int64, []models.Website, error) {
var websites []models.Website
var total int64
if err := facades.Orm().Query().Paginate(page, limit, &websites, &total); err != nil {
return total, websites, err
}
return total, websites, nil
}
// Add 添加网站
func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) {
// 禁止部分保留名称
nameSlices := []string{"phpmyadmin", "mysql", "panel", "ssh"}
if slices.Contains(nameSlices, website.Name) {
return models.Website{}, errors.New("网站名称" + website.Name + "为保留名称,请更换")
}
// path为空时,设置默认值
if len(website.Path) == 0 {
website.Path = r.setting.Get(models.SettingKeyWebsitePath) + "/" + website.Name
}
// path不为/开头时,返回错误
if website.Path[0] != '/' {
return models.Website{}, errors.New("网站路径" + website.Path + "必须以/开头")
}
website.Ssl = false
website.Status = true
website.Domain = strings.TrimSpace(website.Domain)
w := models.Website{
Name: website.Name,
Status: website.Status,
Path: website.Path,
Php: website.Php,
Ssl: website.Ssl,
Remark: website.Remark,
}
if err := facades.Orm().Query().Create(&w); err != nil {
return w, err
}
tools.Mkdir(website.Path, 0755)
index := `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>耗子Linux面板</title>
</head>
<body>
<h1>耗子Linux面板</h1>
<p>这是耗子Linux面板的网站默认页面!</p>
<p>当您看到此页面,说明您的网站已创建成功。</p>
</body>
</html>
`
tools.WriteFile(website.Path+"/index.html", index, 0644)
domainArr := strings.Split(website.Domain, "\n")
portList := ""
portArr := make(map[string]bool)
domainList := ""
for key, value := range domainArr {
temp := strings.Split(value, ":")
domainList += " " + temp[0]
if len(temp) < 2 {
if _, ok := portArr["80"]; !ok {
if key == len(domainArr)-1 {
portList += " listen 80;"
} else {
portList += " listen 80;\n"
}
portArr["80"] = true
}
} else {
if _, ok := portArr[temp[1]]; !ok {
if key == len(domainArr)-1 {
portList += " listen " + temp[1] + ";"
} else {
portList += " listen " + temp[1] + ";\n"
}
portArr[temp[1]] = true
}
}
}
nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
server
{
# port标记位开始
%s
# port标记位结束
# server_name标记位开始
server_name%s;
# server_name标记位结束
# index标记位开始
index index.php index.html;
# index标记位结束
# root标记位开始
root %s;
# root标记位结束
# ssl标记位开始
# ssl标记位结束
# php标记位开始
include enable-php-%d.conf;
# php标记位结束
# waf标记位开始
waf on;
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;
# waf标记位结束
# 错误页配置,可自行设置
#error_page 404 /404.html;
#error_page 502 /502.html;
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
include /www/server/vhost/rewrite/%s.conf;
# 面板默认禁止访问部分敏感目录,可自行修改
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
{
return 404;
}
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
location ~ .*\.(js|css)$
{
expires 1h;
error_log /dev/null;
access_log /dev/null;
}
access_log /www/wwwlogs/%s.log;
error_log /www/wwwlogs/%s.log;
}
`, portList, domainList, website.Path, website.Php, website.Name, website.Name, website.Name)
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.Chmod(r.setting.Get(models.SettingKeyWebsitePath), 0755)
tools.Chmod(website.Path, 0755)
tools.Chown(r.setting.Get(models.SettingKeyWebsitePath), "www", "www")
tools.Chown(website.Path, "www", "www")
tools.ExecShell("systemctl reload openresty")
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
if website.Db && website.DbType == "mysql" {
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + website.DbName + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + website.DbUser + "'@'localhost' IDENTIFIED BY '" + website.DbPassword + "';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + website.DbName + ".* TO '" + website.DbUser + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
}
return w, nil
}
// Delete 删除网站
func (r *WebsiteImpl) Delete(id int) error {
var website models.Website
if err := facades.Orm().Query().Where("id", id).FirstOrFail(&website); err != nil {
return err
}
if _, err := facades.Orm().Query().Delete(&website); err != nil {
return err
}
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.ExecShell("systemctl reload openresty")
return nil
}
// GetConfig 获取网站配置
func (r *WebsiteImpl) GetConfig(id int) (WebsiteSetting, error) {
var website models.Website
if err := facades.Orm().Query().Where("id", id).First(&website); err != nil {
return WebsiteSetting{}, err
}
config := tools.ReadFile("/www/server/vhost/" + website.Name + ".conf")
var setting WebsiteSetting
setting.Name = website.Name
setting.Path = website.Path
setting.Ssl = website.Ssl
setting.Php = website.Php
setting.Raw = config
ports := tools.Cut(config, "# port标记位开始", "# port标记位结束")
matches := regexp.MustCompile(`listen\s+(.*);`).FindAllStringSubmatch(ports, -1)
for _, match := range matches {
if len(match) < 2 {
continue
}
setting.Ports = append(setting.Ports, match[1])
}
serverName := tools.Cut(config, "# server_name标记位开始", "# server_name标记位结束")
match := regexp.MustCompile(`server_name\s+(.*);`).FindStringSubmatch(serverName)
if len(match) > 1 {
setting.Domains = strings.Split(match[1], " ")
}
root := tools.Cut(config, "# root标记位开始", "# root标记位结束")
match = regexp.MustCompile(`root\s+(.*);`).FindStringSubmatch(root)
if len(match) > 1 {
setting.Root = match[1]
}
index := tools.Cut(config, "# index标记位开始", "# index标记位结束")
match = regexp.MustCompile(`index\s+(.*);`).FindStringSubmatch(index)
if len(match) > 1 {
setting.Index = match[1]
}
if tools.Exists(setting.Root + "/.user.ini") {
userIni := tools.ReadFile(setting.Path + "/.user.ini")
if strings.Contains(userIni, "open_basedir") {
setting.OpenBasedir = true
} else {
setting.OpenBasedir = false
}
} else {
setting.OpenBasedir = false
}
if setting.Ssl {
ssl := tools.Cut(config, "# ssl标记位开始", "# ssl标记位结束")
match = regexp.MustCompile(`ssl_certificate\s+(.*);`).FindStringSubmatch(ssl)
if len(match) > 1 {
setting.SslCertificate = tools.ReadFile(match[1])
}
match = regexp.MustCompile(`ssl_certificate_key\s+(.*);`).FindStringSubmatch(ssl)
if len(match) > 1 {
setting.SslCertificateKey = tools.ReadFile(match[1])
}
setting.HttpRedirect = strings.Contains(ssl, "# http重定向标记位")
setting.Hsts = strings.Contains(ssl, "# hsts标记位")
} else {
setting.SslCertificate = ""
setting.SslCertificateKey = ""
setting.HttpRedirect = false
setting.Hsts = false
}
waf := tools.Cut(config, "# waf标记位开始", "# waf标记位结束")
setting.Waf = strings.Contains(waf, "waf on;")
match = regexp.MustCompile(`waf_mode\s+(.+);`).FindStringSubmatch(waf)
if len(match) > 1 {
setting.WafMode = match[1]
}
match = regexp.MustCompile(`waf_cc_deny\s+(.+);`).FindStringSubmatch(waf)
if len(match) > 1 {
setting.WafCcDeny = match[1]
}
match = regexp.MustCompile(`waf_cache\s+(.+);`).FindStringSubmatch(waf)
if len(match) > 1 {
setting.WafCache = match[1]
}
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)
}
+3 -1
View File
@@ -9,6 +9,7 @@ import (
"github.com/goravel/framework/database"
"github.com/goravel/framework/event"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/filesystem"
"github.com/goravel/framework/hash"
"github.com/goravel/framework/http"
"github.com/goravel/framework/log"
@@ -32,7 +33,7 @@ func init() {
//
// This value is the name of your application. This value is used when the
// framework needs to place the application's name in a notification or
// any other location as required by the application or its packages.
// any other location as required by the application or its pkg.
"name": "Panel",
// Application Environment
@@ -77,6 +78,7 @@ func init() {
&auth.ServiceProvider{},
&hash.ServiceProvider{},
&crypt.ServiceProvider{},
&filesystem.ServiceProvider{},
&validation.ServiceProvider{},
&providers.AppServiceProvider{},
&providers.AuthServiceProvider{},
+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
},
+3 -3
View File
@@ -12,7 +12,7 @@ func init() {
// Here you may specify the default filesystem disk that should be used
// by the framework. The "local" disk, as well as a variety of cloud
// based disks are available to your application. Just store away!
"default": config.Env("FILESYSTEM_DISK", "local"),
"default": "local",
// Filesystem Disks
//
@@ -24,8 +24,8 @@ func init() {
"disks": map[string]any{
"local": map[string]any{
"driver": "local",
"root": "storage/app",
"url": "http://localhost/storage",
"root": "/",
"url": "http://localhost/",
},
},
})
+1 -1
View File
@@ -10,7 +10,7 @@ func init() {
// JWT Authentication Secret
//
// Don't forget to set this in your panel.conf file, as it will be used to sign
// your tokens. A helper command is provided for this:
// your tokens. A tools command is provided for this:
// `go run . artisan jwt:secret`
"secret": config.Env("JWT_SECRET", ""),
+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.14",
})
}
+4 -4
View File
@@ -5,14 +5,15 @@ 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.20230710043919-4b0ca1d027f8
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
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/spf13/cast v1.5.1
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
)
require (
@@ -52,7 +53,7 @@ require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/glebarez/sqlite v1.8.0 // indirect
github.com/glebarez/sqlite v1.9.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
@@ -161,7 +162,6 @@ require (
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.11.0 // indirect
+7 -6
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,8 @@ 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=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -343,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=
@@ -356,8 +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.20230710043919-4b0ca1d027f8 h1:77KSGfnxfSIrkCBZfv9xSMjui2w96Z2GO8MsZ2Gdd+s=
github.com/goravel/framework v1.12.1-0.20230710043919-4b0ca1d027f8/go.mod h1:SBsBTY8KTqSDipGPEZXnRRC1mWzKmL55Eo1P5aHNDgY=
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
+1 -1
View File
@@ -1,4 +1,4 @@
package helpers
package tools
import (
"os"
@@ -1,4 +1,4 @@
package helpers
package tools
import (
"testing"
@@ -1,4 +1,4 @@
package helpers
package tools
import (
"crypto/md5"
@@ -89,8 +89,18 @@ func FormatBytes(size float64) string {
}
// Cut 裁剪字符串
func Cut(begin, end, str string) string {
b := utf8.RuneCountInString(str[:strings.Index(str, begin)]) + utf8.RuneCountInString(begin)
e := utf8.RuneCountInString(str[:strings.Index(str, end)]) - b
return string([]rune(str)[b : b+e])
func Cut(str, begin, end string) string {
bIndex := strings.Index(str, begin)
eIndex := strings.Index(str, end)
if bIndex == -1 || eIndex == -1 || bIndex > eIndex {
return ""
}
b := utf8.RuneCountInString(str[:bIndex]) + utf8.RuneCountInString(begin)
e := utf8.RuneCountInString(str[:eIndex])
if b > e {
return ""
}
return string([]rune(str)[b:e])
}
@@ -1,4 +1,4 @@
package helpers
package tools
import (
"testing"
@@ -60,5 +60,5 @@ func (s *StringHelperTestSuite) TestFormatBytes() {
}
func (s *StringHelperTestSuite) TestCut() {
s.Equal("aoZ", Cut("H", "i", "HaoZi"))
s.Equal("aoZ", Cut("HaoZi", "H", "i"))
}
+116
View File
@@ -0,0 +1,116 @@
package tools
import (
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/goravel/framework/facades"
)
// WriteFile 写入文件
func WriteFile(path string, data string, permission os.FileMode) bool {
if err := os.MkdirAll(filepath.Dir(path), permission); err != nil {
facades.Log().Errorf("[面板][Helpers] 创建目录失败: %s", err.Error())
return false
}
err := os.WriteFile(path, []byte(data), permission)
if err != nil {
facades.Log().Errorf("[面板][Helpers] 写入文件 %s 失败: %s", path, err.Error())
return false
}
return true
}
// ReadFile 读取文件
func ReadFile(path string) string {
data, err := os.ReadFile(path)
if err != nil {
facades.Log().Errorf("[面板][Helpers] 读取文件 %s 失败: %s", path, err.Error())
return ""
}
return string(data)
}
// RemoveFile 删除文件
func RemoveFile(path string) bool {
if err := os.Remove(path); err != nil {
facades.Log().Errorf("[面板][Helpers] 删除文件 %s 失败: %s", path, err.Error())
return false
}
return true
}
// ExecShell 执行 shell 命令
func ExecShell(shell string) string {
cmd := exec.Command("bash", "-c", shell)
output, err := cmd.CombinedOutput()
if err != nil {
facades.Log().Errorf("[面板][Helpers] 执行命令 %s 失败: %s", shell, err.Error())
return ""
}
return strings.TrimSpace(string(output))
}
// ExecShellAsync 异步执行 shell 命令
func ExecShellAsync(shell string) {
cmd := exec.Command("bash", "-c", shell)
err := cmd.Start()
if err != nil {
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())
}
}()
}
// Mkdir 创建目录
func Mkdir(path string, permission os.FileMode) bool {
if err := os.MkdirAll(path, permission); err != nil {
facades.Log().Errorf("[面板][Helpers] 创建目录 %s 失败: %s", path, err.Error())
return false
}
return true
}
// Chmod 修改文件权限
func Chmod(path string, permission os.FileMode) bool {
if err := os.Chmod(path, permission); err != nil {
facades.Log().Errorf("[面板][Helpers] 修改文件 %s 权限失败: %s", path, err.Error())
return false
}
return true
}
// Chown 修改路径所有者
func Chown(path, user, group string) bool {
cmd := exec.Command("chown", "-R", user+":"+group, path)
err := cmd.Run()
if err != nil {
facades.Log().Errorf("[面板][Helpers] 修改路径 %s 所有者失败: %s", path, err.Error())
return false
}
return true
}
// Exists 判断路径是否存在
func Exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
+101
View File
@@ -0,0 +1,101 @@
package tools
import (
"os"
"os/user"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type SystemHelperTestSuite struct {
suite.Suite
}
func TestSystemHelperTestSuite(t *testing.T) {
suite.Run(t, &SystemHelperTestSuite{})
}
func (s *SystemHelperTestSuite) TestWriteFile() {
filePath := "/tmp/testfile"
defer os.Remove(filePath)
s.True(WriteFile(filePath, "test data", 0644))
s.FileExists(filePath)
content, _ := os.ReadFile(filePath)
s.Equal("test data", string(content))
}
func (s *SystemHelperTestSuite) TestReadFile() {
filePath := "/tmp/testfile"
defer os.Remove(filePath)
err := os.WriteFile(filePath, []byte("test data"), 0644)
s.Nil(err)
s.Equal("test data", ReadFile(filePath))
}
func (s *SystemHelperTestSuite) TestRemoveFile() {
filePath := "/tmp/testfile"
err := os.WriteFile(filePath, []byte("test data"), 0644)
s.Nil(err)
s.True(RemoveFile(filePath))
}
func (s *SystemHelperTestSuite) TestExecShell() {
s.Equal("test", ExecShell("echo 'test'"))
}
func (s *SystemHelperTestSuite) TestExecShellAsync() {
command := "echo 'test' > /tmp/testfile"
defer os.Remove("/tmp/testfile")
ExecShellAsync(command)
time.Sleep(time.Second)
content, _ := os.ReadFile("/tmp/testfile")
s.Equal("test\n", string(content))
}
func (s *SystemHelperTestSuite) TestMkdir() {
dirPath := "/tmp/testdir"
defer os.RemoveAll(dirPath)
s.True(Mkdir(dirPath, 0755))
}
func (s *SystemHelperTestSuite) TestChmod() {
filePath := "/tmp/testfile"
defer os.Remove(filePath)
err := os.WriteFile(filePath, []byte("test data"), 0644)
s.Nil(err)
s.True(Chmod(filePath, 0755))
}
func (s *SystemHelperTestSuite) TestChown() {
filePath := "/tmp/testfile"
defer os.Remove(filePath)
err := os.WriteFile(filePath, []byte("test data"), 0644)
s.Nil(err)
currentUser, err := user.Current()
s.Nil(err)
groups, err := currentUser.GroupIds()
s.Nil(err)
s.True(Chown(filePath, currentUser.Username, groups[0]))
}
func (s *SystemHelperTestSuite) TestExists() {
s.True(Exists("/tmp"))
s.False(Exists("/tmp/123"))
}
@@ -1,13 +1,15 @@
// Package helpers 存放辅助方法
package helpers
// Package tools 存放辅助方法
package tools
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"
@@ -45,7 +47,7 @@ func GetMonitoringInfo() MonitoringInfo {
res.DiskUsage = make(map[string]*disk.UsageStat)
for _, partition := range res.Disk {
if strings.HasPrefix(partition.Mountpoint, "/dev") || strings.HasPrefix(partition.Mountpoint, "/sys") || strings.HasPrefix(partition.Mountpoint, "/proc") || strings.HasPrefix(partition.Mountpoint, "/run") || strings.HasPrefix(partition.Mountpoint, "/boot") || strings.HasPrefix(partition.Mountpoint, "/usr") {
if strings.HasPrefix(partition.Mountpoint, "/dev") || strings.HasPrefix(partition.Mountpoint, "/sys") || strings.HasPrefix(partition.Mountpoint, "/proc") || strings.HasPrefix(partition.Mountpoint, "/run") || strings.HasPrefix(partition.Mountpoint, "/boot") || strings.HasPrefix(partition.Mountpoint, "/usr") || strings.HasPrefix(partition.Mountpoint, "/var") {
continue
}
usage, _ := disk.Usage(partition.Mountpoint)
@@ -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
}
@@ -1,4 +1,4 @@
package helpers
package tools
import (
"testing"
-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
+3 -1
View File
@@ -40,7 +40,9 @@ layui.define(['all'], function (exports) {
},
// 独立页面路由,可随意添加(无需写参数)
indPage: ['/login', // 登入页
indPage: [
'/login',
'/logout',
],
// 配置业务模块目录中的特殊模块
+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('当前已是最新版本!')
}
+53 -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,56 +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: '单位 Mb',
type: 'value',
axisLabel: {
formatter: '{value} Mb'
}
}]);
// 在窗口大小改变时,重置图表大小
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: 'name', 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.name + '</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.name + '删除成功!');
}
});
});
} else if (obj.event === 'restore') {
layer.confirm('高风险操作,确定要恢复数据库备份 <b style="color: red;">' + data.name + '</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.name + '恢复成功!');
}
});
});
}
});
});
};
</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: 'name', 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.name + '</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.name + '删除成功!');
}
});
});
} else if (obj.event === 'restore') {
layer.confirm('高风险操作,确定要恢复数据库备份 <b style="color: red;">' + data.name + '</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.name + '恢复成功!');
}
});
});
}
});
});
};
</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>
@@ -0,0 +1,92 @@
<!--
Name: phpMyAdmin 管理器
Author: 耗子
Date: 2023-07-22
-->
<title>phpMyAdmin</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">phpMyAdmin 信息</div>
<div class="layui-card-body">
<blockquote class="layui-elem-quote layui-quote-nm">访问地址:<span
id="phpmyadmin-info">获取中...</span></blockquote>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend>基本设置</legend>
</fieldset>
<div class="layui-form" lay-filter="phpmyadmin_setting">
<div class="layui-form-item">
<label class="layui-form-label" style="font-size: 13px;">访问端口</label>
<div class="layui-input-inline">
<input type="text" name="phpmyadmin_port" value="获取中ing..." class="layui-input"
disabled>
</div>
<div class="layui-form-mid layui-word-aux">查看 / 修改 phpMyAdmin 访问端口</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn layui-btn-sm" lay-submit
lay-filter="phpmyadmin_setting_submit">确认修改
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
layui.use(['index', 'code', 'table', 'form'], function () {
let $ = layui.$
, form = layui.form
, admin = layui.admin;
// 获取phpmyadmin信息
admin.req({
url: "/api/plugins/phpmyadmin/info"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
layer.alert('phpMyAdmin 信息获取失败,可能已损坏!', {
icon: 2
});
return false;
}
// 获取当前域名
let hostname = window.location.hostname;
// 拼接phpmyadmin访问地址
let phpmyadmin_url = 'http://' + hostname + ':' + result.data.port + '/' + result.data.phpmyadmin;
$('#phpmyadmin-info').html('<a href="' + phpmyadmin_url + '" target="_blank">' + phpmyadmin_url + '</a>');
$('input[name=phpmyadmin_port]').val(result.data.port);
$('input').attr('disabled', false);
form.render();
}
});
// 监听phpmyadmin设置提交
form.on('submit(phpmyadmin_setting_submit)', function (data) {
data.field.port = data.field.phpmyadmin_port;
index = layer.msg('请稍候...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/phpmyadmin/port"
, type: 'post'
, data: data.field
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
layer.alert('phpMyAdmin设置失败,可能已损坏!', {
icon: 2
});
return false;
}
layer.msg('设置成功', {
icon: 1
});
}
});
});
});
</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>
+7 -14
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,24 +121,19 @@ 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')
}
})
// 提交
form.on('submit(add-website-submit)', function (data) {
// 判断db_type是否为空
if (data.field.db_type === '') {
data.field.db = 0
} else {
data.field.db = 1
}
data.field.db = data.field.db_type !== '';
admin.req({
url: '/api/panel/website/add'
, method: 'post'
, type: 'post'
, data: data.field
, success: function (result) {
if (result.code !== 0) {
@@ -154,7 +148,6 @@ Date: 2023-06-24
, btn: ['确定']
, yes: function (index) {
layer.closeAll()
//location.reload();
}
})
}
+145
View File
@@ -0,0 +1,145 @@
<!--
Name: 网站 - 备份
Author: 耗子
Date: 2023-07-24
-->
<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 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: 'name', 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
, shade: 0.3
});
}
, done: function (res) {
layer.close(index);
layer.msg('上传成功!', {icon: 1});
table.reload('website-backup-list');
}
});
}
});
// 头工具栏事件
table.on('toolbar(website-backup-list)', function (obj) {
if (obj.event === 'backup_website') {
index = layer.msg('正在备份网站,请稍等...', {
icon: 16
, time: 0
, shade: 0.3
});
admin.req({
url: '/api/panel/website/createBackup'
, type: 'post'
, data: {
id: params.data.id
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
layer.alert('备份失败!');
return false;
}
table.reload('website-backup-list');
layer.msg('备份成功!', {icon: 1});
}
});
}
});
// 行工具事件
table.on('tool(website-backup-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/panel/website/deleteBackup"
, type: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
layer.msg('网站备份删除失败,请刷新重试!')
return false;
}
obj.del();
layer.alert('网站备份' + data.name + '删除成功!');
}
});
});
} else if (obj.event === 'restore') {
layer.confirm('高风险操作,确定要恢复网站备份 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
index = layer.msg('正在恢复网站备份,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
, shade: 0.3
});
data.id = params.data.id;
admin.req({
url: "/api/panel/website/restoreBackup"
, type: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
layer.msg('网站备份恢复失败,请刷新重试!')
return false;
}
layer.alert('网站备份' + data.name + '恢复成功!');
}
});
});
}
});
});
};
</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-24
-->
<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.data.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.data.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>
+197 -192
View File
@@ -1,7 +1,7 @@
<!--
Name: 网站 - 列表
Author: 耗子
Date: 2023-06-24
Date: 2023-07-74
-->
<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,199 @@ 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', {
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>
+107 -1
View File
@@ -4,7 +4,12 @@ import (
"github.com/goravel/framework/contracts/route"
"github.com/goravel/framework/facades"
"panel/app/http/controllers/plugins/mysql57"
"panel/app/http/controllers/plugins/mysql80"
"panel/app/http/controllers/plugins/openresty"
"panel/app/http/controllers/plugins/php74"
"panel/app/http/controllers/plugins/php80"
"panel/app/http/controllers/plugins/phpmyadmin"
"panel/app/http/middleware"
)
@@ -21,6 +26,107 @@ func Plugin() {
route.Get("config", openRestyController.GetConfig)
route.Post("config", openRestyController.SaveConfig)
route.Get("errorLog", openRestyController.ErrorLog)
route.Get("cleanErrorLog", 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()
route.Get("status", mysql80Controller.Status)
route.Post("reload", mysql80Controller.Reload)
route.Post("start", mysql80Controller.Start)
route.Post("stop", mysql80Controller.Stop)
route.Post("restart", mysql80Controller.Restart)
route.Get("load", mysql80Controller.Load)
route.Get("config", mysql80Controller.GetConfig)
route.Post("config", mysql80Controller.SaveConfig)
route.Get("errorLog", mysql80Controller.ErrorLog)
route.Post("clearErrorLog", mysql80Controller.ClearErrorLog)
route.Get("slowLog", mysql80Controller.SlowLog)
route.Post("clearSlowLog", mysql80Controller.ClearSlowLog)
route.Get("rootPassword", mysql80Controller.GetRootPassword)
route.Post("rootPassword", mysql80Controller.SetRootPassword)
route.Get("database", mysql80Controller.DatabaseList)
route.Post("addDatabase", mysql80Controller.AddDatabase)
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("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()
route.Get("status", php74Controller.Status)
route.Post("reload", php74Controller.Reload)
route.Post("start", php74Controller.Start)
route.Post("stop", php74Controller.Stop)
route.Post("restart", php74Controller.Restart)
route.Get("load", php74Controller.Load)
route.Get("config", php74Controller.GetConfig)
route.Post("config", php74Controller.SaveConfig)
route.Get("errorLog", php74Controller.ErrorLog)
route.Get("slowLog", php74Controller.SlowLog)
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)
})
facades.Route().Prefix("api/plugins/phpmyadmin").Middleware(middleware.Jwt()).Group(func(route route.Route) {
phpMyAdminController := phpmyadmin.NewPhpMyAdminController()
route.Get("info", phpMyAdminController.Info)
route.Post("port", phpMyAdminController.SetPort)
})
}
+58
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()
@@ -28,10 +31,28 @@ func Web() {
r.Prefix("task").Middleware(middleware.Jwt()).Group(func(r route.Route) {
taskController := controllers.NewTaskController()
r.Get("status", taskController.Status)
r.Get("list", taskController.List)
r.Get("log", taskController.Log)
r.Post("delete", taskController.Delete)
})
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("uploadBackup", websiteController.UploadBackup)
r.Post("restoreBackup", websiteController.RestoreBackup)
r.Post("deleteBackup", websiteController.DeleteBackup)
r.Post("resetConfig", websiteController.ResetConfig)
r.Post("status", websiteController.Status)
})
r.Prefix("plugin").Middleware(middleware.Jwt()).Group(func(r route.Route) {
pluginController := controllers.NewPluginController()
@@ -41,6 +62,43 @@ func Web() {
r.Post("update", pluginController.Update)
r.Post("updateShow", pluginController.UpdateShow)
})
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)
r.Post("status", cronController.Status)
r.Get("log", cronController.Log)
})
r.Prefix("safe").Middleware(middleware.Jwt()).Group(func(r route.Route) {
safeController := controllers.NewSafeController()
r.Get("firewallStatus", safeController.GetFirewallStatus)
r.Post("firewallStatus", safeController.SetFirewallStatus)
r.Get("firewallRules", safeController.GetFirewallRules)
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)
r.Post("sshPort", safeController.SetSshPort)
r.Get("pingStatus", safeController.GetPingStatus)
r.Post("pingStatus", safeController.SetPingStatus)
})
r.Prefix("monitor").Middleware(middleware.Jwt()).Group(func(r route.Route) {
monitorController := controllers.NewMonitorController()
r.Post("switch", monitorController.Switch)
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()
r.Get("list", settingController.List)
r.Post("save", settingController.Save)
})
})
facades.Route().Fallback(func(ctx http.Context) {
+96
View File
@@ -0,0 +1,96 @@
#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
: '
Copyright 2022 HaoZi Technology Co., Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
'
HR="+----------------------------------------------------"
OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown")
if [ "${OS}" == "centos" ]; then
dnf install -y fail2ban
elif [ "${OS}" == "debian" ]; then
apt install -y fail2ban
else
echo -e $HR
echo "错误:不支持的操作系统"
exit 1
fi
if [ "$?" != "0" ]; then
echo -e $HR
echo "错误:fail2ban安装失败,请截图错误信息寻求帮助。"
exit 1
fi
# 修改 fail2ban 配置文件
sed -i 's!# logtarget.*!logtarget = /var/log/fail2ban.log!' /etc/fail2ban/fail2ban.conf
sed -i 's!logtarget\s*=.*!logtarget = /var/log/fail2ban.log!' /etc/fail2ban/jail.conf
cat >/etc/fail2ban/jail.local <<EOF
[DEFAULT]
ignoreip = 127.0.0.1/8
bantime = 600
findtime = 300
maxretry = 5
banaction = firewallcmd-ipset
action = %(action_mwl)s
# ssh-START
[ssh]
enabled = true
filter = sshd
port = 22
maxretry = 5
findtime = 300
bantime = 86400
action = %(action_mwl)s
logpath = /var/log/secure
# ssh-END
# pure-ftpd-START
[pure-ftpd]
enabled = true
filter = pure-ftpd
port = 21
maxretry = 5
findtime = 300
bantime = 86400
action = %(action_mwl)s
logpath = /var/log/messages
# pure-ftpd-END
EOF
# 替换端口
sshPort=$(cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}')
if [ "${sshPort}" == "" ]; then
sshPort="22"
fi
sed -i "s/port = 22/port = ${sshPort}/g" /etc/fail2ban/jail.local
if [ -f "/etc/pure-ftpd/pure-ftpd.conf" ]; then
ftpPort=$(cat /etc/pure-ftpd/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}')
fi
if [ "${ftpPort}" == "" ]; then
ftpPort="21"
sed -i "s/port = 21/port = ${ftpPort}/g" /etc/fail2ban/jail.local
else
sed -i "s/port = 21/port = ${ftpPort}/g" /etc/fail2ban/jail.local
fi
# 启动 fail2ban
systemctl unmask fail2ban
systemctl daemon-reload
systemctl enable fail2ban
systemctl restart fail2ban
panel writePlugin fail2ban
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
: '
Copyright 2022 HaoZi Technology Co., Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
'
HR="+----------------------------------------------------"
OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown")
systemctl stop fail2ban
systemctl disable fail2ban
if [ "${OS}" == "centos" ]; then
dnf remove -y fail2ban
elif [ "${OS}" == "debian" ]; then
apt install -y fail2ban
else
echo -e $HR
echo "错误:不支持的操作系统"
exit 1
fi
rm -rf /etc/fail2ban
panel deletePlugin fail2ban
+284
View File
@@ -0,0 +1,284 @@
#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
: '
Copyright 2022 HaoZi Technology Co., Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
'
LOGO="+----------------------------------------------------\n| 耗子面板安装脚本\n+----------------------------------------------------\n| Copyright © 2022-"$(date +%Y)" 耗子科技 All rights reserved.\n+----------------------------------------------------"
HR="+----------------------------------------------------"
download_Url=""
setup_Path="/www"
sshPort=$(cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}')
ipLocation=$(curl -s https://ip.ping0.cc/geo)
Prepare_system() {
if [ $(whoami) != "root" ]; then
echo -e $HR
echo "错误:请使用root用户运行安装命令。"
exit 1
fi
ARCH=$(uname -m)
OS=$(source /etc/os-release && { [[ "$ID" == "debian" ]] && echo "debian"; } || { [[ "$ID" == "centos" ]] || [[ "$ID" == "rhel" ]] || [[ "$ID" == "rocky" ]] || [[ "$ID" == "almalinux" ]] && echo "centos"; } || echo "unknown")
if [ "${OS}" == "unknown" ]; then
echo -e $HR
echo "错误:该系统不支持安装耗子面板,请更换Debian12/RHEL9安装。"
exit 1
fi
if [ "${ARCH}" != "x86_64" ] && [ "${ARCH}" != "aarch64" ]; then
echo -e $HR
echo "错误:该系统架构不支持安装耗子面板,请更换x86_64/aarch64架构安装。"
exit 1
fi
is64bit=$(getconf LONG_BIT)
if [ "${is64bit}" != '64' ]; then
echo -e $HR
echo "错误:32位系统不支持安装耗子面板,请更换64位系统安装。"
exit 1
fi
isInstalled=$(systemctl status panel 2>&1 | grep "Active")
if [ "${isInstalled}" != "" ]; then
echo -e $HR
echo "错误:耗子面板已安装,请勿重复安装。"
exit 1
fi
if ! id -u "www" > /dev/null 2>&1; then
groupadd www
useradd -s /sbin/nologin -g www www
fi
rm -rf /etc/localtime
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
ulimit -n 204800
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
fi
if [ "${checkHardNofile}" == "" ]; then
echo "* hard nofile 204800" >> /etc/security/limits.conf
fi
if [ "${checkSoftNproc}" == "" ]; then
echo "* soft nproc 204800" >> /etc/security/limits.conf
fi
if [ "${checkHardNproc}" == "" ]; then
echo "* hard nproc 204800 " >> /etc/security/limits.conf
fi
if [ "${checkFsFileMax}" == "" ]; then
echo fs.file-max = 6553560 >> /etc/sysctl.conf
fi
if [ "${OS}" == "centos" ]; then
if [[ ${ipLocation} =~ "中国" ]]; then
sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^#baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.aliyun.com/rockylinux|g' \
-i.bak \
/etc/yum.repos.d/[Rr]ocky*.repo
sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^# baseurl=https://repo.almalinux.org|baseurl=https://mirrors.aliyun.com|g' \
-i.bak \
/etc/yum.repos.d/[Aa]lmalinux*.repo
dnf makecache -y
fi
dnf install dnf-plugins-core -y
dnf install epel-release -y
dnf config-manager --set-enabled epel
if [[ ${ipLocation} =~ "中国" ]]; then
sed -i 's|^#baseurl=https://download.example/pub|baseurl=https://mirrors.aliyun.com|' /etc/yum.repos.d/epel*
sed -i 's|^metalink|#metalink|' /etc/yum.repos.d/epel*
dnf makecache -y
fi
dnf config-manager --set-enabled PowerTools
dnf config-manager --set-enabled powertools
dnf config-manager --set-enabled CRB
dnf config-manager --set-enabled Crb
dnf config-manager --set-enabled crb
/usr/bin/crb enable
dnf makecache -y
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 -y
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() {
# 判断是否有swap
swap=$(LC_ALL=C free | grep Swap | awk '{print $2}')
if [ "${swap}" -gt 1 ]; then
return
fi
if [ ! -d ${setup_Path} ]; then
mkdir ${setup_Path}
fi
# 设置swap
swapFile="${setup_Path}/swap"
dd if=/dev/zero of=$swapFile bs=1M count=4096
chmod 600 $swapFile
mkswap -f $swapFile
swapon $swapFile
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 755 ${setup_Path}/server
mkdir ${setup_Path}/panel
rm -rf ${setup_Path}/panel/*
# 下载面板zip包并解压
if [ "${ARCH}" == "x86_64" ]; then
panelZip=$(curl "https://api.github.com/repos/HaoZi-Team/Panel/releases/latest" | jq -r '.assets[] | select(.name | contains("amd64v3")) | .browser_download_url')
elif [ "${ARCH}" == "aarch64" ]; then
panelZip=$(curl "https://api.github.com/repos/HaoZi-Team/Panel/releases/latest" | jq -r '.assets[] | select(.name | contains("arm64")) | .browser_download_url')
else
echo -e $HR
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
rm -rf panel.zip
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
# 防火墙放行
if [ "${OS}" == "centos" ]; then
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 --reload
elif [ "${OS}" == "debian" ]; then
apt install ufw -y
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
[Unit]
Description=HaoZi Panel
After=syslog.target network.target
Wants=network.target
[Service]
Type=simple
WorkingDirectory=${setup_Path}/panel/
ExecStart=/www/panel/panel --env="/www/panel/panel.conf"
ExecReload=kill -s HUP \$MAINPID
ExecStop=kill -s QUIT \$MAINPID
User=root
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable panel.service
systemctl start panel.service
clear
echo -e $LOGO
echo '面板安装成功!'
echo -e $HR
panel init
panel getInfo
}
clear
echo -e $LOGO
# 安装确认
read -p "面板将安装至 ${setup_Path} 目录,请输入 y 并回车以开始安装:" install
if [ "$install" != 'y' ]; then
echo "输入不正确,已退出安装。"
exit
fi
#代理设置
read -p "是否使用GitHub代理安装(建议大陆机器使用)(y/n)" proxy
if [ "$proxy" == 'y' ]; then
download_Url="https://ghproxy.com/"
fi
clear
echo -e $LOGO
echo '安装面板依赖软件(如报错请检查 APT/Yum 源是否正常)'
echo -e $HR
sleep 3s
Prepare_system
Auto_Swap
clear
echo -e $LOGO
echo '安装面板运行环境(视网络情况可能需要较长时间)'
echo -e $HR
sleep 3s
Init_Panel
+304
View File
@@ -0,0 +1,304 @@
#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
: '
Copyright 2022 HaoZi Technology Co., Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
'
HR="+----------------------------------------------------"
ARCH=$(uname -m)
memTotal=$(LC_ALL=C free -m | grep Mem | awk '{print $2}')
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/mysql"
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}" == "80" ]]; then
mysqlVersion="8.0.33"
elif [[ "${1}" == "57" ]]; then
mysqlVersion="5.7.42"
else
echo -e $HR
echo "错误:不支持的 MySQL 版本!"
exit 1
fi
if [[ "${memTotal}" -lt "4096" ]] && [[ "${1}" == "80" ]]; then
echo -e $HR
echo "错误:这点内存(${memTotal}M)还想装 MySQL 8.0?洗洗睡吧!"
exit 1
fi
# 安装依赖
if [ "${OS}" == "centos" ]; then
dnf makecache -y
dnf groupinstall "Development Tools" -y
dnf install cmake bison ncurses-devel libtirpc-devel openssl-devel pkg-config openldap-devel libudev-devel cyrus-sasl-devel patchelf rpcgen rpcsvc-proto-devel -y
elif [ "${OS}" == "debian" ]; then
apt update
apt install build-essential cmake bison libncurses5-dev libtirpc-dev libssl-dev pkg-config libldap2-dev libudev-dev libsasl2-dev patchelf -y
else
echo -e $HR
echo "错误:耗子Linux面板不支持该系统"
exit 1
fi
mysqlUserCheck=$(cat /etc/passwd | grep mysql)
if [ "${mysqlUserCheck}" == "" ]; then
groupadd mysql
useradd -s /sbin/nologin -g mysql mysql
fi
# 准备目录
rm -rf ${mysqlPath}
mkdir -p ${mysqlPath}
cd ${mysqlPath}
# 下载源码
wget -T 120 -O ${mysqlPath}/mysql-${mysqlVersion}.tar.gz ${downloadUrl}/mysql-boost-${mysqlVersion}.tar.gz
tar -zxvf mysql-${mysqlVersion}.tar.gz
rm -f mysql-${mysqlVersion}.tar.gz
mv mysql-${mysqlVersion} src
# openssl
wget -T 120 -O ${mysqlPath}/openssl-1.1.1u.tar.gz ${downloadUrl}/openssl/openssl-1.1.1u.tar.gz
tar -zxvf openssl-1.1.1u.tar.gz
rm -f openssl-1.1.1u.tar.gz
mv openssl-1.1.1u openssl
cd openssl
./config --prefix=/usr/local/openssl-1.1 --openssldir=/usr/local/openssl-1.1
make -j$(nproc)
make install
echo "/usr/local/openssl-1.1/lib" > /etc/ld.so.conf.d/openssl-1.1.conf
ldconfig
cd ..
rm -rf openssl
# 编译
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
if [ "$?" != "0" ]; then
echo -e $HR
echo "错误:MySQL 编译初始化失败,请截图错误信息寻求帮助。"
rm -rf ${mysqlPath}
exit 1
fi
if [[ "${cpuCore}" -gt "1" ]]; then
make -j2
else
make
fi
if [ "$?" != "0" ]; then
echo -e $HR
echo "错误:MySQL 编译失败,请截图错误信息寻求帮助。"
rm -rf ${mysqlPath}
exit 1
fi
# 安装
make install
if [ "$?" != "0" ]; then
echo -e $HR
echo "错误:MySQL 安装失败,请截图错误信息寻求帮助。"
rm -rf ${mysqlPath}
exit 1
fi
# 配置
mkdir ${mysqlPath}/conf
cat > ${mysqlPath}/conf/my.cnf << EOF
[client]
port = 3306
socket = /tmp/mysql.sock
[mysqld]
port = 3306
socket = /tmp/mysql.sock
datadir = ${mysqlPath}/data
default_storage_engine = InnoDB
skip-external-locking
table_definition_cache = 400
performance_schema_max_table_instances = 400
key_buffer_size = 8M
max_allowed_packet = 1G
table_open_cache = 32
sort_buffer_size = 256K
net_buffer_length = 4K
read_buffer_size = 128K
read_rnd_buffer_size = 256K
myisam_sort_buffer_size = 4M
thread_cache_size = 4
query_cache_size = 4M
tmp_table_size = 8M
explicit_defaults_for_timestamp = 1
#skip-name-resolve
max_connections = 500
max_connect_errors = 100
open_files_limit = 65535
early-plugin-load = ""
log-bin = mysql-bin
binlog_format = mixed
server-id = 1
slow_query_log = 1
slow-query-log-file = ${mysqlPath}/mysql-slow.log
long_query_time = 3
log-error = ${mysqlPath}/mysql-error.log
innodb_data_home_dir = ${mysqlPath}/data
innodb_data_file_path = ibdata1:10M:autoextend
innodb_log_group_home_dir = ${mysqlPath}/data
innodb_buffer_pool_size = 16M
innodb_redo_log_capacity = 5M
innodb_log_buffer_size = 8M
innodb_flush_log_at_trx_commit = 1
innodb_lock_wait_timeout = 50
innodb_max_dirty_pages_pct = 90
innodb_read_io_threads = 4
innodb_write_io_threads = 4
[mysqldump]
quick
max_allowed_packet = 500M
[myisamchk]
key_buffer_size = 20M
sort_buffer_size = 20M
read_buffer = 2M
write_buffer = 2M
[mysqlhotcopy]
interactive-timeout
EOF
# 根据CPU核心数确定写入线程数
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}" == "80" ]]; then
sed -i '/query_cache_size/d' ${mysqlPath}/conf/my.cnf
fi
if [[ "${1}" == "57" ]]; then
sed -i '/innodb_redo_log_capacity/d' ${mysqlPath}/conf/my.cnf
fi
# 根据内存大小调参
if [[ ${memTotal} -gt 1024 && ${memTotal} -lt 2048 ]]; then
sed -i "s#^key_buffer_size.*#key_buffer_size = 32M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^table_open_cache.*#table_open_cache = 128#" ${mysqlPath}/conf/my.cnf
sed -i "s#^sort_buffer_size.*#sort_buffer_size = 768K#" ${mysqlPath}/conf/my.cnf
sed -i "s#^read_buffer_size.*#read_buffer_size = 768K#" ${mysqlPath}/conf/my.cnf
sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 8M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^thread_cache_size.*#thread_cache_size = 16#" ${mysqlPath}/conf/my.cnf
sed -i "s#^query_cache_size.*#query_cache_size = 16M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^tmp_table_size.*#tmp_table_size = 32M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 128M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 64M" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 16M#" ${mysqlPath}/conf/my.cnf
elif [[ ${memTotal} -ge 2048 && ${memTotal} -lt 4096 ]]; then
sed -i "s#^key_buffer_size.*#key_buffer_size = 64M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^table_open_cache.*#table_open_cache = 256#" ${mysqlPath}/conf/my.cnf
sed -i "s#^sort_buffer_size.*#sort_buffer_size = 1M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^read_buffer_size.*#read_buffer_size = 1M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 16M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^thread_cache_size.*#thread_cache_size = 32#" ${mysqlPath}/conf/my.cnf
sed -i "s#^query_cache_size.*#query_cache_size = 32M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^tmp_table_size.*#tmp_table_size = 64M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 256M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 128M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 32M#" ${mysqlPath}/conf/my.cnf
elif [[ ${memTotal} -ge 4096 && ${memTotal} -lt 8192 ]]; then
sed -i "s#^key_buffer_size.*#key_buffer_size = 128M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^table_open_cache.*#table_open_cache = 512#" ${mysqlPath}/conf/my.cnf
sed -i "s#^sort_buffer_size.*#sort_buffer_size = 2M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^read_buffer_size.*#read_buffer_size = 2M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 32M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^thread_cache_size.*#thread_cache_size = 64#" ${mysqlPath}/conf/my.cnf
sed -i "s#^query_cache_size.*#query_cache_size = 64M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^tmp_table_size.*#tmp_table_size = 64M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 512M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 256M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 64M#" ${mysqlPath}/conf/my.cnf
elif [[ ${memTotal} -ge 8192 && ${memTotal} -lt 16384 ]]; then
sed -i "s#^key_buffer_size.*#key_buffer_size = 256M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^table_open_cache.*#table_open_cache = 1024#" ${mysqlPath}/conf/my.cnf
sed -i "s#^sort_buffer_size.*#sort_buffer_size = 4M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^read_buffer_size.*#read_buffer_size = 4M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 64M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^thread_cache_size.*#thread_cache_size = 128#" ${mysqlPath}/conf/my.cnf
sed -i "s#^query_cache_size.*#query_cache_size = 128M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^tmp_table_size.*#tmp_table_size = 128M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 1024M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 512M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 128M#" ${mysqlPath}/conf/my.cnf
elif [[ ${memTotal} -ge 16384 && ${memTotal} -lt 32768 ]]; then
sed -i "s#^key_buffer_size.*#key_buffer_size = 512M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^table_open_cache.*#table_open_cache = 2048#" ${mysqlPath}/conf/my.cnf
sed -i "s#^sort_buffer_size.*#sort_buffer_size = 8M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^read_buffer_size.*#read_buffer_size = 8M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 128M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^thread_cache_size.*#thread_cache_size = 256#" ${mysqlPath}/conf/my.cnf
sed -i "s#^query_cache_size.*#query_cache_size = 256M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^tmp_table_size.*#tmp_table_size = 256M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 2048M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 1G#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 256M#" ${mysqlPath}/conf/my.cnf
elif [[ ${memTotal} -ge 32768 ]]; then
sed -i "s#^key_buffer_size.*#key_buffer_size = 1024M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^table_open_cache.*#table_open_cache = 4096#" ${mysqlPath}/conf/my.cnf
sed -i "s#^sort_buffer_size.*#sort_buffer_size = 16M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^read_buffer_size.*#read_buffer_size = 16M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^myisam_sort_buffer_size.*#myisam_sort_buffer_size = 256M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^thread_cache_size.*#thread_cache_size = 512#" ${mysqlPath}/conf/my.cnf
sed -i "s#^query_cache_size.*#query_cache_size = 512M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^tmp_table_size.*#tmp_table_size = 512M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_buffer_pool_size.*#innodb_buffer_pool_size = 4096M#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_redo_log_capacity.*#innodb_redo_log_capacity = 2G#" ${mysqlPath}/conf/my.cnf
sed -i "s#^innodb_log_buffer_size.*#innodb_log_buffer_size = 512M#" ${mysqlPath}/conf/my.cnf
fi
# 初始化
rm -rf ${mysqlPath}/src
rm -rf ${mysqlPath}/data
mkdir -p ${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
echo "export PATH=${mysqlPath}/bin:\$PATH" >> /etc/profile
source /etc/profile
# 启动
cp ${mysqlPath}/lib/systemd/system/mysqld.service /etc/systemd/system/mysqld.service
sed -i '/ExecStartPre/d' /etc/systemd/system/mysqld.service
systemctl daemon-reload
systemctl enable mysqld
systemctl start mysqld
${mysqlPath}/bin/mysqladmin -u root password ${mysqlPassword}
panel writePlugin mysql${1} ${mysqlVersion}
panel writeMysqlPassword ${mysqlPassword}
echo -e "${HR}\nMySQL-${1} 安装完成\n${HR}"
+41
View File
@@ -0,0 +1,41 @@
#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
: '
Copyright 2022 HaoZi Technology Co., Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
'
HR="+----------------------------------------------------"
systemctl stop mysqld
systemctl disable mysqld
rm -rf /etc/systemd/system/mysqld.service
systemctl daemon-reload
pkill -9 mysqld
rm -rf /www/server/mysql
rm -f /usr/bin/mysql*
rm -f /usr/lib/libmysql*
rm -f /usr/lib64/libmysql*
userdel -r mysql
groupdel mysql
sed -i '#export PATH=/www/server/mysql#d' /etc/profile
source /etc/profile
panel deletePlugin mysql${1}
echo -e "${HR}\nMySQL-${1} 卸载完成\n${HR}"

Some files were not shown because too many files have changed in this diff Show More