mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ffa32160b | |||
| 872da5f8e1 | |||
| eed3651cb1 | |||
| 653900fd73 | |||
| ff1f2569dd | |||
| 0c6354fc6e | |||
| 2ed5c36890 | |||
| ecaf8edf14 | |||
| 2429cf023a | |||
| c7adc39887 | |||
| f4e267ef1b | |||
| 98bb24d97a | |||
| 9e603a3003 | |||
| 7164541415 | |||
| a51b391634 | |||
| edd3b2a622 | |||
| c55ef5c1f2 | |||
| 65a46796ea | |||
| b4720a1c84 | |||
| 5ec6a88a60 | |||
| cc70ab1fde | |||
| cf58857fd1 | |||
| cc5bacedee | |||
| fe0fe810ab | |||
| 826b7f65f8 | |||
| ce7a380e6e | |||
| a128d295ea | |||
| 85db98ec10 | |||
| 44ae4b9d67 | |||
| d9b4162e63 | |||
| a15ee96156 | |||
| 4e71e8884a | |||
| 346bb2d2b9 | |||
| 0b476c2764 | |||
| bd5854cef3 | |||
| 5d7ae8d632 |
@@ -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 }}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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群咨询联系
|
||||
|
||||
|
||||
@@ -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} 删除面板设置数据")
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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: "(需先安装PostgreSQL)pdo_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: "(需先安装PostgreSQL)pdo_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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": "登录已过期",
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -2,7 +2,6 @@ package mysql57
|
||||
|
||||
var (
|
||||
Name = "MySQL-5.7"
|
||||
Author = "耗子"
|
||||
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。"
|
||||
Slug = "mysql57"
|
||||
Version = "5.7.42"
|
||||
|
||||
@@ -2,7 +2,6 @@ package mysql80
|
||||
|
||||
var (
|
||||
Name = "MySQL-8.0"
|
||||
Author = "耗子"
|
||||
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。(内存 < 4G 无法安装)"
|
||||
Slug = "mysql80"
|
||||
Version = "8.0.33"
|
||||
|
||||
@@ -2,7 +2,6 @@ package openresty
|
||||
|
||||
var (
|
||||
Name = "OpenResty"
|
||||
Author = "耗子"
|
||||
Description = "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台。"
|
||||
Slug = "openresty"
|
||||
Version = "1.21.4.1"
|
||||
|
||||
@@ -2,7 +2,6 @@ package php74
|
||||
|
||||
var (
|
||||
Name = "PHP-7.4"
|
||||
Author = "耗子"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php74"
|
||||
Version = "7.4.33"
|
||||
|
||||
@@ -2,7 +2,6 @@ package php80
|
||||
|
||||
var (
|
||||
Name = "PHP-8.0"
|
||||
Author = "耗子"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php80"
|
||||
Version = "8.0.29"
|
||||
|
||||
@@ -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`
|
||||
)
|
||||
@@ -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`
|
||||
)
|
||||
@@ -2,7 +2,6 @@ package phpmyadmin
|
||||
|
||||
var (
|
||||
Name = "phpMyAdmin"
|
||||
Author = "耗子"
|
||||
Description = "phpMyAdmin 是一个以 PHP 为基础,以 Web-Base 方式架构在网站主机上的 MySQL 数据库管理工具。"
|
||||
Slug = "phpmyadmin"
|
||||
Version = "5.2.1"
|
||||
|
||||
@@ -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`
|
||||
)
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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
@@ -8,6 +8,6 @@ func init() {
|
||||
config := facades.Config()
|
||||
config.Add("panel", map[string]any{
|
||||
"name": "耗子面板",
|
||||
"version": "v2.0.12",
|
||||
"version": "v2.0.22",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"}))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}]);
|
||||
|
||||
|
||||
@@ -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 + '恢复成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
// 面板设置
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 + '恢复成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,4 +30,4 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
panel writePlugin s3fs
|
||||
panel writePlugin s3fs 1.9
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user