Compare commits

...

36 Commits

Author SHA1 Message Date
耗子 9ffa32160b feat: release v2.0.22 2023-07-27 13:09:08 +08:00
耗子 872da5f8e1 fix(safe): ping status 2023-07-27 13:08:10 +08:00
耗子 eed3651cb1 fix(cron): editor space 2023-07-27 02:31:46 +08:00
耗子 653900fd73 fix(website): root setting and clean log 2023-07-27 02:29:00 +08:00
耗子 ff1f2569dd fix: login 2023-07-27 01:04:29 +08:00
耗子 0c6354fc6e feat: release v2.0.20 2023-07-27 00:57:58 +08:00
耗子 2ed5c36890 feat: add php 81 82 2023-07-27 00:57:13 +08:00
耗子 ecaf8edf14 feat: fix status 2023-07-27 00:47:54 +08:00
耗子 2429cf023a feat: release v2.0.19 2023-07-26 19:06:32 +08:00
耗子 c7adc39887 feat: update framework 2023-07-26 18:13:09 +08:00
耗子 f4e267ef1b feat: optimize openresty install 2023-07-26 15:26:22 +08:00
耗子 98bb24d97a fix: build 2023-07-26 15:22:38 +08:00
耗子 9e603a3003 revert: use v2 version 2023-07-26 15:16:24 +08:00
耗子 7164541415 feat: use v2 version 2023-07-26 15:15:47 +08:00
耗子 a51b391634 feat: use v2 version 2023-07-26 15:15:34 +08:00
耗子 edd3b2a622 fix: openresty install in rhel9 2023-07-26 14:59:42 +08:00
耗子 c55ef5c1f2 feat: release v2.0.18 2023-07-26 02:19:20 +08:00
耗子 65a46796ea feat: supervisor plugin and optimize code 2023-07-26 02:14:26 +08:00
耗子 b4720a1c84 feat: release v2.0.17 2023-07-25 23:42:27 +08:00
耗子 5ec6a88a60 fix: lint 2023-07-25 23:36:33 +08:00
耗子 cc70ab1fde feat: web ssh 2023-07-25 20:33:25 +08:00
耗子 cf58857fd1 workflow: fix issues-helper 2023-07-25 13:31:47 +08:00
耗子 cc5bacedee feat: support restart panel 2023-07-25 13:19:08 +08:00
耗子 fe0fe810ab feat: release v2.0.15 2023-07-25 03:27:18 +08:00
耗子 826b7f65f8 docs: update README 2023-07-25 03:26:50 +08:00
耗子 ce7a380e6e feat(cron): support backup 2023-07-25 03:21:00 +08:00
耗子 a128d295ea feat(task): show failed tasks 2023-07-25 00:24:28 +08:00
耗子 85db98ec10 feat: 移除插件作者字段 2023-07-25 00:20:31 +08:00
耗子 44ae4b9d67 feat: s3fs 插件 2023-07-25 00:16:00 +08:00
耗子 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
76 changed files with 5539 additions and 1397 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: ✏️ Feature
if: github.event.label.name == '✏️ Feature'
uses: actions-cool/issues-tools@v3
uses: actions-cool/issues-helper@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-tools@v3
uses: actions-cool/issues-helper@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-tools@v3
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issues'
labels: 'question'
+9 -5
View File
@@ -8,9 +8,9 @@
这是耗子Linux面板的开源仓库,基于Apache License 2.0协议进行开源,目前处于积极开发状态。
#### 我们正在进行基于 Golang 的 V2 版本的开发,加入我们的 交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[耗子](https://pd.qq.com/s/fyol46wfy) 获取最新动态。
#### 交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[耗子](https://pd.qq.com/s/fyol46wfy)
广告: [`WeAvatar` 统一头像服务](https://weavatar.com)
广告: [`WeAvatar` 多端多元化的统一头像服务](https://weavatar.com)
## 运行环境
@@ -38,7 +38,7 @@ bash <(curl -sSL https://dl.cdn.haozi.net/panel/install_panel.sh)
## 日常维护
```shell
panel xxx
panel
```
## 问题反馈
@@ -54,13 +54,17 @@ panel xxx
### 服务器
- [盾云](https://www.ddunyun.com/)
- [盾云](https://www.ddunyun.com/aff/PNYAXMKI)
### CDN
- [盾云CDN](http://cdn.ddunyun.com/)
- [又拍云](https://www.upyun.com/)
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
### DevOps
- [极狐GitLab](https://www.jihulab.com/)
#### 接受云资源和资金赞助,可通过QQ群咨询联系
+191 -4
View File
@@ -2,11 +2,15 @@ package commands
import (
"os"
"path/filepath"
"sort"
"strings"
"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"
@@ -39,6 +43,8 @@ func (receiver *Panel) Handle(ctx console.Context) error {
action := ctx.Argument(0)
arg1 := ctx.Argument(1)
arg2 := ctx.Argument(2)
arg3 := ctx.Argument(3)
arg4 := ctx.Argument(4)
switch action {
case "init":
@@ -130,6 +136,15 @@ func (receiver *Panel) Handle(ctx console.Context) error {
case "getEntrance":
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
case "deleteEntrance":
err := services.NewSettingImpl().Set(models.SettingKeyEntrance, "/")
if err != nil {
color.Redln("删除面板入口失败")
return nil
}
color.Greenln("删除面板入口成功")
case "writePlugin":
slug := arg1
version := arg2
@@ -198,6 +213,176 @@ func (receiver *Panel) Handle(ctx console.Context) error {
color.Greenln("清理任务成功")
case "backup":
backupType := arg1
name := arg2
path := arg3
save := arg4
hr := `+----------------------------------------------------`
if len(backupType) == 0 || len(name) == 0 || len(path) == 0 || len(save) == 0 {
color.Redln("参数错误")
return nil
}
color.Greenln(hr)
color.Greenln("★ 开始备份 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
if !tools.Exists(path) {
tools.Mkdir(path, 0644)
}
switch backupType {
case "website":
color.Yellowln("|-目标网站: " + name)
var website models.Website
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil {
color.Redln("|-网站不存在")
color.Greenln(hr)
return nil
}
backupFile := path + "/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".zip"
tools.ExecShell(`cd '` + website.Path + `' && zip -r '` + backupFile + `' .`)
color.Greenln("|-备份成功")
case "mysql":
rootPassword := services.NewSettingImpl().Get(models.SettingKeyMysqlRootPassword)
backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
err := os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
color.Redln("|-备份MySQL数据库失败: " + err.Error())
color.Greenln(hr)
return nil
}
color.Greenln("|-目标MySQL数据库: " + name)
color.Greenln("|-开始导出")
tools.ExecShell(`mysqldump -uroot ` + name + ` > /tmp/` + backupFile + ` 2>&1`)
color.Greenln("|-导出成功")
color.Greenln("|-开始压缩")
tools.ExecShell("cd /tmp && zip -r " + backupFile + ".zip " + backupFile)
tools.RemoveFile("/tmp/" + backupFile)
color.Greenln("|-压缩成功")
color.Greenln("|-开始移动")
tools.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip")
color.Greenln("|-移动成功")
_ = os.Unsetenv("MYSQL_PWD")
color.Greenln("|-备份成功")
case "postgresql":
backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
check := tools.ExecShell(`su - postgres -c "psql -l" 2>&1`)
if strings.Contains(check, name) {
color.Redln("|-数据库不存在")
color.Greenln(hr)
return nil
}
color.Greenln("|-目标PostgreSQL数据库: " + name)
color.Greenln("|-开始导出")
tools.ExecShell(`su - postgres -c "pg_dump '` + name + `'" > /tmp/` + backupFile + ` 2>&1`)
color.Greenln("|-导出成功")
color.Greenln("|-开始压缩")
tools.ExecShell("cd /tmp && zip -r " + backupFile + ".zip " + backupFile)
tools.RemoveFile("/tmp/" + backupFile)
color.Greenln("|-压缩成功")
color.Greenln("|-开始移动")
tools.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip")
color.Greenln("|-移动成功")
color.Greenln("|-备份成功")
}
color.Greenln(hr)
files, err := os.ReadDir(path)
if err != nil {
color.Redln("|-清理失败: " + err.Error())
return nil
}
var filteredFiles []os.FileInfo
for _, file := range files {
if strings.HasPrefix(file.Name(), name) && strings.HasSuffix(file.Name(), ".zip") {
fileInfo, err := os.Stat(filepath.Join(path, file.Name()))
if err != nil {
continue
}
filteredFiles = append(filteredFiles, fileInfo)
}
}
sort.Slice(filteredFiles, func(i, j int) bool {
return filteredFiles[i].ModTime().After(filteredFiles[j].ModTime())
})
for i := cast.ToInt(save); i < len(filteredFiles); i++ {
fileToDelete := filepath.Join(path, filteredFiles[i].Name())
color.Yellowln("|-清理备份: " + fileToDelete)
tools.RemoveFile(fileToDelete)
}
color.Greenln("|-清理完成")
color.Greenln(hr)
color.Greenln("☆ 备份完成 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
case "cutoff":
name := arg1
save := arg2
hr := `+----------------------------------------------------`
if len(name) == 0 || len(save) == 0 {
color.Redln("参数错误")
return nil
}
color.Greenln(hr)
color.Greenln("★ 开始切割 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
color.Yellowln("|-目标网站: " + name)
var website models.Website
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil {
color.Redln("|-网站不存在")
color.Greenln(hr)
return nil
}
logPath := "/www/wwwlogs/" + website.Name + ".log"
if !tools.Exists(logPath) {
color.Redln("|-日志文件不存在")
color.Greenln(hr)
return nil
}
backupPath := "/www/wwwlogs/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".log.zip"
tools.ExecShell(`cd /www/wwwlogs && zip -r ` + backupPath + ` ` + website.Name + ".log")
tools.ExecShell(`echo "" > ` + logPath)
color.Greenln("|-切割成功")
color.Greenln(hr)
files, err := os.ReadDir("/www/wwwlogs")
if err != nil {
color.Redln("|-清理失败: " + err.Error())
return nil
}
var filteredFiles []os.FileInfo
for _, file := range files {
if strings.HasPrefix(file.Name(), website.Name) && strings.HasSuffix(file.Name(), ".log.zip") {
fileInfo, err := os.Stat(filepath.Join("/www/wwwlogs", file.Name()))
if err != nil {
continue
}
filteredFiles = append(filteredFiles, fileInfo)
}
}
sort.Slice(filteredFiles, func(i, j int) bool {
return filteredFiles[i].ModTime().After(filteredFiles[j].ModTime())
})
for i := cast.ToInt(save); i < len(filteredFiles); i++ {
fileToDelete := filepath.Join("/www/wwwlogs", filteredFiles[i].Name())
color.Yellowln("|-清理日志: " + fileToDelete)
tools.RemoveFile(fileToDelete)
}
color.Greenln("|-清理完成")
color.Greenln(hr)
color.Greenln("☆ 切割完成 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
case "writeSite":
name := arg1
@@ -290,12 +475,14 @@ 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 {proxy} 更新/修复面板到最新版本")
color.Greenln("panel update {proxy} 更新 / 修复面板到最新版本")
color.Greenln("panel getInfo 重新初始化面板账号信息")
color.Greenln("panel getPort 获取面板访问端口")
color.Greenln("panel getEntrance 获取面板访问入口")
color.Greenln("panel cleanTask 清理面板运行中和等待中的任务")
color.Greenln("panel backup {website/mysql/postgresql} {name} {path} 备份网站/MySQL数据库/PostgreSQL数据库到指定目录")
color.Greenln("panel deleteEntrance 删除面板访问入口")
color.Greenln("panel cleanTask 清理面板运行中和等待中的任务[任务卡住时使用]")
color.Greenln("panel backup {website/mysql/postgresql} {name} {path} {save_copies} 备份网站 / MySQL数据库 / PostgreSQL数据库到指定目录并保留指定数量")
color.Greenln("panel cutoff {website_name} {save_copies} 切割网站日志并保留指定数量")
color.Redln("以下命令请在开发者指导下使用:")
color.Yellowln("panel init 初始化面板")
color.Yellowln("panel writePlugin {slug} {version} 写入插件安装状态")
@@ -303,7 +490,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
color.Yellowln("panel writeMysqlPassword {password} 写入MySQL root密码")
color.Yellowln("panel writeSite {name} {status} {path} {php} {ssl} 写入网站数据到面板")
color.Yellowln("panel deleteSite {name} 删除面板网站数据")
color.Yellowln("panel writeSetting {name} {value} 写入/更新面板设置数据")
color.Yellowln("panel writeSetting {name} {value} 写入 / 更新面板设置数据")
color.Yellowln("panel deleteSetting {name} 删除面板设置数据")
}
+65 -22
View File
@@ -7,23 +7,25 @@ import (
"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"
"panel/pkg/tools"
)
type CronController struct {
cron services.Cron
cron services.Cron
setting services.Setting
}
func NewCronController() *CronController {
return &CronController{
cron: services.NewCronImpl(),
cron: services.NewCronImpl(),
setting: services.NewSettingImpl(),
}
}
func (r *CronController) List(ctx http.Context) {
func (c *CronController) List(ctx http.Context) {
limit := ctx.Request().QueryInt("limit")
page := ctx.Request().QueryInt("page")
@@ -42,11 +44,13 @@ func (r *CronController) List(ctx http.Context) {
})
}
func (r *CronController) Add(ctx http.Context) {
func (c *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",
"name": "required|min_len:1|max_len:255",
"time": "required",
"script": "required",
"type": "required|in:shell,backup,cutoff",
"backup_type": "required_if:type,backup|in:website,mysql,postgresql",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
@@ -63,7 +67,46 @@ func (r *CronController) Add(ctx http.Context) {
return
}
// 写入shell
shell := ctx.Request().Input("script")
cronType := ctx.Request().Input("type")
if cronType == "backup" {
backupType := ctx.Request().Input("backup_type")
backupName := ctx.Request().Input("backup_database")
if backupType == "website" {
backupName = ctx.Request().Input("website")
}
backupPath := ctx.Request().Input("backup_path", c.setting.Get(models.SettingKeyBackupPath)+"/"+backupType)
backupSave := ctx.Request().InputInt("save", 10)
shell = `#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子面板 - 数据备份脚本
type=` + backupType + `
path=` + backupPath + `
name=` + backupName + `
save=` + cast.ToString(backupSave) + `
# 执行备份
panel backup ${type} ${name} ${path} ${save} 2>&1
`
}
if cronType == "cutoff" {
website := ctx.Request().Input("website")
save := ctx.Request().InputInt("save", 180)
shell = `#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子面板 - 日志切割脚本
name=` + website + `
save=` + cast.ToString(save) + `
# 执行切割
panel cutoff ${name} ${save} 2>&1
`
}
shellDir := "/www/server/cron/"
shellLogDir := "/www/server/cron/logs/"
if !tools.Exists(shellDir) {
@@ -77,7 +120,7 @@ func (r *CronController) Add(ctx http.Context) {
return
}
shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + tools.RandomString(16)
if !tools.WriteFile(shellDir+shellFile+".sh", ctx.Request().Input("script"), 0700) {
if !tools.WriteFile(shellDir+shellFile+".sh", shell, 0700) {
facades.Log().Error("[面板][CronController] 创建计划任务脚本失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
@@ -86,7 +129,7 @@ func (r *CronController) Add(ctx http.Context) {
var cron models.Cron
cron.Name = ctx.Request().Input("name")
cron.Type = "shell"
cron.Type = ctx.Request().Input("type")
cron.Status = true
cron.Time = ctx.Request().Input("time")
cron.Shell = shellDir + shellFile + ".sh"
@@ -99,7 +142,7 @@ func (r *CronController) Add(ctx http.Context) {
return
}
r.cron.AddToSystem(cron)
c.cron.AddToSystem(cron)
Success(ctx, http.Json{
"id": cron.ID,
@@ -107,7 +150,7 @@ func (r *CronController) Add(ctx http.Context) {
}
// Script 获取脚本内容
func (r *CronController) Script(ctx http.Context) {
func (c *CronController) Script(ctx http.Context) {
var cron models.Cron
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
@@ -118,7 +161,7 @@ func (r *CronController) Script(ctx http.Context) {
Success(ctx, tools.ReadFile(cron.Shell))
}
func (r *CronController) Update(ctx http.Context) {
func (c *CronController) Update(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
"time": "required",
@@ -167,15 +210,15 @@ func (r *CronController) Update(ctx http.Context) {
}
tools.ExecShell("dos2unix " + cron.Shell)
r.cron.DeleteFromSystem(cron)
c.cron.DeleteFromSystem(cron)
if cron.Status {
r.cron.AddToSystem(cron)
c.cron.AddToSystem(cron)
}
Success(ctx, nil)
}
func (r *CronController) Delete(ctx http.Context) {
func (c *CronController) Delete(ctx http.Context) {
var cron models.Cron
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
@@ -183,7 +226,7 @@ func (r *CronController) Delete(ctx http.Context) {
return
}
r.cron.DeleteFromSystem(cron)
c.cron.DeleteFromSystem(cron)
tools.RemoveFile(cron.Shell)
_, err = facades.Orm().Query().Delete(&cron)
@@ -196,7 +239,7 @@ func (r *CronController) Delete(ctx http.Context) {
Success(ctx, nil)
}
func (r *CronController) Status(ctx http.Context) {
func (c *CronController) Status(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"status": "bool",
})
@@ -224,15 +267,15 @@ func (r *CronController) Status(ctx http.Context) {
return
}
r.cron.DeleteFromSystem(cron)
c.cron.DeleteFromSystem(cron)
if cron.Status {
r.cron.AddToSystem(cron)
c.cron.AddToSystem(cron)
}
Success(ctx, nil)
}
func (r *CronController) Log(ctx http.Context) {
func (c *CronController) Log(ctx http.Context) {
var cron models.Cron
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
+13
View File
@@ -51,6 +51,7 @@ func (c *InfoController) Menu(ctx http.Context) {
{Name: "safe", Title: "系统安全", Icon: "layui-icon-auz", Jump: "safe"},
/*{Name: "file", Title: "文件管理", Icon: "layui-icon-file", Jump: "file"},*/
{Name: "cron", Title: "计划任务", Icon: "layui-icon-date", Jump: "cron"},
{Name: "ssh", Title: "SSH", Icon: "layui-icon-layer", Jump: "ssh"},
{Name: "plugin", Title: "插件中心", Icon: "layui-icon-app", Jump: "plugin"},
{Name: "setting", Title: "面板设置", Icon: "layui-icon-set", Jump: "setting"},
})
@@ -180,3 +181,15 @@ func (c *InfoController) Update(ctx http.Context) {
Success(ctx, nil)
}
func (c *InfoController) Restart(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
}
tools.ExecShell("systemctl restart panel")
Success(ctx, nil)
}
+12 -4
View File
@@ -129,6 +129,9 @@ func (r *MonitorController) List(ctx http.Context) {
var bytesSent2 uint64
var bytesRecv2 uint64
for _, net := range monitors[0].Info.Net {
if net.Name == "lo" {
continue
}
bytesSent += net.BytesSent
bytesRecv += net.BytesRecv
}
@@ -141,6 +144,9 @@ func (r *MonitorController) List(ctx http.Context) {
continue
}
for _, net := range monitor.Info.Net {
if net.Name == "lo" {
continue
}
bytesSent2 += net.BytesSent
bytesRecv2 += net.BytesRecv
}
@@ -153,15 +159,17 @@ func (r *MonitorController) List(ctx http.Context) {
data.Mem.Used = append(data.Mem.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Used)/1024/1024))
data.Swap.Used = append(data.Swap.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024))
data.Swap.Free = append(data.Swap.Free, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Free)/1024/1024))
data.Net.Sent = append(data.Net.Sent, fmt.Sprintf("%.2f", float64(bytesSent2/1024)))
data.Net.Recv = append(data.Net.Recv, fmt.Sprintf("%.2f", float64(bytesRecv2/1024)))
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))
data.Net.Rx = append(data.Net.Rx, fmt.Sprintf("%.2f", float64(bytesRecv2-bytesRecv)/60/1024))
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)
@@ -41,7 +41,6 @@ func (r *PluginController) List(ctx http.Context) {
type plugin struct {
Name string `json:"name"`
Author string `json:"author"`
Description string `json:"description"`
Slug string `json:"slug"`
Version string `json:"version"`
@@ -62,7 +61,6 @@ func (r *PluginController) List(ctx http.Context) {
}
p = append(p, plugin{
Name: item.Name,
Author: item.Author,
Description: item.Description,
Slug: item.Slug,
Version: item.Version,
@@ -3,15 +3,13 @@ package mysql57
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
@@ -20,11 +18,13 @@ import (
type Mysql57Controller struct {
setting services.Setting
backup services.Backup
}
func NewMysql57Controller() *Mysql57Controller {
return &Mysql57Controller{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
@@ -441,36 +441,14 @@ func (c *Mysql57Controller) DeleteDatabase(ctx http.Context) {
// BackupList 获取备份列表
func (c *Mysql57Controller) BackupList(ctx http.Context) {
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
files, err := os.ReadDir(backupPath)
backupList, err := c.backup.MysqlList()
if err != nil {
facades.Log().Error("[MySQL57] 获取备份列表失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
return
}
var backupFiles []map[string]string
for _, file := range files {
if file.IsDir() {
continue
}
info, err := file.Info()
if err != nil {
continue
}
backupFiles = append(backupFiles, map[string]string{
"file": file.Name(),
"size": tools.FormatBytes(float64(info.Size())),
})
}
controllers.Success(ctx, backupFiles)
controllers.Success(ctx, backupList)
}
// UploadBackup 上传备份
@@ -479,10 +457,6 @@ func (c *Mysql57Controller) UploadBackup(ctx http.Context) {
return
}
// TODO 框架 Bug ? 下面会 panic
controllers.Error(ctx, http.StatusBadRequest, "暂不支持")
return
file, err := ctx.Request().File("file")
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
@@ -495,8 +469,7 @@ func (c *Mysql57Controller) UploadBackup(ctx http.Context) {
}
name := file.GetClientOriginalName()
extension := file.GetClientOriginalExtension()
_, err = file.Store(backupPath + "/" + name + "." + extension)
_, err = file.StoreAs(backupPath, name)
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
return
@@ -523,25 +496,14 @@ func (c *Mysql57Controller) CreateBackup(ctx http.Context) {
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
backupFile := database + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
err = os.Setenv("MYSQL_PWD", rootPassword)
err = c.backup.MysqlBackup(database)
if err != nil {
facades.Log().Error("[MySQL57] 设置环境变量 MYSQL_PWD 失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "备份失败")
facades.Log().Error("[MYSQL57] 创建备份失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
return
}
tools.ExecShell("mysqldump -uroot " + database + " > " + backupPath + "/" + backupFile)
tools.ExecShell("cd " + backupPath + " && zip -r " + backupPath + "/" + backupFile + ".zip " + backupFile)
tools.RemoveFile(backupPath + "/" + backupFile)
_ = os.Unsetenv("MYSQL_PWD")
controllers.Success(ctx, "备份成功")
}
@@ -552,7 +514,7 @@ func (c *Mysql57Controller) DeleteBackup(ctx http.Context) {
}
validator, err := ctx.Request().Validate(map[string]string{
"file": "required|min_len:1|max_len:255",
"name": "required|min_len:1|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
@@ -564,8 +526,8 @@ func (c *Mysql57Controller) DeleteBackup(ctx http.Context) {
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
file := ctx.Request().Input("file")
tools.RemoveFile(backupPath + "/" + file)
fileName := ctx.Request().Input("name")
tools.RemoveFile(backupPath + "/" + fileName)
controllers.Success(ctx, "删除备份成功")
}
@@ -577,7 +539,7 @@ func (c *Mysql57Controller) RestoreBackup(ctx http.Context) {
}
validator, err := ctx.Request().Validate(map[string]string{
"file": "required|min_len:1|max_len:255",
"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 {
@@ -589,57 +551,13 @@ func (c *Mysql57Controller) RestoreBackup(ctx http.Context) {
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
file := ctx.Request().Input("file")
backupFile := backupPath + "/" + file
if !tools.Exists(backupFile) {
controllers.Error(ctx, http.StatusBadRequest, "备份文件不存在")
return
}
err = os.Setenv("MYSQL_PWD", rootPassword)
err = c.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
if err != nil {
facades.Log().Error("[MYSQL57] 设置环境变量 MYSQL_PWD 失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "还原失败")
facades.Log().Error("[MYSQL57] 还原失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
return
}
// 获取文件拓展名
ext := filepath.Ext(file)
switch ext {
case ".zip":
tools.ExecShell("unzip -o " + backupFile + " -d " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".gz":
if strings.HasSuffix(file, ".tar.gz") {
// 解压.tar.gz文件
tools.ExecShell("tar -zxvf " + backupFile + " -C " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ".tar.gz")
} else {
// 解压.gz文件
tools.ExecShell("gzip -d " + backupFile)
backupFile = strings.TrimSuffix(backupFile, ext)
}
case ".bz2":
tools.ExecShell("bzip2 -d " + backupFile)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".tar":
tools.ExecShell("tar -xvf " + backupFile + " -C " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".rar":
tools.ExecShell("unrar x " + backupFile + " " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
}
if !tools.Exists(backupFile) {
controllers.Error(ctx, http.StatusBadRequest, "自动解压备份文件失败,请手动解压")
return
}
tools.ExecShell("mysql -uroot " + ctx.Request().Input("database") + " < " + backupFile)
_ = os.Unsetenv("MYSQL_PWD")
controllers.Success(ctx, "还原成功")
}
@@ -3,15 +3,13 @@ package mysql80
import (
"database/sql"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
@@ -20,11 +18,13 @@ import (
type Mysql80Controller struct {
setting services.Setting
backup services.Backup
}
func NewMysql80Controller() *Mysql80Controller {
return &Mysql80Controller{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
@@ -441,36 +441,14 @@ func (c *Mysql80Controller) DeleteDatabase(ctx http.Context) {
// BackupList 获取备份列表
func (c *Mysql80Controller) BackupList(ctx http.Context) {
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
files, err := os.ReadDir(backupPath)
backupList, err := c.backup.MysqlList()
if err != nil {
facades.Log().Error("[MySQL80] 获取备份列表失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
return
}
var backupFiles []map[string]string
for _, file := range files {
if file.IsDir() {
continue
}
info, err := file.Info()
if err != nil {
continue
}
backupFiles = append(backupFiles, map[string]string{
"file": file.Name(),
"size": tools.FormatBytes(float64(info.Size())),
})
}
controllers.Success(ctx, backupFiles)
controllers.Success(ctx, backupList)
}
// UploadBackup 上传备份
@@ -479,10 +457,6 @@ func (c *Mysql80Controller) UploadBackup(ctx http.Context) {
return
}
// TODO 框架 Bug ? 下面会 panic
controllers.Error(ctx, http.StatusBadRequest, "暂不支持")
return
file, err := ctx.Request().File("file")
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
@@ -495,8 +469,7 @@ func (c *Mysql80Controller) UploadBackup(ctx http.Context) {
}
name := file.GetClientOriginalName()
extension := file.GetClientOriginalExtension()
_, err = file.Store(backupPath + "/" + name + "." + extension)
_, err = file.StoreAs(backupPath, name)
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
return
@@ -523,25 +496,14 @@ func (c *Mysql80Controller) CreateBackup(ctx http.Context) {
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
backupFile := database + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
err = os.Setenv("MYSQL_PWD", rootPassword)
err = c.backup.MysqlBackup(database)
if err != nil {
facades.Log().Error("[MySQL80] 设置环境变量 MYSQL_PWD 失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "备份失败")
facades.Log().Error("[MYSQL80] 创建备份失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
return
}
tools.ExecShell("mysqldump -uroot " + database + " > " + backupPath + "/" + backupFile)
tools.ExecShell("cd " + backupPath + " && zip -r " + backupPath + "/" + backupFile + ".zip " + backupFile)
tools.RemoveFile(backupPath + "/" + backupFile)
_ = os.Unsetenv("MYSQL_PWD")
controllers.Success(ctx, "备份成功")
}
@@ -552,7 +514,7 @@ func (c *Mysql80Controller) DeleteBackup(ctx http.Context) {
}
validator, err := ctx.Request().Validate(map[string]string{
"file": "required|min_len:1|max_len:255",
"name": "required|min_len:1|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
@@ -564,8 +526,8 @@ func (c *Mysql80Controller) DeleteBackup(ctx http.Context) {
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
file := ctx.Request().Input("file")
tools.RemoveFile(backupPath + "/" + file)
fileName := ctx.Request().Input("name")
tools.RemoveFile(backupPath + "/" + fileName)
controllers.Success(ctx, "删除备份成功")
}
@@ -577,7 +539,7 @@ func (c *Mysql80Controller) RestoreBackup(ctx http.Context) {
}
validator, err := ctx.Request().Validate(map[string]string{
"file": "required|min_len:1|max_len:255",
"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 {
@@ -589,57 +551,13 @@ func (c *Mysql80Controller) RestoreBackup(ctx http.Context) {
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
file := ctx.Request().Input("file")
backupFile := backupPath + "/" + file
if !tools.Exists(backupFile) {
controllers.Error(ctx, http.StatusBadRequest, "备份文件不存在")
return
}
err = os.Setenv("MYSQL_PWD", rootPassword)
err = c.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
if err != nil {
facades.Log().Error("[MYSQL80] 设置环境变量 MYSQL_PWD 失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "还原失败")
facades.Log().Error("[MYSQL80] 还原失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
return
}
// 获取文件拓展名
ext := filepath.Ext(file)
switch ext {
case ".zip":
tools.ExecShell("unzip -o " + backupFile + " -d " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".gz":
if strings.HasSuffix(file, ".tar.gz") {
// 解压.tar.gz文件
tools.ExecShell("tar -zxvf " + backupFile + " -C " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ".tar.gz")
} else {
// 解压.gz文件
tools.ExecShell("gzip -d " + backupFile)
backupFile = strings.TrimSuffix(backupFile, ext)
}
case ".bz2":
tools.ExecShell("bzip2 -d " + backupFile)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".tar":
tools.ExecShell("tar -xvf " + backupFile + " -C " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".rar":
tools.ExecShell("unrar x " + backupFile + " " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
}
if !tools.Exists(backupFile) {
controllers.Error(ctx, http.StatusBadRequest, "自动解压备份文件失败,请手动解压")
return
}
tools.ExecShell("mysql -uroot " + ctx.Request().Input("database") + " < " + backupFile)
_ = os.Unsetenv("MYSQL_PWD")
controllers.Success(ctx, "还原成功")
}
@@ -35,8 +35,7 @@ func (c *Php74Controller) Status(ctx http.Context) {
return
}
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
status := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
@@ -35,8 +35,7 @@ func (c *Php80Controller) Status(ctx http.Context) {
return
}
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
status := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
@@ -0,0 +1,371 @@
package php81
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 Php81Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp81Controller() *Php81Controller {
return &Php81Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "81",
}
}
func (c *Php81Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
status := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
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 *Php81Controller) 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 *Php81Controller) 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 *Php81Controller) 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 *Php81Controller) 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 *Php81Controller) 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 *Php81Controller) 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 *Php81Controller) 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 *Php81Controller) 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 *Php81Controller) 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 *Php81Controller) 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 *Php81Controller) 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 *Php81Controller) GetExtensionList(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
extensions := c.GetExtensions()
controllers.Success(ctx, extensions)
}
func (c *Php81Controller) 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 *Php81Controller) 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 *Php81Controller) 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,365 @@
package php82
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 Php82Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp82Controller() *Php82Controller {
return &Php82Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "82",
}
}
func (c *Php82Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
status := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
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 *Php82Controller) 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 *Php82Controller) 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 *Php82Controller) 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 *Php82Controller) 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 *Php82Controller) 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 *Php82Controller) 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 *Php82Controller) 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 *Php82Controller) 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 *Php82Controller) 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 *Php82Controller) 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 *Php82Controller) 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 *Php82Controller) GetExtensionList(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
extensions := c.GetExtensions()
controllers.Success(ctx, extensions)
}
func (c *Php82Controller) 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 *Php82Controller) 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 *Php82Controller) 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,
})
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,222 @@
package s3fs
import (
"strings"
"github.com/bytedance/sonic"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/services"
"panel/pkg/tools"
)
type S3fsController struct {
setting services.Setting
}
type s3fs struct {
ID int64 `json:"id"`
Path string `json:"path"`
Bucket string `json:"bucket"`
Url string `json:"url"`
}
func NewS3fsController() *S3fsController {
return &S3fsController{
setting: services.NewSettingImpl(),
}
}
// List 所有 S3fs 挂载
func (c *S3fsController) List(ctx http.Context) {
if !controllers.Check(ctx, "s3fs") {
return
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
var s3fsList []s3fs
err := sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList)
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "获取 S3fs 挂载失败")
return
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(s3fsList) {
controllers.Success(ctx, http.Json{
"total": 0,
"items": []s3fs{},
})
return
}
if endIndex > len(s3fsList) {
endIndex = len(s3fsList)
}
pagedS3fsList := s3fsList[startIndex:endIndex]
controllers.Success(ctx, http.Json{
"total": len(s3fsList),
"items": pagedS3fsList,
})
}
// Add 添加 S3fs 挂载
func (c *S3fsController) Add(ctx http.Context) {
if !controllers.Check(ctx, "s3fs") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"ak": "required|regex:^[a-zA-Z0-9]*$",
"sk": "required|regex:^[a-zA-Z0-9]*$",
"bucket": "required|regex:^[a-zA-Z0-9_-]*$",
"url": "required|full_url",
"path": "required|regex:^/[a-zA-Z0-9_-]+$",
})
if err != nil {
controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
return
}
ak := ctx.Request().Input("ak")
sk := ctx.Request().Input("sk")
path := ctx.Request().Input("path")
bucket := ctx.Request().Input("bucket")
url := ctx.Request().Input("url")
// 检查下地域节点中是否包含bucket,如果包含了,肯定是错误的
if strings.Contains(url, bucket) {
controllers.Error(ctx, http.StatusUnprocessableEntity, "地域节点不能包含 Bucket 名称")
return
}
// 检查挂载目录是否存在且为空
if !tools.Exists(path) {
tools.Mkdir(path, 0755)
}
if !tools.Empty(path) {
controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录必须为空")
return
}
var s3fsList []s3fs
err = sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
return
}
for _, s := range s3fsList {
if s.Path == path {
controllers.Error(ctx, http.StatusUnprocessableEntity, "路径已存在")
return
}
}
id := carbon.Now().TimestampMilli()
password := ak + ":" + sk
tools.WriteFile("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600)
tools.ExecShell(`echo 's3fs#` + bucket + ` ` + path + ` fuse _netdev,allow_other,nonempty,url=` + url + `,passwd_file=/etc/passwd-s3fs-` + cast.ToString(id) + ` 0 0' >> /etc/fstab`)
check := tools.ExecShell("mount -a 2>&1")
if len(check) != 0 {
tools.ExecShell(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+check)
return
}
check2 := tools.ExecShell("df -h | grep " + path + " 2>&1")
if len(check2) == 0 {
tools.ExecShell(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
controllers.Error(ctx, http.StatusInternalServerError, "挂载失败,请检查配置是否正确")
return
}
s3fsList = append(s3fsList, s3fs{
ID: id,
Path: path,
Bucket: bucket,
Url: url,
})
json, err := sonic.MarshalString(s3fsList)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败")
return
}
err = c.setting.Set("s3fs", json)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败")
return
}
controllers.Success(ctx, nil)
}
// Delete 删除 S3fs 挂载
func (c *S3fsController) Delete(ctx http.Context) {
if !controllers.Check(ctx, "s3fs") {
return
}
id := ctx.Request().Input("id")
if len(id) == 0 {
controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不能为空")
return
}
var s3fsList []s3fs
err := sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
return
}
var mount s3fs
for _, s := range s3fsList {
if cast.ToString(s.ID) == id {
mount = s
break
}
}
if mount.ID == 0 {
controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不存在")
return
}
tools.ExecShell(`fusermount -u '` + mount.Path + `' 2>&1`)
tools.ExecShell(`umount '` + mount.Path + `' 2>&1`)
tools.ExecShell(`sed -i 's@^s3fs#` + mount.Bucket + `\s` + mount.Path + `.*$@@g' /etc/fstab`)
check := tools.ExecShell("mount -a 2>&1")
if len(check) != 0 {
controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+check)
return
}
tools.RemoveFile("/etc/passwd-s3fs-" + cast.ToString(mount.ID))
var newS3fsList []s3fs
for _, s := range s3fsList {
if s.ID != mount.ID {
newS3fsList = append(newS3fsList, s)
}
}
json, err := sonic.MarshalString(newS3fsList)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败")
return
}
err = c.setting.Set("s3fs", json)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败")
return
}
controllers.Success(ctx, nil)
}
@@ -0,0 +1,390 @@
package supervisor
import (
"strconv"
"strings"
"github.com/goravel/framework/contracts/http"
"panel/pkg/tools"
"panel/app/http/controllers"
)
type SupervisorController struct {
ServiceName string
}
func NewSupervisorController() *SupervisorController {
var serviceName string
if tools.IsRHEL() {
serviceName = "supervisord"
} else {
serviceName = "supervisor"
}
return &SupervisorController{
ServiceName: serviceName,
}
}
// Status 状态
func (r *SupervisorController) Status(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
status := tools.ExecShell(`systemctl status ` + r.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Start 启动
func (r *SupervisorController) Start(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
tools.ExecShell(`systemctl start ` + r.ServiceName)
status := tools.ExecShell(`systemctl status ` + r.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Stop 停止
func (r *SupervisorController) Stop(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
tools.ExecShell(`systemctl stop ` + r.ServiceName)
status := tools.ExecShell(`systemctl status ` + r.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
if status != "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Restart 重启
func (r *SupervisorController) Restart(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
tools.ExecShell(`systemctl restart ` + r.ServiceName)
status := tools.ExecShell(`systemctl status ` + r.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Reload 重载
func (r *SupervisorController) Reload(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
tools.ExecShell(`systemctl reload ` + r.ServiceName)
status := tools.ExecShell(`systemctl status ` + r.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Log 日志
func (r *SupervisorController) Log(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
log := tools.ExecShell(`tail -n 200 /var/log/supervisor/supervisord.log`)
controllers.Success(ctx, log)
}
// ClearLog 清空日志
func (r *SupervisorController) ClearLog(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
tools.ExecShell(`echo "" > /var/log/supervisor/supervisord.log`)
controllers.Success(ctx, nil)
}
// Config 获取配置
func (r *SupervisorController) Config(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
var config string
if tools.IsRHEL() {
config = tools.ReadFile(`/etc/supervisord.conf`)
} else {
config = tools.ReadFile(`/etc/supervisor/supervisord.conf`)
}
controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *SupervisorController) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
config := ctx.Request().Input("config")
if tools.IsRHEL() {
tools.WriteFile(`/etc/supervisord.conf`, config, 0644)
} else {
tools.WriteFile(`/etc/supervisor/supervisord.conf`, config, 0644)
}
tools.ExecShell(`systemctl restart ` + r.ServiceName)
controllers.Success(ctx, nil)
}
// Processes 进程列表
func (r *SupervisorController) Processes(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
type process struct {
Name string `json:"name"`
Status string `json:"status"`
Pid string `json:"pid"`
Uptime string `json:"uptime"`
}
out := tools.ExecShell(`supervisorctl status | awk '{print $1}'`)
var processList []process
for _, line := range strings.Split(out, "\n") {
if len(line) == 0 {
continue
}
var p process
p.Name = line
p.Status = tools.ExecShell(`supervisorctl status ` + line + ` | awk '{print $2}'`)
if p.Status == "RUNNING" {
p.Pid = strings.ReplaceAll(tools.ExecShell(`supervisorctl status `+line+` | awk '{print $4}'`), ",", "")
p.Uptime = tools.ExecShell(`supervisorctl status ` + line + ` | awk '{print $6}'`)
} else {
p.Pid = "-"
p.Uptime = "-"
}
processList = append(processList, p)
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(processList) {
controllers.Success(ctx, http.Json{
"total": 0,
"items": []process{},
})
return
}
if endIndex > len(processList) {
endIndex = len(processList)
}
pagedProcessList := processList[startIndex:endIndex]
controllers.Success(ctx, http.Json{
"total": len(processList),
"items": pagedProcessList,
})
}
// StartProcess 启动进程
func (r *SupervisorController) StartProcess(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
tools.ExecShell(`supervisorctl start ` + process)
controllers.Success(ctx, nil)
}
// StopProcess 停止进程
func (r *SupervisorController) StopProcess(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
tools.ExecShell(`supervisorctl stop ` + process)
controllers.Success(ctx, nil)
}
// RestartProcess 重启进程
func (r *SupervisorController) RestartProcess(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
tools.ExecShell(`supervisorctl restart ` + process)
controllers.Success(ctx, nil)
}
// ProcessLog 进程日志
func (r *SupervisorController) ProcessLog(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
var logPath string
if tools.IsRHEL() {
logPath = tools.ExecShell(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
} else {
logPath = tools.ExecShell(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
}
log := tools.ExecShell(`tail -n 200 ` + logPath)
controllers.Success(ctx, log)
}
// ClearProcessLog 清空进程日志
func (r *SupervisorController) ClearProcessLog(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
var logPath string
if tools.IsRHEL() {
logPath = tools.ExecShell(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
} else {
logPath = tools.ExecShell(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
}
tools.ExecShell(`echo "" > ` + logPath)
controllers.Success(ctx, nil)
}
// ProcessConfig 获取进程配置
func (r *SupervisorController) ProcessConfig(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Query("process")
var config string
if tools.IsRHEL() {
config = tools.ReadFile(`/etc/supervisord.d/` + process + `.conf`)
} else {
config = tools.ReadFile(`/etc/supervisor/conf.d/` + process + `.conf`)
}
controllers.Success(ctx, config)
}
// SaveProcessConfig 保存进程配置
func (r *SupervisorController) SaveProcessConfig(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
config := ctx.Request().Input("config")
if tools.IsRHEL() {
tools.WriteFile(`/etc/supervisord.d/`+process+`.conf`, config, 0644)
} else {
tools.WriteFile(`/etc/supervisor/conf.d/`+process+`.conf`, config, 0644)
}
tools.ExecShell(`supervisorctl reread`)
tools.ExecShell(`supervisorctl update`)
tools.ExecShell(`supervisorctl start ` + process)
controllers.Success(ctx, nil)
}
// AddProcess 添加进程
func (r *SupervisorController) AddProcess(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|alpha_dash",
"user": "required|alpha_dash",
"path": "required",
"command": "required",
"num": "required",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
name := ctx.Request().Input("name")
user := ctx.Request().Input("user")
path := ctx.Request().Input("path")
command := ctx.Request().Input("command")
num := ctx.Request().InputInt("num", 1)
config := `[program:` + name + `]
command=` + command + `
process_name=%(program_name)s
directory=` + path + `
autostart=true
autorestart=true
user=` + user + `
numprocs=` + strconv.Itoa(num) + `
redirect_stderr=true
stdout_logfile=/var/log/supervisor/` + name + `.log
stdout_logfile_maxbytes=2MB
`
if tools.IsRHEL() {
tools.WriteFile(`/etc/supervisord.d/`+name+`.conf`, config, 0644)
} else {
tools.WriteFile(`/etc/supervisor/conf.d/`+name+`.conf`, config, 0644)
}
tools.ExecShell(`supervisorctl reread`)
tools.ExecShell(`supervisorctl update`)
tools.ExecShell(`supervisorctl start ` + name)
controllers.Success(ctx, nil)
}
// DeleteProcess 删除进程
func (r *SupervisorController) DeleteProcess(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
tools.ExecShell(`supervisorctl stop ` + process)
var logPath string
if tools.IsRHEL() {
logPath = tools.ExecShell(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
tools.RemoveFile(`/etc/supervisord.d/` + process + `.conf`)
} else {
logPath = tools.ExecShell(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
tools.RemoveFile(`/etc/supervisor/conf.d/` + process + `.conf`)
}
tools.RemoveFile(logPath)
tools.ExecShell(`supervisorctl reread`)
tools.ExecShell(`supervisorctl update`)
controllers.Success(ctx, nil)
}
+8 -8
View File
@@ -221,8 +221,8 @@ func (r *SafeController) SetSshPort(ctx http.Context) {
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" {
out := tools.ExecShell(`firewall-cmd --query-rich-rule='rule protocol value=icmp drop'`)
if strings.Contains(out, "no") {
Success(ctx, true)
} else {
Success(ctx, false)
@@ -240,18 +240,18 @@ func (r *SafeController) GetPingStatus(ctx http.Context) {
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'")
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 --permanent --add-rich-rule='rule protocol value=icmp drop'`)
}
tools.ExecShell("firewall-cmd --reload")
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")
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(`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")
tools.ExecShell(`ufw reload`)
}
Success(ctx, nil)
+164
View File
@@ -0,0 +1,164 @@
package controllers
import (
"bytes"
"context"
nethttp "net/http"
"sync"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/gorilla/websocket"
"panel/app/models"
"panel/app/services"
"panel/pkg/ssh"
)
type SshController struct {
AuthMethod ssh.AuthMethod
setting services.Setting
}
func NewSshController() *SshController {
return &SshController{
AuthMethod: ssh.PASSWORD,
setting: services.NewSettingImpl(),
}
}
func (r *SshController) GetInfo(ctx http.Context) {
host := r.setting.Get(models.SettingKeySshHost)
port := r.setting.Get(models.SettingKeySshPort)
user := r.setting.Get(models.SettingKeySshUser)
password := r.setting.Get(models.SettingKeySshPassword)
if len(host) == 0 || len(user) == 0 || len(password) == 0 {
Error(ctx, http.StatusInternalServerError, "SSH 配置不完整")
return
}
Success(ctx, http.Json{
"host": host,
"port": port,
"user": user,
"password": password,
})
}
func (r *SshController) UpdateInfo(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"host": "required",
"port": "required",
"user": "required",
"password": "required",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
host := ctx.Request().Input("host")
port := ctx.Request().Input("port")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
err = r.setting.Set(models.SettingKeySshHost, host)
if err != nil {
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
err = r.setting.Set(models.SettingKeySshPort, port)
if err != nil {
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
err = r.setting.Set(models.SettingKeySshUser, user)
if err != nil {
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
err = r.setting.Set(models.SettingKeySshPassword, password)
if err != nil {
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
func (r *SshController) Session(ctx http.Context) {
upGrader := websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(r *nethttp.Request) bool {
return true
},
Subprotocols: []string{ctx.Request().Header("Sec-WebSocket-Protocol")},
}
ws, err := upGrader.Upgrade(ctx.Response().Writer(), ctx.Request().Origin(), nil)
if err != nil {
facades.Log().Error("[面板][SSH] 建立连接失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
defer ws.Close()
config := ssh.SSHClientConfigPassword(
r.setting.Get(models.SettingKeySshHost)+":"+r.setting.Get(models.SettingKeySshPort),
r.setting.Get(models.SettingKeySshUser),
r.setting.Get(models.SettingKeySshPassword),
)
client, err := ssh.NewSSHClient(config)
if err != nil {
_ = ws.WriteControl(websocket.CloseMessage,
[]byte(err.Error()), time.Now().Add(time.Second))
return
}
defer client.Close()
turn, err := ssh.NewTurn(ws, client)
if err != nil {
_ = ws.WriteControl(websocket.CloseMessage,
[]byte(err.Error()), time.Now().Add(time.Second))
return
}
defer turn.Close()
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
var logBuff = bufPool.Get().(*bytes.Buffer)
logBuff.Reset()
defer bufPool.Put(logBuff)
ctx2, cancel := context.WithCancel(context.Background())
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
err := turn.LoopRead(logBuff, ctx2)
if err != nil {
facades.Log().Error("[面板][SSH] 读取数据失败 ", err.Error())
}
}()
go func() {
defer wg.Done()
err := turn.SessionWait()
if err != nil {
facades.Log().Error("[面板][SSH] 会话失败 ", err.Error())
}
cancel()
}()
wg.Wait()
}
+1 -1
View File
@@ -39,7 +39,7 @@ func (r *UserController) Login(ctx http.Context) {
}
if user.ID == 0 || !facades.Hash().Check(loginRequest.Password, user.Password) {
Error(ctx, http.StatusUnauthorized, "用户名或密码错误")
Error(ctx, http.StatusForbidden, "用户名或密码错误")
return
}
+90 -2
View File
@@ -28,6 +28,7 @@ func NewWebsiteController() *WebsiteController {
}
}
// List 网站列表
func (c *WebsiteController) List(ctx http.Context) {
limit := ctx.Request().QueryInt("limit")
page := ctx.Request().QueryInt("page")
@@ -45,6 +46,7 @@ func (c *WebsiteController) List(ctx http.Context) {
})
}
// Add 添加网站
func (c *WebsiteController) Add(ctx http.Context) {
if !Check(ctx, "openresty") {
return
@@ -53,7 +55,7 @@ func (c *WebsiteController) Add(ctx http.Context) {
"name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$",
"domain": "required",
"php": "required",
"db": "required",
"db": "bool",
"db_type": "required_if:db,true",
"db_name": "required_if:db,true",
"db_user": "required_if:db,true",
@@ -88,6 +90,7 @@ func (c *WebsiteController) Add(ctx http.Context) {
Success(ctx, newSite)
}
// Delete 删除网站
func (c *WebsiteController) Delete(ctx http.Context) {
if !Check(ctx, "openresty") {
return
@@ -103,6 +106,7 @@ func (c *WebsiteController) Delete(ctx http.Context) {
Success(ctx, nil)
}
// GetDefaultConfig 获取默认配置
func (c *WebsiteController) GetDefaultConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
@@ -116,6 +120,7 @@ func (c *WebsiteController) GetDefaultConfig(ctx http.Context) {
})
}
// SaveDefaultConfig 保存默认配置
func (c *WebsiteController) SaveDefaultConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
@@ -138,6 +143,7 @@ func (c *WebsiteController) SaveDefaultConfig(ctx http.Context) {
Success(ctx, nil)
}
// GetConfig 获取配置
func (c *WebsiteController) GetConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
@@ -158,6 +164,7 @@ func (c *WebsiteController) GetConfig(ctx http.Context) {
Success(ctx, config)
}
// SaveConfig 保存配置
func (c *WebsiteController) SaveConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
@@ -278,7 +285,7 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
Error(ctx, http.StatusBadRequest, "配置文件中root标记位格式错误")
return
}
rootNew := strings.Replace(root, match[1], path, -1)
rootNew := strings.Replace(root, match[1], ctx.Request().Input("root"), -1)
raw = strings.Replace(raw, root, rootNew, -1)
// 默认文件
@@ -393,6 +400,7 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
Success(ctx, nil)
}
// ClearLog 清空日志
func (c *WebsiteController) ClearLog(ctx http.Context) {
if !Check(ctx, "openresty") {
return
@@ -416,6 +424,7 @@ func (c *WebsiteController) ClearLog(ctx http.Context) {
Success(ctx, nil)
}
// UpdateRemark 更新备注
func (c *WebsiteController) UpdateRemark(ctx http.Context) {
id := ctx.Request().InputInt("id")
if id == 0 {
@@ -442,6 +451,7 @@ func (c *WebsiteController) UpdateRemark(ctx http.Context) {
Success(ctx, nil)
}
// BackupList 备份列表
func (c *WebsiteController) BackupList(ctx http.Context) {
backupList, err := c.backup.WebsiteList()
if err != nil {
@@ -453,6 +463,7 @@ func (c *WebsiteController) BackupList(ctx http.Context) {
Success(ctx, backupList)
}
// CreateBackup 创建备份
func (c *WebsiteController) CreateBackup(ctx http.Context) {
id := ctx.Request().InputInt("id")
if id == 0 {
@@ -478,6 +489,82 @@ func (c *WebsiteController) CreateBackup(ctx http.Context) {
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
@@ -569,6 +656,7 @@ server
Success(ctx, nil)
}
// Status 网站状态
func (c *WebsiteController) Status(ctx http.Context) {
if !Check(ctx, "openresty") {
return
+3 -3
View File
@@ -13,7 +13,7 @@ import (
// Jwt 确保通过 JWT 鉴权
func Jwt() http.Middleware {
return func(ctx http.Context) {
token := ctx.Request().Header("access_token", ctx.Request().Input("access_token", ""))
token := ctx.Request().Header("access_token", ctx.Request().Input("access_token", ctx.Request().Header("Sec-WebSocket-Protocol")))
if len(token) == 0 {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"code": 401,
@@ -28,7 +28,7 @@ func Jwt() http.Middleware {
token, err = facades.Auth().Refresh(ctx)
if err != nil {
// Refresh time exceeded
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": 401,
"message": "登录已过期",
})
@@ -37,7 +37,7 @@ func Jwt() http.Middleware {
token = "Bearer " + token
} else {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": 401,
"message": "登录已过期",
})
+4
View File
@@ -10,6 +10,10 @@ const (
SettingKeyWebsitePath = "website_path"
SettingKeyEntrance = "entrance"
SettingKeyMysqlRootPassword = "mysql_root_password"
SettingKeySshHost = "ssh_host"
SettingKeySshPort = "ssh_port"
SettingKeySshUser = "ssh_user"
SettingKeySshPassword = "ssh_password"
)
type Setting struct {
-1
View File
@@ -2,7 +2,6 @@ package mysql57
var (
Name = "MySQL-5.7"
Author = "耗子"
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。"
Slug = "mysql57"
Version = "5.7.42"
-1
View File
@@ -2,7 +2,6 @@ package mysql80
var (
Name = "MySQL-8.0"
Author = "耗子"
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。(内存 < 4G 无法安装)"
Slug = "mysql80"
Version = "8.0.33"
-1
View File
@@ -2,7 +2,6 @@ package openresty
var (
Name = "OpenResty"
Author = "耗子"
Description = "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台。"
Slug = "openresty"
Version = "1.21.4.1"
-1
View File
@@ -2,7 +2,6 @@ package php74
var (
Name = "PHP-7.4"
Author = "耗子"
Description = "PHP 是世界上最好的语言!"
Slug = "php74"
Version = "7.4.33"
-1
View File
@@ -2,7 +2,6 @@ package php80
var (
Name = "PHP-8.0"
Author = "耗子"
Description = "PHP 是世界上最好的语言!"
Slug = "php80"
Version = "8.0.29"
+13
View File
@@ -0,0 +1,13 @@
package php81
var (
Name = "PHP-8.1"
Description = "PHP 是世界上最好的语言!"
Slug = "php81"
Version = "8.1.21"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/php/install.sh 81`
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 81`
Update = `bash /www/panel/scripts/php/install.sh 81`
)
+13
View File
@@ -0,0 +1,13 @@
package php82
var (
Name = "PHP-8.2"
Description = "PHP 是世界上最好的语言!"
Slug = "php82"
Version = "8.2.8"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/php/install.sh 82`
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 82`
Update = `bash /www/panel/scripts/php/install.sh 82`
)
-1
View File
@@ -2,7 +2,6 @@ package phpmyadmin
var (
Name = "phpMyAdmin"
Author = "耗子"
Description = "phpMyAdmin 是一个以 PHP 为基础,以 Web-Base 方式架构在网站主机上的 MySQL 数据库管理工具。"
Slug = "phpmyadmin"
Version = "5.2.1"
+13
View File
@@ -0,0 +1,13 @@
package s3fs
var (
Name = "S3fs"
Description = "S3fs 通过 FUSE 挂载兼容 S3 标准的存储桶,例如Amazon S3、阿里云OSS、腾讯云COS、七牛云Kodo等。"
Slug = "s3fs"
Version = "1.9"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/s3fs/install.sh`
Uninstall = `bash /www/panel/scripts/s3fs/uninstall.sh`
Update = `bash /www/panel/scripts/s3fs/install.sh`
)
+13
View File
@@ -0,0 +1,13 @@
package supervisor
var (
Name = "Supervisor"
Description = "Supervisor 是一个客户端/服务器系统,允许用户监视和控制类 UNIX 操作系统上的多个进程。"
Slug = "supervisor"
Version = "4.2.5"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/supervisor/install.sh`
Uninstall = `bash /www/panel/scripts/supervisor/uninstall.sh`
Update = `bash /www/panel/scripts/supervisor/install.sh`
)
+144 -6
View File
@@ -4,6 +4,8 @@ package services
import (
"errors"
"os"
"path/filepath"
"strings"
"github.com/goravel/framework/support/carbon"
@@ -14,6 +16,10 @@ import (
type Backup interface {
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 {
@@ -31,15 +37,19 @@ func NewBackupImpl() *BackupImpl {
}
}
// WebsiteList 网站备份列表
func (s *BackupImpl) WebsiteList() ([]BackupFile, error) {
path := s.setting.Get(models.SettingKeyBackupPath)
if len(path) == 0 {
backupPath := s.setting.Get(models.SettingKeyBackupPath)
if len(backupPath) == 0 {
return []BackupFile{}, nil
}
path += "/website"
backupPath += "/website"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
files, err := os.ReadDir(path)
files, err := os.ReadDir(backupPath)
if err != nil {
return []BackupFile{}, err
}
@@ -58,6 +68,7 @@ func (s *BackupImpl) WebsiteList() ([]BackupFile, error) {
return backupList, nil
}
// WebSiteBackup 网站备份
func (s *BackupImpl) WebSiteBackup(website models.Website) error {
backupPath := s.setting.Get(models.SettingKeyBackupPath)
if len(backupPath) == 0 {
@@ -69,8 +80,135 @@ func (s *BackupImpl) WebSiteBackup(website models.Website) error {
tools.Mkdir(backupPath, 0644)
}
backupFile := backupPath + "/" + website.Name + carbon.Now().ToShortDateTimeString() + ".zip"
tools.ExecShell("cd " + website.Path + " && zip -r " + backupFile + " .")
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
}
+4
View File
@@ -23,8 +23,10 @@ func NewCronImpl() *CronImpl {
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")
tools.ExecShell("systemctl restart crond")
} else {
tools.ExecShell("echo \"" + cron.Time + " " + cron.Shell + " >> " + cron.Log + " 2>&1\" >> /var/spool/cron/crontabs/root")
tools.ExecShell("systemctl restart cron")
}
}
@@ -34,7 +36,9 @@ func (r *CronImpl) DeleteFromSystem(cron models.Cron) {
cron.Shell = strings.ReplaceAll(cron.Shell, "/", "\\/")
if tools.IsRHEL() {
tools.ExecShell("sed -i '/" + cron.Shell + "/d' /var/spool/cron/root")
tools.ExecShell("systemctl restart crond")
} else {
tools.ExecShell("sed -i '/" + cron.Shell + "/d' /var/spool/cron/crontabs/root")
tools.ExecShell("systemctl restart cron")
}
}
+48 -7
View File
@@ -10,13 +10,16 @@ import (
"panel/app/plugins/openresty"
"panel/app/plugins/php74"
"panel/app/plugins/php80"
"panel/app/plugins/php81"
"panel/app/plugins/php82"
"panel/app/plugins/phpmyadmin"
"panel/app/plugins/s3fs"
"panel/app/plugins/supervisor"
)
// PanelPlugin 插件元数据结构
type PanelPlugin struct {
Name string
Author string
Description string
Slug string
Version string
@@ -57,7 +60,6 @@ func (r *PluginImpl) All() []PanelPlugin {
p = append(p, PanelPlugin{
Name: openresty.Name,
Author: openresty.Author,
Description: openresty.Description,
Slug: openresty.Slug,
Version: openresty.Version,
@@ -69,7 +71,6 @@ func (r *PluginImpl) All() []PanelPlugin {
})
p = append(p, PanelPlugin{
Name: mysql57.Name,
Author: mysql57.Author,
Description: mysql57.Description,
Slug: mysql57.Slug,
Version: mysql57.Version,
@@ -81,7 +82,6 @@ func (r *PluginImpl) All() []PanelPlugin {
})
p = append(p, PanelPlugin{
Name: mysql80.Name,
Author: mysql80.Author,
Description: mysql80.Description,
Slug: mysql80.Slug,
Version: mysql80.Version,
@@ -93,7 +93,6 @@ func (r *PluginImpl) All() []PanelPlugin {
})
p = append(p, PanelPlugin{
Name: php74.Name,
Author: php74.Author,
Description: php74.Description,
Slug: php74.Slug,
Version: php74.Version,
@@ -105,7 +104,6 @@ func (r *PluginImpl) All() []PanelPlugin {
})
p = append(p, PanelPlugin{
Name: php80.Name,
Author: php80.Author,
Description: php80.Description,
Slug: php80.Slug,
Version: php80.Version,
@@ -115,9 +113,30 @@ func (r *PluginImpl) All() []PanelPlugin {
Uninstall: php80.Uninstall,
Update: php80.Update,
})
p = append(p, PanelPlugin{
Name: php81.Name,
Description: php81.Description,
Slug: php81.Slug,
Version: php81.Version,
Requires: php81.Requires,
Excludes: php81.Excludes,
Install: php81.Install,
Uninstall: php81.Uninstall,
Update: php81.Update,
})
p = append(p, PanelPlugin{
Name: php82.Name,
Description: php82.Description,
Slug: php82.Slug,
Version: php82.Version,
Requires: php82.Requires,
Excludes: php82.Excludes,
Install: php82.Install,
Uninstall: php82.Uninstall,
Update: php82.Update,
})
p = append(p, PanelPlugin{
Name: phpmyadmin.Name,
Author: phpmyadmin.Author,
Description: phpmyadmin.Description,
Slug: phpmyadmin.Slug,
Version: phpmyadmin.Version,
@@ -127,6 +146,28 @@ func (r *PluginImpl) All() []PanelPlugin {
Uninstall: phpmyadmin.Uninstall,
Update: phpmyadmin.Update,
})
p = append(p, PanelPlugin{
Name: s3fs.Name,
Description: s3fs.Description,
Slug: s3fs.Slug,
Version: s3fs.Version,
Requires: s3fs.Requires,
Excludes: s3fs.Excludes,
Install: s3fs.Install,
Uninstall: s3fs.Uninstall,
Update: s3fs.Update,
})
p = append(p, PanelPlugin{
Name: supervisor.Name,
Description: supervisor.Description,
Slug: supervisor.Slug,
Version: supervisor.Version,
Requires: supervisor.Requires,
Excludes: supervisor.Excludes,
Install: supervisor.Install,
Uninstall: supervisor.Uninstall,
Update: supervisor.Update,
})
return p
}
+1 -1
View File
@@ -348,7 +348,7 @@ func (r *WebsiteImpl) GetConfig(id int) (WebsiteSetting, error) {
}
setting.Rewrite = tools.ReadFile("/www/server/vhost/rewrite/" + website.Name + ".conf")
setting.Log = tools.ExecShell("tail -n 100 /www/wwwlogs/" + website.Name + ".log")
setting.Log = tools.ExecShell(`tail -n 100 '/www/wwwlogs/` + website.Name + `.log'`)
return setting, nil
}
+2
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"
@@ -77,6 +78,7 @@ func init() {
&auth.ServiceProvider{},
&hash.ServiceProvider{},
&crypt.ServiceProvider{},
&filesystem.ServiceProvider{},
&validation.ServiceProvider{},
&providers.AppServiceProvider{},
&providers.AuthServiceProvider{},
+1 -1
View File
@@ -24,7 +24,7 @@ func init() {
"disks": map[string]any{
"local": map[string]any{
"driver": "local",
"root": "storage/app",
"root": "/",
"url": "http://localhost/",
},
},
+1 -1
View File
@@ -8,6 +8,6 @@ func init() {
config := facades.Config()
config.Add("panel", map[string]any{
"name": "耗子面板",
"version": "v2.0.12",
"version": "v2.0.22",
})
}
+8 -6
View File
@@ -3,16 +3,19 @@ module panel
go 1.18
require (
github.com/bytedance/sonic v1.9.2
github.com/gertd/go-pluralize v0.2.1
github.com/gin-contrib/static v0.0.1
github.com/gookit/color v1.5.4
github.com/goravel/framework v1.12.1-0.20230721095426-6f24ecdaf6a7
github.com/goravel/framework v1.12.1-0.20230726100652-342097efc4ce
github.com/gorilla/websocket v1.5.0
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/crypto v0.11.0
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
)
@@ -34,7 +37,6 @@ require (
github.com/RichardKnop/machinery/v2 v2.0.11 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/aws/aws-sdk-go v1.37.16 // indirect
github.com/bytedance/sonic v1.9.2 // indirect
github.com/cenkalti/backoff/v4 v4.2.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
@@ -66,6 +68,7 @@ require (
github.com/goccy/go-json v0.10.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
github.com/golang-migrate/migrate/v4 v4.16.2 // indirect
github.com/golang-module/carbon/v2 v2.2.3 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
@@ -84,9 +87,9 @@ require (
github.com/google/wire v0.5.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.8.0 // indirect
github.com/gookit/filter v1.1.4 // indirect
github.com/gookit/goutil v0.5.15 // indirect
github.com/gookit/validate v1.4.6 // indirect
github.com/gookit/filter v1.2.0 // indirect
github.com/gookit/goutil v0.6.12 // indirect
github.com/gookit/validate v1.5.0 // indirect
github.com/goravel/file-rotatelogs v0.0.0-20211215053220-2ab31dd9575c // indirect
github.com/goravel/file-rotatelogs/v2 v2.4.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
@@ -161,7 +164,6 @@ require (
go.opencensus.io v0.24.0 // indirect
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/image v0.0.0-20190802002840-cff245a6509b // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.11.0 // indirect
+12 -16
View File
@@ -244,6 +244,8 @@ github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA=
github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o=
github.com/golang-module/carbon/v2 v2.2.3 h1:WvGIc5+qzq9drNzH+Gnjh1TZ0JgDY/IA+m2Dvk7Qm4Q=
@@ -343,24 +345,24 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/googleapis/gax-go/v2 v2.8.0 h1:UBtEZqx1bjXtOQ5BVTkuYghXrr3N4V123VKJK67vJZc=
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.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=
github.com/gookit/goutil v0.5.15 h1:FaRyj0uVqi7j92QHsG+2Sc1VZ7/7ma77UD3/wBpwyTc=
github.com/gookit/goutil v0.5.15/go.mod h1:ozPE16eJS9f89aVbVk05ocEJsia3KPrYUqPTs8GvUTw=
github.com/gookit/validate v1.4.6 h1:Ix8NRy2+6z4YGHWXgZL9+emy9wRI2GWyhW2smPcIlSU=
github.com/gookit/validate v1.4.6/go.mod h1:1rjeYaYlMK/8od4oge5C+Gt/3DnHkXymLPda7+3urC8=
github.com/gookit/filter v1.2.0 h1:r7E01dHVkysb5WgzooiGsfblHGShEZCeGcyYM+5IpYU=
github.com/gookit/filter v1.2.0/go.mod h1:bXs9RcB4Blxwny970opiwABeIEqQ/gzOMmHBhKwBdms=
github.com/gookit/goutil v0.6.12 h1:73vPUcTtVGXbhSzBOFcnSB1aJl7Jq9np3RAE50yIDZc=
github.com/gookit/goutil v0.6.12/go.mod h1:g6krlFib8xSe3G1h02IETowOtrUGpAmetT8IevDpvpM=
github.com/gookit/validate v1.5.0 h1:aTy974I1vPDYPFzoFH4vtJONrK4oRDvjUxteUan7beE=
github.com/gookit/validate v1.5.0/go.mod h1:SskOHUQokzMNt6T3r7N+N/4me/6fxDx+tmoXf/3ZQog=
github.com/goravel/file-rotatelogs v0.0.0-20211215053220-2ab31dd9575c h1:obhFK91JAhcf7s6h5sggZishm1VyGW/gBCreo+7/SwQ=
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.20230721095426-6f24ecdaf6a7 h1:2rkC/6M7tLx/Ya1TulO2GjtVRIGXTmpp0XppzZRloYA=
github.com/goravel/framework v1.12.1-0.20230721095426-6f24ecdaf6a7/go.mod h1:1lxwtCXMkotSmt0YWRg/s7EnMUFfmne9L1a50X9J0fA=
github.com/goravel/framework v1.12.1-0.20230726100652-342097efc4ce h1:1qgkBySstIMAkzRjbrjPjF6J0UIKBfXY1osSfL7azNY=
github.com/goravel/framework v1.12.1-0.20230726100652-342097efc4ce/go.mod h1:qLv6cmf4RqXkTZ782H38cY/7vhkFLamPV9pAU0s7nKQ=
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/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
@@ -456,7 +458,6 @@ github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3v
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microsoft/go-mssqldb v1.1.0 h1:jsV+tpvcPTbNNKW0o3kiCD69kOHICsfjZ2VcVu2lKYc=
@@ -632,7 +633,6 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
@@ -687,7 +687,6 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
@@ -872,8 +871,6 @@ golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -883,7 +880,6 @@ golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+72
View File
@@ -0,0 +1,72 @@
package ssh
import (
"time"
"golang.org/x/crypto/ssh"
"panel/pkg/tools"
)
type AuthMethod int8
const (
PASSWORD AuthMethod = iota + 1
PUBLICKEY
)
type SSHClientConfig struct {
AuthMethod AuthMethod
HostAddr string
User string
Password string
KeyPath string
Timeout time.Duration
}
func SSHClientConfigPassword(hostAddr, user, Password string) *SSHClientConfig {
return &SSHClientConfig{
Timeout: time.Second * 5,
AuthMethod: PASSWORD,
HostAddr: hostAddr,
User: user,
Password: Password,
}
}
func SSHClientConfigPulicKey(hostAddr, user, keyPath string) *SSHClientConfig {
return &SSHClientConfig{
Timeout: time.Second * 5,
AuthMethod: PUBLICKEY,
HostAddr: hostAddr,
User: user,
KeyPath: keyPath,
}
}
func NewSSHClient(conf *SSHClientConfig) (*ssh.Client, error) {
config := &ssh.ClientConfig{
Timeout: conf.Timeout,
User: conf.User,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
switch conf.AuthMethod {
case PASSWORD:
config.Auth = []ssh.AuthMethod{ssh.Password(conf.Password)}
case PUBLICKEY:
signer, err := getKey(conf.KeyPath)
if err != nil {
return nil, err
}
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
}
c, err := ssh.Dial("tcp", conf.HostAddr, config)
if err != nil {
return nil, err
}
return c, nil
}
func getKey(keyPath string) (ssh.Signer, error) {
return ssh.ParsePrivateKey([]byte(tools.ReadFile(keyPath)))
}
+139
View File
@@ -0,0 +1,139 @@
package ssh
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"github.com/gorilla/websocket"
"golang.org/x/crypto/ssh"
)
const (
MsgData = '1'
MsgResize = '2'
)
type Turn struct {
StdinPipe io.WriteCloser
Session *ssh.Session
WsConn *websocket.Conn
}
func NewTurn(wsConn *websocket.Conn, sshClient *ssh.Client) (*Turn, error) {
sess, err := sshClient.NewSession()
if err != nil {
return nil, err
}
stdinPipe, err := sess.StdinPipe()
if err != nil {
return nil, err
}
turn := &Turn{StdinPipe: stdinPipe, Session: sess, WsConn: wsConn}
sess.Stdout = turn
sess.Stderr = turn
modes := ssh.TerminalModes{
ssh.ECHO: 1,
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err := sess.RequestPty("xterm", 150, 30, modes); err != nil {
return nil, err
}
if err := sess.Shell(); err != nil {
return nil, err
}
return turn, nil
}
func (t *Turn) Write(p []byte) (n int, err error) {
writer, err := t.WsConn.NextWriter(websocket.BinaryMessage)
if err != nil {
return 0, err
}
defer writer.Close()
return writer.Write(p)
}
func (t *Turn) Close() error {
if t.Session != nil {
t.Session.Close()
}
return t.WsConn.Close()
}
func (t *Turn) Read(p []byte) (n int, err error) {
for {
msgType, reader, err := t.WsConn.NextReader()
if err != nil {
return 0, err
}
if msgType != websocket.BinaryMessage {
continue
}
return reader.Read(p)
}
}
func (t *Turn) LoopRead(logBuff *bytes.Buffer, context context.Context) error {
for {
select {
case <-context.Done():
return errors.New("LoopRead exit")
default:
_, wsData, err := t.WsConn.ReadMessage()
if err != nil {
return fmt.Errorf("reading webSocket message err:%s", err)
}
body := decode(wsData[1:])
switch wsData[0] {
case MsgResize:
var args Resize
err := json.Unmarshal(body, &args)
if err != nil {
return fmt.Errorf("ssh pty resize windows err:%s", err)
}
if args.Columns > 0 && args.Rows > 0 {
if err := t.Session.WindowChange(args.Rows, args.Columns); err != nil {
return fmt.Errorf("ssh pty resize windows err:%s", err)
}
}
case MsgData:
if _, err := t.StdinPipe.Write(body); err != nil {
return fmt.Errorf("StdinPipe write err:%s", err)
}
if _, err := logBuff.Write(body); err != nil {
return fmt.Errorf("logBuff write err:%s", err)
}
}
}
}
}
func (t *Turn) SessionWait() error {
if err := t.Session.Wait(); err != nil {
return err
}
return nil
}
func decode(p []byte) []byte {
decodeString, _ := base64.StdEncoding.DecodeString(string(p))
return decodeString
}
type Resize struct {
Columns int
Rows int
}
-26
View File
@@ -5,36 +5,10 @@ import (
"crypto/rand"
"fmt"
"io"
"reflect"
"strings"
"unicode/utf8"
)
// Empty 类似于 PHP 的 empty() 函数
func Empty(val interface{}) bool {
if val == nil {
return true
}
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.String, reflect.Array:
return v.Len() == 0
case reflect.Map, reflect.Slice:
return v.Len() == 0 || v.IsNil()
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return reflect.DeepEqual(val, reflect.Zero(v.Type()).Interface())
}
// FirstElement 安全地获取 args[0],避免 panic: runtime error: index out of range
func FirstElement(args []string) string {
if len(args) > 0 {
-17
View File
@@ -14,23 +14,6 @@ func TestStringHelperTestSuite(t *testing.T) {
suite.Run(t, &StringHelperTestSuite{})
}
func (s *StringHelperTestSuite) TestEmpty() {
s.True(Empty(""))
s.True(Empty(nil))
s.True(Empty([]string{}))
s.True(Empty(map[string]string{}))
s.True(Empty(0))
s.True(Empty(0.0))
s.True(Empty(false))
s.False(Empty(" "))
s.False(Empty([]string{"Panel"}))
s.False(Empty(map[string]string{"Panel": "HaoZi"}))
s.False(Empty(1))
s.False(Empty(1.0))
s.False(Empty(true))
}
func (s *StringHelperTestSuite) TestFirstElement() {
s.Equal("HaoZi", FirstElement([]string{"HaoZi"}))
}
+36
View File
@@ -114,3 +114,39 @@ func Exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// Empty 判断路径是否为空
func Empty(path string) bool {
files, err := os.ReadDir(path)
if err != nil {
return true
}
return len(files) == 0
}
// Mv 移动路径
func Mv(src, dst string) bool {
cmd := exec.Command("mv", src, dst)
err := cmd.Run()
if err != nil {
facades.Log().Errorf("[面板][Helpers] 移动 %s 到 %s 失败: %s", src, dst, err.Error())
return false
}
return true
}
// Cp 复制路径
func Cp(src, dst string) bool {
cmd := exec.Command("cp", "-r", src, dst)
err := cmd.Run()
if err != nil {
facades.Log().Errorf("[面板][Helpers] 复制 %s 到 %s 失败: %s", src, dst, err.Error())
return false
}
return true
}
+5
View File
@@ -99,3 +99,8 @@ func (s *SystemHelperTestSuite) TestExists() {
s.True(Exists("/tmp"))
s.False(Exists("/tmp/123"))
}
func (s *SystemHelperTestSuite) TestEmpty() {
s.True(Empty("/tmp/123"))
s.False(Empty("/tmp"))
}
+1 -1
View File
@@ -107,7 +107,7 @@ func GetLatestPanelVersion() (PanelInfo, error) {
downloadUrlOutput, _ := downloadUrl.Output()
info.DownloadUrl = strings.TrimSpace(string(downloadUrlOutput))
} else {
downloadUrl := exec.Command("/bin/bash", "-c", "jq -r '.assets[] | select(.name | contains(\"amd64v3\")) | .browser_download_url' "+fileName)
downloadUrl := exec.Command("/bin/bash", "-c", "jq -r '.assets[] | select(.name | contains(\"amd64v2\")) | .browser_download_url' "+fileName)
downloadUrlOutput, _ := downloadUrl.Output()
info.DownloadUrl = strings.TrimSpace(string(downloadUrlOutput))
}
+6 -3
View File
@@ -20,7 +20,7 @@
// `=---=' //
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
// 佛祖保佑 永无Bug 永不宕机 //
// Name: 耗子Linux面板 Author: 耗子 Date: 2023-06-22 //
// Name: 耗子Linux面板 Author: 耗子 Date: 2023-07-25 //
////////////////////////////////////////////////////////////////////
-->
@@ -32,13 +32,16 @@
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdnjs.cdn.haozi.net/layui/2.8.8/css/layui.min.css" rel="stylesheet">
<link href="https://cdnjs.cdn.haozi.net/layui/2.8.11/css/layui.min.css" rel="stylesheet">
</head>
<body>
<div id="Panel_app"></div>
<script src="https://cdnjs.cdn.haozi.net/layui/2.8.8/layui.min.js"></script>
<script src="https://cdnjs.cdn.haozi.net/layui/2.8.11/layui.min.js"></script>
<script src="https://cdnjs.cdn.haozi.net/ace/1.6.1/ace.js"></script>
<script src="https://cdnjs.cdn.haozi.net/echarts/5.4.2/echarts.min.js"></script>
<script src="https://registry.npmmirror.com/xterm/5.2.1/files/lib/xterm.js"></script>
<script src="https://registry.npmmirror.com/xterm-addon-fit/0.7.0/files/lib/xterm-addon-fit.js"></script>
<script src="https://cdnjs.cdn.haozi.net/crypto-js/4.1.1/crypto-js.min.js"></script>
<script>
layui.config({
base: 'panel/', // 静态资源所在路径
File diff suppressed because it is too large Load Diff
+130 -11
View File
@@ -9,8 +9,22 @@ Date: 2023-07-21
<div class="layui-card">
<div class="layui-card-header">添加计划任务</div>
<div class="layui-card-body">
<blockquote class="layui-elem-quote">
<p>面板的计划任务均基于脚本运行,若任务类型满足不了需求,可自行修改对应的脚本。</p>
</blockquote>
<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">
<select name="type" lay-verify="required" lay-filter="cron-type">
<option value="shell">运行脚本</option>
<option value="backup">备份数据</option>
<option value="cutoff">切割日志</option>
</select>
</div>
<div class="layui-form-mid layui-word-aux">请选择任务类型</div>
</div>
<div class="layui-form-item" id="cron-add-name-input">
<label class="layui-form-label">任务名</label>
<div class="layui-input-inline">
<input type="text" name="name" lay-verify="required" placeholder="请输入任务名称"
@@ -18,6 +32,61 @@ Date: 2023-07-21
</div>
<div class="layui-form-mid layui-word-aux">请填写任务名称</div>
</div>
<div class="layui-form-item" id="cron-add-backup-type-input" style="display: none;">
<label class="layui-form-label">备份类型</label>
<div class="layui-input-block">
<input type="radio" lay-filter="cron-add-backup-type-radio" name="backup_type" value="website"
title="网站目录" checked="">
<input type="radio" lay-filter="cron-add-backup-type-radio" name="backup_type" value="mysql"
title="MySQL数据库">
<input type="radio" lay-filter="cron-add-backup-type-radio" name="backup_type"
value="postgresql"
title="PostgreSQL数据库">
</div>
</div>
<div class="layui-form-item" id="cron-add-website-input" style="display: none;">
<label class="layui-form-label">网站</label>
<div class="layui-input-inline">
<script type="text/html" template lay-url="/api/panel/website/list?page=1&limit=10000">
<select name="website" lay-filter="cron-add-website">
{{# layui.each(d.data.items, function(index, item){ }}
{{# if(index == 0){ }}
<option value="{{ item.name }}" selected="">{{ item.name }}</option>
{{# }else{ }}
<option value="{{ item.name }}">{{ item.name }}</option>
{{# } }}
{{# }); }}
</select>
</script>
</div>
<div class="layui-form-mid layui-word-aux">请选择网站</div>
</div>
<div class="layui-form-item" id="cron-add-backup-database-input" style="display: none;">
<label class="layui-form-label">数据库名</label>
<div class="layui-input-inline">
<input type="text" name="backup_database" placeholder="请输入数据库名"
autocomplete="off" class="layui-input">
</div>
<div class="layui-form-mid layui-word-aux">请填写数据库名</div>
</div>
<div class="layui-form-item" id="cron-add-backup-path-input" style="display: none;">
<label class="layui-form-label">保存目录</label>
<div class="layui-input-inline">
<input type="text" name="backup_path" placeholder=""
autocomplete="off" class="layui-input">
</div>
<div class="layui-form-mid layui-word-aux">
目录可以为对象存储挂载目录,不填则默认为面板设置的备份目录
</div>
</div>
<div class="layui-form-item" id="cron-add-save-input" style="display: none;">
<label class="layui-form-label">保留份数</label>
<div class="layui-input-inline">
<input type="number" name="save" placeholder="10"
autocomplete="off" class="layui-input" min="0" step="1" lay-affix="number">
</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">
@@ -26,7 +95,7 @@ Date: 2023-07-21
</div>
<div class="layui-form-mid layui-word-aux">请务必正确填写执行周期</div>
</div>
<div class="layui-form-item layui-form-text">
<div class="layui-form-item layui-form-text" id="cron-add-shell">
<label class="layui-form-label">脚本内容</label>
<div class="layui-input-block">
<div id="cron-add-script-editor"
@@ -52,13 +121,22 @@ Date: 2023-07-21
<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-type">
{{# if(d.type == 'shell'){ }}
运行脚本
{{# }else if(d.type == 'backup'){ }}
备份数据
{{# }else if(d.type == 'cutoff'){ }}
切割日志
{{# } }}
</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' : '' }}>
{{ d.status ? 'checked' : '' }}>
</script>
</div>
</div>
@@ -94,10 +172,11 @@ Date: 2023-07-21
, cols: [[
{field: 'id', hide: true, title: 'ID'}
, {field: 'name', width: 150, title: '任务名', sort: true}
, {field: 'type', width: 150, title: '任务类型', sort: true}
, {field: 'type', width: 150, title: '任务类型', templet: '#cron-table-type', sort: true}
, {field: 'status', title: '启用', width: 100, templet: '#cron-table-status', unresize: true}
, {field: 'time', width: 200, title: '任务周期(cron表达式)'}
, {field: 'updated_at', title: '上次运行时间'}
, {field: 'created_at', title: '创建时间'}
, {field: 'updated_at', title: '最后更新时间'}
, {
field: 'edit',
width: 180,
@@ -120,14 +199,12 @@ Date: 2023-07-21
};
}
});
// 工具条
table.on('tool(panel-cron)', function (obj) {
let data = obj.data;
if (obj.event === 'log') {
// 打开日志弹窗
admin.popup({
title: '日志'
title: '日志 - ' + data.name
,
area: ['80%', '80%']
,
@@ -136,10 +213,12 @@ Date: 2023-07-21
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) {
index2 = layer.msg('正在获取日志...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: '/api/panel/cron/log?id=' + data.id
, type: 'GET'
, success: function (res) {
layer.close(index2);
if (res.code === 0) {
$('#cron-log-view').html(res.data);
} else {
@@ -158,7 +237,7 @@ Date: 2023-07-21
, success: function (res) {
if (res.code === 0) {
admin.popup({
title: '编辑'
title: '编辑 - ' + data.name
,
area: ['80%', '80%']
,
@@ -180,6 +259,7 @@ Date: 2023-07-21
}
});
$('#cron-edit-' + data.id).click(function () {
index2 = layer.msg('正在保存...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: '/api/panel/cron/update'
, type: 'POST'
@@ -193,6 +273,7 @@ Date: 2023-07-21
if (res.code === 0) {
layer.msg('保存成功', {icon: 1, time: 1000});
table.reload('panel-cron');
layer.close(index2);
layer.close(index);
} else {
layer.msg(res.message, {icon: 2, time: 1000});
@@ -233,6 +314,46 @@ Date: 2023-07-21
}
});
form.on('select(cron-type)', function (data) {
if (data.value === 'shell') {
$("#cron-add-backup-type-input").hide();
$("#cron-add-save-input").hide();
$('#cron-add-website-input').hide();
$('#cron-add-backup-database-input').hide();
$('#cron-add-backup-path-input').hide();
$("#cron-add-shell").show();
} else if (data.value === 'backup') {
let selectedType = $('input[name="backup_type"]:checked').val();
if (selectedType === 'website') {
$('#cron-add-website-input').show();
$('#cron-add-backup-database-input').hide();
} else {
$('#cron-add-website-input').hide();
$('#cron-add-backup-database-input').show();
}
$("#cron-add-backup-type-input").show();
$('#cron-add-backup-path-input').show();
$("#cron-add-save-input").show();
$("#cron-add-shell").hide();
} else if (data.value === 'cutoff') {
$('#cron-add-website-input').show();
$('#cron-add-save-input').show();
$("#cron-add-shell").hide();
$('#cron-add-backup-database-input').hide();
$("#cron-add-backup-type-input").hide();
$('#cron-add-backup-path-input').hide();
}
});
form.on('radio(cron-add-backup-type-radio)', function (data) {
if (data.value == 'website') {
$('#cron-add-website-input').show();
$('#cron-add-backup-database-input').hide();
} else {
$('#cron-add-website-input').hide();
$('#cron-add-backup-database-input').show();
}
});
form.on('switch(cron-status)', function (obj) {
let $ = layui.$;
let id = $(this).data('id');
@@ -263,7 +384,6 @@ Date: 2023-07-21
, data: data.field
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:计划任务添加失败,接口返回' + result);
layer.msg('计划任务添加失败!')
return false;
}
@@ -274,7 +394,6 @@ Date: 2023-07-21
, btn: ['确定']
, yes: function (index) {
layer.closeAll();
//location.reload();
}
});
}
+175 -161
View File
@@ -126,6 +126,7 @@ Date: 2023-06-22
<td>操作</td>
<td>
<button id="update_panel" class="layui-btn layui-btn-xs">更新</button>
<button id="restart_panel" class="layui-btn layui-btn-xs">重启</button>
</td>
</tr>
</tbody>
@@ -178,182 +179,195 @@ Date: 2023-06-22
</div>
<script>
var uptime_1 = '获取中', uptime_5 = '获取中', uptime_15 = '获取中'
var net_send = 0, net_recv = 0, net_send_last = 0, net_recv_last = 0
var uptime_1 = '获取中', uptime_5 = '获取中', uptime_15 = '获取中'
var net_send = 0, net_recv = 0, net_send_last = 0, net_recv_last = 0
function formatBytes (size) {
size = Number(size)
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
var i = 0
function formatBytes(size) {
size = Number(size)
var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
var i = 0
while (size >= 1024 && i < units.length) {
size /= 1024
i++
while (size >= 1024 && i < units.length) {
size /= 1024
i++
}
return size.toFixed(2) + ' ' + units[i]
}
return size.toFixed(2) + ' ' + units[i]
}
function formatPercentage(num) {
num = Number(num)
return num.toFixed(2) + '%'
}
function formatPercentage (num) {
num = Number(num)
return num.toFixed(2) + '%'
}
function refresh_home_info(first = false) {
layui.use(['index', 'jquery', 'admin'], function () {
let $ = layui.jquery
, admin = layui.admin
, element = layui.element
function refresh_home_info (first = false) {
layui.use(['index', 'jquery', 'admin'], function () {
let $ = layui.jquery
, admin = layui.admin
, element = layui.element
let device = layui.device()
let cpu_info
admin.req({
url: '/api/panel/info/nowMonitor'
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
layer.msg('系统资源获取失败,请刷新重试!')
return false
}
element.progress('home_cpu', formatPercentage(result.data.percent[0]))
element.progress('home_mem', formatPercentage(result.data.mem.usedPercent))
let device = layui.device()
let cpu_info
admin.req({
url: '/api/panel/info/nowMonitor'
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
layer.msg('系统资源获取失败,请刷新重试!')
return false
}
element.progress('home_cpu', formatPercentage(result.data.percent[0]))
element.progress('home_mem', formatPercentage(result.data.mem.usedPercent))
// 计算核心数
let cores = 0
for (let i = 0; i < result.data.cpus.length; i++) {
cores += result.data.cpus[i].cores
}
// 计算核心数
let cores = 0
for (let i = 0; i < result.data.cpus.length; i++) {
cores += result.data.cpus[i].cores
}
// 计算负载百分比
uptime_1 = formatPercentage(result.data.load.load1 / cores * 100)
uptime_5 = formatPercentage(result.data.load.load5 / cores * 100)
uptime_15 = formatPercentage(result.data.load.load15 / cores * 100)
element.progress('uptime_1', uptime_1)
element.progress('uptime_5', uptime_5)
// 计算负载百分比
uptime_1 = formatPercentage(result.data.load.load1 / cores * 100)
uptime_5 = formatPercentage(result.data.load.load5 / cores * 100)
uptime_15 = formatPercentage(result.data.load.load15 / cores * 100)
element.progress('uptime_1', uptime_1)
element.progress('uptime_5', uptime_5)
// 计算网卡流量
for (let i = 0; i < result.data.net.length; i++) {
if (result.data.net[i].name === 'lo') {
continue
}
net_send += result.data.net[i].bytesSent
net_recv += result.data.net[i].bytesRecv
}
if (first) {
net_send_last = net_send
net_recv_last = net_recv
}
$('#home_net_now').html(formatBytes((net_send - net_send_last) / 3) + '/s / ' + formatBytes((net_recv - net_recv_last) / 3) + '/s')
$('#home_net_total').html(formatBytes(net_send) + ' / ' + formatBytes(net_recv))
net_send_last = net_send
net_recv_last = net_recv
net_send = 0
net_recv = 0
// 计算网卡流量
for (let i = 0; i < result.data.net.length; i++) {
if (result.data.net[i].name === 'lo') {
continue
cpu_info = result.data.cpus[0].modelName + ' ' + cores + '线程'
$('#home_cpu').text(cpu_info)
$('#home_mem').text('使用 ' + formatBytes(result.data.mem.used) + ' / ' + '总计 ' + formatBytes(result.data.mem.total))
element.render('progress')
}
})
})
}
refresh_home_info(true)
setInterval(refresh_home_info, 3000)
// 获取系统信息,这部分信息无需更新。
layui.use(['index', 'jquery', 'admin', 'carousel'], function () {
let $ = layui.jquery
, admin = layui.admin
, carousel = layui.carousel
carousel.render({
elem: '#ad1-carousel'
, width: '100%'
, height: '22px'
, anim: 'fade'
, arrow: 'none'
, indicator: 'none'
})
admin.req({
url: '/api/panel/info/systemInfo'
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:系统信息获取失败,接口返回' + result)
layer.msg('系统信息获取失败,请刷新重试!')
return false
}
$('#home_os_name').text(result.data.os_name)
$('#home_panel_version').text(result.data.panel_version)
$('#home_uptime').text('已不间断运行 ' + result.data.uptime + ' 天')
}
net_send += result.data.net[i].bytesSent
net_recv += result.data.net[i].bytesRecv
}
if (first) {
net_send_last = net_send
net_recv_last = net_recv
}
$('#home_net_now').html(formatBytes((net_send - net_send_last) / 3) + '/s / ' + formatBytes((net_recv - net_recv_last) / 3) + '/s')
$('#home_net_total').html(formatBytes(net_send) + ' / ' + formatBytes(net_recv))
net_send_last = net_send
net_recv_last = net_recv
net_send = 0
net_recv = 0
cpu_info = result.data.cpus[0].modelName + ' ' + cores + '线程'
$('#home_cpu').text(cpu_info)
$('#home_mem').text('使用 ' + formatBytes(result.data.mem.used) + ' / ' + '总计 ' + formatBytes(result.data.mem.total))
element.render('progress')
}
})
})
})
}
refresh_home_info(true)
setInterval(refresh_home_info, 3000)
// 获取系统信息,这部分信息无需更新。
layui.use(['index', 'jquery', 'admin', 'carousel'], function () {
let $ = layui.jquery
, admin = layui.admin
, carousel = layui.carousel
carousel.render({
elem: '#ad1-carousel'
, width: '100%'
, height: '22px'
, anim: 'fade'
, arrow: 'none'
, indicator: 'none'
})
admin.req({
url: '/api/panel/info/systemInfo'
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:系统信息获取失败,接口返回' + result)
layer.msg('系统信息获取失败,请刷新重试!')
return false
}
$('#home_os_name').text(result.data.os_name)
$('#home_panel_version').text(result.data.panel_version)
$('#home_uptime').text('已不间断运行 ' + result.data.uptime + ' 天')
}
})
})
layui.use(['jquery', 'layer'], function () {
let $ = layui.jquery
, layer = layui.layer
, admin = layui.admin
// 监听鼠标悬停到uptime上的事件
// 用于显示1分钟、5分钟、15分钟的负载
$('#monitor2').hover(function () {
layer.tips('1分钟负载:' + uptime_1 + '<br>5分钟负载:' + uptime_5 + '<br>15分钟负载:' + uptime_15, '#monitor2', {
tips: 1,
time: 0
})
}, function () {
layer.closeAll('tips')
})
layui.use(['jquery', 'layer'], function () {
let $ = layui.jquery
, layer = layui.layer
, admin = layui.admin
// 监听鼠标悬停到uptime上的事件
// 用于显示1分钟、5分钟、15分钟的负载
$('#monitor2').hover(function () {
layer.tips('1分钟负载:' + uptime_1 + '<br>5分钟负载:' + uptime_5 + '<br>15分钟负载:' + uptime_15, '#monitor2', {
tips: 1,
time: 0
})
}, function () {
layer.closeAll('tips')
})
// 监听更新按钮点击事件
$('#update_panel').click(function () {
index = layer.msg('正在获取版本信息...', {
icon: 16
, time: 0
})
admin.req(
{
url: '/api/panel/info/checkUpdate'
, type: 'get'
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
layer.msg('获取版本信息失败,请刷新重试!')
return false
}
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 }
})
})
$('#update_panel').click(function () {
index = layer.msg('正在获取版本信息...', {
icon: 16
, time: 0
})
admin.req(
{
url: '/api/panel/info/checkUpdate'
, type: 'get'
, success: function (result) {
layer.close(index)
if (result.code !== 0) {
layer.msg('获取版本信息失败,请刷新重试!')
return false
}
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('当前已是最新版本!')
}
}
}
)
})
$('#restart_panel').click(function () {
layer.confirm('真的要重启吗?', {
btn: ['是', '否']
}, function () {
proxy = true
index = layer.msg('正在重启,稍后请手动刷新...', {icon: 16, time: 0, shade: 0.3})
admin.req({
url: '/api/panel/info/restart'
, type: 'post'
})
} else {
layer.msg('当前已是最新版本!')
}
}
}
)
})
})
})
})
</script>
+2 -2
View File
@@ -368,10 +368,10 @@ Date: 2023-07-21
data: [{type: 'average', name: '平均值'}]
}
}], [{
name: '单位 Kb/s',
name: '单位 Mb',
type: 'value',
axisLabel: {
formatter: '{value} Kb'
formatter: '{value} Mb'
}
}]);
-1
View File
@@ -48,7 +48,6 @@
{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}
@@ -38,7 +38,7 @@ Date: 2023-07-22
, toolbar: '#mysql-database-backup-bar'
, title: '备份列表'
, cols: [[
{field: 'file', title: '备份名称', width: 500}
{field: 'name', title: '备份名称', width: 500}
, {field: 'size', title: '文件大小'}
, {field: 'right', title: '操作', width: 150, toolbar: '#mysql-database-backup-control'}
]]
@@ -95,7 +95,7 @@ Date: 2023-07-22
table.on('tool(mysql-backup-list)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('确定要删除数据库备份 <b style="color: red;">' + data.file + '</b> 吗?', function (index) {
layer.confirm('确定要删除数据库备份 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
index = layer.msg('正在删除数据库备份,请稍等...', {
icon: 16
, time: 0
@@ -112,12 +112,12 @@ Date: 2023-07-22
return false;
}
obj.del();
layer.alert('数据库备份' + data.file + '删除成功!');
layer.alert('数据库备份' + data.name + '删除成功!');
}
});
});
} else if (obj.event === 'restore') {
layer.confirm('高风险操作,确定要恢复数据库备份 <b style="color: red;">' + data.file + '</b> 吗?', function (index) {
layer.confirm('高风险操作,确定要恢复数据库备份 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
index = layer.msg('正在恢复数据库备份,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
@@ -134,7 +134,7 @@ Date: 2023-07-22
layer.msg('数据库备份恢复失败,请刷新重试!')
return false;
}
layer.alert('数据库备份' + data.file + '恢复成功!');
layer.alert('数据库备份' + data.name + '恢复成功!');
}
});
});
@@ -38,7 +38,7 @@ Date: 2023-07-22
, toolbar: '#mysql-database-backup-bar'
, title: '备份列表'
, cols: [[
{field: 'file', title: '备份名称', width: 500}
{field: 'name', title: '备份名称', width: 500}
, {field: 'size', title: '文件大小'}
, {field: 'right', title: '操作', width: 150, toolbar: '#mysql-database-backup-control'}
]]
@@ -95,7 +95,7 @@ Date: 2023-07-22
table.on('tool(mysql-backup-list)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('确定要删除数据库备份 <b style="color: red;">' + data.file + '</b> 吗?', function (index) {
layer.confirm('确定要删除数据库备份 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
index = layer.msg('正在删除数据库备份,请稍等...', {
icon: 16
, time: 0
@@ -112,12 +112,12 @@ Date: 2023-07-22
return false;
}
obj.del();
layer.alert('数据库备份' + data.file + '删除成功!');
layer.alert('数据库备份' + data.name + '删除成功!');
}
});
});
} else if (obj.event === 'restore') {
layer.confirm('高风险操作,确定要恢复数据库备份 <b style="color: red;">' + data.file + '</b> 吗?', function (index) {
layer.confirm('高风险操作,确定要恢复数据库备份 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
index = layer.msg('正在恢复数据库备份,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
@@ -134,7 +134,7 @@ Date: 2023-07-22
layer.msg('数据库备份恢复失败,请刷新重试!')
return false;
}
layer.alert('数据库备份' + data.file + '恢复成功!');
layer.alert('数据库备份' + data.name + '恢复成功!');
}
});
});
+381
View File
@@ -0,0 +1,381 @@
<!--
Name: PHP管理器
Author: 耗子
Date: 2023-07-22
-->
<title>PHP-8.1</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.1管理</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="php81-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="php81-start" class="layui-btn">启动</button>
<button id="php81-stop" class="layui-btn layui-btn-danger">停止</button>
<button id="php81-restart" class="layui-btn layui-btn-warm">重启</button>
<button id="php81-reload" class="layui-btn layui-btn-normal">重载</button>
</div>
</div>
<div class="layui-tab-item">
<table id="php81-extension" lay-filter="php81-extension"></table>
<!-- 操作按钮模板 -->
<script type="text/html" id="php81-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="php81-config-editor"
style="height: 600px;"></div>
<div class="layui-btn-container" style="padding-top: 30px;">
<button id="php81-config-save" class="layui-btn">保存</button>
</div>
</div>
<div class="layui-tab-item">
<table class="layui-hide" id="php81-load-status"></table>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="php81-clean-error-log" class="layui-btn">清空日志</button>
</div>
<pre id="php81-error-log" class="layui-code">
获取中...
</pre>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="php81-clean-slow-log" class="layui-btn">清空日志</button>
</div>
<pre id="php81-slow-log" class="layui-code">
获取中...
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let php81_config_editor;
layui.use(['index', 'code', 'table'], function () {
let $ = layui.$
, admin = layui.admin
, element = layui.element
, code = layui.code
, table = layui.table;
// 获取php81运行状态并渲染
admin.req({
url: "/api/plugins/php81/status"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:PHP运行状态获取失败,接口返回' + result);
return false;
}
if (result.data) {
$('#php81-status').html('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
} else {
$('#php81-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
}
}
});
// 获取php81错误日志并渲染
admin.req({
url: "/api/plugins/php81/errorLog"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
$('#php81-error-log').text('PHP日志获取失败,请刷新重试!');
code({
elem: '#php81-error-log'
, title: 'php-fpm.log'
, encode: true
, about: false
});
return false;
}
$('#php81-error-log').text(result.data);
code({
elem: '#php81-error-log'
, title: 'php-fpm.log'
, encode: true
, about: false
});
}
});
// 获取php81慢日志并渲染
admin.req({
url: "/api/plugins/php81/slowLog"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
$('#php81-slow-log').text('PHP慢日志获取失败,请刷新重试!');
code({
elem: '#php81-slow-log'
, title: 'slow.log'
, encode: true
, about: false
});
return false;
}
$('#php81-slow-log').text(result.data);
code({
elem: '#php81-slow-log'
, title: 'slow.log'
, encode: true
, about: false
});
}
});
// 获取php81配置并渲染
admin.req({
url: "/api/plugins/php81/config"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:PHP主配置获取失败,接口返回' + result);
return false;
}
$('#php81-config-editor').text(result.data);
php81_config_editor = ace.edit("php81-config-editor", {
mode: "ace/mode/ini",
selectionStyle: "text"
});
}
});
// 获取php81负载状态并渲染
table.render({
elem: '#php81-load-status'
, url: '/api/plugins/php81/load'
, cols: [[
{field: 'name', width: '81%', title: '属性',}
, {field: 'value', width: '20%', title: '当前值'}
]]
});
element.render();
// 获取php81扩展并渲染
table.render({
elem: '#php81-extension'
, url: '/api/plugins/php81/extensions'
, cols: [[
{field: 'slug', hide: true, title: 'Slug', sort: true}
, {field: 'name', width: '20%', title: '拓展名'}
, {field: 'description', width: '70%', title: '描述'}
, {
field: 'control',
title: '操作',
templet: '#php81-extension-control',
fixed: 'right',
align: 'left'
}
]]
, page: false
, text: {
none: '暂无拓展'
}
});
// 工具条
table.on('tool(php81-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/php81/installExtension',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code === 0) {
layer.close(index);
table.reload('php81-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/php81/uninstallExtension',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
layer.close(index);
if (res.code === 0) {
table.reload('php81-extension');
layer.msg('卸载:' + data.name + ' 成功加入任务队列', {icon: 1, time: 1000});
} else {
layer.msg(res.msg, {icon: 2, time: 1000});
}
}
});
});
}
});
// 事件监听
$('#php81-start').click(function () {
index = layer.msg('正在启动PHP,请稍后...', {icon: 16, time: 0});
layer.confirm('确定要启动PHP吗?', {
btn: ['启动', '取消']
}, function () {
admin.req({
url: "/api/plugins/php81/start"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('PHP启动成功!');
}
});
});
});
$('#php81-stop').click(function () {
layer.confirm('停止PHP将导致使用PHP的网站无法访问,是否继续停止?', {
btn: ['停止', '取消']
}, function () {
index = layer.msg('正在停止PHP,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php81/stop"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('PHP停止成功!');
}
});
});
});
$('#php81-restart').click(function () {
layer.confirm('重启PHP将导致使用PHP的网站短时间无法访问,是否继续重启?', {
btn: ['重启', '取消']
}, function () {
index = layer.msg('正在重启PHP,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php81/restart"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('PHP重启成功!');
}
});
});
});
$('#php81-reload').click(function () {
index = layer.msg('正在重载PHP,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php81/reload"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
layer.alert('PHP重载成功!');
}
});
});
$('#php81-config-save').click(function () {
index = layer.msg('正在保存配置,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php81/config"
, type: 'post'
, data: {
config: php81_config_editor.getValue()
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
layer.alert('PHP配置保存成功!');
}
});
});
$('#php81-clean-error-log').click(function () {
index = layer.msg('正在清空错误日志,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php81/clearErrorLog"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.msg('PHP日志已清空!');
}
});
});
$('#php81-clean-slow-log').click(function () {
index = layer.msg('正在清空慢日志,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php81/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.2</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.2管理</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="php82-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="php82-start" class="layui-btn">启动</button>
<button id="php82-stop" class="layui-btn layui-btn-danger">停止</button>
<button id="php82-restart" class="layui-btn layui-btn-warm">重启</button>
<button id="php82-reload" class="layui-btn layui-btn-normal">重载</button>
</div>
</div>
<div class="layui-tab-item">
<table id="php82-extension" lay-filter="php82-extension"></table>
<!-- 操作按钮模板 -->
<script type="text/html" id="php82-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="php82-config-editor"
style="height: 600px;"></div>
<div class="layui-btn-container" style="padding-top: 30px;">
<button id="php82-config-save" class="layui-btn">保存</button>
</div>
</div>
<div class="layui-tab-item">
<table class="layui-hide" id="php82-load-status"></table>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="php82-clean-error-log" class="layui-btn">清空日志</button>
</div>
<pre id="php82-error-log" class="layui-code">
获取中...
</pre>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="php82-clean-slow-log" class="layui-btn">清空日志</button>
</div>
<pre id="php82-slow-log" class="layui-code">
获取中...
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let php82_config_editor;
layui.use(['index', 'code', 'table'], function () {
let $ = layui.$
, admin = layui.admin
, element = layui.element
, code = layui.code
, table = layui.table;
// 获取php82运行状态并渲染
admin.req({
url: "/api/plugins/php82/status"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:PHP运行状态获取失败,接口返回' + result);
return false;
}
if (result.data) {
$('#php82-status').html('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
} else {
$('#php82-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
}
}
});
// 获取php82错误日志并渲染
admin.req({
url: "/api/plugins/php82/errorLog"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
$('#php82-error-log').text('PHP日志获取失败,请刷新重试!');
code({
elem: '#php82-error-log'
, title: 'php-fpm.log'
, encode: true
, about: false
});
return false;
}
$('#php82-error-log').text(result.data);
code({
elem: '#php82-error-log'
, title: 'php-fpm.log'
, encode: true
, about: false
});
}
});
// 获取php82慢日志并渲染
admin.req({
url: "/api/plugins/php82/slowLog"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
$('#php82-slow-log').text('PHP慢日志获取失败,请刷新重试!');
code({
elem: '#php82-slow-log'
, title: 'slow.log'
, encode: true
, about: false
});
return false;
}
$('#php82-slow-log').text(result.data);
code({
elem: '#php82-slow-log'
, title: 'slow.log'
, encode: true
, about: false
});
}
});
// 获取php82配置并渲染
admin.req({
url: "/api/plugins/php82/config"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
console.log('耗子Linux面板:PHP主配置获取失败,接口返回' + result);
return false;
}
$('#php82-config-editor').text(result.data);
php82_config_editor = ace.edit("php82-config-editor", {
mode: "ace/mode/ini",
selectionStyle: "text"
});
}
});
// 获取php82负载状态并渲染
table.render({
elem: '#php82-load-status'
, url: '/api/plugins/php82/load'
, cols: [[
{field: 'name', width: '82%', title: '属性',}
, {field: 'value', width: '20%', title: '当前值'}
]]
});
element.render();
// 获取php82扩展并渲染
table.render({
elem: '#php82-extension'
, url: '/api/plugins/php82/extensions'
, cols: [[
{field: 'slug', hide: true, title: 'Slug', sort: true}
, {field: 'name', width: '20%', title: '拓展名'}
, {field: 'description', width: '70%', title: '描述'}
, {
field: 'control',
title: '操作',
templet: '#php82-extension-control',
fixed: 'right',
align: 'left'
}
]]
, page: false
, text: {
none: '暂无拓展'
}
});
// 工具条
table.on('tool(php82-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/php82/installExtension',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
if (res.code === 0) {
layer.close(index);
table.reload('php82-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/php82/uninstallExtension',
type: 'POST',
data: {
slug: data.slug
}
, success: function (res) {
layer.close(index);
if (res.code === 0) {
table.reload('php82-extension');
layer.msg('卸载:' + data.name + ' 成功加入任务队列', {icon: 1, time: 1000});
} else {
layer.msg(res.msg, {icon: 2, time: 1000});
}
}
});
});
}
});
// 事件监听
$('#php82-start').click(function () {
index = layer.msg('正在启动PHP,请稍后...', {icon: 16, time: 0});
layer.confirm('确定要启动PHP吗?', {
btn: ['启动', '取消']
}, function () {
admin.req({
url: "/api/plugins/php82/start"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('PHP启动成功!');
}
});
});
});
$('#php82-stop').click(function () {
layer.confirm('停止PHP将导致使用PHP的网站无法访问,是否继续停止?', {
btn: ['停止', '取消']
}, function () {
index = layer.msg('正在停止PHP,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php82/stop"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('PHP停止成功!');
}
});
});
});
$('#php82-restart').click(function () {
layer.confirm('重启PHP将导致使用PHP的网站短时间无法访问,是否继续重启?', {
btn: ['重启', '取消']
}, function () {
index = layer.msg('正在重启PHP,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php82/restart"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('PHP重启成功!');
}
});
});
});
$('#php82-reload').click(function () {
index = layer.msg('正在重载PHP,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php82/reload"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
layer.alert('PHP重载成功!');
}
});
});
$('#php82-config-save').click(function () {
index = layer.msg('正在保存配置,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php82/config"
, type: 'post'
, data: {
config: php82_config_editor.getValue()
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
layer.alert('PHP配置保存成功!');
}
});
});
$('#php82-clean-error-log').click(function () {
index = layer.msg('正在清空错误日志,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php82/clearErrorLog"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.msg('PHP日志已清空!');
}
});
});
$('#php82-clean-slow-log').click(function () {
index = layer.msg('正在清空慢日志,请稍后...', {icon: 16, time: 0});
admin.req({
url: "/api/plugins/php82/clearSlowLog"
, type: 'post'
, success: function (result) {
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.msg('PHP慢日志已清空!');
}
});
});
});
</script>
+102
View File
@@ -0,0 +1,102 @@
<!--
Name: S3fs
Author: 耗子
Date: 2023-07-25
-->
<title>S3fs</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">S3fs 挂载列表</div>
<div class="layui-card-body">
<table class="layui-hide" id="s3fs-list" lay-filter="s3fs-list"></table>
<!-- 顶部工具栏 -->
<script type="text/html" id="s3fs-list-bar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="add_mount">新建挂载</button>
</div>
</script>
<!-- 右侧管理 -->
<script type="text/html" id="s3fs-list-control">
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="umount">卸载</a>
</script>
</div>
</div>
</div>
</div>
</div>
<script>
layui.use(['index', 'code', 'table'], function () {
let admin = layui.admin
, table = layui.table
, form = layui.form
, view = layui.view;
// 获取备份任务列表
table.render({
elem: '#s3fs-list'
, url: '/api/plugins/s3fs/list'
, toolbar: '#s3fs-list-bar'
, title: 'S3fs挂载列表'
, cols: [[
{field: 'id', hide: true, title: 'ID', sort: true}
, {field: 'bucket', title: 'Bucket', fixed: 'left', unresize: true, sort: true}
, {field: 'url', title: 'URL', sort: true}
, {field: 'path', title: '挂载目录', sort: true}
, {fixed: 'right', title: '操作', toolbar: '#s3fs-list-control', width: 150}
]]
, page: true
, text: {
none: '无挂载'
}
, parseData: function (res) {
return {
"code": res.code,
"msg": res.message,
"count": res.data.total,
"data": res.data.items
};
}
});
// 头工具栏事件
table.on('toolbar(s3fs-list)', function (obj) {
if (obj.event === 'add_mount') {
admin.popup({
title: '新建挂载'
, area: ['600px', '600px']
, id: 'LAY-popup-s3fs-mount-add'
, success: function (layer, index) {
view(this.id).render('plugins/s3fs/add_mount', {}).done(function () {
form.render(null, 'LAY-popup-s3fs-mount-add');
});
}
});
}
});
// 行工具事件
table.on('tool(s3fs-list)', function (obj) {
let data = obj.data;
if (obj.event === 'umount') {
layer.confirm('确定要卸载 <b style="color: red;">' + data.path + '</b> 吗?', function (index) {
index = layer.msg('请稍候...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/s3fs/delete"
, type: 'post'
, data: data
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
obj.del();
layer.alert(data.path + ' 卸载成功!');
}
});
layer.close(index);
});
}
});
});
</script>
@@ -0,0 +1,104 @@
<!--
Name: S3fs - 新增挂载
Author: 耗子
Date: 2023-07-25
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<form class="layui-form" action="" lay-filter="add-mount-s3fs-form">
<div class="layui-form-item">
<label class="layui-form-label">Bucket</label>
<div class="layui-input-block">
<input type="text" name="bucket" id="add-mount-s3fs-bucket"
lay-verify="required" placeholder="请输入Bucket名" class="layui-input"
value="">
</div>
<div class="layui-form-mid layui-word-aux">输入Bucket名字腾讯云COS为xxxx-用户ID</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">AK</label>
<div class="layui-input-block">
<input type="text" name="ak" id="add-mount-s3fs-ak"
lay-verify="required" placeholder="请输入AK密钥" class="layui-input"
value="">
</div>
<div class="layui-form-mid layui-word-aux">访问密钥中的Access Key需具备Bucket操作权限腾讯云为SecretId
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">SK</label>
<div class="layui-input-block">
<input type="text" name="sk" id="add-mount-s3fs-sk"
lay-verify="required" placeholder="请输入SK密钥" class="layui-input"
value="">
</div>
<div class="layui-form-mid layui-word-aux">访问密钥中的Access Key
Secret需具备Bucket操作权限腾讯云为SecretKey
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">地域节点</label>
<div class="layui-input-block">
<input type="text" name="url" id="add-mount-s3fs-url"
lay-verify="required" placeholder="请输入Bucket地域节点" class="layui-input"
value="">
</div>
<div class="layui-form-mid layui-word-aux">
地域节点可在<a target="_blank" href="https://github.com/s3fs-fuse/s3fs-fuse/wiki/Non-Amazon-S3">https://github.com/s3fs-fuse/s3fs-fuse/wiki/Non-Amazon-S3</a>查找
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">挂载目录</label>
<div class="layui-input-block">
<input type="text" name="path" id="add-mount-s3fs-path"
lay-verify="required" placeholder="请输入挂载目录" class="layui-input"
value="">
</div>
<div class="layui-form-mid layui-word-aux">挂载目录/data不存在将会自动创建</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-mount-s3fs-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', 'jquery', 'cron'], function () {
var admin = layui.admin
, layer = layui.layer
, form = layui.form
, table = layui.table;
form.render();
form.on('submit(add-mount-s3fs-submit)', function (data) {
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/s3fs/add"
, type: 'post'
, data: data.field
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
table.reload('s3fs-list');
layer.alert('S3fs挂载已提交,请自行检查是否挂载成功!', {
icon: 1
, title: '提示'
, btn: ['确定']
, yes: function (index) {
layer.closeAll();
}
});
}
});
return false;
});
});
};
</script>
+451
View File
@@ -0,0 +1,451 @@
<!--
Name: Supervisor
Author: 耗子
Date: 2023-07-26
-->
<title>Supervisor</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">Supervisor 管理</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>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<blockquote id="supervisor-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="supervisor-start" class="layui-btn">启动</button>
<button id="supervisor-stop" class="layui-btn layui-btn-danger">停止</button>
<button id="supervisor-restart" class="layui-btn layui-btn-warm">重启</button>
<button id="supervisor-reload" class="layui-btn layui-btn-normal">重载</button>
</div>
</div>
<div class="layui-tab-item">
<table class="layui-hide" id="supervisor-list" lay-filter="supervisor-list"></table>
<!-- 顶部工具栏 -->
<script type="text/html" id="supervisor-list-bar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-sm" lay-event="add_process">添加进程</button>
</div>
</script>
<!-- 右侧管理 -->
<script type="text/html" id="supervisor-list-control">
<a class="layui-btn layui-btn-xs" lay-event="log">日志</a>
<a class="layui-btn layui-bg-blue layui-btn-xs" lay-event="config">配置</a>
<a class="layui-btn layui-border layui-btn-xs" lay-event="start">启动</a>
<a class="layui-btn layui-bg-purple layui-btn-xs" lay-event="restart">重启</a>
<a class="layui-btn layui-bg-orange layui-btn-xs" lay-event="stop">停止</a>
<a class="layui-btn layui-bg-red layui-btn-xs" lay-event="delete">删除</a>
</script>
</div>
<div class="layui-tab-item">
<blockquote class="layui-elem-quote">此处修改的是Supervisor主配置文件,如果你不了解各参数的含义,请不要随意修改!<br>
提示:Ctrl+F 搜索关键字,Ctrl+S 保存,Ctrl+H 查找替换!
</blockquote>
<div id="supervisor-config-editor"
style="height: 600px;"></div>
<div class="layui-btn-container" style="padding-top: 30px;">
<button id="supervisor-config-save" class="layui-btn">保存</button>
</div>
</div>
<div class="layui-tab-item">
<div class="layui-btn-container">
<button id="supervisor-clear-log" class="layui-btn">清空日志</button>
</div>
<pre id="supervisor-log" class="layui-code">
获取中...
</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let supervisor_config_editor;
let supervisor_process_config_editor;
layui.use(['jquery', 'admin', 'view', 'form', 'code', 'table'], function () {
let $ = layui.$
, admin = layui.admin
, table = layui.table
, form = layui.form
, view = layui.view
, code = layui.code;
admin.req({
url: "/api/plugins/supervisor/status"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
return false;
}
if (result.data) {
$('#supervisor-status').html('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
} else {
$('#supervisor-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
}
}
});
table.render({
elem: '#supervisor-list'
, url: '/api/plugins/supervisor/processes'
, toolbar: '#supervisor-list-bar'
, title: 'Supervisor进程列表'
, cols: [[
{field: 'name', title: '名称', fixed: 'left', unresize: true, sort: true}
, {field: 'status', title: '状态', sort: true}
, {field: 'pid', title: 'PID', sort: true}
, {field: 'uptime', title: '运行时间(天)', sort: true}
, {fixed: 'right', title: '操作', toolbar: '#supervisor-list-control', width: 350}
]]
, page: true
, text: {
none: '无进程'
}
, parseData: function (res) {
return {
"code": res.code,
"msg": res.message,
"count": res.data.total,
"data": res.data.items
};
}
});
// 头工具栏事件
table.on('toolbar(supervisor-list)', function (obj) {
if (obj.event === 'add_process') {
admin.popup({
title: '添加进程'
, area: ['600px', '600px']
, id: 'LAY-popup-supervisor-process-add'
, success: function (layer, index) {
view(this.id).render('plugins/supervisor/add_process', {}).done(function () {
form.render(null, 'LAY-popup-supervisor-process-add');
});
}
});
}
});
// 行工具事件
table.on('tool(supervisor-list)', function (obj) {
let data = obj.data;
if (obj.event === 'log') {
admin.popup({
title: '日志 - ' + data.name
,
area: ['80%', '80%']
,
id: 'process-log'
,
content: '<pre id="process-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) {
index2 = layer.msg('正在获取日志...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: '/api/plugins/supervisor/processLog?process=' + data.name
, type: 'GET'
, success: function (res) {
layer.close(index2);
if (res.code === 0) {
$('#process-log-view').html(res.data);
} else {
layer.msg(res.message, {icon: 2, time: 1000});
}
}
});
}
});
}
if (obj.event === 'config') {
admin.req({
url: '/api/plugins/supervisor/processConfig?process=' + data.name
, type: 'GET'
, success: function (res) {
if (res.code === 0) {
admin.popup({
title: '编辑 - ' + data.name
,
area: ['80%', '80%']
,
id: 'process-config'
,
content: '<div id="process-config-editor" style="height: 80%;">' + res.data + '</div><br><button id="process-config-' + data.name + '" class="layui-btn">保存</button>'
,
success: function (layero, index) {
supervisor_process_config_editor = ace.edit("process-config-editor", {
mode: "ace/mode/ini",
selectionStyle: "text"
});
$('#process-config-' + data.name).click(function () {
index2 = layer.msg('正在保存配置...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: '/api/plugins/supervisor/processConfig'
, type: 'POST'
, data: {
process: data.name,
config: supervisor_process_config_editor.getValue()
}
, success: function (res) {
if (res.code === 0) {
layer.msg('保存成功', {icon: 1, time: 1000});
table.reload('supervisor-list');
layer.close(index2);
layer.close(index);
} else {
layer.msg(res.message, {icon: 2, time: 1000});
}
}
});
});
}
});
} else {
layer.msg(res.message, {icon: 2, time: 1000});
}
}
});
}
if (obj.event === 'start') {
index = layer.msg('正在启动进程...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/supervisor/startProcess"
, type: 'post'
, data: {
process: data.name
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('进程启动成功!');
}
});
}
if (obj.event === 'restart') {
layer.confirm('确定要重启进程吗?', function (index) {
index = layer.msg('请稍候...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/supervisor/restartProcess"
, type: 'post'
, data: {
process: data.name
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
layer.alert('进程重启成功!');
}
});
layer.close(index);
});
}
if (obj.event === 'stop') {
layer.confirm('确定要停止进程吗?', function (index) {
index = layer.msg('请稍候...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/supervisor/stopProcess"
, type: 'post'
, data: {
process: data.name
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
layer.alert('进程停止成功!');
}
});
layer.close(index);
});
}
if (obj.event === 'delete') {
layer.confirm('确定要删除进程吗?', function (index) {
index = layer.msg('请稍候...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/supervisor/deleteProcess"
, type: 'post'
, data: {
process: data.name
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
obj.del();
layer.alert('进程删除成功!');
}
});
layer.close(index);
});
}
});
admin.req({
url: "/api/plugins/supervisor/log"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
$('#supervisor-log').text('Supervisor日志获取失败,请刷新重试!');
code({
elem: '#supervisor-log'
, title: 'supervisor.log'
, encode: true
, about: false
});
return false;
}
$('#supervisor-log').text(result.data);
code({
elem: '#supervisor-log'
, title: 'supervisor.log'
, encode: true
, about: false
});
}
});
admin.req({
url: "/api/plugins/supervisor/config"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
return false;
}
$('#supervisor-config-editor').text(result.data);
supervisor_config_editor = ace.edit("supervisor-config-editor", {
mode: "ace/mode/ini",
selectionStyle: "text"
});
}
});
$('#supervisor-start').click(function () {
layer.confirm('确定要启动Supervisor吗?', {
btn: ['启动', '取消']
}, function () {
index = layer.msg('正在启动Supervisor...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/supervisor/start"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('Supervisor启动成功!');
}
});
});
});
$('#supervisor-stop').click(function () {
layer.confirm('确定要停止Supervisor吗?', {
btn: ['停止', '取消']
}, function () {
index = layer.msg('正在停止Supervisor...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/supervisor/stop"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('Supervisor停止成功!');
}
});
});
});
$('#supervisor-restart').click(function () {
layer.confirm('确定要重启Supervisor吗?', {
btn: ['重启', '取消']
}, function () {
index = layer.msg('正在重启Supervisor...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/supervisor/restart"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('Supervisor重启成功!');
}
});
});
});
$('#supervisor-reload').click(function () {
index = layer.msg('正在重载Supervisor...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/supervisor/reload"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
admin.events.refresh();
layer.alert('Supervisor重载成功!');
}
});
});
$('#supervisor-config-save').click(function () {
index = layer.msg('正在保存配置...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/supervisor/config"
, type: 'post'
, data: {
config: supervisor_config_editor.getValue()
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
layer.alert('Supervisor配置保存成功!');
}
});
});
$('#supervisor-clear-log').click(function () {
index = layer.msg('正在清空Supervisor日志...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/supervisor/clearLog"
, type: 'post'
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
layer.msg('Supervisor日志已清空!');
setTimeout(function () {
admin.events.refresh();
}, 1000);
}
});
});
});
</script>
@@ -0,0 +1,94 @@
<!--
Name: Supervisor - 添加进程
Author: 耗子
Date: 2023-07-26
-->
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<form class="layui-form" action="" lay-filter="add-mount-s3fs-form">
<div class="layui-form-item">
<label class="layui-form-label">名称</label>
<div class="layui-input-block">
<input type="text" name="name" lay-verify="required" placeholder="请输入名称" class="layui-input"
value="">
</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-block">
<input type="text" name="command" lay-verify="required" placeholder="请输入启动命令" class="layui-input"
value="">
</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-block">
<input type="text" name="path" lay-verify="required" placeholder="请输入运行目录" class="layui-input"
value="">
</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-block">
<input type="text" name="user" lay-verify="required" placeholder="请输入启动用户" class="layui-input"
value="">
</div>
<div class="layui-form-mid layui-word-aux">一般情况下填写www即可</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">进程数量</label>
<div class="layui-input-block">
<input type="number" name="num" lay-verify="required" placeholder="请输入进程数量" class="layui-input"
value="1" min="1" step="1" lay-affix="number">
</div>
<div class="layui-form-mid layui-word-aux">一般情况下填写1即可</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<div class="layui-footer">
<button class="layui-btn" lay-submit="" lay-filter="supervisor-add-process-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', 'jquery', 'table'], function () {
var admin = layui.admin
, layer = layui.layer
, form = layui.form
, table = layui.table;
form.render();
form.on('submit(supervisor-add-process-submit)', function (data) {
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
admin.req({
url: "/api/plugins/supervisor/addProcess"
, type: 'post'
, data: data.field
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
return false;
}
table.reload('supervisor-list');
layer.alert('Supervisor进程添加成功!', {
icon: 1
, title: '提示'
, btn: ['确定']
, yes: function (index) {
layer.closeAll();
}
});
}
});
return false;
});
});
};
</script>
-3
View File
@@ -135,9 +135,6 @@ Date: 2023-07-21
$('#setting-api-token input').attr('readonly', true);
}
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error);
}
});
// 面板设置
+158
View File
@@ -0,0 +1,158 @@
<!--
Name: SSH
Author: 耗子
Date: 2023-07-25
-->
<title>SSH</title>
<link href="https://registry.npmmirror.com/xterm/5.2.1/files/css/xterm.css" rel="stylesheet">
<div class="layui-fluid">
<div class="layui-card">
<div class="layui-card-header">
SSH
</div>
<div class="layui-card-body">
<div class="layui-form" style="overflow: hidden;" lay-filter="ssh_setting">
<div class="layui-inline">
<span style="margin-right: 10px;">地址</span>
<div class="layui-input-inline">
<input type="text" name="host" class="layui-input"
style="height: 30px; margin-top: 5px;">
</div>
<span style="margin-left: 40px; margin-right: 10px;">端口</span>
<div class="layui-input-inline">
<input type="text" name="port" class="layui-input"
style="height: 30px; margin-top: 5px;">
</div>
<span style="margin-left: 40px; margin-right: 10px;">账号</span>
<div class="layui-input-inline">
<input type="text" name="user" class="layui-input"
style="height: 30px; margin-top: 5px;">
</div>
<span style="margin-left: 40px; margin-right: 10px;">密码</span>
<div class="layui-input-inline">
<input type="text" name="password" class="layui-input"
style="height: 30px; margin-top: 5px;">
</div>
<div class="layui-input-inline">
<button lay-filter="save_ssh_setting" lay-submit class="layui-btn layui-btn-sm"
style="margin-left: 10px;">
保存
</button>
</div>
</div>
</div>
<div id="terminal" style="width: 100%; height: 70vh; background-color: #000000; margin-top: 20px;">
</div>
</div>
</div>
</div>
<script>
layui.use(['admin', 'jquery', 'form'], function () {
var admin = layui.admin;
var $ = layui.jquery;
var form = layui.form;
form.render();
admin.req({
url: "/api/panel/ssh/info"
, type: 'get'
, success: function (result) {
if (result.code !== 0) {
layer.msg('SSH信息获取失败,请刷新重试!')
return false;
}
form.val("ssh_setting",
result.data
);
}
});
form.on('submit(save_ssh_setting)', function (data) {
admin.req({
url: "/api/panel/ssh/info"
, type: 'post'
, data: data.field
, success: function (result) {
if (result.code !== 0) {
layer.alert('SSH信息保存失败,请刷新重试!')
return false;
}
layer.alert('SSH信息保存成功!')
}
});
return false;
});
})
</script>
<script>
const msgData = '1'
const msgResize = '2'
var terminal = new Terminal({
rendererType: "canvas",
fontSize: 15,
screenKeys: true,
useStyle: true,
cursorBlink: true, // 光标闪烁
theme: {
foreground: "#ECECEC", // 字体
background: "#000000", //背景色
cursor: "help", // 设置光标
lineHeight: 20
}
});
let fitAddon = new FitAddon.FitAddon();
terminal.loadAddon(fitAddon);
fitAddon.fit()
let terminalContainer = document.getElementById("terminal")
let token = layui.data('HaoZiPanel')['access_token']
const webSocket = new WebSocket(`ws://${window.location.host}/api/panel/ssh/session`, [token]);
webSocket.binaryType = 'arraybuffer';
const enc = new TextDecoder("utf-8");
webSocket.onmessage = (event) => {
terminal.write(enc.decode(event.data));
}
webSocket.onopen = () => {
terminal.open(terminalContainer)
fitAddon.fit()
terminal.write("\r\nWelcome to HaoZiPanel SSH. Connection success.\r\n")
terminal.focus()
}
webSocket.onclose = () => {
terminal.write("\r\nSSH connection closed. Please refresh the page.\r\n")
}
webSocket.onerror = (event) => {
console.error(event)
webSocket.close()
}
terminal.onData((data) => {
webSocket.send(msgData + CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(data)), ArrayBuffer)
})
terminal.onResize(({cols, rows}) => {
if (webSocket.readyState === 1) {
webSocket.send(msgResize +
CryptoJS.enc.Base64.stringify(
CryptoJS.enc.Utf8.parse(
JSON.stringify({
columns: cols,
rows: rows
})
)
), ArrayBuffer
)
}
})
window.addEventListener("resize", () => {
fitAddon.fit()
}, false)
</script>
+60 -3
View File
@@ -16,6 +16,7 @@ Date: 2023-07-22
<li class="layui-this">进行中</li>
<li>等待中</li>
<li>已完成</li>
<li>已失败</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
@@ -41,6 +42,12 @@ Date: 2023-07-22
<a class="layui-btn layui-btn-xs" lay-event="remove">移除</a>
</script>
</div>
<div class="layui-tab-item">
<table id="panel-task-failed" lay-filter="panel-task-failed"></table>
<script type="text/html" id="panel-task-failed-control-tpl">
<a class="layui-btn layui-btn-xs" lay-event="remove">移除</a>
</script>
</div>
</div>
</div>
</div>
@@ -131,8 +138,6 @@ Date: 2023-07-22
};
}
});
// 工具条
table.on('tool(panel-task-finished)', function (obj) {
let data = obj.data;
if (obj.event === 'remove') {
@@ -147,7 +152,6 @@ Date: 2023-07-22
, success: function (res) {
if (res.code == 0) {
layer.msg('移除任务:' + data.name + ' 成功!', {icon: 1, time: 1000}, function () {
// 重载表格
table.reload('panel-task-finished-table');
});
} else {
@@ -158,5 +162,58 @@ Date: 2023-07-22
});
}
});
table.render({
elem: '#panel-task-failed'
, id: 'panel-task-failed-table'
, url: '/api/panel/task/list?status=failed'
, cols: [[
{field: 'id', hide: true, title: 'ID', sort: true}
, {field: 'name', width: '80%', title: '任务名'}
, {
field: 'control',
title: '操作',
templet: '#panel-task-failed-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-failed)', 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-failed-table');
});
} else {
layer.msg(res.message, {icon: 2, time: 1000});
}
}
});
});
}
});
});
</script>
+2 -2
View File
@@ -114,7 +114,7 @@ Date: 2023-06-24
})
form.on('select(add-website-db)', function (data) {
if (data.value === '') {
if (data.value === "false") {
$('#add-website-db-info').hide()
return false
}
@@ -130,7 +130,7 @@ Date: 2023-06-24
})
// 提交
form.on('submit(add-website-submit)', function (data) {
data.field.db = data.field.db_type !== '';
data.field.db = data.field.db_type !== "false";
admin.req({
url: '/api/panel/website/add'
, type: 'post'
+15 -28
View File
@@ -1,9 +1,8 @@
<!--
Name: 网站 - 备份
Author: 耗子
Date: 2023-07-21
Date: 2023-07-24
-->
<h1>这里正在装修,下个版本再来看看吧!</h1>
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
<div class="layui-row">
<div class="layui-col-xs12 layui-col-sm12 layui-col-md12">
@@ -26,8 +25,7 @@ Date: 2023-07-21
<script>
layui.data.sendParams = function (params) {
layui.use(['admin', 'form', 'laydate', 'code'], function () {
var $ = layui.$
, admin = layui.admin
var admin = layui.admin
, layer = layui.layer
, table = layui.table
, upload = layui.upload;
@@ -39,7 +37,7 @@ Date: 2023-07-21
, toolbar: '#website-backup-bar'
, title: '备份列表'
, cols: [[
{field: 'backup', title: '备份名称', width: 500}
{field: 'name', title: '备份名称', width: 500}
, {field: 'size', title: '文件大小'}
, {field: 'right', title: '操作', width: 150, toolbar: '#website-backup-control'}
]]
@@ -56,6 +54,7 @@ Date: 2023-07-21
index = layer.msg('正在上传备份文件,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
, shade: 0.3
});
}
, done: function (res) {
@@ -63,9 +62,6 @@ Date: 2023-07-21
layer.msg('上传成功!', {icon: 1});
table.reload('website-backup-list');
}
, error: function (res) {
layer.msg('上传失败:' + res.msg, {icon: 2});
}
});
}
});
@@ -75,26 +71,23 @@ Date: 2023-07-21
index = layer.msg('正在备份网站,请稍等...', {
icon: 16
, time: 0
, shade: 0.3
});
admin.req({
url: '/api/panel/website/createBackup'
, type: 'post'
, data: {
name: params.data.name
id: params.data.id
}
, success: function (result) {
layer.close(index);
if (result.code !== 0) {
console.log('耗子Linux面板:网站失败,接口返回' + result);
layer.alert('备份失败!');
return false;
}
table.reload('website-backup-list');
layer.msg('备份成功!', {icon: 1});
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error);
}
});
}
});
@@ -102,52 +95,46 @@ Date: 2023-07-21
table.on('tool(website-backup-list)', function (obj) {
let data = obj.data;
if (obj.event === 'del') {
layer.confirm('确定要删除网站备份 <b style="color: red;">' + data.backup + '</b> 吗?', function (index) {
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"
, method: 'post'
, 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.backup + '删除成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error);
layer.alert('网站备份' + data.name + '删除成功!');
}
});
});
} else if (obj.event === 'restore') {
layer.confirm('高风险操作,确定要恢复网站备份 <b style="color: red;">' + data.backup + '</b> 吗?', function (index) {
layer.confirm('高风险操作,确定要恢复网站备份 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
index = layer.msg('正在恢复网站备份,可能需要较长时间,请勿操作...', {
icon: 16
, time: 0
, shade: 0.3
});
data.name = params.data.name;
data.id = params.data.id;
admin.req({
url: "/api/panel/website/restoreBackup"
, method: 'post'
, 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.backup + '恢复成功!');
}
, error: function (xhr, status, error) {
console.log('耗子Linux面板:ajax请求出错,错误' + error);
layer.alert('网站备份' + data.name + '恢复成功!');
}
});
});
+4 -4
View File
@@ -1,7 +1,7 @@
<!--
Name: 网站 - 编辑
Author: 耗子
Date: 2023-07-21
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">
@@ -289,7 +289,7 @@ Date: 2023-07-21
layer.close(index)
layer.load()
admin.req({
url: '/api/panel/website/clearSiteLog'
url: '/api/panel/website/clearLog'
, type: 'post'
, data: {name: params.config.name}
, success: function (res) {
@@ -321,7 +321,7 @@ Date: 2023-07-21
url: '/api/panel/website/config'
, type: 'post'
, data: {
id: params.id,
id: params.data.id,
domains: domains.join('\n'),
ports: ports.join('\n'),
ssl: $('input[name="ssl"]').prop('checked'),
@@ -365,7 +365,7 @@ Date: 2023-07-21
admin.req({
url: '/api/panel/website/resetConfig'
, type: 'post'
, data: {id: params.id}
, data: {id: params.data.id}
, success: function (res) {
layer.close(index)
if (res.code === 0) {
+1 -2
View File
@@ -1,7 +1,7 @@
<!--
Name: 网站 - 列表
Author: 耗子
Date: 2023-07-71
Date: 2023-07-74
-->
<title>网站</title>
<div class="layui-fluid">
@@ -167,7 +167,6 @@ Date: 2023-07-71
, id: 'LAY-popup-website-edit'
, success: function (layero, index) {
view(this.id).render('website/edit', {
id : data.id,
php: php,
mysql: mysql,
postgresql: postgresql,
+69
View File
@@ -9,7 +9,11 @@ import (
"panel/app/http/controllers/plugins/openresty"
"panel/app/http/controllers/plugins/php74"
"panel/app/http/controllers/plugins/php80"
"panel/app/http/controllers/plugins/php81"
"panel/app/http/controllers/plugins/php82"
"panel/app/http/controllers/plugins/phpmyadmin"
"panel/app/http/controllers/plugins/s3fs"
"panel/app/http/controllers/plugins/supervisor"
"panel/app/http/middleware"
)
@@ -124,9 +128,74 @@ func Plugin() {
route.Post("installExtension", php80Controller.InstallExtension)
route.Post("uninstallExtension", php80Controller.UninstallExtension)
})
facades.Route().Prefix("api/plugins/php81").Middleware(middleware.Jwt()).Group(func(route route.Route) {
php81Controller := php81.NewPhp81Controller()
route.Get("status", php81Controller.Status)
route.Post("reload", php81Controller.Reload)
route.Post("start", php81Controller.Start)
route.Post("stop", php81Controller.Stop)
route.Post("restart", php81Controller.Restart)
route.Get("load", php81Controller.Load)
route.Get("config", php81Controller.GetConfig)
route.Post("config", php81Controller.SaveConfig)
route.Get("errorLog", php81Controller.ErrorLog)
route.Get("slowLog", php81Controller.SlowLog)
route.Post("clearErrorLog", php81Controller.ClearErrorLog)
route.Post("clearSlowLog", php81Controller.ClearSlowLog)
route.Get("extensions", php81Controller.GetExtensionList)
route.Post("installExtension", php81Controller.InstallExtension)
route.Post("uninstallExtension", php81Controller.UninstallExtension)
})
facades.Route().Prefix("api/plugins/php82").Middleware(middleware.Jwt()).Group(func(route route.Route) {
php82Controller := php82.NewPhp82Controller()
route.Get("status", php82Controller.Status)
route.Post("reload", php82Controller.Reload)
route.Post("start", php82Controller.Start)
route.Post("stop", php82Controller.Stop)
route.Post("restart", php82Controller.Restart)
route.Get("load", php82Controller.Load)
route.Get("config", php82Controller.GetConfig)
route.Post("config", php82Controller.SaveConfig)
route.Get("errorLog", php82Controller.ErrorLog)
route.Get("slowLog", php82Controller.SlowLog)
route.Post("clearErrorLog", php82Controller.ClearErrorLog)
route.Post("clearSlowLog", php82Controller.ClearSlowLog)
route.Get("extensions", php82Controller.GetExtensionList)
route.Post("installExtension", php82Controller.InstallExtension)
route.Post("uninstallExtension", php82Controller.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)
})
facades.Route().Prefix("api/plugins/s3fs").Middleware(middleware.Jwt()).Group(func(route route.Route) {
s3fsController := s3fs.NewS3fsController()
route.Get("list", s3fsController.List)
route.Post("add", s3fsController.Add)
route.Post("delete", s3fsController.Delete)
})
facades.Route().Prefix("api/plugins/supervisor").Middleware(middleware.Jwt()).Group(func(route route.Route) {
supervisorController := supervisor.NewSupervisorController()
route.Get("status", supervisorController.Status)
route.Post("start", supervisorController.Start)
route.Post("stop", supervisorController.Stop)
route.Post("restart", supervisorController.Restart)
route.Post("reload", supervisorController.Reload)
route.Get("log", supervisorController.Log)
route.Post("clearLog", supervisorController.ClearLog)
route.Get("config", supervisorController.Config)
route.Post("config", supervisorController.SaveConfig)
route.Get("processes", supervisorController.Processes)
route.Post("startProcess", supervisorController.StartProcess)
route.Post("stopProcess", supervisorController.StopProcess)
route.Post("restartProcess", supervisorController.RestartProcess)
route.Get("processLog", supervisorController.ProcessLog)
route.Post("clearProcessLog", supervisorController.ClearProcessLog)
route.Get("processConfig", supervisorController.ProcessConfig)
route.Post("processConfig", supervisorController.SaveProcessConfig)
route.Post("deleteProcess", supervisorController.DeleteProcess)
route.Post("addProcess", supervisorController.AddProcess)
})
}
+10
View File
@@ -22,6 +22,7 @@ func Web() {
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.Middleware(middleware.Jwt()).Post("restart", infoController.Restart)
})
r.Prefix("user").Group(func(r route.Route) {
userController := controllers.NewUserController()
@@ -48,6 +49,9 @@ func Web() {
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)
})
@@ -91,6 +95,12 @@ func Web() {
r.Get("list", monitorController.List)
r.Get("switchAndDays", monitorController.SwitchAndDays)
})
r.Prefix("ssh").Middleware(middleware.Jwt()).Group(func(r route.Route) {
sshController := controllers.NewSshController()
r.Get("info", sshController.GetInfo)
r.Post("info", sshController.UpdateInfo)
r.Get("session", sshController.Session)
})
r.Prefix("setting").Middleware(middleware.Jwt()).Group(func(r route.Route) {
settingController := controllers.NewSettingController()
r.Get("list", settingController.List)
+1 -1
View File
@@ -170,7 +170,7 @@ Init_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')
panelZip=$(curl "https://api.github.com/repos/HaoZi-Team/Panel/releases/latest" | jq -r '.assets[] | select(.name | contains("amd64v2")) | .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
+12 -10
View File
@@ -30,7 +30,7 @@ ipLocation=$(curl -s https://ip.ping0.cc/geo)
# 安装依赖
if [ "${OS}" == "centos" ]; then
# EPEL 9 仓库中目前没有 GeoIP-devel 包,暂时使用 Remi 仓库
dnf install https://rpms.remirepo.net/enterprise/remi-release-9.rpm
dnf install https://rpms.remirepo.net/enterprise/remi-release-9.rpm -y
if [[ ${ipLocation} =~ "中国" ]]; then
sed -e 's!^#mirrorlist=!mirrorlist=!g' -e 's!^mirrorlist=!#mirrorlist=!g' -e 's!^#baseurl=!baseurl=!g' -e 's!http://rpms.remirepo.net/enterprise!https://mirrors.tuna.tsinghua.edu.cn/remi/enterprise!g' -i /etc/yum.repos.d/remi*
fi
@@ -133,7 +133,7 @@ cd ${openrestyPath}/src
export LD_LIBRARY_PATH=/usr/local/lib/:$LD_LIBRARY_PATH
export LIB_UTHASH=${openrestyPath}/src/uthash
./configure --user=www --group=www --prefix=${openrestyPath} --with-luajit --add-module=${openrestyPath}/src/ngx_cache_purge --add-module=${openrestyPath}/src/nginx-sticky-module --with-openssl=${openrestyPath}/src/openssl --with-pcre=${openrestyPath}/src/pcre --with-http_v2_module --with-http_slice_module --with-threads --with-stream --with-stream_ssl_module --with-stream_realip_module --with-stream_geoip_module --with-stream_ssl_preread_module --with-http_stub_status_module --with-http_ssl_module --with-http_image_filter_module --with-http_gzip_static_module --with-http_gunzip_module --with-ipv6 --with-http_geoip_module --with-http_sub_module --with-http_flv_module --with-http_addition_module --with-http_realip_module --with-http_mp4_module --with-ld-opt="-Wl,-E" --with-cc-opt="-O2 -std=gnu99" --with-cpu-opt="amd64" --with-http_dav_module --add-module=${openrestyPath}/src/nginx-dav-ext-module --add-module=${openrestyPath}/src/ngx_brotli --add-module=${openrestyPath}/ngx_waf
./configure --user=www --group=www --prefix=${openrestyPath} --with-luajit --add-module=${openrestyPath}/src/ngx_cache_purge --add-module=${openrestyPath}/src/nginx-sticky-module --with-openssl=${openrestyPath}/src/openssl --with-pcre=${openrestyPath}/src/pcre --with-http_v2_module --with-http_slice_module --with-threads --with-stream --with-stream_ssl_module --with-stream_realip_module --with-stream_geoip_module --with-stream_ssl_preread_module --with-http_stub_status_module --with-http_ssl_module --with-http_image_filter_module --with-http_gzip_static_module --with-http_gunzip_module --with-ipv6 --with-http_geoip_module --with-http_sub_module --with-http_flv_module --with-http_addition_module --with-http_realip_module --with-http_mp4_module --with-ld-opt="-Wl,-E" --with-cc-opt="-std=gnu99" --with-http_dav_module --add-module=${openrestyPath}/src/nginx-dav-ext-module --add-module=${openrestyPath}/src/ngx_brotli --add-module=${openrestyPath}/ngx_waf
if [[ "${cpuCore}" -gt "1" ]]; then
make -j2
else
@@ -320,12 +320,13 @@ chmod -R 644 /www/server/vhost
echo "" > ${openrestyPath}/conf/enable-php-0.conf
# 自动为所有PHP版本创建配置文件
cd ${setupPath}/server/php
phpList=$(ls -l | grep ^d | awk '{print $NF}')
for phpVersion in ${phpList}; do
if [ -d "${setupPath}/server/php/${phpVersion}" ]; then
# 写入PHP配置文件
cat > ${openrestyPath}/conf/enable-php-${phpVersion}.conf << EOF
if [ -d "${setupPath}/server/php" ]; then
cd ${setupPath}/server/php
phpList=$(ls -l | grep ^d | awk '{print $NF}')
for phpVersion in ${phpList}; do
if [ -d "${setupPath}/server/php/${phpVersion}" ]; then
# 写入PHP配置文件
cat > ${openrestyPath}/conf/enable-php-${phpVersion}.conf << EOF
location ~ \.php$ {
try_files \$uri =404;
fastcgi_pass unix:/tmp/php-cgi-${phpVersion}.sock;
@@ -334,8 +335,9 @@ location ~ \.php$ {
include pathinfo.conf;
}
EOF
fi
done
fi
done
fi
# 写入代理默认配置文件
cat > ${openrestyPath}/conf/proxy.conf << EOF
+1 -1
View File
@@ -30,4 +30,4 @@ else
exit 1
fi
panel writePlugin s3fs
panel writePlugin s3fs 1.9
+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")
if [ "${OS}" == "centos" ]; then
dnf install -y supervisor
sed -i 's#files = supervisord.d/\*.ini#files = supervisord.d/*.conf#g' /etc/supervisord.conf
systemctl enable supervisord
systemctl start supervisord
elif [ "${OS}" == "debian" ]; then
apt install -y supervisor
systemctl enable supervisor
systemctl start supervisor
else
echo -e $HR
echo "错误:不支持的操作系统"
exit 1
fi
panel writePlugin supervisor 4.2.5
+33
View File
@@ -0,0 +1,33 @@
#!/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 remove -y supervisor
elif [ "${OS}" == "debian" ]; then
apt remove -y supervisor
else
echo -e $HR
echo "错误:不支持的操作系统"
exit 1
fi
panel deletePlugin supervisor