mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
Compare commits
150 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e260652373 | |||
| 427f7dc230 | |||
| c32ae4a733 | |||
| 34d9d245e1 | |||
| 2a2008b959 | |||
| bab4efa807 | |||
| 5052170046 | |||
| 32fef1e9e4 | |||
| 67ff105cba | |||
| 514baf4fb3 | |||
| 30f9fada39 | |||
| 8b01035d1a | |||
| 883e489ca1 | |||
| d16854ceaf | |||
| bdb5442659 | |||
| f991586f94 | |||
| b7869c36dc | |||
| 83d7aec8f5 | |||
| b43e5b5c80 | |||
| acb792950a | |||
| 41d83719ff | |||
| de1c269a4a | |||
| c3bf62ebf7 | |||
| 317851d982 | |||
| f52d95ae11 | |||
| 6420092c90 | |||
| 2fa108dd67 | |||
| 86bfcfb9f1 | |||
| db65a493dc | |||
| 4dd99a149c | |||
| 5d1fbb2e2b | |||
| 94569cca02 | |||
| 905aaf102b | |||
| d91aa666cc | |||
| 4f90bbc177 | |||
| f6c3bd27fd | |||
| 137c5ceba4 | |||
| c6f5b99373 | |||
| d7f647a8db | |||
| 77277a6af6 | |||
| d9acaec3c5 | |||
| c8692844b6 | |||
| 0b49301e9e | |||
| 30fded4552 | |||
| 9b3b76d1d0 | |||
| f37f22b86b | |||
| 52eedad3da | |||
| 0e2dda597f | |||
| 313511b474 | |||
| d2ed2b5082 | |||
| cce19e1a64 | |||
| 62b84a8909 | |||
| ff7b4f5f3b | |||
| 43a8d8cc2a | |||
| bf09ffd316 | |||
| b0d08e2901 | |||
| 62684ccfda | |||
| 6353231f4a | |||
| 6abd6b139f | |||
| 40358b2551 | |||
| 6e6342f0a6 | |||
| cea485133f | |||
| 08e14a84a5 | |||
| bc179923a0 | |||
| 67ef7ccd7a | |||
| 1abb9691f4 | |||
| 4dc2895781 | |||
| e7635d4b36 | |||
| e731c025b5 | |||
| 8234a16f9e | |||
| 0e3dba7dc8 | |||
| 7e8ef74bee | |||
| 26ef34ef9b | |||
| 4aa6dfa1ee | |||
| 51989254d6 | |||
| 5cfeb49468 | |||
| 614a5f15b5 | |||
| f5a6f17108 | |||
| 6356b82a21 | |||
| b3b1afe77e | |||
| f9628643be | |||
| c8352d567d | |||
| 2b92a0c693 | |||
| 8fec8af0d7 | |||
| 5b83e042c2 | |||
| 525cb154ca | |||
| 1cbd282e40 | |||
| 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 | |||
| adbca77687 | |||
| 6377a2111c | |||
| c9cc9f1fa7 | |||
| 49e32beb8a | |||
| f6f10aca26 | |||
| cb5d9acf3f | |||
| 903218dabb | |||
| 71f0e5d7cd | |||
| a5e1fbc232 | |||
| 1689ef4e4a | |||
| fb383f9f90 | |||
| 624fc9c77c | |||
| 793b2ee989 | |||
| e56cf991ae | |||
| 1daaab1e93 | |||
| b9f75534ef | |||
| 594dfea986 | |||
| 915403e3c0 | |||
| 0715a4e497 | |||
| 0ab4738e1d | |||
| 0d4f8bbea1 | |||
| ea59149175 | |||
| 87a7350f9a | |||
| 59243d5203 | |||
| 83e2526ce8 | |||
| 1f5718cd67 | |||
| 3d8f2948be |
@@ -58,7 +58,7 @@ body:
|
||||
attributes:
|
||||
label: 耗子面板版本 (HaoZi Panel Version)
|
||||
description: |
|
||||
请提供耗子面板的版本号。
|
||||
请提供面板的版本号。
|
||||
Please provide the version number of the panel.
|
||||
placeholder: "2.0.0"
|
||||
validations:
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: gomod
|
||||
directory: /
|
||||
labels:
|
||||
- "🤖 Dependencies"
|
||||
schedule:
|
||||
interval: daily
|
||||
groups:
|
||||
go-modules:
|
||||
patterns:
|
||||
- "*"
|
||||
open-pull-requests-limit: 100
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
labels:
|
||||
- "🤖 Dependencies"
|
||||
schedule:
|
||||
interval: daily
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
open-pull-requests-limit: 100
|
||||
@@ -1,22 +0,0 @@
|
||||
name: Code Review
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ opened, reopened, synchronize, labeled ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, '🚀 Review Ready') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: anc95/ChatGPT-CodeReview@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENAI_API_ENDPOINT: https://api.openai-sb.com/v1
|
||||
MODEL: gpt-3.5-turbo
|
||||
PROMPT: "Below is a code patch, please help me do a brief code review on it. Only answer important bug risks and/or important improvement suggestions, Answer in Chinese and answer must be concisely and short: "
|
||||
@@ -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 }}
|
||||
@@ -27,10 +27,10 @@ jobs:
|
||||
我们认为您的建议非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献!
|
||||
We think your suggestion is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution!
|
||||
|
||||
"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 }}
|
||||
@@ -41,4 +41,4 @@ jobs:
|
||||
我们认为您的反馈非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献!
|
||||
We think your feedback is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution!
|
||||
|
||||
"Talk is cheap, Show me the Code." - Linus Torvalds
|
||||

|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
name: PR Check Title
|
||||
on:
|
||||
pull_request:
|
||||
jobs:
|
||||
pr-check-title:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: Slashgear/action-check-pr-title@v4.3.0
|
||||
with:
|
||||
regexp: "^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release|revert)(\\(.+\\))?!?: .+"
|
||||
helpMessage: "Your PR title is invalid. Please follow the Conventional Commits specification: https://www.conventionalcommits.org/zh-hans/v1.0.0/"
|
||||
+3
-4
@@ -1,8 +1,6 @@
|
||||
tmp
|
||||
.env
|
||||
.history
|
||||
.air.toml
|
||||
panel.conf
|
||||
/.air.toml
|
||||
/panel.conf
|
||||
|
||||
# Golang #
|
||||
# `go test -c` 生成的二进制文件
|
||||
@@ -23,6 +21,7 @@ _cgo_export.*
|
||||
*.exe
|
||||
*.o
|
||||
*.so
|
||||
/panel
|
||||
|
||||
# 压缩包 #
|
||||
# Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit
|
||||
|
||||
@@ -1,69 +1,128 @@
|
||||
# 耗子Linux面板
|
||||
|
||||
[](https://github.com/HaoZi-Team/Panel/releases)
|
||||
[](https://github.com/HaoZi-Team/Panel/actions)
|
||||
[](https://goreportcard.com/report/github.com/HaoZi-Team/Panel)
|
||||
[](https://codecov.io/gh/HaoZi-Team/Panel)
|
||||

|
||||
[](https://github.com/haozi-team/panel/releases)
|
||||
[](https://github.com/haozi-team/panel/actions)
|
||||
[](https://goreportcard.com/report/github.com/haozi-team/panel)
|
||||
[](https://codecov.io/gh/haozi-team/panel)
|
||||

|
||||
|
||||
这是耗子Linux面板的开源仓库,基于Apache License 2.0协议进行开源,目前处于积极开发状态。
|
||||
这是耗子Linux面板,一种使用GO开发的轻量Linux服务器运维管理面板。
|
||||
|
||||
#### 我们正在进行基于 Golang 的 V2 版本的开发,加入我们的 交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[耗子](https://pd.qq.com/s/fyol46wfy) 获取最新动态。
|
||||
通知:严禁使用耗子Linux面板从事任何非法活动,非法站点请勿向我们请求任何形式的技术支持,如果在技术支持过程中发现非法内容,我们将立即停止技术支持并留存相关证据。
|
||||
|
||||
广告: [`WeAvatar` —统一头像服务](https://weavatar.com)
|
||||
#### 交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[pd.qq.com/s/fyol46wfy](https://pd.qq.com/s/fyol46wfy)
|
||||
|
||||
广告: [`WeAvatar` 多端多元化的统一头像服务](https://weavatar.com)
|
||||
|
||||
## 运行环境
|
||||
|
||||
面板仅支持主流 `amd64` | `arm64` 系统的最新版本,且随着系统版本的更新,面板也会逐步停止对旧版本的支持。
|
||||
面板仅支持主流 `amd64` | `arm64` 系统的最新版本,且随着系统版本的更新,面板也会逐步停止对旧版本系统的支持。
|
||||
|
||||
不支持 `Ubuntu`,因为其发版太频繁,难以维护。
|
||||
不支持 `Ubuntu`,因为其发版太过频繁,难以维护。
|
||||
|
||||
低配机器建议使用 `Debian`,资源占用较 `RHEL` 系更低。其他机器建议使用 `RockyLinux` | `AlmaLinux`,维护周期更长。
|
||||
|
||||
不在下表中的其他同版本的 `RHEL` 系统(OpenCloudOS、Anolis、CentOS Stream等),可自行尝试安装,但不保证能够正常运行,且不提供技术支持。
|
||||
不在下表中的其他系统(OpenCloudOS 8、Anolis 8、CentOS Stream 8/9、Debian 11等),可自行尝试安装,但不保证能够正常运行,且不提供技术支持(理论上不会有大问题)。
|
||||
|
||||
| 系统 | 版本 |
|
||||
|------------|----|
|
||||
| RHEL | 9 |
|
||||
| RockyLinux | 9 |
|
||||
| AlmaLinux | 9 |
|
||||
| Debian | 12 |
|
||||
|
||||
## 安装面板
|
||||
|
||||
安装面板前,你需要了解LNMP环境的基本知识,以及如何处理常见的LNMP环境问题,我们不建议0基础的用户安装和使用耗子Linux面板。
|
||||
|
||||
如果你决定继续,请以`root`用户登录服务器,执行以下命令安装面板:
|
||||
|
||||
```shell
|
||||
TODO
|
||||
bash <(curl -sSL https://dl.cdn.haozi.net/panel/install_panel.sh)
|
||||
```
|
||||
|
||||
## 日常维护
|
||||
|
||||
使用`panel`命令进行日常维护:
|
||||
|
||||
```shell
|
||||
TODO
|
||||
panel
|
||||
```
|
||||
|
||||
## 问题反馈
|
||||
|
||||
使用类问题,可在 [WePublish社区论坛](https://wepublish.cn/forums) 提问寻求帮助。
|
||||
使用类问题,可在 [WePublish社区论坛](https://wepublish.cn/forums) 提问或QQ群`@汤姆猫`寻求帮助。
|
||||
|
||||
对于面板自身问题,可在 GitHub 的`Issues`
|
||||
面板自身问题,可在 GitHub 的`Issues`
|
||||
页面提交问题反馈,注意[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。
|
||||
|
||||
交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[耗子](https://pd.qq.com/s/fyol46wfy)
|
||||
## 贡献代码
|
||||
|
||||
### 寻找/创建 Issue
|
||||
|
||||
您可以在 [Issue 列表](https://github.com/haozi-team/panel/issues) 中寻找或创建一个 Issue,留言表达想要处理该 Issue
|
||||
的意愿,得到维护者的确认后,即可开始处理。
|
||||
|
||||
### 创建 PR
|
||||
|
||||
- 在开发过程中,如果遇到问题可以随时在 Issue 中详尽描述该问题,以进一步沟通,但在此之前请确保自己已通过 Google
|
||||
等方式尽可能的尝试解决问题;
|
||||
- PR 须提交至我们的极狐GitLab仓库[https://jihulab.com/haozi-team/panel](https://jihulab.com/haozi-team/panel)
|
||||
- 当 PR 开发完毕后,请为其添加 `🚀 Review Ready` 标签,维护者将及时进行评审;
|
||||
- 我们非常欢迎您的贡献,将在下次发版时将您添加到首页贡献者中;❤️
|
||||
|
||||
## 赞助商
|
||||
|
||||
### 服务器
|
||||
|
||||
- [盾云](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)
|
||||
|
||||
- [AnyCast.Ai](https://www.anycast.ai/)
|
||||
|
||||
### DevOps
|
||||
|
||||
- [极狐GitLab](https://www.jihulab.com/)
|
||||
|
||||
#### 接受云资源和资金赞助,可通过QQ群咨询联系
|
||||
|
||||
## Star 趋势
|
||||
## 行为准则
|
||||
|
||||
[](https://starchart.cc/HaoZi-Team/Panel)
|
||||
耗子Linux面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
|
||||
|
||||
- 参与者将容忍反对意见。
|
||||
- 参与者必须确保他们的语言和行为没有人身攻击和贬低个人言论。
|
||||
- 在解释他人的言行时,参与者应始终保持良好的意图。
|
||||
- 不能容忍可合理视为骚扰的行为。
|
||||
|
||||
## 安全说明
|
||||
|
||||
安全性是我们最关心的问题之一,我们已在生产环境广泛应用耗子Linux面板。
|
||||
|
||||
耗子Linux面板采用业界多种方案尽可能保证面板的安全性,但是我们不能保证面板的绝对安全性,**因此我们不对面板的安全性做任何保证**。
|
||||
|
||||
如果您在使用面板的过程中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
|
||||
|
||||
- 邮箱:i@haozi.net
|
||||
- QQ:826896000
|
||||
|
||||
## 贡献者
|
||||
|
||||
这个项目的存在要归功于所有做出贡献的人,参与贡献请先查看贡献代码部分。
|
||||
|
||||
<a href="https://github.com/DevHaoZi" target="_blank"><img src="https://avatars.githubusercontent.com/u/115467771?v=4" width="48" height="48"></a>
|
||||
|
||||
## Star 历史
|
||||
|
||||
<a href="https://star-history.com/#haozi-team/panel&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
"github.com/goravel/framework/contracts/console/command"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
@@ -36,10 +38,8 @@ func (receiver *Monitoring) Extend() command.Extend {
|
||||
// Handle Execute the console command.
|
||||
func (receiver *Monitoring) Handle(ctx console.Context) error {
|
||||
var setting models.Setting
|
||||
if err := facades.Orm().Query().Where("key", "monitor").First(&setting); err != nil {
|
||||
return nil
|
||||
}
|
||||
if setting.Value == "0" || len(setting.Value) == 0 {
|
||||
monitor := services.NewSettingImpl().Get(models.SettingKeyMonitor)
|
||||
if !cast.ToBool(monitor) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+237
-17
@@ -1,14 +1,18 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/console/command"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
@@ -40,6 +44,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":
|
||||
@@ -50,7 +56,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子Linux面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyPanelEntrance, Value: "/"}}
|
||||
settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子Linux面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyEntrance, Value: "/"}, {Key: models.SettingKeyVersion, Value: facades.Config().GetString("panel.version")}}
|
||||
err = facades.Orm().Query().Create(&settings)
|
||||
if err != nil {
|
||||
color.Redln("初始化失败")
|
||||
@@ -73,7 +79,19 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
color.Greenln("初始化成功")
|
||||
|
||||
case "update":
|
||||
err := tools.UpdatePanel()
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
|
||||
if err == nil {
|
||||
color.Redln("当前有任务正在执行,禁止更新")
|
||||
return nil
|
||||
}
|
||||
|
||||
input := arg1
|
||||
proxy := false
|
||||
if input == "y" || input == "Y" || input == "yes" || input == "Yes" {
|
||||
proxy = true
|
||||
}
|
||||
err = tools.UpdatePanel(cast.ToBool(proxy))
|
||||
if err != nil {
|
||||
color.Redln("更新失败: " + err.Error())
|
||||
return nil
|
||||
@@ -105,19 +123,28 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
port := tools.ExecShell("cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}'")
|
||||
port := tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
|
||||
color.Greenln("用户名: " + user.Username)
|
||||
color.Greenln("密码: " + password)
|
||||
color.Greenln("面板端口: " + port)
|
||||
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyPanelEntrance, "/"))
|
||||
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
|
||||
|
||||
case "getPort":
|
||||
port := tools.ExecShell("cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}'")
|
||||
port := tools.Exec("cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}'")
|
||||
color.Greenln("面板端口: " + port)
|
||||
|
||||
case "getEntrance":
|
||||
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyPanelEntrance, "/"))
|
||||
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
|
||||
|
||||
case "deleteEntrance":
|
||||
err := services.NewSettingImpl().Set(models.SettingKeyEntrance, "/")
|
||||
if err != nil {
|
||||
color.Redln("删除面板入口失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("删除面板入口成功")
|
||||
|
||||
case "writePlugin":
|
||||
slug := arg1
|
||||
@@ -177,16 +204,192 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
|
||||
color.Greenln("写入MySQL root密码成功")
|
||||
|
||||
case "cleanRunningTask":
|
||||
_, err := facades.Orm().Query().Model(&models.Task{}).Where("status", models.TaskStatusRunning).Update("status", models.TaskStatusFailed)
|
||||
case "cleanTask":
|
||||
_, err := facades.Orm().Query().Model(&models.Task{}).Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).Update("status", models.TaskStatusFailed)
|
||||
if err != nil {
|
||||
color.Redln("清理正在运行的任务失败")
|
||||
color.Redln("清理任务失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("清理正在运行的任务成功")
|
||||
color.Greenln("清理任务成功")
|
||||
|
||||
case "backup":
|
||||
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.Exec(`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.Exec(`mysqldump -uroot ` + name + ` > /tmp/` + backupFile + ` 2>&1`)
|
||||
color.Greenln("|-导出成功")
|
||||
color.Greenln("|-开始压缩")
|
||||
tools.Exec("cd /tmp && zip -r " + backupFile + ".zip " + backupFile)
|
||||
tools.Remove("/tmp/" + backupFile)
|
||||
color.Greenln("|-压缩成功")
|
||||
color.Greenln("|-开始移动")
|
||||
if _, err := tools.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil {
|
||||
color.Redln("|-移动失败: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
color.Greenln("|-移动成功")
|
||||
_ = os.Unsetenv("MYSQL_PWD")
|
||||
color.Greenln("|-备份成功")
|
||||
|
||||
case "postgresql":
|
||||
backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
|
||||
check := tools.Exec(`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.Exec(`su - postgres -c "pg_dump '` + name + `'" > /tmp/` + backupFile + ` 2>&1`)
|
||||
color.Greenln("|-导出成功")
|
||||
color.Greenln("|-开始压缩")
|
||||
tools.Exec("cd /tmp && zip -r " + backupFile + ".zip " + backupFile)
|
||||
tools.Remove("/tmp/" + backupFile)
|
||||
color.Greenln("|-压缩成功")
|
||||
color.Greenln("|-开始移动")
|
||||
if _, err := tools.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil {
|
||||
color.Redln("|-移动失败: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
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.Remove(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.Exec(`cd /www/wwwlogs && zip -r ` + backupPath + ` ` + website.Name + ".log")
|
||||
tools.Exec(`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.Remove(fileToDelete)
|
||||
}
|
||||
color.Greenln("|-清理完成")
|
||||
color.Greenln(hr)
|
||||
color.Greenln("☆ 切割完成 [" + carbon.Now().ToDateTimeString() + "]")
|
||||
color.Greenln(hr)
|
||||
|
||||
case "writeSite":
|
||||
name := arg1
|
||||
@@ -261,6 +464,20 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
|
||||
color.Greenln("写入设置成功")
|
||||
|
||||
case "getSetting":
|
||||
key := arg1
|
||||
if len(key) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
var setting models.Setting
|
||||
if err := facades.Orm().Query().Where("key", key).FirstOrFail(&setting); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s", setting.Value)
|
||||
|
||||
case "deleteSetting":
|
||||
key := arg1
|
||||
if len(key) == 0 {
|
||||
@@ -279,20 +496,23 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
default:
|
||||
color.Yellowln(facades.Config().GetString("panel.name") + "命令行工具 - " + facades.Config().GetString("panel.version"))
|
||||
color.Greenln("请使用以下命令:")
|
||||
color.Greenln("panel update 更新/修复面板到最新版本")
|
||||
color.Greenln("panel update {proxy} 更新 / 修复面板到最新版本")
|
||||
color.Greenln("panel getInfo 重新初始化面板账号信息")
|
||||
color.Greenln("panel getPort 获取面板访问端口")
|
||||
color.Greenln("panel getEntrance 获取面板访问入口")
|
||||
color.Greenln("panel cleanRunningTask 强制清理面板正在运行的任务")
|
||||
color.Greenln("panel 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} 写入插件安装状态")
|
||||
color.Yellowln("panel writePlugin {slug} {version} 写入插件安装状态")
|
||||
color.Yellowln("panel deletePlugin {slug} 移除插件安装状态")
|
||||
color.Yellowln("panel writeMysqlPassword {password} 写入MySQL root密码")
|
||||
color.Yellowln("panel writeSite {name} {status} {path} {php} {ssl} 写入网站数据到面板")
|
||||
color.Yellowln("panel deleteSite {name} 删除面板网站数据")
|
||||
color.Yellowln("panel writeSetting {name} {value} 写入/更新面板设置数据")
|
||||
color.Yellowln("panel getSetting {name} 获取面板设置数据")
|
||||
color.Yellowln("panel writeSetting {name} {value} 写入 / 更新面板设置数据")
|
||||
color.Yellowln("panel deleteSetting {name} 删除面板设置数据")
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package console
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/schedule"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/console/commands"
|
||||
)
|
||||
@@ -11,7 +12,9 @@ type Kernel struct {
|
||||
}
|
||||
|
||||
func (kernel *Kernel) Schedule() []schedule.Event {
|
||||
return []schedule.Event{}
|
||||
return []schedule.Event{
|
||||
facades.Schedule().Command("panel:monitoring").EveryMinute().SkipIfStillRunning(),
|
||||
}
|
||||
}
|
||||
|
||||
func (kernel *Kernel) Commands() []console.Command {
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"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")
|
||||
|
||||
@@ -41,22 +44,72 @@ 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|regex:^((\\*|\\d+|\\d+-\\d+|\\d+\\/\\d+|\\d+-\\d+\\/\\d+|\\*\\/\\d+)(\\,(\\*|\\d+|\\d+-\\d+|\\d+\\/\\d+|\\d+-\\d+\\/\\d+|\\*\\/\\d+))*\\s?){5}$",
|
||||
"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())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
// 写入shell
|
||||
// 单独验证时间格式
|
||||
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) {
|
||||
Error(ctx, http.StatusBadRequest, "时间格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
shell := 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")
|
||||
if len(backupName) == 0 {
|
||||
backupPath = 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
|
||||
|
||||
# 耗子Linux面板 - 数据备份脚本
|
||||
|
||||
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
|
||||
|
||||
# 耗子Linux面板 - 日志切割脚本
|
||||
|
||||
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) {
|
||||
@@ -70,16 +123,16 @@ func (r *CronController) Add(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + tools.RandomString(16)
|
||||
if !tools.WriteFile(shellDir+shellFile+".sh", ctx.Request().Input("script"), 0644) {
|
||||
if !tools.Write(shellDir+shellFile+".sh", shell, 0700) {
|
||||
facades.Log().Error("[面板][CronController] 创建计划任务脚本失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
tools.ExecShell("dos2unix " + shellDir + shellFile + ".sh")
|
||||
tools.Exec("dos2unix " + shellDir + shellFile + ".sh")
|
||||
|
||||
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"
|
||||
@@ -92,18 +145,29 @@ func (r *CronController) Add(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
r.cron.AddToSystem(cron)
|
||||
c.cron.AddToSystem(cron)
|
||||
|
||||
Success(ctx, http.Json{
|
||||
"id": cron.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *CronController) Update(ctx http.Context) {
|
||||
// Script 获取脚本内容
|
||||
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 {
|
||||
Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
return
|
||||
}
|
||||
|
||||
Success(ctx, tools.Read(cron.Shell))
|
||||
}
|
||||
|
||||
func (c *CronController) Update(ctx http.Context) {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"id": "required|int",
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
"time": "required|regex:^((\\*|\\d+|\\d+-\\d+|\\d+\\/\\d+|\\d+-\\d+\\/\\d+|\\*\\/\\d+)(\\,(\\*|\\d+|\\d+-\\d+|\\d+\\/\\d+|\\d+-\\d+\\/\\d+|\\*\\/\\d+))*\\s?){5}$",
|
||||
"time": "required",
|
||||
"script": "required",
|
||||
})
|
||||
if err != nil {
|
||||
@@ -111,7 +175,13 @@ func (r *CronController) Update(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
// 单独验证时间格式
|
||||
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) {
|
||||
Error(ctx, http.StatusBadRequest, "时间格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,41 +192,46 @@ func (r *CronController) Update(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !tools.WriteFile(cron.Shell, ctx.Request().Input("script"), 0644) {
|
||||
if !cron.Status {
|
||||
Error(ctx, http.StatusBadRequest, "计划任务已禁用")
|
||||
return
|
||||
}
|
||||
|
||||
cron.Time = ctx.Request().Input("time")
|
||||
cron.Name = ctx.Request().Input("name")
|
||||
err = facades.Orm().Query().Save(&cron)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][CronController] 更新计划任务失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
if !tools.Write(cron.Shell, ctx.Request().Input("script"), 0644) {
|
||||
facades.Log().Error("[面板][CronController] 更新计划任务脚本失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
tools.ExecShell("dos2unix " + cron.Shell)
|
||||
tools.Exec("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) {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"id": "required|int",
|
||||
})
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
return
|
||||
}
|
||||
|
||||
func (c *CronController) Delete(ctx http.Context) {
|
||||
var cron models.Cron
|
||||
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
return
|
||||
}
|
||||
|
||||
c.cron.DeleteFromSystem(cron)
|
||||
tools.Remove(cron.Shell)
|
||||
|
||||
_, err = facades.Orm().Query().Delete(&cron)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][CronController] 删除计划任务失败 ", err)
|
||||
@@ -164,22 +239,19 @@ func (r *CronController) Delete(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
r.cron.DeleteFromSystem(cron)
|
||||
|
||||
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{
|
||||
"id": "required|int",
|
||||
"status": "required|in:true,false",
|
||||
"status": "bool",
|
||||
})
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -191,36 +263,24 @@ func (r *CronController) Status(ctx http.Context) {
|
||||
}
|
||||
|
||||
cron.Status = ctx.Request().InputBool("status")
|
||||
_, err = facades.Orm().Query().Update(&cron)
|
||||
err = facades.Orm().Query().Save(&cron)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][CronController] 更新计划任务状态失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
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) {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"id": "required|int",
|
||||
})
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
return
|
||||
}
|
||||
|
||||
func (c *CronController) Log(ctx http.Context) {
|
||||
var cron models.Cron
|
||||
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
return
|
||||
@@ -231,7 +291,5 @@ func (r *CronController) Log(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
Success(ctx, http.Json{
|
||||
"log": tools.ReadFile(cron.Log),
|
||||
})
|
||||
Success(ctx, tools.Read(cron.Log))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
package controllers
|
||||
|
||||
import "github.com/goravel/framework/contracts/http"
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
func Success(ctx http.Context, data any) {
|
||||
ctx.Response().Success().Json(http.Json{
|
||||
@@ -16,3 +23,51 @@ func Error(ctx http.Context, code int, message any) {
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// Check 检查插件是否可用
|
||||
func Check(ctx http.Context, slug string) bool {
|
||||
plugin := services.NewPluginImpl().GetBySlug(slug)
|
||||
installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug)
|
||||
installedPlugins, err := services.NewPluginImpl().AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][插件] 获取已安装插件失败")
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return false
|
||||
}
|
||||
|
||||
if installedPlugin.Version != plugin.Version || installedPlugin.Slug != plugin.Slug {
|
||||
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要更新至 "+plugin.Version+" 版本")
|
||||
return false
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
pluginsMap[p.Slug] = true
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
lock.RLock()
|
||||
_, requireFound := pluginsMap[require]
|
||||
lock.RUnlock()
|
||||
if !requireFound {
|
||||
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
@@ -19,16 +21,16 @@ type MenuItem struct {
|
||||
}
|
||||
|
||||
type InfoController struct {
|
||||
// Dependent services
|
||||
plugin services.Plugin
|
||||
}
|
||||
|
||||
func NewInfoController() *InfoController {
|
||||
return &InfoController{
|
||||
// Inject services
|
||||
plugin: services.NewPluginImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *InfoController) Name(ctx http.Context) {
|
||||
func (c *InfoController) Name(ctx http.Context) {
|
||||
var setting models.Setting
|
||||
err := facades.Orm().Query().Where("key", "name").First(&setting)
|
||||
if err != nil {
|
||||
@@ -42,20 +44,21 @@ func (r *InfoController) Name(ctx http.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (r *InfoController) Menu(ctx http.Context) {
|
||||
func (c *InfoController) Menu(ctx http.Context) {
|
||||
Success(ctx, []MenuItem{
|
||||
{Name: "home", Title: "主页", Icon: "layui-icon-home", Jump: "/"},
|
||||
{Name: "website", Title: "网站管理", Icon: "layui-icon-website", Jump: "website/list"},
|
||||
{Name: "monitor", Title: "资源监控", Icon: "layui-icon-chart-screen", Jump: "monitor"},
|
||||
{Name: "safe", Title: "系统安全", Icon: "layui-icon-auz", Jump: "safe"},
|
||||
{Name: "file", Title: "文件管理", Icon: "layui-icon-file", Jump: "file"},
|
||||
/*{Name: "file", Title: "文件管理", Icon: "layui-icon-file", Jump: "file"},*/
|
||||
{Name: "cron", Title: "计划任务", Icon: "layui-icon-date", Jump: "cron"},
|
||||
{Name: "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"},
|
||||
})
|
||||
}
|
||||
|
||||
func (r *InfoController) HomePlugins(ctx http.Context) {
|
||||
func (c *InfoController) HomePlugins(ctx http.Context) {
|
||||
var plugins []models.Plugin
|
||||
err := facades.Orm().Query().Where("show", 1).Find(&plugins)
|
||||
if err != nil {
|
||||
@@ -80,11 +83,11 @@ func (r *InfoController) HomePlugins(ctx http.Context) {
|
||||
Success(ctx, pluginsJson)
|
||||
}
|
||||
|
||||
func (r *InfoController) NowMonitor(ctx http.Context) {
|
||||
func (c *InfoController) NowMonitor(ctx http.Context) {
|
||||
Success(ctx, tools.GetMonitoringInfo())
|
||||
}
|
||||
|
||||
func (r *InfoController) SystemInfo(ctx http.Context) {
|
||||
func (c *InfoController) SystemInfo(ctx http.Context) {
|
||||
monitorInfo := tools.GetMonitoringInfo()
|
||||
|
||||
Success(ctx, http.Json{
|
||||
@@ -94,7 +97,7 @@ func (r *InfoController) SystemInfo(ctx http.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (r *InfoController) InstalledDbAndPhp(ctx http.Context) {
|
||||
func (c *InfoController) InstalledDbAndPhp(ctx http.Context) {
|
||||
var php []models.Plugin
|
||||
err := facades.Orm().Query().Where("slug like ?", "php%").Find(&php)
|
||||
if err != nil {
|
||||
@@ -116,9 +119,83 @@ func (r *InfoController) InstalledDbAndPhp(ctx http.Context) {
|
||||
postgresqlInstalled = false
|
||||
}
|
||||
|
||||
type data struct {
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
var phpData []data
|
||||
phpData = append(phpData, data{Slug: "0", Name: "不使用"})
|
||||
for _, p := range php {
|
||||
match := regexp.MustCompile(`php(\d+)`).FindStringSubmatch(p.Slug)
|
||||
if len(match) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
phpData = append(phpData, data{Slug: strings.ReplaceAll(p.Slug, "php", ""), Name: c.plugin.GetBySlug(p.Slug).Name})
|
||||
}
|
||||
|
||||
Success(ctx, http.Json{
|
||||
"php": php,
|
||||
"php": phpData,
|
||||
"mysql": mysqlInstalled,
|
||||
"postgresql": postgresqlInstalled,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *InfoController) CheckUpdate(ctx http.Context) {
|
||||
version := facades.Config().GetString("panel.version")
|
||||
remote, err := tools.GetLatestPanelVersion()
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
|
||||
return
|
||||
}
|
||||
|
||||
if version == remote.Version {
|
||||
Success(ctx, http.Json{
|
||||
"update": false,
|
||||
"version": remote.Version,
|
||||
"name": remote.Name,
|
||||
"body": remote.Body,
|
||||
"date": remote.Date,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
Success(ctx, http.Json{
|
||||
"update": true,
|
||||
"version": remote.Version,
|
||||
"name": remote.Name,
|
||||
"body": remote.Body,
|
||||
"date": remote.Date,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *InfoController) Update(ctx http.Context) {
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
|
||||
if err == nil {
|
||||
Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止更新")
|
||||
return
|
||||
}
|
||||
|
||||
proxy := ctx.Request().InputBool("proxy")
|
||||
err = tools.UpdatePanel(proxy)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][InfoController] 更新面板失败 ", err.Error())
|
||||
Error(ctx, http.StatusInternalServerError, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Success(ctx, nil)
|
||||
}
|
||||
|
||||
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.Exec("systemctl restart panel")
|
||||
Success(ctx, nil)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
"panel/app/models"
|
||||
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
@@ -46,9 +47,20 @@ func (r *MonitorController) SaveDays(ctx http.Context) {
|
||||
Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SwitchAndDays 监控开关和监控天数
|
||||
func (r *MonitorController) SwitchAndDays(ctx http.Context) {
|
||||
monitor := r.setting.Get(models.SettingKeyMonitor)
|
||||
monitorDays := r.setting.Get(models.SettingKeyMonitorDays)
|
||||
|
||||
Success(ctx, http.Json{
|
||||
"switch": cast.ToBool(monitor),
|
||||
"days": cast.ToInt(monitorDays),
|
||||
})
|
||||
}
|
||||
|
||||
// Clear 清空监控数据
|
||||
func (r *MonitorController) Clear(ctx http.Context) {
|
||||
_, err := facades.Orm().Query().Delete(&models.Monitor{})
|
||||
_, err := facades.Orm().Query().Where("1 = 1").Delete(&models.Monitor{})
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][MonitorController] 清空监控数据失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
@@ -60,18 +72,105 @@ func (r *MonitorController) Clear(ctx http.Context) {
|
||||
|
||||
// List 监控数据列表
|
||||
func (r *MonitorController) List(ctx http.Context) {
|
||||
start := ctx.Request().Input("start")
|
||||
end := ctx.Request().Input("end")
|
||||
startTime := carbon.Parse(start)
|
||||
endTime := carbon.Parse(end)
|
||||
start := ctx.Request().InputInt64("start")
|
||||
end := ctx.Request().InputInt64("end")
|
||||
startTime := carbon.FromTimestampMilli(start)
|
||||
endTime := carbon.FromTimestampMilli(end)
|
||||
|
||||
var monitors []models.Monitor
|
||||
err := facades.Orm().Query().Where("created_at", ">=", startTime).Where("created_at", "<=", endTime).Get(&monitors)
|
||||
err := facades.Orm().Query().Where("created_at >= ?", startTime.ToDateTimeString()).Where("created_at <= ?", endTime.ToDateTimeString()).Get(&monitors)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][MonitorController] 查询监控数据失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
Success(ctx, monitors)
|
||||
if len(monitors) == 0 {
|
||||
Error(ctx, http.StatusNotFound, "监控数据为空")
|
||||
return
|
||||
}
|
||||
|
||||
type load struct {
|
||||
Load1 []float64 `json:"load1"`
|
||||
Load5 []float64 `json:"load5"`
|
||||
Load15 []float64 `json:"load15"`
|
||||
}
|
||||
type cpu struct {
|
||||
Percent []string `json:"percent"`
|
||||
}
|
||||
type mem struct {
|
||||
Total string `json:"total"`
|
||||
Available []string `json:"available"`
|
||||
Used []string `json:"used"`
|
||||
}
|
||||
type swap struct {
|
||||
Total string `json:"total"`
|
||||
Used []string `json:"used"`
|
||||
Free []string `json:"free"`
|
||||
}
|
||||
type network struct {
|
||||
Sent []string `json:"sent"`
|
||||
Recv []string `json:"recv"`
|
||||
Tx []string `json:"tx"`
|
||||
Rx []string `json:"rx"`
|
||||
}
|
||||
type monitorData struct {
|
||||
Times []string `json:"times"`
|
||||
Load load `json:"load"`
|
||||
Cpu cpu `json:"cpu"`
|
||||
Mem mem `json:"mem"`
|
||||
Swap swap `json:"swap"`
|
||||
Net network `json:"net"`
|
||||
}
|
||||
|
||||
var data monitorData
|
||||
var bytesSent uint64
|
||||
var bytesRecv uint64
|
||||
var bytesSent2 uint64
|
||||
var bytesRecv2 uint64
|
||||
for _, net := range monitors[0].Info.Net {
|
||||
if net.Name == "lo" {
|
||||
continue
|
||||
}
|
||||
bytesSent += net.BytesSent
|
||||
bytesRecv += net.BytesRecv
|
||||
}
|
||||
for i, monitor := range monitors {
|
||||
// 跳过第一条数据,因为第一条数据的流量为 0
|
||||
if i == 0 {
|
||||
// MB
|
||||
data.Mem.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Total)/1024/1024)
|
||||
data.Swap.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Total)/1024/1024)
|
||||
continue
|
||||
}
|
||||
for _, net := range monitor.Info.Net {
|
||||
if net.Name == "lo" {
|
||||
continue
|
||||
}
|
||||
bytesSent2 += net.BytesSent
|
||||
bytesRecv2 += net.BytesRecv
|
||||
}
|
||||
data.Times = append(data.Times, monitor.CreatedAt.ToDateTimeString())
|
||||
data.Load.Load1 = append(data.Load.Load1, monitor.Info.Load.Load1)
|
||||
data.Load.Load5 = append(data.Load.Load5, monitor.Info.Load.Load5)
|
||||
data.Load.Load15 = append(data.Load.Load15, monitor.Info.Load.Load15)
|
||||
data.Cpu.Percent = append(data.Cpu.Percent, fmt.Sprintf("%.2f", monitor.Info.Percent[0]))
|
||||
data.Mem.Available = append(data.Mem.Available, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Available)/1024/1024))
|
||||
data.Mem.Used = append(data.Mem.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Used)/1024/1024))
|
||||
data.Swap.Used = append(data.Swap.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024))
|
||||
data.Swap.Free = append(data.Swap.Free, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Free)/1024/1024))
|
||||
data.Net.Sent = append(data.Net.Sent, fmt.Sprintf("%.2f", float64(bytesSent2/1024/1024)))
|
||||
data.Net.Recv = append(data.Net.Recv, fmt.Sprintf("%.2f", float64(bytesRecv2/1024/1024)))
|
||||
|
||||
// 监控频率为 1 分钟,所以这里除以 60 即可得到每秒的流量
|
||||
data.Net.Tx = append(data.Net.Tx, fmt.Sprintf("%.2f", float64(bytesSent2-bytesSent)/60/1024/1024))
|
||||
data.Net.Rx = append(data.Net.Rx, fmt.Sprintf("%.2f", float64(bytesRecv2-bytesRecv)/60/1024/1024))
|
||||
|
||||
bytesSent = bytesSent2
|
||||
bytesRecv = bytesRecv2
|
||||
bytesSent2 = 0
|
||||
bytesRecv2 = 0
|
||||
}
|
||||
|
||||
Success(ctx, data)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -80,35 +78,53 @@ func (r *PluginController) List(ctx http.Context) {
|
||||
// Install 安装插件
|
||||
func (r *PluginController) Install(ctx http.Context) {
|
||||
slug := ctx.Request().Input("slug")
|
||||
plugins := r.plugin.All()
|
||||
|
||||
var plugin services.PanelPlugin
|
||||
check := false
|
||||
for _, item := range plugins {
|
||||
if item.Slug == slug {
|
||||
check = true
|
||||
plugin = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if !check {
|
||||
Error(ctx, http.StatusBadRequest, "插件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var installedPlugin models.Plugin
|
||||
if err := facades.Orm().Query().Where("slug", slug).First(&installedPlugin); err != nil {
|
||||
plugin := r.plugin.GetBySlug(slug)
|
||||
installedPlugin := r.plugin.GetInstalledBySlug(slug)
|
||||
installedPlugins, err := r.plugin.AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
if installedPlugin.ID != 0 {
|
||||
Error(ctx, http.StatusBadRequest, "插件已安装")
|
||||
return
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
pluginsMap[p.Slug] = true
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
lock.RLock()
|
||||
_, requireFound := pluginsMap[require]
|
||||
lock.RUnlock()
|
||||
if !requireFound {
|
||||
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "安装插件 " + plugin.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = "bash scripts/plugins/" + plugin.Slug + "/install.sh >> /tmp/" + plugin.Slug + ".log 2>&1"
|
||||
task.Shell = plugin.Install + " >> /tmp/" + plugin.Slug + ".log 2>&1"
|
||||
task.Log = "/tmp/" + plugin.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 创建任务失败: " + err.Error())
|
||||
@@ -123,35 +139,53 @@ func (r *PluginController) Install(ctx http.Context) {
|
||||
// Uninstall 卸载插件
|
||||
func (r *PluginController) Uninstall(ctx http.Context) {
|
||||
slug := ctx.Request().Input("slug")
|
||||
plugins := r.plugin.All()
|
||||
|
||||
var plugin services.PanelPlugin
|
||||
check := false
|
||||
for _, item := range plugins {
|
||||
if item.Slug == slug {
|
||||
check = true
|
||||
plugin = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if !check {
|
||||
Error(ctx, http.StatusBadRequest, "插件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var installedPlugin models.Plugin
|
||||
if err := facades.Orm().Query().Where("slug", slug).First(&installedPlugin); err != nil {
|
||||
plugin := r.plugin.GetBySlug(slug)
|
||||
installedPlugin := r.plugin.GetInstalledBySlug(slug)
|
||||
installedPlugins, err := r.plugin.AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
if installedPlugin.ID == 0 {
|
||||
Error(ctx, http.StatusBadRequest, "插件未安装")
|
||||
return
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
pluginsMap[p.Slug] = true
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
lock.RLock()
|
||||
_, requireFound := pluginsMap[require]
|
||||
lock.RUnlock()
|
||||
if !requireFound {
|
||||
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "卸载插件 " + plugin.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = "bash scripts/plugins/" + plugin.Slug + "/uninstall.sh >> /tmp/" + plugin.Slug + ".log 2>&1"
|
||||
task.Shell = plugin.Uninstall + " >> /tmp/" + plugin.Slug + ".log 2>&1"
|
||||
task.Log = "/tmp/" + plugin.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 创建任务失败: " + err.Error())
|
||||
@@ -166,35 +200,53 @@ func (r *PluginController) Uninstall(ctx http.Context) {
|
||||
// Update 更新插件
|
||||
func (r *PluginController) Update(ctx http.Context) {
|
||||
slug := ctx.Request().Input("slug")
|
||||
plugins := r.plugin.All()
|
||||
|
||||
var plugin services.PanelPlugin
|
||||
check := false
|
||||
for _, item := range plugins {
|
||||
if item.Slug == slug {
|
||||
check = true
|
||||
plugin = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if !check {
|
||||
Error(ctx, http.StatusBadRequest, "插件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var installedPlugin models.Plugin
|
||||
if err := facades.Orm().Query().Where("slug", slug).First(&installedPlugin); err != nil {
|
||||
plugin := r.plugin.GetBySlug(slug)
|
||||
installedPlugin := r.plugin.GetInstalledBySlug(slug)
|
||||
installedPlugins, err := r.plugin.AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
if installedPlugin.ID == 0 {
|
||||
Error(ctx, http.StatusBadRequest, "插件未安装")
|
||||
return
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
pluginsMap[p.Slug] = true
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
lock.RLock()
|
||||
_, requireFound := pluginsMap[require]
|
||||
lock.RUnlock()
|
||||
if !requireFound {
|
||||
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "更新插件 " + plugin.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = "bash scripts/plugins/" + plugin.Slug + "/update.sh >> /tmp/" + plugin.Slug + ".log 2>&1"
|
||||
task.Shell = plugin.Update + " >> /tmp/" + plugin.Slug + ".log 2>&1"
|
||||
task.Log = "/tmp/" + plugin.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 创建任务失败: " + err.Error())
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
package fail2ban
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Fail2banController struct {
|
||||
website services.Website
|
||||
}
|
||||
|
||||
func NewFail2banController() *Fail2banController {
|
||||
return &Fail2banController{
|
||||
website: services.NewWebsiteImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
type Jail struct {
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
LogPath string `json:"log_path"`
|
||||
MaxRetry int `json:"max_retry"`
|
||||
FindTime int `json:"find_time"`
|
||||
BanTime int `json:"ban_time"`
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *Fail2banController) Status(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "fail2ban") {
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Fail2banController) Reload(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "fail2ban") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload fail2ban")
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Fail2banController) Restart(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "fail2ban") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart fail2ban")
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Fail2banController) Start(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "fail2ban") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start fail2ban")
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Fail2banController) Stop(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "fail2ban") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop fail2ban")
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// List 所有 Fail2ban 规则
|
||||
func (c *Fail2banController) List(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "fail2ban") {
|
||||
return
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
if len(raw) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "Fail2ban 规则为空")
|
||||
return
|
||||
}
|
||||
|
||||
jailList := regexp.MustCompile(`\[(.*?)]`).FindAllStringSubmatch(raw, -1)
|
||||
if len(jailList) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "Fail2ban 规则为空")
|
||||
return
|
||||
}
|
||||
|
||||
var jails []Jail
|
||||
for i, jail := range jailList {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
jailName := jail[1]
|
||||
jailRaw := tools.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
|
||||
if len(jailRaw) == 0 {
|
||||
continue
|
||||
}
|
||||
jailEnabled := strings.Contains(jailRaw, "enabled = true")
|
||||
jailLogPath := regexp.MustCompile(`logpath = (.*)`).FindStringSubmatch(jailRaw)
|
||||
jailMaxRetry := regexp.MustCompile(`maxretry = (.*)`).FindStringSubmatch(jailRaw)
|
||||
jailFindTime := regexp.MustCompile(`findtime = (.*)`).FindStringSubmatch(jailRaw)
|
||||
jailBanTime := regexp.MustCompile(`bantime = (.*)`).FindStringSubmatch(jailRaw)
|
||||
|
||||
jails = append(jails, Jail{
|
||||
Name: jailName,
|
||||
Enabled: jailEnabled,
|
||||
LogPath: jailLogPath[1],
|
||||
MaxRetry: cast.ToInt(jailMaxRetry[1]),
|
||||
FindTime: cast.ToInt(jailFindTime[1]),
|
||||
BanTime: cast.ToInt(jailBanTime[1]),
|
||||
})
|
||||
}
|
||||
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(jails) {
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []Jail{},
|
||||
})
|
||||
return
|
||||
}
|
||||
if endIndex > len(jails) {
|
||||
endIndex = len(jails)
|
||||
}
|
||||
pagedJails := jails[startIndex:endIndex]
|
||||
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": len(jails),
|
||||
"items": pagedJails,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加 Fail2ban 规则
|
||||
func (c *Fail2banController) Add(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "fail2ban") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required",
|
||||
"type": "required|in:website,service",
|
||||
"maxretry": "required",
|
||||
"findtime": "required",
|
||||
"bantime": "required",
|
||||
"website_mode": "required_if:type,website",
|
||||
"website_path": "required_if:type,website",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
jailName := ctx.Request().Input("name")
|
||||
jailType := ctx.Request().Input("type")
|
||||
jailMaxRetry := ctx.Request().Input("maxretry")
|
||||
jailFindTime := ctx.Request().Input("findtime")
|
||||
jailBanTime := ctx.Request().Input("bantime")
|
||||
jailWebsiteMode := ctx.Request().Input("website_mode")
|
||||
jailWebsitePath := ctx.Request().Input("website_path")
|
||||
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
if strings.Contains(raw, "["+jailName+"]") || (strings.Contains(raw, "["+jailName+"]"+"-cc") && jailWebsiteMode == "cc") || (strings.Contains(raw, "["+jailName+"]"+"-path") && jailWebsiteMode == "path") {
|
||||
controllers.Error(ctx, http.StatusUnprocessableEntity, "规则已存在")
|
||||
return
|
||||
}
|
||||
|
||||
switch jailType {
|
||||
case "website":
|
||||
var website models.Website
|
||||
err := facades.Orm().Query().Where("name", jailName).FirstOrFail(&website)
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusUnprocessableEntity, "网站不存在")
|
||||
return
|
||||
}
|
||||
config, err := c.website.GetConfig(int(website.ID))
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusUnprocessableEntity, "获取网站配置失败")
|
||||
return
|
||||
}
|
||||
var ports string
|
||||
for _, port := range config.Ports {
|
||||
if len(strings.Split(port, " ")) > 1 {
|
||||
ports += strings.Split(port, " ")[0] + ","
|
||||
} else {
|
||||
ports += port + ","
|
||||
}
|
||||
}
|
||||
|
||||
rule := `
|
||||
# ` + jailName + `-` + jailWebsiteMode + `-START
|
||||
[` + jailName + `-` + jailWebsiteMode + `]
|
||||
enabled = true
|
||||
filter = haozi-` + jailName + `-` + jailWebsiteMode + `
|
||||
port = ` + ports + `
|
||||
maxretry = ` + jailMaxRetry + `
|
||||
findtime = ` + jailFindTime + `
|
||||
bantime = ` + jailBanTime + `
|
||||
action = %(action_mwl)s
|
||||
logpath = /www/wwwlogs/` + website.Name + `.log
|
||||
# ` + jailName + `-` + jailWebsiteMode + `-END
|
||||
`
|
||||
raw += rule
|
||||
tools.Write("/etc/fail2ban/jail.local", raw, 0644)
|
||||
|
||||
var filter string
|
||||
if jailWebsiteMode == "cc" {
|
||||
filter = `
|
||||
[Definition]
|
||||
failregex = ^<HOST>\s-.*HTTP/.*$
|
||||
ignoreregex =
|
||||
`
|
||||
} else {
|
||||
filter = `
|
||||
[Definition]
|
||||
failregex = ^<HOST>\s-.*\s` + jailWebsitePath + `.*HTTP/.*$
|
||||
ignoreregex =
|
||||
`
|
||||
}
|
||||
tools.Write("/etc/fail2ban/filter.d/haozi-"+jailName+"-"+jailWebsiteMode+".conf", filter, 0644)
|
||||
|
||||
case "service":
|
||||
var logPath string
|
||||
var filter string
|
||||
var port string
|
||||
switch jailName {
|
||||
case "ssh":
|
||||
if tools.IsDebian() {
|
||||
logPath = "/var/log/auth.log"
|
||||
} else {
|
||||
logPath = "/var/log/secure"
|
||||
}
|
||||
filter = "sshd"
|
||||
port = tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
case "mysql":
|
||||
logPath = "/www/server/mysql/mysql-error.log"
|
||||
filter = "mysqld-auth"
|
||||
port = tools.Exec("cat /www/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'")
|
||||
case "pure-ftpd":
|
||||
logPath = "/var/log/messages"
|
||||
filter = "pure-ftpd"
|
||||
port = tools.Exec(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
|
||||
default:
|
||||
controllers.Error(ctx, http.StatusUnprocessableEntity, "未知服务")
|
||||
return
|
||||
}
|
||||
if len(port) == 0 {
|
||||
controllers.Error(ctx, http.StatusUnprocessableEntity, "获取服务端口失败,请检查是否安装")
|
||||
return
|
||||
}
|
||||
|
||||
rule := `
|
||||
# ` + jailName + `-START
|
||||
[` + jailName + `]
|
||||
enabled = true
|
||||
filter = ` + filter + `
|
||||
port = ` + port + `
|
||||
maxretry = ` + jailMaxRetry + `
|
||||
findtime = ` + jailFindTime + `
|
||||
bantime = ` + jailBanTime + `
|
||||
action = %(action_mwl)s
|
||||
logpath = ` + logPath + `
|
||||
# ` + jailName + `-END
|
||||
`
|
||||
raw += rule
|
||||
tools.Write("/etc/fail2ban/jail.local", raw, 0644)
|
||||
}
|
||||
|
||||
tools.Exec("fail2ban-client reload")
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除规则
|
||||
func (c *Fail2banController) Delete(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "fail2ban") {
|
||||
return
|
||||
}
|
||||
|
||||
jailName := ctx.Request().Input("name")
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
if !strings.Contains(raw, "["+jailName+"]") {
|
||||
controllers.Error(ctx, http.StatusUnprocessableEntity, "规则不存在")
|
||||
return
|
||||
}
|
||||
|
||||
rule := tools.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
|
||||
raw = strings.Replace(raw, "\n# "+jailName+"-START"+rule+"# "+jailName+"-END", "", -1)
|
||||
raw = strings.TrimSpace(raw)
|
||||
tools.Write("/etc/fail2ban/jail.local", raw, 0644)
|
||||
|
||||
tools.Exec("fail2ban-client reload")
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BanList 获取封禁列表
|
||||
func (c *Fail2banController) BanList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "fail2ban") {
|
||||
return
|
||||
}
|
||||
|
||||
name := ctx.Request().Query("name")
|
||||
if len(name) == 0 {
|
||||
controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
|
||||
return
|
||||
}
|
||||
|
||||
currentlyBan := tools.Exec(`fail2ban-client status ` + name + ` | grep "Currently banned" | awk '{print $4}'`)
|
||||
totalBan := tools.Exec(`fail2ban-client status ` + name + ` | grep "Total banned" | awk '{print $4}'`)
|
||||
bannedIp := tools.Exec(`fail2ban-client status ` + name + ` | grep "Banned IP list" | awk -F ":" '{print $2}'`)
|
||||
bannedIpList := strings.Split(bannedIp, " ")
|
||||
|
||||
var list []map[string]string
|
||||
for _, ip := range bannedIpList {
|
||||
if len(ip) > 0 {
|
||||
list = append(list, map[string]string{
|
||||
"name": name,
|
||||
"ip": ip,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
controllers.Success(ctx, http.Json{
|
||||
"currentlyBan": currentlyBan,
|
||||
"totalBan": totalBan,
|
||||
"bannedIpList": list,
|
||||
})
|
||||
}
|
||||
|
||||
// Unban 解封
|
||||
func (c *Fail2banController) Unban(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "fail2ban") {
|
||||
return
|
||||
}
|
||||
|
||||
name := ctx.Request().Input("name")
|
||||
ip := ctx.Request().Input("ip")
|
||||
if len(name) == 0 || len(ip) == 0 {
|
||||
controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("fail2ban-client set " + name + " unbanip " + ip)
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SetWhiteList 设置白名单
|
||||
func (c *Fail2banController) SetWhiteList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "fail2ban") {
|
||||
return
|
||||
}
|
||||
|
||||
ip := ctx.Request().Input("ip")
|
||||
if len(ip) == 0 {
|
||||
controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
|
||||
return
|
||||
}
|
||||
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
// 正则替换
|
||||
reg := regexp.MustCompile(`ignoreip\s*=\s*.*\n`)
|
||||
if reg.MatchString(raw) {
|
||||
raw = reg.ReplaceAllString(raw, "ignoreip = "+ip+"\n")
|
||||
} else {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
|
||||
return
|
||||
}
|
||||
|
||||
tools.Write("/etc/fail2ban/jail.local", raw, 0644)
|
||||
tools.Exec("fail2ban-client reload")
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetWhiteList 获取白名单
|
||||
func (c *Fail2banController) GetWhiteList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "fail2ban") {
|
||||
return
|
||||
}
|
||||
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
reg := regexp.MustCompile(`ignoreip\s*=\s*(.*)\n`)
|
||||
if reg.MatchString(raw) {
|
||||
ignoreIp := reg.FindStringSubmatch(raw)[1]
|
||||
controllers.Success(ctx, ignoreIp)
|
||||
} else {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,785 @@
|
||||
package mysql57
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Mysql57Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
}
|
||||
|
||||
func NewMysql57Controller() *Mysql57Controller {
|
||||
return &Mysql57Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *Mysql57Controller) Status(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Mysql57Controller) Reload(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Mysql57Controller) Restart(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Mysql57Controller) Start(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Mysql57Controller) Stop(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *Mysql57Controller) GetConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/mysql/conf/my.cnf")
|
||||
if len(config) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *Mysql57Controller) SaveConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/mysql/conf/my.cnf", config, 0644) {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *Mysql57Controller) Load(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "MySQL 已停止运行")
|
||||
return
|
||||
}
|
||||
|
||||
raw := tools.Exec("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
|
||||
if strings.Contains(raw, "Access denied for user") {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码错误")
|
||||
return
|
||||
}
|
||||
if !strings.Contains(raw, "Uptime") {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
|
||||
return
|
||||
}
|
||||
|
||||
data := make(map[int]map[string]string)
|
||||
expressions := []struct {
|
||||
regex string
|
||||
name string
|
||||
}{
|
||||
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
|
||||
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
|
||||
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
|
||||
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
|
||||
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
|
||||
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
|
||||
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
|
||||
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
|
||||
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
|
||||
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
|
||||
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
|
||||
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
|
||||
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
|
||||
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
|
||||
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
|
||||
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
|
||||
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
|
||||
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
|
||||
}
|
||||
|
||||
for i, expression := range expressions {
|
||||
re := regexp.MustCompile(expression.regex)
|
||||
matches := re.FindStringSubmatch(raw)
|
||||
if len(matches) > 1 {
|
||||
data[i] = make(map[string]string)
|
||||
data[i] = map[string]string{"name": expression.name, "value": matches[1]}
|
||||
|
||||
if expression.name == "发送" || expression.name == "接收" {
|
||||
data[i]["value"] = tools.FormatBytes(cast.ToFloat64(matches[1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 索引命中率
|
||||
readRequests := cast.ToFloat64(data[9]["value"])
|
||||
reads := cast.ToFloat64(data[10]["value"])
|
||||
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
|
||||
// Innodb 索引命中率
|
||||
bufferPoolReads := cast.ToFloat64(data[11]["value"])
|
||||
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
|
||||
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
|
||||
|
||||
controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
// ErrorLog 获取错误日志
|
||||
func (c *Mysql57Controller) ErrorLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/mysql/mysql-error.log"))
|
||||
controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (c *Mysql57Controller) ClearErrorLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/mysql/mysql-error.log")
|
||||
controllers.Success(ctx, "清空错误日志成功")
|
||||
}
|
||||
|
||||
// SlowLog 获取慢查询日志
|
||||
func (c *Mysql57Controller) SlowLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/mysql/mysql-slow.log"))
|
||||
controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearSlowLog 清空慢查询日志
|
||||
func (c *Mysql57Controller) ClearSlowLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/mysql/mysql-slow.log")
|
||||
controllers.Success(ctx, "清空慢查询日志成功")
|
||||
}
|
||||
|
||||
// GetRootPassword 获取root密码
|
||||
func (c *Mysql57Controller) GetRootPassword(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, rootPassword)
|
||||
}
|
||||
|
||||
// SetRootPassword 设置root密码
|
||||
func (c *Mysql57Controller) SetRootPassword(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
return
|
||||
}
|
||||
if status != "active" {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := ctx.Request().Input(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
oldRootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if oldRootPassword != rootPassword {
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
err := c.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
|
||||
if err != nil {
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
controllers.Success(ctx, "设置root密码成功")
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (c *Mysql57Controller) DatabaseList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
type database struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL57] 连接数据库失败" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query("SHOW DATABASES")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL57] 获取数据库列表失败" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var databases []database
|
||||
for rows.Next() {
|
||||
var d database
|
||||
err := rows.Scan(&d.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
databases = append(databases, d)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
facades.Log().Error("[MySQL57] 获取数据库列表失败" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(databases) {
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
return
|
||||
}
|
||||
if endIndex > len(databases) {
|
||||
endIndex = len(databases)
|
||||
}
|
||||
pagedDatabases := databases[startIndex:endIndex]
|
||||
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": len(databases),
|
||||
"items": pagedDatabases,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (c *Mysql57Controller) AddDatabase(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
database := ctx.Request().Input("database")
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
controllers.Success(ctx, "添加数据库成功")
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (c *Mysql57Controller) DeleteDatabase(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\"")
|
||||
|
||||
controllers.Success(ctx, "删除数据库成功")
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (c *Mysql57Controller) BackupList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
backupList, err := c.backup.MysqlList()
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL57] 获取备份列表失败:" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, backupList)
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (c *Mysql57Controller) UploadBackup(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
name := file.GetClientOriginalName()
|
||||
_, err = file.StoreAs(backupPath, name)
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, "上传文件成功")
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (c *Mysql57Controller) CreateBackup(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
err = c.backup.MysqlBackup(database)
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL57] 创建备份失败:" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, "备份成功")
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (c *Mysql57Controller) DeleteBackup(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
||||
fileName := ctx.Request().Input("name")
|
||||
tools.Remove(backupPath + "/" + fileName)
|
||||
|
||||
controllers.Success(ctx, "删除备份成功")
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (c *Mysql57Controller) RestoreBackup(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
err = c.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL57] 还原失败:" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, "还原成功")
|
||||
}
|
||||
|
||||
// UserList 用户列表
|
||||
func (c *Mysql57Controller) UserList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
type user struct {
|
||||
User string `json:"user"`
|
||||
Host string `json:"host"`
|
||||
Grants []string `json:"grants"`
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL57] 连接数据库失败:" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query("SELECT user, host FROM mysql.user")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL57] 查询数据库失败:" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "查询数据库失败")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var userGrants []user
|
||||
|
||||
for rows.Next() {
|
||||
var u user
|
||||
err := rows.Scan(&u.User, &u.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 查询用户权限
|
||||
grantsRows, err := db.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", u.User, u.Host))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer grantsRows.Close()
|
||||
|
||||
for grantsRows.Next() {
|
||||
var grant string
|
||||
err := grantsRows.Scan(&grant)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
u.Grants = append(u.Grants, grant)
|
||||
}
|
||||
|
||||
if err := grantsRows.Err(); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
userGrants = append(userGrants, u)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(userGrants) {
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []user{},
|
||||
})
|
||||
return
|
||||
}
|
||||
if endIndex > len(userGrants) {
|
||||
endIndex = len(userGrants)
|
||||
}
|
||||
pagedUserGrants := userGrants[startIndex:endIndex]
|
||||
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": len(userGrants),
|
||||
"items": pagedUserGrants,
|
||||
})
|
||||
}
|
||||
|
||||
// AddUser 添加用户
|
||||
func (c *Mysql57Controller) AddUser(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
controllers.Success(ctx, "添加成功")
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (c *Mysql57Controller) DeleteUser(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\"")
|
||||
|
||||
controllers.Success(ctx, "删除成功")
|
||||
}
|
||||
|
||||
// SetUserPassword 设置用户密码
|
||||
func (c *Mysql57Controller) SetUserPassword(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
controllers.Success(ctx, "修改成功")
|
||||
}
|
||||
|
||||
// SetUserPrivileges 设置用户权限
|
||||
func (c *Mysql57Controller) SetUserPrivileges(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql57") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"database": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
controllers.Success(ctx, "修改成功")
|
||||
}
|
||||
@@ -1,43 +1,40 @@
|
||||
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"
|
||||
"golang.org/x/exp/slices"
|
||||
"panel/app/models"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/http/controllers/plugins"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Mysql80Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
}
|
||||
|
||||
func NewMysql80Controller() *Mysql80Controller {
|
||||
return &Mysql80Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (r *Mysql80Controller) Status(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) Status(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
out := tools.ExecShell("systemctl status mysql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
return
|
||||
@@ -51,14 +48,13 @@ func (r *Mysql80Controller) Status(ctx http.Context) {
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (r *Mysql80Controller) Reload(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) Reload(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.ExecShell("systemctl reload mysql")
|
||||
out := tools.ExecShell("systemctl status mysql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
tools.Exec("systemctl reload mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
return
|
||||
@@ -72,14 +68,13 @@ func (r *Mysql80Controller) Reload(ctx http.Context) {
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (r *Mysql80Controller) Restart(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) Restart(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.ExecShell("systemctl restart mysql")
|
||||
out := tools.ExecShell("systemctl status mysql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
tools.Exec("systemctl restart mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
return
|
||||
@@ -93,14 +88,13 @@ func (r *Mysql80Controller) Restart(ctx http.Context) {
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (r *Mysql80Controller) Start(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) Start(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.ExecShell("systemctl start mysql")
|
||||
out := tools.ExecShell("systemctl status mysql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
tools.Exec("systemctl start mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
return
|
||||
@@ -114,14 +108,13 @@ func (r *Mysql80Controller) Start(ctx http.Context) {
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (r *Mysql80Controller) Stop(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) Stop(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.ExecShell("systemctl stop mysql")
|
||||
out := tools.ExecShell("systemctl status mysql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
tools.Exec("systemctl stop mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
return
|
||||
@@ -135,13 +128,13 @@ func (r *Mysql80Controller) Stop(ctx http.Context) {
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (r *Mysql80Controller) GetConfig(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) GetConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.ReadFile("mysql80")
|
||||
config := tools.Read("/www/server/mysql/conf/my.cnf")
|
||||
if len(config) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
|
||||
return
|
||||
@@ -151,8 +144,8 @@ func (r *Mysql80Controller) GetConfig(ctx http.Context) {
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (r *Mysql80Controller) SaveConfig(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) SaveConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -162,33 +155,33 @@ func (r *Mysql80Controller) SaveConfig(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !tools.WriteFile("mysql80", config, 0644) {
|
||||
if !tools.Write("/www/server/mysql/conf/my.cnf", config, 0644) {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, "保存MySQL配置成功")
|
||||
c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (r *Mysql80Controller) Load(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) Load(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if strings.TrimSpace(status) != "active" {
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "MySQL 已停止运行")
|
||||
return
|
||||
}
|
||||
|
||||
raw := tools.ExecShell("mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
|
||||
raw := tools.Exec("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
|
||||
if strings.Contains(raw, "Access denied for user") {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码错误")
|
||||
return
|
||||
@@ -203,24 +196,24 @@ func (r *Mysql80Controller) Load(ctx http.Context) {
|
||||
regex string
|
||||
name string
|
||||
}{
|
||||
{`Uptime\s+\|\s+(\d+)\s+\|`, "总查询次数"},
|
||||
{`Queries\s+\|\s+(\d+)\s+\|`, "总连接次数"},
|
||||
{`Connections\s+\|\s+(\d+)\s+\|`, "每秒事务"},
|
||||
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
|
||||
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "发送"},
|
||||
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "接收"},
|
||||
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "活动连接数"},
|
||||
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
|
||||
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "索引命中率"},
|
||||
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
|
||||
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
|
||||
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
|
||||
{`Open_tables\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
|
||||
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
|
||||
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
|
||||
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
|
||||
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "锁表次数"},
|
||||
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, ""},
|
||||
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
|
||||
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
|
||||
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
|
||||
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
|
||||
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
|
||||
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
|
||||
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
|
||||
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
|
||||
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
|
||||
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
|
||||
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
|
||||
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
|
||||
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
|
||||
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
|
||||
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
|
||||
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
|
||||
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
|
||||
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
|
||||
}
|
||||
|
||||
for i, expression := range expressions {
|
||||
@@ -240,7 +233,7 @@ func (r *Mysql80Controller) Load(ctx http.Context) {
|
||||
readRequests := cast.ToFloat64(data[9]["value"])
|
||||
reads := cast.ToFloat64(data[10]["value"])
|
||||
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
|
||||
// Innodb索引命中率
|
||||
// Innodb 索引命中率
|
||||
bufferPoolReads := cast.ToFloat64(data[11]["value"])
|
||||
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
|
||||
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
|
||||
@@ -249,52 +242,52 @@ func (r *Mysql80Controller) Load(ctx http.Context) {
|
||||
}
|
||||
|
||||
// ErrorLog 获取错误日志
|
||||
func (r *Mysql80Controller) ErrorLog(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) ErrorLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
log := tools.ExecShell("tail -n 100 /www/server/mysql/mysql-error.log")
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/mysql/mysql-error.log"))
|
||||
controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (r *Mysql80Controller) ClearErrorLog(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) ClearErrorLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.ExecShell("echo '' > /www/server/mysql/mysql-error.log")
|
||||
tools.Exec("echo '' > /www/server/mysql/mysql-error.log")
|
||||
controllers.Success(ctx, "清空错误日志成功")
|
||||
}
|
||||
|
||||
// SlowLog 获取慢查询日志
|
||||
func (r *Mysql80Controller) SlowLog(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) SlowLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
log := tools.ExecShell("tail -n 100 /www/server/mysql/mysql-slow.log")
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/mysql/mysql-slow.log"))
|
||||
controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearSlowLog 清空慢查询日志
|
||||
func (r *Mysql80Controller) ClearSlowLog(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) ClearSlowLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.ExecShell("echo '' > /www/server/mysql/mysql-slow.log")
|
||||
tools.Exec("echo '' > /www/server/mysql/mysql-slow.log")
|
||||
controllers.Success(ctx, "清空慢查询日志成功")
|
||||
}
|
||||
|
||||
// GetRootPassword 获取root密码
|
||||
func (r *Mysql80Controller) GetRootPassword(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) GetRootPassword(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
|
||||
return
|
||||
@@ -304,13 +297,12 @@ func (r *Mysql80Controller) GetRootPassword(ctx http.Context) {
|
||||
}
|
||||
|
||||
// SetRootPassword 设置root密码
|
||||
func (r *Mysql80Controller) SetRootPassword(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) SetRootPassword(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
out := tools.ExecShell("systemctl status mysql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
return
|
||||
@@ -326,14 +318,14 @@ func (r *Mysql80Controller) SetRootPassword(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
oldRootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
oldRootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if oldRootPassword != rootPassword {
|
||||
tools.ExecShell("mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\"")
|
||||
tools.ExecShell("mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
err := r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
err := c.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
|
||||
if err != nil {
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\"")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
|
||||
return
|
||||
}
|
||||
@@ -343,72 +335,107 @@ func (r *Mysql80Controller) SetRootPassword(ctx http.Context) {
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (r *Mysql80Controller) DatabaseList(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) DatabaseList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
out := tools.ExecShell("mysql -uroot -p" + r.setting.Get(models.SettingKeyMysqlRootPassword) + " -e \"show databases;\"")
|
||||
databases := strings.Split(out, "\n")
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
type database struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
databases = databases[1 : len(databases)-1]
|
||||
systemDatabases := []string{"information_schema", "mysql", "performance_schema", "sys"}
|
||||
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL80] 连接数据库失败" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var userDatabases []string
|
||||
for _, db := range databases {
|
||||
if !slices.Contains(systemDatabases, db) {
|
||||
userDatabases = append(userDatabases, db)
|
||||
rows, err := db.Query("SHOW DATABASES")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL80] 获取数据库列表失败" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var databases []database
|
||||
for rows.Next() {
|
||||
var d database
|
||||
err := rows.Scan(&d.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
databases = append(databases, d)
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
Name string
|
||||
if err := rows.Err(); err != nil {
|
||||
facades.Log().Error("[MySQL80] 获取数据库列表失败" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
var dbStructs []Database
|
||||
for _, db := range userDatabases {
|
||||
dbStructs = append(dbStructs, Database{Name: db})
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(databases) {
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
return
|
||||
}
|
||||
if endIndex > len(databases) {
|
||||
endIndex = len(databases)
|
||||
}
|
||||
pagedDatabases := databases[startIndex:endIndex]
|
||||
|
||||
controllers.Success(ctx, dbStructs)
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": len(databases),
|
||||
"items": pagedDatabases,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (r *Mysql80Controller) AddDatabase(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) AddDatabase(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255|regex:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*(_|[^\\w])).+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
database := ctx.Request().Input("database")
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\"")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
controllers.Success(ctx, "添加数据库成功")
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (r *Mysql80Controller) DeleteDatabase(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) DeleteDatabase(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -420,54 +447,63 @@ func (r *Mysql80Controller) DeleteDatabase(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
database := ctx.Request().Input("database")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\"")
|
||||
|
||||
controllers.Success(ctx, "删除数据库成功")
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (r *Mysql80Controller) BackupList(ctx http.Context) {
|
||||
backupPath := "/www/backup/mysql"
|
||||
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
func (c *Mysql80Controller) BackupList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
controllers.Success(ctx, backupList)
|
||||
}
|
||||
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
backupFiles = append(backupFiles, map[string]string{
|
||||
"file": file.Name(),
|
||||
"size": tools.FormatBytes(float64(info.Size())),
|
||||
})
|
||||
// UploadBackup 上传备份
|
||||
func (c *Mysql80Controller) UploadBackup(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, backupFiles)
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
name := file.GetClientOriginalName()
|
||||
_, err = file.StoreAs(backupPath, name)
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, "上传文件成功")
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (r *Mysql80Controller) CreateBackup(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) CreateBackup(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -479,65 +515,54 @@ func (r *Mysql80Controller) CreateBackup(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
backupPath := "/www/backup/mysql"
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
database := ctx.Request().Input("database")
|
||||
backupFile := backupPath + "/" + 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 + " > " + backupFile)
|
||||
tools.ExecShell("zip -r " + backupFile + ".zip " + backupFile)
|
||||
tools.RemoveFile(backupFile)
|
||||
_ = os.Unsetenv("MYSQL_PWD")
|
||||
|
||||
controllers.Success(ctx, "备份成功")
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (r *Mysql80Controller) DeleteBackup(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) DeleteBackup(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
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())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
backupPath := "/www/backup/mysql"
|
||||
file := ctx.Request().Input("file")
|
||||
tools.RemoveFile(backupPath + "/" + file)
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
||||
fileName := ctx.Request().Input("name")
|
||||
tools.Remove(backupPath + "/" + fileName)
|
||||
|
||||
controllers.Success(ctx, "删除备份成功")
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (r *Mysql80Controller) RestoreBackup(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) RestoreBackup(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -545,136 +570,141 @@ func (r *Mysql80Controller) RestoreBackup(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
backupPath := "/www/backup/mysql"
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
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, "还原成功")
|
||||
}
|
||||
|
||||
// UserList 用户列表
|
||||
func (r *Mysql80Controller) UserList(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) UserList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Host string `json:"host"`
|
||||
Privileges string `json:"privileges"`
|
||||
type user struct {
|
||||
User string `json:"user"`
|
||||
Host string `json:"host"`
|
||||
Grants []string `json:"grants"`
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
out := tools.ExecShell("mysql -uroot -p" + rootPassword + " -e 'select user,host from mysql.user'")
|
||||
rawUsers := strings.Split(out, "\n")
|
||||
users := make([]User, 0)
|
||||
for _, rawUser := range rawUsers {
|
||||
user := strings.Split(rawUser, "\t")
|
||||
if user[0] == "root" || user[0] == "mysql.sys" || user[0] == "mysql.infoschema" || user[0] == "mysql.session" {
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL80] 连接数据库失败:" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query("SELECT user, host FROM mysql.user")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL80] 查询数据库失败:" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "查询数据库失败")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var userGrants []user
|
||||
|
||||
for rows.Next() {
|
||||
var u user
|
||||
err := rows.Scan(&u.User, &u.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
out := tools.ExecShell("mysql -uroot -p" + rootPassword + " -e 'show grants for " + user[0] + "@" + user[1] + "'")
|
||||
rawPrivileges := strings.Split(out, "\n")
|
||||
privileges := make([]string, 0)
|
||||
for _, rawPrivilege := range rawPrivileges {
|
||||
if rawPrivilege == "" {
|
||||
// 查询用户权限
|
||||
grantsRows, err := db.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", u.User, u.Host))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer grantsRows.Close()
|
||||
|
||||
for grantsRows.Next() {
|
||||
var grant string
|
||||
err := grantsRows.Scan(&grant)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
privilege := rawPrivilege[6:strings.Index(rawPrivilege, " TO")]
|
||||
privileges = append(privileges, privilege)
|
||||
|
||||
u.Grants = append(u.Grants, grant)
|
||||
}
|
||||
users = append(users, User{Username: user[0], Host: user[1], Privileges: strings.Join(privileges, " | ")})
|
||||
|
||||
if err := grantsRows.Err(); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
userGrants = append(userGrants, u)
|
||||
}
|
||||
|
||||
controllers.Success(ctx, users)
|
||||
if err := rows.Err(); err != nil {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(userGrants) {
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []user{},
|
||||
})
|
||||
return
|
||||
}
|
||||
if endIndex > len(userGrants) {
|
||||
endIndex = len(userGrants)
|
||||
}
|
||||
pagedUserGrants := userGrants[startIndex:endIndex]
|
||||
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": len(userGrants),
|
||||
"items": pagedUserGrants,
|
||||
})
|
||||
}
|
||||
|
||||
// AddUser 添加用户
|
||||
func (r *Mysql80Controller) AddUser(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) AddUser(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255|regex:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*(_|[^\\w])).+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
database := ctx.Request().Input("database")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\"")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
controllers.Success(ctx, "添加成功")
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (r *Mysql80Controller) DeleteUser(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) DeleteUser(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -686,48 +716,48 @@ func (r *Mysql80Controller) DeleteUser(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\"")
|
||||
|
||||
controllers.Success(ctx, "删除成功")
|
||||
}
|
||||
|
||||
// SetUserPassword 设置用户密码
|
||||
func (r *Mysql80Controller) SetUserPassword(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) SetUserPassword(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255|regex:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*(_|[^\\w])).+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
controllers.Success(ctx, "修改成功")
|
||||
}
|
||||
|
||||
// SetUserPrivileges 设置用户权限
|
||||
func (r *Mysql80Controller) SetUserPrivileges(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "mysql80") {
|
||||
func (c *Mysql80Controller) SetUserPrivileges(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "mysql80") {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -740,16 +770,16 @@ func (r *Mysql80Controller) SetUserPrivileges(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
database := ctx.Request().Input("database")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\"")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
controllers.Success(ctx, "修改成功")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package openresty
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
@@ -11,7 +10,6 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/http/controllers/plugins"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
@@ -26,13 +24,12 @@ func NewOpenrestyController() *OpenRestyController {
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (r *OpenRestyController) Status(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "openresty") {
|
||||
func (c *OpenRestyController) Status(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
out := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
@@ -46,14 +43,13 @@ func (r *OpenRestyController) Status(ctx http.Context) {
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (r *OpenRestyController) Reload(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "openresty") {
|
||||
func (c *OpenRestyController) Reload(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.ExecShell("systemctl reload openresty")
|
||||
out := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
tools.Exec("systemctl reload openresty")
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
@@ -62,19 +58,18 @@ func (r *OpenRestyController) Reload(ctx http.Context) {
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, "重载OpenResty成功")
|
||||
} else {
|
||||
controllers.Error(ctx, 1, "重载OpenResty失败: "+string(out))
|
||||
controllers.Error(ctx, 1, "重载OpenResty失败: "+status)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动OpenResty
|
||||
func (r *OpenRestyController) Start(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "openresty") {
|
||||
func (c *OpenRestyController) Start(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.ExecShell("systemctl start openresty")
|
||||
out := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
tools.Exec("systemctl start openresty")
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
@@ -83,19 +78,18 @@ func (r *OpenRestyController) Start(ctx http.Context) {
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, "启动OpenResty成功")
|
||||
} else {
|
||||
controllers.Error(ctx, 1, "启动OpenResty失败: "+string(out))
|
||||
controllers.Error(ctx, 1, "启动OpenResty失败: "+status)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止OpenResty
|
||||
func (r *OpenRestyController) Stop(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "openresty") {
|
||||
func (c *OpenRestyController) Stop(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.ExecShell("systemctl stop openresty")
|
||||
out := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
tools.Exec("systemctl stop openresty")
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
@@ -104,19 +98,18 @@ func (r *OpenRestyController) Stop(ctx http.Context) {
|
||||
if status != "active" {
|
||||
controllers.Success(ctx, "停止OpenResty成功")
|
||||
} else {
|
||||
controllers.Error(ctx, 1, "停止OpenResty失败: "+string(out))
|
||||
controllers.Error(ctx, 1, "停止OpenResty失败: "+status)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启OpenResty
|
||||
func (r *OpenRestyController) Restart(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "openresty") {
|
||||
func (c *OpenRestyController) Restart(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.ExecShell("systemctl restart openresty")
|
||||
out := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
tools.Exec("systemctl restart openresty")
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
@@ -125,17 +118,17 @@ func (r *OpenRestyController) Restart(ctx http.Context) {
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, "重启OpenResty成功")
|
||||
} else {
|
||||
controllers.Error(ctx, 1, "重启OpenResty失败: "+string(out))
|
||||
controllers.Error(ctx, 1, "重启OpenResty失败: "+status)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (r *OpenRestyController) GetConfig(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "openresty") {
|
||||
func (c *OpenRestyController) GetConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
config := tools.ReadFile("/www/server/openresty/conf/nginx.conf")
|
||||
config := tools.Read("/www/server/openresty/conf/nginx.conf")
|
||||
if len(config) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty配置失败")
|
||||
return
|
||||
@@ -145,8 +138,8 @@ func (r *OpenRestyController) GetConfig(ctx http.Context) {
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (r *OpenRestyController) SaveConfig(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "openresty") {
|
||||
func (c *OpenRestyController) SaveConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -156,37 +149,42 @@ func (r *OpenRestyController) SaveConfig(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !tools.WriteFile("/www/server/openresty/conf/nginx.conf", config, 0644) {
|
||||
if !tools.Write("/www/server/openresty/conf/nginx.conf", config, 0644) {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "保存OpenResty配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, "保存OpenResty配置成功")
|
||||
c.Reload(ctx)
|
||||
}
|
||||
|
||||
// ErrorLog 获取错误日志
|
||||
func (r *OpenRestyController) ErrorLog(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "openresty") {
|
||||
func (c *OpenRestyController) ErrorLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
out := tools.ExecShell("tail -n 100 /www/wwwlogs/nginx_error.log")
|
||||
if !tools.Exists("/www/wwwlogs/nginx_error.log") {
|
||||
controllers.Success(ctx, "")
|
||||
return
|
||||
}
|
||||
|
||||
out := tools.Exec("tail -n 100 /www/wwwlogs/nginx_error.log")
|
||||
controllers.Success(ctx, out)
|
||||
}
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (r *OpenRestyController) ClearErrorLog(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "openresty") {
|
||||
func (c *OpenRestyController) ClearErrorLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
_ = tools.ExecShell("echo '' > /www/wwwlogs/nginx_error.log")
|
||||
tools.Exec("echo '' > /www/wwwlogs/nginx_error.log")
|
||||
controllers.Success(ctx, "清空OpenResty错误日志成功")
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (r *OpenRestyController) Load(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "openresty") {
|
||||
func (c *OpenRestyController) Load(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -199,42 +197,63 @@ func (r *OpenRestyController) Load(ctx http.Context) {
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
var data map[int]map[string]any
|
||||
type nginxStatus struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
var data []nginxStatus
|
||||
|
||||
out := tools.ExecShell("ps aux | grep nginx | grep 'worker process' | wc -l")
|
||||
workers := strings.TrimSpace(out)
|
||||
data[0]["name"] = "工作进程"
|
||||
data[0]["value"] = workers
|
||||
workers := tools.Exec("ps aux | grep nginx | grep 'worker process' | wc -l")
|
||||
data = append(data, nginxStatus{
|
||||
Name: "工作进程",
|
||||
Value: workers,
|
||||
})
|
||||
|
||||
out = tools.ExecShell("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
|
||||
mem := tools.FormatBytes(cast.ToFloat64(strings.TrimSpace(out)))
|
||||
data[1]["name"] = "内存占用"
|
||||
data[1]["value"] = mem
|
||||
out := tools.Exec("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
|
||||
mem := tools.FormatBytes(cast.ToFloat64(out))
|
||||
data = append(data, nginxStatus{
|
||||
Name: "内存占用",
|
||||
Value: mem,
|
||||
})
|
||||
|
||||
match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 2 {
|
||||
data[2]["name"] = "活跃连接数"
|
||||
data[2]["value"] = match[1]
|
||||
data = append(data, nginxStatus{
|
||||
Name: "活跃连接数",
|
||||
Value: match[1],
|
||||
})
|
||||
}
|
||||
|
||||
match = regexp.MustCompile(`server accepts handled requests\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 4 {
|
||||
data[3]["name"] = "总连接次数"
|
||||
data[3]["value"] = match[1]
|
||||
data[4]["name"] = "总握手次数"
|
||||
data[4]["value"] = match[2]
|
||||
data[5]["name"] = "总请求次数"
|
||||
data[5]["value"] = match[3]
|
||||
data = append(data, nginxStatus{
|
||||
Name: "总连接次数",
|
||||
Value: match[1],
|
||||
})
|
||||
data = append(data, nginxStatus{
|
||||
Name: "总握手次数",
|
||||
Value: match[2],
|
||||
})
|
||||
data = append(data, nginxStatus{
|
||||
Name: "总请求次数",
|
||||
Value: match[3],
|
||||
})
|
||||
}
|
||||
|
||||
match = regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 4 {
|
||||
data[6]["name"] = "请求数"
|
||||
data[6]["value"] = match[1]
|
||||
data[7]["name"] = "响应数"
|
||||
data[7]["value"] = match[2]
|
||||
data[8]["name"] = "驻留进程"
|
||||
data[8]["value"] = match[3]
|
||||
data = append(data, nginxStatus{
|
||||
Name: "请求数",
|
||||
Value: match[1],
|
||||
})
|
||||
data = append(data, nginxStatus{
|
||||
Name: "响应数",
|
||||
Value: match[2],
|
||||
})
|
||||
data = append(data, nginxStatus{
|
||||
Name: "驻留进程",
|
||||
Value: match[3],
|
||||
})
|
||||
}
|
||||
|
||||
controllers.Success(ctx, data)
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/http/controllers/plugins"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
@@ -32,12 +31,11 @@ func NewPhp74Controller() *Php74Controller {
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Status(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
status := tools.Exec("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
|
||||
@@ -51,12 +49,12 @@ func (c *Php74Controller) Status(ctx http.Context) {
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Reload(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
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}'")
|
||||
tools.Exec("systemctl reload php-fpm-" + c.version)
|
||||
out := tools.Exec("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+"运行状态失败")
|
||||
@@ -71,12 +69,12 @@ func (c *Php74Controller) Reload(ctx http.Context) {
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Start(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
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}'")
|
||||
tools.Exec("systemctl start php-fpm-" + c.version)
|
||||
out := tools.Exec("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+"运行状态失败")
|
||||
@@ -91,12 +89,12 @@ func (c *Php74Controller) Start(ctx http.Context) {
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Stop(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
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}'")
|
||||
tools.Exec("systemctl stop php-fpm-" + c.version)
|
||||
out := tools.Exec("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+"运行状态失败")
|
||||
@@ -111,12 +109,12 @@ func (c *Php74Controller) Stop(ctx http.Context) {
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Restart(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
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}'")
|
||||
tools.Exec("systemctl restart php-fpm-" + c.version)
|
||||
out := tools.Exec("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+"运行状态失败")
|
||||
@@ -131,31 +129,31 @@ func (c *Php74Controller) Restart(ctx http.Context) {
|
||||
}
|
||||
|
||||
func (c *Php74Controller) GetConfig(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
config := tools.ReadFile("/www/server/php/" + c.version + "/etc/php.ini")
|
||||
config := tools.Read("/www/server/php/" + c.version + "/etc/php.ini")
|
||||
controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) SaveConfig(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
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)
|
||||
tools.Write("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
|
||||
c.Reload(ctx)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Load(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/phpfpm_" + c.version + "_status")
|
||||
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + c.version)
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Error("获取PHP-" + c.version + "运行状态失败")
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
|
||||
@@ -186,38 +184,38 @@ func (c *Php74Controller) Load(ctx http.Context) {
|
||||
}
|
||||
|
||||
func (c *Php74Controller) ErrorLog(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
log := tools.ExecShell("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log")
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log"))
|
||||
controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) SlowLog(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
log := tools.ExecShell("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log")
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log"))
|
||||
controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) ClearErrorLog(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
tools.ExecShell("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
|
||||
controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) ClearSlowLog(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
tools.ExecShell("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
|
||||
controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
@@ -229,7 +227,7 @@ type Extension struct {
|
||||
}
|
||||
|
||||
func (c *Php74Controller) GetExtensionList(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -238,7 +236,7 @@ func (c *Php74Controller) GetExtensionList(ctx http.Context) {
|
||||
}
|
||||
|
||||
func (c *Php74Controller) InstallExtension(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -259,7 +257,7 @@ func (c *Php74Controller) InstallExtension(ctx http.Context) {
|
||||
var task models.Task
|
||||
task.Name = "安装PHP-" + c.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = "bash scripts/php_extensions/" + item.Slug + ".sh install " + c.version + ">> /tmp/" + item.Slug + ".log 2>&1"
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[PHP-" + c.version + "] 创建安装拓展任务失败:" + err.Error())
|
||||
@@ -278,7 +276,7 @@ func (c *Php74Controller) InstallExtension(ctx http.Context) {
|
||||
}
|
||||
|
||||
func (c *Php74Controller) UninstallExtension(ctx http.Context) {
|
||||
if !plugins.Check(ctx, "php"+c.version) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -299,7 +297,7 @@ func (c *Php74Controller) UninstallExtension(ctx http.Context) {
|
||||
var task models.Task
|
||||
task.Name = "卸载PHP-" + c.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = "bash scripts/php_extensions/" + item.Slug + ".sh uninstall " + c.version + ">> /tmp/" + item.Slug + ".log 2>&1"
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[PHP-" + c.version + "] 创建卸载拓展任务失败:" + err.Error())
|
||||
@@ -356,13 +354,13 @@ func (c *Php74Controller) GetExtensions() []Extension {
|
||||
Installed: false,
|
||||
})
|
||||
|
||||
raw := tools.ExecShell("/www/server/php/" + c.version + "/bin/php -m")
|
||||
raw := tools.Exec("/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].Name == item {
|
||||
if extensions[i].Slug == item {
|
||||
extensions[i].Installed = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
package php80
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Php80Controller struct {
|
||||
setting services.Setting
|
||||
task services.Task
|
||||
version string
|
||||
}
|
||||
|
||||
func NewPhp80Controller() *Php80Controller {
|
||||
return &Php80Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
task: services.NewTaskImpl(),
|
||||
version: "80",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Status(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.Exec("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 *Php80Controller) Reload(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Start(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Stop(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Restart(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) GetConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
config := tools.Read("/www/server/php/" + c.version + "/etc/php.ini")
|
||||
controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) SaveConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
tools.Write("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
|
||||
c.Reload(ctx)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Load(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + c.version)
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Error("获取PHP-" + c.version + "运行状态失败")
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
|
||||
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
|
||||
|
||||
type Data struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
data := make([]Data, len(dataKeys))
|
||||
for i := range dataKeys {
|
||||
data[i].Name = dataKeys[i]
|
||||
|
||||
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
|
||||
match := r.FindStringSubmatch(raw)
|
||||
|
||||
if len(match) > 1 {
|
||||
data[i].Value = strings.TrimSpace(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) ErrorLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log"))
|
||||
controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) SlowLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log"))
|
||||
controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) ClearErrorLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
|
||||
controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) ClearSlowLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
|
||||
controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
type Extension struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
func (c *Php80Controller) GetExtensionList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) InstallExtension(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
|
||||
return
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "安装PHP-" + c.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[PHP-" + c.version + "] 创建安装拓展任务失败:" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
controllers.Success(ctx, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php80Controller) UninstallExtension(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "php"+c.version) {
|
||||
return
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
|
||||
return
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "卸载PHP-" + c.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[PHP-" + c.version + "] 创建卸载拓展任务失败:" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
controllers.Success(ctx, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php80Controller) GetExtensions() []Extension {
|
||||
var extensions []Extension
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "OPcache",
|
||||
Slug: "Zend OPcache",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "PhpRedis",
|
||||
Slug: "redis",
|
||||
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ImageMagick",
|
||||
Slug: "imagick",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "Exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "pdo_pgsql",
|
||||
Slug: "pdo_pgsql",
|
||||
Description: "(需先安装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.Exec("/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,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.Exec("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.Exec("systemctl reload php-fpm-" + c.version)
|
||||
out := tools.Exec("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.Exec("systemctl start php-fpm-" + c.version)
|
||||
out := tools.Exec("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.Exec("systemctl stop php-fpm-" + c.version)
|
||||
out := tools.Exec("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.Exec("systemctl restart php-fpm-" + c.version)
|
||||
out := tools.Exec("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.Read("/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.Write("/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.Escape(tools.Exec("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.Escape(tools.Exec("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.Exec("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.Exec("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.Exec("/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.Exec("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.Exec("systemctl reload php-fpm-" + c.version)
|
||||
out := tools.Exec("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.Exec("systemctl start php-fpm-" + c.version)
|
||||
out := tools.Exec("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.Exec("systemctl stop php-fpm-" + c.version)
|
||||
out := tools.Exec("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.Exec("systemctl restart php-fpm-" + c.version)
|
||||
out := tools.Exec("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.Read("/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.Write("/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.Escape(tools.Exec("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.Escape(tools.Exec("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.Exec("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.Exec("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.Exec("/www/server/php/" + c.version + "/bin/php -m")
|
||||
rawExtensionList := strings.Split(raw, "\n")
|
||||
|
||||
for _, item := range rawExtensionList {
|
||||
if !strings.Contains(item, "[") && item != "" {
|
||||
for i := range extensions {
|
||||
if extensions[i].Slug == item {
|
||||
extensions[i].Installed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package phpmyadmin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type PhpMyAdminController struct {
|
||||
}
|
||||
|
||||
func NewPhpMyAdminController() *PhpMyAdminController {
|
||||
return &PhpMyAdminController{}
|
||||
}
|
||||
|
||||
func (c *PhpMyAdminController) Info(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "phpmyadmin") {
|
||||
return
|
||||
}
|
||||
|
||||
files, err := os.ReadDir("/www/server/phpmyadmin")
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 目录")
|
||||
return
|
||||
}
|
||||
|
||||
var phpmyadmin string
|
||||
for _, f := range files {
|
||||
if strings.HasPrefix(f.Name(), "phpmyadmin_") {
|
||||
phpmyadmin = f.Name()
|
||||
}
|
||||
}
|
||||
if len(phpmyadmin) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 目录")
|
||||
return
|
||||
}
|
||||
|
||||
conf := tools.Read("/www/server/vhost/phpmyadmin.conf")
|
||||
match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf)
|
||||
if len(match) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 端口")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, http.Json{
|
||||
"phpmyadmin": phpmyadmin,
|
||||
"port": match[1],
|
||||
})
|
||||
}
|
||||
|
||||
func (c *PhpMyAdminController) SetPort(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "phpmyadmin") {
|
||||
return
|
||||
}
|
||||
|
||||
port := ctx.Request().Input("port")
|
||||
if len(port) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "端口不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
conf := tools.Read("/www/server/vhost/phpmyadmin.conf")
|
||||
conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+port+";")
|
||||
tools.Write("/www/server/vhost/phpmyadmin.conf", conf, 0644)
|
||||
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("firewall-cmd --zone=public --add-port=" + port + "/tcp --permanent")
|
||||
tools.Exec("firewall-cmd --reload")
|
||||
} else {
|
||||
tools.Exec("ufw allow " + port + "/tcp")
|
||||
tools.Exec("ufw reload")
|
||||
}
|
||||
tools.Exec("systemctl reload openresty")
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
// Check 检查插件是否可用
|
||||
func Check(ctx http.Context, slug string) bool {
|
||||
plugin := services.NewPluginImpl().GetBySlug(slug)
|
||||
installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug)
|
||||
installedPlugins, err := services.NewPluginImpl().AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][插件] 获取已安装插件失败")
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return false
|
||||
}
|
||||
|
||||
if installedPlugin.Version != plugin.Version || installedPlugin.Slug != plugin.Slug {
|
||||
controllers.Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要更新至 "+plugin.Version+" 版本")
|
||||
return false
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
pluginsMap[p.Slug] = true
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
lock.RLock()
|
||||
_, requireFound := pluginsMap[require]
|
||||
lock.RUnlock()
|
||||
if !requireFound {
|
||||
controllers.Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
controllers.Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
package postgresql15
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Postgresql15Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func NewPostgresql15Controller() *Postgresql15Controller {
|
||||
return &Postgresql15Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *Postgresql15Controller) Status(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Postgresql15Controller) Reload(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Postgresql15Controller) Restart(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Postgresql15Controller) Start(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Postgresql15Controller) Stop(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *Postgresql15Controller) GetConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/postgresql/data/postgresql.conf")
|
||||
if len(config) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// GetUserConfig 获取用户配置
|
||||
func (c *Postgresql15Controller) GetUserConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/postgresql/data/pg_hba.conf")
|
||||
if len(config) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *Postgresql15Controller) SaveConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/postgresql/data/postgresql.conf", config, 0644) {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Restart(ctx)
|
||||
}
|
||||
|
||||
// SaveUserConfig 保存用户配置
|
||||
func (c *Postgresql15Controller) SaveUserConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644) {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *Postgresql15Controller) Load(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL 已停止运行")
|
||||
return
|
||||
}
|
||||
|
||||
data := []Info{
|
||||
{"启动时间", carbon.Parse(tools.Exec(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`)).ToDateTimeString()},
|
||||
{"进程 PID", tools.Exec(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)},
|
||||
{"进程数", tools.Exec(`ps aux | grep postgres | grep -v grep | wc -l`)},
|
||||
{"总连接数", tools.Exec(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)},
|
||||
{"空间占用", tools.Exec(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`)},
|
||||
}
|
||||
|
||||
controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
// Log 获取日志
|
||||
func (c *Postgresql15Controller) Log(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
log := tools.Exec("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
|
||||
controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (c *Postgresql15Controller) ClearLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (c *Postgresql15Controller) DatabaseList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL 已停止运行")
|
||||
return
|
||||
}
|
||||
|
||||
raw := tools.Exec(`echo "\l" | su - postgres -c "psql"`)
|
||||
databases := strings.Split(raw, "\n")
|
||||
databases = databases[3 : len(databases)-1]
|
||||
|
||||
type database struct {
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
|
||||
var databaseList []database
|
||||
for _, db := range databases {
|
||||
parts := strings.Split(db, "|")
|
||||
if len(parts) != 8 || len(strings.TrimSpace(parts[0])) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
databaseList = append(databaseList, database{
|
||||
Name: strings.TrimSpace(parts[0]),
|
||||
Owner: strings.TrimSpace(parts[1]),
|
||||
Encoding: strings.TrimSpace(parts[2]),
|
||||
})
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(databaseList) {
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
return
|
||||
}
|
||||
if endIndex > len(databaseList) {
|
||||
endIndex = len(databaseList)
|
||||
}
|
||||
pagedDatabases := databaseList[startIndex:endIndex]
|
||||
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": len(databaseList),
|
||||
"items": pagedDatabases,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (c *Postgresql15Controller) AddDatabase(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
tools.Exec(`echo "CREATE DATABASE ` + database + `;" | su - postgres -c "psql"`)
|
||||
tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`)
|
||||
tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`)
|
||||
|
||||
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
|
||||
tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`)
|
||||
|
||||
c.Reload(ctx)
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (c *Postgresql15Controller) DeleteDatabase(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`)
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (c *Postgresql15Controller) BackupList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
backupList, err := c.backup.PostgresqlList()
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 获取备份列表失败:" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, backupList)
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (c *Postgresql15Controller) UploadBackup(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
name := file.GetClientOriginalName()
|
||||
_, err = file.StoreAs(backupPath, name)
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (c *Postgresql15Controller) CreateBackup(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
err = c.backup.PostgresqlBackup(database)
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 创建备份失败:" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (c *Postgresql15Controller) DeleteBackup(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
|
||||
fileName := ctx.Request().Input("name")
|
||||
tools.Remove(backupPath + "/" + fileName)
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (c *Postgresql15Controller) RestoreBackup(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
err = c.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 还原失败:" + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UserList 用户列表
|
||||
func (c *Postgresql15Controller) UserList(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
type user struct {
|
||||
User string `json:"user"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
raw := tools.Exec(`echo "\du" | su - postgres -c "psql"`)
|
||||
users := strings.Split(raw, "\n")
|
||||
if len(users) < 4 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "用户列表为空")
|
||||
return
|
||||
}
|
||||
users = users[3:]
|
||||
|
||||
var userList []user
|
||||
for _, u := range users {
|
||||
userInfo := strings.Split(u, "|")
|
||||
if len(userInfo) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
userList = append(userList, user{
|
||||
User: strings.TrimSpace(userInfo[0]),
|
||||
Role: strings.TrimSpace(userInfo[1]),
|
||||
})
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(userList) {
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []user{},
|
||||
})
|
||||
return
|
||||
}
|
||||
if endIndex > len(userList) {
|
||||
endIndex = len(userList)
|
||||
}
|
||||
pagedUsers := userList[startIndex:endIndex]
|
||||
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": len(userList),
|
||||
"items": pagedUsers,
|
||||
})
|
||||
}
|
||||
|
||||
// AddUser 添加用户
|
||||
func (c *Postgresql15Controller) AddUser(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`)
|
||||
tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`)
|
||||
|
||||
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
|
||||
tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`)
|
||||
|
||||
c.Reload(ctx)
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (c *Postgresql15Controller) DeleteUser(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
tools.Exec(`echo "DROP USER ` + user + `;" | su - postgres -c "psql"`)
|
||||
tools.Exec(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`)
|
||||
|
||||
c.Reload(ctx)
|
||||
}
|
||||
|
||||
// SetUserPassword 设置用户密码
|
||||
func (c *Postgresql15Controller) SetUserPassword(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "postgresql15") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
tools.Exec(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`)
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package pureftpd
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type PureFtpdController struct {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func NewPureFtpdController() *PureFtpdController {
|
||||
return &PureFtpdController{}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *PureFtpdController) Status(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "pureftpd") {
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *PureFtpdController) Reload(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "pureftpd") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload pure-ftpd")
|
||||
status := tools.Exec("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *PureFtpdController) Restart(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "pureftpd") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart pure-ftpd")
|
||||
status := tools.Exec("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *PureFtpdController) Start(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "pureftpd") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start pure-ftpd")
|
||||
status := tools.Exec("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *PureFtpdController) Stop(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "pureftpd") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop pure-ftpd")
|
||||
status := tools.Exec("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (c *PureFtpdController) List(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "pureftpd") {
|
||||
return
|
||||
}
|
||||
|
||||
listRaw := tools.Exec("pure-pw list")
|
||||
if len(listRaw) == 0 {
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []User{},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
listArr := strings.Split(listRaw, "\n")
|
||||
var users []User
|
||||
for _, v := range listArr {
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`(\S+)\s+(\S+)`).FindStringSubmatch(v)
|
||||
users = append(users, User{
|
||||
Username: match[1],
|
||||
Path: strings.Replace(match[2], "/./", "/", 1),
|
||||
})
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(users) {
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []User{},
|
||||
})
|
||||
return
|
||||
}
|
||||
if endIndex > len(users) {
|
||||
endIndex = len(users)
|
||||
}
|
||||
pagedUsers := users[startIndex:endIndex]
|
||||
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": len(users),
|
||||
"items": pagedUsers,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加用户
|
||||
func (c *PureFtpdController) Add(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "pureftpd") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"username": "required",
|
||||
"password": "required|min_len:6",
|
||||
"path": "required",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
username := ctx.Request().Input("username")
|
||||
password := ctx.Request().Input("password")
|
||||
path := ctx.Request().Input("path")
|
||||
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if !tools.Exists(path) {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "目录不存在")
|
||||
return
|
||||
}
|
||||
|
||||
tools.Chmod(path, 0755)
|
||||
tools.Chown(path, "www", "www")
|
||||
tools.Exec(`yes '` + password + `' | pure-pw useradd ` + username + ` -u www -g www -d ` + path)
|
||||
tools.Exec("pure-pw mkdb")
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (c *PureFtpdController) Delete(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "pureftpd") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"username": "required",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
username := ctx.Request().Input("username")
|
||||
|
||||
tools.Exec("pure-pw userdel " + username + " -m")
|
||||
tools.Exec("pure-pw mkdb")
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (c *PureFtpdController) ChangePassword(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "pureftpd") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"username": "required",
|
||||
"password": "required|min_len:6",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
username := ctx.Request().Input("username")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
tools.Exec(`yes '` + password + `' | pure-pw passwd ` + username + ` -m`)
|
||||
tools.Exec("pure-pw mkdb")
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetPort 获取端口
|
||||
func (c *PureFtpdController) GetPort(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "pureftpd") {
|
||||
return
|
||||
}
|
||||
|
||||
port := tools.Exec(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
|
||||
if len(port) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd端口失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, port)
|
||||
}
|
||||
|
||||
// SetPort 设置端口
|
||||
func (c *PureFtpdController) SetPort(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "pureftpd") {
|
||||
return
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"port": "required",
|
||||
})
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
port := ctx.Request().Input("port")
|
||||
tools.Exec(`sed -i "s/Bind.*/Bind 0.0.0.0,` + port + `/g" /www/server/pure-ftpd/etc/pure-ftpd.conf`)
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("firewall-cmd --zone=public --add-port=" + port + "/tcp --permanent")
|
||||
tools.Exec("firewall-cmd --reload")
|
||||
} else {
|
||||
tools.Exec("ufw allow " + port + "/tcp")
|
||||
tools.Exec("ufw reload")
|
||||
}
|
||||
tools.Exec("systemctl restart pure-ftpd")
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type RedisController struct {
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func NewRedisController() *RedisController {
|
||||
return &RedisController{}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *RedisController) Status(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "redis") {
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *RedisController) Reload(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "redis") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload redis")
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *RedisController) Restart(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "redis") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart redis")
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *RedisController) Start(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "redis") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start redis")
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *RedisController) Stop(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "redis") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop redis")
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *RedisController) GetConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "redis") {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/redis/redis.conf")
|
||||
if len(config) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取Redis配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *RedisController) SaveConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "redis") {
|
||||
return
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/redis/redis.conf", config, 0644) {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "写入Redis配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *RedisController) Load(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "redis") {
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "Redis 已停止运行")
|
||||
return
|
||||
}
|
||||
|
||||
raw := tools.Exec("redis-cli info")
|
||||
if len(raw) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取Redis负载失败")
|
||||
return
|
||||
}
|
||||
|
||||
infoLines := strings.Split(raw, "\n")
|
||||
dataRaw := make(map[string]string)
|
||||
|
||||
for _, item := range infoLines {
|
||||
parts := strings.Split(item, ":")
|
||||
if len(parts) == 2 {
|
||||
dataRaw[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
data := []Info{
|
||||
{"TCP 端口", dataRaw["tcp_port"]},
|
||||
{"已运行天数", dataRaw["uptime_in_days"]},
|
||||
{"连接的客户端数", dataRaw["connected_clients"]},
|
||||
{"已分配的内存总量", dataRaw["used_memory_human"]},
|
||||
{"占用内存总量", dataRaw["used_memory_rss_human"]},
|
||||
{"占用内存峰值", dataRaw["used_memory_peak_human"]},
|
||||
{"内存碎片比率", dataRaw["mem_fragmentation_ratio"]},
|
||||
{"运行以来连接过的客户端的总数", dataRaw["total_connections_received"]},
|
||||
{"运行以来执行过的命令的总数", dataRaw["total_commands_processed"]},
|
||||
{"每秒执行的命令数", dataRaw["instantaneous_ops_per_sec"]},
|
||||
{"查找数据库键成功次数", dataRaw["keyspace_hits"]},
|
||||
{"查找数据库键失败次数", dataRaw["keyspace_misses"]},
|
||||
{"最近一次 fork() 操作耗费的毫秒数", dataRaw["latest_fork_usec"]},
|
||||
}
|
||||
|
||||
controllers.Success(ctx, data)
|
||||
}
|
||||
@@ -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.Write("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600)
|
||||
tools.Exec(`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.Exec("mount -a 2>&1")
|
||||
if len(check) != 0 {
|
||||
tools.Exec(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+check)
|
||||
return
|
||||
}
|
||||
check2 := tools.Exec("df -h | grep " + path + " 2>&1")
|
||||
if len(check2) == 0 {
|
||||
tools.Exec(`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.Exec(`fusermount -u '` + mount.Path + `' 2>&1`)
|
||||
tools.Exec(`umount '` + mount.Path + `' 2>&1`)
|
||||
tools.Exec(`sed -i 's@^s3fs#` + mount.Bucket + `\s` + mount.Path + `.*$@@g' /etc/fstab`)
|
||||
check := tools.Exec("mount -a 2>&1")
|
||||
if len(check) != 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+check)
|
||||
return
|
||||
}
|
||||
tools.Remove("/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,389 @@
|
||||
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 (c *SupervisorController) Status(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动
|
||||
func (c *SupervisorController) Start(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec(`systemctl start ` + c.ServiceName)
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止
|
||||
func (c *SupervisorController) Stop(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec(`systemctl stop ` + c.ServiceName)
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status != "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启
|
||||
func (c *SupervisorController) Restart(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec(`systemctl restart ` + c.ServiceName)
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载
|
||||
func (c *SupervisorController) Reload(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec(`systemctl reload ` + c.ServiceName)
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Log 日志
|
||||
func (c *SupervisorController) Log(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
log := tools.Exec(`tail -n 200 /var/log/supervisor/supervisord.log`)
|
||||
controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (c *SupervisorController) ClearLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec(`echo "" > /var/log/supervisor/supervisord.log`)
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Config 获取配置
|
||||
func (c *SupervisorController) Config(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
var config string
|
||||
if tools.IsRHEL() {
|
||||
config = tools.Read(`/etc/supervisord.conf`)
|
||||
} else {
|
||||
config = tools.Read(`/etc/supervisor/supervisord.conf`)
|
||||
}
|
||||
controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *SupervisorController) SaveConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if tools.IsRHEL() {
|
||||
tools.Write(`/etc/supervisord.conf`, config, 0644)
|
||||
} else {
|
||||
tools.Write(`/etc/supervisor/supervisord.conf`, config, 0644)
|
||||
}
|
||||
|
||||
c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Processes 进程列表
|
||||
func (c *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.Exec(`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.Exec(`supervisorctl status ` + line + ` | awk '{print $2}'`)
|
||||
if p.Status == "RUNNING" {
|
||||
p.Pid = strings.ReplaceAll(tools.Exec(`supervisorctl status `+line+` | awk '{print $4}'`), ",", "")
|
||||
p.Uptime = tools.Exec(`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 (c *SupervisorController) StartProcess(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
tools.Exec(`supervisorctl start ` + process)
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// StopProcess 停止进程
|
||||
func (c *SupervisorController) StopProcess(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
tools.Exec(`supervisorctl stop ` + process)
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RestartProcess 重启进程
|
||||
func (c *SupervisorController) RestartProcess(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
tools.Exec(`supervisorctl restart ` + process)
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ProcessLog 进程日志
|
||||
func (c *SupervisorController) ProcessLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
var logPath string
|
||||
if tools.IsRHEL() {
|
||||
logPath = tools.Exec(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
|
||||
} else {
|
||||
logPath = tools.Exec(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
|
||||
}
|
||||
|
||||
log := tools.Exec(`tail -n 200 ` + logPath)
|
||||
controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearProcessLog 清空进程日志
|
||||
func (c *SupervisorController) ClearProcessLog(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
var logPath string
|
||||
if tools.IsRHEL() {
|
||||
logPath = tools.Exec(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
|
||||
} else {
|
||||
logPath = tools.Exec(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
|
||||
}
|
||||
|
||||
tools.Exec(`echo "" > ` + logPath)
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ProcessConfig 获取进程配置
|
||||
func (c *SupervisorController) ProcessConfig(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
process := ctx.Request().Query("process")
|
||||
var config string
|
||||
if tools.IsRHEL() {
|
||||
config = tools.Read(`/etc/supervisord.d/` + process + `.conf`)
|
||||
} else {
|
||||
config = tools.Read(`/etc/supervisor/conf.d/` + process + `.conf`)
|
||||
}
|
||||
|
||||
controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveProcessConfig 保存进程配置
|
||||
func (c *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.Write(`/etc/supervisord.d/`+process+`.conf`, config, 0644)
|
||||
} else {
|
||||
tools.Write(`/etc/supervisor/conf.d/`+process+`.conf`, config, 0644)
|
||||
}
|
||||
tools.Exec(`supervisorctl reread`)
|
||||
tools.Exec(`supervisorctl update`)
|
||||
tools.Exec(`supervisorctl start ` + process)
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// AddProcess 添加进程
|
||||
func (c *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.Write(`/etc/supervisord.d/`+name+`.conf`, config, 0644)
|
||||
} else {
|
||||
tools.Write(`/etc/supervisor/conf.d/`+name+`.conf`, config, 0644)
|
||||
}
|
||||
tools.Exec(`supervisorctl reread`)
|
||||
tools.Exec(`supervisorctl update`)
|
||||
tools.Exec(`supervisorctl start ` + name)
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteProcess 删除进程
|
||||
func (c *SupervisorController) DeleteProcess(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "supervisor") {
|
||||
return
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
tools.Exec(`supervisorctl stop ` + process)
|
||||
var logPath string
|
||||
if tools.IsRHEL() {
|
||||
logPath = tools.Exec(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
|
||||
tools.Remove(`/etc/supervisord.d/` + process + `.conf`)
|
||||
} else {
|
||||
logPath = tools.Exec(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
|
||||
tools.Remove(`/etc/supervisor/conf.d/` + process + `.conf`)
|
||||
}
|
||||
tools.Remove(logPath)
|
||||
tools.Exec(`supervisorctl reread`)
|
||||
tools.Exec(`supervisorctl update`)
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package toolbox
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type ToolBoxController struct {
|
||||
}
|
||||
|
||||
func NewToolBoxController() *ToolBoxController {
|
||||
return &ToolBoxController{}
|
||||
}
|
||||
|
||||
// GetDNS 获取 DNS 信息
|
||||
func (c *ToolBoxController) GetDNS(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "toolbox") {
|
||||
return
|
||||
}
|
||||
|
||||
raw := tools.Read("/etc/resolv.conf")
|
||||
match := regexp.MustCompile(`nameserver\s+(\S+)`).FindAllStringSubmatch(raw, -1)
|
||||
if len(match) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "找不到 DNS 信息")
|
||||
return
|
||||
}
|
||||
|
||||
var dns []string
|
||||
for _, m := range match {
|
||||
dns = append(dns, m[1])
|
||||
}
|
||||
|
||||
controllers.Success(ctx, dns)
|
||||
}
|
||||
|
||||
// SetDNS 设置 DNS 信息
|
||||
func (c *ToolBoxController) SetDNS(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "toolbox") {
|
||||
return
|
||||
}
|
||||
|
||||
dns1 := ctx.Request().Input("dns1")
|
||||
dns2 := ctx.Request().Input("dns2")
|
||||
|
||||
if len(dns1) == 0 || len(dns2) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "DNS 信息不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var dns string
|
||||
dns += "nameserver " + dns1 + "\n"
|
||||
dns += "nameserver " + dns2 + "\n"
|
||||
|
||||
tools.Write("/etc/resolv.conf", dns, 0644)
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetSWAP 获取 SWAP 信息
|
||||
func (c *ToolBoxController) GetSWAP(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "toolbox") {
|
||||
return
|
||||
}
|
||||
|
||||
var total, size, used, free string
|
||||
if tools.Exists("/www/swap") {
|
||||
s, _ := tools.FileSize("/www/swap")
|
||||
size = strconv.Itoa(int(s / 1024 / 1024))
|
||||
total = tools.FormatBytes(float64(s))
|
||||
} else {
|
||||
size = "0.00 B"
|
||||
total = "0.00 B"
|
||||
}
|
||||
|
||||
raw := tools.Exec("LC_ALL=C free | grep Swap")
|
||||
match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) > 0 {
|
||||
used = tools.FormatBytes(cast.ToFloat64(match[2]) * 1024)
|
||||
free = tools.FormatBytes(cast.ToFloat64(match[3]) * 1024)
|
||||
}
|
||||
|
||||
controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"size": size,
|
||||
"used": used,
|
||||
"free": free,
|
||||
})
|
||||
}
|
||||
|
||||
// SetSWAP 设置 SWAP 信息
|
||||
func (c *ToolBoxController) SetSWAP(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "toolbox") {
|
||||
return
|
||||
}
|
||||
|
||||
size := ctx.Request().InputInt("size")
|
||||
|
||||
if tools.Exists("/www/swap") {
|
||||
tools.Exec("swapoff /www/swap")
|
||||
tools.Exec("rm -f /www/swap")
|
||||
tools.Exec("sed -i '/www\\/swap/d' /etc/fstab")
|
||||
}
|
||||
|
||||
if size > 1 {
|
||||
free := tools.Exec("df -k /www | awk '{print $4}' | tail -n 1")
|
||||
if cast.ToInt64(free)*1024 < int64(size)*1024*1024 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "磁盘空间不足,当前剩余 "+tools.FormatBytes(cast.ToFloat64(free)))
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(tools.Exec("df -T /www | awk '{print $2}' | tail -n 1"), "btrfs") {
|
||||
tools.Exec("btrfs filesystem mkswapfile --size " + cast.ToString(size) + "M --uuid clear /www/swap")
|
||||
} else {
|
||||
tools.Exec("dd if=/dev/zero of=/www/swap bs=1M count=" + cast.ToString(size))
|
||||
tools.Exec("mkswap -f /www/swap")
|
||||
tools.Chmod("/www/swap", 0600)
|
||||
}
|
||||
tools.Exec("swapon /www/swap")
|
||||
tools.Exec("echo '/www/swap swap swap defaults 0 0' >> /etc/fstab")
|
||||
}
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetTimezone 获取时区
|
||||
func (c *ToolBoxController) GetTimezone(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "toolbox") {
|
||||
return
|
||||
}
|
||||
|
||||
raw := tools.Exec("LC_ALL=C timedatectl | grep zone")
|
||||
match := regexp.MustCompile(`zone:\s+(\S+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "找不到时区信息")
|
||||
return
|
||||
}
|
||||
|
||||
zonesRaw := tools.Exec("LC_ALL=C timedatectl list-timezones")
|
||||
zones := strings.Split(zonesRaw, "\n")
|
||||
|
||||
controllers.Success(ctx, http.Json{
|
||||
"zone": match[1],
|
||||
"zones": zones,
|
||||
})
|
||||
}
|
||||
|
||||
// SetTimezone 设置时区
|
||||
func (c *ToolBoxController) SetTimezone(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "toolbox") {
|
||||
return
|
||||
}
|
||||
|
||||
timezone := ctx.Request().Input("timezone")
|
||||
if len(timezone) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "时区不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
tools.Exec("timedatectl set-timezone " + timezone)
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetHosts 获取 hosts 信息
|
||||
func (c *ToolBoxController) GetHosts(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "toolbox") {
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, tools.Read("/etc/hosts"))
|
||||
}
|
||||
|
||||
// SetHosts 设置 hosts 信息
|
||||
func (c *ToolBoxController) SetHosts(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "toolbox") {
|
||||
return
|
||||
}
|
||||
|
||||
hosts := ctx.Request().Input("hosts")
|
||||
if len(hosts) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "hosts 信息不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
tools.Write("/etc/hosts", hosts, 0644)
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SetRootPassword 设置 root 密码
|
||||
func (c *ToolBoxController) SetRootPassword(ctx http.Context) {
|
||||
if !controllers.Check(ctx, "toolbox") {
|
||||
return
|
||||
}
|
||||
|
||||
password := ctx.Request().Input("password")
|
||||
if len(password) == 0 {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "密码不能为空")
|
||||
return
|
||||
}
|
||||
if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(password) {
|
||||
controllers.Error(ctx, http.StatusBadRequest, "密码必须为 6-20 位字母、数字或特殊字符")
|
||||
return
|
||||
}
|
||||
|
||||
password = strings.ReplaceAll(password, `'`, `\'`)
|
||||
tools.Exec(`yes '` + password + `' | passwd root`)
|
||||
|
||||
controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -26,17 +26,17 @@ func (r *SafeController) GetFirewallStatus(ctx http.Context) {
|
||||
|
||||
func (r *SafeController) SetFirewallStatus(ctx http.Context) {
|
||||
var out string
|
||||
if ctx.Request().QueryBool("status") {
|
||||
if ctx.Request().InputBool("status") {
|
||||
if tools.IsRHEL() {
|
||||
out = tools.ExecShell("systemctl start firewalld")
|
||||
out = tools.Exec("systemctl start firewalld")
|
||||
} else {
|
||||
out = tools.ExecShell("echo y | ufw enable")
|
||||
out = tools.Exec("echo y | ufw enable")
|
||||
}
|
||||
} else {
|
||||
if tools.IsRHEL() {
|
||||
out = tools.ExecShell("systemctl stop firewalld")
|
||||
out = tools.Exec("systemctl stop firewalld")
|
||||
} else {
|
||||
out = tools.ExecShell("ufw disable")
|
||||
out = tools.Exec("ufw disable")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,15 +45,20 @@ func (r *SafeController) SetFirewallStatus(ctx http.Context) {
|
||||
|
||||
func (r *SafeController) GetFirewallRules(ctx http.Context) {
|
||||
if !r.firewallStatus() {
|
||||
Error(ctx, http.StatusBadRequest, "防火墙未启动")
|
||||
Success(ctx, nil)
|
||||
return
|
||||
}
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
|
||||
if tools.IsRHEL() {
|
||||
out := tools.ExecShell("firewall-cmd --list-all 2>&1")
|
||||
out := tools.Exec("firewall-cmd --list-all 2>&1")
|
||||
match := regexp.MustCompile(`ports: (.*)`).FindStringSubmatch(out)
|
||||
if len(match) == 0 {
|
||||
Success(ctx, nil)
|
||||
Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
return
|
||||
}
|
||||
ports := strings.Split(match[1], " ")
|
||||
@@ -66,18 +71,35 @@ func (r *SafeController) GetFirewallRules(ctx http.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
Success(ctx, rules)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(rules) {
|
||||
Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
return
|
||||
}
|
||||
if endIndex > len(rules) {
|
||||
endIndex = len(rules)
|
||||
}
|
||||
pagedRules := rules[startIndex:endIndex]
|
||||
|
||||
Success(ctx, http.Json{
|
||||
"total": len(rules),
|
||||
"items": pagedRules,
|
||||
})
|
||||
} else {
|
||||
out := tools.ExecShell("ufw status numbered | grep ALLOW | awk '{print $2}'")
|
||||
out := tools.Exec("ufw status | grep -v '(v6)' | grep ALLOW | awk '{print $1}'")
|
||||
if len(out) == 0 {
|
||||
Success(ctx, nil)
|
||||
Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
return
|
||||
}
|
||||
var rules []map[string]string
|
||||
for _, port := range strings.Split(out, "\n") {
|
||||
if strings.Contains(port, "]") {
|
||||
continue
|
||||
}
|
||||
rule := strings.Split(port, "/")
|
||||
rules = append(rules, map[string]string{
|
||||
"port": rule[0],
|
||||
@@ -85,7 +107,24 @@ func (r *SafeController) GetFirewallRules(ctx http.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
Success(ctx, rules)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(rules) {
|
||||
Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
return
|
||||
}
|
||||
if endIndex > len(rules) {
|
||||
endIndex = len(rules)
|
||||
}
|
||||
pagedRules := rules[startIndex:endIndex]
|
||||
|
||||
Success(ctx, http.Json{
|
||||
"total": len(rules),
|
||||
"items": pagedRules,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,13 +142,13 @@ func (r *SafeController) AddFirewallRule(ctx http.Context) {
|
||||
}
|
||||
|
||||
if tools.IsRHEL() {
|
||||
tools.ExecShell("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
|
||||
tools.ExecShell("firewall-cmd --add-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
|
||||
tools.ExecShell("firewall-cmd --reload")
|
||||
tools.Exec("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
|
||||
tools.Exec("firewall-cmd --add-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
|
||||
tools.Exec("firewall-cmd --reload")
|
||||
} else {
|
||||
tools.ExecShell("ufw delete allow " + cast.ToString(port) + "/" + protocol)
|
||||
tools.ExecShell("ufw allow " + cast.ToString(port) + "/" + protocol)
|
||||
tools.ExecShell("ufw reload")
|
||||
tools.Exec("ufw delete allow " + cast.ToString(port) + "/" + protocol)
|
||||
tools.Exec("ufw allow " + cast.ToString(port) + "/" + protocol)
|
||||
tools.Exec("ufw reload")
|
||||
}
|
||||
|
||||
Success(ctx, nil)
|
||||
@@ -129,11 +168,11 @@ func (r *SafeController) DeleteFirewallRule(ctx http.Context) {
|
||||
}
|
||||
|
||||
if tools.IsRHEL() {
|
||||
tools.ExecShell("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
|
||||
tools.ExecShell("firewall-cmd --reload")
|
||||
tools.Exec("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
|
||||
tools.Exec("firewall-cmd --reload")
|
||||
} else {
|
||||
tools.ExecShell("ufw delete allow " + cast.ToString(port) + "/" + protocol)
|
||||
tools.ExecShell("ufw reload")
|
||||
tools.Exec("ufw delete allow " + cast.ToString(port) + "/" + protocol)
|
||||
tools.Exec("ufw reload")
|
||||
}
|
||||
|
||||
Success(ctx, nil)
|
||||
@@ -143,14 +182,14 @@ func (r *SafeController) firewallStatus() bool {
|
||||
var out string
|
||||
var running bool
|
||||
if tools.IsRHEL() {
|
||||
out = tools.ExecShell("systemctl status firewalld | grep Active | awk '{print $3}'")
|
||||
out = tools.Exec("systemctl status firewalld | grep Active | awk '{print $3}'")
|
||||
if out == "(running)" {
|
||||
running = true
|
||||
} else {
|
||||
running = false
|
||||
}
|
||||
} else {
|
||||
out = tools.ExecShell("ufw status | grep Status | awk '{print $2}'")
|
||||
out = tools.Exec("ufw status | grep Status | awk '{print $2}'")
|
||||
if out == "active" {
|
||||
running = true
|
||||
} else {
|
||||
@@ -162,7 +201,13 @@ func (r *SafeController) firewallStatus() bool {
|
||||
}
|
||||
|
||||
func (r *SafeController) GetSshStatus(ctx http.Context) {
|
||||
out := tools.ExecShell("systemctl status sshd | grep Active | awk '{print $3}'")
|
||||
var out string
|
||||
if tools.IsRHEL() {
|
||||
out = tools.Exec("systemctl status sshd | grep Active | awk '{print $3}'")
|
||||
} else {
|
||||
out = tools.Exec("systemctl status ssh | grep Active | awk '{print $3}'")
|
||||
}
|
||||
|
||||
running := false
|
||||
if out == "(running)" {
|
||||
running = true
|
||||
@@ -172,19 +217,29 @@ func (r *SafeController) GetSshStatus(ctx http.Context) {
|
||||
}
|
||||
|
||||
func (r *SafeController) SetSshStatus(ctx http.Context) {
|
||||
if ctx.Request().QueryBool("status") {
|
||||
tools.ExecShell("systemctl enable sshd")
|
||||
tools.ExecShell("systemctl start sshd")
|
||||
if ctx.Request().InputBool("status") {
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("systemctl enable sshd")
|
||||
tools.Exec("systemctl start sshd")
|
||||
} else {
|
||||
tools.Exec("systemctl enable ssh")
|
||||
tools.Exec("systemctl start ssh")
|
||||
}
|
||||
} else {
|
||||
tools.ExecShell("systemctl stop sshd")
|
||||
tools.ExecShell("systemctl disable sshd")
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("systemctl stop sshd")
|
||||
tools.Exec("systemctl disable sshd")
|
||||
} else {
|
||||
tools.Exec("systemctl stop ssh")
|
||||
tools.Exec("systemctl disable ssh")
|
||||
}
|
||||
}
|
||||
|
||||
Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *SafeController) GetSshPort(ctx http.Context) {
|
||||
out := tools.ExecShell("cat /etc/ssh/sshd_config | grep Port | awk '{print $2}'")
|
||||
out := tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
Success(ctx, out)
|
||||
}
|
||||
|
||||
@@ -195,12 +250,12 @@ func (r *SafeController) SetSshPort(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
oldPort := tools.ExecShell("cat /etc/ssh/sshd_config | grep Port | awk '{print $2}'")
|
||||
tools.ExecShell("sed -i 's/#Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
|
||||
tools.ExecShell("sed -i 's/Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
|
||||
oldPort := tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
tools.Exec("sed -i 's/#Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
|
||||
tools.Exec("sed -i 's/Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
|
||||
|
||||
if status := tools.ExecShell("systemctl status sshd | grep Active | awk '{print $3}'"); status == "(running)" {
|
||||
tools.ExecShell("systemctl restart sshd")
|
||||
if status := tools.Exec("systemctl status sshd | grep Active | awk '{print $3}'"); status == "(running)" {
|
||||
tools.Exec("systemctl restart sshd")
|
||||
}
|
||||
|
||||
Success(ctx, nil)
|
||||
@@ -208,14 +263,14 @@ 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.Exec(`firewall-cmd --list-all 2>&1`)
|
||||
if !strings.Contains(out, `rule protocol value="icmp" drop`) {
|
||||
Success(ctx, true)
|
||||
} else {
|
||||
Success(ctx, false)
|
||||
}
|
||||
} else {
|
||||
config := tools.ReadFile("/etc/ufw/before.rules")
|
||||
config := tools.Read("/etc/ufw/before.rules")
|
||||
if strings.Contains(config, "-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT") {
|
||||
Success(ctx, true)
|
||||
} else {
|
||||
@@ -226,19 +281,19 @@ func (r *SafeController) GetPingStatus(ctx http.Context) {
|
||||
|
||||
func (r *SafeController) SetPingStatus(ctx http.Context) {
|
||||
if tools.IsRHEL() {
|
||||
if ctx.Request().QueryBool("status") {
|
||||
tools.ExecShell("firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'")
|
||||
if ctx.Request().InputBool("status") {
|
||||
tools.Exec(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`)
|
||||
} else {
|
||||
tools.ExecShell("firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'")
|
||||
tools.Exec(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`)
|
||||
}
|
||||
tools.ExecShell("firewall-cmd --reload")
|
||||
tools.Exec(`firewall-cmd --reload`)
|
||||
} else {
|
||||
if ctx.Request().QueryBool("status") {
|
||||
tools.ExecShell("sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules")
|
||||
if ctx.Request().InputBool("status") {
|
||||
tools.Exec(`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 DROP/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/g' /etc/ufw/before.rules")
|
||||
tools.Exec(`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.Exec(`ufw reload`)
|
||||
}
|
||||
|
||||
Success(ctx, nil)
|
||||
|
||||
@@ -46,10 +46,11 @@ func (r *SettingController) List(ctx http.Context) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
result["username"] = user.Username
|
||||
result["email"] = user.Email
|
||||
|
||||
result["port"] = tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
|
||||
Success(ctx, result)
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@ func (r *SettingController) Save(ctx http.Context) {
|
||||
port := ctx.Request().Input("port")
|
||||
backupPath := ctx.Request().Input("backup_path")
|
||||
websitePath := ctx.Request().Input("website_path")
|
||||
panelEntrance := ctx.Request().Input("panel_entrance")
|
||||
entrance := ctx.Request().Input("entrance")
|
||||
username := ctx.Request().Input("username")
|
||||
email := ctx.Request().Input("email")
|
||||
password := ctx.Request().Input("password")
|
||||
@@ -70,9 +71,12 @@ func (r *SettingController) Save(ctx http.Context) {
|
||||
|
||||
return
|
||||
}
|
||||
oldPort := tools.ExecShell("cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}'")
|
||||
oldPort := tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
if oldPort != port {
|
||||
tools.ExecShell("sed -i 's/APP_PORT=" + oldPort + "/APP_PORT=" + port + "/g' /www/panel/panel.conf")
|
||||
tools.Exec("sed -i 's/APP_PORT=" + oldPort + "/APP_PORT=" + port + "/g' /www/panel/panel.conf")
|
||||
}
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeyBackupPath, backupPath)
|
||||
if err != nil {
|
||||
@@ -81,6 +85,10 @@ func (r *SettingController) Save(ctx http.Context) {
|
||||
|
||||
return
|
||||
}
|
||||
if !tools.Exists(websitePath) {
|
||||
tools.Mkdir(websitePath, 0755)
|
||||
tools.Chown(websitePath, "www", "www")
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeyWebsitePath, websitePath)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
@@ -88,7 +96,7 @@ func (r *SettingController) Save(ctx http.Context) {
|
||||
|
||||
return
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeyPanelEntrance, panelEntrance)
|
||||
err = r.setting.Set(models.SettingKeyEntrance, entrance)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
@@ -105,8 +113,12 @@ func (r *SettingController) Save(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user.Username = username
|
||||
user.Email = email
|
||||
if len(username) > 0 {
|
||||
user.Username = username
|
||||
}
|
||||
if len(email) > 0 {
|
||||
user.Email = email
|
||||
}
|
||||
if len(password) > 0 {
|
||||
hash, err := facades.Hash().Make(password)
|
||||
if err != nil {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -62,14 +62,14 @@ func (r *TaskController) Log(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log := tools.ExecShell("tail -n 30 " + task.Log)
|
||||
log := tools.Exec("tail -n 30 " + task.Log)
|
||||
|
||||
Success(ctx, log)
|
||||
}
|
||||
|
||||
func (r *TaskController) Delete(ctx http.Context) {
|
||||
var task models.Task
|
||||
_, err := facades.Orm().Query().Where("id", ctx.Request().QueryInt("id")).Delete(&task)
|
||||
_, err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).Delete(&task)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][TaskController] 删除任务失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"panel/app/models"
|
||||
"panel/pkg/tools"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type WebsiteController struct {
|
||||
@@ -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,32 +46,39 @@ func (c *WebsiteController) List(ctx http.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加网站
|
||||
func (c *WebsiteController) Add(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$",
|
||||
"name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$|not_exists:websites,name",
|
||||
"domain": "required",
|
||||
"php": "required",
|
||||
"db": "required",
|
||||
"db_type": "required_if:db,1",
|
||||
"db_name": "required_if:db,1",
|
||||
"db_user": "required_if:db,1",
|
||||
"db_password": "required_if:db,1",
|
||||
"db": "bool",
|
||||
"db_type": "required_if:db,true",
|
||||
"db_name": "required_if:db,true",
|
||||
"db_user": "required_if:db,true",
|
||||
"db_password": "required_if:db,true",
|
||||
})
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
var website services.PanelWebsite
|
||||
err = ctx.Request().Bind(&website)
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
website.Name = ctx.Request().Input("name")
|
||||
website.Domain = ctx.Request().Input("domain")
|
||||
website.Php = ctx.Request().InputInt("php")
|
||||
website.Db = ctx.Request().InputBool("db")
|
||||
website.DbType = ctx.Request().Input("db_type")
|
||||
website.DbName = ctx.Request().Input("db_name")
|
||||
website.DbUser = ctx.Request().Input("db_user")
|
||||
website.DbPassword = ctx.Request().Input("db_password")
|
||||
|
||||
newSite, err := c.website.Add(website)
|
||||
if err != nil {
|
||||
@@ -82,33 +90,29 @@ func (c *WebsiteController) Add(ctx http.Context) {
|
||||
Success(ctx, newSite)
|
||||
}
|
||||
|
||||
// Delete 删除网站
|
||||
func (c *WebsiteController) Delete(ctx http.Context) {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"id": "required|int",
|
||||
})
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusBadRequest, err.Error())
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
return
|
||||
}
|
||||
|
||||
id := ctx.Request().InputInt("id")
|
||||
err = c.website.Delete(id)
|
||||
err := c.website.Delete(id)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 删除网站失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
Error(ctx, http.StatusInternalServerError, "删除网站失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetDefaultConfig 获取默认配置
|
||||
func (c *WebsiteController) GetDefaultConfig(ctx http.Context) {
|
||||
index := tools.ReadFile("/www/server/openresty/html/index.html")
|
||||
stop := tools.ReadFile("/www/server/openresty/html/stop.html")
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
index := tools.Read("/www/server/openresty/html/index.html")
|
||||
stop := tools.Read("/www/server/openresty/html/stop.html")
|
||||
|
||||
Success(ctx, http.Json{
|
||||
"index": index,
|
||||
@@ -116,17 +120,21 @@ func (c *WebsiteController) GetDefaultConfig(ctx http.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// SaveDefaultConfig 保存默认配置
|
||||
func (c *WebsiteController) SaveDefaultConfig(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
index := ctx.Request().Input("index")
|
||||
stop := ctx.Request().Input("stop")
|
||||
|
||||
if !tools.WriteFile("/www/server/openresty/html/index.html", index, 0644) {
|
||||
if !tools.Write("/www/server/openresty/html/index.html", index, 0644) {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存默认配置失败")
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
|
||||
if !tools.WriteFile("/www/server/openresty/html/stop.html", stop, 0644) {
|
||||
if !tools.Write("/www/server/openresty/html/stop.html", stop, 0644) {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存默认配置失败")
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
@@ -135,7 +143,11 @@ func (c *WebsiteController) SaveDefaultConfig(ctx http.Context) {
|
||||
Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *WebsiteController) GetConfig(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
@@ -152,22 +164,42 @@ func (c *WebsiteController) GetConfig(ctx http.Context) {
|
||||
Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *WebsiteController) SaveConfig(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"id": "required|int",
|
||||
"id": "required",
|
||||
"domains": "required",
|
||||
"ports": "required",
|
||||
"hsts": "bool",
|
||||
"ssl": "bool",
|
||||
"http_redirect": "bool",
|
||||
"open_basedir": "bool",
|
||||
"waf": "required",
|
||||
"waf_cache": "required",
|
||||
"waf_mode": "required",
|
||||
"waf_cc_deny": "required",
|
||||
"index": "required",
|
||||
"path": "required",
|
||||
"root": "required",
|
||||
"raw": "required",
|
||||
"php": "required",
|
||||
"ssl_certificate": "required_if:ssl,true",
|
||||
"ssl_certificate_key": "required_if:ssl,true",
|
||||
})
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if validator.Fails() {
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().All())
|
||||
Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
return
|
||||
}
|
||||
|
||||
id := ctx.Request().InputInt("id")
|
||||
var website models.Website
|
||||
if facades.Orm().Query().Where("id", id).FirstOrFail(&website) != nil {
|
||||
if facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&website) != nil {
|
||||
Error(ctx, http.StatusBadRequest, "网站不存在")
|
||||
return
|
||||
}
|
||||
@@ -178,9 +210,10 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
|
||||
}
|
||||
|
||||
// 原文
|
||||
raw := tools.ReadFile("/www/server/panel/vhost/openresty/" + website.Name + ".conf")
|
||||
raw := tools.Read("/www/server/vhost/" + website.Name + ".conf")
|
||||
if strings.TrimSpace(raw) != strings.TrimSpace(ctx.Request().Input("raw")) {
|
||||
tools.WriteFile("/www/server/panel/vhost/openresty/"+website.Name+".conf", ctx.Request().Input("raw"), 0644)
|
||||
tools.Write("/www/server/vhost/"+website.Name+".conf", ctx.Request().Input("raw"), 0644)
|
||||
tools.Exec("systemctl reload openresty")
|
||||
Success(ctx, nil)
|
||||
return
|
||||
}
|
||||
@@ -195,7 +228,7 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
|
||||
|
||||
// 域名
|
||||
domain := "server_name"
|
||||
domains := strings.Split(ctx.Request().Input("domain"), "\n")
|
||||
domains := strings.Split(ctx.Request().Input("domains"), "\n")
|
||||
if len(domains) == 0 {
|
||||
Error(ctx, http.StatusBadRequest, "域名不能为空")
|
||||
return
|
||||
@@ -216,7 +249,7 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
|
||||
|
||||
// 端口
|
||||
var port strings.Builder
|
||||
ports := strings.Split(ctx.Request().Input("port"), "\n")
|
||||
ports := strings.Split(ctx.Request().Input("ports"), "\n")
|
||||
if len(ports) == 0 {
|
||||
Error(ctx, http.StatusBadRequest, "端口不能为空")
|
||||
return
|
||||
@@ -253,7 +286,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)
|
||||
|
||||
// 默认文件
|
||||
@@ -271,14 +304,15 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
|
||||
raw = strings.Replace(raw, index, indexNew, -1)
|
||||
|
||||
// 防跨站
|
||||
if !strings.HasSuffix(path, "/") {
|
||||
path += "/"
|
||||
root = ctx.Request().Input("root")
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root += "/"
|
||||
}
|
||||
if ctx.Request().InputBool("open_basedir") {
|
||||
tools.WriteFile(path+".user.ini", "open_basedir="+path+":/tmp/", 0644)
|
||||
tools.Write(root+".user.ini", "open_basedir="+path+":/tmp/", 0644)
|
||||
} else {
|
||||
if tools.Exists(path + ".user.ini") {
|
||||
tools.RemoveFile(path + ".user.ini")
|
||||
if tools.Exists(root + ".user.ini") {
|
||||
tools.Remove(root + ".user.ini")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,15 +321,13 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
|
||||
wafMode := ctx.Request().Input("waf_mode", "DYNAMIC")
|
||||
wafCcDeny := ctx.Request().Input("waf_cc_deny", "rate=1000r/m duration=60m")
|
||||
wafCache := ctx.Request().Input("waf_cache", "capacity=50")
|
||||
wafConfig := `
|
||||
# waf标记位开始
|
||||
wafConfig := `# waf标记位开始
|
||||
waf ` + waf + `;
|
||||
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
|
||||
waf_mode ` + wafMode + `;
|
||||
waf_cc_deny ` + wafCcDeny + `;
|
||||
waf_cache ` + wafCache + `;
|
||||
|
||||
`
|
||||
`
|
||||
wafConfigOld := tools.Cut(raw, "# waf标记位开始", "# waf标记位结束")
|
||||
if len(strings.TrimSpace(wafConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, wafConfigOld, "", -1)
|
||||
@@ -305,11 +337,10 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
|
||||
// SSL
|
||||
ssl := ctx.Request().InputBool("ssl")
|
||||
website.Ssl = ssl
|
||||
tools.Write("/www/server/vhost/ssl/"+website.Name+".pem", ctx.Request().Input("ssl_certificate"), 0644)
|
||||
tools.Write("/www/server/vhost/ssl/"+website.Name+".key", ctx.Request().Input("ssl_certificate_key"), 0644)
|
||||
if ssl {
|
||||
tools.WriteFile("/www/server/vhost/ssl/"+website.Name+".pem", ctx.Request().Input("ssl_certificate"), 0644)
|
||||
tools.WriteFile("/www/server/vhost/ssl/"+website.Name+".key", ctx.Request().Input("ssl_certificate_key"), 0644)
|
||||
sslConfig := `
|
||||
# ssl标记位开始
|
||||
sslConfig := `# ssl标记位开始
|
||||
ssl_certificate /www/server/vhost/ssl/` + website.Name + `.pem;
|
||||
ssl_certificate_key /www/server/vhost/ssl/` + website.Name + `.key;
|
||||
ssl_session_timeout 1d;
|
||||
@@ -318,28 +349,21 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
`
|
||||
`
|
||||
if ctx.Request().InputBool("http_redirect") {
|
||||
sslConfig += `
|
||||
|
||||
# http重定向标记位开始
|
||||
if (\$server_port !~ 443){
|
||||
return 301 https://\$host\$request_uri;
|
||||
sslConfig += `# http重定向标记位开始
|
||||
if ($server_port !~ 443){
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
error_page 497 https://\$host\$request_uri;
|
||||
error_page 497 https://$host$request_uri;
|
||||
# http重定向标记位结束
|
||||
|
||||
`
|
||||
`
|
||||
}
|
||||
if ctx.Request().InputBool("hsts") {
|
||||
sslConfig += `
|
||||
|
||||
# hsts标记位开始
|
||||
sslConfig += `# hsts标记位开始
|
||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
# hsts标记位结束
|
||||
|
||||
`
|
||||
`
|
||||
}
|
||||
sslConfigOld := tools.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
|
||||
if len(strings.TrimSpace(sslConfigOld)) != 0 {
|
||||
@@ -349,7 +373,7 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
|
||||
} else {
|
||||
sslConfigOld := tools.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
|
||||
if len(strings.TrimSpace(sslConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, sslConfigOld, "", -1)
|
||||
raw = strings.Replace(raw, sslConfigOld, "\n ", -1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,9 +381,8 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
|
||||
website.Php = ctx.Request().InputInt("php")
|
||||
phpConfigOld := tools.Cut(raw, "# php标记位开始", "# php标记位结束")
|
||||
phpConfig := `
|
||||
include enable-php` + strconv.Itoa(website.Php) + `.conf;
|
||||
|
||||
`
|
||||
include enable-php-` + strconv.Itoa(website.Php) + `.conf;
|
||||
`
|
||||
if len(strings.TrimSpace(phpConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, phpConfigOld, phpConfig, -1)
|
||||
}
|
||||
@@ -372,14 +395,18 @@ func (c *WebsiteController) SaveConfig(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", raw, 0644)
|
||||
tools.WriteFile("/www/server/vhost/rewrite/"+website.Name+".conf", ctx.Request().Input("rewrite"), 0644)
|
||||
tools.ExecShell("systemctl reload openresty")
|
||||
tools.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644)
|
||||
tools.Write("/www/server/vhost/rewrite/"+website.Name+".conf", ctx.Request().Input("rewrite"), 0644)
|
||||
tools.Exec("systemctl reload openresty")
|
||||
|
||||
Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (c *WebsiteController) ClearSiteLpg(ctx http.Context) {
|
||||
// ClearLog 清空日志
|
||||
func (c *WebsiteController) ClearLog(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
@@ -394,11 +421,12 @@ func (c *WebsiteController) ClearSiteLpg(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
tools.RemoveFile("/www/wwwlogs/" + website.Name + ".log")
|
||||
tools.Remove("/www/wwwlogs/" + website.Name + ".log")
|
||||
|
||||
Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UpdateRemark 更新备注
|
||||
func (c *WebsiteController) UpdateRemark(ctx http.Context) {
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
@@ -425,8 +453,8 @@ 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 {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站备份列表失败 ", err)
|
||||
@@ -437,6 +465,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 {
|
||||
@@ -462,7 +491,86 @@ 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.Remove(backupPath + "/" + fileName) {
|
||||
Error(ctx, http.StatusInternalServerError, "删除备份失败")
|
||||
return
|
||||
}
|
||||
|
||||
Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ResetConfig 重置配置
|
||||
func (c *WebsiteController) ResetConfig(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
@@ -511,7 +619,7 @@ server
|
||||
|
||||
# waf标记位开始
|
||||
waf on;
|
||||
waf_rule_path /www/server/nginx/ngx_waf/assets/rules/;
|
||||
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
|
||||
waf_mode DYNAMIC;
|
||||
waf_cc_deny rate=1000r/m duration=60m;
|
||||
waf_cache capacity=50;
|
||||
@@ -543,14 +651,18 @@ server
|
||||
|
||||
`, website.Path, website.Php, website.Name, website.Name, website.Name)
|
||||
|
||||
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", raw, 0644)
|
||||
tools.WriteFile("/www/server/vhost/rewrite"+website.Name+".conf", "", 0644)
|
||||
tools.ExecShell("systemctl reload openresty")
|
||||
tools.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644)
|
||||
tools.Write("/www/server/vhost/rewrite"+website.Name+".conf", "", 0644)
|
||||
tools.Exec("systemctl reload openresty")
|
||||
|
||||
Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (c *WebsiteController) SetStatus(ctx http.Context) {
|
||||
// Status 网站状态
|
||||
func (c *WebsiteController) Status(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
@@ -571,34 +683,34 @@ func (c *WebsiteController) SetStatus(ctx http.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
raw := tools.ReadFile("/www/server/vhost/" + website.Name + ".conf")
|
||||
raw := tools.Read("/www/server/vhost/" + website.Name + ".conf")
|
||||
|
||||
// 运行目录
|
||||
rootConfig := tools.Cut(raw, "# root标记位开始", "# root标记位结束")
|
||||
rootConfig := tools.Cut(raw, "# root标记位开始\n", "# root标记位结束")
|
||||
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig)
|
||||
if len(match) == 2 {
|
||||
if website.Status {
|
||||
root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig)
|
||||
raw = strings.ReplaceAll(raw, rootConfig, "root "+root[1]+";")
|
||||
raw = strings.ReplaceAll(raw, rootConfig, " root "+root[1]+";\n ")
|
||||
} else {
|
||||
raw = strings.ReplaceAll(raw, rootConfig, "root /www/server/openresty/html;\n# root "+match[1]+";\n")
|
||||
raw = strings.ReplaceAll(raw, rootConfig, " root /www/server/openresty/html;\n # root "+match[1]+";\n ")
|
||||
}
|
||||
}
|
||||
|
||||
// 默认文件
|
||||
indexConfig := tools.Cut(raw, "# index标记位开始", "# index标记位结束")
|
||||
indexConfig := tools.Cut(raw, "# index标记位开始\n", "# index标记位结束")
|
||||
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig)
|
||||
if len(match) == 2 {
|
||||
if website.Status {
|
||||
index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig)
|
||||
raw = strings.ReplaceAll(raw, indexConfig, "index "+index[1]+";")
|
||||
raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ")
|
||||
} else {
|
||||
raw = strings.ReplaceAll(raw, indexConfig, "index stop.html;\n# index "+match[1]+";\n")
|
||||
raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ")
|
||||
}
|
||||
}
|
||||
|
||||
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", raw, 0644)
|
||||
tools.ExecShell("systemctl reload openresty")
|
||||
tools.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644)
|
||||
tools.Exec("systemctl reload openresty")
|
||||
|
||||
Success(ctx, nil)
|
||||
}
|
||||
|
||||
@@ -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": "登录已过期",
|
||||
})
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
func Static() contractshttp.Middleware {
|
||||
return func(ctx contractshttp.Context) {
|
||||
static.Serve(services.NewSettingImpl().Get("panel_entrance", "/"), static.LocalFile("./public", false))(ctx.(*frameworkhttp.GinContext).Instance())
|
||||
static.Serve(services.NewSettingImpl().Get("entrance", "/"), static.LocalFile("/www/panel/public", false))(ctx.(*frameworkhttp.GinContext).Instance())
|
||||
|
||||
ctx.Request().Next()
|
||||
}
|
||||
|
||||
@@ -4,12 +4,17 @@ import "github.com/goravel/framework/support/carbon"
|
||||
|
||||
const (
|
||||
SettingKeyName = "name"
|
||||
SettingKeyVersion = "version"
|
||||
SettingKeyMonitor = "monitor"
|
||||
SettingKeyMonitorDays = "monitor_days"
|
||||
SettingKeyBackupPath = "backup_path"
|
||||
SettingKeyWebsitePath = "website_path"
|
||||
SettingKeyPanelEntrance = "panel_entrance"
|
||||
SettingKeyEntrance = "entrance"
|
||||
SettingKeyMysqlRootPassword = "mysql_root_password"
|
||||
SettingKeySshHost = "ssh_host"
|
||||
SettingKeySshPort = "ssh_port"
|
||||
SettingKeySshUser = "ssh_user"
|
||||
SettingKeySshPassword = "ssh_password"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package fail2ban
|
||||
|
||||
var (
|
||||
Name = "Fail2ban"
|
||||
Description = "Fail2ban 扫描系统日志文件并从中找出多次尝试失败的IP地址,将该IP地址加入防火墙的拒绝访问列表中。"
|
||||
Slug = "fail2ban"
|
||||
Version = "1.0.0"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/fail2ban/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/fail2ban/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/fail2ban/update.sh`
|
||||
)
|
||||
@@ -2,10 +2,12 @@ package mysql57
|
||||
|
||||
var (
|
||||
Name = "MySQL-5.7"
|
||||
Author = "耗子"
|
||||
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。"
|
||||
Slug = "mysql57"
|
||||
Version = "5.7.42"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"mysql80"}
|
||||
Install = `bash /www/panel/scripts/mysql/install.sh 57`
|
||||
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 57`
|
||||
Update = `echo "not support now"`
|
||||
)
|
||||
|
||||
@@ -2,10 +2,12 @@ package mysql80
|
||||
|
||||
var (
|
||||
Name = "MySQL-8.0"
|
||||
Author = "耗子"
|
||||
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。"
|
||||
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。(内存 < 4G 无法安装)"
|
||||
Slug = "mysql80"
|
||||
Version = "8.0.33"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"mysql57"}
|
||||
Install = `bash /www/panel/scripts/mysql/install.sh 80`
|
||||
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 80`
|
||||
Update = `echo "not support now"`
|
||||
)
|
||||
|
||||
@@ -2,10 +2,12 @@ package openresty
|
||||
|
||||
var (
|
||||
Name = "OpenResty"
|
||||
Author = "耗子"
|
||||
Description = "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台。"
|
||||
Slug = "openresty"
|
||||
Version = "1.21.4.1"
|
||||
Version = "1.21.4.2"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = "bash /www/panel/scripts/openresty/install.sh"
|
||||
Uninstall = "bash /www/panel/scripts/openresty/uninstall.sh"
|
||||
Update = "bash /www/panel/scripts/openresty/install.sh"
|
||||
)
|
||||
|
||||
@@ -2,10 +2,12 @@ package php74
|
||||
|
||||
var (
|
||||
Name = "PHP-7.4"
|
||||
Author = "耗子"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php74"
|
||||
Version = "7.4.33"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/php/install.sh 74`
|
||||
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 74`
|
||||
Update = `bash /www/panel/scripts/php/install.sh 74`
|
||||
)
|
||||
|
||||
@@ -2,10 +2,12 @@ package php80
|
||||
|
||||
var (
|
||||
Name = "PHP-8.0"
|
||||
Author = "耗子"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php80"
|
||||
Version = "8.0.29"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/php/install.sh 80`
|
||||
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 80`
|
||||
Update = `bash /www/panel/scripts/php/install.sh 80`
|
||||
)
|
||||
|
||||
@@ -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`
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package phpmyadmin
|
||||
|
||||
var (
|
||||
Name = "phpMyAdmin"
|
||||
Description = "phpMyAdmin 是一个以 PHP 为基础,以 Web-Base 方式架构在网站主机上的 MySQL 数据库管理工具。"
|
||||
Slug = "phpmyadmin"
|
||||
Version = "5.2.1"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/phpmyadmin/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/phpmyadmin/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/phpmyadmin/uninstall.sh && bash /www/panel/scripts/phpmyadmin/install.sh`
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package postgresql15
|
||||
|
||||
var (
|
||||
Name = "PostgreSQL-15"
|
||||
Description = "PostgreSQL 是开源的对象 - 关系数据库数据库管理系统,在类似 BSD 许可与 MIT 许可的 PostgreSQL 许可下发行。"
|
||||
Slug = "postgresql15"
|
||||
Version = "15.3"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/postgresql/install.sh 15`
|
||||
Uninstall = `bash /www/panel/scripts/postgresql/uninstall.sh 15`
|
||||
Update = `bash /www/panel/scripts/postgresql/update.sh 15`
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package pureftpd
|
||||
|
||||
var (
|
||||
Name = "Pure-FTPd"
|
||||
Description = "Pure-Ftpd 是一个快速、高效、轻便、安全的 FTP 服务器,它以安全和配置简单为设计目标,支持虚拟主机,IPV6,PAM 等功能。"
|
||||
Slug = "pureftpd"
|
||||
Version = "1.0.50"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/pureftpd/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/pureftpd/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/pureftpd/update.sh`
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package redis
|
||||
|
||||
var (
|
||||
Name = "Redis"
|
||||
Description = "Redis 是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。"
|
||||
Slug = "redis"
|
||||
Version = "7.0.12"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/redis/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/redis/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/redis/update.sh`
|
||||
)
|
||||
@@ -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`
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package toolbox
|
||||
|
||||
var (
|
||||
Name = "系统工具箱"
|
||||
Description = "可视化调整一些常用的配置项,如 DNS、SWAP、时区 等"
|
||||
Slug = "toolbox"
|
||||
Version = "1.0"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `panel writePlugin toolbox 1.0`
|
||||
Uninstall = `panel deletePlugin toolbox`
|
||||
Update = `panel writePlugin toolbox 1.0`
|
||||
)
|
||||
+236
-6
@@ -4,6 +4,8 @@ package services
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
@@ -14,6 +16,13 @@ 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
|
||||
PostgresqlList() ([]BackupFile, error)
|
||||
PostgresqlBackup(database string) error
|
||||
PostgresqlRestore(database string, backupFile string) error
|
||||
}
|
||||
|
||||
type BackupFile struct {
|
||||
@@ -31,15 +40,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 +71,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 +83,224 @@ 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.Exec(`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.Exec(`rm -rf '` + website.Path + `/*'`)
|
||||
tools.Exec(`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.Exec("/www/server/mysql/bin/mysqldump -uroot " + database + " > " + backupPath + "/" + backupFile)
|
||||
tools.Exec("cd " + backupPath + " && zip -r " + backupPath + "/" + backupFile + ".zip " + backupFile)
|
||||
tools.Remove(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.Exec("unzip -o " + backupFile + " -d " + backupPath)
|
||||
backupFile = strings.TrimSuffix(backupFile, ext)
|
||||
case ".gz":
|
||||
if strings.HasSuffix(backupFile, ".tar.gz") {
|
||||
// 解压.tar.gz文件
|
||||
tools.Exec("tar -zxvf " + backupFile + " -C " + backupPath)
|
||||
backupFile = strings.TrimSuffix(backupFile, ".tar.gz")
|
||||
} else {
|
||||
// 解压.gz文件
|
||||
tools.Exec("gzip -d " + backupFile)
|
||||
backupFile = strings.TrimSuffix(backupFile, ext)
|
||||
}
|
||||
case ".bz2":
|
||||
tools.Exec("bzip2 -d " + backupFile)
|
||||
backupFile = strings.TrimSuffix(backupFile, ext)
|
||||
case ".tar":
|
||||
tools.Exec("tar -xvf " + backupFile + " -C " + backupPath)
|
||||
backupFile = strings.TrimSuffix(backupFile, ext)
|
||||
case ".rar":
|
||||
tools.Exec("unrar x " + backupFile + " " + backupPath)
|
||||
backupFile = strings.TrimSuffix(backupFile, ext)
|
||||
}
|
||||
|
||||
if !tools.Exists(backupFile) {
|
||||
return errors.New("自动解压失败,请手动解压")
|
||||
}
|
||||
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot " + database + " < " + backupFile)
|
||||
_ = os.Unsetenv("MYSQL_PWD")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostgresqlList PostgreSQL备份列表
|
||||
func (s *BackupImpl) PostgresqlList() ([]BackupFile, error) {
|
||||
backupPath := s.setting.Get(models.SettingKeyBackupPath)
|
||||
if len(backupPath) == 0 {
|
||||
return []BackupFile{}, nil
|
||||
}
|
||||
|
||||
backupPath += "/postgresql"
|
||||
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
|
||||
}
|
||||
|
||||
// PostgresqlBackup PostgreSQL备份
|
||||
func (s *BackupImpl) PostgresqlBackup(database string) error {
|
||||
backupPath := s.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
|
||||
backupFile := database + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
tools.Exec(`su - postgres -c "pg_dump ` + database + `" > ` + backupPath + "/" + backupFile)
|
||||
tools.Exec("cd " + backupPath + " && zip -r " + backupPath + "/" + backupFile + ".zip " + backupFile)
|
||||
tools.Remove(backupPath + "/" + backupFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PostgresqlRestore PostgreSQL恢复
|
||||
func (s *BackupImpl) PostgresqlRestore(database string, backupFile string) error {
|
||||
backupPath := s.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
|
||||
ext := filepath.Ext(backupFile)
|
||||
backupFile = backupPath + "/" + backupFile
|
||||
if !tools.Exists(backupFile) {
|
||||
return errors.New("备份文件不存在")
|
||||
}
|
||||
|
||||
switch ext {
|
||||
case ".zip":
|
||||
tools.Exec("unzip -o " + backupFile + " -d " + backupPath)
|
||||
backupFile = strings.TrimSuffix(backupFile, ext)
|
||||
case ".gz":
|
||||
if strings.HasSuffix(backupFile, ".tar.gz") {
|
||||
// 解压.tar.gz文件
|
||||
tools.Exec("tar -zxvf " + backupFile + " -C " + backupPath)
|
||||
backupFile = strings.TrimSuffix(backupFile, ".tar.gz")
|
||||
} else {
|
||||
// 解压.gz文件
|
||||
tools.Exec("gzip -d " + backupFile)
|
||||
backupFile = strings.TrimSuffix(backupFile, ext)
|
||||
}
|
||||
case ".bz2":
|
||||
tools.Exec("bzip2 -d " + backupFile)
|
||||
backupFile = strings.TrimSuffix(backupFile, ext)
|
||||
case ".tar":
|
||||
tools.Exec("tar -xvf " + backupFile + " -C " + backupPath)
|
||||
backupFile = strings.TrimSuffix(backupFile, ext)
|
||||
case ".rar":
|
||||
tools.Exec("unrar x " + backupFile + " " + backupPath)
|
||||
backupFile = strings.TrimSuffix(backupFile, ext)
|
||||
}
|
||||
|
||||
if !tools.Exists(backupFile) {
|
||||
return errors.New("自动解压失败,请手动解压")
|
||||
}
|
||||
|
||||
tools.Exec(`su - postgres -c "psql ` + database + `" < ` + backupFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
+12
-8
@@ -1,6 +1,8 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
@@ -20,21 +22,23 @@ func NewCronImpl() *CronImpl {
|
||||
// AddToSystem 添加到系统
|
||||
func (r *CronImpl) AddToSystem(cron models.Cron) {
|
||||
if tools.IsRHEL() {
|
||||
tools.ExecShell("echo \"" + cron.Time + " " + cron.Shell + " >> " + cron.Log + " 2>&1\" >> /var/spool/cron/root")
|
||||
tools.Exec("echo \"" + cron.Time + " " + cron.Shell + " >> " + cron.Log + " 2>&1\" >> /var/spool/cron/root")
|
||||
tools.Exec("systemctl restart crond")
|
||||
} else {
|
||||
tools.ExecShell("echo \"" + cron.Time + " " + cron.Shell + " >> " + cron.Log + " 2>&1\" >> /var/spool/cron/crontabs/root")
|
||||
tools.Exec("echo \"" + cron.Time + " " + cron.Shell + " >> " + cron.Log + " 2>&1\" >> /var/spool/cron/crontabs/root")
|
||||
tools.Exec("systemctl restart cron")
|
||||
}
|
||||
|
||||
tools.ExecShell("systemctl restart crond")
|
||||
}
|
||||
|
||||
// DeleteFromSystem 从系统中删除
|
||||
func (r *CronImpl) DeleteFromSystem(cron models.Cron) {
|
||||
// 需要转义Shell路径的/为\/
|
||||
cron.Shell = strings.ReplaceAll(cron.Shell, "/", "\\/")
|
||||
if tools.IsRHEL() {
|
||||
tools.ExecShell("sed -i '/" + cron.Shell + "/d' /var/spool/cron/root")
|
||||
tools.Exec("sed -i '/" + cron.Shell + "/d' /var/spool/cron/root")
|
||||
tools.Exec("systemctl restart crond")
|
||||
} else {
|
||||
tools.ExecShell("sed -i '/" + cron.Shell + "/d' /var/spool/cron/crontabs/root")
|
||||
tools.Exec("sed -i '/" + cron.Shell + "/d' /var/spool/cron/crontabs/root")
|
||||
tools.Exec("systemctl restart cron")
|
||||
}
|
||||
|
||||
tools.ExecShell("systemctl restart crond")
|
||||
}
|
||||
|
||||
+158
-4
@@ -5,25 +5,41 @@ import (
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/plugins/fail2ban"
|
||||
"panel/app/plugins/mysql57"
|
||||
"panel/app/plugins/mysql80"
|
||||
"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/postgresql15"
|
||||
"panel/app/plugins/pureftpd"
|
||||
"panel/app/plugins/redis"
|
||||
"panel/app/plugins/s3fs"
|
||||
"panel/app/plugins/supervisor"
|
||||
"panel/app/plugins/toolbox"
|
||||
)
|
||||
|
||||
// PanelPlugin 插件元数据结构
|
||||
type PanelPlugin struct {
|
||||
Name string
|
||||
Author string
|
||||
Description string
|
||||
Slug string
|
||||
Version string
|
||||
Requires []string
|
||||
Excludes []string
|
||||
Install string
|
||||
Uninstall string
|
||||
Update string
|
||||
}
|
||||
|
||||
type Plugin interface {
|
||||
AllInstalled() ([]models.Plugin, error)
|
||||
All() []PanelPlugin
|
||||
GetBySlug(slug string) PanelPlugin
|
||||
GetInstalledBySlug(slug string) models.Plugin
|
||||
}
|
||||
|
||||
type PluginImpl struct {
|
||||
@@ -49,30 +65,168 @@ func (r *PluginImpl) All() []PanelPlugin {
|
||||
|
||||
p = append(p, PanelPlugin{
|
||||
Name: openresty.Name,
|
||||
Author: openresty.Author,
|
||||
Description: openresty.Description,
|
||||
Slug: openresty.Slug,
|
||||
Version: openresty.Version,
|
||||
Requires: openresty.Requires,
|
||||
Excludes: openresty.Excludes,
|
||||
Install: openresty.Install,
|
||||
Uninstall: openresty.Uninstall,
|
||||
Update: openresty.Update,
|
||||
})
|
||||
p = append(p, PanelPlugin{
|
||||
Name: mysql57.Name,
|
||||
Description: mysql57.Description,
|
||||
Slug: mysql57.Slug,
|
||||
Version: mysql57.Version,
|
||||
Requires: mysql57.Requires,
|
||||
Excludes: mysql57.Excludes,
|
||||
Install: mysql57.Install,
|
||||
Uninstall: mysql57.Uninstall,
|
||||
Update: mysql57.Update,
|
||||
})
|
||||
p = append(p, PanelPlugin{
|
||||
Name: mysql80.Name,
|
||||
Author: mysql80.Author,
|
||||
Description: mysql80.Description,
|
||||
Slug: mysql80.Slug,
|
||||
Version: mysql80.Version,
|
||||
Requires: mysql80.Requires,
|
||||
Excludes: mysql80.Excludes,
|
||||
Install: mysql80.Install,
|
||||
Uninstall: mysql80.Uninstall,
|
||||
Update: mysql80.Update,
|
||||
})
|
||||
p = append(p, PanelPlugin{
|
||||
Name: postgresql15.Name,
|
||||
Description: postgresql15.Description,
|
||||
Slug: postgresql15.Slug,
|
||||
Version: postgresql15.Version,
|
||||
Requires: postgresql15.Requires,
|
||||
Excludes: postgresql15.Excludes,
|
||||
Install: postgresql15.Install,
|
||||
Uninstall: postgresql15.Uninstall,
|
||||
Update: postgresql15.Update,
|
||||
})
|
||||
p = append(p, PanelPlugin{
|
||||
Name: php74.Name,
|
||||
Author: php74.Author,
|
||||
Description: php74.Description,
|
||||
Slug: php74.Slug,
|
||||
Version: php74.Version,
|
||||
Requires: php74.Requires,
|
||||
Excludes: php74.Excludes,
|
||||
Install: php74.Install,
|
||||
Uninstall: php74.Uninstall,
|
||||
Update: php74.Update,
|
||||
})
|
||||
p = append(p, PanelPlugin{
|
||||
Name: php80.Name,
|
||||
Description: php80.Description,
|
||||
Slug: php80.Slug,
|
||||
Version: php80.Version,
|
||||
Requires: php80.Requires,
|
||||
Excludes: php80.Excludes,
|
||||
Install: php80.Install,
|
||||
Uninstall: php80.Uninstall,
|
||||
Update: php80.Update,
|
||||
})
|
||||
p = append(p, PanelPlugin{
|
||||
Name: 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,
|
||||
Description: phpmyadmin.Description,
|
||||
Slug: phpmyadmin.Slug,
|
||||
Version: phpmyadmin.Version,
|
||||
Requires: phpmyadmin.Requires,
|
||||
Excludes: phpmyadmin.Excludes,
|
||||
Install: phpmyadmin.Install,
|
||||
Uninstall: phpmyadmin.Uninstall,
|
||||
Update: phpmyadmin.Update,
|
||||
})
|
||||
p = append(p, PanelPlugin{
|
||||
Name: pureftpd.Name,
|
||||
Description: pureftpd.Description,
|
||||
Slug: pureftpd.Slug,
|
||||
Version: pureftpd.Version,
|
||||
Requires: pureftpd.Requires,
|
||||
Excludes: pureftpd.Excludes,
|
||||
Install: pureftpd.Install,
|
||||
Uninstall: pureftpd.Uninstall,
|
||||
Update: pureftpd.Update,
|
||||
})
|
||||
p = append(p, PanelPlugin{
|
||||
Name: redis.Name,
|
||||
Description: redis.Description,
|
||||
Slug: redis.Slug,
|
||||
Version: redis.Version,
|
||||
Requires: redis.Requires,
|
||||
Excludes: redis.Excludes,
|
||||
Install: redis.Install,
|
||||
Uninstall: redis.Uninstall,
|
||||
Update: redis.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,
|
||||
})
|
||||
p = append(p, PanelPlugin{
|
||||
Name: fail2ban.Name,
|
||||
Description: fail2ban.Description,
|
||||
Slug: fail2ban.Slug,
|
||||
Version: fail2ban.Version,
|
||||
Requires: fail2ban.Requires,
|
||||
Excludes: fail2ban.Excludes,
|
||||
Install: fail2ban.Install,
|
||||
Uninstall: fail2ban.Uninstall,
|
||||
Update: fail2ban.Update,
|
||||
})
|
||||
p = append(p, PanelPlugin{
|
||||
Name: toolbox.Name,
|
||||
Description: toolbox.Description,
|
||||
Slug: toolbox.Slug,
|
||||
Version: toolbox.Version,
|
||||
Requires: toolbox.Requires,
|
||||
Excludes: toolbox.Excludes,
|
||||
Install: toolbox.Install,
|
||||
Uninstall: toolbox.Uninstall,
|
||||
Update: toolbox.Update,
|
||||
})
|
||||
|
||||
return p
|
||||
|
||||
+59
-39
@@ -2,6 +2,8 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
@@ -49,6 +51,11 @@ type WebsiteSetting struct {
|
||||
Ssl bool `json:"ssl"`
|
||||
SslCertificate string `json:"ssl_certificate"`
|
||||
SslCertificateKey string `json:"ssl_certificate_key"`
|
||||
SslNotBefore string `json:"ssl_not_before"`
|
||||
SslNotAfter string `json:"ssl_not_after"`
|
||||
SSlDNSNames []string `json:"ssl_dns_names"`
|
||||
SslIssuer string `json:"ssl_issuer"`
|
||||
SslOCSPServer []string `json:"ssl_ocsp_server"`
|
||||
HttpRedirect bool `json:"http_redirect"`
|
||||
Hsts bool `json:"hsts"`
|
||||
Waf bool `json:"waf"`
|
||||
@@ -61,10 +68,13 @@ type WebsiteSetting struct {
|
||||
}
|
||||
|
||||
type WebsiteImpl struct {
|
||||
setting Setting
|
||||
}
|
||||
|
||||
func NewWebsiteImpl() *WebsiteImpl {
|
||||
return &WebsiteImpl{}
|
||||
return &WebsiteImpl{
|
||||
setting: NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 列出网站
|
||||
@@ -88,7 +98,7 @@ func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) {
|
||||
|
||||
// path为空时,设置默认值
|
||||
if len(website.Path) == 0 {
|
||||
website.Path = "/www/wwwroot/" + website.Name
|
||||
website.Path = r.setting.Get(models.SettingKeyWebsitePath) + "/" + website.Name
|
||||
}
|
||||
// path不为/开头时,返回错误
|
||||
if website.Path[0] != '/' {
|
||||
@@ -113,8 +123,7 @@ func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) {
|
||||
|
||||
tools.Mkdir(website.Path, 0755)
|
||||
|
||||
index := `
|
||||
<!DOCTYPE html>
|
||||
index := `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
@@ -126,9 +135,8 @@ func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) {
|
||||
<p>当您看到此页面,说明您的网站已创建成功。</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
`
|
||||
tools.WriteFile(website.Path+"/index.html", index, 0644)
|
||||
tools.Write(website.Path+"/index.html", index, 0644)
|
||||
|
||||
domainArr := strings.Split(website.Domain, "\n")
|
||||
portList := ""
|
||||
@@ -159,8 +167,7 @@ func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) {
|
||||
}
|
||||
}
|
||||
|
||||
nginxConf := fmt.Sprintf(`
|
||||
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
|
||||
nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
|
||||
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
|
||||
server
|
||||
{
|
||||
@@ -186,7 +193,7 @@ server
|
||||
|
||||
# waf标记位开始
|
||||
waf on;
|
||||
waf_rule_path /www/server/nginx/ngx_waf/assets/rules/;
|
||||
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
|
||||
waf_mode DYNAMIC;
|
||||
waf_cc_deny rate=1000r/m duration=60m;
|
||||
waf_cache capacity=50;
|
||||
@@ -215,17 +222,27 @@ server
|
||||
access_log /www/wwwlogs/%s.log;
|
||||
error_log /www/wwwlogs/%s.log;
|
||||
}
|
||||
|
||||
`, portList, domainList, website.Path, website.Php, website.Name, website.Name, website.Name)
|
||||
|
||||
tools.WriteFile("/www/server/panel/vhost/openresty/"+website.Name+".conf", nginxConf, 0644)
|
||||
tools.WriteFile("/www/server/panel/vhost/openresty/rewrite/"+website.Name+".conf", "", 0644)
|
||||
tools.WriteFile("/www/server/panel/vhost/openresty/ssl/"+website.Name+".pem", "", 0644)
|
||||
tools.WriteFile("/www/server/panel/vhost/openresty/ssl/"+website.Name+".key", "", 0644)
|
||||
tools.Write("/www/server/vhost/"+website.Name+".conf", nginxConf, 0644)
|
||||
tools.Write("/www/server/vhost/rewrite/"+website.Name+".conf", "", 0644)
|
||||
tools.Write("/www/server/vhost/ssl/"+website.Name+".pem", "", 0644)
|
||||
tools.Write("/www/server/vhost/ssl/"+website.Name+".key", "", 0644)
|
||||
|
||||
tools.ExecShellAsync("systemctl reload openresty")
|
||||
tools.Chmod(r.setting.Get(models.SettingKeyWebsitePath), 0755)
|
||||
tools.Chmod(website.Path, 0755)
|
||||
tools.Chown(r.setting.Get(models.SettingKeyWebsitePath), "www", "www")
|
||||
tools.Chown(website.Path, "www", "www")
|
||||
|
||||
// TODO 创建数据库
|
||||
tools.Exec("systemctl reload openresty")
|
||||
|
||||
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if website.Db && website.DbType == "mysql" {
|
||||
tools.Exec(`/www/server/mysql/bin/mysql -uroot -p` + rootPassword + ` -e "CREATE DATABASE IF NOT EXISTS ` + website.DbName + ` DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;"`)
|
||||
tools.Exec(`/www/server/mysql/bin/mysql -uroot -p` + rootPassword + ` -e "CREATE USER '` + website.DbUser + `'@'localhost' IDENTIFIED BY '` + website.DbPassword + `';"`)
|
||||
tools.Exec(`/www/server/mysql/bin/mysql -uroot -p` + rootPassword + ` -e "GRANT ALL PRIVILEGES ON ` + website.DbName + `.* TO '` + website.DbUser + `'@'localhost';"`)
|
||||
tools.Exec(`/www/server/mysql/bin/mysql -uroot -p` + rootPassword + ` -e "FLUSH PRIVILEGES;"`)
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
@@ -233,7 +250,7 @@ server
|
||||
// Delete 删除网站
|
||||
func (r *WebsiteImpl) Delete(id int) error {
|
||||
var website models.Website
|
||||
if err := facades.Orm().Query().Where("id", id).First(&website); err != nil {
|
||||
if err := facades.Orm().Query().Where("id", id).FirstOrFail(&website); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -241,15 +258,13 @@ func (r *WebsiteImpl) Delete(id int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
tools.RemoveFile("/www/server/panel/vhost/openresty/" + website.Name + ".conf")
|
||||
tools.RemoveFile("/www/server/panel/vhost/openresty/rewrite/" + website.Name + ".conf")
|
||||
tools.RemoveFile("/www/server/panel/vhost/openresty/ssl/" + website.Name + ".pem")
|
||||
tools.RemoveFile("/www/server/panel/vhost/openresty/ssl/" + website.Name + ".key")
|
||||
tools.RemoveFile(website.Path)
|
||||
tools.Remove("/www/server/vhost/" + website.Name + ".conf")
|
||||
tools.Remove("/www/server/vhost/rewrite/" + website.Name + ".conf")
|
||||
tools.Remove("/www/server/vhost/ssl/" + website.Name + ".pem")
|
||||
tools.Remove("/www/server/vhost/ssl/" + website.Name + ".key")
|
||||
tools.Remove(website.Path)
|
||||
|
||||
tools.ExecShellAsync("systemctl reload openresty")
|
||||
|
||||
// TODO 删除数据库
|
||||
tools.Exec("systemctl reload openresty")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -261,7 +276,7 @@ func (r *WebsiteImpl) GetConfig(id int) (WebsiteSetting, error) {
|
||||
return WebsiteSetting{}, err
|
||||
}
|
||||
|
||||
config := tools.ReadFile("/www/server/panel/vhost/openresty/" + website.Name + ".conf")
|
||||
config := tools.Read("/www/server/vhost/" + website.Name + ".conf")
|
||||
|
||||
var setting WebsiteSetting
|
||||
setting.Name = website.Name
|
||||
@@ -295,7 +310,7 @@ func (r *WebsiteImpl) GetConfig(id int) (WebsiteSetting, error) {
|
||||
}
|
||||
|
||||
if tools.Exists(setting.Root + "/.user.ini") {
|
||||
userIni := tools.ReadFile(setting.Path + "/.user.ini")
|
||||
userIni := tools.Read(setting.Path + "/.user.ini")
|
||||
if strings.Contains(userIni, "open_basedir") {
|
||||
setting.OpenBasedir = true
|
||||
} else {
|
||||
@@ -305,21 +320,26 @@ func (r *WebsiteImpl) GetConfig(id int) (WebsiteSetting, error) {
|
||||
setting.OpenBasedir = false
|
||||
}
|
||||
|
||||
setting.SslCertificate = tools.Read("/www/server/vhost/ssl/" + website.Name + ".pem")
|
||||
setting.SslCertificateKey = tools.Read("/www/server/vhost/ssl/" + website.Name + ".key")
|
||||
if setting.Ssl {
|
||||
ssl := tools.Cut(config, "# ssl标记位开始", "# ssl标记位结束")
|
||||
match = regexp.MustCompile(`ssl_certificate\s+(.*);`).FindStringSubmatch(ssl)
|
||||
if len(match) > 1 {
|
||||
setting.SslCertificate = tools.ReadFile(match[1])
|
||||
}
|
||||
match = regexp.MustCompile(`ssl_certificate_key\s+(.*);`).FindStringSubmatch(ssl)
|
||||
if len(match) > 1 {
|
||||
setting.SslCertificateKey = tools.ReadFile(match[1])
|
||||
}
|
||||
setting.HttpRedirect = strings.Contains(ssl, "# http重定向标记位")
|
||||
setting.Hsts = strings.Contains(ssl, "# hsts标记位")
|
||||
|
||||
certData := tools.Read("/www/server/vhost/ssl/" + website.Name + ".pem")
|
||||
block, _ := pem.Decode([]byte(certData))
|
||||
if block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
setting.SslNotBefore = cert.NotBefore.Format("2006-01-02 15:04:05")
|
||||
setting.SslNotAfter = cert.NotAfter.Format("2006-01-02 15:04:05")
|
||||
setting.SslIssuer = cert.Issuer.CommonName
|
||||
setting.SslOCSPServer = cert.OCSPServer
|
||||
setting.SSlDNSNames = cert.DNSNames
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setting.SslCertificate = ""
|
||||
setting.SslCertificateKey = ""
|
||||
setting.HttpRedirect = false
|
||||
setting.Hsts = false
|
||||
}
|
||||
@@ -339,8 +359,8 @@ func (r *WebsiteImpl) GetConfig(id int) (WebsiteSetting, error) {
|
||||
setting.WafCache = match[1]
|
||||
}
|
||||
|
||||
setting.Rewrite = tools.ReadFile("/www/server/panel/vhost/openresty/rewrite/" + website.Name + ".conf")
|
||||
setting.Log = tools.ExecShell("tail -n 100 /www/wwwlogs/" + website.Name + ".log")
|
||||
setting.Rewrite = tools.Read("/www/server/vhost/rewrite/" + website.Name + ".conf")
|
||||
setting.Log = tools.Escape(tools.Exec(`tail -n 100 '/www/wwwlogs/` + website.Name + `.log'`))
|
||||
|
||||
return setting, nil
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,9 +9,9 @@ import (
|
||||
func Boot() {
|
||||
app := foundation.NewApplication()
|
||||
|
||||
//Bootstrap the application
|
||||
// Bootstrap the application
|
||||
app.Boot()
|
||||
|
||||
//Bootstrap the config.
|
||||
// Bootstrap the config.
|
||||
config.Boot()
|
||||
}
|
||||
|
||||
+3
-1
@@ -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"
|
||||
@@ -50,7 +51,7 @@ func init() {
|
||||
// Here you may specify the default timezone for your application.
|
||||
// Example: UTC, Asia/Shanghai
|
||||
// More: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
"timezone": carbon.UTC,
|
||||
"timezone": carbon.PRC,
|
||||
|
||||
// Encryption Key
|
||||
//
|
||||
@@ -77,6 +78,7 @@ func init() {
|
||||
&auth.ServiceProvider{},
|
||||
&hash.ServiceProvider{},
|
||||
&crypt.ServiceProvider{},
|
||||
&filesystem.ServiceProvider{},
|
||||
&validation.ServiceProvider{},
|
||||
&providers.AppServiceProvider{},
|
||||
&providers.AuthServiceProvider{},
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import (
|
||||
func init() {
|
||||
config := facades.Config()
|
||||
config.Add("panel", map[string]any{
|
||||
"name": "耗子面板",
|
||||
"version": "2.0.0",
|
||||
"name": "耗子Linux面板",
|
||||
"version": "v2.0.39",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,17 +3,20 @@ 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.3
|
||||
github.com/goravel/framework v1.12.1-0.20230717105343-6ce864794883
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/imroc/req/v3 v3.37.2
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/goravel/framework v1.12.1-0.20230809103226-03eb8ad21087
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/imroc/req/v3 v3.40.1
|
||||
github.com/mojocn/base64Captcha v1.3.5
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/spf13/cast v1.5.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -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
|
||||
@@ -49,7 +51,7 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gaukas/godicttls v0.0.3 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
@@ -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
|
||||
@@ -77,16 +80,16 @@ require (
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
|
||||
github.com/google/pprof v0.0.0-20230808223545-4887780b67fb // indirect
|
||||
github.com/google/s2a-go v0.1.3 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
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.1 // 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
|
||||
@@ -103,7 +106,7 @@ require (
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kelseyhightower/envconfig v1.4.0 // indirect
|
||||
github.com/klauspost/compress v1.16.6 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/lestrrat-go/strftime v1.0.5 // indirect
|
||||
@@ -126,10 +129,9 @@ require (
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
|
||||
github.com/quic-go/quic-go v0.36.0 // indirect
|
||||
github.com/refraction-networking/utls v1.3.2 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.3.2 // indirect
|
||||
github.com/quic-go/quic-go v0.37.4 // indirect
|
||||
github.com/refraction-networking/utls v1.3.3 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
@@ -160,22 +162,21 @@ require (
|
||||
go.mongodb.org/mongo-driver v1.7.5 // indirect
|
||||
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
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/image v0.11.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/oauth2 v0.7.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/tools v0.10.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
golang.org/x/tools v0.12.0 // indirect
|
||||
google.golang.org/api v0.122.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230525234025-438c736192d0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a // indirect
|
||||
google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/grpc v1.56.2 // indirect
|
||||
google.golang.org/grpc v1.57.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
@@ -184,7 +185,7 @@ require (
|
||||
gorm.io/driver/postgres v1.5.2 // indirect
|
||||
gorm.io/driver/sqlserver v1.5.1 // indirect
|
||||
gorm.io/gorm v1.25.2 // indirect
|
||||
gorm.io/plugin/dbresolver v1.4.1 // indirect
|
||||
gorm.io/plugin/dbresolver v1.4.2 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
|
||||
@@ -85,6 +85,7 @@ github.com/aws/aws-sdk-go v1.37.16 h1:Q4YOP2s00NpB9wfmTDZArdcLRuG9ijbnoAwTW3ivle
|
||||
github.com/aws/aws-sdk-go v1.37.16/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/brianvoe/gofakeit/v6 v6.23.1 h1:k2gX0hQpJStvixDbbw8oJOvPBg0XmHJWbSOF5JkiUHw=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM=
|
||||
github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
@@ -155,8 +156,8 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
|
||||
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA=
|
||||
github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
@@ -169,8 +170,6 @@ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.8.0 h1:02X12E2I/4C1n+v90yTqrjRa8yuo7c3KeHI3FRznCvc=
|
||||
github.com/glebarez/sqlite v1.8.0/go.mod h1:bpET16h1za2KOOMb8+jCp6UBP/iahDpfPQqSaYLTLx8=
|
||||
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
|
||||
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
@@ -245,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=
|
||||
@@ -323,8 +324,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
|
||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
|
||||
github.com/google/pprof v0.0.0-20230808223545-4887780b67fb h1:oqpb3Cwpc7EOml5PVGMYbSGmwNui2R7i8IW83gs4W0c=
|
||||
github.com/google/pprof v0.0.0-20230808223545-4887780b67fb/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.3 h1:FAgZmpLl/SXurPEZyCMPBIiiYeTbqfjlbdnCNTAkbGE=
|
||||
github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
|
||||
@@ -344,26 +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.3 h1:twfIhZs4QLCtimkP7MOxlF3A0U/5cDPseRT9M/+2SCE=
|
||||
github.com/gookit/color v1.5.3/go.mod h1:NUzwzeehUfl7GIb36pqId+UGmRfQcU/WiiyTTeNjHtE=
|
||||
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/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
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.1 h1:rPp64QZQJM+fysGFAhKpvekQAav4Ok6sjfTs9ZtxcpA=
|
||||
github.com/gookit/validate v1.5.1/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.20230710102458-737cce0f687c h1:KPggCIxAZghopvPXC0g0jCmFNbC+r5EawjHCcbfBOhQ=
|
||||
github.com/goravel/framework v1.12.1-0.20230710102458-737cce0f687c/go.mod h1:SBsBTY8KTqSDipGPEZXnRRC1mWzKmL55Eo1P5aHNDgY=
|
||||
github.com/goravel/framework v1.12.1-0.20230717105343-6ce864794883 h1:8+87CCYqt5O6GqAYmGWz/W6SXGKp6xyn+McMbtWgyug=
|
||||
github.com/goravel/framework v1.12.1-0.20230717105343-6ce864794883/go.mod h1:quBqUHAyZutEs/TFfm9b/caI+BtyF82njGhNIL1QJM0=
|
||||
github.com/goravel/framework v1.12.1-0.20230809103226-03eb8ad21087 h1:YOKs5GFzbWOZz3ojiX5jl082Y1BRGOCig3ldnNBTrRs=
|
||||
github.com/goravel/framework v1.12.1-0.20230809103226-03eb8ad21087/go.mod h1:qc8dK2vVVVdTSashuby7+KZZ2CfGztNgMaoDGWtvtVI=
|
||||
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=
|
||||
@@ -380,14 +379,14 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I=
|
||||
github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
|
||||
github.com/imroc/req/v3 v3.40.1 h1:/7YAmkrI4HsOTO6Md5FUUWPId90bTD074BV8sA0bt18=
|
||||
github.com/imroc/req/v3 v3.40.1/go.mod h1:4wMbz0QYY5jmXNWk0BsWrRTR9ItZqOxzSJdGL0M9kzY=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
@@ -429,8 +428,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
|
||||
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
@@ -459,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=
|
||||
@@ -527,14 +525,12 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
|
||||
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
||||
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
|
||||
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
|
||||
github.com/quic-go/quic-go v0.36.0 h1:JIrO7p7Ug6hssFcARjWDiqS2RAKJHCiwPxBAA989rbI=
|
||||
github.com/quic-go/quic-go v0.36.0/go.mod h1:zPetvwDlILVxt15n3hr3Gf/I3mDf7LpLKPhR4Ez0AZQ=
|
||||
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
|
||||
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.2 h1:rRgN3WfnKbyik4dBV8A6girlJVxGand/d+jVKbQq5GI=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.2/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.37.4 h1:ke8B73yMCWGq9MfrCCAw0Uzdm7GaViC3i39dsIdDlH4=
|
||||
github.com/quic-go/quic-go v0.37.4/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU=
|
||||
github.com/refraction-networking/utls v1.3.3 h1:f/TBLX7KBciRyFH3bwupp+CE4fzoYKCirhdRcC490sw=
|
||||
github.com/refraction-networking/utls v1.3.3/go.mod h1:DlecWW1LMlMJu+9qpzzQqdHDT/C2LAe03EdpLUz/RL8=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
@@ -635,7 +631,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=
|
||||
@@ -674,8 +669,8 @@ go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
|
||||
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
@@ -690,12 +685,11 @@ 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=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -706,12 +700,13 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 h1:EDuYyU/MkFXllv9QF9819VlI9a4tzGuCbhG0ExK9o1U=
|
||||
golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
|
||||
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -736,8 +731,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -781,8 +776,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -875,22 +870,19 @@ 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=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.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=
|
||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -904,8 +896,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -969,8 +961,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
|
||||
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
||||
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
||||
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1044,10 +1036,10 @@ google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210207032614-bba0dbe2a9ea/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20230525234025-438c736192d0 h1:x1vNwUhVOcsYoKyEGCZBH694SBmmBjA2EfauFVEI2+M=
|
||||
google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a h1:HiYVD+FGJkTo+9zj1gqz0anapsa1JxjiSrN+BJKyUmE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8=
|
||||
google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M=
|
||||
google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 h1:m8v1xLLLzMe1m5P+gCTF8nJB9epwZQUBERm20Oy1poQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@@ -1069,8 +1061,8 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
|
||||
google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
||||
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -1119,12 +1111,11 @@ gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBp
|
||||
gorm.io/driver/sqlserver v1.5.1 h1:wpyW/pR26U94uaujltiFGXY7fd2Jw5hC9PB1ZF/Y5s4=
|
||||
gorm.io/driver/sqlserver v1.5.1/go.mod h1:AYHzzte2msKTmYBYsSIq8ZUsznLJwBdkB2wpI+kt0nM=
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.24.3/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
|
||||
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/plugin/dbresolver v1.4.1 h1:Ug4LcoPhrvqq71UhxtF346f+skTYoCa/nEsdjvHwEzk=
|
||||
gorm.io/plugin/dbresolver v1.4.1/go.mod h1:CTbCtMWhsjXSiJqiW2R8POvJ2cq18RVOl4WGyT5nhNc=
|
||||
gorm.io/plugin/dbresolver v1.4.2 h1:IeLSH20ayxbo4rN6HMIQ0ccdsh/fkLK23pp6ivZrqBI=
|
||||
gorm.io/plugin/dbresolver v1.4.2/go.mod h1:l4Cn87EHLEYuqUncpEeTC2tTJQkjngPSD+lo8hIvcT0=
|
||||
gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
@@ -32,5 +32,8 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
// 启动计划任务
|
||||
go facades.Schedule().Run()
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
APP_ENV=production
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_DEBUG=false
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8888
|
||||
|
||||
|
||||
@@ -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.Read(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
|
||||
}
|
||||
+6
-26
@@ -5,36 +5,11 @@ import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"text/template"
|
||||
"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 {
|
||||
@@ -104,3 +79,8 @@ func Cut(str, begin, end string) string {
|
||||
|
||||
return string([]rune(str)[b:e])
|
||||
}
|
||||
|
||||
// Escape 转义字符串
|
||||
func Escape(str string) string {
|
||||
return template.HTMLEscapeString(str)
|
||||
}
|
||||
|
||||
@@ -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"}))
|
||||
}
|
||||
|
||||
+79
-14
@@ -4,12 +4,13 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/facades"
|
||||
)
|
||||
|
||||
// WriteFile 写入文件
|
||||
func WriteFile(path string, data string, permission os.FileMode) bool {
|
||||
// Write 写入文件
|
||||
func Write(path string, data string, permission os.FileMode) bool {
|
||||
if err := os.MkdirAll(filepath.Dir(path), permission); err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 创建目录失败: %s", err.Error())
|
||||
return false
|
||||
@@ -24,8 +25,8 @@ func WriteFile(path string, data string, permission os.FileMode) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ReadFile 读取文件
|
||||
func ReadFile(path string) string {
|
||||
// Read 读取文件
|
||||
func Read(path string) string {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 读取文件 %s 失败: %s", path, err.Error())
|
||||
@@ -35,8 +36,8 @@ func ReadFile(path string) string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// RemoveFile 删除文件
|
||||
func RemoveFile(path string) bool {
|
||||
// Remove 删除文件
|
||||
func Remove(path string) bool {
|
||||
if err := os.Remove(path); err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 删除文件 %s 失败: %s", path, err.Error())
|
||||
return false
|
||||
@@ -45,32 +46,32 @@ func RemoveFile(path string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ExecShell 执行 shell 命令
|
||||
func ExecShell(shell string) string {
|
||||
// Exec 执行 shell 命令
|
||||
func Exec(shell string) string {
|
||||
cmd := exec.Command("bash", "-c", shell)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error())
|
||||
facades.Log().Errorf("[面板][Helpers] 执行命令 %s 失败: %s", shell, err.Error())
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(output)
|
||||
return strings.TrimSpace(string(output))
|
||||
}
|
||||
|
||||
// ExecShellAsync 异步执行 shell 命令
|
||||
func ExecShellAsync(shell string) {
|
||||
// ExecAsync 异步执行 shell 命令
|
||||
func ExecAsync(shell string) {
|
||||
cmd := exec.Command("bash", "-c", shell)
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error())
|
||||
facades.Log().Errorf("[面板][Helpers] 执行命令 %s 失败: %s", shell, err.Error())
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
if err != nil {
|
||||
facades.Log().Errorf("[面板][Helpers] 执行命令 $s 失败: %s", shell, err.Error())
|
||||
facades.Log().Errorf("[面板][Helpers] 执行命令 %s 失败: %s", shell, err.Error())
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -113,3 +114,67 @@ 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, error) {
|
||||
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, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Cp 复制路径
|
||||
func Cp(src, dst string) (bool, error) {
|
||||
cmd := exec.Command("cp", "-r", src, dst)
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Size 获取路径大小
|
||||
func Size(path string) (int64, error) {
|
||||
var size int64
|
||||
|
||||
err := filepath.Walk(path, func(filePath string, info os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
size += info.Size()
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return size, nil
|
||||
}
|
||||
|
||||
// FileSize 获取文件大小
|
||||
func FileSize(path string) (int64, error) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return info.Size(), nil
|
||||
}
|
||||
|
||||
+49
-10
@@ -17,45 +17,45 @@ func TestSystemHelperTestSuite(t *testing.T) {
|
||||
suite.Run(t, &SystemHelperTestSuite{})
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestWriteFile() {
|
||||
func (s *SystemHelperTestSuite) TestWrite() {
|
||||
filePath := "/tmp/testfile"
|
||||
defer os.Remove(filePath)
|
||||
|
||||
s.True(WriteFile(filePath, "test data", 0644))
|
||||
s.True(Write(filePath, "test data", 0644))
|
||||
s.FileExists(filePath)
|
||||
|
||||
content, _ := os.ReadFile(filePath)
|
||||
s.Equal("test data", string(content))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestReadFile() {
|
||||
func (s *SystemHelperTestSuite) TestRead() {
|
||||
filePath := "/tmp/testfile"
|
||||
defer os.Remove(filePath)
|
||||
|
||||
err := os.WriteFile(filePath, []byte("test data"), 0644)
|
||||
s.Nil(err)
|
||||
|
||||
s.Equal("test data", ReadFile(filePath))
|
||||
s.Equal("test data", Read(filePath))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestRemoveFile() {
|
||||
func (s *SystemHelperTestSuite) TestRemove() {
|
||||
filePath := "/tmp/testfile"
|
||||
|
||||
err := os.WriteFile(filePath, []byte("test data"), 0644)
|
||||
s.Nil(err)
|
||||
|
||||
s.True(RemoveFile(filePath))
|
||||
s.True(Remove(filePath))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestExecShell() {
|
||||
s.Equal("test\n", ExecShell("echo 'test'"))
|
||||
func (s *SystemHelperTestSuite) TestExec() {
|
||||
s.Equal("test", Exec("echo 'test'"))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestExecShellAsync() {
|
||||
func (s *SystemHelperTestSuite) TestExecAsync() {
|
||||
command := "echo 'test' > /tmp/testfile"
|
||||
defer os.Remove("/tmp/testfile")
|
||||
|
||||
ExecShellAsync(command)
|
||||
ExecAsync(command)
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
@@ -99,3 +99,42 @@ 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"))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestMv() {
|
||||
filePath := "/tmp/testfile"
|
||||
defer os.Remove(filePath)
|
||||
|
||||
err := os.WriteFile(filePath, []byte("test data"), 0644)
|
||||
s.Nil(err)
|
||||
|
||||
s.True(Mv(filePath, "/tmp/testfile2"))
|
||||
s.False(Exists(filePath))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestCp() {
|
||||
filePath := "/tmp/testfile"
|
||||
defer os.Remove(filePath)
|
||||
|
||||
err := os.WriteFile(filePath, []byte("test data"), 0644)
|
||||
s.Nil(err)
|
||||
|
||||
s.True(Cp(filePath, "/tmp/testfile2"))
|
||||
s.True(Exists(filePath))
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestSize() {
|
||||
size, err := Size("/tmp/123")
|
||||
s.Equal(int64(0), size)
|
||||
s.Error(err)
|
||||
}
|
||||
|
||||
func (s *SystemHelperTestSuite) TestFileSize() {
|
||||
size, err := FileSize("/tmp/123")
|
||||
s.Equal(int64(0), size)
|
||||
s.Error(err)
|
||||
}
|
||||
|
||||
+47
-8
@@ -5,9 +5,11 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/disk"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
@@ -45,7 +47,7 @@ func GetMonitoringInfo() MonitoringInfo {
|
||||
|
||||
res.DiskUsage = make(map[string]*disk.UsageStat)
|
||||
for _, partition := range res.Disk {
|
||||
if strings.HasPrefix(partition.Mountpoint, "/dev") || strings.HasPrefix(partition.Mountpoint, "/sys") || strings.HasPrefix(partition.Mountpoint, "/proc") || strings.HasPrefix(partition.Mountpoint, "/run") || strings.HasPrefix(partition.Mountpoint, "/boot") || strings.HasPrefix(partition.Mountpoint, "/usr") {
|
||||
if strings.HasPrefix(partition.Mountpoint, "/dev") || strings.HasPrefix(partition.Mountpoint, "/sys") || strings.HasPrefix(partition.Mountpoint, "/proc") || strings.HasPrefix(partition.Mountpoint, "/run") || strings.HasPrefix(partition.Mountpoint, "/boot") || strings.HasPrefix(partition.Mountpoint, "/usr") || strings.HasPrefix(partition.Mountpoint, "/var") {
|
||||
continue
|
||||
}
|
||||
usage, _ := disk.Usage(partition.Mountpoint)
|
||||
@@ -67,7 +69,7 @@ type PanelInfo struct {
|
||||
func GetLatestPanelVersion() (PanelInfo, error) {
|
||||
var info PanelInfo
|
||||
|
||||
cmd := exec.Command("/bin/bash", "-c", "curl \"https://api.github.com/repos/HaoZi-Team/Panel/releases/latest\"")
|
||||
cmd := exec.Command("/bin/bash", "-c", "curl \"https://api.github.com/repos/haozi-team/panel/releases/latest\"")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return info, errors.New("获取最新版本失败")
|
||||
@@ -105,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))
|
||||
}
|
||||
@@ -114,17 +116,54 @@ func GetLatestPanelVersion() (PanelInfo, error) {
|
||||
}
|
||||
|
||||
// UpdatePanel 更新面板
|
||||
func UpdatePanel() error {
|
||||
func UpdatePanel(proxy bool) error {
|
||||
panelInfo, err := GetLatestPanelVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("/bin/bash", "-c", "wget -O panel.tar.gz "+panelInfo.DownloadUrl+" && tar -zxvf panel.tar.gz && rm -rf panel.tar.gz && chmod +x panel && ./panel artisan migrate")
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
return errors.New("更新面板失败")
|
||||
color.Greenln("最新版本: " + panelInfo.Version)
|
||||
color.Greenln("下载链接: " + panelInfo.DownloadUrl)
|
||||
color.Greenln("使用代理: " + strconv.FormatBool(proxy))
|
||||
|
||||
color.Greenln("备份面板配置...")
|
||||
Exec("cp -f /www/panel/database/panel.db /tmp/panel.db.bak")
|
||||
Exec("cp -f /www/panel/panel.conf /tmp/panel.conf.bak")
|
||||
if !Exists("/tmp/panel.db.bak") || !Exists("/tmp/panel.conf.bak") {
|
||||
return errors.New("备份面板配置失败")
|
||||
}
|
||||
color.Greenln("备份完成")
|
||||
|
||||
color.Greenln("清理旧版本...")
|
||||
Exec("rm -rf /www/panel/*")
|
||||
color.Greenln("清理完成")
|
||||
|
||||
color.Greenln("正在下载...")
|
||||
if proxy {
|
||||
Exec("wget -O /www/panel/panel.zip https://ghproxy.com/" + panelInfo.DownloadUrl)
|
||||
} else {
|
||||
Exec("wget -O /www/panel/panel.zip " + panelInfo.DownloadUrl)
|
||||
}
|
||||
color.Greenln("下载完成")
|
||||
|
||||
color.Greenln("更新新版本...")
|
||||
Exec("cd /www/panel && unzip -o panel.zip && rm -rf panel.zip && chmod 700 panel && bash scripts/update_panel.sh")
|
||||
color.Greenln("更新完成")
|
||||
|
||||
color.Greenln("恢复面板配置...")
|
||||
Exec("cp -f /tmp/panel.db.bak /www/panel/database/panel.db")
|
||||
Exec("cp -f /tmp/panel.conf.bak /www/panel/panel.conf")
|
||||
if !Exists("/www/panel/database/panel.db") || !Exists("/www/panel/panel.conf") {
|
||||
return errors.New("恢复面板配置失败")
|
||||
}
|
||||
Exec("/www/panel/panel --env=panel.conf artisan migrate")
|
||||
color.Greenln("恢复完成")
|
||||
|
||||
Exec("panel writeSetting version " + panelInfo.Version)
|
||||
|
||||
color.Greenln("重启面板...")
|
||||
Exec("systemctl restart panel")
|
||||
color.Greenln("重启完成")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
pnpm-lock.yaml
|
||||
+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
@@ -37,7 +37,7 @@ layui.define(['laytpl', 'layer'], function (exports) {
|
||||
})
|
||||
|
||||
//跳转到登入页
|
||||
location.hash = '/user/login'
|
||||
location.hash = '/login'
|
||||
}
|
||||
|
||||
//Ajax请求
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
|
||||
/* 样式加载完毕的标识 */
|
||||
html #layuicss-cron {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 1989px;
|
||||
}
|
||||
|
||||
|
||||
/* 主体结构 */
|
||||
.layui-cron {
|
||||
width: 550px;
|
||||
position: absolute;
|
||||
z-index: 99999999;
|
||||
margin: 5px 0;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
-webkit-animation-duration: 0.3s;
|
||||
animation-duration: 0.3s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
animation-fill-mode: both;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 2px 5px 0px;
|
||||
-webkit-animation-name: cron-upbit;
|
||||
animation-name: cron-upbit;
|
||||
border: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.layui-cron-main ul {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes cron-upbit {
|
||||
|
||||
/* 微微往上滑入 */
|
||||
from {
|
||||
-webkit-transform: translate3d(0, 20px, 0);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
to {
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cron-upbit {
|
||||
from {
|
||||
transform: translate3d(0, 20px, 0);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate3d(0, 0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* tabs */
|
||||
.layui-cron>.layui-tab {
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 行 */
|
||||
.cron-row {
|
||||
padding-left: 13px;
|
||||
}
|
||||
/* 格 */
|
||||
.cron-grid {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
/* 表达式 */
|
||||
.cron-title {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
margin: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.cron-box {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.cron-box+.cron-box {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* 按钮 */
|
||||
.cron-footer-btns {
|
||||
text-align: right;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cron-footer-btns span {
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
margin: 0 0 0 -1px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #C9C9C9;
|
||||
background-color: #fff;
|
||||
white-space: nowrap;
|
||||
vertical-align: top;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cron-footer-btns span:hover {
|
||||
color: #5FB878;
|
||||
}
|
||||
|
||||
|
||||
/* 表单 */
|
||||
.layui-cron .layui-form-radio {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.cron-form {
|
||||
line-height: 28px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cron-input-mid {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-top: 6px;
|
||||
background-color: #e5e5e5;
|
||||
padding: 0 12px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
border: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cron-input {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-top: 6px;
|
||||
padding: 0 8px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
box-sizing: border-box;
|
||||
width: 80px;
|
||||
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||
}
|
||||
|
||||
.cron-input:focus {
|
||||
outline: 0;
|
||||
border: 1px solid #01AAED;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 4px 0px #01AAED;
|
||||
translate: 1s;
|
||||
}
|
||||
|
||||
.layui-cron .layui-form-checkbox[lay-skin="primary"] span {
|
||||
padding-right: 10px;
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.layui-cron .layui-form-checkbox[lay-skin="primary"] {
|
||||
padding-left: 22px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.layui-cron input[type=number] {
|
||||
-moz-appearance:textfield;
|
||||
}
|
||||
.layui-cron input[type=number]::-webkit-inner-spin-button,
|
||||
.layui-cron input[type=number]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
.cron-tips{
|
||||
color: grey;
|
||||
line-height: 28px;
|
||||
height: 28px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-top: 8px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
@@ -0,0 +1,970 @@
|
||||
/**
|
||||
@ Name:layui.cron Cron表达式解析器
|
||||
@ Author:贝哥哥
|
||||
@ License:MIT
|
||||
*/
|
||||
|
||||
layui.define(['lay', 'element', 'form'], function (exports) { //假如该组件依赖 layui.form
|
||||
var $ = layui.$, layer = layui.layer, lay = layui.lay, element = layui.element, form = layui.form
|
||||
|
||||
|
||||
//字符常量
|
||||
, MOD_NAME = 'cron', ELEM = '.layui-cron', THIS = 'layui-this', SHOW = 'layui-show', HIDE = 'layui-hide'
|
||||
|
||||
, ELEM_STATIC = 'layui-cron-static', ELEM_FOOTER = 'layui-cron-footer', ELEM_CONFIRM = '.cron-btns-confirm',
|
||||
ELEM_HINT = 'layui-cron-hint'
|
||||
|
||||
, ELEM_RUN_HINT = 'layui-cron-run-hint'
|
||||
|
||||
//外部接口
|
||||
, cron = {
|
||||
v: '2.0.0' // cron 组件当前版本
|
||||
, index: layui.cron ? (layui.cron.index + 10000) : 0 // corn 实例标识
|
||||
|
||||
//设置全局项
|
||||
, set: function (options) {
|
||||
var that = this;
|
||||
that.config = $.extend({}, that.config, options);
|
||||
return that;
|
||||
}
|
||||
|
||||
//事件监听
|
||||
, on: function (events, callback) {
|
||||
return layui.onevent.call(this, MOD_NAME, events, callback);
|
||||
}
|
||||
|
||||
//主体CSS等待事件
|
||||
, ready: function (fn) {
|
||||
var cssPath = layui.cache.base + "cron.css?v=" + cron.v;
|
||||
layui.link(cssPath, fn, "cron"); //此处的“cron”要对应 cron.css 中的样式: html #layuicss-cron{}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
//操作当前实例
|
||||
, thisIns = function () {
|
||||
var that = this, options = that.config, id = options.id || options.index;
|
||||
|
||||
return {
|
||||
//提示框
|
||||
hint: function (content) {
|
||||
that.hint.call(that, content);
|
||||
}, config: options
|
||||
}
|
||||
}
|
||||
|
||||
//构造器,创建实例
|
||||
, Class = function (options) {
|
||||
var that = this;
|
||||
that.index = ++cron.index;
|
||||
that.config = $.extend({}, that.config, cron.config, options);
|
||||
cron.ready(function () {
|
||||
that.init();
|
||||
});
|
||||
};
|
||||
|
||||
//默认配置
|
||||
Class.prototype.config = {
|
||||
value: null // 当前表达式值,每秒执行一次
|
||||
,
|
||||
isInitValue: true //用于控制是否自动向元素填充初始值(需配合 value 参数使用)
|
||||
,
|
||||
lang: "cn" //语言,只支持cn/en,即中文和英文
|
||||
,
|
||||
tabs: [{key: 'minutes', range: '0-59'}, {
|
||||
key: 'hours', range: '0-23'
|
||||
}, {key: 'days', range: '1-31'}, {key: 'months', range: '1-12'}, {key: 'weeks', range: '1-7'}],
|
||||
defaultCron: {minutes: "*", hours: "*", days: "*", months: "*", weeks: "*"},
|
||||
trigger: "click" //呼出控件的事件
|
||||
,
|
||||
btns: ['run', 'confirm'] //右下角显示的按钮,会按照数组顺序排列
|
||||
,
|
||||
position: null //控件定位方式定位, 默认absolute,支持:fixed/absolute/static
|
||||
,
|
||||
zIndex: null //控件层叠顺序
|
||||
,
|
||||
show: false //是否直接显示,如果设置 true,则默认直接显示控件
|
||||
,
|
||||
showBottom: true //是否显示底部栏
|
||||
,
|
||||
done: null //控件选择完毕后的回调,点击运行/确定也均会触发
|
||||
,
|
||||
run: null // 最近运行时间接口
|
||||
};
|
||||
|
||||
//多语言
|
||||
Class.prototype.lang = function () {
|
||||
var that = this, options = that.config, text = {
|
||||
cn: {
|
||||
tabs: [{title: "分"}, {title: "时"}, {title: "日"}, {title: "月"}, {
|
||||
title: "周",
|
||||
rateBegin: "第",
|
||||
rateMid: "周的星期",
|
||||
rateEnd: ""
|
||||
}],
|
||||
every: "每",
|
||||
unspecified: "不指定",
|
||||
period: "周期",
|
||||
periodFrom: "从",
|
||||
rate: "按照",
|
||||
rateBegin: "从",
|
||||
rateMid: "开始,每",
|
||||
rateEnd: "执行一次",
|
||||
weekday: "工作日",
|
||||
weekdayPrefix: "每月",
|
||||
weekdaySuffix: "号最近的那个工作日",
|
||||
lastday: "本月最后一日",
|
||||
lastweek: "本月最后一个星期",
|
||||
custom: "指定",
|
||||
tools: {
|
||||
confirm: '确定', run: '运行'
|
||||
},
|
||||
formatError: ['Cron格式不合法', '<br>已为你重置']
|
||||
}, en: {
|
||||
tabs: [{title: "Minutes"}, {title: "Hours"}, {title: "Days"}, {title: "Months"}, {title: "Weeks"}],
|
||||
every: "Every ",
|
||||
unspecified: "Unspecified",
|
||||
period: "Period",
|
||||
periodFrom: "From",
|
||||
rate: "According to",
|
||||
rateBegin: "begin at",
|
||||
rateMid: ", every",
|
||||
rateEnd: " execute once",
|
||||
weekday: "Weekday",
|
||||
weekdayPrefix: "Every month at ",
|
||||
weekdaySuffix: "号最近的那个工作日",
|
||||
lastday: "Last day of the month",
|
||||
lastweek: "本月最后一个星期",
|
||||
custom: "Custom",
|
||||
tools: {
|
||||
confirm: 'Confirm', run: 'Run'
|
||||
},
|
||||
formatError: ['The cron format error', '<br>It has been reset']
|
||||
}
|
||||
};
|
||||
return text[options.lang] || text['cn'];
|
||||
};
|
||||
|
||||
//初始准备
|
||||
Class.prototype.init = function () {
|
||||
var that = this, options = that.config, isStatic = options.position === 'static';
|
||||
|
||||
options.elem = lay(options.elem);
|
||||
|
||||
options.eventElem = lay(options.eventElem);
|
||||
|
||||
if (!options.elem[0]) return;
|
||||
|
||||
//如果不是input|textarea元素,则默认采用click事件
|
||||
if (!that.isInput(options.elem[0])) {
|
||||
if (options.trigger === 'focus') {
|
||||
options.trigger = 'click';
|
||||
}
|
||||
}
|
||||
|
||||
// 设置渲染所绑定元素的唯一KEY
|
||||
if (!options.elem.attr('lay-key')) {
|
||||
options.elem.attr('lay-key', that.index);
|
||||
options.eventElem.attr('lay-key', that.index);
|
||||
}
|
||||
|
||||
// 当前实例主面板ID
|
||||
that.elemID = 'layui-icon' + options.elem.attr('lay-key');
|
||||
|
||||
//默认赋值
|
||||
if (options.value && options.isInitValue) {
|
||||
that.setValue(options.value);
|
||||
}
|
||||
if (!options.value) {
|
||||
options.value = options.elem[0].value || '';
|
||||
}
|
||||
var cronArr = options.value.split(' ');
|
||||
if (cronArr.length >= 6) {
|
||||
options.cron = {
|
||||
minutes: cronArr[0],
|
||||
hours: cronArr[1],
|
||||
days: cronArr[2],
|
||||
months: cronArr[3],
|
||||
weeks: cronArr[4],
|
||||
};
|
||||
} else {
|
||||
options.cron = lay.extend({}, options.defaultCron);
|
||||
}
|
||||
|
||||
|
||||
if (options.show || isStatic) that.render();
|
||||
isStatic || that.events();
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
// 控件主体渲染
|
||||
Class.prototype.render = function () {
|
||||
var that = this, options = that.config, lang = that.lang(), isStatic = options.position === 'static',
|
||||
tabFilter = 'cron-tab' + options.elem.attr('lay-key')
|
||||
//主面板
|
||||
, elem = that.elem = lay.elem('div', {
|
||||
id: that.elemID, 'class': ['layui-cron', isStatic ? (' ' + ELEM_STATIC) : ''].join('')
|
||||
})
|
||||
|
||||
// tab 内容区域
|
||||
, elemTab = that.elemTab = lay.elem('div', {
|
||||
'class': 'layui-tab layui-tab-card', 'lay-filter': tabFilter
|
||||
}), tabHead = lay.elem('ul', {
|
||||
'class': 'layui-tab-title'
|
||||
}), tabContent = lay.elem('div', {
|
||||
'class': 'layui-tab-content'
|
||||
})
|
||||
|
||||
//底部区域
|
||||
, divFooter = that.footer = lay.elem('div', {
|
||||
'class': ELEM_FOOTER
|
||||
});
|
||||
|
||||
if (options.zIndex) elem.style.zIndex = options.zIndex;
|
||||
|
||||
// 生成tab 内容区域
|
||||
elemTab.appendChild(tabHead);
|
||||
elemTab.appendChild(tabContent);
|
||||
lay.each(lang.tabs, function (i, item) {
|
||||
// 表头
|
||||
var li = lay.elem('li', {
|
||||
'class': i === 0 ? THIS : "", 'lay-id': i
|
||||
});
|
||||
li.innerHTML = item.title;
|
||||
tabHead.appendChild(li);
|
||||
|
||||
// 表体
|
||||
tabContent.appendChild(that.getTabContentChildElem(i));
|
||||
});
|
||||
|
||||
// 主区域
|
||||
elemMain = that.elemMain = lay.elem('div', {
|
||||
'class': 'layui-cron-main'
|
||||
});
|
||||
elemMain.appendChild(elemTab);
|
||||
|
||||
//生成底部栏
|
||||
lay(divFooter).html(function () {
|
||||
var html = [], btns = [];
|
||||
lay.each(options.btns, function (i, item) {
|
||||
var title = lang.tools[item] || 'btn';
|
||||
btns.push('<span lay-type="' + item + '" class="cron-btns-' + item + '">' + title + '</span>');
|
||||
});
|
||||
html.push('<div class="cron-footer-btns">' + btns.join('') + '</div>');
|
||||
return html.join('');
|
||||
}());
|
||||
|
||||
//插入到主区域
|
||||
elem.appendChild(elemMain);
|
||||
|
||||
options.showBottom && elem.appendChild(divFooter);
|
||||
|
||||
|
||||
//移除上一个控件
|
||||
that.remove(Class.thisElemCron);
|
||||
|
||||
//如果是静态定位,则插入到指定的容器中,否则,插入到body
|
||||
isStatic ? options.elem.append(elem) : (document.body.appendChild(elem)
|
||||
, that.position());
|
||||
|
||||
|
||||
that.checkCron();
|
||||
|
||||
that.elemEvent(); // 主面板事件
|
||||
|
||||
Class.thisElemCron = that.elemID;
|
||||
|
||||
form.render();
|
||||
|
||||
}
|
||||
|
||||
// 渲染 tab 子控件
|
||||
Class.prototype.getTabContentChildElem = function (index) {
|
||||
var that = this, options = that.config, tabItem = options.tabs[index], tabItemKey = tabItem.key,
|
||||
lang = that.lang(), tabItemLang = lang.tabs[index], cron = options.cron,
|
||||
formFilter = 'cronForm' + tabItemKey + options.elem.attr('lay-key'), data = function () {
|
||||
if (cron[tabItemKey].indexOf('-') != -1) {
|
||||
// 周期数据
|
||||
var arr = cron[tabItemKey].split('-');
|
||||
return {
|
||||
type: 'range', start: arr[0], end: arr[1]
|
||||
};
|
||||
}
|
||||
if (cron[tabItemKey].indexOf('/') != -1) {
|
||||
// 频率数据
|
||||
var arr = cron[tabItemKey].split('/');
|
||||
return {
|
||||
type: 'rate', begin: arr[0], rate: arr[1]
|
||||
};
|
||||
}
|
||||
if (cron[tabItemKey].indexOf(',') != -1 || /^\+?[0-9][0-9]*$/.test(cron[tabItemKey])) {
|
||||
// 按照指定执行
|
||||
var arr = cron[tabItemKey].split(',').map(Number);
|
||||
return {
|
||||
type: 'custom', values: arr
|
||||
};
|
||||
}
|
||||
if (cron[tabItemKey].indexOf('W') != -1) {
|
||||
// 最近的工作日
|
||||
var value = cron[tabItemKey].replace('W', '');
|
||||
return {
|
||||
type: 'weekday', value: value
|
||||
};
|
||||
}
|
||||
if (index === 2 && cron[tabItemKey] === 'L') {
|
||||
// 本月最后一日
|
||||
return {
|
||||
type: 'lastday', value: 'L'
|
||||
};
|
||||
}
|
||||
if (index === 4 && cron[tabItemKey].indexOf('L') != -1) {
|
||||
// 本月最后一个周 value
|
||||
var value = cron[tabItemKey].replace('L', '');
|
||||
return {
|
||||
type: 'lastweek', value: value
|
||||
};
|
||||
}
|
||||
if (cron[tabItemKey] === '*') {
|
||||
// 每次
|
||||
return {
|
||||
type: 'every', value: '*'
|
||||
};
|
||||
}
|
||||
if (cron[tabItemKey] === '?' || cron[tabItemKey] === undefined || cron[tabItemKey] === '') {
|
||||
// 不指定
|
||||
return {
|
||||
//type: 'unspecified', value: cron[tabItemKey]
|
||||
type: 'every', value: '*'
|
||||
};
|
||||
}
|
||||
}(), rangeData = function () {
|
||||
if (tabItem.range) {
|
||||
var arr = tabItem.range.split('-');
|
||||
return {
|
||||
min: parseInt(arr[0]), max: parseInt(arr[1])
|
||||
};
|
||||
}
|
||||
}();
|
||||
var elem = lay.elem('div', {
|
||||
'class': 'layui-tab-item layui-form ' + (index === 0 ? SHOW : ""), 'lay-filter': formFilter
|
||||
});
|
||||
|
||||
// 每次
|
||||
elem.appendChild(function () {
|
||||
var everyRadio = lay.elem('input', {
|
||||
'name': tabItemKey + '[type]',
|
||||
'type': 'radio',
|
||||
'value': 'every',
|
||||
'title': lang.every + tabItemLang.title
|
||||
});
|
||||
if (data.type === 'every') {
|
||||
lay(everyRadio).attr('checked', true);
|
||||
}
|
||||
var everyDiv = lay.elem('div', {
|
||||
'class': 'cron-row'
|
||||
});
|
||||
everyDiv.appendChild(everyRadio);
|
||||
return everyDiv;
|
||||
}());
|
||||
|
||||
// 不指定,从日开始
|
||||
/*if (index >= 2) {
|
||||
elem.appendChild(function () {
|
||||
var unspecifiedRadio = lay.elem('input', {
|
||||
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'unspecified', 'title': lang.unspecified
|
||||
});
|
||||
if (data.type === 'unspecified') {
|
||||
lay(unspecifiedRadio).attr('checked', true);
|
||||
}
|
||||
var unspecifiedDiv = lay.elem('div', {
|
||||
'class': 'cron-row'
|
||||
});
|
||||
unspecifiedDiv.appendChild(unspecifiedRadio);
|
||||
return unspecifiedDiv;
|
||||
}());
|
||||
}*/
|
||||
|
||||
// 周期
|
||||
var rangeChild = [function () {
|
||||
var rangeRadio = lay.elem('input', {
|
||||
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'range', 'title': lang.period
|
||||
});
|
||||
if (data.type === 'range') {
|
||||
lay(rangeRadio).attr('checked', true);
|
||||
}
|
||||
return rangeRadio;
|
||||
}(), function () {
|
||||
var elem = lay.elem('div', {
|
||||
'class': 'cron-input-mid'
|
||||
});
|
||||
elem.innerHTML = lang.periodFrom;
|
||||
return elem;
|
||||
}(), function () {
|
||||
var elem = lay.elem('input', {
|
||||
'class': 'cron-input', 'type': 'number', 'name': 'rangeStart', 'value': data.start || ''
|
||||
});
|
||||
return elem;
|
||||
}(), function () {
|
||||
var elem = lay.elem('div', {
|
||||
'class': 'cron-input-mid'
|
||||
});
|
||||
elem.innerHTML = '-';
|
||||
return elem;
|
||||
}(), function () {
|
||||
var elem = lay.elem('input', {
|
||||
'class': 'cron-input', 'type': 'number', 'name': 'rangeEnd', 'value': data.end || ''
|
||||
});
|
||||
return elem;
|
||||
}(), function () {
|
||||
var elem = lay.elem('div', {
|
||||
'class': 'cron-input-mid'
|
||||
});
|
||||
elem.innerHTML = tabItemLang.title;
|
||||
return elem;
|
||||
}()]
|
||||
|
||||
, rangeDiv = lay.elem('div', {
|
||||
'class': 'cron-row'
|
||||
});
|
||||
lay.each(rangeChild, function (i, item) {
|
||||
rangeDiv.appendChild(item);
|
||||
});
|
||||
if (tabItem.range) {
|
||||
var rangeTip = lay.elem('div', {
|
||||
'class': 'cron-tips'
|
||||
});
|
||||
rangeTip.innerHTML = ['(', tabItem.range, ')'].join('');
|
||||
rangeDiv.appendChild(rangeTip);
|
||||
}
|
||||
elem.appendChild(rangeDiv);
|
||||
|
||||
// 频率,年没有
|
||||
if (index < 6) {
|
||||
var rateChild = [function () {
|
||||
var rateRadio = lay.elem('input', {
|
||||
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'rate', 'title': lang.rate
|
||||
});
|
||||
if (data.type === 'rate') {
|
||||
lay(rateRadio).attr('checked', true);
|
||||
}
|
||||
return rateRadio;
|
||||
}(), function () {
|
||||
var elem = lay.elem('div', {
|
||||
'class': 'cron-input-mid'
|
||||
});
|
||||
elem.innerHTML = tabItemLang.rateBegin || lang.rateBegin;
|
||||
return elem;
|
||||
}(), function () {
|
||||
var elem = lay.elem('input', {
|
||||
'class': 'cron-input', 'type': 'number', 'name': 'begin', 'value': data.begin || ''
|
||||
});
|
||||
return elem;
|
||||
}(), function () {
|
||||
var elem = lay.elem('div', {
|
||||
'class': 'cron-input-mid'
|
||||
});
|
||||
elem.innerHTML = tabItemLang.rateMid || (tabItemLang.title + lang.rateMid);
|
||||
return elem;
|
||||
}(), function () {
|
||||
var elem = lay.elem('input', {
|
||||
'class': 'cron-input', 'type': 'number', 'name': 'rate', 'value': data.rate || ''
|
||||
});
|
||||
return elem;
|
||||
}(), function () {
|
||||
var elem = lay.elem('div', {
|
||||
'class': 'cron-input-mid'
|
||||
});
|
||||
elem.innerHTML = undefined != tabItemLang.rateEnd ? tabItemLang.rateEnd : (tabItemLang.title + lang.rateEnd);
|
||||
if (undefined != tabItemLang.rateEnd && tabItemLang.rateEnd === '') {
|
||||
lay(elem).addClass(HIDE);
|
||||
}
|
||||
return elem;
|
||||
}()]
|
||||
|
||||
, rateDiv = lay.elem('div', {
|
||||
'class': 'cron-row'
|
||||
});
|
||||
lay.each(rateChild, function (i, item) {
|
||||
rateDiv.appendChild(item);
|
||||
});
|
||||
if (tabItem.range) {
|
||||
var rateTip = lay.elem('div', {
|
||||
'class': 'cron-tips'
|
||||
});
|
||||
if (index === 4) {
|
||||
// 周
|
||||
rateTip.innerHTML = '(1-4/1-7)';
|
||||
} else {
|
||||
rateTip.innerHTML = ['(', rangeData.min, '/', (rangeData.max + (index <= 1 ? 1 : 0)), ')'].join('');
|
||||
}
|
||||
rateDiv.appendChild(rateTip);
|
||||
}
|
||||
elem.appendChild(rateDiv);
|
||||
}
|
||||
|
||||
// 特殊:日(最近的工作日、最后一日),周(最后一周)
|
||||
/*if (index === 2) {
|
||||
// 日
|
||||
// 最近的工作日
|
||||
var weekChild = [function () {
|
||||
var weekRadio = lay.elem('input', {
|
||||
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'weekday', 'title': lang.weekday
|
||||
});
|
||||
if (data.type === 'weekday') {
|
||||
lay(weekRadio).attr('checked', true);
|
||||
}
|
||||
return weekRadio;
|
||||
}(), function () {
|
||||
var elem = lay.elem('div', {
|
||||
'class': 'cron-input-mid'
|
||||
});
|
||||
elem.innerHTML = lang.weekdayPrefix;
|
||||
return elem;
|
||||
}(), function () {
|
||||
var elem = lay.elem('input', {
|
||||
'class': 'cron-input', 'type': 'number', 'name': 'weekday', 'value': data.value || ''
|
||||
});
|
||||
return elem;
|
||||
}(), function () {
|
||||
var elem = lay.elem('div', {
|
||||
'class': 'cron-input-mid'
|
||||
});
|
||||
elem.innerHTML = lang.weekdaySuffix;
|
||||
return elem;
|
||||
}(), function () {
|
||||
var elem = lay.elem('div', {
|
||||
'class': 'cron-tips'
|
||||
});
|
||||
elem.innerHTML = ['(', tabItem.range, ')'].join('');
|
||||
return elem;
|
||||
}()]
|
||||
|
||||
, weekDiv = lay.elem('div', {
|
||||
'class': 'cron-row'
|
||||
});
|
||||
lay.each(weekChild, function (i, item) {
|
||||
weekDiv.appendChild(item);
|
||||
});
|
||||
elem.appendChild(weekDiv);
|
||||
|
||||
// 本月最后一日
|
||||
elem.appendChild(function () {
|
||||
var lastRadio = lay.elem('input', {
|
||||
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'lastday', 'title': lang.lastday
|
||||
});
|
||||
if (data.type === 'lastday') {
|
||||
lay(lastRadio).attr('checked', true);
|
||||
}
|
||||
var lastDiv = lay.elem('div', {
|
||||
'class': 'cron-row'
|
||||
});
|
||||
lastDiv.appendChild(lastRadio);
|
||||
return lastDiv;
|
||||
}());
|
||||
|
||||
}
|
||||
|
||||
if (index === 4) {
|
||||
// 本月最后一个周几
|
||||
var lastWeekChild = [function () {
|
||||
var lastWeekRadio = lay.elem('input', {
|
||||
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'lastweek', 'title': lang.lastweek
|
||||
});
|
||||
if (data.type === 'lastweek') {
|
||||
lay(lastWeekRadio).attr('checked', true);
|
||||
}
|
||||
return lastWeekRadio;
|
||||
}(), function () {
|
||||
var elem = lay.elem('input', {
|
||||
'class': 'cron-input', 'type': 'number', 'name': 'lastweek', 'value': data.value || ''
|
||||
});
|
||||
return elem;
|
||||
}(), function () {
|
||||
var elem = lay.elem('div', {
|
||||
'class': 'cron-tips'
|
||||
});
|
||||
elem.innerHTML = ['(', tabItem.range, ')'].join('');
|
||||
return elem;
|
||||
}()]
|
||||
|
||||
, lastWeekDiv = lay.elem('div', {
|
||||
'class': 'cron-row'
|
||||
});
|
||||
lay.each(lastWeekChild, function (i, item) {
|
||||
lastWeekDiv.appendChild(item);
|
||||
});
|
||||
elem.appendChild(lastWeekDiv);
|
||||
|
||||
}*/
|
||||
|
||||
// 指定
|
||||
if (index <= 4) {
|
||||
elem.appendChild(function () {
|
||||
var customRadio = lay.elem('input', {
|
||||
'name': tabItemKey + '[type]', 'type': 'radio', 'value': 'custom', 'title': lang.custom
|
||||
});
|
||||
if (data.type === 'custom') {
|
||||
lay(customRadio).attr('checked', true);
|
||||
}
|
||||
var customDiv = lay.elem('div', {
|
||||
'class': 'cron-row'
|
||||
});
|
||||
customDiv.appendChild(customRadio);
|
||||
return customDiv;
|
||||
}());
|
||||
|
||||
// 指定数值,时分秒显示两位数,自动补零
|
||||
elem.appendChild(function () {
|
||||
var customGrid = lay.elem('div', {
|
||||
'class': 'cron-grid'
|
||||
});
|
||||
var i = rangeData.min;
|
||||
while (i <= rangeData.max) {
|
||||
// 时分秒显示两位数,自动补零
|
||||
var gridItemValue = index <= 1 ? lay.digit(i, 2) : i;
|
||||
var gridItem = lay.elem('input', {
|
||||
'type': 'checkbox',
|
||||
'title': gridItemValue,
|
||||
'lay-skin': 'primary',
|
||||
'name': tabItemKey + '[custom]',
|
||||
'value': i
|
||||
});
|
||||
if (data.values && data.values.includes(i)) {
|
||||
lay(gridItem).attr('checked', true);
|
||||
}
|
||||
customGrid.appendChild(gridItem);
|
||||
i++;
|
||||
}
|
||||
return customGrid;
|
||||
}());
|
||||
}
|
||||
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
//是否输入框
|
||||
Class.prototype.isInput = function (elem) {
|
||||
return /input|textarea/.test(elem.tagName.toLocaleLowerCase());
|
||||
};
|
||||
|
||||
// 绑定的元素事件处理
|
||||
Class.prototype.events = function () {
|
||||
var that = this, options = that.config
|
||||
|
||||
//绑定呼出控件事件
|
||||
, showEvent = function (elem, bind) {
|
||||
elem.on(options.trigger, function () {
|
||||
bind && (that.bindElem = this);
|
||||
that.render();
|
||||
});
|
||||
};
|
||||
|
||||
if (!options.elem[0] || options.elem[0].eventHandler) return;
|
||||
|
||||
showEvent(options.elem, 'bind');
|
||||
showEvent(options.eventElem);
|
||||
|
||||
//绑定关闭控件事件
|
||||
lay(document).on('click', function (e) {
|
||||
if (e.target === options.elem[0] || e.target === options.eventElem[0] || e.target === lay(options.closeStop)[0]) {
|
||||
return;
|
||||
}
|
||||
that.remove();
|
||||
}).on('keydown', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
if (lay('#' + that.elemID)[0] && that.elemID === Class.thisElemDate) {
|
||||
e.preventDefault();
|
||||
lay(that.footer).find(ELEM_CONFIRM)[0].click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//自适应定位
|
||||
lay(window).on('resize', function () {
|
||||
if (!that.elem || !lay(ELEM)[0]) {
|
||||
return false;
|
||||
}
|
||||
that.position();
|
||||
});
|
||||
|
||||
options.elem[0].eventHandler = true;
|
||||
};
|
||||
|
||||
// 主面板事件
|
||||
Class.prototype.elemEvent = function () {
|
||||
var that = this, options = that.config, tabFilter = 'cron-tab' + options.elem.attr('lay-key');
|
||||
|
||||
// 阻止主面板点击冒泡,避免因触发文档事件而关闭主面
|
||||
lay(that.elem).on('click', function (e) {
|
||||
lay.stope(e);
|
||||
});
|
||||
|
||||
// tab选项卡切换
|
||||
var lis = lay(that.elemTab).find('li');
|
||||
lis.on('click', function () {
|
||||
var layid = lay(this).attr('lay-id');
|
||||
if (undefined === layid) {
|
||||
return;
|
||||
}
|
||||
element.tabChange(tabFilter, layid);
|
||||
});
|
||||
|
||||
// cron选项点击
|
||||
form.on('radio', function (data) {
|
||||
var $parent = data.othis.parent();
|
||||
var formFilter = $parent.parent().attr('lay-filter');
|
||||
var formData = form.val(formFilter);
|
||||
var radioType = data.value;
|
||||
if ('range' === radioType) {
|
||||
// 范围
|
||||
form.val(formFilter, {
|
||||
rangeStart: formData.rangeStart || 0, rangeEnd: formData.rangeEnd || 2
|
||||
});
|
||||
}
|
||||
if ('rate' === radioType) {
|
||||
// 频率
|
||||
form.val(formFilter, {
|
||||
begin: formData.begin || 0, rate: formData.rate || 2
|
||||
});
|
||||
}
|
||||
if ('custom' === radioType) {
|
||||
// custom
|
||||
var $grid = $parent.next();
|
||||
if ($grid.find(':checkbox:checked').length <= 0) {
|
||||
$grid.children(':checkbox:first').next().click()
|
||||
}
|
||||
}
|
||||
if ('weekday' === radioType) {
|
||||
// weekday
|
||||
form.val(formFilter, {
|
||||
weekday: formData.weekday || 1
|
||||
});
|
||||
}
|
||||
if ('lastweek' === radioType) {
|
||||
// lastweek
|
||||
form.val(formFilter, {
|
||||
lastweek: formData.lastweek || 1
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
//gird checkbox点击时,自动选中radio
|
||||
form.on('checkbox', function (data) {
|
||||
//触发选项的点击事件
|
||||
var $parent = data.othis.parent().parent();
|
||||
//循环父级,找到子级的子级找到value="custom"的radio
|
||||
var $radio = $parent.children().find('input[value="custom"]');
|
||||
//触发radio的点击事件
|
||||
$radio.next().click();
|
||||
});
|
||||
|
||||
//点击底部按钮
|
||||
lay(that.footer).find('span').on('click', function () {
|
||||
var type = lay(this).attr('lay-type');
|
||||
that.tool(this, type);
|
||||
});
|
||||
};
|
||||
|
||||
//底部按钮点击事件
|
||||
Class.prototype.tool = function (btn, type) {
|
||||
var that = this, options = that.config, lang = that.lang(), isStatic = options.position === 'static', active = {
|
||||
//运行
|
||||
run: function () {
|
||||
var value = that.parse();
|
||||
var loading = layer.load();
|
||||
$.get(options.run, {cron: value}, function (res) {
|
||||
layer.close(loading);
|
||||
if (res.code !== 0) {
|
||||
return that.hint(res.msg);
|
||||
}
|
||||
that.runHint(res.data);
|
||||
}, 'json').fail(function () {
|
||||
layer.close(loading);
|
||||
that.hint('服务器异常!');
|
||||
});
|
||||
}
|
||||
|
||||
//确定
|
||||
, confirm: function () {
|
||||
var value = that.parse();
|
||||
that.done([value]);
|
||||
that.setValue(value).remove()
|
||||
}
|
||||
};
|
||||
active[type] && active[type]();
|
||||
};
|
||||
|
||||
//执行 done/change 回调
|
||||
Class.prototype.done = function (param, type) {
|
||||
var that = this, options = that.config;
|
||||
|
||||
param = param || [that.parse()];
|
||||
typeof options[type || 'done'] === 'function' && options[type || 'done'].apply(options, param);
|
||||
|
||||
return that;
|
||||
};
|
||||
|
||||
// 解析cron表达式
|
||||
Class.prototype.parse = function () {
|
||||
var that = this, options = that.config, valueArr = [];
|
||||
|
||||
lay.each(options.tabs, function (index, item) {
|
||||
var key = item.key;
|
||||
var formFilter = 'cronForm' + key + options.elem.attr('lay-key');
|
||||
var formData = form.val(formFilter);
|
||||
var radioType = (key + '[type]');
|
||||
var current = "";
|
||||
if (formData[radioType] === 'every') {
|
||||
// 每次
|
||||
current = "*";
|
||||
}
|
||||
if (formData[radioType] === 'range') {
|
||||
// 范围
|
||||
current = formData.rangeStart + "-" + formData.rangeEnd;
|
||||
}
|
||||
if (formData[radioType] === 'rate') {
|
||||
// 频率
|
||||
current = formData.begin + "/" + formData.rate;
|
||||
}
|
||||
if (formData[radioType] === 'custom') {
|
||||
// 指定
|
||||
var checkboxName = (item.key + '[custom]');
|
||||
var customArr = [];
|
||||
$('input[name="' + checkboxName + '"]:checked').each(function () {
|
||||
customArr.push($(this).val());
|
||||
});
|
||||
current = customArr.join(',');
|
||||
}
|
||||
if (formData[radioType] === 'weekday') {
|
||||
// 每月 formData.weekday 号最近的那个工作日
|
||||
current = formData.weekday + "W";
|
||||
}
|
||||
if (formData[radioType] === 'lastday') {
|
||||
// 本月最后一日
|
||||
current = "L";
|
||||
}
|
||||
if (formData[radioType] === 'lastweek') {
|
||||
// 本月最后星期
|
||||
current = formData.lastweek + "L";
|
||||
}
|
||||
|
||||
if (formData[radioType] === 'unspecified' && index != 6) {
|
||||
// 不指定
|
||||
current = "?";
|
||||
}
|
||||
if (current !== "") {
|
||||
valueArr.push(current);
|
||||
options.cron[key] = current;
|
||||
}
|
||||
});
|
||||
return valueArr.join(' ');
|
||||
};
|
||||
|
||||
//控件移除
|
||||
Class.prototype.remove = function (prev) {
|
||||
var that = this, options = that.config, elem = lay('#' + (prev || that.elemID));
|
||||
if (!elem[0]) return that;
|
||||
|
||||
if (!elem.hasClass(ELEM_STATIC)) {
|
||||
that.checkCron(function () {
|
||||
elem.remove();
|
||||
});
|
||||
}
|
||||
return that;
|
||||
};
|
||||
|
||||
//定位算法
|
||||
Class.prototype.position = function () {
|
||||
var that = this, options = that.config;
|
||||
lay.position(that.bindElem || options.elem[0], that.elem, {
|
||||
position: options.position
|
||||
});
|
||||
return that;
|
||||
};
|
||||
|
||||
//提示
|
||||
Class.prototype.hint = function (content) {
|
||||
var that = this, options = that.config, div = lay.elem('div', {
|
||||
'class': ELEM_HINT
|
||||
});
|
||||
|
||||
if (!that.elem) return;
|
||||
|
||||
div.innerHTML = content || '';
|
||||
lay(that.elem).find('.' + ELEM_HINT).remove();
|
||||
that.elem.appendChild(div);
|
||||
|
||||
clearTimeout(that.hinTimer);
|
||||
that.hinTimer = setTimeout(function () {
|
||||
lay(that.elem).find('.' + ELEM_HINT).remove();
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
//运行提示
|
||||
Class.prototype.runHint = function (runList) {
|
||||
var that = this, options = that.config, div = lay.elem('div', {
|
||||
'class': ELEM_RUN_HINT
|
||||
});
|
||||
// debugger;
|
||||
if (!that.elem || !runList || !runList.length) return;
|
||||
|
||||
|
||||
lay(div).html(function () {
|
||||
var html = [];
|
||||
lay.each(runList, function (i, item) {
|
||||
html.push('<div class="cron-run-list">' + item + '</div>');
|
||||
});
|
||||
return html.join('');
|
||||
}());
|
||||
|
||||
lay(that.elem).find('.' + ELEM_RUN_HINT).remove();
|
||||
that.elem.appendChild(div);
|
||||
};
|
||||
|
||||
//赋值
|
||||
Class.prototype.setValue = function (value = '') {
|
||||
var that = this, options = that.config, elem = that.bindElem || options.elem[0],
|
||||
valType = that.isInput(elem) ? 'val' : 'html'
|
||||
|
||||
options.position === 'static' || lay(elem)[valType](value || '');
|
||||
elem.textContent = '生成';
|
||||
return this;
|
||||
};
|
||||
|
||||
//cron校验
|
||||
Class.prototype.checkCron = function (fn) {
|
||||
var that = this, options = that.config, lang = that.lang(), elem = that.bindElem || options.elem[0],
|
||||
value = that.isInput(elem) ? elem.value : (options.position === 'static' ? '' : elem.innerHTML)
|
||||
|
||||
, checkValid = function (value = "") {
|
||||
|
||||
};
|
||||
|
||||
// cron 值,多个空格替换为一个空格,去掉首尾空格
|
||||
value = value || options.value;
|
||||
if (typeof value === 'string') {
|
||||
value = value.replace(/\s+/g, ' ').replace(/^\s|\s$/g, '');
|
||||
}
|
||||
|
||||
if (fn === 'init') return checkValid(value), that;
|
||||
|
||||
value = that.parse();
|
||||
if (value) {
|
||||
that.setValue(value);
|
||||
}
|
||||
fn && fn();
|
||||
return that;
|
||||
};
|
||||
|
||||
//核心入口
|
||||
cron.render = function (options) {
|
||||
var ins = new Class(options);
|
||||
return thisIns.call(ins);
|
||||
};
|
||||
|
||||
exports('cron', cron);
|
||||
});
|
||||
@@ -0,0 +1,406 @@
|
||||
<!--
|
||||
Name: 计划任务
|
||||
Author: 耗子
|
||||
Date: 2023-07-21
|
||||
-->
|
||||
<title>计划任务</title>
|
||||
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">添加计划任务</div>
|
||||
<div class="layui-card-body">
|
||||
<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="请输入任务名称"
|
||||
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-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">
|
||||
<input type="text" name="time" id="cron-add-time"
|
||||
lay-verify="required" placeholder="请选择或输入cron表达式" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-form-mid layui-word-aux">请务必正确填写执行周期</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text" id="cron-add-shell">
|
||||
<label class="layui-form-label">脚本内容</label>
|
||||
<div class="layui-input-block">
|
||||
<div id="cron-add-script-editor"
|
||||
style="height: 250px;"># 在此输入你要执行的脚本内容</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" lay-submit="" lay-filter="cron-add-submit">立即提交</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">计划任务列表</div>
|
||||
<div class="layui-card-body">
|
||||
<table id="panel-cron" lay-filter="panel-cron"></table>
|
||||
<!-- 操作按钮模板 -->
|
||||
<script type="text/html" id="cron-table-edit">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="log">日志</a>
|
||||
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="del">删除</a>
|
||||
</script>
|
||||
<!-- 任务类型 -->
|
||||
<script type="text/html" id="cron-table-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 ? 'checked' : '' }}>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var cronAddScriptEditor = ace.edit("cron-add-script-editor", {
|
||||
mode: "ace/mode/sh",
|
||||
selectionStyle: "text"
|
||||
});
|
||||
var cronEditScriptEditor;
|
||||
layui.use(['admin', 'table', 'jquery', 'cron'], function () {
|
||||
var $ = layui.$
|
||||
, form = layui.form
|
||||
, table = layui.table
|
||||
, admin = layui.admin
|
||||
, cron = layui.cron;
|
||||
|
||||
cron.render({
|
||||
elem: "#cron-add-time",
|
||||
btns: ['confirm'],
|
||||
show: false,
|
||||
done: function (value) {
|
||||
$('#cron-add-time').val(value);
|
||||
}
|
||||
});
|
||||
|
||||
form.render();
|
||||
|
||||
table.render({
|
||||
elem: '#panel-cron'
|
||||
, url: '/api/panel/cron/list'
|
||||
, cols: [[
|
||||
{field: 'id', hide: true, title: 'ID'}
|
||||
, {field: 'name', width: 250, 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: 'created_at', title: '创建时间'}
|
||||
, {field: 'updated_at', title: '最后更新时间'}
|
||||
, {
|
||||
field: 'edit',
|
||||
width: 180,
|
||||
title: '操作',
|
||||
templet: '#cron-table-edit',
|
||||
fixed: 'right',
|
||||
align: 'left'
|
||||
}
|
||||
]]
|
||||
, page: true
|
||||
, text: {
|
||||
none: '暂无数据'
|
||||
}
|
||||
, parseData: function (res) {
|
||||
return {
|
||||
"code": res.code,
|
||||
"msg": res.message,
|
||||
"count": res.data.total,
|
||||
"data": res.data.items
|
||||
};
|
||||
}
|
||||
});
|
||||
table.on('tool(panel-cron)', function (obj) {
|
||||
let data = obj.data;
|
||||
if (obj.event === 'log') {
|
||||
// 打开日志弹窗
|
||||
admin.popup({
|
||||
title: '日志 - ' + data.name
|
||||
,
|
||||
area: ['80%', '80%']
|
||||
,
|
||||
id: 'cron-log'
|
||||
,
|
||||
content: '<pre id="cron-log-view" style="overflow: auto; height: 95%;border: 0 none; line-height:23px; padding: 15px; margin: 0; white-space: pre-wrap; background-color: rgb(51,51,51); color:#f1f1f1; border-radius:0;"></pre>'
|
||||
,
|
||||
success: function (layero, index) {
|
||||
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 {
|
||||
layer.msg(res.message, {icon: 2, time: 1000});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
} else if (obj.event === 'edit') {
|
||||
// 打开编辑弹窗
|
||||
admin.req({
|
||||
url: '/api/panel/cron/script?id=' + data.id
|
||||
, type: 'GET'
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
admin.popup({
|
||||
title: '编辑 - ' + data.name
|
||||
,
|
||||
area: ['80%', '80%']
|
||||
,
|
||||
id: 'cron-log'
|
||||
,
|
||||
content: '任务名 <div class="layui-input-inline" style="width: 190px;"><input type="text" name="cron-edit-name" placeholder="请输入任务名称" autocomplete="off" class="layui-input" value="' + data.name + '"></div> 执行周期 <div class="layui-input-inline" style="width: 190px;"><input id="cron-edit-time-' + data.id + '" type="text" name="cron-edit-time" placeholder="请输入执行周期" autocomplete="off" class="layui-input" value="' + data.time + '"/></div><hr><div id="cron-edit-script-editor" style="height: 80%;">' + res.data + '</div><br><button id="cron-edit-' + data.id + '" class="layui-btn">保存</button>'
|
||||
,
|
||||
success: function (layero, index) {
|
||||
cronEditScriptEditor = ace.edit("cron-edit-script-editor", {
|
||||
mode: "ace/mode/sh",
|
||||
selectionStyle: "text"
|
||||
});
|
||||
cron.render({
|
||||
elem: "#cron-edit-time-" + data.id,
|
||||
btns: ['confirm'],
|
||||
show: false,
|
||||
done: function (value) {
|
||||
$('#cron-add-time').val(value);
|
||||
}
|
||||
});
|
||||
$('#cron-edit-' + data.id).click(function () {
|
||||
index2 = layer.msg('正在保存...', {icon: 16, time: 0, shade: 0.3});
|
||||
admin.req({
|
||||
url: '/api/panel/cron/update'
|
||||
, type: 'POST'
|
||||
, data: {
|
||||
id: data.id,
|
||||
name: $('input[name="cron-edit-name"]').val(),
|
||||
time: $('input[name="cron-edit-time"]').val(),
|
||||
script: cronEditScriptEditor.getValue()
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('保存成功', {icon: 1, time: 1000});
|
||||
table.reload('panel-cron');
|
||||
layer.close(index2);
|
||||
layer.close(index);
|
||||
} else {
|
||||
layer.msg(res.message, {icon: 2, time: 1000});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.message, {icon: 2, time: 1000});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} else if (obj.event === 'del') {
|
||||
layer.confirm('确定删除计划任务' + data.name + '吗?', function (index) {
|
||||
layer.close(index);
|
||||
admin.req({
|
||||
url: '/api/panel/cron/delete',
|
||||
type: 'POST',
|
||||
data: {
|
||||
id: data.id
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
table.reload('panel-cron');
|
||||
layer.msg('计划任务:' + data.name + ' 已删除', {
|
||||
icon: 1,
|
||||
time: 1000
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.message, {icon: 2, time: 1000});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
form.on('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.render();
|
||||
});
|
||||
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.render();
|
||||
});
|
||||
|
||||
form.on('switch(cron-status)', function (obj) {
|
||||
let $ = layui.$;
|
||||
let id = $(this).data('id');
|
||||
let status = obj.elem.checked;
|
||||
|
||||
admin.req({
|
||||
url: '/api/panel/cron/status',
|
||||
type: 'POST',
|
||||
data: {
|
||||
id: id,
|
||||
status: status
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('设置成功', {icon: 1, time: 1000});
|
||||
} else {
|
||||
layer.msg(res.message, {icon: 2, time: 1000});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
form.on('submit(cron-add-submit)', function (data) {
|
||||
data.field.script = cronAddScriptEditor.getValue();
|
||||
admin.req({
|
||||
url: "/api/panel/cron/add"
|
||||
, type: 'post'
|
||||
, data: data.field
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
layer.msg('计划任务添加失败!')
|
||||
return false;
|
||||
}
|
||||
table.reload('panel-cron');
|
||||
layer.alert('计划任务添加成功!', {
|
||||
icon: 1
|
||||
, title: '提示'
|
||||
, btn: ['确定']
|
||||
, yes: function (index) {
|
||||
layer.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
+177
-151
@@ -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,170 +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'
|
||||
, method: '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')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
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')
|
||||
})
|
||||
|
||||
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'
|
||||
, method: '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 + ' 天')
|
||||
}
|
||||
})
|
||||
})
|
||||
$('#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}
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
} 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'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
// 监听更新按钮点击事件
|
||||
$('#update_panel').click(function () {
|
||||
index = layer.msg('正在获取版本信息...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
})
|
||||
admin.req(
|
||||
{
|
||||
url: '/api/panel/info/checkUpdate'
|
||||
, method: 'get'
|
||||
, success: function (result) {
|
||||
layer.close(index)
|
||||
if (result.code !== 0) {
|
||||
layer.msg('获取版本信息失败,请刷新重试!')
|
||||
return false
|
||||
}
|
||||
if (result.data.version) {
|
||||
admin.popup({
|
||||
title: '提示'
|
||||
,
|
||||
shade: 0
|
||||
,
|
||||
anim: -1
|
||||
,
|
||||
area: ['400px', '200px']
|
||||
,
|
||||
id: 'layadmin-layer-skin-update-panel'
|
||||
,
|
||||
skin: 'layui-anim layui-anim-upbit'
|
||||
,
|
||||
content: '最新版本:' + result.data.version + '<br><br>更新日志:' + result.data.describe + '<br><br>请在SSH执行<span class="layui-badge-rim">panel update</span>以更新面板!'
|
||||
})
|
||||
} else {
|
||||
layer.msg('当前已是最新版本!')
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script type="text/html" template>
|
||||
<link rel="stylesheet" href="/panel/adminui/src/css/login.css?v={{ layui.admin.v }}" media="all">
|
||||
<link rel="stylesheet" href="panel/adminui/src/css/login.css?v={{ layui.admin.v }}" media="all">
|
||||
</script>
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
placeholder="密码" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-form-item" style="margin-bottom: 20px;">
|
||||
<a target="_blank" href="https://jq.qq.com/?_wv=1027&k=I1oJKSTH" class="layadmin-user-jump-change layadmin-link"
|
||||
<a target="_blank" href="https://jq.qq.com/?_wv=1027&k=I1oJKSTH"
|
||||
class="layadmin-user-jump-change layadmin-link"
|
||||
style="margin-top: 7px;">忘记密码?</a>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
@@ -41,55 +42,59 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['admin', 'form'], function () {
|
||||
let $ = layui.$
|
||||
, setter = layui.setter
|
||||
, admin = layui.admin
|
||||
, form = layui.form
|
||||
, router = layui.router()
|
||||
, search = router.search;
|
||||
layui.use(['admin', 'form'], function () {
|
||||
let $ = layui.$
|
||||
, setter = layui.setter
|
||||
, admin = layui.admin
|
||||
, form = layui.form
|
||||
, router = layui.router()
|
||||
, search = router.search;
|
||||
|
||||
// 设置面板名称
|
||||
$('#panel-login-name').text(setter.name);
|
||||
|
||||
form.render();
|
||||
|
||||
//提交
|
||||
form.on('submit(panel-login-submit)', function (obj) {
|
||||
|
||||
// 判断obj.field.remember是否存在
|
||||
if (obj.field.remember) {
|
||||
obj.field.remember = 1;
|
||||
} else {
|
||||
obj.field.remember = 0;
|
||||
}
|
||||
admin.req({
|
||||
url: '/api/panel/user/login'
|
||||
, data: obj.field
|
||||
, method: 'post'
|
||||
, done: function (res) {
|
||||
// 请求成功后,写入 access_token
|
||||
layui.data(setter.tableName, {
|
||||
key: setter.request.tokenName
|
||||
, value: res.data.access_token
|
||||
});
|
||||
|
||||
// 登入成功的提示与跳转
|
||||
layer.msg('登录成功', {
|
||||
offset: '15px'
|
||||
, icon: 1
|
||||
, time: 1000
|
||||
}, function () {
|
||||
/**
|
||||
* 可能是LayuiAdmin的bug,如果直接跳转到主页,会出现表格无法获取access_token的问题
|
||||
* 所以这里强制刷新到主页
|
||||
* location.hash = search.redirect ? decodeURIComponent(search.redirect) : '/';
|
||||
*/
|
||||
location.href = '/';
|
||||
});
|
||||
// 判断是否已登录
|
||||
if (layui.data(setter.tableName)[setter.request.tokenName]) {
|
||||
location.hash = search.redirect ? decodeURIComponent(search.redirect) : '/';
|
||||
}
|
||||
});
|
||||
|
||||
// 设置面板名称
|
||||
$('#panel-login-name').text(setter.name);
|
||||
|
||||
form.render();
|
||||
|
||||
//提交
|
||||
form.on('submit(panel-login-submit)', function (obj) {
|
||||
|
||||
// 判断obj.field.remember是否存在
|
||||
if (obj.field.remember) {
|
||||
obj.field.remember = 1;
|
||||
} else {
|
||||
obj.field.remember = 0;
|
||||
}
|
||||
admin.req({
|
||||
url: '/api/panel/user/login'
|
||||
, data: obj.field
|
||||
, type: 'post'
|
||||
, done: function (res) {
|
||||
// 请求成功后,写入 access_token
|
||||
layui.data(setter.tableName, {
|
||||
key: setter.request.tokenName
|
||||
, value: res.data.access_token
|
||||
});
|
||||
|
||||
// 登入成功的提示与跳转
|
||||
layer.msg('登录成功', {
|
||||
offset: '15px'
|
||||
, icon: 1
|
||||
, time: 1000
|
||||
}, function () {
|
||||
/**
|
||||
* 可能是LayuiAdmin的bug,如果直接跳转到主页,会出现表格无法获取access_token的问题
|
||||
* 所以这里强制刷新一次页面
|
||||
*/
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script type="text/html" template>
|
||||
<link rel="stylesheet" href="/panel/adminui/src/css/login.css?v={{ layui.admin.v }}" media="all">
|
||||
<link rel="stylesheet" href="panel/adminui/src/css/login.css?v={{ layui.admin.v }}" media="all">
|
||||
</script>
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['admin'], function () {
|
||||
let admin = layui.admin
|
||||
setTimeout(function () {
|
||||
admin.exit()
|
||||
window.location = '/'
|
||||
}, 3000)
|
||||
})
|
||||
layui.use(['admin'], function () {
|
||||
let admin = layui.admin
|
||||
setTimeout(function () {
|
||||
admin.exit()
|
||||
location.hash = '/login'
|
||||
}, 3000)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
<!--
|
||||
Name: 资源监控
|
||||
Date: 2023-08-13
|
||||
-->
|
||||
<title>资源监控</title>
|
||||
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-card">
|
||||
<div class="layui-form layui-card-body" style="overflow: hidden;">
|
||||
<div class="layui-inline">
|
||||
<span style="margin-right: 10px;">开启监控</span><input type="checkbox" id="monitor-switch"
|
||||
lay-filter="monitor" lay-skin="switch"
|
||||
lay-text="ON|OFF">
|
||||
<span style="margin-left: 40px; margin-right: 10px;">保存天数</span>
|
||||
<div class="layui-input-inline"><input type="number" name="monitor-save-days" class="layui-input"
|
||||
style="height: 30px; margin-top: 5px;" min=0 max=30 disabled>
|
||||
</div>
|
||||
<div class="layui-input-inline">
|
||||
<button id="save_monitor_date" class="layui-btn layui-btn-sm" style="margin-left: 10px;">确定
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-inline" style="float: right;">
|
||||
<span style="margin-right: 10px;">时间选择</span>
|
||||
<div id="monitor-date" class="layui-input-inline">
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" autocomplete="off" id="monitor-startDate"
|
||||
class="layui-input"
|
||||
placeholder="开始时间">
|
||||
</div>
|
||||
-
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" autocomplete="off" id="monitor-endDate"
|
||||
class="layui-input"
|
||||
placeholder="结束时间">
|
||||
</div>
|
||||
</div>
|
||||
<span style="margin-left: 40px;"></span>
|
||||
<button id="clear_monitor_record" class="layui-btn layui-btn-sm layui-btn-danger">清空监控记录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-row layui-col-space10">
|
||||
<div class="layui-col-xs12 layui-col-md6">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
<span>负载</span>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<div id="load_monitor" style="width: 100%;height: 400px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-col-xs12 layui-col-md6">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
<span>CPU</span>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<div id="cpu_monitor" style="width: 100%;height: 400px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-row layui-col-space10">
|
||||
<div class="layui-col-xs12 layui-col-md6">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
<span>内存</span>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<div id="memory_monitor" style="width: 100%;height: 400px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-xs12 layui-col-md6">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">
|
||||
<span>网络</span>
|
||||
</div>
|
||||
<div class="layui-card-body">
|
||||
<div id="network_monitor" style="width: 100%;height: 400px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
layui.use(['admin', 'view', 'form', 'element', 'carousel'], function () {
|
||||
var admin = layui.admin;
|
||||
var $ = layui.jquery;
|
||||
var form = layui.form;
|
||||
var laydate = layui.laydate;
|
||||
|
||||
// 默认开始时间为今天0点,结束时间为当前时间
|
||||
var startDate = new Date(new Date().setHours(0, 0, 0, 0));
|
||||
var endDate = new Date();
|
||||
// 拼接时间字符串
|
||||
var startStr = startDate.getFullYear() + '-' + startDate.getMonth() + '-' + startDate.getDate() + ' ' + startDate.getHours() + ':' + startDate.getMinutes() + ':' + startDate.getSeconds();
|
||||
var endStr = endDate.getFullYear() + '-' + endDate.getMonth() + '-' + endDate.getDate() + ' ' + endDate.getHours() + ':' + endDate.getMinutes() + ':' + endDate.getSeconds();
|
||||
laydate.render({
|
||||
elem: '#monitor-date'
|
||||
, type: 'datetime'
|
||||
, range: ['#monitor-startDate', '#monitor-endDate']
|
||||
, value: startStr + ' - ' + endStr
|
||||
, calendar: true
|
||||
, done: function (value, date, endDate) {
|
||||
let start_date,
|
||||
end_date;
|
||||
// 初始化时间日期对象
|
||||
start_date = new Date(date.year, date.month, date.date, date.hours, date.minutes, date.seconds);
|
||||
end_date = new Date(endDate.year, endDate.month, endDate.date, endDate.hours, endDate.minutes, endDate.seconds);
|
||||
getMonitorData(start_date.getTime(), end_date.getTime());
|
||||
}
|
||||
});
|
||||
|
||||
// 获取监控开关和保存天数
|
||||
admin.req({
|
||||
url: '/api/panel/monitor/switchAndDays',
|
||||
type: 'get',
|
||||
dataType: 'json',
|
||||
success: function (res) {
|
||||
if (res.code === 0) {
|
||||
if (res.data.switch) {
|
||||
$('#monitor-switch').attr('checked', true);
|
||||
} else {
|
||||
$('#monitor-switch').attr('checked', false);
|
||||
}
|
||||
$('input[name="monitor-save-days"]').val(res.data.days);
|
||||
$('input[name="monitor-save-days"]').removeAttr('disabled');
|
||||
form.render();
|
||||
}
|
||||
}
|
||||
});
|
||||
form.on('switch(monitor)', function (data) {
|
||||
admin.req({
|
||||
url: '/api/panel/monitor/switch',
|
||||
type: 'post',
|
||||
dataType: 'json',
|
||||
data: {switch: data.elem.checked},
|
||||
success: function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('修改成功', {icon: 1});
|
||||
} else {
|
||||
layer.msg(res.message, {icon: 2});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#save_monitor_date').click(function () {
|
||||
var days = $('input[name="monitor-save-days"]').val();
|
||||
if (days == '') {
|
||||
layer.msg('请输入保存天数', {icon: 2});
|
||||
return false;
|
||||
}
|
||||
admin.req({
|
||||
url: '/api/panel/monitor/saveDays',
|
||||
type: 'post',
|
||||
dataType: 'json',
|
||||
data: {days: days},
|
||||
success: function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('修改成功', {icon: 1});
|
||||
} else {
|
||||
layer.msg(res.message, {icon: 2});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#clear_monitor_record').click(function () {
|
||||
layer.confirm('确定要清除监控数据吗?', function (index) {
|
||||
admin.req({
|
||||
url: '/api/panel/monitor/clear',
|
||||
type: 'post',
|
||||
dataType: 'json',
|
||||
success: function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('清除成功', {icon: 1});
|
||||
setTimeout(function () {
|
||||
admin.render();
|
||||
}, 1000);
|
||||
} else {
|
||||
layer.msg(res.message, {icon: 2});
|
||||
}
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
});
|
||||
|
||||
getMonitorData(startDate.getTime(), endDate.getTime());
|
||||
});
|
||||
|
||||
// 获取监控数据
|
||||
function getMonitorData(startDate, endDate) {
|
||||
layui.use(['admin'], function () {
|
||||
let admin = layui.admin;
|
||||
admin.req({
|
||||
url: '/api/panel/monitor/list?start=' + startDate + '&end=' + endDate,
|
||||
type: 'get',
|
||||
dataType: 'json',
|
||||
success: function (res) {
|
||||
if (res.code !== 0) {
|
||||
layer.msg(res.message, {icon: 2, shade: 0.3});
|
||||
return false;
|
||||
}
|
||||
let loadChart = renderEcharts('load_monitor', '负载', {
|
||||
x: 'left',
|
||||
data: ["1分钟", "5分钟", "15分钟"]
|
||||
}, res.data.times, [{
|
||||
name: '1分钟',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: res.data.load.load1,
|
||||
markPoint: {
|
||||
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
|
||||
},
|
||||
markLine: {
|
||||
data: [{type: 'average', name: '平均值'}]
|
||||
}
|
||||
}, {
|
||||
name: '5分钟',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: res.data.load.load5,
|
||||
markPoint: {
|
||||
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
|
||||
},
|
||||
markLine: {
|
||||
data: [{type: 'average', name: '平均值'}]
|
||||
}
|
||||
}, {
|
||||
name: '15分钟',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: res.data.load.load15,
|
||||
markPoint: {
|
||||
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
|
||||
},
|
||||
markLine: {
|
||||
data: [{type: 'average', name: '平均值'}]
|
||||
}
|
||||
}], [{
|
||||
type: 'value',
|
||||
}]);
|
||||
let cpuChart = renderEcharts('cpu_monitor', 'CPU', undefined, res.data.times, [{
|
||||
name: '使用率',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: res.data.cpu.percent,
|
||||
markPoint: {
|
||||
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
|
||||
},
|
||||
markLine: {
|
||||
data: [{type: 'average', name: '平均值'}]
|
||||
}
|
||||
}], [{
|
||||
name: '单位 %',
|
||||
min: 0,
|
||||
max: 100,
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value} %'
|
||||
}
|
||||
}]);
|
||||
let memoryChart = renderEcharts('memory_monitor', '内存', {
|
||||
x: 'left',
|
||||
data: ["内存", "Swap"]
|
||||
}, res.data.times, [{
|
||||
name: '内存',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: res.data.mem.used,
|
||||
markPoint: {
|
||||
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
|
||||
},
|
||||
markLine: {
|
||||
data: [{type: 'average', name: '平均值'}]
|
||||
}
|
||||
}, {
|
||||
name: 'Swap',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: res.data.swap.used,
|
||||
markPoint: {
|
||||
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
|
||||
},
|
||||
markLine: {
|
||||
data: [{type: 'average', name: '平均值'}]
|
||||
}
|
||||
}], [{
|
||||
name: '单位 MB',
|
||||
min: 0,
|
||||
max: res.data.mem.total,
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value} M'
|
||||
}
|
||||
}]);
|
||||
let networkChart = renderEcharts('network_monitor', '网络', {
|
||||
x: 'left',
|
||||
data: ["总计出", "总计入", "每秒出", "每秒入"]
|
||||
}, res.data.times, [{
|
||||
name: '总计出',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: res.data.net.sent,
|
||||
markPoint: {
|
||||
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
|
||||
},
|
||||
markLine: {
|
||||
data: [{type: 'average', name: '平均值'}]
|
||||
}
|
||||
}, {
|
||||
name: '总计入',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: res.data.net.recv,
|
||||
markPoint: {
|
||||
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
|
||||
},
|
||||
markLine: {
|
||||
data: [{type: 'average', name: '平均值'}]
|
||||
}
|
||||
}, {
|
||||
name: '每秒出',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: res.data.net.tx,
|
||||
markPoint: {
|
||||
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
|
||||
},
|
||||
markLine: {
|
||||
data: [{type: 'average', name: '平均值'}]
|
||||
}
|
||||
}, {
|
||||
name: '每秒入',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {normal: {areaStyle: {type: 'default'}}},
|
||||
data: res.data.net.rx,
|
||||
markPoint: {
|
||||
data: [{type: 'max', name: '最大值'}, {type: 'min', name: '最小值'}]
|
||||
},
|
||||
markLine: {
|
||||
data: [{type: 'average', name: '平均值'}]
|
||||
}
|
||||
}], [{
|
||||
name: '单位 Mb',
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value} Mb'
|
||||
}
|
||||
}]);
|
||||
|
||||
// 在窗口大小改变时,重置图表大小
|
||||
window.addEventListener("resize", function () {
|
||||
loadChart.resize();
|
||||
cpuChart.resize();
|
||||
memoryChart.resize();
|
||||
networkChart.resize();
|
||||
});
|
||||
}, error: function (xhr, status, error) {
|
||||
console.log('耗子Linux面板:ajax请求出错,错误' + error);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染图表
|
||||
function renderEcharts(element_id, title, legend = undefined, data_xAxis, series, yAxis = undefined) {
|
||||
var Chart = echarts.init(document.getElementById(element_id), layui.echartsTheme);
|
||||
var option = {
|
||||
title: {text: title, x: 'center', textStyle: {fontSize: 20}},
|
||||
tooltip: {trigger: 'axis'},
|
||||
legend: legend,
|
||||
xAxis: [{type: 'category', boundaryGap: false, data: data_xAxis}],
|
||||
yAxis: yAxis,
|
||||
dataZoom: {
|
||||
show: true,
|
||||
realtime: true,
|
||||
start: 0,
|
||||
end: 100
|
||||
},
|
||||
series: series
|
||||
};
|
||||
|
||||
Chart.setOption(option);
|
||||
return Chart;
|
||||
}
|
||||
</script>
|
||||
+138
-137
@@ -35,150 +35,151 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['admin', 'table', 'jquery'], function () {
|
||||
var $ = layui.$
|
||||
, form = layui.form
|
||||
, table = layui.table
|
||||
, admin = layui.admin
|
||||
layui.use(['admin', 'table', 'jquery'], function () {
|
||||
var $ = layui.$
|
||||
, form = layui.form
|
||||
, table = layui.table
|
||||
, admin = layui.admin
|
||||
|
||||
table.render({
|
||||
elem: '#panel-plugin'
|
||||
, url: '/api/panel/plugin/list'
|
||||
, cols: [[
|
||||
{ field: 'slug', hide: true, title: 'Slug' }
|
||||
, { field: 'name', width: 150, title: '插件名', sort: true }
|
||||
, { field: 'description', title: '描述' }
|
||||
, { field: 'author', width: 100, title: '作者' }
|
||||
, { field: 'installed_version', width: 140, title: '已装版本' }
|
||||
, { field: 'version', width: 140, title: '最新版本' }
|
||||
, { field: 'show', title: '首页显示', width: 90, templet: '#plugin-show', unresize: true }
|
||||
, {
|
||||
field: 'control',
|
||||
width: 180,
|
||||
title: '操作',
|
||||
templet: '#panel-plugin-control',
|
||||
fixed: 'right',
|
||||
align: 'left'
|
||||
}
|
||||
]]
|
||||
, page: false
|
||||
, text: '耗子Linux面板:数据加载出现异常!'
|
||||
, done: function () {
|
||||
//element.render('progress');
|
||||
}
|
||||
})
|
||||
|
||||
// 工具条
|
||||
table.on('tool(panel-plugin)', function (obj) {
|
||||
let data = obj.data
|
||||
if (obj.event === 'open') {
|
||||
location.hash = '/plugins/' + data.slug
|
||||
} else if (obj.event === 'install') {
|
||||
layer.confirm('确定安装该插件吗?', function (index) {
|
||||
layer.close(index)
|
||||
admin.req({
|
||||
url: '/api/panel/plugin/install',
|
||||
type: 'POST',
|
||||
data: {
|
||||
slug: data.slug
|
||||
table.render({
|
||||
elem: '#panel-plugin'
|
||||
, url: '/api/panel/plugin/list'
|
||||
, cols: [[
|
||||
{field: 'slug', hide: true, title: 'Slug'}
|
||||
, {field: 'name', width: 150, title: '插件名', sort: true}
|
||||
, {field: 'description', title: '描述'}
|
||||
, {field: 'installed_version', width: 140, title: '已装版本'}
|
||||
, {field: 'version', width: 140, title: '最新版本'}
|
||||
, {field: 'show', title: '首页显示', width: 90, templet: '#plugin-show', unresize: true}
|
||||
, {
|
||||
field: 'control',
|
||||
width: 180,
|
||||
title: '操作',
|
||||
templet: '#panel-plugin-control',
|
||||
fixed: 'right',
|
||||
align: 'left'
|
||||
}
|
||||
]]
|
||||
, page: false
|
||||
, text: {
|
||||
none: '无数据'
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
table.reload('panel-plugin')
|
||||
layer.msg('安装:' + data.name + ' 成功加入任务队列', {
|
||||
icon: 1,
|
||||
time: 1000
|
||||
})
|
||||
} else {
|
||||
layer.msg(res.msg, { icon: 2, time: 1000 })
|
||||
}
|
||||
, done: function () {
|
||||
//element.render('progress');
|
||||
}
|
||||
})
|
||||
})
|
||||
} else if (obj.event === 'uninstall') {
|
||||
layer.confirm('确定卸载该插件吗?', function (index) {
|
||||
layer.close(index)
|
||||
admin.req({
|
||||
url: '/api/panel/plugin/uninstall',
|
||||
type: 'POST',
|
||||
data: {
|
||||
slug: data.slug
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
table.reload('panel-plugin')
|
||||
layer.msg('卸载:' + data.name + ' 成功加入任务队列', {
|
||||
icon: 1,
|
||||
time: 1000
|
||||
|
||||
// 工具条
|
||||
table.on('tool(panel-plugin)', function (obj) {
|
||||
let data = obj.data
|
||||
if (obj.event === 'open') {
|
||||
location.hash = '/plugins/' + data.slug
|
||||
} else if (obj.event === 'install') {
|
||||
layer.confirm('确定安装该插件吗?', function (index) {
|
||||
layer.close(index)
|
||||
admin.req({
|
||||
url: '/api/panel/plugin/install',
|
||||
type: 'POST',
|
||||
data: {
|
||||
slug: data.slug
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
table.reload('panel-plugin')
|
||||
layer.msg('安装:' + data.name + ' 成功加入任务队列', {
|
||||
icon: 1,
|
||||
time: 1000
|
||||
})
|
||||
} else {
|
||||
layer.msg(res.message, {icon: 2, time: 1000})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
layer.msg(res.msg, { icon: 2, time: 1000 })
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
} else if (obj.event === 'update') {
|
||||
layer.confirm('确定升级该插件吗?', function (index) {
|
||||
layer.close(index)
|
||||
admin.req({
|
||||
url: '/api/panel/plugin/update',
|
||||
type: 'POST',
|
||||
data: {
|
||||
slug: data.slug
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
table.reload('panel-plugin')
|
||||
layer.msg('升级:' + data.name + ' 成功加入任务队列', {
|
||||
icon: 1,
|
||||
time: 1000
|
||||
} else if (obj.event === 'uninstall') {
|
||||
layer.confirm('确定卸载该插件吗?', function (index) {
|
||||
layer.close(index)
|
||||
admin.req({
|
||||
url: '/api/panel/plugin/uninstall',
|
||||
type: 'POST',
|
||||
data: {
|
||||
slug: data.slug
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
table.reload('panel-plugin')
|
||||
layer.msg('卸载:' + data.name + ' 成功加入任务队列', {
|
||||
icon: 1,
|
||||
time: 1000
|
||||
})
|
||||
} else {
|
||||
layer.msg(res.message, {icon: 2, time: 1000})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
} else if (obj.event === 'update') {
|
||||
layer.confirm('确定升级该插件吗?', function (index) {
|
||||
layer.close(index)
|
||||
admin.req({
|
||||
url: '/api/panel/plugin/update',
|
||||
type: 'POST',
|
||||
data: {
|
||||
slug: data.slug
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
table.reload('panel-plugin')
|
||||
layer.msg('升级:' + data.name + ' 成功加入任务队列', {
|
||||
icon: 1,
|
||||
time: 1000
|
||||
})
|
||||
} else {
|
||||
layer.msg(res.message, {icon: 2, time: 1000})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
layer.msg(res.msg, { icon: 2, time: 1000 })
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
form.on('switch(plugin-show-home)', function (obj) {
|
||||
let $ = layui.$
|
||||
let plugin_slug = $(this).data('plugin-slug')
|
||||
let show = obj.elem.checked ? 1 : 0
|
||||
|
||||
admin.req({
|
||||
url: '/api/panel/plugin/updateShow',
|
||||
type: 'POST',
|
||||
data: {
|
||||
slug: plugin_slug,
|
||||
show: show
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('设置成功', {icon: 1, time: 1000})
|
||||
} else {
|
||||
// 还原开关状态
|
||||
obj.elem.checked = !obj.elem.checked
|
||||
form.render('checkbox')
|
||||
layer.msg(res.message, {icon: 2, time: 1000})
|
||||
}
|
||||
}
|
||||
, error: function (xhr, status, error) {
|
||||
console.log('耗子Linux面板:ajax请求出错,错误' + error)
|
||||
}
|
||||
})
|
||||
})
|
||||
/*form.render(null, 'plugin-form');
|
||||
|
||||
//搜索
|
||||
form.on('submit(plugin-search-submit)', function (data) {
|
||||
var field = data.field;
|
||||
|
||||
//执行重载
|
||||
table.reload('plugin-search-submit', {
|
||||
where: field
|
||||
});
|
||||
});*/
|
||||
})
|
||||
|
||||
form.on('switch(plugin-show-home)', function (obj) {
|
||||
let $ = layui.$
|
||||
let plugin_slug = $(this).data('plugin-slug')
|
||||
let show = obj.elem.checked ? 1 : 0
|
||||
|
||||
admin.req({
|
||||
url: '/api/panel/plugin/updateShow',
|
||||
type: 'POST',
|
||||
data: {
|
||||
slug: plugin_slug,
|
||||
show: show
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.msg('设置成功', { icon: 1, time: 1000 })
|
||||
} else {
|
||||
// 还原开关状态
|
||||
obj.elem.checked = !obj.elem.checked
|
||||
form.render('checkbox')
|
||||
layer.msg(res.msg, { icon: 2, time: 1000 })
|
||||
}
|
||||
}
|
||||
, error: function (xhr, status, error) {
|
||||
console.log('耗子Linux面板:ajax请求出错,错误' + error)
|
||||
}
|
||||
})
|
||||
})
|
||||
/*form.render(null, 'plugin-form');
|
||||
|
||||
//搜索
|
||||
form.on('submit(plugin-search-submit)', function (data) {
|
||||
var field = data.field;
|
||||
|
||||
//执行重载
|
||||
table.reload('plugin-search-submit', {
|
||||
where: field
|
||||
});
|
||||
});*/
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
<!--
|
||||
Name: Fail2ban管理器
|
||||
Author: 耗子
|
||||
Date: 2023-07-30
|
||||
-->
|
||||
<title>Fail2ban</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">Fail2ban 运行状态</div>
|
||||
<div class="layui-card-body">
|
||||
<blockquote id="fail2ban-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="fail2ban-start" class="layui-btn">启动</button>
|
||||
<button id="fail2ban-stop" class="layui-btn layui-btn-danger">停止</button>
|
||||
<button id="fail2ban-restart" class="layui-btn layui-btn-warm">重启</button>
|
||||
<button id="fail2ban-reload" class="layui-btn layui-btn-normal">重载</button>
|
||||
</div>
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
|
||||
<legend>基本设置</legend>
|
||||
</fieldset>
|
||||
<div class="layui-form" lay-filter="fail2ban_setting">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="font-size: 13px;">IP白名单</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="fail2ban_white_list" value="获取中ing..." class="layui-input"
|
||||
disabled>
|
||||
</div>
|
||||
<div class="layui-form-mid layui-word-aux">IP白名单,以英文逗号,分隔</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn layui-btn-sm" lay-submit lay-filter="fail2ban_setting_submit">
|
||||
确认修改
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">Fail2ban 规则列表</div>
|
||||
<div class="layui-card-body">
|
||||
<table class="layui-hide" id="fail2ban-rule-list" lay-filter="fail2ban-rule-list"></table>
|
||||
<!-- 顶部工具栏 -->
|
||||
<script type="text/html" id="fail2ban-rule-list-bar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="add_rule">新建规则</button>
|
||||
</div>
|
||||
</script>
|
||||
<!-- 右侧管理 -->
|
||||
<script type="text/html" id="fail2ban-rule-list-control">
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="view">查看</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
layui.use(['index', 'form', 'table', 'view'], function () {
|
||||
let $ = layui.$
|
||||
, admin = layui.admin
|
||||
, table = layui.table
|
||||
, form = layui.form
|
||||
, view = layui.view;
|
||||
|
||||
// 获取运行状态并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/fail2ban/status"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
if (result.data) {
|
||||
$('#fail2ban-status').html('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
|
||||
} else {
|
||||
$('#fail2ban-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 获取白名单并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/fail2ban/whiteList"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
$('input[name=fail2ban_white_list]').val(result.data);
|
||||
$('input').attr('disabled', false);
|
||||
form.render();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听提交
|
||||
form.on('submit(fail2ban_setting_submit)', function (data) {
|
||||
data.field.ip = $('input[name=fail2ban_white_list]').val();
|
||||
index = layer.msg('请稍候...', {icon: 16, time: 0, shade: 0.3});
|
||||
admin.req({
|
||||
url: "/api/plugins/fail2ban/whiteList"
|
||||
, type: 'post'
|
||||
, data: data.field
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
layer.msg('设置成功', {icon: 1});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
let websiteList = [];
|
||||
admin.req({
|
||||
url: '/api/panel/website/list?page=1&limit=10000'
|
||||
, type: 'get'
|
||||
, async: false
|
||||
, success: function (res) {
|
||||
websiteList = res.data.items;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取规则列表
|
||||
table.render({
|
||||
elem: '#fail2ban-rule-list'
|
||||
, url: '/api/plugins/fail2ban/list'
|
||||
, toolbar: '#fail2ban-rule-list-bar'
|
||||
, title: 'Fail2ban 规则列表'
|
||||
, cols: [[
|
||||
{field: 'name', title: '规则名', fixed: 'left', unresize: true, sort: true}
|
||||
, {
|
||||
field: 'enabled', title: '状态', templet: function (d) {
|
||||
if (d.enabled) {
|
||||
return '<span class="layui-badge layui-bg-green">启用</span>';
|
||||
} else {
|
||||
return '<span class="layui-badge layui-bg-gray">禁用</span>';
|
||||
}
|
||||
}, sort: true
|
||||
}
|
||||
, {field: 'max_retry', title: '最大尝试次数', sort: true}
|
||||
, {field: 'ban_time', title: '封禁时间', sort: true}
|
||||
, {field: 'find_time', title: '查找时间', sort: true}
|
||||
, {field: 'log_path', title: '日志路径'}
|
||||
, {fixed: 'right', title: '操作', toolbar: '#fail2ban-rule-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(fail2ban-rule-list)', function (obj) {
|
||||
if (obj.event === 'add_rule') {
|
||||
index = layer.msg('加载中...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
, shade: 0.3
|
||||
});
|
||||
admin.popup({
|
||||
title: '新建Fail2ban规则'
|
||||
, area: ['600px', '600px']
|
||||
, id: 'LAY-popup-fail2ban-rule-add'
|
||||
, success: function () {
|
||||
layer.close(index);
|
||||
view(this.id).render('plugins/fail2ban/add_rule', {
|
||||
websiteList: websiteList
|
||||
}).done(function () {
|
||||
form.render(null, 'LAY-popup-fail2ban-rule-add');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// 行工具事件
|
||||
table.on('tool(fail2ban-rule-list)', function (obj) {
|
||||
let data = obj.data;
|
||||
if (obj.event === 'del') {
|
||||
layer.confirm('确定要删除 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
|
||||
index = layer.msg('请稍等...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
, shade: 0.3
|
||||
});
|
||||
admin.req({
|
||||
url: "/api/plugins/fail2ban/delete"
|
||||
, type: 'post'
|
||||
, data: data
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
obj.del();
|
||||
layer.alert(data.name + '删除成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (obj.event === 'view') {
|
||||
index = layer.msg('加载中...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
, shade: 0.3
|
||||
});
|
||||
admin.req({
|
||||
url: "/api/plugins/fail2ban/ban?name=" + data.name
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.popup({
|
||||
title: '查看Fail2ban规则'
|
||||
, area: ['600px', '600px']
|
||||
, id: 'LAY-popup-fail2ban-rule-view'
|
||||
, success: function () {
|
||||
layer.close(index);
|
||||
view(this.id).render('plugins/fail2ban/view_rule', {
|
||||
data: data
|
||||
, banList: result.data
|
||||
}).done(function () {
|
||||
form.render(null, 'LAY-popup-fail2ban-rule-view');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#fail2ban-start').click(function () {
|
||||
layer.confirm('确定要启动Fail2ban吗?', {
|
||||
btn: ['启动', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('请稍等...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
, shade: 0.3
|
||||
});
|
||||
admin.req({
|
||||
url: "/api/plugins/fail2ban/start"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('Fail2ban启动成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#fail2ban-stop').click(function () {
|
||||
layer.confirm('停止Fail2ban将导致Fail2ban防护失效,是否继续停止?', {
|
||||
btn: ['停止', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('请稍等...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
, shade: 0.3
|
||||
});
|
||||
admin.req({
|
||||
url: "/api/plugins/fail2ban/stop"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('Fail2ban停止成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#fail2ban-restart').click(function () {
|
||||
layer.confirm('确定要重启Fail2ban吗?', {
|
||||
btn: ['重启', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('请稍等...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
, shade: 0.3
|
||||
});
|
||||
admin.req({
|
||||
url: "/api/plugins/fail2ban/restart"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('Fail2ban重启成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#fail2ban-reload').click(function () {
|
||||
index = layer.msg('请稍等...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
, shade: 0.3
|
||||
});
|
||||
admin.req({
|
||||
url: "/api/plugins/fail2ban/reload"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
layer.alert('Fail2ban重载成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,170 @@
|
||||
<!--
|
||||
Name: Fail2ban管理器 - 新建规则
|
||||
Author: 耗子
|
||||
Date: 2023-07-30
|
||||
-->
|
||||
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
|
||||
<form class="layui-form" action="" lay-filter="add-pure-ftpd-user-form">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">类型</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" lay-filter="add-fail2ban-rule-type-radio" name="type" value="website"
|
||||
title="网站" checked="">
|
||||
<input type="radio" lay-filter="add-fail2ban-rule-type-radio" name="type" value="service"
|
||||
title="服务">
|
||||
</div>
|
||||
</div>
|
||||
<div id="add-fail2ban-rule-website-input" class="layui-form-item">
|
||||
<label class="layui-form-label">网站</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="website" lay-filter="add-fail2ban-rule-website">
|
||||
{{# layui.each(d.params.websiteList, function(index, item){ }}
|
||||
{{# if(index == 0){ }}
|
||||
<option value="{{ item.name }}" selected="">{{ item.name }}</option>
|
||||
{{# }else{ }}
|
||||
<option value="{{ item.name }}">{{ item.name }}</option>
|
||||
{{# } }}
|
||||
{{# }); }}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="add-fail2ban-rule-service-input" class="layui-form-item">
|
||||
<label class="layui-form-label">服务</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="service" lay-filter="add-fail2ban-rule-service">
|
||||
<option value="ssh" selected="">ssh</option>
|
||||
<option value="mysql">mysql</option>
|
||||
<option value="pure-ftpd">pure-ftpd</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="add-fail2ban-rule-website-mode-input" class="layui-form-item">
|
||||
<label class="layui-form-label">网站模式</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="radio" lay-filter="add-fail2ban-rule-website-mode-radio" name="website_mode" value="cc"
|
||||
title="CC" checked="">
|
||||
<input type="radio" lay-filter="add-fail2ban-rule-website-mode-radio" name="website_mode" value="path"
|
||||
title="目录">
|
||||
</div>
|
||||
</div>
|
||||
<div id="add-fail2ban-rule-website-path-input" class="layui-form-item">
|
||||
<label class="layui-form-label">网站目录</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="website_path"
|
||||
lay-verify="required" placeholder="输入一个禁止访问的目录" class="layui-input"
|
||||
value="/admin">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">最大重试</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="maxretry"
|
||||
lay-verify="required" placeholder="单位:次" class="layui-input"
|
||||
value="30">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">周期</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="findtime"
|
||||
lay-verify="required" placeholder="单位:秒" class="layui-input"
|
||||
value="300">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">禁止时间</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="bantime"
|
||||
lay-verify="required" placeholder="单位:秒" class="layui-input"
|
||||
value="600">
|
||||
</div>
|
||||
<div class="layui-form-mid layui-word-aux">
|
||||
在设置周期内(秒)有超过最大重试(次)的IP访问,将禁止该IP禁止时间(秒)
|
||||
</div>
|
||||
<div class="layui-form-mid layui-word-aux">
|
||||
<span style="color: red;">防护端口自动获取,如果修改了规则项对应的端口,请删除重新添加,否则防护可能不会生效</span>
|
||||
</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-fail2ban-rule-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 $ = layui.jquery
|
||||
, admin = layui.admin
|
||||
, layer = layui.layer
|
||||
, form = layui.form
|
||||
, table = layui.table;
|
||||
|
||||
form.render();
|
||||
|
||||
// 监听类型选择
|
||||
$('#add-fail2ban-rule-service-input').hide();
|
||||
form.on('radio(add-fail2ban-rule-type-radio)', function (data) {
|
||||
if (data.value == 'website') {
|
||||
$('#add-fail2ban-rule-website-input').show();
|
||||
$('#add-fail2ban-rule-website-mode-input').show();
|
||||
$('#add-fail2ban-rule-service-input').hide();
|
||||
if ($('input[name="website_mode"]:checked').val() == 'cc') {
|
||||
$('#add-fail2ban-rule-website-path-input').hide();
|
||||
} else {
|
||||
$('#add-fail2ban-rule-website-path-input').show();
|
||||
}
|
||||
} else if (data.value == 'service') {
|
||||
$('#add-fail2ban-rule-website-input').hide();
|
||||
$('#add-fail2ban-rule-website-mode-input').hide();
|
||||
$('#add-fail2ban-rule-service-input').show();
|
||||
$('#add-fail2ban-rule-website-path-input').hide();
|
||||
}
|
||||
});
|
||||
// 监听网站模式选择
|
||||
$('#add-fail2ban-rule-website-path-input').hide();
|
||||
form.on('radio(add-fail2ban-rule-website-mode-radio)', function (data) {
|
||||
if (data.value == 'cc') {
|
||||
$('#add-fail2ban-rule-website-path-input').hide();
|
||||
} else if (data.value == 'path') {
|
||||
$('#add-fail2ban-rule-website-path-input').show();
|
||||
}
|
||||
});
|
||||
|
||||
// 提交
|
||||
form.on('submit(add-fail2ban-rule-submit)', function (data) {
|
||||
if (data.field.type === 'website') {
|
||||
data.field.name = data.field.website;
|
||||
} else if (data.field.type === 'service') {
|
||||
data.field.name = data.field.service;
|
||||
}
|
||||
index = layer.msg('提交中...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/fail2ban/add"
|
||||
, type: 'post'
|
||||
, data: data.field
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
table.reload('fail2ban-rule-list');
|
||||
layer.alert('Fail2ban规则添加成功!', {
|
||||
icon: 1
|
||||
, title: '提示'
|
||||
, btn: ['确定']
|
||||
, yes: function (index) {
|
||||
layer.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,101 @@
|
||||
<!--
|
||||
Name: Fail2ban管理器 - 查看规则
|
||||
Author: 耗子
|
||||
Date: 2023-07-30
|
||||
-->
|
||||
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
|
||||
{{# console.log(d.params) }}
|
||||
<div class="layui-fluid">
|
||||
<div class="layui-row layui-col-space15">
|
||||
<div class="layui-col-sm6 layui-col-md6">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-body layuiadmin-card-list">
|
||||
<p class="layuiadmin-big-font">{{ d.params.banList.currentlyBan }}</p>
|
||||
<p>当前封禁IP数</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-col-sm6 layui-col-md6">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-body layuiadmin-card-list">
|
||||
<p class="layuiadmin-big-font">{{ d.params.banList.totalBan }}</p>
|
||||
<p>累计封禁IP数</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-row layui-col-space15">
|
||||
<div class="layui-col-sm12 layui-col-md12">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-body layuiadmin-card-list">
|
||||
<table class="layui-table" id="fail2ban-view-rule-table"
|
||||
lay-filter="fail2ban-view-rule-table"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<script type="text/html" id="fail2ban-view-rule-ip-control">
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="unBan">解封</a>
|
||||
</script>
|
||||
<script>
|
||||
layui.data.sendParams = function (params) {
|
||||
if (params.banList.bannedIpList === null) {
|
||||
params.banList.bannedIpList = [];
|
||||
}
|
||||
|
||||
layui.use(['admin', 'jquery'], function () {
|
||||
var admin = layui.admin
|
||||
, layer = layui.layer
|
||||
, table = layui.table;
|
||||
|
||||
table.render({
|
||||
elem: '#fail2ban-view-rule-table'
|
||||
, cols: [[
|
||||
{field: 'name', title: '规则名', unresize: true, hide: true}
|
||||
, {field: 'ip', title: 'IP', unresize: true, sort: true}
|
||||
, {
|
||||
fixed: 'right',
|
||||
title: '操作',
|
||||
width: 150,
|
||||
unresize: true,
|
||||
toolbar: '#fail2ban-view-rule-ip-control'
|
||||
}
|
||||
]]
|
||||
, data: params.banList.bannedIpList
|
||||
, page: true
|
||||
, text: {
|
||||
none: '无数据'
|
||||
}
|
||||
});
|
||||
|
||||
// 监听工具条
|
||||
table.on('tool(fail2ban-view-rule-table)', function (obj) {
|
||||
var data = obj.data;
|
||||
if (obj.event === 'unBan') {
|
||||
layer.confirm('确定要解封 <b style="color: red;">' + data.ip + '</b> 吗?', function (index) {
|
||||
index = layer.msg('请稍等...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
});
|
||||
admin.req({
|
||||
url: "/api/plugins/fail2ban/unban"
|
||||
, type: 'post'
|
||||
, data: data
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
obj.del();
|
||||
layer.alert(data.ip + '解封成功!');
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,576 @@
|
||||
<!--
|
||||
Name: MySQL管理器
|
||||
Author: 耗子
|
||||
Date: 2022-07-22
|
||||
-->
|
||||
<title>MySQL</title>
|
||||
<div class="layui-fluid" id="component-tabs">
|
||||
<div class="layui-row">
|
||||
<div class="layui-col-md12">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">MySQL管理</div>
|
||||
<div class="layui-card-body">
|
||||
<div class="layui-tab">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this">基本信息</li>
|
||||
<li>管理</li>
|
||||
<li>配置修改</li>
|
||||
<li>负载状态</li>
|
||||
<li>错误日志</li>
|
||||
<li>慢查询日志</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<div class="layui-tab-item layui-show">
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
|
||||
<legend>运行状态</legend>
|
||||
</fieldset>
|
||||
<blockquote id="mysql-status" class="layui-elem-quote layui-quote-nm">当前状态:<span
|
||||
class="layui-badge layui-bg-black">获取中</span></blockquote>
|
||||
<div class="layui-btn-container" style="padding-top: 30px;">
|
||||
<button id="mysql-start" class="layui-btn">启动</button>
|
||||
<button id="mysql-stop" class="layui-btn layui-btn-danger">停止</button>
|
||||
<button id="mysql-restart" class="layui-btn layui-btn-warm">重启</button>
|
||||
<button id="mysql-reload" class="layui-btn layui-btn-normal">重载</button>
|
||||
</div>
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
|
||||
<legend>基本设置</legend>
|
||||
</fieldset>
|
||||
<div class="layui-form" lay-filter="mysql_setting">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="font-size: 13px;">root 密码</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="mysql_root_password" value="获取中ing..."
|
||||
class="layui-input" disabled>
|
||||
</div>
|
||||
<div class="layui-form-mid layui-word-aux">查看/修改MySQL的root密码</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn layui-btn-sm" lay-submit
|
||||
lay-filter="mysql_setting_submit">确认修改
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<blockquote class="layui-elem-quote">面板仅集成了部分常用功能,如需更多功能,建议安装
|
||||
phpMyAdmin 使用。
|
||||
</blockquote>
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
|
||||
<legend>数据库列表</legend>
|
||||
</fieldset>
|
||||
<table class="layui-hide" id="mysql-database-list"
|
||||
lay-filter="mysql-database-list"></table>
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
|
||||
<legend>用户列表</legend>
|
||||
</fieldset>
|
||||
<table class="layui-hide" id="mysql-user-list" lay-filter="mysql-user-list"></table>
|
||||
<!-- 数据库顶部工具栏 -->
|
||||
<script type="text/html" id="mysql-database-list-bar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="add_database">新建数据库
|
||||
</button>
|
||||
</div>
|
||||
</script>
|
||||
<!-- 用户顶部工具栏 -->
|
||||
<script type="text/html" id="mysql-user-list-bar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="add_user">新建用户</button>
|
||||
</div>
|
||||
</script>
|
||||
<!-- 数据库右侧管理 -->
|
||||
<script type="text/html" id="mysql-database-list-control">
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="backup">备份</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
</script>
|
||||
<!-- 用户右侧管理 -->
|
||||
<script type="text/html" id="mysql-user-list-control">
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs"
|
||||
lay-event="change_password">改密</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
</script>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<blockquote class="layui-elem-quote">此处修改的是MySQL主配置文件,如果你不了解各参数的含义,请不要随意修改!<br>
|
||||
提示:Ctrl+F 搜索关键字,Ctrl+S 保存,Ctrl+H 查找替换!
|
||||
</blockquote>
|
||||
<div id="mysql-config-editor"
|
||||
style="height: 600px;"></div>
|
||||
<div class="layui-btn-container" style="padding-top: 30px;">
|
||||
<button id="mysql-config-save" class="layui-btn">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<table class="layui-hide" id="mysql-load-status"></table>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-btn-container">
|
||||
<button id="mysql-clean-error-log" class="layui-btn">清空日志</button>
|
||||
</div>
|
||||
<pre id="mysql-error-log" class="layui-code">
|
||||
获取中...
|
||||
</pre>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-btn-container">
|
||||
<button id="mysql-clean-slow-log" class="layui-btn">清空日志</button>
|
||||
</div>
|
||||
<pre id="mysql-slow-log" class="layui-code">
|
||||
获取中...
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let mysql_config_editor;// 定义mysql配置编辑器的全局变量
|
||||
layui.use(['index', 'code', 'table'], function () {
|
||||
let $ = layui.$
|
||||
, admin = layui.admin
|
||||
, element = layui.element
|
||||
, code = layui.code
|
||||
, table = layui.table
|
||||
, form = layui.form
|
||||
, view = layui.view;
|
||||
|
||||
// 渲染表单
|
||||
form.render();
|
||||
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/rootPassword"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:系统信息获取失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
form.val("mysql_setting", {
|
||||
"mysql_root_password": result.data
|
||||
});
|
||||
$('input').attr('disabled', false);
|
||||
}
|
||||
});
|
||||
// 提交修改
|
||||
form.on('submit(mysql_setting_submit)', function (data) {
|
||||
index = layer.msg('请稍候...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/rootPassword"
|
||||
, type: 'post'
|
||||
, data: data.field
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL设置保存失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
layer.msg('修改成功!')
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
// 获取mysql运行状态并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/status"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL运行状态获取失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
if (result.data) {
|
||||
$('#mysql-status').html('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
|
||||
} else {
|
||||
$('#mysql-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// 获取数据库列表
|
||||
table.render({
|
||||
elem: '#mysql-database-list'
|
||||
, url: '/api/plugins/mysql57/database'
|
||||
, toolbar: '#mysql-database-list-bar'
|
||||
, title: '数据库列表'
|
||||
, cols: [[
|
||||
{field: 'name', title: '库名', fixed: 'left', unresize: true, sort: true}
|
||||
, {fixed: 'right', title: '操作', toolbar: '#mysql-database-list-control', width: 150}
|
||||
]]
|
||||
, parseData: function (res) {
|
||||
return {
|
||||
"code": res.code,
|
||||
"msg": res.message,
|
||||
"count": res.data.total,
|
||||
"data": res.data.items
|
||||
};
|
||||
}
|
||||
, page: true
|
||||
});
|
||||
// 头工具栏事件
|
||||
table.on('toolbar(mysql-database-list)', function (obj) {
|
||||
if (obj.event === 'add_database') {
|
||||
admin.popup({
|
||||
title: '新建数据库'
|
||||
, area: ['600px', '300px']
|
||||
, id: 'LAY-popup-mysql-database-add'
|
||||
, success: function (layer, index) {
|
||||
view(this.id).render('plugins/mysql57/add_database', {}).done(function () {
|
||||
form.render(null, 'LAY-popup-mysql-database-add');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// 行工具事件
|
||||
table.on('tool(mysql-database-list)', function (obj) {
|
||||
let data = obj.data;
|
||||
if (obj.event === 'del') {
|
||||
layer.confirm('高风险操作,确定要删除数据库 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
|
||||
index = layer.msg('请稍候...', {icon: 16, time: 0, shade: 0.3});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/deleteDatabase"
|
||||
, type: 'post'
|
||||
, data: {
|
||||
database: data.name
|
||||
}
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:数据库删除失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
obj.del();
|
||||
layer.alert('数据库' + data.name + '删除成功!');
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
} else if (obj.event === 'backup') {
|
||||
// 打开备份页面
|
||||
admin.popup({
|
||||
title: '备份管理 - ' + data.name
|
||||
, area: ['70%', '80%']
|
||||
, id: 'LAY-popup-mysql-backup'
|
||||
, success: function (layero, index) {
|
||||
view(this.id).render('plugins/mysql57/backup', {
|
||||
data: data
|
||||
}).done(function () {
|
||||
form.render(null, 'LAY-popup-mysql-backup');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取数据库用户列表
|
||||
table.render({
|
||||
elem: '#mysql-user-list'
|
||||
, url: '/api/plugins/mysql57/user'
|
||||
, toolbar: '#mysql-user-list-bar'
|
||||
, title: '用户列表'
|
||||
, cols: [[
|
||||
{field: 'user', title: '用户名', fixed: 'left', width: 300, sort: true}
|
||||
, {field: 'host', title: '主机', width: 250, sort: true}
|
||||
, {field: 'grants', title: '权限'}
|
||||
, {fixed: 'right', title: '操作', toolbar: '#mysql-user-list-control', width: 150}
|
||||
]]
|
||||
, parseData: function (res) {
|
||||
return {
|
||||
"code": res.code,
|
||||
"msg": res.message,
|
||||
"count": res.data.total,
|
||||
"data": res.data.items
|
||||
};
|
||||
}
|
||||
, page: true
|
||||
});
|
||||
// 头工具栏事件
|
||||
table.on('toolbar(mysql-user-list)', function (obj) {
|
||||
if (obj.event === 'add_user') {
|
||||
admin.popup({
|
||||
title: '新建用户'
|
||||
, area: ['600px', '300px']
|
||||
, id: 'LAY-popup-mysql-user-add'
|
||||
, success: function (layer, index) {
|
||||
view(this.id).render('plugins/mysql57/add_user', {}).done(function () {
|
||||
form.render(null, 'LAY-popup-mysql-user-add');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// 行工具事件
|
||||
table.on('tool(mysql-user-list)', function (obj) {
|
||||
let data = obj.data;
|
||||
if (obj.event === 'del') {
|
||||
layer.confirm('高风险操作,确定要删除用户 <b style="color: red;">' + data.user + '</b> 吗?', function (index) {
|
||||
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/deleteUser"
|
||||
, type: 'post'
|
||||
, data: data
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:用户删除失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
obj.del();
|
||||
layer.alert('用户' + data.user + '删除成功!');
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
} else if (obj.event === 'change_password') {
|
||||
// 弹出输入密码框
|
||||
layer.prompt({
|
||||
formType: 1
|
||||
, title: '请输入新密码(8位以上大小写数字特殊符号混合)'
|
||||
}, function (value, index) {
|
||||
layer.close(index);
|
||||
index = layer.msg('正在提交...', {icon: 16, time: 0});
|
||||
// 发送请求
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/userPassword"
|
||||
, type: 'post'
|
||||
, data: {
|
||||
user: data.user,
|
||||
password: value
|
||||
}
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:密码修改失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
layer.alert('用户' + data.user + '密码修改成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取mysql错误日志并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/errorLog"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL错误日志获取失败,接口返回' + result);
|
||||
$('#mysql-error-log').text('MySQL错误日志获取失败,请刷新重试!');
|
||||
code({
|
||||
elem: '#mysql-error-log'
|
||||
, title: 'error.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
return false;
|
||||
}
|
||||
$('#mysql-error-log').text(result.data);
|
||||
code({
|
||||
elem: '#mysql-error-log'
|
||||
, title: 'error.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取mysql慢查询日志并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/slowLog"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL慢查询日志获取失败,接口返回' + result);
|
||||
$('#mysql-slow-log').text('MySQL慢查询日志获取失败,请刷新重试!');
|
||||
code({
|
||||
elem: '#mysql-slow-log'
|
||||
, title: 'slow.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
return false;
|
||||
}
|
||||
$('#mysql-slow-log').text(result.data);
|
||||
code({
|
||||
elem: '#mysql-slow-log'
|
||||
, title: 'slow.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取mysql配置并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/config"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL主配置获取失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
$('#mysql-config-editor').text(result.data);
|
||||
mysql_config_editor = ace.edit("mysql-config-editor", {
|
||||
mode: "ace/mode/ini",
|
||||
selectionStyle: "text"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取mysql负载状态并渲染
|
||||
table.render({
|
||||
elem: '#mysql-load-status'
|
||||
, url: '/api/plugins/mysql57/load'
|
||||
, cols: [[
|
||||
{field: 'name', width: '80%', title: '属性',}
|
||||
, {field: 'value', width: '20%', title: '当前值'}
|
||||
]]
|
||||
});
|
||||
element.render();
|
||||
|
||||
// 事件监听
|
||||
$('#mysql-start').click(function () {
|
||||
layer.confirm('确定要启动MySQL吗?', {
|
||||
btn: ['启动', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('正在启动MySQL...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/start"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL启动失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('MySQL启动成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#mysql-stop').click(function () {
|
||||
layer.confirm('停止MySQL将导致使用MySQL的网站无法访问,是否继续停止?', {
|
||||
btn: ['停止', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('正在停止MySQL...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/stop"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL停止失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('MySQL停止成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#mysql-restart').click(function () {
|
||||
layer.confirm('重启MySQL将导致使用MySQL的网站短时间无法访问,是否继续重启?', {
|
||||
btn: ['重启', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('正在重启MySQL...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/restart"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL重启失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('MySQL重启成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#mysql-reload').click(function () {
|
||||
index = layer.msg('正在重载MySQL...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/reload"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL重载失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('MySQL重载成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#mysql-config-save').click(function () {
|
||||
index = layer.msg('正在保存配置...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/config"
|
||||
, type: 'post'
|
||||
, data: {
|
||||
config: mysql_config_editor.getValue()
|
||||
}
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL配置保存失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
layer.alert('MySQL配置保存成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#mysql-clean-error-log').click(function () {
|
||||
index = layer.msg('正在清空错误日志...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/clearErrorLog"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL错误日志清空失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
layer.msg('MySQL错误日志已清空!');
|
||||
setTimeout(function () {
|
||||
admin.events.refresh();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#mysql-clean-slow-log').click(function () {
|
||||
index = layer.msg('正在清空慢查询日志...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/clearSlowLog"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL慢查询日志清空失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
layer.msg('MySQL慢查询日志已清空!');
|
||||
setTimeout(function () {
|
||||
admin.events.refresh();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
<!--
|
||||
Name: MySQL管理器 - 添加数据库
|
||||
Author: 耗子
|
||||
Date: 2023-07-22
|
||||
-->
|
||||
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
|
||||
<form class="layui-form" action="" lay-filter="add-mysql-database-form">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">数据库名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="database" lay-verify="required" placeholder="请输入数据库名"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="user" lay-verify="required" placeholder="请输入用户名"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="password" lay-verify="required" placeholder="请输入密码(8位以上大小写数字特殊符号混合)"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<div class="layui-footer">
|
||||
<button class="layui-btn" lay-submit="" lay-filter="add-mysql-database-submit">立即提交</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
<script>
|
||||
layui.data.sendParams = function (params) {
|
||||
layui.use(['admin', 'form'], function () {
|
||||
var $ = layui.$
|
||||
, admin = layui.admin
|
||||
, layer = layui.layer
|
||||
, form = layui.form
|
||||
, table = layui.table
|
||||
|
||||
form.render();
|
||||
|
||||
// 提交
|
||||
form.on('submit(add-mysql-database-submit)', function (data) {
|
||||
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/addDatabase"
|
||||
, type: 'post'
|
||||
, data: data.field
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:数据库添加失败,接口返回' + result);
|
||||
layer.msg('数据库添加失败,请刷新重试!')
|
||||
return false;
|
||||
}
|
||||
table.reload('mysql-database-list');
|
||||
table.reload('mysql-user-list');
|
||||
layer.alert('数据库添加成功!', {
|
||||
icon: 1
|
||||
, title: '提示'
|
||||
, btn: ['确定']
|
||||
, yes: function (index) {
|
||||
layer.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
<!--
|
||||
Name: MySQL管理器 - 添加用户
|
||||
Author: 耗子
|
||||
Date: 2023-07-22
|
||||
-->
|
||||
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
|
||||
<form class="layui-form" action="" lay-filter="add-mysql-user-form">
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="user" lay-verify="required" placeholder="请输入用户名"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="password" lay-verify="required" placeholder="请输入密码(8位以上大小写数字特殊符号混合)"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">数据库</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="database" lay-verify="required" placeholder="输入授权给该用户的数据库名"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<div class="layui-footer">
|
||||
<button class="layui-btn" lay-submit="" lay-filter="add-mysql-user-submit">立即提交</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
<script>
|
||||
layui.data.sendParams = function (params) {
|
||||
layui.use(['admin', 'form'], function () {
|
||||
var $ = layui.$
|
||||
, admin = layui.admin
|
||||
, layer = layui.layer
|
||||
, form = layui.form
|
||||
, table = layui.table
|
||||
|
||||
form.render();
|
||||
|
||||
// 提交
|
||||
form.on('submit(add-mysql-user-submit)', function (data) {
|
||||
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/addUser"
|
||||
, type: 'post'
|
||||
, data: data.field
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:用户添加失败,接口返回' + result);
|
||||
layer.msg('用户添加失败,请刷新重试!')
|
||||
return false;
|
||||
}
|
||||
table.reload('mysql-database-list');
|
||||
table.reload('mysql-user-list');
|
||||
layer.alert('用户添加成功!', {
|
||||
icon: 1
|
||||
, title: '提示'
|
||||
, btn: ['确定']
|
||||
, yes: function (index) {
|
||||
layer.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,145 @@
|
||||
<!--
|
||||
Name: MySQL管理器 - 数据库备份
|
||||
Author: 耗子
|
||||
Date: 2023-07-22
|
||||
-->
|
||||
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
|
||||
<div class="layui-row">
|
||||
<div class="layui-col-xs12 layui-col-sm12 layui-col-md12">
|
||||
<table class="layui-hide" id="mysql-backup-list" lay-filter="mysql-backup-list"></table>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<!-- 备份顶部工具栏 -->
|
||||
<script type="text/html" id="mysql-database-backup-bar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="backup_database">备份数据库</button>
|
||||
<button class="layui-btn layui-btn-sm" id="upload_mysql_backup">上传备份</button>
|
||||
</div>
|
||||
</script>
|
||||
<!-- 备份右侧管理 -->
|
||||
<script type="text/html" id="mysql-database-backup-control">
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="restore">恢复</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
</script>
|
||||
<script>
|
||||
layui.data.sendParams = function (params) {
|
||||
layui.use(['admin', 'form', 'laydate', 'code'], function () {
|
||||
var $ = layui.$
|
||||
, admin = layui.admin
|
||||
, layer = layui.layer
|
||||
, table = layui.table
|
||||
, upload = layui.upload;
|
||||
|
||||
// 渲染表格
|
||||
table.render({
|
||||
elem: '#mysql-backup-list'
|
||||
, url: '/api/plugins/mysql57/backup'
|
||||
, toolbar: '#mysql-database-backup-bar'
|
||||
, title: '备份列表'
|
||||
, cols: [[
|
||||
{field: 'name', title: '备份名称', width: 500}
|
||||
, {field: 'size', title: '文件大小'}
|
||||
, {field: 'right', title: '操作', width: 150, toolbar: '#mysql-database-backup-control'}
|
||||
]]
|
||||
, text: {
|
||||
none: '无备份数据'
|
||||
}
|
||||
, done: function (res, curr, count) {
|
||||
upload.render({
|
||||
elem: '#upload_mysql_backup'
|
||||
, url: '/api/plugins/mysql57/uploadBackup'
|
||||
, accept: 'file'
|
||||
, ext: 'sql|zip|rar|tar|gz|bz2'
|
||||
, before: function (obj) {
|
||||
index = layer.msg('正在上传备份文件,可能需要较长时间,请勿操作...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
});
|
||||
}
|
||||
, done: function (res) {
|
||||
layer.close(index);
|
||||
layer.msg('上传成功!', {icon: 1});
|
||||
table.reload('mysql-backup-list');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// 头工具栏事件
|
||||
table.on('toolbar(mysql-backup-list)', function (obj) {
|
||||
if (obj.event === 'backup_database') {
|
||||
index = layer.msg('正在备份数据库,请稍等...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
});
|
||||
admin.req({
|
||||
url: '/api/plugins/mysql57/createBackup'
|
||||
, type: 'post'
|
||||
, data: {
|
||||
database: params.data.name
|
||||
}
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:备份数据库失败,接口返回' + result);
|
||||
layer.alert('备份失败!');
|
||||
return false;
|
||||
}
|
||||
table.reload('mysql-backup-list');
|
||||
layer.msg('备份成功!', {icon: 1});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// 行工具事件
|
||||
table.on('tool(mysql-backup-list)', function (obj) {
|
||||
let data = obj.data;
|
||||
if (obj.event === 'del') {
|
||||
layer.confirm('确定要删除数据库备份 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
|
||||
index = layer.msg('正在删除数据库备份,请稍等...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/deleteBackup"
|
||||
, type: 'post'
|
||||
, data: data
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:数据库备份删除失败,接口返回' + result);
|
||||
layer.msg('数据库备份删除失败,请刷新重试!')
|
||||
return false;
|
||||
}
|
||||
obj.del();
|
||||
layer.alert('数据库备份' + data.name + '删除成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (obj.event === 'restore') {
|
||||
layer.confirm('高风险操作,确定要恢复数据库备份 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
|
||||
index = layer.msg('正在恢复数据库备份,可能需要较长时间,请勿操作...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
});
|
||||
data.database = params.data.name;
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql57/restoreBackup"
|
||||
, type: 'post'
|
||||
, data: data
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:数据库恢复失败,接口返回' + result);
|
||||
layer.msg('数据库备份恢复失败,请刷新重试!')
|
||||
return false;
|
||||
}
|
||||
layer.alert('数据库备份' + data.name + '恢复成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,576 @@
|
||||
<!--
|
||||
Name: MySQL管理器
|
||||
Author: 耗子
|
||||
Date: 2022-07-22
|
||||
-->
|
||||
<title>MySQL</title>
|
||||
<div class="layui-fluid" id="component-tabs">
|
||||
<div class="layui-row">
|
||||
<div class="layui-col-md12">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">MySQL管理</div>
|
||||
<div class="layui-card-body">
|
||||
<div class="layui-tab">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this">基本信息</li>
|
||||
<li>管理</li>
|
||||
<li>配置修改</li>
|
||||
<li>负载状态</li>
|
||||
<li>错误日志</li>
|
||||
<li>慢查询日志</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<div class="layui-tab-item layui-show">
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
|
||||
<legend>运行状态</legend>
|
||||
</fieldset>
|
||||
<blockquote id="mysql-status" class="layui-elem-quote layui-quote-nm">当前状态:<span
|
||||
class="layui-badge layui-bg-black">获取中</span></blockquote>
|
||||
<div class="layui-btn-container" style="padding-top: 30px;">
|
||||
<button id="mysql-start" class="layui-btn">启动</button>
|
||||
<button id="mysql-stop" class="layui-btn layui-btn-danger">停止</button>
|
||||
<button id="mysql-restart" class="layui-btn layui-btn-warm">重启</button>
|
||||
<button id="mysql-reload" class="layui-btn layui-btn-normal">重载</button>
|
||||
</div>
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
|
||||
<legend>基本设置</legend>
|
||||
</fieldset>
|
||||
<div class="layui-form" lay-filter="mysql_setting">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label" style="font-size: 13px;">root 密码</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="text" name="mysql_root_password" value="获取中ing..."
|
||||
class="layui-input" disabled>
|
||||
</div>
|
||||
<div class="layui-form-mid layui-word-aux">查看/修改MySQL的root密码</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn layui-btn-sm" lay-submit
|
||||
lay-filter="mysql_setting_submit">确认修改
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<blockquote class="layui-elem-quote">面板仅集成了部分常用功能,如需更多功能,建议安装
|
||||
phpMyAdmin 使用。
|
||||
</blockquote>
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
|
||||
<legend>数据库列表</legend>
|
||||
</fieldset>
|
||||
<table class="layui-hide" id="mysql-database-list"
|
||||
lay-filter="mysql-database-list"></table>
|
||||
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
|
||||
<legend>用户列表</legend>
|
||||
</fieldset>
|
||||
<table class="layui-hide" id="mysql-user-list" lay-filter="mysql-user-list"></table>
|
||||
<!-- 数据库顶部工具栏 -->
|
||||
<script type="text/html" id="mysql-database-list-bar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="add_database">新建数据库
|
||||
</button>
|
||||
</div>
|
||||
</script>
|
||||
<!-- 用户顶部工具栏 -->
|
||||
<script type="text/html" id="mysql-user-list-bar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="add_user">新建用户</button>
|
||||
</div>
|
||||
</script>
|
||||
<!-- 数据库右侧管理 -->
|
||||
<script type="text/html" id="mysql-database-list-control">
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="backup">备份</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
</script>
|
||||
<!-- 用户右侧管理 -->
|
||||
<script type="text/html" id="mysql-user-list-control">
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs"
|
||||
lay-event="change_password">改密</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
</script>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<blockquote class="layui-elem-quote">此处修改的是MySQL主配置文件,如果你不了解各参数的含义,请不要随意修改!<br>
|
||||
提示:Ctrl+F 搜索关键字,Ctrl+S 保存,Ctrl+H 查找替换!
|
||||
</blockquote>
|
||||
<div id="mysql-config-editor"
|
||||
style="height: 600px;"></div>
|
||||
<div class="layui-btn-container" style="padding-top: 30px;">
|
||||
<button id="mysql-config-save" class="layui-btn">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<table class="layui-hide" id="mysql-load-status"></table>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-btn-container">
|
||||
<button id="mysql-clean-error-log" class="layui-btn">清空日志</button>
|
||||
</div>
|
||||
<pre id="mysql-error-log" class="layui-code">
|
||||
获取中...
|
||||
</pre>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-btn-container">
|
||||
<button id="mysql-clean-slow-log" class="layui-btn">清空日志</button>
|
||||
</div>
|
||||
<pre id="mysql-slow-log" class="layui-code">
|
||||
获取中...
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let mysql_config_editor;// 定义mysql配置编辑器的全局变量
|
||||
layui.use(['index', 'code', 'table'], function () {
|
||||
let $ = layui.$
|
||||
, admin = layui.admin
|
||||
, element = layui.element
|
||||
, code = layui.code
|
||||
, table = layui.table
|
||||
, form = layui.form
|
||||
, view = layui.view;
|
||||
|
||||
// 渲染表单
|
||||
form.render();
|
||||
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/rootPassword"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:系统信息获取失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
form.val("mysql_setting", {
|
||||
"mysql_root_password": result.data
|
||||
});
|
||||
$('input').attr('disabled', false);
|
||||
}
|
||||
});
|
||||
// 提交修改
|
||||
form.on('submit(mysql_setting_submit)', function (data) {
|
||||
index = layer.msg('请稍候...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/rootPassword"
|
||||
, type: 'post'
|
||||
, data: data.field
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL设置保存失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
layer.msg('修改成功!')
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
// 获取mysql运行状态并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/status"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL运行状态获取失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
if (result.data) {
|
||||
$('#mysql-status').html('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
|
||||
} else {
|
||||
$('#mysql-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// 获取数据库列表
|
||||
table.render({
|
||||
elem: '#mysql-database-list'
|
||||
, url: '/api/plugins/mysql80/database'
|
||||
, toolbar: '#mysql-database-list-bar'
|
||||
, title: '数据库列表'
|
||||
, cols: [[
|
||||
{field: 'name', title: '库名', fixed: 'left', unresize: true, sort: true}
|
||||
, {fixed: 'right', title: '操作', toolbar: '#mysql-database-list-control', width: 150}
|
||||
]]
|
||||
, parseData: function (res) {
|
||||
return {
|
||||
"code": res.code,
|
||||
"msg": res.message,
|
||||
"count": res.data.total,
|
||||
"data": res.data.items
|
||||
};
|
||||
}
|
||||
, page: true
|
||||
});
|
||||
// 头工具栏事件
|
||||
table.on('toolbar(mysql-database-list)', function (obj) {
|
||||
if (obj.event === 'add_database') {
|
||||
admin.popup({
|
||||
title: '新建数据库'
|
||||
, area: ['600px', '300px']
|
||||
, id: 'LAY-popup-mysql-database-add'
|
||||
, success: function (layer, index) {
|
||||
view(this.id).render('plugins/mysql80/add_database', {}).done(function () {
|
||||
form.render(null, 'LAY-popup-mysql-database-add');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// 行工具事件
|
||||
table.on('tool(mysql-database-list)', function (obj) {
|
||||
let data = obj.data;
|
||||
if (obj.event === 'del') {
|
||||
layer.confirm('高风险操作,确定要删除数据库 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
|
||||
index = layer.msg('请稍候...', {icon: 16, time: 0, shade: 0.3});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/deleteDatabase"
|
||||
, type: 'post'
|
||||
, data: {
|
||||
database: data.name
|
||||
}
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:数据库删除失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
obj.del();
|
||||
layer.alert('数据库' + data.name + '删除成功!');
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
} else if (obj.event === 'backup') {
|
||||
// 打开备份页面
|
||||
admin.popup({
|
||||
title: '备份管理 - ' + data.name
|
||||
, area: ['70%', '80%']
|
||||
, id: 'LAY-popup-mysql-backup'
|
||||
, success: function (layero, index) {
|
||||
view(this.id).render('plugins/mysql80/backup', {
|
||||
data: data
|
||||
}).done(function () {
|
||||
form.render(null, 'LAY-popup-mysql-backup');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取数据库用户列表
|
||||
table.render({
|
||||
elem: '#mysql-user-list'
|
||||
, url: '/api/plugins/mysql80/user'
|
||||
, toolbar: '#mysql-user-list-bar'
|
||||
, title: '用户列表'
|
||||
, cols: [[
|
||||
{field: 'user', title: '用户名', fixed: 'left', width: 300, sort: true}
|
||||
, {field: 'host', title: '主机', width: 250, sort: true}
|
||||
, {field: 'grants', title: '权限'}
|
||||
, {fixed: 'right', title: '操作', toolbar: '#mysql-user-list-control', width: 150}
|
||||
]]
|
||||
, parseData: function (res) {
|
||||
return {
|
||||
"code": res.code,
|
||||
"msg": res.message,
|
||||
"count": res.data.total,
|
||||
"data": res.data.items
|
||||
};
|
||||
}
|
||||
, page: true
|
||||
});
|
||||
// 头工具栏事件
|
||||
table.on('toolbar(mysql-user-list)', function (obj) {
|
||||
if (obj.event === 'add_user') {
|
||||
admin.popup({
|
||||
title: '新建用户'
|
||||
, area: ['600px', '300px']
|
||||
, id: 'LAY-popup-mysql-user-add'
|
||||
, success: function (layer, index) {
|
||||
view(this.id).render('plugins/mysql80/add_user', {}).done(function () {
|
||||
form.render(null, 'LAY-popup-mysql-user-add');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// 行工具事件
|
||||
table.on('tool(mysql-user-list)', function (obj) {
|
||||
let data = obj.data;
|
||||
if (obj.event === 'del') {
|
||||
layer.confirm('高风险操作,确定要删除用户 <b style="color: red;">' + data.user + '</b> 吗?', function (index) {
|
||||
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/deleteUser"
|
||||
, type: 'post'
|
||||
, data: data
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:用户删除失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
obj.del();
|
||||
layer.alert('用户' + data.user + '删除成功!');
|
||||
}
|
||||
});
|
||||
layer.close(index);
|
||||
});
|
||||
} else if (obj.event === 'change_password') {
|
||||
// 弹出输入密码框
|
||||
layer.prompt({
|
||||
formType: 1
|
||||
, title: '请输入新密码(8位以上大小写数字特殊符号混合)'
|
||||
}, function (value, index) {
|
||||
layer.close(index);
|
||||
index = layer.msg('正在提交...', {icon: 16, time: 0});
|
||||
// 发送请求
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/userPassword"
|
||||
, type: 'post'
|
||||
, data: {
|
||||
user: data.user,
|
||||
password: value
|
||||
}
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:密码修改失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
layer.alert('用户' + data.user + '密码修改成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取mysql错误日志并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/errorLog"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL错误日志获取失败,接口返回' + result);
|
||||
$('#mysql-error-log').text('MySQL错误日志获取失败,请刷新重试!');
|
||||
code({
|
||||
elem: '#mysql-error-log'
|
||||
, title: 'error.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
return false;
|
||||
}
|
||||
$('#mysql-error-log').text(result.data);
|
||||
code({
|
||||
elem: '#mysql-error-log'
|
||||
, title: 'error.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取mysql慢查询日志并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/slowLog"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL慢查询日志获取失败,接口返回' + result);
|
||||
$('#mysql-slow-log').text('MySQL慢查询日志获取失败,请刷新重试!');
|
||||
code({
|
||||
elem: '#mysql-slow-log'
|
||||
, title: 'slow.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
return false;
|
||||
}
|
||||
$('#mysql-slow-log').text(result.data);
|
||||
code({
|
||||
elem: '#mysql-slow-log'
|
||||
, title: 'slow.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取mysql配置并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/config"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL主配置获取失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
$('#mysql-config-editor').text(result.data);
|
||||
mysql_config_editor = ace.edit("mysql-config-editor", {
|
||||
mode: "ace/mode/ini",
|
||||
selectionStyle: "text"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取mysql负载状态并渲染
|
||||
table.render({
|
||||
elem: '#mysql-load-status'
|
||||
, url: '/api/plugins/mysql80/load'
|
||||
, cols: [[
|
||||
{field: 'name', width: '80%', title: '属性',}
|
||||
, {field: 'value', width: '20%', title: '当前值'}
|
||||
]]
|
||||
});
|
||||
element.render();
|
||||
|
||||
// 事件监听
|
||||
$('#mysql-start').click(function () {
|
||||
layer.confirm('确定要启动MySQL吗?', {
|
||||
btn: ['启动', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('正在启动MySQL...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/start"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL启动失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('MySQL启动成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#mysql-stop').click(function () {
|
||||
layer.confirm('停止MySQL将导致使用MySQL的网站无法访问,是否继续停止?', {
|
||||
btn: ['停止', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('正在停止MySQL...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/stop"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL停止失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('MySQL停止成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#mysql-restart').click(function () {
|
||||
layer.confirm('重启MySQL将导致使用MySQL的网站短时间无法访问,是否继续重启?', {
|
||||
btn: ['重启', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('正在重启MySQL...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/restart"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL重启失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('MySQL重启成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#mysql-reload').click(function () {
|
||||
index = layer.msg('正在重载MySQL...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/reload"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL重载失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('MySQL重载成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#mysql-config-save').click(function () {
|
||||
index = layer.msg('正在保存配置...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/config"
|
||||
, type: 'post'
|
||||
, data: {
|
||||
config: mysql_config_editor.getValue()
|
||||
}
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL配置保存失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
layer.alert('MySQL配置保存成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#mysql-clean-error-log').click(function () {
|
||||
index = layer.msg('正在清空错误日志...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/clearErrorLog"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL错误日志清空失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
layer.msg('MySQL错误日志已清空!');
|
||||
setTimeout(function () {
|
||||
admin.events.refresh();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#mysql-clean-slow-log').click(function () {
|
||||
index = layer.msg('正在清空慢查询日志...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/clearSlowLog"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:MySQL慢查询日志清空失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
layer.msg('MySQL慢查询日志已清空!');
|
||||
setTimeout(function () {
|
||||
admin.events.refresh();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
<!--
|
||||
Name: MySQL管理器 - 添加数据库
|
||||
Author: 耗子
|
||||
Date: 2023-07-22
|
||||
-->
|
||||
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
|
||||
<form class="layui-form" action="" lay-filter="add-mysql-database-form">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">数据库名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="database" lay-verify="required" placeholder="请输入数据库名"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="user" lay-verify="required" placeholder="请输入用户名"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="password" lay-verify="required" placeholder="请输入密码(8位以上大小写数字特殊符号混合)"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<div class="layui-footer">
|
||||
<button class="layui-btn" lay-submit="" lay-filter="add-mysql-database-submit">立即提交</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
<script>
|
||||
layui.data.sendParams = function (params) {
|
||||
layui.use(['admin', 'form'], function () {
|
||||
var $ = layui.$
|
||||
, admin = layui.admin
|
||||
, layer = layui.layer
|
||||
, form = layui.form
|
||||
, table = layui.table
|
||||
|
||||
form.render();
|
||||
|
||||
// 提交
|
||||
form.on('submit(add-mysql-database-submit)', function (data) {
|
||||
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/addDatabase"
|
||||
, type: 'post'
|
||||
, data: data.field
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:数据库添加失败,接口返回' + result);
|
||||
layer.msg('数据库添加失败,请刷新重试!')
|
||||
return false;
|
||||
}
|
||||
table.reload('mysql-database-list');
|
||||
table.reload('mysql-user-list');
|
||||
layer.alert('数据库添加成功!', {
|
||||
icon: 1
|
||||
, title: '提示'
|
||||
, btn: ['确定']
|
||||
, yes: function (index) {
|
||||
layer.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
<!--
|
||||
Name: MySQL管理器 - 添加用户
|
||||
Author: 耗子
|
||||
Date: 2023-07-22
|
||||
-->
|
||||
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
|
||||
<form class="layui-form" action="" lay-filter="add-mysql-user-form">
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">用户名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="user" lay-verify="required" placeholder="请输入用户名"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item layui-form-text">
|
||||
<label class="layui-form-label">密码</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="password" lay-verify="required" placeholder="请输入密码(8位以上大小写数字特殊符号混合)"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">数据库</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="database" lay-verify="required" placeholder="输入授权给该用户的数据库名"
|
||||
autocomplete="off" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<div class="layui-footer">
|
||||
<button class="layui-btn" lay-submit="" lay-filter="add-mysql-user-submit">立即提交</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
<script>
|
||||
layui.data.sendParams = function (params) {
|
||||
layui.use(['admin', 'form'], function () {
|
||||
var $ = layui.$
|
||||
, admin = layui.admin
|
||||
, layer = layui.layer
|
||||
, form = layui.form
|
||||
, table = layui.table
|
||||
|
||||
form.render();
|
||||
|
||||
// 提交
|
||||
form.on('submit(add-mysql-user-submit)', function (data) {
|
||||
index = layer.msg('正在提交...', {icon: 16, time: 0, shade: 0.3});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/addUser"
|
||||
, type: 'post'
|
||||
, data: data.field
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:用户添加失败,接口返回' + result);
|
||||
layer.msg('用户添加失败,请刷新重试!')
|
||||
return false;
|
||||
}
|
||||
table.reload('mysql-database-list');
|
||||
table.reload('mysql-user-list');
|
||||
layer.alert('用户添加成功!', {
|
||||
icon: 1
|
||||
, title: '提示'
|
||||
, btn: ['确定']
|
||||
, yes: function (index) {
|
||||
layer.closeAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,145 @@
|
||||
<!--
|
||||
Name: MySQL管理器 - 数据库备份
|
||||
Author: 耗子
|
||||
Date: 2023-07-22
|
||||
-->
|
||||
<script type="text/html" template lay-done="layui.data.sendParams(d.params)">
|
||||
<div class="layui-row">
|
||||
<div class="layui-col-xs12 layui-col-sm12 layui-col-md12">
|
||||
<table class="layui-hide" id="mysql-backup-list" lay-filter="mysql-backup-list"></table>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
<!-- 备份顶部工具栏 -->
|
||||
<script type="text/html" id="mysql-database-backup-bar">
|
||||
<div class="layui-btn-container">
|
||||
<button class="layui-btn layui-btn-sm" lay-event="backup_database">备份数据库</button>
|
||||
<button class="layui-btn layui-btn-sm" id="upload_mysql_backup">上传备份</button>
|
||||
</div>
|
||||
</script>
|
||||
<!-- 备份右侧管理 -->
|
||||
<script type="text/html" id="mysql-database-backup-control">
|
||||
<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="restore">恢复</a>
|
||||
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>
|
||||
</script>
|
||||
<script>
|
||||
layui.data.sendParams = function (params) {
|
||||
layui.use(['admin', 'form', 'laydate', 'code'], function () {
|
||||
var $ = layui.$
|
||||
, admin = layui.admin
|
||||
, layer = layui.layer
|
||||
, table = layui.table
|
||||
, upload = layui.upload;
|
||||
|
||||
// 渲染表格
|
||||
table.render({
|
||||
elem: '#mysql-backup-list'
|
||||
, url: '/api/plugins/mysql80/backup'
|
||||
, toolbar: '#mysql-database-backup-bar'
|
||||
, title: '备份列表'
|
||||
, cols: [[
|
||||
{field: 'name', title: '备份名称', width: 500}
|
||||
, {field: 'size', title: '文件大小'}
|
||||
, {field: 'right', title: '操作', width: 150, toolbar: '#mysql-database-backup-control'}
|
||||
]]
|
||||
, text: {
|
||||
none: '无备份数据'
|
||||
}
|
||||
, done: function (res, curr, count) {
|
||||
upload.render({
|
||||
elem: '#upload_mysql_backup'
|
||||
, url: '/api/plugins/mysql80/uploadBackup'
|
||||
, accept: 'file'
|
||||
, ext: 'sql|zip|rar|tar|gz|bz2'
|
||||
, before: function (obj) {
|
||||
index = layer.msg('正在上传备份文件,可能需要较长时间,请勿操作...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
});
|
||||
}
|
||||
, done: function (res) {
|
||||
layer.close(index);
|
||||
layer.msg('上传成功!', {icon: 1});
|
||||
table.reload('mysql-backup-list');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// 头工具栏事件
|
||||
table.on('toolbar(mysql-backup-list)', function (obj) {
|
||||
if (obj.event === 'backup_database') {
|
||||
index = layer.msg('正在备份数据库,请稍等...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
});
|
||||
admin.req({
|
||||
url: '/api/plugins/mysql80/createBackup'
|
||||
, type: 'post'
|
||||
, data: {
|
||||
database: params.data.name
|
||||
}
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:备份数据库失败,接口返回' + result);
|
||||
layer.alert('备份失败!');
|
||||
return false;
|
||||
}
|
||||
table.reload('mysql-backup-list');
|
||||
layer.msg('备份成功!', {icon: 1});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// 行工具事件
|
||||
table.on('tool(mysql-backup-list)', function (obj) {
|
||||
let data = obj.data;
|
||||
if (obj.event === 'del') {
|
||||
layer.confirm('确定要删除数据库备份 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
|
||||
index = layer.msg('正在删除数据库备份,请稍等...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
});
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/deleteBackup"
|
||||
, type: 'post'
|
||||
, data: data
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:数据库备份删除失败,接口返回' + result);
|
||||
layer.msg('数据库备份删除失败,请刷新重试!')
|
||||
return false;
|
||||
}
|
||||
obj.del();
|
||||
layer.alert('数据库备份' + data.name + '删除成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (obj.event === 'restore') {
|
||||
layer.confirm('高风险操作,确定要恢复数据库备份 <b style="color: red;">' + data.name + '</b> 吗?', function (index) {
|
||||
index = layer.msg('正在恢复数据库备份,可能需要较长时间,请勿操作...', {
|
||||
icon: 16
|
||||
, time: 0
|
||||
});
|
||||
data.database = params.data.name;
|
||||
admin.req({
|
||||
url: "/api/plugins/mysql80/restoreBackup"
|
||||
, type: 'post'
|
||||
, data: data
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:数据库恢复失败,接口返回' + result);
|
||||
layer.msg('数据库备份恢复失败,请刷新重试!')
|
||||
return false;
|
||||
}
|
||||
layer.alert('数据库备份' + data.name + '恢复成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@@ -43,7 +43,7 @@ Date: 2023-06-24
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-btn-container">
|
||||
<button id="openresty-clean-error-log" class="layui-btn">清空日志</button>
|
||||
<button id="openresty-clear-error-log" class="layui-btn">清空日志</button>
|
||||
</div>
|
||||
<pre id="openresty-error-log" class="layui-code">
|
||||
获取中...
|
||||
@@ -68,7 +68,7 @@ Date: 2023-06-24
|
||||
|
||||
admin.req({
|
||||
url: '/api/plugins/openresty/status'
|
||||
, method: 'get'
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:OpenResty运行状态获取失败,接口返回' + result)
|
||||
@@ -86,7 +86,7 @@ Date: 2023-06-24
|
||||
// 获取openresty错误日志并渲染
|
||||
admin.req({
|
||||
url: '/api/plugins/openresty/errorLog'
|
||||
, method: 'get'
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:OpenResty错误日志获取失败,接口返回' + result)
|
||||
@@ -114,7 +114,7 @@ Date: 2023-06-24
|
||||
// 获取openresty配置并渲染
|
||||
admin.req({
|
||||
url: '/api/plugins/openresty/config'
|
||||
, method: 'get'
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:OpenResty主配置获取失败,接口返回' + result)
|
||||
@@ -141,54 +141,50 @@ Date: 2023-06-24
|
||||
|
||||
// 事件监听
|
||||
$('#openresty-start').click(function () {
|
||||
admin.popup({
|
||||
title: '<span style="color: red;">警告</span>'
|
||||
,
|
||||
shade: 0
|
||||
,
|
||||
anim: -1
|
||||
,
|
||||
area: ['300px', '200px']
|
||||
,
|
||||
id: 'layadmin-layer-skin-openresty-start'
|
||||
,
|
||||
skin: 'layui-anim layui-anim-upbit'
|
||||
,
|
||||
content: '面板的正常访问依赖OpenResty,因此不支持在面板启动OpenResty,如您确需操作,请在SSH执行<span class="layui-badge-rim">systemctl start nginx</span>以启动OpenResty!'
|
||||
})
|
||||
index = layer.msg('正在启动OpenResty...', { icon: 16, time: 0, shade: 0.3 })
|
||||
admin.req({
|
||||
url: '/api/plugins/openresty/start'
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index)
|
||||
if (result.code !== 0) {
|
||||
return false
|
||||
}
|
||||
admin.events.refresh()
|
||||
layer.alert('OpenResty启动成功!')
|
||||
}
|
||||
})
|
||||
})
|
||||
$('#openresty-stop').click(function () {
|
||||
admin.popup({
|
||||
title: '<span style="color: red;">警告</span>'
|
||||
,
|
||||
shade: 0
|
||||
,
|
||||
anim: -1
|
||||
,
|
||||
area: ['300px', '200px']
|
||||
,
|
||||
id: 'layadmin-layer-skin-openresty-stop'
|
||||
,
|
||||
skin: 'layui-anim layui-anim-upbit'
|
||||
,
|
||||
content: '面板的正常访问依赖OpenResty,因此不支持在面板停止OpenResty,如您确需操作,请在SSH执行<span class="layui-badge-rim">systemctl stop nginx</span>以停止OpenResty!'
|
||||
})
|
||||
layer.confirm('停止OpenResty将导致网站无法访问,是否继续停止?', {
|
||||
btn: ['停止', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('正在停止OpenResty...', { icon: 16, time: 0, shade: 0.3 })
|
||||
admin.req({
|
||||
url: '/api/plugins/openresty/stop'
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index)
|
||||
if (result.code !== 0) {
|
||||
return false
|
||||
}
|
||||
admin.events.refresh()
|
||||
layer.alert('OpenResty停止成功!')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
$('#openresty-restart').click(function () {
|
||||
layer.confirm('重启OpenResty有可能导致面板短时间无法访问,是否继续重启?', {
|
||||
layer.confirm('重启OpenResty将导致网站短时间无法访问,是否继续重启?', {
|
||||
btn: ['重启', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('正在重启OpenResty...', { icon: 16, time: 0 })
|
||||
index = layer.msg('正在重启OpenResty...', { icon: 16, time: 0, shade: 0.3 })
|
||||
admin.req({
|
||||
url: '/api/plugins/openresty/restart'
|
||||
, method: 'post'
|
||||
, beforeSend: function () {
|
||||
layer.msg('已发送重启请求,请稍后刷新确认重启状态。')
|
||||
}
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index)
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:OpenResty重启失败,接口返回' + result)
|
||||
return false
|
||||
}
|
||||
admin.events.refresh()
|
||||
@@ -198,43 +194,42 @@ Date: 2023-06-24
|
||||
})
|
||||
})
|
||||
$('#openresty-reload').click(function () {
|
||||
index = layer.msg('正在重载OpenResty...', { icon: 16, time: 0 })
|
||||
index = layer.msg('正在重载OpenResty...', { icon: 16, time: 0, shade: 0.3 })
|
||||
admin.req({
|
||||
url: '/api/plugins/openresty/reload'
|
||||
, method: 'post'
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index)
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:OpenResty重载失败,接口返回' + result)
|
||||
return false
|
||||
}
|
||||
admin.events.refresh()
|
||||
layer.alert('OpenResty重载成功!')
|
||||
}
|
||||
})
|
||||
})
|
||||
$('#openresty-config-save').click(function () {
|
||||
index = layer.msg('正在保存OpenResty主配置...', { icon: 16, time: 0 })
|
||||
index = layer.msg('正在保存OpenResty主配置...', { icon: 16, time: 0, shade: 0.3 })
|
||||
admin.req({
|
||||
url: '/api/plugins/openresty/config'
|
||||
, method: 'post'
|
||||
, type: 'post'
|
||||
, data: {
|
||||
config: openresty_config_editor.getValue()
|
||||
}
|
||||
, success: function (result) {
|
||||
layer.close(index)
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:OpenResty配置保存失败,接口返回' + result)
|
||||
return false
|
||||
}
|
||||
layer.alert('OpenResty配置保存成功!')
|
||||
}
|
||||
})
|
||||
})
|
||||
$('#openresty-clean-error-log').click(function () {
|
||||
index = layer.msg('正在清空OpenResty错误日志...', { icon: 16, time: 0 })
|
||||
$('#openresty-clear-error-log').click(function () {
|
||||
index = layer.msg('正在清空OpenResty错误日志...', { icon: 16, time: 0, shade: 0.3 })
|
||||
admin.req({
|
||||
url: '/api/plugins/openresty/cleanErrorLog'
|
||||
, method: 'post'
|
||||
url: '/api/plugins/openresty/clearErrorLog'
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index)
|
||||
if (result.code !== 0) {
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
<!--
|
||||
Name: PHP管理器
|
||||
Author: 耗子
|
||||
Date: 2023-07-22
|
||||
-->
|
||||
<title>PHP-7.4</title>
|
||||
<div class="layui-fluid" id="component-tabs">
|
||||
<div class="layui-row">
|
||||
<div class="layui-col-md12">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">PHP-7.4管理</div>
|
||||
<div class="layui-card-body">
|
||||
<div class="layui-tab">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this">运行状态</li>
|
||||
<li>拓展管理</li>
|
||||
<li>配置修改</li>
|
||||
<li>负载状态</li>
|
||||
<li>运行日志</li>
|
||||
<li>慢日志</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<div class="layui-tab-item layui-show">
|
||||
<blockquote id="php74-status" class="layui-elem-quote layui-quote-nm">当前状态:<span
|
||||
class="layui-badge layui-bg-black">获取中</span></blockquote>
|
||||
<div class="layui-btn-container" style="padding-top: 30px;">
|
||||
<button id="php74-start" class="layui-btn">启动</button>
|
||||
<button id="php74-stop" class="layui-btn layui-btn-danger">停止</button>
|
||||
<button id="php74-restart" class="layui-btn layui-btn-warm">重启</button>
|
||||
<button id="php74-reload" class="layui-btn layui-btn-normal">重载</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<table id="php74-extension" lay-filter="php74-extension"></table>
|
||||
<!-- 操作按钮模板 -->
|
||||
<script type="text/html" id="php74-extension-control">
|
||||
{{# if(d.installed == true){ }}
|
||||
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="uninstall">卸载</a>
|
||||
{{# } else{ }}
|
||||
<a class="layui-btn layui-btn-xs" lay-event="install">安装</a>
|
||||
{{# } }}
|
||||
</script>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<blockquote class="layui-elem-quote">此处修改的是PHP主配置文件,如果你不了解各参数的含义,请不要随意修改!<br>
|
||||
提示:Ctrl+F 搜索关键字,Ctrl+S 保存,Ctrl+H 查找替换!
|
||||
</blockquote>
|
||||
<div id="php74-config-editor"
|
||||
style="height: 600px;"></div>
|
||||
<div class="layui-btn-container" style="padding-top: 30px;">
|
||||
<button id="php74-config-save" class="layui-btn">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<table class="layui-hide" id="php74-load-status"></table>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-btn-container">
|
||||
<button id="php74-clean-error-log" class="layui-btn">清空日志</button>
|
||||
</div>
|
||||
<pre id="php74-error-log" class="layui-code">
|
||||
获取中...
|
||||
</pre>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-btn-container">
|
||||
<button id="php74-clean-slow-log" class="layui-btn">清空日志</button>
|
||||
</div>
|
||||
<pre id="php74-slow-log" class="layui-code">
|
||||
获取中...
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let php74_config_editor;
|
||||
layui.use(['index', 'code', 'table'], function () {
|
||||
let $ = layui.$
|
||||
, admin = layui.admin
|
||||
, element = layui.element
|
||||
, code = layui.code
|
||||
, table = layui.table;
|
||||
|
||||
// 获取php74运行状态并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/php74/status"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:PHP运行状态获取失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
if (result.data) {
|
||||
$('#php74-status').html('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
|
||||
} else {
|
||||
$('#php74-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// 获取php74错误日志并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/php74/errorLog"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
$('#php74-error-log').text('PHP日志获取失败,请刷新重试!');
|
||||
code({
|
||||
elem: '#php74-error-log'
|
||||
, title: 'php-fpm.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
return false;
|
||||
}
|
||||
$('#php74-error-log').text(result.data);
|
||||
code({
|
||||
elem: '#php74-error-log'
|
||||
, title: 'php-fpm.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取php74慢日志并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/php74/slowLog"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
$('#php74-slow-log').text('PHP慢日志获取失败,请刷新重试!');
|
||||
code({
|
||||
elem: '#php74-slow-log'
|
||||
, title: 'slow.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
return false;
|
||||
}
|
||||
$('#php74-slow-log').text(result.data);
|
||||
code({
|
||||
elem: '#php74-slow-log'
|
||||
, title: 'slow.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取php74配置并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/php74/config"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:PHP主配置获取失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
$('#php74-config-editor').text(result.data);
|
||||
php74_config_editor = ace.edit("php74-config-editor", {
|
||||
mode: "ace/mode/ini",
|
||||
selectionStyle: "text"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取php74负载状态并渲染
|
||||
table.render({
|
||||
elem: '#php74-load-status'
|
||||
, url: '/api/plugins/php74/load'
|
||||
, cols: [[
|
||||
{field: 'name', width: '80%', title: '属性',}
|
||||
, {field: 'value', width: '20%', title: '当前值'}
|
||||
]]
|
||||
});
|
||||
element.render();
|
||||
|
||||
// 获取php74扩展并渲染
|
||||
table.render({
|
||||
elem: '#php74-extension'
|
||||
, url: '/api/plugins/php74/extensions'
|
||||
, cols: [[
|
||||
{field: 'slug', hide: true, title: 'Slug', sort: true}
|
||||
, {field: 'name', width: '20%', title: '拓展名'}
|
||||
, {field: 'description', width: '70%', title: '描述'}
|
||||
, {
|
||||
field: 'control',
|
||||
title: '操作',
|
||||
templet: '#php74-extension-control',
|
||||
fixed: 'right',
|
||||
align: 'left'
|
||||
}
|
||||
]]
|
||||
, page: false
|
||||
, text: {
|
||||
none: '暂无拓展'
|
||||
}
|
||||
});
|
||||
// 工具条
|
||||
table.on('tool(php74-extension)', function (obj) {
|
||||
let data = obj.data;
|
||||
if (obj.event === 'install') {
|
||||
layer.confirm('确定安装该拓展吗?', function (index) {
|
||||
layer.close(index);
|
||||
index = layer.msg('请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: '/api/plugins/php74/installExtension',
|
||||
type: 'POST',
|
||||
data: {
|
||||
slug: data.slug
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.close(index);
|
||||
table.reload('php74-extension');
|
||||
layer.msg('安装:' + data.name + ' 成功加入任务队列', {
|
||||
icon: 1,
|
||||
time: 1000
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.msg, {icon: 2, time: 1000});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (obj.event === 'uninstall') {
|
||||
layer.confirm('确定卸载该拓展吗?', function (index) {
|
||||
layer.close(index);
|
||||
index = layer.msg('请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: '/api/plugins/php74/uninstallExtension',
|
||||
type: 'POST',
|
||||
data: {
|
||||
slug: data.slug
|
||||
}
|
||||
, success: function (res) {
|
||||
layer.close(index);
|
||||
if (res.code === 0) {
|
||||
table.reload('php74-extension');
|
||||
layer.msg('卸载:' + data.name + ' 成功加入任务队列', {icon: 1, time: 1000});
|
||||
} else {
|
||||
layer.msg(res.msg, {icon: 2, time: 1000});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 事件监听
|
||||
$('#php74-start').click(function () {
|
||||
index = layer.msg('正在启动PHP,请稍后...', {icon: 16, time: 0});
|
||||
layer.confirm('确定要启动PHP吗?', {
|
||||
btn: ['启动', '取消']
|
||||
}, function () {
|
||||
admin.req({
|
||||
url: "/api/plugins/php74/start"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('PHP启动成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#php74-stop').click(function () {
|
||||
layer.confirm('停止PHP将导致使用PHP的网站无法访问,是否继续停止?', {
|
||||
btn: ['停止', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('正在停止PHP,请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/php74/stop"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('PHP停止成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#php74-restart').click(function () {
|
||||
layer.confirm('重启PHP将导致使用PHP的网站短时间无法访问,是否继续重启?', {
|
||||
btn: ['重启', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('正在重启PHP,请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/php74/restart"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('PHP重启成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#php74-reload').click(function () {
|
||||
index = layer.msg('正在重载PHP,请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/php74/reload"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
layer.alert('PHP重载成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#php74-config-save').click(function () {
|
||||
index = layer.msg('正在保存配置,请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/php74/config"
|
||||
, type: 'post'
|
||||
, data: {
|
||||
config: php74_config_editor.getValue()
|
||||
}
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
layer.alert('PHP配置保存成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#php74-clean-error-log').click(function () {
|
||||
index = layer.msg('正在清空错误日志,请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/php74/clearErrorLog"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.msg('PHP日志已清空!');
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#php74-clean-slow-log').click(function () {
|
||||
index = layer.msg('正在清空慢日志,请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/php74/clearSlowLog"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.msg('PHP慢日志已清空!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,381 @@
|
||||
<!--
|
||||
Name: PHP管理器
|
||||
Author: 耗子
|
||||
Date: 2023-07-22
|
||||
-->
|
||||
<title>PHP-8.0</title>
|
||||
<div class="layui-fluid" id="component-tabs">
|
||||
<div class="layui-row">
|
||||
<div class="layui-col-md12">
|
||||
<div class="layui-card">
|
||||
<div class="layui-card-header">PHP-8.0管理</div>
|
||||
<div class="layui-card-body">
|
||||
<div class="layui-tab">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this">运行状态</li>
|
||||
<li>拓展管理</li>
|
||||
<li>配置修改</li>
|
||||
<li>负载状态</li>
|
||||
<li>运行日志</li>
|
||||
<li>慢日志</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<div class="layui-tab-item layui-show">
|
||||
<blockquote id="php80-status" class="layui-elem-quote layui-quote-nm">当前状态:<span
|
||||
class="layui-badge layui-bg-black">获取中</span></blockquote>
|
||||
<div class="layui-btn-container" style="padding-top: 30px;">
|
||||
<button id="php80-start" class="layui-btn">启动</button>
|
||||
<button id="php80-stop" class="layui-btn layui-btn-danger">停止</button>
|
||||
<button id="php80-restart" class="layui-btn layui-btn-warm">重启</button>
|
||||
<button id="php80-reload" class="layui-btn layui-btn-normal">重载</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<table id="php80-extension" lay-filter="php80-extension"></table>
|
||||
<!-- 操作按钮模板 -->
|
||||
<script type="text/html" id="php80-extension-control">
|
||||
{{# if(d.installed == true){ }}
|
||||
<a class="layui-btn layui-btn-warm layui-btn-xs" lay-event="uninstall">卸载</a>
|
||||
{{# } else{ }}
|
||||
<a class="layui-btn layui-btn-xs" lay-event="install">安装</a>
|
||||
{{# } }}
|
||||
</script>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<blockquote class="layui-elem-quote">此处修改的是PHP主配置文件,如果你不了解各参数的含义,请不要随意修改!<br>
|
||||
提示:Ctrl+F 搜索关键字,Ctrl+S 保存,Ctrl+H 查找替换!
|
||||
</blockquote>
|
||||
<div id="php80-config-editor"
|
||||
style="height: 600px;"></div>
|
||||
<div class="layui-btn-container" style="padding-top: 30px;">
|
||||
<button id="php80-config-save" class="layui-btn">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<table class="layui-hide" id="php80-load-status"></table>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-btn-container">
|
||||
<button id="php80-clean-error-log" class="layui-btn">清空日志</button>
|
||||
</div>
|
||||
<pre id="php80-error-log" class="layui-code">
|
||||
获取中...
|
||||
</pre>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-btn-container">
|
||||
<button id="php80-clean-slow-log" class="layui-btn">清空日志</button>
|
||||
</div>
|
||||
<pre id="php80-slow-log" class="layui-code">
|
||||
获取中...
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let php80_config_editor;
|
||||
layui.use(['index', 'code', 'table'], function () {
|
||||
let $ = layui.$
|
||||
, admin = layui.admin
|
||||
, element = layui.element
|
||||
, code = layui.code
|
||||
, table = layui.table;
|
||||
|
||||
// 获取php80运行状态并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/php80/status"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:PHP运行状态获取失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
if (result.data) {
|
||||
$('#php80-status').html('当前状态:<span class="layui-badge layui-bg-green">运行中</span>');
|
||||
} else {
|
||||
$('#php80-status').html('当前状态:<span class="layui-badge layui-bg-red">已停止</span>');
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// 获取php80错误日志并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/php80/errorLog"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
$('#php80-error-log').text('PHP日志获取失败,请刷新重试!');
|
||||
code({
|
||||
elem: '#php80-error-log'
|
||||
, title: 'php-fpm.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
return false;
|
||||
}
|
||||
$('#php80-error-log').text(result.data);
|
||||
code({
|
||||
elem: '#php80-error-log'
|
||||
, title: 'php-fpm.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取php80慢日志并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/php80/slowLog"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
$('#php80-slow-log').text('PHP慢日志获取失败,请刷新重试!');
|
||||
code({
|
||||
elem: '#php80-slow-log'
|
||||
, title: 'slow.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
return false;
|
||||
}
|
||||
$('#php80-slow-log').text(result.data);
|
||||
code({
|
||||
elem: '#php80-slow-log'
|
||||
, title: 'slow.log'
|
||||
, encode: true
|
||||
, about: false
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取php80配置并渲染
|
||||
admin.req({
|
||||
url: "/api/plugins/php80/config"
|
||||
, type: 'get'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
console.log('耗子Linux面板:PHP主配置获取失败,接口返回' + result);
|
||||
return false;
|
||||
}
|
||||
$('#php80-config-editor').text(result.data);
|
||||
php80_config_editor = ace.edit("php80-config-editor", {
|
||||
mode: "ace/mode/ini",
|
||||
selectionStyle: "text"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取php80负载状态并渲染
|
||||
table.render({
|
||||
elem: '#php80-load-status'
|
||||
, url: '/api/plugins/php80/load'
|
||||
, cols: [[
|
||||
{field: 'name', width: '80%', title: '属性',}
|
||||
, {field: 'value', width: '20%', title: '当前值'}
|
||||
]]
|
||||
});
|
||||
element.render();
|
||||
|
||||
// 获取php80扩展并渲染
|
||||
table.render({
|
||||
elem: '#php80-extension'
|
||||
, url: '/api/plugins/php80/extensions'
|
||||
, cols: [[
|
||||
{field: 'slug', hide: true, title: 'Slug', sort: true}
|
||||
, {field: 'name', width: '20%', title: '拓展名'}
|
||||
, {field: 'description', width: '70%', title: '描述'}
|
||||
, {
|
||||
field: 'control',
|
||||
title: '操作',
|
||||
templet: '#php80-extension-control',
|
||||
fixed: 'right',
|
||||
align: 'left'
|
||||
}
|
||||
]]
|
||||
, page: false
|
||||
, text: {
|
||||
none: '暂无拓展'
|
||||
}
|
||||
});
|
||||
// 工具条
|
||||
table.on('tool(php80-extension)', function (obj) {
|
||||
let data = obj.data;
|
||||
if (obj.event === 'install') {
|
||||
layer.confirm('确定安装该拓展吗?', function (index) {
|
||||
layer.close(index);
|
||||
index = layer.msg('请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: '/api/plugins/php80/installExtension',
|
||||
type: 'POST',
|
||||
data: {
|
||||
slug: data.slug
|
||||
}
|
||||
, success: function (res) {
|
||||
if (res.code === 0) {
|
||||
layer.close(index);
|
||||
table.reload('php80-extension');
|
||||
layer.msg('安装:' + data.name + ' 成功加入任务队列', {
|
||||
icon: 1,
|
||||
time: 1000
|
||||
});
|
||||
} else {
|
||||
layer.msg(res.msg, {icon: 2, time: 1000});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (obj.event === 'uninstall') {
|
||||
layer.confirm('确定卸载该拓展吗?', function (index) {
|
||||
layer.close(index);
|
||||
index = layer.msg('请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: '/api/plugins/php80/uninstallExtension',
|
||||
type: 'POST',
|
||||
data: {
|
||||
slug: data.slug
|
||||
}
|
||||
, success: function (res) {
|
||||
layer.close(index);
|
||||
if (res.code === 0) {
|
||||
table.reload('php80-extension');
|
||||
layer.msg('卸载:' + data.name + ' 成功加入任务队列', {icon: 1, time: 1000});
|
||||
} else {
|
||||
layer.msg(res.msg, {icon: 2, time: 1000});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 事件监听
|
||||
$('#php80-start').click(function () {
|
||||
index = layer.msg('正在启动PHP,请稍后...', {icon: 16, time: 0});
|
||||
layer.confirm('确定要启动PHP吗?', {
|
||||
btn: ['启动', '取消']
|
||||
}, function () {
|
||||
admin.req({
|
||||
url: "/api/plugins/php80/start"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('PHP启动成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#php80-stop').click(function () {
|
||||
layer.confirm('停止PHP将导致使用PHP的网站无法访问,是否继续停止?', {
|
||||
btn: ['停止', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('正在停止PHP,请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/php80/stop"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('PHP停止成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#php80-restart').click(function () {
|
||||
layer.confirm('重启PHP将导致使用PHP的网站短时间无法访问,是否继续重启?', {
|
||||
btn: ['重启', '取消']
|
||||
}, function () {
|
||||
index = layer.msg('正在重启PHP,请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/php80/restart"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.alert('PHP重启成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#php80-reload').click(function () {
|
||||
index = layer.msg('正在重载PHP,请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/php80/reload"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
layer.alert('PHP重载成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#php80-config-save').click(function () {
|
||||
index = layer.msg('正在保存配置,请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/php80/config"
|
||||
, type: 'post'
|
||||
, data: {
|
||||
config: php80_config_editor.getValue()
|
||||
}
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
layer.alert('PHP配置保存成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#php80-clean-error-log').click(function () {
|
||||
index = layer.msg('正在清空错误日志,请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/php80/clearErrorLog"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
layer.close(index);
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.msg('PHP日志已清空!');
|
||||
}
|
||||
});
|
||||
});
|
||||
$('#php80-clean-slow-log').click(function () {
|
||||
index = layer.msg('正在清空慢日志,请稍后...', {icon: 16, time: 0});
|
||||
admin.req({
|
||||
url: "/api/plugins/php80/clearSlowLog"
|
||||
, type: 'post'
|
||||
, success: function (result) {
|
||||
if (result.code !== 0) {
|
||||
return false;
|
||||
}
|
||||
admin.events.refresh();
|
||||
layer.msg('PHP慢日志已清空!');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user