mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
Compare commits
228 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70e9342c5d | |||
| 0756a7b507 | |||
| 47f104bcae | |||
| d20e57dca9 | |||
| 09cc10a563 | |||
| b188b3f1b8 | |||
| 99dac8721e | |||
| c93eb4b309 | |||
| 266f12d15a | |||
| 3a18b36df2 | |||
| ebcc022418 | |||
| aa0fb2afcc | |||
| bde2c50696 | |||
| 0945949b37 | |||
| 4fcd65da70 | |||
| afb12dd664 | |||
| 8c5d0e905e | |||
| 92e77720f7 | |||
| 124912368e | |||
| 084f0a06ca | |||
| c08fd0af7f | |||
| 3229685729 | |||
| 77ae021a2f | |||
| 69cf5dcb65 | |||
| 9bd9833ca9 | |||
| 5fb58476ba | |||
| da4a04ad87 | |||
| ae4736d86e | |||
| 198c56cb25 | |||
| 4950b48fc9 | |||
| 3f323588c0 | |||
| e651a8ed6b | |||
| bc9274a8b7 | |||
| 1047374c2b | |||
| 1f2f51fb55 | |||
| 872af0b298 | |||
| bc22e23cda | |||
| 4ca2446dc3 | |||
| 840d6824f0 | |||
| f366e4e267 | |||
| 25436325b2 | |||
| 25d295f4a1 | |||
| f8d874b9d7 | |||
| 959fdaa6de | |||
| c4d672173e | |||
| 262e72c17a | |||
| 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 | |||
| 6ef713ab3d | |||
| feb377a8f1 | |||
| bce2f27500 | |||
| 5ce65653ad | |||
| ec7138ae2a | |||
| 46644c352d | |||
| 2a3119fe9a | |||
| 6119626f50 | |||
| c004acbfa0 | |||
| da58071a0a | |||
| ab599ef0f7 | |||
| 03f531eff4 | |||
| 09d4d15f72 | |||
| a4cc6570f4 | |||
| 9556aac923 | |||
| 8ef737253c | |||
| 047a743aed | |||
| ab78122036 | |||
| ce918533ab | |||
| e5226c4bf3 | |||
| 98c4764b95 | |||
| 8b31415f5c | |||
| b606431305 | |||
| e2c2717852 | |||
| 4ff6561942 | |||
| f5529b732f | |||
| 397273012c | |||
| 91f1125e6a | |||
| e44a783e9f | |||
| b33a35286d | |||
| 200d763ba4 | |||
| b9b1735d03 |
@@ -2,7 +2,7 @@ root = "."
|
||||
tmp_dir = "storage/temp"
|
||||
|
||||
[build]
|
||||
bin = "./storage/temp/main.exe"
|
||||
bin = "./storage/temp/main.exe --env=panel.conf"
|
||||
cmd = "go build -o ./storage/temp/main.exe ."
|
||||
delay = 1000
|
||||
exclude_dir = ["storage", "database"]
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
|
||||
JWT_SECRET=
|
||||
@@ -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
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
goarch: [ amd64, arm64 ]
|
||||
fail-fast: true
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
cache: true
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
codecov:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 'stable'
|
||||
|
||||
@@ -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: "
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean
|
||||
|
||||
@@ -27,7 +27,7 @@ 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-helper@v3
|
||||
@@ -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
|
||||

|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 'stable'
|
||||
|
||||
@@ -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/"
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 'stable'
|
||||
|
||||
+3
-3
@@ -1,7 +1,6 @@
|
||||
tmp
|
||||
.env
|
||||
.history
|
||||
.air.toml
|
||||
/.air.toml
|
||||
/panel.conf
|
||||
|
||||
# Golang #
|
||||
# `go test -c` 生成的二进制文件
|
||||
@@ -22,6 +21,7 @@ _cgo_export.*
|
||||
*.exe
|
||||
*.o
|
||||
*.so
|
||||
/panel
|
||||
|
||||
# 压缩包 #
|
||||
# Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit
|
||||
|
||||
+2
-1
@@ -30,4 +30,5 @@ archives:
|
||||
- storage/*
|
||||
- database/*
|
||||
- scripts/*
|
||||
- .env.example
|
||||
- resources/*
|
||||
- panel-example.conf
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
## 行为准则
|
||||
|
||||
耗子Linux面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
|
||||
|
||||
- 参与者将容忍反对意见。
|
||||
- 参与者必须确保他们的语言和行为没有人身攻击和贬低个人言论。
|
||||
- 在解释他人的言行时,参与者应始终保持良好的意图。
|
||||
- 不能容忍可合理视为骚扰的行为。
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
The HaoZi Linux Panel complies with the industry's common code of conduct. Any breach of the Code of Conduct can be reported to us:
|
||||
|
||||
- Participants will be tolerant of opposing views.
|
||||
- Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
||||
- When interpreting the words and actions of others, participants should always assume good intentions.
|
||||
- Behavior that can be reasonably considered harassment will not be tolerated.
|
||||
@@ -1,69 +1,121 @@
|
||||
# 耗子Linux面板
|
||||
<h1 align="center">耗子 Linux 面板</h1>
|
||||
|
||||
[](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)
|
||||

|
||||
<p align="center">
|
||||
<a href="https://github.com/haozi-team/panel/releases"><img src="https://img.shields.io/github/release/haozi-team/panel.svg"></a>
|
||||
<a href="https://github.com/haozi-team/panel/actions"><img src="https://github.com/haozi-team/panel/actions/workflows/test.yml/badge.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/haozi-team/panel"><img src="https://goreportcard.com/badge/github.com/haozi-team/panel"></a>
|
||||
<a href="https://codecov.io/gh/haozi-team/panel"><img src="https://codecov.io/gh/haozi-team/panel/branch/main/graph/badge.svg?token=XFT5NGNSRG"></a>
|
||||
<a href="https://img.shields.io/github/license/haozi-team/panel"><img src="https://img.shields.io/github/license/haozi-team/panel"></a>
|
||||
</p>
|
||||
|
||||
这是耗子Linux面板的开源仓库,基于Apache License 2.0协议进行开源,目前处于积极开发状态。
|
||||
<p align="center">
|
||||
[简体中文] | [<a href="README_EN.md">English</a>]
|
||||
</p>
|
||||
|
||||
#### 我们正在进行基于 Golang 的 V2 版本的开发,加入我们的 交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[耗子](https://pd.qq.com/s/fyol46wfy) 获取最新动态。
|
||||
耗子 Linux 面板是针对我们自身业务需要使用 Golang 开发的轻量 Linux 服务器运维管理面板,以 Apache 2.0 协议开源。
|
||||
|
||||
广告: [`WeAvatar` —统一头像服务](https://weavatar.com)
|
||||
免责声明:严禁使用耗子 Linux 面板从事任何非法活动,非法站点请勿向我们请求任何形式的技术支持,如果在技术支持过程中发现非法内容,我们将立即停止技术支持并留存相关证据。
|
||||
|
||||
交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[pd.qq.com/s/fyol46wfy](https://pd.qq.com/s/fyol46wfy)
|
||||
|
||||
## UI 展示
|
||||
|
||||

|
||||
|
||||
## 运行环境
|
||||
|
||||
面板仅支持主流 `amd64` | `arm64` 系统的最新版本,且随着系统版本的更新,面板也会逐步停止对旧版本的支持。
|
||||
耗子Linux面板仅支持 `amd64` | `arm64` 架构下的主流系统的最新版本,不支持 `Ubuntu`,因为其发版太过频繁,难以维护。
|
||||
|
||||
不支持 `Ubuntu`,因为其发版太频繁,难以维护。
|
||||
低配机器建议使用 `Debian`,资源占用较 `RHEL` 系更低。其他机器建议使用 `RockyLinux` | `AlmaLinux`,维护周期更长也更稳定。
|
||||
|
||||
低配机器建议使用 `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面板([推荐: 宝塔 - 简单好用服务器运维面板](https://www.bt.cn/?invite_code=M190eXRpZWE=))。
|
||||
|
||||
如果你决定继续,请以`root`用户登录服务器,执行以下命令安装面板:
|
||||
|
||||
```shell
|
||||
TODO
|
||||
bash <(curl -sSL https://dl.cdn.haozi.net/panel/install_panel.sh)
|
||||
```
|
||||
|
||||
## 日常维护
|
||||
|
||||
使用`panel`命令进行日常维护:
|
||||
|
||||
```shell
|
||||
TODO
|
||||
panel
|
||||
```
|
||||
|
||||
在 [Wiki](https://github.com/haozi-team/panel/wiki) 中查看更多使用方法和技巧。
|
||||
|
||||
## 问题反馈
|
||||
|
||||
使用类问题,可在 [WePublish社区论坛](https://wepublish.cn/forum) 提问寻求帮助。
|
||||
使用类问题,可在 [WePublish 社区论坛](https://wepublish.cn/forums) 提问或QQ群`@坤坤`寻求 AI 帮助,亦可在群里寻求付费支持。
|
||||
|
||||
对于面板自身问题,可在 GitHub 的`Issues`
|
||||
页面提交问题反馈,注意[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。
|
||||
面板自身问题,可在 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),勿在 GitHub 上提交;
|
||||
- 当 PR 开发完毕后,请为其添加 `🚀 Review Ready` 标签,维护者将及时进行评审;
|
||||
- 我们非常欢迎您的贡献,将在下次发版时将您添加到首页贡献者中;❤️
|
||||
|
||||
## 赞助商
|
||||
|
||||
### 服务器
|
||||
|
||||
- [盾云](https://www.ddunyun.com/)
|
||||
- [盾云](https://www.ddunyun.com/aff/PNYAXMKI)
|
||||
|
||||
### CDN
|
||||
|
||||
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
|
||||
|
||||
- [又拍云](https://www.upyun.com/?utm_source=lianmeng&utm_medium=referral)
|
||||
|
||||
- [AnyCast.Ai](https://www.anycast.ai/)
|
||||
|
||||
- [盾云CDN](http://cdn.ddunyun.com/)
|
||||
|
||||
- [又拍云](https://www.upyun.com/)
|
||||
### 对象存储
|
||||
|
||||
#### 接受云资源和资金赞助,可通过QQ群咨询联系
|
||||
- [又拍云](https://www.upyun.com/?utm_source=lianmeng&utm_medium=referral)
|
||||
|
||||
## Star 趋势
|
||||
### DevOps
|
||||
|
||||
[](https://starchart.cc/HaoZi-Team/Panel)
|
||||
- [极狐 GitLab](https://www.jihulab.com/)
|
||||
|
||||
**接受云资源和资金赞助,可通过QQ群咨询联系**
|
||||
|
||||
## 贡献者
|
||||
|
||||
这个项目的存在要归功于所有做出贡献的人,参与贡献请先查看贡献代码部分。
|
||||
|
||||
<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>
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
<h1 align="center">HaoZi Linux Panel</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/haozi-team/panel/releases"><img src="https://img.shields.io/github/release/haozi-team/panel.svg"></a>
|
||||
<a href="https://github.com/haozi-team/panel/actions"><img src="https://github.com/haozi-team/panel/actions/workflows/test.yml/badge.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/haozi-team/panel"><img src="https://goreportcard.com/badge/github.com/haozi-team/panel"></a>
|
||||
<a href="https://codecov.io/gh/haozi-team/panel"><img src="https://codecov.io/gh/haozi-team/panel/branch/main/graph/badge.svg?token=XFT5NGNSRG"></a>
|
||||
<a href="https://img.shields.io/github/license/haozi-team/panel"><img src="https://img.shields.io/github/license/haozi-team/panel"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
[<a href="README.md">简体中文</a>] | [English]
|
||||
</p>
|
||||
|
||||
The HaoZi Linux Panel is a lightweight Linux server operation and maintenance management panel developed using Golang for our own business needs. It is open source under the Apache 2.0 protocol.
|
||||
|
||||
Disclaimer: It is strictly prohibited to use the HaoZi Linux Panel to engage in any illegal activities. Please do not request any form of technical support from us for illegal sites. If illegal content is discovered during the technical support process, we will immediately stop technical support and retain relevant evidence.
|
||||
|
||||
Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ channel: [pd.qq.com/s/fyol46wfy](https://pd.qq.com /s/fyol46wfy)
|
||||
|
||||
## UI Screenshots
|
||||
|
||||

|
||||
|
||||
## Operating Environment
|
||||
|
||||
HaoZi Linux Panel only supports the latest version of mainstream systems under the `amd64` | `arm64` architecture. It does not support `Ubuntu` because its releases are too frequent and difficult to maintain.
|
||||
|
||||
Recommended to use `Debian` for low-configuration machines, as its resource usage is lower than that of the `RHEL` system. For other machines, recommended to use `RockyLinux` | `AlmaLinux`, which has a longer maintenance cycle and is more stable.
|
||||
|
||||
For other systems not in the table below (OpenCloudOS 8, Anolis 8, CentOS Stream 8/9, Debian 11, etc.), you can try to install it yourself, but normal operation is not guaranteed, and free technical support is not provided (theoretically there will be no major question).
|
||||
|
||||
| OS | Version |
|
||||
|------------|---------|
|
||||
| RHEL | 9 |
|
||||
| RockyLinux | 9 |
|
||||
| AlmaLinux | 9 |
|
||||
| Debian | 12 |
|
||||
|
||||
As system versions are constantly updated, we may also terminate support for some older systems to ensure the stability of the panel.
|
||||
|
||||
## Install Panel
|
||||
|
||||
Before installing the panel, you need to understand the basic knowledge of the LNMP environment and how to deal with common LNMP environment problems. It is not recommended for users with zero basic knowledge to install and use HaoZi Linux Panel ([Recommended: aaPanel - easy-to-use server operation and maintenance panel](https://aapanel.com)).
|
||||
|
||||
If you decide to continue, please log in to the server as `root` user and execute the following command to install the panel:
|
||||
|
||||
```shell
|
||||
bash <(curl -sSL https://dl.cdn.haozi.net/panel/install_panel.sh)
|
||||
```
|
||||
|
||||
## Daily Maintenance
|
||||
|
||||
Use `panel` command for daily maintenance:
|
||||
|
||||
```shell
|
||||
panel
|
||||
```
|
||||
|
||||
See more usage methods and tips in [Wiki](https://github.com/haozi-team/panel/wiki).
|
||||
|
||||
## Feedback
|
||||
|
||||
For usage issues, you can ask questions in the [WePublish Community Forum](https://wepublish.cn/forums) or seek AI help in the QQ group `@坤坤`. You can also seek paid support in the group.
|
||||
|
||||
For issues with the panel itself, you can submit feedback on the `Issues` page of GitHub. Please note [the wisdom of asking questions](http://www.catb.org/~esr/faqs/smart-questions.html).
|
||||
|
||||
## Contribute code
|
||||
|
||||
### Find/Create Issue
|
||||
|
||||
You can find or create an Issue in [Issue List](https://github.com/haozi-team/panel/issues), leave a message to express your willingness to deal with the Issue, and get confirmation from the maintainer. Start processing.
|
||||
|
||||
### Create PR
|
||||
|
||||
- During the development process, if you encounter a problem, you can describe the problem in detail in the Issue at any time for further communication, but before doing so, please make sure that you have tried your best to solve the problem through Google and other methods;
|
||||
- PRs must be submitted to our JiHu GitLab repository [https://jihulab.com/haozi-team/panel](https://jihulab.com/haozi-team/panel). Do not submit on GitHub;
|
||||
- When the PR is completed, please add the `🚀 Review Ready` tag to it, and the maintainer will review it in time;
|
||||
- We very much welcome your contributions and will add you to the homepage contributors in the next release; ❤️
|
||||
|
||||
## Sponsor
|
||||
|
||||
### Server
|
||||
|
||||
- [盾云](https://www.ddunyun.com/aff/PNYAXMKI)
|
||||
|
||||
### CDN
|
||||
|
||||
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
|
||||
|
||||
- [又拍云](https://www.upyun.com/?utm_source=lianmeng&utm_medium=referral)
|
||||
|
||||
- [AnyCast.Ai](https://www.anycast.ai/)
|
||||
|
||||
- [盾云CDN](http://cdn.ddunyun.com/)
|
||||
|
||||
### Object Storage
|
||||
|
||||
- [又拍云](https://www.upyun.com/?utm_source=lianmeng&utm_medium=referral)
|
||||
|
||||
### DevOps
|
||||
|
||||
- [JiHu GitLab](https://www.jihulab.com/)
|
||||
|
||||
**Accept cloud resources and financial sponsorship, you can contact us through QQ group**
|
||||
|
||||
## Contributor
|
||||
|
||||
This project owes its existence to all those who have contributed. To contribute, please check the contributed code section first.
|
||||
|
||||
<a href="https://github.com/DevHaoZi" target="_blank"><img src="https://avatars.githubusercontent.com/u/115467771?v=4" width="48" height="48"></a>
|
||||
|
||||
## Star History
|
||||
|
||||
<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>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
## 安全说明
|
||||
|
||||
安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子Linux面板,至今无一例安全事故。
|
||||
|
||||
耗子Linux面板采用业界多种方案尽可能保证面板的安全性,但是我们不能保证面板的绝对安全性,**因此我们不对面板的安全性做任何保证**。
|
||||
|
||||
如果您在使用面板的过程中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
|
||||
|
||||
- 邮箱:i@haozi.net
|
||||
- QQ:826896000
|
||||
|
||||
致某些 CVE 爱好者:通过面板 `access_token` 执行的任何操作(包括且不限于:获取 root 权限、读取/写入系统敏感文件、执行任意 shell 命令等)均不被认为是安全问题,请不要刷此类报告浪费彼此的时间,这类低水平的报告对你的简历也没有任何帮助。
|
||||
|
||||
## Security Policy
|
||||
|
||||
Security is one of our biggest concerns. We have widely used HaoZi Linux Panel in production environments for many different applications, and there has been no safety incident so far.
|
||||
|
||||
HaoZi Linux Panel adopts various solutions in the industry to ensure the security of the panel as much as possible, but we cannot guarantee the absolute security of the panel, **therefore we do not make any guarantees on the security of the panel**.
|
||||
|
||||
If you find any security issues while using the panel, please do not submit an Issue. You can contact us directly through the following methods:
|
||||
|
||||
- Email: i@haozi.net
|
||||
- Telegram: @devhaozi
|
||||
|
||||
To some CVE enthusiasts: Any operation performed through the panel `access_token` (including but not limited to: obtaining root permissions, reading/writing system sensitive files, executing arbitrary shell commands, etc.) is not considered a security issue, please Don’t waste each other’s time with these types of reports. These low-level reports won’t do anything to help your resume.
|
||||
@@ -4,14 +4,15 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gookit/color"
|
||||
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/console/command"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/packages/helpers"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Monitoring struct {
|
||||
@@ -37,14 +38,12 @@ func (receiver *Monitoring) Extend() command.Extend {
|
||||
// Handle Execute the console command.
|
||||
func (receiver *Monitoring) Handle(ctx console.Context) error {
|
||||
var setting models.Setting
|
||||
if err := facades.Orm().Query().Where("key", "monitor").First(&setting); err != nil {
|
||||
return nil
|
||||
}
|
||||
if setting.Value == "0" || len(setting.Value) == 0 {
|
||||
monitor := services.NewSettingImpl().Get(models.SettingKeyMonitor)
|
||||
if !cast.ToBool(monitor) {
|
||||
return nil
|
||||
}
|
||||
|
||||
info := helpers.GetMonitoringInfo()
|
||||
info := tools.GetMonitoringInfo()
|
||||
|
||||
err := facades.Orm().Query().Create(&models.Monitor{
|
||||
Info: info,
|
||||
|
||||
+244
-35
@@ -1,19 +1,22 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"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"
|
||||
"panel/packages/helpers"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Panel struct {
|
||||
@@ -41,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":
|
||||
@@ -51,14 +56,14 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
settings := []models.Setting{{Key: "name", Value: "耗子Linux面板"}, {Key: "monitor", Value: "1"}, {Key: "monitor_days", Value: "30"}, {Key: "backup_path", Value: "/www/backup"}, {Key: "website_path", Value: "/www/wwwroot"}, {Key: "panel_entrance", Value: "/"}}
|
||||
settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子Linux面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyEntrance, Value: "/"}, {Key: models.SettingKeyVersion, Value: facades.Config().GetString("panel.version")}}
|
||||
err = facades.Orm().Query().Create(&settings)
|
||||
if err != nil {
|
||||
color.Redln("初始化失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
hash, err := facades.Hash().Make(helpers.RandomString(32))
|
||||
hash, err := facades.Hash().Make(tools.RandomString(32))
|
||||
if err != nil {
|
||||
color.Redln("初始化失败")
|
||||
return nil
|
||||
@@ -74,7 +79,19 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
color.Greenln("初始化成功")
|
||||
|
||||
case "update":
|
||||
err := helpers.UpdatePanel()
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
|
||||
if err == nil {
|
||||
color.Redln("当前有任务正在执行,禁止更新")
|
||||
return nil
|
||||
}
|
||||
|
||||
input := arg1
|
||||
proxy := false
|
||||
if input == "y" || input == "Y" || input == "yes" || input == "Yes" {
|
||||
proxy = true
|
||||
}
|
||||
err = tools.UpdatePanel(cast.ToBool(proxy))
|
||||
if err != nil {
|
||||
color.Redln("更新失败: " + err.Error())
|
||||
return nil
|
||||
@@ -90,14 +107,14 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
password := helpers.RandomString(16)
|
||||
password := tools.RandomString(16)
|
||||
hash, err := facades.Hash().Make(password)
|
||||
if err != nil {
|
||||
color.Redln("生成密码失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
user.Username = helpers.RandomString(8)
|
||||
user.Username = tools.RandomString(8)
|
||||
user.Password = hash
|
||||
|
||||
err = facades.Orm().Query().Save(&user)
|
||||
@@ -106,29 +123,28 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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("panel_entrance", "/"))
|
||||
color.Greenln("面板端口: " + port)
|
||||
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
|
||||
|
||||
case "getPort":
|
||||
nginxConf, err := os.ReadFile("/www/server/nginx/conf/nginx.conf")
|
||||
if err != nil {
|
||||
color.Redln("获取面板端口失败,请检查Nginx主配置文件")
|
||||
return nil
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`listen\s+(\d+)`).FindStringSubmatch(string(nginxConf))
|
||||
if len(match) < 2 {
|
||||
color.Redln("获取面板端口失败,请检查Nginx主配置文件")
|
||||
return nil
|
||||
}
|
||||
|
||||
port := match[1]
|
||||
port := tools.Exec("cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}'")
|
||||
color.Greenln("面板端口: " + port)
|
||||
|
||||
case "getEntrance":
|
||||
color.Greenln("面板入口: " + services.NewSettingImpl().Get("panel_entrance", "/"))
|
||||
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
|
||||
|
||||
case "deleteEntrance":
|
||||
err := services.NewSettingImpl().Set(models.SettingKeyEntrance, "/")
|
||||
if err != nil {
|
||||
color.Redln("删除面板入口失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("删除面板入口成功")
|
||||
|
||||
case "writePlugin":
|
||||
slug := arg1
|
||||
@@ -176,7 +192,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
|
||||
var setting models.Setting
|
||||
err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{
|
||||
Key: "mysql_root_password",
|
||||
Key: models.SettingKeyMysqlRootPassword,
|
||||
}, models.Setting{
|
||||
Value: password,
|
||||
})
|
||||
@@ -188,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
|
||||
@@ -272,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 {
|
||||
@@ -290,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 {
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
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
|
||||
setting services.Setting
|
||||
}
|
||||
|
||||
func NewCronController() *CronController {
|
||||
return &CronController{
|
||||
cron: services.NewCronImpl(),
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取计划任务列表
|
||||
func (c *CronController) List(ctx http.Context) http.Response {
|
||||
limit := ctx.Request().QueryInt("limit")
|
||||
page := ctx.Request().QueryInt("page")
|
||||
|
||||
var crons []models.Cron
|
||||
var total int64
|
||||
err := facades.Orm().Query().Paginate(page, limit, &crons, &total)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][CronController] 查询计划任务列表失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": crons,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加计划任务
|
||||
func (c *CronController) Add(ctx http.Context) http.Response {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"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 {
|
||||
return Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
// 单独验证时间格式
|
||||
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")) {
|
||||
return Error(ctx, http.StatusBadRequest, "时间格式错误")
|
||||
}
|
||||
|
||||
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) {
|
||||
facades.Log().Error("[面板][CronController] 计划任务目录不存在")
|
||||
return Error(ctx, http.StatusInternalServerError, "计划任务目录不存在")
|
||||
}
|
||||
if !tools.Exists(shellLogDir) {
|
||||
facades.Log().Error("[面板][CronController] 计划任务日志目录不存在")
|
||||
return Error(ctx, http.StatusInternalServerError, "计划任务日志目录不存在")
|
||||
}
|
||||
shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + tools.RandomString(16)
|
||||
if !tools.Write(shellDir+shellFile+".sh", shell, 0700) {
|
||||
facades.Log().Error("[面板][CronController] 创建计划任务脚本失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
tools.Exec("dos2unix " + shellDir + shellFile + ".sh")
|
||||
|
||||
var cron models.Cron
|
||||
cron.Name = ctx.Request().Input("name")
|
||||
cron.Type = ctx.Request().Input("type")
|
||||
cron.Status = true
|
||||
cron.Time = ctx.Request().Input("time")
|
||||
cron.Shell = shellDir + shellFile + ".sh"
|
||||
cron.Log = shellLogDir + shellFile + ".log"
|
||||
|
||||
err = facades.Orm().Query().Create(&cron)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][CronController] 创建计划任务失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.cron.AddToSystem(cron)
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"id": cron.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// Script 获取脚本内容
|
||||
func (c *CronController) Script(ctx http.Context) http.Response {
|
||||
var cron models.Cron
|
||||
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
}
|
||||
|
||||
return Success(ctx, tools.Read(cron.Shell))
|
||||
}
|
||||
|
||||
// Update 更新计划任务
|
||||
func (c *CronController) Update(ctx http.Context) http.Response {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
"time": "required",
|
||||
"script": "required",
|
||||
})
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
// 单独验证时间格式
|
||||
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")) {
|
||||
return Error(ctx, http.StatusBadRequest, "时间格式错误")
|
||||
}
|
||||
|
||||
var cron models.Cron
|
||||
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
}
|
||||
|
||||
if !cron.Status {
|
||||
return Error(ctx, http.StatusBadRequest, "计划任务已禁用")
|
||||
}
|
||||
|
||||
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)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if !tools.Write(cron.Shell, ctx.Request().Input("script"), 0644) {
|
||||
facades.Log().Error("[面板][CronController] 更新计划任务脚本失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
tools.Exec("dos2unix " + cron.Shell)
|
||||
|
||||
c.cron.DeleteFromSystem(cron)
|
||||
if cron.Status {
|
||||
c.cron.AddToSystem(cron)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除计划任务
|
||||
func (c *CronController) Delete(ctx http.Context) http.Response {
|
||||
var cron models.Cron
|
||||
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
}
|
||||
|
||||
c.cron.DeleteFromSystem(cron)
|
||||
tools.Remove(cron.Shell)
|
||||
|
||||
_, err = facades.Orm().Query().Delete(&cron)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][CronController] 删除计划任务失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Status 更新计划任务状态
|
||||
func (c *CronController) Status(ctx http.Context) http.Response {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"status": "bool",
|
||||
})
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
var cron models.Cron
|
||||
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
}
|
||||
|
||||
cron.Status = ctx.Request().InputBool("status")
|
||||
err = facades.Orm().Query().Save(&cron)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][CronController] 更新计划任务状态失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.cron.DeleteFromSystem(cron)
|
||||
if cron.Status {
|
||||
c.cron.AddToSystem(cron)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Log 获取计划任务日志
|
||||
func (c *CronController) Log(ctx http.Context) http.Response {
|
||||
var cron models.Cron
|
||||
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
}
|
||||
|
||||
if !tools.Exists(cron.Log) {
|
||||
return Error(ctx, http.StatusBadRequest, "日志文件不存在")
|
||||
}
|
||||
|
||||
return Success(ctx, tools.Read(cron.Log))
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package controllers
|
||||
|
||||
type FileController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewFileController() *FileController {
|
||||
return &FileController{
|
||||
// Inject services
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,69 @@
|
||||
package controllers
|
||||
|
||||
import "github.com/goravel/framework/contracts/http"
|
||||
import (
|
||||
"sync"
|
||||
|
||||
func Success(ctx http.Context, data any) {
|
||||
ctx.Response().Success().Json(http.Json{
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
func Success(ctx http.Context, data any) http.Response {
|
||||
return ctx.Response().Success().Json(http.Json{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
func Error(ctx http.Context, code int, message any) {
|
||||
ctx.Response().Json(http.StatusOK, http.Json{
|
||||
func Error(ctx http.Context, code int, message any) http.Response {
|
||||
return ctx.Response().Json(http.StatusOK, http.Json{
|
||||
"code": code,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// Check 检查插件是否可用
|
||||
func Check(ctx http.Context, slug string) http.Response {
|
||||
plugin := services.NewPluginImpl().GetBySlug(slug)
|
||||
installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug)
|
||||
installedPlugins, err := services.NewPluginImpl().AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][插件] 获取已安装插件失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if installedPlugin.Version != plugin.Version || installedPlugin.Slug != plugin.Slug {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要更新至 "+plugin.Version+" 版本")
|
||||
}
|
||||
|
||||
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 {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/packages/helpers"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type MenuItem struct {
|
||||
@@ -19,49 +21,51 @@ 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) {
|
||||
// Name 获取面板名称
|
||||
func (c *InfoController) Name(ctx http.Context) http.Response {
|
||||
var setting models.Setting
|
||||
err := facades.Orm().Query().Where("key", "name").First(&setting)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][InfoController] 查询面板名称失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
Success(ctx, http.Json{
|
||||
return Success(ctx, http.Json{
|
||||
"name": setting.Value,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *InfoController) Menu(ctx http.Context) {
|
||||
Success(ctx, []MenuItem{
|
||||
// Menu 获取面板菜单
|
||||
func (c *InfoController) Menu(ctx http.Context) http.Response {
|
||||
return 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) {
|
||||
// HomePlugins 获取首页插件
|
||||
func (c *InfoController) HomePlugins(ctx http.Context) http.Response {
|
||||
var plugins []models.Plugin
|
||||
err := facades.Orm().Query().Where("show", 1).Find(&plugins)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][InfoController] 查询首页插件失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
type pluginsData struct {
|
||||
@@ -77,29 +81,31 @@ func (r *InfoController) HomePlugins(ctx http.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
Success(ctx, pluginsJson)
|
||||
return Success(ctx, pluginsJson)
|
||||
}
|
||||
|
||||
func (r *InfoController) NowMonitor(ctx http.Context) {
|
||||
Success(ctx, helpers.GetMonitoringInfo())
|
||||
// NowMonitor 获取当前监控信息
|
||||
func (c *InfoController) NowMonitor(ctx http.Context) http.Response {
|
||||
return Success(ctx, tools.GetMonitoringInfo())
|
||||
}
|
||||
|
||||
func (r *InfoController) SystemInfo(ctx http.Context) {
|
||||
monitorInfo := helpers.GetMonitoringInfo()
|
||||
// SystemInfo 获取系统信息
|
||||
func (c *InfoController) SystemInfo(ctx http.Context) http.Response {
|
||||
monitorInfo := tools.GetMonitoringInfo()
|
||||
|
||||
Success(ctx, http.Json{
|
||||
return Success(ctx, http.Json{
|
||||
"os_name": monitorInfo.Host.Platform + " " + monitorInfo.Host.PlatformVersion,
|
||||
"uptime": fmt.Sprintf("%.2f", float64(monitorInfo.Host.Uptime)/86400),
|
||||
"panel_version": facades.Config().GetString("panel.version"),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *InfoController) InstalledDbAndPhp(ctx http.Context) {
|
||||
// InstalledDbAndPhp 获取已安装的数据库和 PHP 版本
|
||||
func (c *InfoController) InstalledDbAndPhp(ctx http.Context) http.Response {
|
||||
var php []models.Plugin
|
||||
err := facades.Orm().Query().Where("slug like ?", "php%").Find(&php)
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
var mysql models.Plugin
|
||||
@@ -116,9 +122,81 @@ func (r *InfoController) InstalledDbAndPhp(ctx http.Context) {
|
||||
postgresqlInstalled = false
|
||||
}
|
||||
|
||||
Success(ctx, http.Json{
|
||||
"php": php,
|
||||
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})
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"php": phpData,
|
||||
"mysql": mysqlInstalled,
|
||||
"postgresql": postgresqlInstalled,
|
||||
})
|
||||
}
|
||||
|
||||
// CheckUpdate 检查面板更新
|
||||
func (c *InfoController) CheckUpdate(ctx http.Context) http.Response {
|
||||
version := facades.Config().GetString("panel.version")
|
||||
remote, err := tools.GetLatestPanelVersion()
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
|
||||
}
|
||||
|
||||
if version == remote.Version {
|
||||
return 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,
|
||||
})
|
||||
}
|
||||
|
||||
// Update 更新面板
|
||||
func (c *InfoController) Update(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
|
||||
if err == nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止更新")
|
||||
}
|
||||
|
||||
proxy := ctx.Request().InputBool("proxy")
|
||||
err = tools.UpdatePanel(proxy)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][InfoController] 更新面板失败 ", err.Error())
|
||||
return Error(ctx, http.StatusInternalServerError, "更新失败: "+err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Restart 重启面板
|
||||
func (c *InfoController) Restart(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
|
||||
if err == nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止重启")
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart panel")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
type MonitorController struct {
|
||||
setting services.Setting
|
||||
}
|
||||
|
||||
func NewMonitorController() *MonitorController {
|
||||
return &MonitorController{
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Switch 监控开关
|
||||
func (r *MonitorController) Switch(ctx http.Context) http.Response {
|
||||
value := ctx.Request().InputBool("switch")
|
||||
err := r.setting.Set(models.SettingKeyMonitor, cast.ToString(value))
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][MonitorController] 更新监控开关失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SaveDays 保存监控天数
|
||||
func (r *MonitorController) SaveDays(ctx http.Context) http.Response {
|
||||
days := ctx.Request().Input("days")
|
||||
err := r.setting.Set(models.SettingKeyMonitorDays, days)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][MonitorController] 更新监控天数失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SwitchAndDays 监控开关和监控天数
|
||||
func (r *MonitorController) SwitchAndDays(ctx http.Context) http.Response {
|
||||
monitor := r.setting.Get(models.SettingKeyMonitor)
|
||||
monitorDays := r.setting.Get(models.SettingKeyMonitorDays)
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"switch": cast.ToBool(monitor),
|
||||
"days": cast.ToInt(monitorDays),
|
||||
})
|
||||
}
|
||||
|
||||
// Clear 清空监控数据
|
||||
func (r *MonitorController) Clear(ctx http.Context) http.Response {
|
||||
_, err := facades.Orm().Query().Where("1 = 1").Delete(&models.Monitor{})
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][MonitorController] 清空监控数据失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// List 监控数据列表
|
||||
func (r *MonitorController) List(ctx http.Context) http.Response {
|
||||
start := ctx.Request().InputInt64("start")
|
||||
end := ctx.Request().InputInt64("end")
|
||||
startTime := carbon.FromTimestampMilli(start)
|
||||
endTime := carbon.FromTimestampMilli(end)
|
||||
|
||||
var monitors []models.Monitor
|
||||
err := facades.Orm().Query().Where("created_at >= ?", startTime.ToDateTimeString()).Where("created_at <= ?", endTime.ToDateTimeString()).Get(&monitors)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][MonitorController] 查询监控数据失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if len(monitors) == 0 {
|
||||
return Error(ctx, http.StatusNotFound, "监控数据为空")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return Success(ctx, data)
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/queue"
|
||||
"github.com/goravel/framework/facades"
|
||||
"panel/app/jobs"
|
||||
"sync"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
@@ -14,20 +12,22 @@ import (
|
||||
|
||||
type PluginController struct {
|
||||
plugin services.Plugin
|
||||
task services.Task
|
||||
}
|
||||
|
||||
func NewPluginController() *PluginController {
|
||||
return &PluginController{
|
||||
plugin: services.NewPluginImpl(),
|
||||
task: services.NewTaskImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 列出所有插件
|
||||
func (r *PluginController) List(ctx http.Context) {
|
||||
func (r *PluginController) List(ctx http.Context) http.Response {
|
||||
plugins := r.plugin.All()
|
||||
installedPlugins, err := r.plugin.AllInstalled()
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
@@ -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,
|
||||
@@ -74,173 +72,196 @@ func (r *PluginController) List(ctx http.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
Success(ctx, p)
|
||||
return Success(ctx, p)
|
||||
}
|
||||
|
||||
// Install 安装插件
|
||||
func (r *PluginController) Install(ctx http.Context) {
|
||||
func (r *PluginController) Install(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
plugins := r.plugin.All()
|
||||
plugin := r.plugin.GetBySlug(slug)
|
||||
installedPlugin := r.plugin.GetInstalledBySlug(slug)
|
||||
installedPlugins, err := r.plugin.AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
var plugin services.PanelPlugin
|
||||
check := false
|
||||
for _, item := range plugins {
|
||||
if item.Slug == slug {
|
||||
check = true
|
||||
plugin = item
|
||||
break
|
||||
if installedPlugin.ID != 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "插件已安装")
|
||||
}
|
||||
|
||||
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 {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
}
|
||||
}
|
||||
if !check {
|
||||
Error(ctx, http.StatusBadRequest, "插件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var installedPlugin models.Plugin
|
||||
if err := facades.Orm().Query().Where("slug", slug).First(&installedPlugin); err != nil {
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
if installedPlugin.ID != 0 {
|
||||
Error(ctx, http.StatusBadRequest, "插件已安装")
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
processTask(task.ID)
|
||||
Success(ctx, "任务已提交")
|
||||
r.task.Process(task.ID)
|
||||
return Success(ctx, "任务已提交")
|
||||
}
|
||||
|
||||
// Uninstall 卸载插件
|
||||
func (r *PluginController) Uninstall(ctx http.Context) {
|
||||
func (r *PluginController) Uninstall(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
plugins := r.plugin.All()
|
||||
plugin := r.plugin.GetBySlug(slug)
|
||||
installedPlugin := r.plugin.GetInstalledBySlug(slug)
|
||||
installedPlugins, err := r.plugin.AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
var plugin services.PanelPlugin
|
||||
check := false
|
||||
for _, item := range plugins {
|
||||
if item.Slug == slug {
|
||||
check = true
|
||||
plugin = item
|
||||
break
|
||||
if installedPlugin.ID == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "插件未安装")
|
||||
}
|
||||
|
||||
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 {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
}
|
||||
}
|
||||
if !check {
|
||||
Error(ctx, http.StatusBadRequest, "插件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var installedPlugin models.Plugin
|
||||
if err := facades.Orm().Query().Where("slug", slug).First(&installedPlugin); err != nil {
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
if installedPlugin.ID == 0 {
|
||||
Error(ctx, http.StatusBadRequest, "插件未安装")
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
processTask(task.ID)
|
||||
Success(ctx, "任务已提交")
|
||||
r.task.Process(task.ID)
|
||||
return Success(ctx, "任务已提交")
|
||||
}
|
||||
|
||||
// Update 更新插件
|
||||
func (r *PluginController) Update(ctx http.Context) {
|
||||
func (r *PluginController) Update(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
plugins := r.plugin.All()
|
||||
plugin := r.plugin.GetBySlug(slug)
|
||||
installedPlugin := r.plugin.GetInstalledBySlug(slug)
|
||||
installedPlugins, err := r.plugin.AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
var plugin services.PanelPlugin
|
||||
check := false
|
||||
for _, item := range plugins {
|
||||
if item.Slug == slug {
|
||||
check = true
|
||||
plugin = item
|
||||
break
|
||||
if installedPlugin.ID == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "插件未安装")
|
||||
}
|
||||
|
||||
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 {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
}
|
||||
}
|
||||
if !check {
|
||||
Error(ctx, http.StatusBadRequest, "插件不存在")
|
||||
return
|
||||
}
|
||||
|
||||
var installedPlugin models.Plugin
|
||||
if err := facades.Orm().Query().Where("slug", slug).First(&installedPlugin); err != nil {
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
}
|
||||
if installedPlugin.ID == 0 {
|
||||
Error(ctx, http.StatusBadRequest, "插件未安装")
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
processTask(task.ID)
|
||||
Success(ctx, "任务已提交")
|
||||
r.task.Process(task.ID)
|
||||
return Success(ctx, "任务已提交")
|
||||
}
|
||||
|
||||
// UpdateShow 更新插件首页显示状态
|
||||
func (r *PluginController) UpdateShow(ctx http.Context) {
|
||||
func (r *PluginController) UpdateShow(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
show := ctx.Request().InputBool("show")
|
||||
|
||||
var plugin models.Plugin
|
||||
if err := facades.Orm().Query().Where("slug", slug).First(&plugin); err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 查询插件失败: " + err.Error())
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
if plugin.ID == 0 {
|
||||
Error(ctx, http.StatusBadRequest, "插件未安装")
|
||||
return
|
||||
return Error(ctx, http.StatusBadRequest, "插件未安装")
|
||||
}
|
||||
|
||||
plugin.Show = show
|
||||
if err := facades.Orm().Query().Save(&plugin); err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 更新插件失败: " + err.Error())
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
Success(ctx, "操作成功")
|
||||
}
|
||||
|
||||
// processTask 处理任务
|
||||
func processTask(taskID uint) {
|
||||
go func() {
|
||||
err := facades.Queue().Job(&jobs.ProcessTask{}, []queue.Arg{
|
||||
{Type: "uint", Value: taskID},
|
||||
}).Dispatch()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 运行任务失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
}()
|
||||
return Success(ctx, "操作成功")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,451 @@
|
||||
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) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Fail2banController) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload fail2ban")
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Fail2banController) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart fail2ban")
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Fail2banController) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start fail2ban")
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Fail2banController) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop fail2ban")
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// List 所有 Fail2ban 规则
|
||||
func (c *Fail2banController) List(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
if len(raw) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "Fail2ban 规则为空")
|
||||
}
|
||||
|
||||
jailList := regexp.MustCompile(`\[(.*?)]`).FindAllStringSubmatch(raw, -1)
|
||||
if len(jailList) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "Fail2ban 规则为空")
|
||||
}
|
||||
|
||||
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) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []Jail{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(jails) {
|
||||
endIndex = len(jails)
|
||||
}
|
||||
pagedJails := jails[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(jails),
|
||||
"items": pagedJails,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加 Fail2ban 规则
|
||||
func (c *Fail2banController) Add(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
|
||||
}
|
||||
|
||||
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") {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "规则已存在")
|
||||
}
|
||||
|
||||
switch jailType {
|
||||
case "website":
|
||||
var website models.Website
|
||||
err := facades.Orm().Query().Where("name", jailName).FirstOrFail(&website)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "网站不存在")
|
||||
}
|
||||
config, err := c.website.GetConfig(int(website.ID))
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取网站配置失败")
|
||||
}
|
||||
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:
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "未知服务")
|
||||
}
|
||||
if len(port) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取服务端口失败,请检查是否安装")
|
||||
}
|
||||
|
||||
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")
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除规则
|
||||
func (c *Fail2banController) Delete(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
jailName := ctx.Request().Input("name")
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
if !strings.Contains(raw, "["+jailName+"]") {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "规则不存在")
|
||||
}
|
||||
|
||||
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")
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BanList 获取封禁列表
|
||||
func (c *Fail2banController) BanList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
name := ctx.Request().Query("name")
|
||||
if len(name) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"currentlyBan": currentlyBan,
|
||||
"totalBan": totalBan,
|
||||
"bannedIpList": list,
|
||||
})
|
||||
}
|
||||
|
||||
// Unban 解封
|
||||
func (c *Fail2banController) Unban(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
name := ctx.Request().Input("name")
|
||||
ip := ctx.Request().Input("ip")
|
||||
if len(name) == 0 || len(ip) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
|
||||
}
|
||||
|
||||
tools.Exec("fail2ban-client set " + name + " unbanip " + ip)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SetWhiteList 设置白名单
|
||||
func (c *Fail2banController) SetWhiteList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
ip := ctx.Request().Input("ip")
|
||||
if len(ip) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
|
||||
}
|
||||
|
||||
tools.Write("/etc/fail2ban/jail.local", raw, 0644)
|
||||
tools.Exec("fail2ban-client reload")
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetWhiteList 获取白名单
|
||||
func (c *Fail2banController) GetWhiteList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
reg := regexp.MustCompile(`ignoreip\s*=\s*(.*)\n`)
|
||||
if reg.MatchString(raw) {
|
||||
ignoreIp := reg.FindStringSubmatch(raw)[1]
|
||||
return controllers.Success(ctx, ignoreIp)
|
||||
} else {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,766 @@
|
||||
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) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Mysql57Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Mysql57Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Mysql57Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Mysql57Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *Mysql57Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/mysql/conf/my.cnf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *Mysql57Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/mysql/conf/my.cnf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *Mysql57Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 已停止运行")
|
||||
}
|
||||
|
||||
raw := tools.Exec("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
|
||||
if strings.Contains(raw, "Access denied for user") {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码错误")
|
||||
}
|
||||
if !strings.Contains(raw, "Uptime") {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
// ErrorLog 获取错误日志
|
||||
func (c *Mysql57Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/mysql/mysql-error.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (c *Mysql57Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/mysql/mysql-error.log")
|
||||
return controllers.Success(ctx, "清空错误日志成功")
|
||||
}
|
||||
|
||||
// SlowLog 获取慢查询日志
|
||||
func (c *Mysql57Controller) SlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/mysql/mysql-slow.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearSlowLog 清空慢查询日志
|
||||
func (c *Mysql57Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/mysql/mysql-slow.log")
|
||||
return controllers.Success(ctx, "清空慢查询日志成功")
|
||||
}
|
||||
|
||||
// GetRootPassword 获取root密码
|
||||
func (c *Mysql57Controller) GetRootPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, rootPassword)
|
||||
}
|
||||
|
||||
// SetRootPassword 设置root密码
|
||||
func (c *Mysql57Controller) SetRootPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
|
||||
}
|
||||
|
||||
rootPassword := ctx.Request().Input(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码不能为空")
|
||||
}
|
||||
|
||||
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;\"")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "设置root密码成功")
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (c *Mysql57Controller) DatabaseList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query("SHOW DATABASES")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL57] 获取数据库列表失败" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
}
|
||||
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())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(databases) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(databases) {
|
||||
endIndex = len(databases)
|
||||
}
|
||||
pagedDatabases := databases[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(databases),
|
||||
"items": pagedDatabases,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (c *Mysql57Controller) AddDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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;\"")
|
||||
|
||||
return controllers.Success(ctx, "添加数据库成功")
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (c *Mysql57Controller) DeleteDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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 + ";\"")
|
||||
|
||||
return controllers.Success(ctx, "删除数据库成功")
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (c *Mysql57Controller) BackupList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
backupList, err := c.backup.MysqlList()
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL57] 获取备份列表失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, backupList)
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (c *Mysql57Controller) UploadBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "上传文件成功")
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (c *Mysql57Controller) CreateBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
err = c.backup.MysqlBackup(database)
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL57] 创建备份失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "备份成功")
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (c *Mysql57Controller) DeleteBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
||||
fileName := ctx.Request().Input("name")
|
||||
tools.Remove(backupPath + "/" + fileName)
|
||||
|
||||
return controllers.Success(ctx, "删除备份成功")
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (c *Mysql57Controller) RestoreBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
err = c.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL57] 还原失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "还原成功")
|
||||
}
|
||||
|
||||
// UserList 用户列表
|
||||
func (c *Mysql57Controller) UserList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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())
|
||||
return 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())
|
||||
return 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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(userGrants) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []user{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(userGrants) {
|
||||
endIndex = len(userGrants)
|
||||
}
|
||||
pagedUserGrants := userGrants[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(userGrants),
|
||||
"items": pagedUserGrants,
|
||||
})
|
||||
}
|
||||
|
||||
// AddUser 添加用户
|
||||
func (c *Mysql57Controller) AddUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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;\"")
|
||||
|
||||
return controllers.Success(ctx, "添加成功")
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (c *Mysql57Controller) DeleteUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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';\"")
|
||||
|
||||
return controllers.Success(ctx, "删除成功")
|
||||
}
|
||||
|
||||
// SetUserPassword 设置用户密码
|
||||
func (c *Mysql57Controller) SetUserPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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;\"")
|
||||
|
||||
return controllers.Success(ctx, "修改成功")
|
||||
}
|
||||
|
||||
// SetUserPrivileges 设置用户权限
|
||||
func (c *Mysql57Controller) SetUserPrivileges(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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;\"")
|
||||
|
||||
return controllers.Success(ctx, "修改成功")
|
||||
}
|
||||
@@ -0,0 +1,766 @@
|
||||
package mysql80
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Mysql80Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
}
|
||||
|
||||
func NewMysql80Controller() *Mysql80Controller {
|
||||
return &Mysql80Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *Mysql80Controller) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Mysql80Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Mysql80Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Mysql80Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Mysql80Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *Mysql80Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/mysql/conf/my.cnf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *Mysql80Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/mysql/conf/my.cnf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *Mysql80Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 已停止运行")
|
||||
}
|
||||
|
||||
raw := tools.Exec("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
|
||||
if strings.Contains(raw, "Access denied for user") {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码错误")
|
||||
}
|
||||
if !strings.Contains(raw, "Uptime") {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
// ErrorLog 获取错误日志
|
||||
func (c *Mysql80Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/mysql/mysql-error.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (c *Mysql80Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/mysql/mysql-error.log")
|
||||
return controllers.Success(ctx, "清空错误日志成功")
|
||||
}
|
||||
|
||||
// SlowLog 获取慢查询日志
|
||||
func (c *Mysql80Controller) SlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/mysql/mysql-slow.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearSlowLog 清空慢查询日志
|
||||
func (c *Mysql80Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/mysql/mysql-slow.log")
|
||||
return controllers.Success(ctx, "清空慢查询日志成功")
|
||||
}
|
||||
|
||||
// GetRootPassword 获取root密码
|
||||
func (c *Mysql80Controller) GetRootPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, rootPassword)
|
||||
}
|
||||
|
||||
// SetRootPassword 设置root密码
|
||||
func (c *Mysql80Controller) SetRootPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
|
||||
}
|
||||
|
||||
rootPassword := ctx.Request().Input(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码不能为空")
|
||||
}
|
||||
|
||||
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;\"")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "设置root密码成功")
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (c *Mysql80Controller) DatabaseList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
type database struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL80] 连接数据库失败" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query("SHOW DATABASES")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL80] 获取数据库列表失败" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var databases []database
|
||||
for rows.Next() {
|
||||
var d database
|
||||
err := rows.Scan(&d.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
databases = append(databases, d)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
facades.Log().Error("[MySQL80] 获取数据库列表失败" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(databases) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(databases) {
|
||||
endIndex = len(databases)
|
||||
}
|
||||
pagedDatabases := databases[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(databases),
|
||||
"items": pagedDatabases,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (c *Mysql80Controller) AddDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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;\"")
|
||||
|
||||
return controllers.Success(ctx, "添加数据库成功")
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (c *Mysql80Controller) DeleteDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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 + ";\"")
|
||||
|
||||
return controllers.Success(ctx, "删除数据库成功")
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (c *Mysql80Controller) BackupList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
backupList, err := c.backup.MysqlList()
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL80] 获取备份列表失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, backupList)
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (c *Mysql80Controller) UploadBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "上传文件成功")
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (c *Mysql80Controller) CreateBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
err = c.backup.MysqlBackup(database)
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL80] 创建备份失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "备份成功")
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (c *Mysql80Controller) DeleteBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
||||
fileName := ctx.Request().Input("name")
|
||||
tools.Remove(backupPath + "/" + fileName)
|
||||
|
||||
return controllers.Success(ctx, "删除备份成功")
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (c *Mysql80Controller) RestoreBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
err = c.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL80] 还原失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "还原成功")
|
||||
}
|
||||
|
||||
// UserList 用户列表
|
||||
func (c *Mysql80Controller) UserList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
type user struct {
|
||||
User string `json:"user"`
|
||||
Host string `json:"host"`
|
||||
Grants []string `json:"grants"`
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL80] 连接数据库失败:" + err.Error())
|
||||
return 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())
|
||||
return 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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(userGrants) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []user{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(userGrants) {
|
||||
endIndex = len(userGrants)
|
||||
}
|
||||
pagedUserGrants := userGrants[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(userGrants),
|
||||
"items": pagedUserGrants,
|
||||
})
|
||||
}
|
||||
|
||||
// AddUser 添加用户
|
||||
func (c *Mysql80Controller) AddUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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;\"")
|
||||
|
||||
return controllers.Success(ctx, "添加成功")
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (c *Mysql80Controller) DeleteUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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';\"")
|
||||
|
||||
return controllers.Success(ctx, "删除成功")
|
||||
}
|
||||
|
||||
// SetUserPassword 设置用户密码
|
||||
func (c *Mysql80Controller) SetUserPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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;\"")
|
||||
|
||||
return controllers.Success(ctx, "修改成功")
|
||||
}
|
||||
|
||||
// SetUserPrivileges 设置用户权限
|
||||
func (c *Mysql80Controller) SetUserPrivileges(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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;\"")
|
||||
|
||||
return controllers.Success(ctx, "修改成功")
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package openresty
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type OpenRestyController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewOpenrestyController() *OpenRestyController {
|
||||
return &OpenRestyController{
|
||||
// Inject services
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *OpenRestyController) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *OpenRestyController) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload openresty")
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, "重载OpenResty成功")
|
||||
} else {
|
||||
return controllers.Error(ctx, 1, "重载OpenResty失败: "+status)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动OpenResty
|
||||
func (c *OpenRestyController) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start openresty")
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, "启动OpenResty成功")
|
||||
} else {
|
||||
return controllers.Error(ctx, 1, "启动OpenResty失败: "+status)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止OpenResty
|
||||
func (c *OpenRestyController) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop openresty")
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, "停止OpenResty成功")
|
||||
} else {
|
||||
return controllers.Error(ctx, 1, "停止OpenResty失败: "+status)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启OpenResty
|
||||
func (c *OpenRestyController) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart openresty")
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, "重启OpenResty成功")
|
||||
} else {
|
||||
return controllers.Error(ctx, 1, "重启OpenResty失败: "+status)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *OpenRestyController) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := tools.Read("/www/server/openresty/conf/nginx.conf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *OpenRestyController) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/openresty/conf/nginx.conf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "保存OpenResty配置失败")
|
||||
}
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// ErrorLog 获取错误日志
|
||||
func (c *OpenRestyController) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
if !tools.Exists("/www/wwwlogs/nginx_error.log") {
|
||||
return controllers.Success(ctx, "")
|
||||
}
|
||||
|
||||
out := tools.Exec("tail -n 100 /www/wwwlogs/nginx_error.log")
|
||||
return controllers.Success(ctx, out)
|
||||
}
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (c *OpenRestyController) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/wwwlogs/nginx_error.log")
|
||||
return controllers.Success(ctx, "清空OpenResty错误日志成功")
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *OpenRestyController) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/nginx_status")
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Error("[OpenResty] 获取OpenResty负载失败: " + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty负载失败")
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
type nginxStatus struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
var data []nginxStatus
|
||||
|
||||
workers := tools.Exec("ps aux | grep nginx | grep 'worker process' | wc -l")
|
||||
data = append(data, nginxStatus{
|
||||
Name: "工作进程",
|
||||
Value: workers,
|
||||
})
|
||||
|
||||
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 = 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 = 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 = append(data, nginxStatus{
|
||||
Name: "请求数",
|
||||
Value: match[1],
|
||||
})
|
||||
data = append(data, nginxStatus{
|
||||
Name: "响应数",
|
||||
Value: match[2],
|
||||
})
|
||||
data = append(data, nginxStatus{
|
||||
Name: "驻留进程",
|
||||
Value: match[3],
|
||||
})
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/packages/helpers"
|
||||
)
|
||||
|
||||
type OpenRestyController struct {
|
||||
//Dependent services
|
||||
}
|
||||
|
||||
func NewOpenrestyController() *OpenRestyController {
|
||||
return &OpenRestyController{
|
||||
//Inject services
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (r *OpenRestyController) Status(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", "systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 获取OpenResty状态失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(string(out))
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, true)
|
||||
} else {
|
||||
controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (r *OpenRestyController) Reload(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", "systemctl reload openresty")
|
||||
_, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 重载OpenResty失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "重载OpenResty失败")
|
||||
return
|
||||
}
|
||||
|
||||
cmd = exec.Command("bash", "-c", "systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 获取OpenResty状态失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(string(out))
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, "重载OpenResty成功")
|
||||
} else {
|
||||
controllers.Error(ctx, 1, "重载OpenResty失败: "+string(out))
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动OpenResty
|
||||
func (r *OpenRestyController) Start(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", "systemctl start openresty")
|
||||
_, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 启动OpenResty失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "启动OpenResty失败")
|
||||
return
|
||||
}
|
||||
|
||||
cmd = exec.Command("bash", "-c", "systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 获取OpenResty状态失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(string(out))
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, "启动OpenResty成功")
|
||||
} else {
|
||||
controllers.Error(ctx, 1, "启动OpenResty失败: "+string(out))
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止OpenResty
|
||||
func (r *OpenRestyController) Stop(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", "systemctl stop openresty")
|
||||
_, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 停止OpenResty失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "停止OpenResty失败")
|
||||
return
|
||||
}
|
||||
|
||||
cmd = exec.Command("bash", "-c", "systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 获取OpenResty状态失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(string(out))
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, "停止OpenResty成功")
|
||||
} else {
|
||||
controllers.Error(ctx, 1, "停止OpenResty失败: "+string(out))
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启OpenResty
|
||||
func (r *OpenRestyController) Restart(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", "systemctl restart openresty")
|
||||
_, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 重启OpenResty失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "重启OpenResty失败")
|
||||
return
|
||||
}
|
||||
|
||||
cmd = exec.Command("bash", "-c", "systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 获取OpenResty状态失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
status := strings.TrimSpace(string(out))
|
||||
if len(status) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
return
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
controllers.Success(ctx, "重启OpenResty成功")
|
||||
} else {
|
||||
controllers.Error(ctx, 1, "重启OpenResty失败: "+string(out))
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (r *OpenRestyController) GetConfig(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
config, err := os.ReadFile("/www/server/openresty/conf/nginx.conf")
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, string(config))
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (r *OpenRestyController) SaveConfig(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "配置不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
oldConfig, err := os.ReadFile("/www/server/openresty/conf/nginx.conf")
|
||||
if err != nil {
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile("/www/server/openresty/conf/nginx.conf", []byte(config), 0644)
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 保存OpenResty配置失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "保存OpenResty配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", "openresty -t 2>&1")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 检查OpenResty配置失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "检查OpenResty配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.Contains(string(out), "test is successful") {
|
||||
err = os.WriteFile("/www/server/openresty/conf/nginx.conf", oldConfig, 0644)
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 保存OpenResty配置失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "保存OpenResty配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Error(ctx, http.StatusForbidden, "OpenResty配置有误,请修正后再保存: "+string(out))
|
||||
}
|
||||
|
||||
cmd = exec.Command("bash", "-c", "systemctl reload openresty")
|
||||
_, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 重载OpenResty配置失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "重载OpenResty配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, "保存OpenResty配置成功")
|
||||
}
|
||||
|
||||
// ErrorLog 获取错误日志
|
||||
func (r *OpenRestyController) ErrorLog(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", "tail -n 100 /www/wwwlogs/nginx_error.log")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 获取OpenResty错误日志失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty错误日志失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, string(out))
|
||||
}
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (r *OpenRestyController) ClearErrorLog(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", "echo '' > /www/wwwlogs/nginx_error.log")
|
||||
_, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 清空OpenResty错误日志失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "清空OpenResty错误日志失败")
|
||||
return
|
||||
}
|
||||
|
||||
controllers.Success(ctx, "清空OpenResty错误日志成功")
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (r *OpenRestyController) Load(ctx http.Context) {
|
||||
if !Check(ctx, "openresty") {
|
||||
return
|
||||
}
|
||||
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/nginx_status")
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Error("[OpenResty] 获取OpenResty负载失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty负载失败")
|
||||
return
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
var data map[int]map[string]any
|
||||
|
||||
cmd := exec.Command("bash", "-c", "ps aux | grep nginx | grep 'worker process' | wc -l")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 获取OpenResty工作进程失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty工作进程失败")
|
||||
return
|
||||
}
|
||||
workers := strings.TrimSpace(string(out))
|
||||
data[0]["name"] = "工作进程"
|
||||
data[0]["value"] = workers
|
||||
|
||||
cmd = exec.Command("bash", "-c", "ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
|
||||
out, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
facades.Log().Error("[OpenResty] 获取OpenResty内存占用失败: " + err.Error())
|
||||
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty内存占用失败")
|
||||
return
|
||||
}
|
||||
mem := helpers.FormatBytes(cast.ToFloat64(strings.TrimSpace(string(out))))
|
||||
data[1]["name"] = "内存占用"
|
||||
data[1]["value"] = mem
|
||||
|
||||
match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 2 {
|
||||
data[2]["name"] = "活跃连接数"
|
||||
data[2]["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]
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
controllers.Success(ctx, data)
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
package php74
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Php74Controller struct {
|
||||
setting services.Setting
|
||||
task services.Task
|
||||
version string
|
||||
}
|
||||
|
||||
func NewPhp74Controller() *Php74Controller {
|
||||
return &Php74Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
task: services.NewTaskImpl(),
|
||||
version: "74",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php74Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := tools.Read("/www/server/php/" + c.version + "/etc/php.ini")
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
tools.Write("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 + "运行状态失败")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) SlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
type Extension struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
func (c *Php74Controller) GetExtensionList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) InstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
|
||||
}
|
||||
|
||||
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())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php74Controller) UninstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
|
||||
}
|
||||
|
||||
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())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php74Controller) GetExtensions() []Extension {
|
||||
var extensions []Extension
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "OPcache",
|
||||
Slug: "Zend OPcache",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "PhpRedis",
|
||||
Slug: "redis",
|
||||
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ImageMagick",
|
||||
Slug: "imagick",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "Exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "pdo_pgsql",
|
||||
Slug: "pdo_pgsql",
|
||||
Description: "(需先安装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,372 @@
|
||||
package php80
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Php80Controller struct {
|
||||
setting services.Setting
|
||||
task services.Task
|
||||
version string
|
||||
}
|
||||
|
||||
func NewPhp80Controller() *Php80Controller {
|
||||
return &Php80Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
task: services.NewTaskImpl(),
|
||||
version: "80",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := tools.Read("/www/server/php/" + c.version + "/etc/php.ini")
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
tools.Write("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 + "运行状态失败")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) SlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
|
||||
return 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) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) InstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
|
||||
}
|
||||
|
||||
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())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php80Controller) UninstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
|
||||
}
|
||||
|
||||
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())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return 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,372 @@
|
||||
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) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php81Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php81Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php81Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php81Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php81Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := tools.Read("/www/server/php/" + c.version + "/etc/php.ini")
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
tools.Write("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 + "运行状态失败")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) SlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
|
||||
return 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) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) InstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
|
||||
}
|
||||
|
||||
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())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php81Controller) UninstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
|
||||
}
|
||||
|
||||
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())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return 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,366 @@
|
||||
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) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php82Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php82Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php82Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php82Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php82Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := tools.Read("/www/server/php/" + c.version + "/etc/php.ini")
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
tools.Write("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 + "运行状态失败")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) SlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
|
||||
return 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) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) InstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
|
||||
}
|
||||
|
||||
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())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php82Controller) UninstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
|
||||
}
|
||||
|
||||
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())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return 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,79 @@
|
||||
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) http.Response {
|
||||
check := controllers.Check(ctx, "phpmyadmin")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
files, err := os.ReadDir("/www/server/phpmyadmin")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 目录")
|
||||
}
|
||||
|
||||
var phpmyadmin string
|
||||
for _, f := range files {
|
||||
if strings.HasPrefix(f.Name(), "phpmyadmin_") {
|
||||
phpmyadmin = f.Name()
|
||||
}
|
||||
}
|
||||
if len(phpmyadmin) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 目录")
|
||||
}
|
||||
|
||||
conf := tools.Read("/www/server/vhost/phpmyadmin.conf")
|
||||
match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf)
|
||||
if len(match) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 端口")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"phpmyadmin": phpmyadmin,
|
||||
"port": match[1],
|
||||
})
|
||||
}
|
||||
|
||||
func (c *PhpMyAdminController) SetPort(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "phpmyadmin")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
port := ctx.Request().Input("port")
|
||||
if len(port) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "端口不能为空")
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
return 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,612 @@
|
||||
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) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Postgresql15Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Postgresql15Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Postgresql15Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Postgresql15Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *Postgresql15Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/postgresql/data/postgresql.conf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// GetUserConfig 获取用户配置
|
||||
func (c *Postgresql15Controller) GetUserConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/postgresql/data/pg_hba.conf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *Postgresql15Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/postgresql/data/postgresql.conf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// SaveUserConfig 保存用户配置
|
||||
func (c *Postgresql15Controller) SaveUserConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *Postgresql15Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL 已停止运行")
|
||||
}
|
||||
|
||||
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`)},
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
// Log 获取日志
|
||||
func (c *Postgresql15Controller) Log(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Exec("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (c *Postgresql15Controller) ClearLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (c *Postgresql15Controller) DatabaseList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL 已停止运行")
|
||||
}
|
||||
|
||||
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) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(databaseList) {
|
||||
endIndex = len(databaseList)
|
||||
}
|
||||
pagedDatabases := databaseList[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(databaseList),
|
||||
"items": pagedDatabases,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (c *Postgresql15Controller) AddDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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 "ALTER DATABASE ` + database + ` OWNER TO ` + user + `;" | 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`)
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (c *Postgresql15Controller) DeleteDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (c *Postgresql15Controller) BackupList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
backupList, err := c.backup.PostgresqlList()
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 获取备份列表失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, backupList)
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (c *Postgresql15Controller) UploadBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (c *Postgresql15Controller) CreateBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
err = c.backup.PostgresqlBackup(database)
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 创建备份失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (c *Postgresql15Controller) DeleteBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
|
||||
fileName := ctx.Request().Input("name")
|
||||
tools.Remove(backupPath + "/" + fileName)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (c *Postgresql15Controller) RestoreBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
err = c.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 还原失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UserList 用户列表
|
||||
func (c *Postgresql15Controller) UserList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "用户列表为空")
|
||||
}
|
||||
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) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []user{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(userList) {
|
||||
endIndex = len(userList)
|
||||
}
|
||||
pagedUsers := userList[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(userList),
|
||||
"items": pagedUsers,
|
||||
})
|
||||
}
|
||||
|
||||
// AddUser 添加用户
|
||||
func (c *Postgresql15Controller) AddUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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`)
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (c *Postgresql15Controller) DeleteUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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`)
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// SetUserPassword 设置用户密码
|
||||
func (c *Postgresql15Controller) SetUserPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
tools.Exec(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
package postgresql16
|
||||
|
||||
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 Postgresql16Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func NewPostgresql16Controller() *Postgresql16Controller {
|
||||
return &Postgresql16Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *Postgresql16Controller) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Postgresql16Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Postgresql16Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Postgresql16Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Postgresql16Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *Postgresql16Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/postgresql/data/postgresql.conf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// GetUserConfig 获取用户配置
|
||||
func (c *Postgresql16Controller) GetUserConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/postgresql/data/pg_hba.conf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *Postgresql16Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/postgresql/data/postgresql.conf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// SaveUserConfig 保存用户配置
|
||||
func (c *Postgresql16Controller) SaveUserConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *Postgresql16Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL 已停止运行")
|
||||
}
|
||||
|
||||
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`)},
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
// Log 获取日志
|
||||
func (c *Postgresql16Controller) Log(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Exec("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (c *Postgresql16Controller) ClearLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (c *Postgresql16Controller) DatabaseList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL 已停止运行")
|
||||
}
|
||||
|
||||
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) != 9 || 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) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(databaseList) {
|
||||
endIndex = len(databaseList)
|
||||
}
|
||||
pagedDatabases := databaseList[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(databaseList),
|
||||
"items": pagedDatabases,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (c *Postgresql16Controller) AddDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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 "ALTER DATABASE ` + database + ` OWNER TO ` + user + `;" | 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`)
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (c *Postgresql16Controller) DeleteDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (c *Postgresql16Controller) BackupList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
backupList, err := c.backup.PostgresqlList()
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 获取备份列表失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, backupList)
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (c *Postgresql16Controller) UploadBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (c *Postgresql16Controller) CreateBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
err = c.backup.PostgresqlBackup(database)
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 创建备份失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (c *Postgresql16Controller) DeleteBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
|
||||
fileName := ctx.Request().Input("name")
|
||||
tools.Remove(backupPath + "/" + fileName)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (c *Postgresql16Controller) RestoreBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
err = c.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 还原失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UserList 用户列表
|
||||
func (c *Postgresql16Controller) UserList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "用户列表为空")
|
||||
}
|
||||
users = users[3:]
|
||||
|
||||
var userList []user
|
||||
for _, u := range users {
|
||||
userInfo := strings.Split(u, "|")
|
||||
if len(userInfo) != 2 {
|
||||
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) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []user{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(userList) {
|
||||
endIndex = len(userList)
|
||||
}
|
||||
pagedUsers := userList[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(userList),
|
||||
"items": pagedUsers,
|
||||
})
|
||||
}
|
||||
|
||||
// AddUser 添加用户
|
||||
func (c *Postgresql16Controller) AddUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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`)
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (c *Postgresql16Controller) DeleteUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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`)
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// SetUserPassword 设置用户密码
|
||||
func (c *Postgresql16Controller) SetUserPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
tools.Exec(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
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) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *PureFtpdController) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *PureFtpdController) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *PureFtpdController) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *PureFtpdController) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (c *PureFtpdController) List(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
listRaw := tools.Exec("pure-pw list")
|
||||
if len(listRaw) == 0 {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []User{},
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []User{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(users) {
|
||||
endIndex = len(users)
|
||||
}
|
||||
pagedUsers := users[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(users),
|
||||
"items": pagedUsers,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加用户
|
||||
func (c *PureFtpdController) Add(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"username": "required",
|
||||
"password": "required|min_len:6",
|
||||
"path": "required",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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) {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "目录不存在")
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (c *PureFtpdController) Delete(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"username": "required",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
username := ctx.Request().Input("username")
|
||||
|
||||
tools.Exec("pure-pw userdel " + username + " -m")
|
||||
tools.Exec("pure-pw mkdb")
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (c *PureFtpdController) ChangePassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"username": "required",
|
||||
"password": "required|min_len:6",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
username := ctx.Request().Input("username")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
tools.Exec(`yes '` + password + `' | pure-pw passwd ` + username + ` -m`)
|
||||
tools.Exec("pure-pw mkdb")
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetPort 获取端口
|
||||
func (c *PureFtpdController) GetPort(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd端口失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, port)
|
||||
}
|
||||
|
||||
// SetPort 设置端口
|
||||
func (c *PureFtpdController) SetPort(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"port": "required",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
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) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *RedisController) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload redis")
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *RedisController) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart redis")
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *RedisController) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start redis")
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *RedisController) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop redis")
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *RedisController) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/redis/redis.conf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *RedisController) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/redis/redis.conf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入Redis配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *RedisController) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "Redis 已停止运行")
|
||||
}
|
||||
|
||||
raw := tools.Exec("redis-cli info")
|
||||
if len(raw) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis负载失败")
|
||||
}
|
||||
|
||||
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"]},
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
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) http.Response {
|
||||
check := controllers.Check(ctx, "s3fs")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "获取 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(s3fsList) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []s3fs{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(s3fsList) {
|
||||
endIndex = len(s3fsList)
|
||||
}
|
||||
pagedS3fsList := s3fsList[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(s3fsList),
|
||||
"items": pagedS3fsList,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加 S3fs 挂载
|
||||
func (c *S3fsController) Add(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "s3fs")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
|
||||
}
|
||||
|
||||
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) {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "地域节点不能包含 Bucket 名称")
|
||||
}
|
||||
|
||||
// 检查挂载目录是否存在且为空
|
||||
if !tools.Exists(path) {
|
||||
tools.Mkdir(path, 0755)
|
||||
}
|
||||
if !tools.Empty(path) {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录必须为空")
|
||||
}
|
||||
|
||||
var s3fsList []s3fs
|
||||
err = sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
for _, s := range s3fsList {
|
||||
if s.Path == path {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "路径已存在")
|
||||
}
|
||||
}
|
||||
|
||||
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`)
|
||||
mountCheck := tools.Exec("mount -a 2>&1")
|
||||
if len(mountCheck) != 0 {
|
||||
tools.Exec(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck)
|
||||
}
|
||||
dfCheck := tools.Exec("df -h | grep " + path + " 2>&1")
|
||||
if len(dfCheck) == 0 {
|
||||
tools.Exec(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "挂载失败,请检查配置是否正确")
|
||||
}
|
||||
|
||||
s3fsList = append(s3fsList, s3fs{
|
||||
ID: id,
|
||||
Path: path,
|
||||
Bucket: bucket,
|
||||
Url: url,
|
||||
})
|
||||
json, err := sonic.MarshalString(s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败")
|
||||
}
|
||||
err = c.setting.Set("s3fs", json)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除 S3fs 挂载
|
||||
func (c *S3fsController) Delete(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "s3fs")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
id := ctx.Request().Input("id")
|
||||
if len(id) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不能为空")
|
||||
}
|
||||
|
||||
var s3fsList []s3fs
|
||||
err := sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
var mount s3fs
|
||||
for _, s := range s3fsList {
|
||||
if cast.ToString(s.ID) == id {
|
||||
mount = s
|
||||
break
|
||||
}
|
||||
}
|
||||
if mount.ID == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不存在")
|
||||
}
|
||||
|
||||
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`)
|
||||
mountCheck := tools.Exec("mount -a 2>&1")
|
||||
if len(mountCheck) != 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck)
|
||||
}
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败")
|
||||
}
|
||||
err = c.setting.Set("s3fs", json)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
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) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动
|
||||
func (c *SupervisorController) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec(`systemctl start ` + c.ServiceName)
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止
|
||||
func (c *SupervisorController) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec(`systemctl stop ` + c.ServiceName)
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启
|
||||
func (c *SupervisorController) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec(`systemctl restart ` + c.ServiceName)
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载
|
||||
func (c *SupervisorController) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec(`systemctl reload ` + c.ServiceName)
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Log 日志
|
||||
func (c *SupervisorController) Log(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Exec(`tail -n 200 /var/log/supervisor/supervisord.log`)
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (c *SupervisorController) ClearLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec(`echo "" > /var/log/supervisor/supervisord.log`)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Config 获取配置
|
||||
func (c *SupervisorController) Config(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
var config string
|
||||
if tools.IsRHEL() {
|
||||
config = tools.Read(`/etc/supervisord.conf`)
|
||||
} else {
|
||||
config = tools.Read(`/etc/supervisor/supervisord.conf`)
|
||||
}
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *SupervisorController) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if tools.IsRHEL() {
|
||||
tools.Write(`/etc/supervisord.conf`, config, 0644)
|
||||
} else {
|
||||
tools.Write(`/etc/supervisor/supervisord.conf`, config, 0644)
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Processes 进程列表
|
||||
func (c *SupervisorController) Processes(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []process{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(processList) {
|
||||
endIndex = len(processList)
|
||||
}
|
||||
pagedProcessList := processList[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(processList),
|
||||
"items": pagedProcessList,
|
||||
})
|
||||
}
|
||||
|
||||
// StartProcess 启动进程
|
||||
func (c *SupervisorController) StartProcess(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
tools.Exec(`supervisorctl start ` + process)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// StopProcess 停止进程
|
||||
func (c *SupervisorController) StopProcess(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
tools.Exec(`supervisorctl stop ` + process)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RestartProcess 重启进程
|
||||
func (c *SupervisorController) RestartProcess(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
tools.Exec(`supervisorctl restart ` + process)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ProcessLog 进程日志
|
||||
func (c *SupervisorController) ProcessLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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)
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearProcessLog 清空进程日志
|
||||
func (c *SupervisorController) ClearProcessLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ProcessConfig 获取进程配置
|
||||
func (c *SupervisorController) ProcessConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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`)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveProcessConfig 保存进程配置
|
||||
func (c *SupervisorController) SaveProcessConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// AddProcess 添加进程
|
||||
func (c *SupervisorController) AddProcess(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteProcess 删除进程
|
||||
func (c *SupervisorController) DeleteProcess(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
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) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
raw := tools.Read("/etc/resolv.conf")
|
||||
match := regexp.MustCompile(`nameserver\s+(\S+)`).FindAllStringSubmatch(raw, -1)
|
||||
if len(match) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "找不到 DNS 信息")
|
||||
}
|
||||
|
||||
var dns []string
|
||||
for _, m := range match {
|
||||
dns = append(dns, m[1])
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, dns)
|
||||
}
|
||||
|
||||
// SetDNS 设置 DNS 信息
|
||||
func (c *ToolBoxController) SetDNS(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
dns1 := ctx.Request().Input("dns1")
|
||||
dns2 := ctx.Request().Input("dns2")
|
||||
|
||||
if len(dns1) == 0 || len(dns2) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "DNS 信息不能为空")
|
||||
}
|
||||
|
||||
var dns string
|
||||
dns += "nameserver " + dns1 + "\n"
|
||||
dns += "nameserver " + dns2 + "\n"
|
||||
|
||||
tools.Write("/etc/resolv.conf", dns, 0644)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetSWAP 获取 SWAP 信息
|
||||
func (c *ToolBoxController) GetSWAP(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"size": size,
|
||||
"used": used,
|
||||
"free": free,
|
||||
})
|
||||
}
|
||||
|
||||
// SetSWAP 设置 SWAP 信息
|
||||
func (c *ToolBoxController) SetSWAP(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
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 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "磁盘空间不足,当前剩余 "+tools.FormatBytes(cast.ToFloat64(free)))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetTimezone 获取时区
|
||||
func (c *ToolBoxController) GetTimezone(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
raw := tools.Exec("LC_ALL=C timedatectl | grep zone")
|
||||
match := regexp.MustCompile(`zone:\s+(\S+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "找不到时区信息")
|
||||
}
|
||||
|
||||
zonesRaw := tools.Exec("LC_ALL=C timedatectl list-timezones")
|
||||
zones := strings.Split(zonesRaw, "\n")
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"zone": match[1],
|
||||
"zones": zones,
|
||||
})
|
||||
}
|
||||
|
||||
// SetTimezone 设置时区
|
||||
func (c *ToolBoxController) SetTimezone(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
timezone := ctx.Request().Input("timezone")
|
||||
if len(timezone) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "时区不能为空")
|
||||
}
|
||||
|
||||
tools.Exec("timedatectl set-timezone " + timezone)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetHosts 获取 hosts 信息
|
||||
func (c *ToolBoxController) GetHosts(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, tools.Read("/etc/hosts"))
|
||||
}
|
||||
|
||||
// SetHosts 设置 hosts 信息
|
||||
func (c *ToolBoxController) SetHosts(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
hosts := ctx.Request().Input("hosts")
|
||||
if len(hosts) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "hosts 信息不能为空")
|
||||
}
|
||||
|
||||
tools.Write("/etc/hosts", hosts, 0644)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SetRootPassword 设置 root 密码
|
||||
func (c *ToolBoxController) SetRootPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
password := ctx.Request().Input("password")
|
||||
if len(password) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "密码不能为空")
|
||||
}
|
||||
if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(password) {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "密码必须为 6-20 位字母、数字或特殊字符")
|
||||
}
|
||||
|
||||
password = strings.ReplaceAll(password, `'`, `\'`)
|
||||
tools.Exec(`yes '` + password + `' | passwd root`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type SafeController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewSafeController() *SafeController {
|
||||
return &SafeController{
|
||||
// Inject services
|
||||
}
|
||||
}
|
||||
|
||||
// GetFirewallStatus 获取防火墙状态
|
||||
func (r *SafeController) GetFirewallStatus(ctx http.Context) http.Response {
|
||||
return Success(ctx, r.firewallStatus())
|
||||
}
|
||||
|
||||
// SetFirewallStatus 设置防火墙状态
|
||||
func (r *SafeController) SetFirewallStatus(ctx http.Context) http.Response {
|
||||
var out string
|
||||
if ctx.Request().InputBool("status") {
|
||||
if tools.IsRHEL() {
|
||||
out = tools.Exec("systemctl start firewalld")
|
||||
} else {
|
||||
out = tools.Exec("echo y | ufw enable")
|
||||
}
|
||||
} else {
|
||||
if tools.IsRHEL() {
|
||||
out = tools.Exec("systemctl stop firewalld")
|
||||
} else {
|
||||
out = tools.Exec("ufw disable")
|
||||
}
|
||||
}
|
||||
|
||||
return Success(ctx, out)
|
||||
}
|
||||
|
||||
// GetFirewallRules 获取防火墙规则
|
||||
func (r *SafeController) GetFirewallRules(ctx http.Context) http.Response {
|
||||
if !r.firewallStatus() {
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
|
||||
if tools.IsRHEL() {
|
||||
out := tools.Exec("firewall-cmd --list-all 2>&1")
|
||||
match := regexp.MustCompile(`ports: (.*)`).FindStringSubmatch(out)
|
||||
if len(match) == 0 {
|
||||
return Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
}
|
||||
ports := strings.Split(match[1], " ")
|
||||
var rules []map[string]string
|
||||
for _, port := range ports {
|
||||
rule := strings.Split(port, "/")
|
||||
rules = append(rules, map[string]string{
|
||||
"port": rule[0],
|
||||
"protocol": rule[1],
|
||||
})
|
||||
}
|
||||
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(rules) {
|
||||
return Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(rules) {
|
||||
endIndex = len(rules)
|
||||
}
|
||||
pagedRules := rules[startIndex:endIndex]
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": len(rules),
|
||||
"items": pagedRules,
|
||||
})
|
||||
} else {
|
||||
out := tools.Exec("ufw status | grep -v '(v6)' | grep ALLOW | awk '{print $1}'")
|
||||
if len(out) == 0 {
|
||||
return Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
}
|
||||
var rules []map[string]string
|
||||
for _, port := range strings.Split(out, "\n") {
|
||||
rule := strings.Split(port, "/")
|
||||
rules = append(rules, map[string]string{
|
||||
"port": rule[0],
|
||||
"protocol": rule[1],
|
||||
})
|
||||
}
|
||||
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(rules) {
|
||||
return Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(rules) {
|
||||
endIndex = len(rules)
|
||||
}
|
||||
pagedRules := rules[startIndex:endIndex]
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": len(rules),
|
||||
"items": pagedRules,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// AddFirewallRule 添加防火墙规则
|
||||
func (r *SafeController) AddFirewallRule(ctx http.Context) http.Response {
|
||||
if !r.firewallStatus() {
|
||||
return Error(ctx, http.StatusBadRequest, "防火墙未启动")
|
||||
}
|
||||
|
||||
port := ctx.Request().InputInt("port", 0)
|
||||
protocol := ctx.Request().Input("protocol", "")
|
||||
if port == 0 || protocol == "" {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
if tools.IsRHEL() {
|
||||
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.Exec("ufw delete allow " + cast.ToString(port) + "/" + protocol)
|
||||
tools.Exec("ufw allow " + cast.ToString(port) + "/" + protocol)
|
||||
tools.Exec("ufw reload")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteFirewallRule 删除防火墙规则
|
||||
func (r *SafeController) DeleteFirewallRule(ctx http.Context) http.Response {
|
||||
if !r.firewallStatus() {
|
||||
return Error(ctx, http.StatusBadRequest, "防火墙未启动")
|
||||
}
|
||||
|
||||
port := ctx.Request().InputInt("port", 0)
|
||||
protocol := ctx.Request().Input("protocol", "")
|
||||
if port == 0 || protocol == "" {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
|
||||
tools.Exec("firewall-cmd --reload")
|
||||
} else {
|
||||
tools.Exec("ufw delete allow " + cast.ToString(port) + "/" + protocol)
|
||||
tools.Exec("ufw reload")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// firewallStatus 获取防火墙状态
|
||||
func (r *SafeController) firewallStatus() bool {
|
||||
var out string
|
||||
var running bool
|
||||
if tools.IsRHEL() {
|
||||
out = tools.Exec("systemctl status firewalld | grep Active | awk '{print $3}'")
|
||||
if out == "(running)" {
|
||||
running = true
|
||||
} else {
|
||||
running = false
|
||||
}
|
||||
} else {
|
||||
out = tools.Exec("ufw status | grep Status | awk '{print $2}'")
|
||||
if out == "active" {
|
||||
running = true
|
||||
} else {
|
||||
running = false
|
||||
}
|
||||
}
|
||||
|
||||
return running
|
||||
}
|
||||
|
||||
// GetSshStatus 获取 SSH 状态
|
||||
func (r *SafeController) GetSshStatus(ctx http.Context) http.Response {
|
||||
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
|
||||
}
|
||||
|
||||
return Success(ctx, running)
|
||||
}
|
||||
|
||||
// SetSshStatus 设置 SSH 状态
|
||||
func (r *SafeController) SetSshStatus(ctx http.Context) http.Response {
|
||||
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 {
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("systemctl stop sshd")
|
||||
tools.Exec("systemctl disable sshd")
|
||||
} else {
|
||||
tools.Exec("systemctl stop ssh")
|
||||
tools.Exec("systemctl disable ssh")
|
||||
}
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetSshPort 获取 SSH 端口
|
||||
func (r *SafeController) GetSshPort(ctx http.Context) http.Response {
|
||||
out := tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
return Success(ctx, out)
|
||||
}
|
||||
|
||||
// SetSshPort 设置 SSH 端口
|
||||
func (r *SafeController) SetSshPort(ctx http.Context) http.Response {
|
||||
port := ctx.Request().InputInt("port", 0)
|
||||
if port == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
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.Exec("systemctl status sshd | grep Active | awk '{print $3}'"); status == "(running)" {
|
||||
tools.Exec("systemctl restart sshd")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetPingStatus 获取 Ping 状态
|
||||
func (r *SafeController) GetPingStatus(ctx http.Context) http.Response {
|
||||
if tools.IsRHEL() {
|
||||
out := tools.Exec(`firewall-cmd --list-all 2>&1`)
|
||||
if !strings.Contains(out, `rule protocol value="icmp" drop`) {
|
||||
return Success(ctx, true)
|
||||
} else {
|
||||
return Success(ctx, false)
|
||||
}
|
||||
} else {
|
||||
config := tools.Read("/etc/ufw/before.rules")
|
||||
if strings.Contains(config, "-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT") {
|
||||
return Success(ctx, true)
|
||||
} else {
|
||||
return Success(ctx, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetPingStatus 设置 Ping 状态
|
||||
func (r *SafeController) SetPingStatus(ctx http.Context) http.Response {
|
||||
if tools.IsRHEL() {
|
||||
if ctx.Request().InputBool("status") {
|
||||
tools.Exec(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`)
|
||||
} else {
|
||||
tools.Exec(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`)
|
||||
}
|
||||
tools.Exec(`firewall-cmd --reload`)
|
||||
} else {
|
||||
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.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.Exec(`ufw reload`)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"panel/pkg/tools"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
type SettingController struct {
|
||||
setting services.Setting
|
||||
}
|
||||
|
||||
func NewSettingController() *SettingController {
|
||||
return &SettingController{
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取设置列表
|
||||
func (r *SettingController) List(ctx http.Context) http.Response {
|
||||
var settings []models.Setting
|
||||
err := facades.Orm().Query().Get(&settings)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 查询设置列表失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
var result = make(map[string]string)
|
||||
for _, setting := range settings {
|
||||
if setting.Key == models.SettingKeyMysqlRootPassword {
|
||||
continue
|
||||
}
|
||||
|
||||
result[setting.Key] = setting.Value
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err = facades.Auth().User(ctx, &user)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 获取用户失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
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'`)
|
||||
|
||||
return Success(ctx, result)
|
||||
}
|
||||
|
||||
// Save 保存设置
|
||||
func (r *SettingController) Save(ctx http.Context) http.Response {
|
||||
name := ctx.Request().Input("name")
|
||||
port := ctx.Request().Input("port")
|
||||
backupPath := ctx.Request().Input("backup_path")
|
||||
websitePath := ctx.Request().Input("website_path")
|
||||
entrance := ctx.Request().Input("entrance")
|
||||
username := ctx.Request().Input("username")
|
||||
email := ctx.Request().Input("email")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
err := r.setting.Set(models.SettingKeyName, name)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
oldPort := tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
if oldPort != port {
|
||||
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 {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
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)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeyEntrance, entrance)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err = facades.Auth().User(ctx, &user)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 获取用户失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if len(username) > 0 {
|
||||
user.Username = username
|
||||
}
|
||||
if len(email) > 0 {
|
||||
user.Email = email
|
||||
}
|
||||
if len(password) > 0 {
|
||||
hash, err := facades.Hash().Make(password)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
user.Password = hash
|
||||
}
|
||||
|
||||
if err = facades.Orm().Query().Save(&user); err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo 获取 SSH 配置
|
||||
func (r *SshController) GetInfo(ctx http.Context) http.Response {
|
||||
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 {
|
||||
return Error(ctx, http.StatusInternalServerError, "SSH 配置不完整")
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"host": host,
|
||||
"port": port,
|
||||
"user": user,
|
||||
"password": password,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateInfo 更新 SSH 配置
|
||||
func (r *SshController) UpdateInfo(ctx http.Context) http.Response {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"host": "required",
|
||||
"port": "required",
|
||||
"user": "required",
|
||||
"password": "required",
|
||||
})
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
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)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeySshPort, port)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeySshUser, user)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeySshPassword, password)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Session SSH 会话
|
||||
func (r *SshController) Session(ctx http.Context) http.Response {
|
||||
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)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
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 Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
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 Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
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()
|
||||
|
||||
return Success(ctx, logBuff.String())
|
||||
}
|
||||
@@ -4,29 +4,77 @@ import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"panel/app/models"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type TaskController struct {
|
||||
//Dependent services
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewTaskController() *TaskController {
|
||||
return &TaskController{
|
||||
//Inject services
|
||||
// Inject services
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TaskController) Status(ctx http.Context) {
|
||||
// Status 获取当前任务状态
|
||||
func (r *TaskController) Status(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("status", models.TaskStatusWaiting).OrWhere("status", models.TaskStatusRunning).FirstOrFail(&task)
|
||||
if err == nil {
|
||||
Success(ctx, http.Json{
|
||||
return Success(ctx, http.Json{
|
||||
"task": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
Success(ctx, http.Json{
|
||||
return Success(ctx, http.Json{
|
||||
"task": false,
|
||||
})
|
||||
}
|
||||
|
||||
// List 获取任务列表
|
||||
func (r *TaskController) List(ctx http.Context) http.Response {
|
||||
status := ctx.Request().Query("status")
|
||||
if len(status) == 0 {
|
||||
status = models.TaskStatusWaiting
|
||||
}
|
||||
|
||||
var tasks []models.Task
|
||||
var total int64
|
||||
err := facades.Orm().Query().Where("status", status).Paginate(ctx.Request().QueryInt("page"), ctx.Request().QueryInt("limit"), &tasks, &total)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][TaskController] 查询任务列表失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": tasks,
|
||||
})
|
||||
}
|
||||
|
||||
// Log 获取任务日志
|
||||
func (r *TaskController) Log(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("id", ctx.Request().QueryInt("id")).FirstOrFail(&task)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][TaskController] 查询任务失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
log := tools.Exec("tail -n 30 " + task.Log)
|
||||
|
||||
return Success(ctx, log)
|
||||
}
|
||||
|
||||
// Delete 删除任务
|
||||
func (r *TaskController) Delete(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
_, err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).Delete(&task)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][TaskController] 删除任务失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
@@ -9,69 +9,64 @@ import (
|
||||
)
|
||||
|
||||
type UserController struct {
|
||||
//Dependent services
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewUserController() *UserController {
|
||||
return &UserController{
|
||||
//Inject services
|
||||
// Inject services
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UserController) Login(ctx http.Context) {
|
||||
// Login 用户登录
|
||||
func (r *UserController) Login(ctx http.Context) http.Response {
|
||||
var loginRequest requests.LoginRequest
|
||||
errors, err := ctx.Request().ValidateRequest(&loginRequest)
|
||||
if err != nil {
|
||||
Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
if errors != nil {
|
||||
Error(ctx, http.StatusUnprocessableEntity, errors.All())
|
||||
return
|
||||
return Error(ctx, http.StatusUnprocessableEntity, errors.All())
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err = facades.Orm().Query().Where("username", loginRequest.Username).First(&user)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][UserController] 查询用户失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if user.ID == 0 || !facades.Hash().Check(loginRequest.Password, user.Password) {
|
||||
Error(ctx, http.StatusUnauthorized, "用户名或密码错误")
|
||||
return
|
||||
return Error(ctx, http.StatusForbidden, "用户名或密码错误")
|
||||
}
|
||||
|
||||
if facades.Hash().NeedsRehash(user.Password) {
|
||||
user.Password, err = facades.Hash().Make(loginRequest.Password)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][UserController] 更新密码失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
}
|
||||
|
||||
token, loginErr := facades.Auth().LoginUsingID(ctx, user.ID)
|
||||
if loginErr != nil {
|
||||
facades.Log().Error("[面板][UserController] 登录失败 ", loginErr)
|
||||
Error(ctx, http.StatusInternalServerError, loginErr.Error())
|
||||
return
|
||||
return Error(ctx, http.StatusInternalServerError, loginErr.Error())
|
||||
}
|
||||
|
||||
Success(ctx, http.Json{
|
||||
return Success(ctx, http.Json{
|
||||
"access_token": token,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *UserController) Info(ctx http.Context) {
|
||||
// Info 用户信息
|
||||
func (r *UserController) Info(ctx http.Context) http.Response {
|
||||
user, ok := ctx.Value("user").(models.User)
|
||||
if !ok {
|
||||
Error(ctx, http.StatusUnauthorized, "登录已过期")
|
||||
return
|
||||
return Error(ctx, http.StatusUnauthorized, "登录已过期")
|
||||
}
|
||||
|
||||
Success(ctx, http.Json{
|
||||
return Success(ctx, http.Json{
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
})
|
||||
|
||||
@@ -1,36 +1,677 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type WebsiteController struct {
|
||||
//Dependent services
|
||||
website services.Website
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
}
|
||||
|
||||
func NewWebsiteController() *WebsiteController {
|
||||
return &WebsiteController{
|
||||
//Inject services
|
||||
website: services.NewWebsiteImpl(),
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *WebsiteController) List(ctx http.Context) {
|
||||
// List 网站列表
|
||||
func (c *WebsiteController) List(ctx http.Context) http.Response {
|
||||
limit := ctx.Request().QueryInt("limit")
|
||||
page := ctx.Request().QueryInt("page")
|
||||
|
||||
var websites []models.Website
|
||||
var total int64
|
||||
err := facades.Orm().Query().Paginate(page, limit, &websites, &total)
|
||||
total, websites, err := c.website.List(page, limit)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 查询网站列表失败 ", err)
|
||||
Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
return
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站列表失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
Success(ctx, http.Json{
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": websites,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加网站
|
||||
func (c *WebsiteController) Add(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$|not_exists:websites,name",
|
||||
"domain": "required",
|
||||
"php": "required",
|
||||
"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 {
|
||||
return Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
var website services.PanelWebsite
|
||||
website.Name = ctx.Request().Input("name")
|
||||
website.Domain = ctx.Request().Input("domain")
|
||||
website.Php = ctx.Request().InputInt("php")
|
||||
website.Db = ctx.Request().InputBool("db")
|
||||
website.DbType = ctx.Request().Input("db_type")
|
||||
website.DbName = ctx.Request().Input("db_name")
|
||||
website.DbUser = ctx.Request().Input("db_user")
|
||||
website.DbPassword = ctx.Request().Input("db_password")
|
||||
|
||||
newSite, err := c.website.Add(website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 添加网站失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, newSite)
|
||||
}
|
||||
|
||||
// Delete 删除网站
|
||||
func (c *WebsiteController) Delete(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
err := c.website.Delete(id)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 删除网站失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "删除网站失败: "+err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetDefaultConfig 获取默认配置
|
||||
func (c *WebsiteController) GetDefaultConfig(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
index := tools.Read("/www/server/openresty/html/index.html")
|
||||
stop := tools.Read("/www/server/openresty/html/stop.html")
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"index": index,
|
||||
"stop": stop,
|
||||
})
|
||||
}
|
||||
|
||||
// SaveDefaultConfig 保存默认配置
|
||||
func (c *WebsiteController) SaveDefaultConfig(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
index := ctx.Request().Input("index")
|
||||
stop := ctx.Request().Input("stop")
|
||||
|
||||
if !tools.Write("/www/server/openresty/html/index.html", index, 0644) {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存默认配置失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/openresty/html/stop.html", stop, 0644) {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存默认配置失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *WebsiteController) GetConfig(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
config, err := c.website.GetConfig(id)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *WebsiteController) SaveConfig(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"id": "required",
|
||||
"domains": "required",
|
||||
"ports": "required",
|
||||
"hsts": "bool",
|
||||
"ssl": "bool",
|
||||
"http_redirect": "bool",
|
||||
"open_basedir": "bool",
|
||||
"waf": "required",
|
||||
"waf_cache": "required",
|
||||
"waf_mode": "required",
|
||||
"waf_cc_deny": "required",
|
||||
"index": "required",
|
||||
"path": "required",
|
||||
"root": "required",
|
||||
"raw": "required",
|
||||
"php": "required",
|
||||
"ssl_certificate": "required_if:ssl,true",
|
||||
"ssl_certificate_key": "required_if:ssl,true",
|
||||
})
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
var website models.Website
|
||||
if facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&website) != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "网站不存在")
|
||||
}
|
||||
|
||||
if !website.Status {
|
||||
return Error(ctx, http.StatusBadRequest, "网站已停用,请先启用")
|
||||
}
|
||||
|
||||
// 原文
|
||||
raw := tools.Read("/www/server/vhost/" + website.Name + ".conf")
|
||||
if strings.TrimSpace(raw) != strings.TrimSpace(ctx.Request().Input("raw")) {
|
||||
tools.Write("/www/server/vhost/"+website.Name+".conf", ctx.Request().Input("raw"), 0644)
|
||||
tools.Exec("systemctl reload openresty")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// 目录
|
||||
path := ctx.Request().Input("path")
|
||||
if !tools.Exists(path) {
|
||||
return Error(ctx, http.StatusBadRequest, "网站目录不存在")
|
||||
}
|
||||
website.Path = path
|
||||
|
||||
// 域名
|
||||
domain := "server_name"
|
||||
domains := strings.Split(ctx.Request().Input("domains"), "\n")
|
||||
if len(domains) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "域名不能为空")
|
||||
}
|
||||
for _, v := range domains {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
domain += " " + v
|
||||
}
|
||||
domain += ";"
|
||||
domainConfigOld := tools.Cut(raw, "# server_name标记位开始", "# server_name标记位结束")
|
||||
if len(strings.TrimSpace(domainConfigOld)) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "配置文件中缺少server_name标记位")
|
||||
}
|
||||
raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1)
|
||||
|
||||
// 端口
|
||||
var port strings.Builder
|
||||
ports := strings.Split(ctx.Request().Input("ports"), "\n")
|
||||
if len(ports) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "端口不能为空")
|
||||
}
|
||||
for i, v := range ports {
|
||||
if _, err := strconv.Atoi(v); err != nil && v != "443 ssl http2" {
|
||||
return Error(ctx, http.StatusBadRequest, "端口格式错误")
|
||||
}
|
||||
if v == "443" && ctx.Request().InputBool("ssl") {
|
||||
v = "443 ssl http2"
|
||||
}
|
||||
if i != len(ports)-1 {
|
||||
port.WriteString(" listen " + v + ";\n")
|
||||
} else {
|
||||
port.WriteString(" listen " + v + ";")
|
||||
}
|
||||
}
|
||||
portConfigOld := tools.Cut(raw, "# port标记位开始", "# port标记位结束")
|
||||
if len(strings.TrimSpace(portConfigOld)) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "配置文件中缺少port标记位")
|
||||
}
|
||||
raw = strings.Replace(raw, portConfigOld, "\n"+port.String()+"\n ", -1)
|
||||
|
||||
// 运行目录
|
||||
root := tools.Cut(raw, "# root标记位开始", "# root标记位结束")
|
||||
if len(strings.TrimSpace(root)) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "配置文件中缺少root标记位")
|
||||
}
|
||||
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root)
|
||||
if len(match) != 2 {
|
||||
return Error(ctx, http.StatusBadRequest, "配置文件中root标记位格式错误")
|
||||
}
|
||||
rootNew := strings.Replace(root, match[1], ctx.Request().Input("root"), -1)
|
||||
raw = strings.Replace(raw, root, rootNew, -1)
|
||||
|
||||
// 默认文件
|
||||
index := tools.Cut(raw, "# index标记位开始", "# index标记位结束")
|
||||
if len(strings.TrimSpace(index)) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "配置文件中缺少index标记位")
|
||||
}
|
||||
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index)
|
||||
if len(match) != 2 {
|
||||
return Error(ctx, http.StatusBadRequest, "配置文件中index标记位格式错误")
|
||||
}
|
||||
indexNew := strings.Replace(index, match[1], ctx.Request().Input("index"), -1)
|
||||
raw = strings.Replace(raw, index, indexNew, -1)
|
||||
|
||||
// 防跨站
|
||||
root = ctx.Request().Input("root")
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root += "/"
|
||||
}
|
||||
if ctx.Request().InputBool("open_basedir") {
|
||||
tools.Write(root+".user.ini", "open_basedir="+path+":/tmp/", 0644)
|
||||
} else {
|
||||
if tools.Exists(root + ".user.ini") {
|
||||
tools.Remove(root + ".user.ini")
|
||||
}
|
||||
}
|
||||
|
||||
// WAF
|
||||
waf := ctx.Request().Input("waf")
|
||||
wafMode := ctx.Request().Input("waf_mode", "DYNAMIC")
|
||||
wafCcDeny := ctx.Request().Input("waf_cc_deny", "rate=1000r/m duration=60m")
|
||||
wafCache := ctx.Request().Input("waf_cache", "capacity=50")
|
||||
wafConfig := `# waf标记位开始
|
||||
waf ` + waf + `;
|
||||
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
|
||||
waf_mode ` + wafMode + `;
|
||||
waf_cc_deny ` + wafCcDeny + `;
|
||||
waf_cache ` + wafCache + `;
|
||||
`
|
||||
wafConfigOld := tools.Cut(raw, "# waf标记位开始", "# waf标记位结束")
|
||||
if len(strings.TrimSpace(wafConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, wafConfigOld, "", -1)
|
||||
}
|
||||
raw = strings.Replace(raw, "# waf标记位开始", wafConfig, -1)
|
||||
|
||||
// SSL
|
||||
ssl := ctx.Request().InputBool("ssl")
|
||||
website.Ssl = ssl
|
||||
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 {
|
||||
sslConfig := `# ssl标记位开始
|
||||
ssl_certificate /www/server/vhost/ssl/` + website.Name + `.pem;
|
||||
ssl_certificate_key /www/server/vhost/ssl/` + website.Name + `.key;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_tickets off;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
`
|
||||
if ctx.Request().InputBool("http_redirect") {
|
||||
sslConfig += `# http重定向标记位开始
|
||||
if ($server_port !~ 443){
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
error_page 497 https://$host$request_uri;
|
||||
# http重定向标记位结束
|
||||
`
|
||||
}
|
||||
if ctx.Request().InputBool("hsts") {
|
||||
sslConfig += `# hsts标记位开始
|
||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
# hsts标记位结束
|
||||
`
|
||||
}
|
||||
sslConfigOld := tools.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
|
||||
if len(strings.TrimSpace(sslConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, sslConfigOld, "", -1)
|
||||
}
|
||||
raw = strings.Replace(raw, "# ssl标记位开始", sslConfig, -1)
|
||||
} else {
|
||||
sslConfigOld := tools.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
|
||||
if len(strings.TrimSpace(sslConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, sslConfigOld, "\n ", -1)
|
||||
}
|
||||
}
|
||||
|
||||
if website.Php != ctx.Request().InputInt("php") {
|
||||
website.Php = ctx.Request().InputInt("php")
|
||||
phpConfigOld := tools.Cut(raw, "# php标记位开始", "# php标记位结束")
|
||||
phpConfig := `
|
||||
include enable-php-` + strconv.Itoa(website.Php) + `.conf;
|
||||
`
|
||||
if len(strings.TrimSpace(phpConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, phpConfigOld, phpConfig, -1)
|
||||
}
|
||||
}
|
||||
|
||||
err = facades.Orm().Query().Save(&website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存网站配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (c *WebsiteController) ClearLog(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
err := facades.Orm().Query().Where("id", id).Get(&website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
tools.Remove("/www/wwwlogs/" + website.Name + ".log")
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UpdateRemark 更新备注
|
||||
func (c *WebsiteController) UpdateRemark(ctx http.Context) http.Response {
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
err := facades.Orm().Query().Where("id", id).Get(&website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
website.Remark = ctx.Request().Input("remark")
|
||||
err = facades.Orm().Query().Save(&website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存网站备注失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BackupList 备份列表
|
||||
func (c *WebsiteController) BackupList(ctx http.Context) http.Response {
|
||||
backupList, err := c.backup.WebsiteList()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站备份列表失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, backupList)
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (c *WebsiteController) CreateBackup(ctx http.Context) http.Response {
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
err := facades.Orm().Query().Where("id", id).Get(&website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "获取网站信息失败: "+err.Error())
|
||||
}
|
||||
|
||||
err = c.backup.WebSiteBackup(website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 备份网站失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "备份网站失败: "+err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (c *WebsiteController) UploadBackup(ctx http.Context) http.Response {
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
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 {
|
||||
return Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
return Success(ctx, "上传文件成功")
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (c *WebsiteController) RestoreBackup(ctx http.Context) http.Response {
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
fileName := ctx.Request().Input("name")
|
||||
if len(fileName) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
err := facades.Orm().Query().Where("id", id).Get(&website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "获取网站信息失败: "+err.Error())
|
||||
}
|
||||
|
||||
err = c.backup.WebsiteRestore(website, fileName)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 还原网站失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "还原网站失败: "+err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (c *WebsiteController) DeleteBackup(ctx http.Context) http.Response {
|
||||
fileName := ctx.Request().Input("name")
|
||||
if len(fileName) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/website"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
if !tools.Remove(backupPath + "/" + fileName) {
|
||||
return Error(ctx, http.StatusInternalServerError, "删除备份失败")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ResetConfig 重置配置
|
||||
func (c *WebsiteController) ResetConfig(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
if err := facades.Orm().Query().Where("id", id).Get(&website); err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
website.Status = true
|
||||
website.Ssl = false
|
||||
if err := facades.Orm().Query().Save(&website); err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存网站配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
raw := fmt.Sprintf(`
|
||||
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
|
||||
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
|
||||
server
|
||||
{
|
||||
# port标记位开始
|
||||
listen 80;
|
||||
# port标记位结束
|
||||
# server_name标记位开始
|
||||
server_name localhost;
|
||||
# server_name标记位结束
|
||||
# index标记位开始
|
||||
index index.php index.html;
|
||||
# index标记位结束
|
||||
# root标记位开始
|
||||
root %s;
|
||||
# root标记位结束
|
||||
|
||||
# ssl标记位开始
|
||||
# ssl标记位结束
|
||||
|
||||
# php标记位开始
|
||||
include enable-php-%d.conf;
|
||||
# php标记位结束
|
||||
|
||||
# waf标记位开始
|
||||
waf on;
|
||||
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
|
||||
waf_mode DYNAMIC;
|
||||
waf_cc_deny rate=1000r/m duration=60m;
|
||||
waf_cache capacity=50;
|
||||
# waf标记位结束
|
||||
|
||||
# 错误页配置,可自行设置
|
||||
#error_page 404 /404.html;
|
||||
#error_page 502 /502.html;
|
||||
|
||||
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
|
||||
include /www/server/vhost/rewrite/%s.conf;
|
||||
|
||||
# 面板默认禁止访问部分敏感目录,可自行修改
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
|
||||
{
|
||||
return 404;
|
||||
}
|
||||
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
|
||||
location ~ .*\.(js|css)$
|
||||
{
|
||||
expires 1h;
|
||||
error_log /dev/null;
|
||||
access_log /dev/null;
|
||||
}
|
||||
|
||||
access_log /www/wwwlogs/%s.log;
|
||||
error_log /www/wwwlogs/%s.log;
|
||||
}
|
||||
|
||||
`, website.Path, website.Php, website.Name, website.Name, website.Name)
|
||||
|
||||
tools.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644)
|
||||
tools.Write("/www/server/vhost/rewrite"+website.Name+".conf", "", 0644)
|
||||
tools.Exec("systemctl reload openresty")
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Status 网站状态
|
||||
func (c *WebsiteController) Status(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
if err := facades.Orm().Query().Where("id", id).Get(&website); err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
website.Status = ctx.Request().InputBool("status")
|
||||
if err := facades.Orm().Query().Save(&website); err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存网站配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
raw := tools.Read("/www/server/vhost/" + website.Name + ".conf")
|
||||
|
||||
// 运行目录
|
||||
rootConfig := tools.Cut(raw, "# root标记位开始\n", "# root标记位结束")
|
||||
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig)
|
||||
if len(match) == 2 {
|
||||
if website.Status {
|
||||
root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig)
|
||||
raw = strings.ReplaceAll(raw, rootConfig, " root "+root[1]+";\n ")
|
||||
} else {
|
||||
raw = strings.ReplaceAll(raw, rootConfig, " root /www/server/openresty/html;\n # root "+match[1]+";\n ")
|
||||
}
|
||||
}
|
||||
|
||||
// 默认文件
|
||||
indexConfig := tools.Cut(raw, "# index标记位开始\n", "# index标记位结束")
|
||||
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig)
|
||||
if len(match) == 2 {
|
||||
if website.Status {
|
||||
index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig)
|
||||
raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ")
|
||||
} else {
|
||||
raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ")
|
||||
}
|
||||
}
|
||||
|
||||
tools.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644)
|
||||
tools.Exec("systemctl reload openresty")
|
||||
|
||||
return 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": "登录已过期",
|
||||
})
|
||||
|
||||
@@ -2,16 +2,15 @@ package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-contrib/static"
|
||||
|
||||
contractshttp "github.com/goravel/framework/contracts/http"
|
||||
frameworkhttp "github.com/goravel/framework/http"
|
||||
"github.com/goravel/gin"
|
||||
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
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.(*gin.Context).Instance())
|
||||
|
||||
ctx.Request().Next()
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ package models
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"panel/packages/helpers"
|
||||
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Monitor struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Info helpers.MonitoringInfo `gorm:"type:json;serializer:json" json:"info"`
|
||||
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
|
||||
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Info tools.MonitoringInfo `gorm:"type:json;serializer:json" json:"info"`
|
||||
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
|
||||
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,21 @@ package models
|
||||
|
||||
import "github.com/goravel/framework/support/carbon"
|
||||
|
||||
const (
|
||||
SettingKeyName = "name"
|
||||
SettingKeyVersion = "version"
|
||||
SettingKeyMonitor = "monitor"
|
||||
SettingKeyMonitorDays = "monitor_days"
|
||||
SettingKeyBackupPath = "backup_path"
|
||||
SettingKeyWebsitePath = "website_path"
|
||||
SettingKeyEntrance = "entrance"
|
||||
SettingKeyMysqlRootPassword = "mysql_root_password"
|
||||
SettingKeySshHost = "ssh_host"
|
||||
SettingKeySshPort = "ssh_port"
|
||||
SettingKeySshUser = "ssh_user"
|
||||
SettingKeySshPassword = "ssh_password"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Key string `gorm:"unique;not null" json:"key"`
|
||||
|
||||
@@ -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`
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package mysql57
|
||||
|
||||
var (
|
||||
Name = "MySQL-5.7"
|
||||
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。"
|
||||
Slug = "mysql57"
|
||||
Version = "5.7.43"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"mysql80"}
|
||||
Install = `bash /www/panel/scripts/mysql/install.sh 57`
|
||||
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 57`
|
||||
Update = `bash /www/panel/scripts/mysql/update.sh 57`
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package mysql80
|
||||
|
||||
var (
|
||||
Name = "MySQL-8.0"
|
||||
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。(内存 < 4G 无法安装)"
|
||||
Slug = "mysql80"
|
||||
Version = "8.0.34"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"mysql57"}
|
||||
Install = `bash /www/panel/scripts/mysql/install.sh 80`
|
||||
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 80`
|
||||
Update = `bash /www/panel/scripts/mysql/update.sh 80`
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package openresty
|
||||
|
||||
var (
|
||||
Name = "OpenResty"
|
||||
Description = "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台。"
|
||||
Slug = "openresty"
|
||||
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"
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package php74
|
||||
|
||||
var (
|
||||
Name = "PHP-7.4"
|
||||
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`
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package php80
|
||||
|
||||
var (
|
||||
Name = "PHP-8.0"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php80"
|
||||
Version = "8.0.30"
|
||||
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.23"
|
||||
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.10"
|
||||
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.4"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"postgresql16"}
|
||||
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 postgresql16
|
||||
|
||||
var (
|
||||
Name = "PostgreSQL-16"
|
||||
Description = "PostgreSQL 是世界上最先进的开源关系数据库,在类似 BSD 与 MIT 许可的 PostgreSQL 许可下发行。"
|
||||
Slug = "postgresql16"
|
||||
Version = "16.0"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"postgresql15"}
|
||||
Install = `bash /www/panel/scripts/postgresql/install.sh 16`
|
||||
Uninstall = `bash /www/panel/scripts/postgresql/uninstall.sh 16`
|
||||
Update = `bash /www/panel/scripts/postgresql/update.sh 16`
|
||||
)
|
||||
@@ -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`
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/database/seeder"
|
||||
"github.com/goravel/framework/contracts/foundation"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/database/seeders"
|
||||
)
|
||||
|
||||
type DatabaseServiceProvider struct {
|
||||
}
|
||||
|
||||
func (receiver *DatabaseServiceProvider) Register(app foundation.Application) {
|
||||
|
||||
}
|
||||
|
||||
func (receiver *DatabaseServiceProvider) Boot(app foundation.Application) {
|
||||
facades.Seeder().Register([]seeder.Seeder{
|
||||
&seeders.DatabaseSeeder{},
|
||||
})
|
||||
}
|
||||
@@ -12,14 +12,15 @@ type RouteServiceProvider struct {
|
||||
}
|
||||
|
||||
func (receiver *RouteServiceProvider) Register(app foundation.Application) {
|
||||
//Add HTTP middlewares
|
||||
facades.Route().GlobalMiddleware(http.Kernel{}.Middleware()...)
|
||||
}
|
||||
|
||||
func (receiver *RouteServiceProvider) Boot(app foundation.Application) {
|
||||
// Add HTTP middlewares
|
||||
facades.Route().GlobalMiddleware(http.Kernel{}.Middleware()...)
|
||||
|
||||
receiver.configureRateLimiting()
|
||||
|
||||
routes.Web()
|
||||
routes.Api()
|
||||
routes.Plugin()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
|
||||
"panel/packages/captcha"
|
||||
"panel/pkg/captcha"
|
||||
)
|
||||
|
||||
type Captcha struct {
|
||||
|
||||
+301
-2
@@ -1,7 +1,306 @@
|
||||
// Package services 备份服务
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Backup interface {
|
||||
BackupWebSite(string) error
|
||||
BackupDatabase(string, string) error
|
||||
WebsiteList() ([]BackupFile, error)
|
||||
WebSiteBackup(website models.Website) error
|
||||
WebsiteRestore(website models.Website, backupFile string) error
|
||||
MysqlList() ([]BackupFile, error)
|
||||
MysqlBackup(database string) error
|
||||
MysqlRestore(database string, backupFile string) error
|
||||
PostgresqlList() ([]BackupFile, error)
|
||||
PostgresqlBackup(database string) error
|
||||
PostgresqlRestore(database string, backupFile string) error
|
||||
}
|
||||
|
||||
type BackupFile struct {
|
||||
Name string `json:"name"`
|
||||
Size string `json:"size"`
|
||||
}
|
||||
|
||||
type BackupImpl struct {
|
||||
setting Setting
|
||||
}
|
||||
|
||||
func NewBackupImpl() *BackupImpl {
|
||||
return &BackupImpl{
|
||||
setting: NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// WebsiteList 网站备份列表
|
||||
func (s *BackupImpl) WebsiteList() ([]BackupFile, error) {
|
||||
backupPath := s.setting.Get(models.SettingKeyBackupPath)
|
||||
if len(backupPath) == 0 {
|
||||
return []BackupFile{}, nil
|
||||
}
|
||||
|
||||
backupPath += "/website"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(backupPath)
|
||||
if err != nil {
|
||||
return []BackupFile{}, err
|
||||
}
|
||||
var backupList []BackupFile
|
||||
for _, file := range files {
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
backupList = append(backupList, BackupFile{
|
||||
Name: file.Name(),
|
||||
Size: tools.FormatBytes(float64(info.Size())),
|
||||
})
|
||||
}
|
||||
|
||||
return backupList, nil
|
||||
}
|
||||
|
||||
// WebSiteBackup 网站备份
|
||||
func (s *BackupImpl) WebSiteBackup(website models.Website) error {
|
||||
backupPath := s.setting.Get(models.SettingKeyBackupPath)
|
||||
if len(backupPath) == 0 {
|
||||
return errors.New("未正确配置备份路径")
|
||||
}
|
||||
|
||||
backupPath += "/website"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
backupFile := backupPath + "/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".zip"
|
||||
tools.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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Cron interface {
|
||||
AddToSystem(cron models.Cron)
|
||||
DeleteFromSystem(cron models.Cron)
|
||||
}
|
||||
|
||||
type CronImpl struct {
|
||||
}
|
||||
|
||||
func NewCronImpl() *CronImpl {
|
||||
return &CronImpl{}
|
||||
}
|
||||
|
||||
// AddToSystem 添加到系统
|
||||
func (r *CronImpl) AddToSystem(cron models.Cron) {
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("echo \"" + cron.Time + " " + cron.Shell + " >> " + cron.Log + " 2>&1\" >> /var/spool/cron/root")
|
||||
tools.Exec("systemctl restart crond")
|
||||
} else {
|
||||
tools.Exec("echo \"" + cron.Time + " " + cron.Shell + " >> " + cron.Log + " 2>&1\" >> /var/spool/cron/crontabs/root")
|
||||
tools.Exec("systemctl restart cron")
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteFromSystem 从系统中删除
|
||||
func (r *CronImpl) DeleteFromSystem(cron models.Cron) {
|
||||
// 需要转义Shell路径的/为\/
|
||||
cron.Shell = strings.ReplaceAll(cron.Shell, "/", "\\/")
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("sed -i '/" + cron.Shell + "/d' /var/spool/cron/root")
|
||||
tools.Exec("systemctl restart crond")
|
||||
} else {
|
||||
tools.Exec("sed -i '/" + cron.Shell + "/d' /var/spool/cron/crontabs/root")
|
||||
tools.Exec("systemctl restart cron")
|
||||
}
|
||||
}
|
||||
+189
-3
@@ -5,23 +5,42 @@ import (
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/plugins/openresty"
|
||||
"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/postgresql16"
|
||||
"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 {
|
||||
@@ -47,12 +66,179 @@ 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,
|
||||
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: postgresql16.Name,
|
||||
Description: postgresql16.Description,
|
||||
Slug: postgresql16.Slug,
|
||||
Version: postgresql16.Version,
|
||||
Requires: postgresql16.Requires,
|
||||
Excludes: postgresql16.Excludes,
|
||||
Install: postgresql16.Install,
|
||||
Uninstall: postgresql16.Uninstall,
|
||||
Update: postgresql16.Update,
|
||||
})
|
||||
p = append(p, PanelPlugin{
|
||||
Name: php74.Name,
|
||||
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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/queue"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/jobs"
|
||||
)
|
||||
|
||||
type Task interface {
|
||||
Process(taskID uint)
|
||||
}
|
||||
|
||||
type TaskImpl struct {
|
||||
}
|
||||
|
||||
func NewTaskImpl() *TaskImpl {
|
||||
return &TaskImpl{}
|
||||
}
|
||||
|
||||
func (r *TaskImpl) Process(taskID uint) {
|
||||
go func() {
|
||||
err := facades.Queue().Job(&jobs.ProcessTask{}, []queue.Arg{
|
||||
{Type: "uint", Value: taskID},
|
||||
}).Dispatch()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][TaskService] 运行任务失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
+325
-4
@@ -1,18 +1,35 @@
|
||||
// Package services 网站服务
|
||||
package services
|
||||
|
||||
import "panel/app/models"
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/facades"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Website interface {
|
||||
List() ([]models.Website, error)
|
||||
List(page int, limit int) (int64, []models.Website, error)
|
||||
Add(website PanelWebsite) (models.Website, error)
|
||||
Delete(id int) error
|
||||
GetConfig(id int) (WebsiteSetting, error)
|
||||
}
|
||||
|
||||
type PanelWebsite struct {
|
||||
Name string `json:"name"`
|
||||
Status bool `json:"status"`
|
||||
Domain string `json:"domain"`
|
||||
Path string `json:"path"`
|
||||
Php string `json:"php"`
|
||||
Ssl string `json:"ssl"`
|
||||
Php int `json:"php"`
|
||||
Ssl bool `json:"ssl"`
|
||||
Remark string `json:"remark"`
|
||||
Db bool `json:"db"`
|
||||
DbType string `json:"db_type"`
|
||||
@@ -29,10 +46,16 @@ type WebsiteSetting struct {
|
||||
Root string `json:"root"`
|
||||
Path string `json:"path"`
|
||||
Index string `json:"index"`
|
||||
Php int `json:"php"`
|
||||
OpenBasedir bool `json:"open_basedir"`
|
||||
Ssl bool `json:"ssl"`
|
||||
SslCertificate string `json:"ssl_certificate"`
|
||||
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"`
|
||||
@@ -43,3 +66,301 @@ type WebsiteSetting struct {
|
||||
Raw string `json:"raw"`
|
||||
Log string `json:"log"`
|
||||
}
|
||||
|
||||
type WebsiteImpl struct {
|
||||
setting Setting
|
||||
}
|
||||
|
||||
func NewWebsiteImpl() *WebsiteImpl {
|
||||
return &WebsiteImpl{
|
||||
setting: NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 列出网站
|
||||
func (r *WebsiteImpl) List(page, limit int) (int64, []models.Website, error) {
|
||||
var websites []models.Website
|
||||
var total int64
|
||||
if err := facades.Orm().Query().Paginate(page, limit, &websites, &total); err != nil {
|
||||
return total, websites, err
|
||||
}
|
||||
|
||||
return total, websites, nil
|
||||
}
|
||||
|
||||
// Add 添加网站
|
||||
func (r *WebsiteImpl) Add(website PanelWebsite) (models.Website, error) {
|
||||
// 禁止部分保留名称
|
||||
nameSlices := []string{"phpmyadmin", "mysql", "panel", "ssh"}
|
||||
if slices.Contains(nameSlices, website.Name) {
|
||||
return models.Website{}, errors.New("网站名称" + website.Name + "为保留名称,请更换")
|
||||
}
|
||||
|
||||
// path为空时,设置默认值
|
||||
if len(website.Path) == 0 {
|
||||
website.Path = r.setting.Get(models.SettingKeyWebsitePath) + "/" + website.Name
|
||||
}
|
||||
// path不为/开头时,返回错误
|
||||
if website.Path[0] != '/' {
|
||||
return models.Website{}, errors.New("网站路径" + website.Path + "必须以/开头")
|
||||
}
|
||||
|
||||
website.Ssl = false
|
||||
website.Status = true
|
||||
website.Domain = strings.TrimSpace(website.Domain)
|
||||
|
||||
w := models.Website{
|
||||
Name: website.Name,
|
||||
Status: website.Status,
|
||||
Path: website.Path,
|
||||
Php: website.Php,
|
||||
Ssl: website.Ssl,
|
||||
Remark: website.Remark,
|
||||
}
|
||||
if err := facades.Orm().Query().Create(&w); err != nil {
|
||||
return w, err
|
||||
}
|
||||
|
||||
tools.Mkdir(website.Path, 0755)
|
||||
|
||||
index := `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>耗子Linux面板</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>耗子Linux面板</h1>
|
||||
<p>这是耗子Linux面板的网站默认页面!</p>
|
||||
<p>当您看到此页面,说明您的网站已创建成功。</p>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
tools.Write(website.Path+"/index.html", index, 0644)
|
||||
|
||||
domainArr := strings.Split(website.Domain, "\n")
|
||||
portList := ""
|
||||
portArr := make(map[string]bool)
|
||||
domainList := ""
|
||||
for key, value := range domainArr {
|
||||
temp := strings.Split(value, ":")
|
||||
domainList += " " + temp[0]
|
||||
|
||||
if len(temp) < 2 {
|
||||
if _, ok := portArr["80"]; !ok {
|
||||
if key == len(domainArr)-1 {
|
||||
portList += " listen 80;"
|
||||
} else {
|
||||
portList += " listen 80;\n"
|
||||
}
|
||||
portArr["80"] = true
|
||||
}
|
||||
} else {
|
||||
if _, ok := portArr[temp[1]]; !ok {
|
||||
if key == len(domainArr)-1 {
|
||||
portList += " listen " + temp[1] + ";"
|
||||
} else {
|
||||
portList += " listen " + temp[1] + ";\n"
|
||||
}
|
||||
portArr[temp[1]] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
|
||||
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
|
||||
server
|
||||
{
|
||||
# port标记位开始
|
||||
%s
|
||||
# port标记位结束
|
||||
# server_name标记位开始
|
||||
server_name%s;
|
||||
# server_name标记位结束
|
||||
# index标记位开始
|
||||
index index.php index.html;
|
||||
# index标记位结束
|
||||
# root标记位开始
|
||||
root %s;
|
||||
# root标记位结束
|
||||
|
||||
# ssl标记位开始
|
||||
# ssl标记位结束
|
||||
|
||||
# php标记位开始
|
||||
include enable-php-%d.conf;
|
||||
# php标记位结束
|
||||
|
||||
# waf标记位开始
|
||||
waf on;
|
||||
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
|
||||
waf_mode DYNAMIC;
|
||||
waf_cc_deny rate=1000r/m duration=60m;
|
||||
waf_cache capacity=50;
|
||||
# waf标记位结束
|
||||
|
||||
# 错误页配置,可自行设置
|
||||
#error_page 404 /404.html;
|
||||
#error_page 502 /502.html;
|
||||
|
||||
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
|
||||
include /www/server/vhost/rewrite/%s.conf;
|
||||
|
||||
# 面板默认禁止访问部分敏感目录,可自行修改
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
|
||||
{
|
||||
return 404;
|
||||
}
|
||||
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
|
||||
location ~ .*\.(js|css)$
|
||||
{
|
||||
expires 1h;
|
||||
error_log /dev/null;
|
||||
access_log /dev/null;
|
||||
}
|
||||
|
||||
access_log /www/wwwlogs/%s.log;
|
||||
error_log /www/wwwlogs/%s.log;
|
||||
}
|
||||
`, portList, domainList, website.Path, website.Php, website.Name, website.Name, website.Name)
|
||||
|
||||
tools.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.Chmod(r.setting.Get(models.SettingKeyWebsitePath), 0755)
|
||||
tools.Chmod(website.Path, 0755)
|
||||
tools.Chown(r.setting.Get(models.SettingKeyWebsitePath), "www", "www")
|
||||
tools.Chown(website.Path, "www", "www")
|
||||
|
||||
tools.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
|
||||
}
|
||||
|
||||
// Delete 删除网站
|
||||
func (r *WebsiteImpl) Delete(id int) error {
|
||||
var website models.Website
|
||||
if err := facades.Orm().Query().Where("id", id).FirstOrFail(&website); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := facades.Orm().Query().Delete(&website); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tools.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.Exec("systemctl reload openresty")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfig 获取网站配置
|
||||
func (r *WebsiteImpl) GetConfig(id int) (WebsiteSetting, error) {
|
||||
var website models.Website
|
||||
if err := facades.Orm().Query().Where("id", id).First(&website); err != nil {
|
||||
return WebsiteSetting{}, err
|
||||
}
|
||||
|
||||
config := tools.Read("/www/server/vhost/" + website.Name + ".conf")
|
||||
|
||||
var setting WebsiteSetting
|
||||
setting.Name = website.Name
|
||||
setting.Path = website.Path
|
||||
setting.Ssl = website.Ssl
|
||||
setting.Php = website.Php
|
||||
setting.Raw = config
|
||||
|
||||
ports := tools.Cut(config, "# port标记位开始", "# port标记位结束")
|
||||
matches := regexp.MustCompile(`listen\s+(.*);`).FindAllStringSubmatch(ports, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) < 2 {
|
||||
continue
|
||||
}
|
||||
setting.Ports = append(setting.Ports, match[1])
|
||||
}
|
||||
serverName := tools.Cut(config, "# server_name标记位开始", "# server_name标记位结束")
|
||||
match := regexp.MustCompile(`server_name\s+(.*);`).FindStringSubmatch(serverName)
|
||||
if len(match) > 1 {
|
||||
setting.Domains = strings.Split(match[1], " ")
|
||||
}
|
||||
root := tools.Cut(config, "# root标记位开始", "# root标记位结束")
|
||||
match = regexp.MustCompile(`root\s+(.*);`).FindStringSubmatch(root)
|
||||
if len(match) > 1 {
|
||||
setting.Root = match[1]
|
||||
}
|
||||
index := tools.Cut(config, "# index标记位开始", "# index标记位结束")
|
||||
match = regexp.MustCompile(`index\s+(.*);`).FindStringSubmatch(index)
|
||||
if len(match) > 1 {
|
||||
setting.Index = match[1]
|
||||
}
|
||||
|
||||
if tools.Exists(setting.Root + "/.user.ini") {
|
||||
userIni := tools.Read(setting.Path + "/.user.ini")
|
||||
if strings.Contains(userIni, "open_basedir") {
|
||||
setting.OpenBasedir = true
|
||||
} else {
|
||||
setting.OpenBasedir = false
|
||||
}
|
||||
} else {
|
||||
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标记位结束")
|
||||
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.HttpRedirect = false
|
||||
setting.Hsts = false
|
||||
}
|
||||
|
||||
waf := tools.Cut(config, "# waf标记位开始", "# waf标记位结束")
|
||||
setting.Waf = strings.Contains(waf, "waf on;")
|
||||
match = regexp.MustCompile(`waf_mode\s+(.+);`).FindStringSubmatch(waf)
|
||||
if len(match) > 1 {
|
||||
setting.WafMode = match[1]
|
||||
}
|
||||
match = regexp.MustCompile(`waf_cc_deny\s+(.+);`).FindStringSubmatch(waf)
|
||||
if len(match) > 1 {
|
||||
setting.WafCcDeny = match[1]
|
||||
}
|
||||
match = regexp.MustCompile(`waf_cache\s+(.+);`).FindStringSubmatch(waf)
|
||||
if len(match) > 1 {
|
||||
setting.WafCache = match[1]
|
||||
}
|
||||
|
||||
setting.Rewrite = tools.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()
|
||||
}
|
||||
|
||||
+10
-3
@@ -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"
|
||||
@@ -17,7 +18,9 @@ import (
|
||||
"github.com/goravel/framework/route"
|
||||
"github.com/goravel/framework/schedule"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/goravel/framework/testing"
|
||||
"github.com/goravel/framework/validation"
|
||||
"github.com/goravel/gin"
|
||||
|
||||
"panel/app/providers"
|
||||
)
|
||||
@@ -32,14 +35,14 @@ func init() {
|
||||
//
|
||||
// This value is the name of your application. This value is used when the
|
||||
// framework needs to place the application's name in a notification or
|
||||
// any other location as required by the application or its packages.
|
||||
// any other location as required by the application or its pkg.
|
||||
"name": "Panel",
|
||||
|
||||
// Application Environment
|
||||
//
|
||||
// This value determines the "environment" your application is currently
|
||||
// running in. This may determine how you prefer to configure various
|
||||
// services the application utilizes. Set this in your ".env" file.
|
||||
// services the application utilizes. Set this in your "panel.conf" file.
|
||||
"env": config.Env("APP_ENV", "production"),
|
||||
|
||||
// Application Debug Mode
|
||||
@@ -50,7 +53,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,7 +80,9 @@ func init() {
|
||||
&auth.ServiceProvider{},
|
||||
&hash.ServiceProvider{},
|
||||
&crypt.ServiceProvider{},
|
||||
&filesystem.ServiceProvider{},
|
||||
&validation.ServiceProvider{},
|
||||
&testing.ServiceProvider{},
|
||||
&providers.AppServiceProvider{},
|
||||
&providers.AuthServiceProvider{},
|
||||
&providers.RouteServiceProvider{},
|
||||
@@ -85,6 +90,8 @@ func init() {
|
||||
&providers.QueueServiceProvider{},
|
||||
&providers.EventServiceProvider{},
|
||||
&providers.ValidationServiceProvider{},
|
||||
&providers.DatabaseServiceProvider{},
|
||||
&gin.ServiceProvider{},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
+2
-1
@@ -14,10 +14,11 @@ func init() {
|
||||
// in web browsers. You are free to adjust these settings as needed.
|
||||
//
|
||||
// To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
"paths": []string{"*"},
|
||||
"allowed_methods": []string{"*"},
|
||||
"allowed_origins": []string{"*"},
|
||||
"allowed_headers": []string{"*"},
|
||||
"exposed_headers": []string{"*"},
|
||||
"exposed_headers": []string{""},
|
||||
"max_age": 0,
|
||||
"supports_credentials": false,
|
||||
})
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ func init() {
|
||||
"connections": map[string]any{
|
||||
"panel": map[string]any{
|
||||
"driver": "sqlite",
|
||||
"database": "database/panel.db",
|
||||
"database": "/www/panel/database/panel.db",
|
||||
"prefix": "",
|
||||
"singular": false, // Table name is singular
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@ func init() {
|
||||
// Here you may specify the default filesystem disk that should be used
|
||||
// by the framework. The "local" disk, as well as a variety of cloud
|
||||
// based disks are available to your application. Just store away!
|
||||
"default": config.Env("FILESYSTEM_DISK", "local"),
|
||||
"default": "local",
|
||||
|
||||
// Filesystem Disks
|
||||
//
|
||||
@@ -24,8 +24,8 @@ func init() {
|
||||
"disks": map[string]any{
|
||||
"local": map[string]any{
|
||||
"driver": "local",
|
||||
"root": "storage/app",
|
||||
"url": "http://localhost/storage",
|
||||
"root": "/",
|
||||
"url": "http://localhost/",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
+16
-4
@@ -1,24 +1,36 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/route"
|
||||
"github.com/goravel/framework/facades"
|
||||
ginfacades "github.com/goravel/gin/facades"
|
||||
)
|
||||
|
||||
func init() {
|
||||
config := facades.Config()
|
||||
config.Add("http", map[string]any{
|
||||
// HTTP Driver
|
||||
"default": "gin",
|
||||
// HTTP Drivers
|
||||
"drivers": map[string]any{
|
||||
"gin": map[string]any{
|
||||
"route": func() (route.Route, error) {
|
||||
return ginfacades.Route("gin"), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
// HTTP URL
|
||||
"url": "http://localhost",
|
||||
// HTTP Host
|
||||
"host": "0.0.0.0",
|
||||
"host": config.Env("APP_HOST", "0.0.0.0"),
|
||||
// HTTP Port
|
||||
"port": "8888",
|
||||
"port": config.Env("APP_PORT", "8888"),
|
||||
// HTTPS Configuration
|
||||
"tls": map[string]any{
|
||||
// HTTPS Host
|
||||
"host": "0.0.0.0",
|
||||
"host": config.Env("APP_HOST", "0.0.0.0"),
|
||||
// HTTPS Port
|
||||
"port": "8899",
|
||||
"port": config.Env("APP_PORT", "8888"),
|
||||
// SSL Certificate
|
||||
"ssl": map[string]any{
|
||||
// ca.pem
|
||||
|
||||
+3
-3
@@ -9,8 +9,8 @@ func init() {
|
||||
config.Add("jwt", map[string]any{
|
||||
// JWT Authentication Secret
|
||||
//
|
||||
// Don't forget to set this in your .env file, as it will be used to sign
|
||||
// your tokens. A helper command is provided for this:
|
||||
// Don't forget to set this in your panel.conf file, as it will be used to sign
|
||||
// your tokens. A tools command is provided for this:
|
||||
// `go run . artisan jwt:secret`
|
||||
"secret": config.Env("JWT_SECRET", ""),
|
||||
|
||||
@@ -32,7 +32,7 @@ func init() {
|
||||
// the original token being created until they must re-authenticate.
|
||||
// Defaults to 2 weeks.
|
||||
//
|
||||
// You can also set this to null, to yield an infinite refresh time.
|
||||
// You can also set this to 0, to yield an infinite refresh time.
|
||||
// Some may want this instead of never expiring tokens for e.g. a mobile app.
|
||||
// This is not particularly recommended, so make sure you have appropriate
|
||||
// systems in place to revoke the token if necessary.
|
||||
|
||||
+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.51",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package seeders
|
||||
|
||||
type DatabaseSeeder struct {
|
||||
}
|
||||
|
||||
// Signature The name and signature of the seeder.
|
||||
func (s *DatabaseSeeder) Signature() string {
|
||||
return "DatabaseSeeder"
|
||||
}
|
||||
|
||||
// Run executes the seeder logic.
|
||||
func (s *DatabaseSeeder) Run() error {
|
||||
return nil
|
||||
}
|
||||
@@ -3,24 +3,29 @@ module panel
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.10.1
|
||||
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.5
|
||||
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.13.1
|
||||
github.com/goravel/gin v1.1.5
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/imroc/req/v3 v3.42.0
|
||||
github.com/mojocn/base64Captcha v1.3.5
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/spf13/cast v1.5.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
golang.org/x/crypto v0.13.0
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.0 // indirect
|
||||
cloud.google.com/go/compute v1.19.1 // indirect
|
||||
cloud.google.com/go v0.110.6 // indirect
|
||||
cloud.google.com/go/compute v1.23.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v0.13.0 // indirect
|
||||
cloud.google.com/go/pubsub v1.30.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.1 // indirect
|
||||
cloud.google.com/go/pubsub v1.33.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.16 // indirect
|
||||
@@ -33,30 +38,31 @@ 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/cenkalti/backoff/v4 v4.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/containerd/continuity v0.3.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.3 // indirect
|
||||
github.com/containerd/continuity v0.4.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/cli v20.10.22+incompatible // indirect
|
||||
github.com/docker/docker v20.10.24+incompatible // indirect
|
||||
github.com/docker/cli v24.0.6+incompatible // indirect
|
||||
github.com/docker/docker v24.0.6+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
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.1 // indirect
|
||||
github.com/glebarez/sqlite v1.8.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/glebarez/sqlite v1.9.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.15.3 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/go-redsync/redsync/v4 v4.0.4 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
@@ -65,8 +71,9 @@ 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-module/carbon/v2 v2.2.6 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
@@ -76,23 +83,23 @@ 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/s2a-go v0.1.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20230907193218-d3ddc7976beb // indirect
|
||||
github.com/google/s2a-go v0.1.4 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/uuid v1.3.1 // 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/googleapis/gax-go/v2 v2.11.0 // 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
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.3.1 // indirect
|
||||
@@ -102,7 +109,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
|
||||
@@ -114,24 +121,25 @@ require (
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.11.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.12.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/opencontainers/runc v1.1.5 // indirect
|
||||
github.com/opencontainers/runc v1.1.9 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/ory/dockertest/v3 v3.10.0 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
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.4 // indirect
|
||||
github.com/quic-go/quic-go v0.38.1 // indirect
|
||||
github.com/refraction-networking/utls v1.5.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
|
||||
github.com/rotisserie/eris v0.5.4 // indirect
|
||||
github.com/rs/cors v1.10.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
@@ -145,6 +153,7 @@ require (
|
||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/unrolled/secure v1.13.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.25.7 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.1 // indirect
|
||||
@@ -159,34 +168,32 @@ 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.10.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b // indirect
|
||||
golang.org/x/mod v0.11.0 // indirect
|
||||
golang.org/x/net v0.11.0 // indirect
|
||||
golang.org/x/oauth2 v0.7.0 // indirect
|
||||
golang.org/x/arch v0.5.0 // indirect
|
||||
golang.org/x/image v0.11.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/oauth2 v0.10.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
golang.org/x/sys v0.9.0 // indirect
|
||||
golang.org/x/text v0.10.0 // indirect
|
||||
golang.org/x/tools v0.10.0 // indirect
|
||||
google.golang.org/api v0.122.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
golang.org/x/tools v0.13.0 // indirect
|
||||
google.golang.org/api v0.126.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/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/grpc v1.56.1 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/grpc v1.58.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/mysql v1.5.1 // indirect
|
||||
gorm.io/driver/postgres v1.5.2 // indirect
|
||||
gorm.io/driver/sqlserver v1.5.1 // indirect
|
||||
gorm.io/gorm v1.25.1 // indirect
|
||||
gorm.io/plugin/dbresolver v1.4.1 // indirect
|
||||
modernc.org/libc v1.22.3 // indirect
|
||||
gorm.io/gorm v1.25.4 // indirect
|
||||
gorm.io/plugin/dbresolver v1.4.7 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/sqlite v1.21.1 // indirect
|
||||
modernc.org/sqlite v1.23.1 // indirect
|
||||
)
|
||||
|
||||
@@ -17,31 +17,30 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb
|
||||
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
|
||||
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
|
||||
cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY=
|
||||
cloud.google.com/go v0.110.6 h1:8uYAkj3YHTP/1iwReuHPxLSbdcyc+dSBbzFMrVwDR6Q=
|
||||
cloud.google.com/go v0.110.6/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
|
||||
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
|
||||
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
|
||||
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k=
|
||||
cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0=
|
||||
cloud.google.com/go/kms v1.10.1 h1:7hm1bRqGCA1GBRQUrp831TwJ9TWhP+tvLuP497CQS2g=
|
||||
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
|
||||
cloud.google.com/go/iam v1.1.1 h1:lW7fzj15aVIXYHREOqjRBV9PsH0Z6u8Y46a1YGvQP4Y=
|
||||
cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU=
|
||||
cloud.google.com/go/kms v1.15.0 h1:xYl5WEaSekKYN5gGRyhjvZKM22GVBBCzegGNVPy+aIs=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/pubsub v1.10.0/go.mod h1:eNpTrkOy7dCpkNyaSNetMa6udbgecJMd0ZsTJS/cuNo=
|
||||
cloud.google.com/go/pubsub v1.30.0 h1:vCge8m7aUKBJYOgrZp7EsNDf6QMd2CAlXZqWTn3yq6s=
|
||||
cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4=
|
||||
cloud.google.com/go/pubsub v1.33.0 h1:6SPCPvWav64tj0sVX/+npCBKhUi/UjJehy9op/V3p2g=
|
||||
cloud.google.com/go/pubsub v1.33.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
@@ -85,24 +84,29 @@ 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.2 h1:lVde18uhad5wII/f5RMVFLtdQNE0HaGFuBUXmYKk8i8=
|
||||
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=
|
||||
github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4=
|
||||
github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
|
||||
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
@@ -110,15 +114,12 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
|
||||
github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg=
|
||||
github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
|
||||
github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -127,14 +128,13 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
|
||||
github.com/dhui/dktest v0.3.16 h1:i6gq2YQEtcrjKbeJpBkWjE8MmLZPYllcjOFbTZuPDnw=
|
||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/docker/cli v20.10.22+incompatible h1:0E7UqWPcn4SlvLImMHyh6xwyNRUGdPxhstpHeh0bFL0=
|
||||
github.com/docker/cli v20.10.22+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY=
|
||||
github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
|
||||
github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE=
|
||||
github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE=
|
||||
github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
@@ -147,7 +147,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw=
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
@@ -155,8 +154,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=
|
||||
@@ -167,10 +166,10 @@ github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTI
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
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.1 h1:7MZyUPh2XTrHS7xNEHQbrhfMZuPSzhkm2A1qgg0y5NY=
|
||||
github.com/glebarez/go-sqlite v1.21.1/go.mod h1:ISs8MF6yk5cL4n/43rSOmVMGJJjHYr7L2MbZZ5Q4E2E=
|
||||
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/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.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
|
||||
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -188,8 +187,8 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
|
||||
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo=
|
||||
github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
|
||||
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4=
|
||||
@@ -235,18 +234,18 @@ github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/V
|
||||
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
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=
|
||||
github.com/golang-module/carbon/v2 v2.2.3/go.mod h1:LdzRApgmDT/wt0eNT8MEJbHfJdSqCtT46uZhfF30dqI=
|
||||
github.com/golang-module/carbon/v2 v2.2.6 h1:4nHkIx7A7d+aoRDAGLwxZEJWAAZpmH5Bm/pVUl58FXI=
|
||||
github.com/golang-module/carbon/v2 v2.2.6/go.mod h1:XDALX7KgqmHk95xyLeaqX9/LJGbfLATyruTziq68SZ8=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
@@ -321,45 +320,48 @@ 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-20230907193218-d3ddc7976beb h1:LCMfzVg3sflxTs4UvuP4D8CkoZnfHLe2qzqgDn/4OHs=
|
||||
github.com/google/pprof v0.0.0-20230907193218-d3ddc7976beb/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
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=
|
||||
github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
|
||||
github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8=
|
||||
github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
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/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4=
|
||||
github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=
|
||||
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.5 h1:TDTUiKX6OX+nS9ZacpKQgN4xDN0npr12PuXvW11R7VQ=
|
||||
github.com/goravel/framework v1.12.5/go.mod h1:TVXmtCGX4XSBx5DtzHxvdP+U0vB3a8UBkKobACdWycg=
|
||||
github.com/goravel/framework v1.13.1 h1:3VM0ZEqbPtHN5llrtB1Mtw9Vr4VK/wPcupzNU7dreSs=
|
||||
github.com/goravel/framework v1.13.1/go.mod h1:CKsMPL8D72wKEZILHUo8Zzi6I9GDW424GpoLAMlB9b8=
|
||||
github.com/goravel/gin v1.1.5 h1:HWVBxSDHsfpq2lWvuagQVMJlCwznf6ifc71IYEo3sCE=
|
||||
github.com/goravel/gin v1.1.5/go.mod h1:FxnhnqcMJk/Sk6jA/xcXn6tHtFzayNXeFuw7GnLORMY=
|
||||
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=
|
||||
@@ -376,14 +378,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/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
|
||||
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||
github.com/imroc/req/v3 v3.42.0 h1:g7wWva3aIJI02mrnqmXe0N5SVkXHQPsYN3Tmw2ZHn3U=
|
||||
github.com/imroc/req/v3 v3.42.0/go.mod h1:W7dOrfQORA9nFoj+CafIZ6P5iyk+rWdbp2sffOAvABU=
|
||||
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=
|
||||
@@ -425,16 +427,16 @@ 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=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
@@ -455,14 +457,12 @@ 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=
|
||||
github.com/microsoft/go-mssqldb v1.1.0/go.mod h1:LzkFdl4z2Ck+Hi+ycGOTbL56VEfgoyA2DvYejrNGbRk=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -477,7 +477,6 @@ github.com/mojocn/base64Captcha v1.3.5/go.mod h1:/tTTXn4WTpX9CfrmipqRytCpJ27Uw3G
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@@ -486,22 +485,20 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108
|
||||
github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
|
||||
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
|
||||
github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI=
|
||||
github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
|
||||
github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc=
|
||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
|
||||
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs=
|
||||
github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg=
|
||||
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
|
||||
github.com/opencontainers/runc v1.1.9 h1:XR0VIHTGce5eWPkaPesqTBrhW2yAcaraWfsEalNwQLM=
|
||||
github.com/opencontainers/runc v1.1.9/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||
@@ -510,8 +507,8 @@ github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnz
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -523,14 +520,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.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.38.1 h1:M36YWA5dEhEeT+slOu/SwMEucbYd0YFidxG3KlGPZaE=
|
||||
github.com/quic-go/quic-go v0.38.1/go.mod h1:ijnZM7JsFIkp4cRyjxJNIzdSfCLmUMg9wdyhGmg+SN4=
|
||||
github.com/refraction-networking/utls v1.5.3 h1:Ds5Ocg1+MC1ahNx5iBEcHe0jHeLaA/fLey61EENm7ro=
|
||||
github.com/refraction-networking/utls v1.5.3/go.mod h1:SPuDbBmgLGp8s+HLNc83FuavwZCFoMmExj+ltUHiHUw=
|
||||
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=
|
||||
@@ -543,18 +538,19 @@ github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=
|
||||
github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk=
|
||||
github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s=
|
||||
github.com/rs/cors v1.10.0 h1:62NOS1h+r8p1mW6FM0FSB0exioXLhd/sh15KpjWBZ+8=
|
||||
github.com/rs/cors v1.10.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
|
||||
@@ -586,14 +582,12 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
|
||||
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ=
|
||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
@@ -608,12 +602,10 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||
@@ -631,7 +623,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=
|
||||
@@ -670,8 +661,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.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
|
||||
golang.org/x/arch v0.5.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=
|
||||
@@ -686,12 +677,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.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
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=
|
||||
@@ -702,12 +692,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-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
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=
|
||||
@@ -732,8 +723,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=
|
||||
@@ -777,8 +768,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.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
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=
|
||||
@@ -789,8 +780,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210113205817-d3ed898aa8a3/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
|
||||
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
|
||||
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
|
||||
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -820,7 +811,6 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -828,8 +818,6 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -865,28 +853,23 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.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.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
|
||||
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
||||
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=
|
||||
@@ -900,8 +883,9 @@ 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.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.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=
|
||||
@@ -965,8 +949,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.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
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=
|
||||
@@ -991,8 +975,8 @@ google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.39.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es=
|
||||
google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
|
||||
google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o=
|
||||
google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -1040,12 +1024,12 @@ 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/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/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g=
|
||||
google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e h1:z3vDksarJxsAKM5dmEGv0GHwE2hKJ096wZra71Vs4sw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -1065,8 +1049,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.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ=
|
||||
google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/grpc v1.58.0 h1:32JY8YpPMSR45K+c3o6b8VL73V+rR8k+DeMIr4vRH8o=
|
||||
google.golang.org/grpc v1.58.0/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
|
||||
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=
|
||||
@@ -1079,9 +1063,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -1104,7 +1087,6 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
|
||||
@@ -1115,11 +1097,12 @@ 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 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
|
||||
gorm.io/gorm v1.25.1/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/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
|
||||
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/plugin/dbresolver v1.4.7 h1:ZwtwmJQxTx9us7o6zEHFvH1q4OeEo1pooU7efmnunJA=
|
||||
gorm.io/plugin/dbresolver v1.4.7/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=
|
||||
@@ -1128,14 +1111,15 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
|
||||
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
||||
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.21.1 h1:GyDFqNnESLOhwwDRaHGdp2jKLDzpyT/rNLglX3ZkMSU=
|
||||
modernc.org/sqlite v1.21.1/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
||||
@@ -32,5 +32,8 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
// 启动计划任务
|
||||
go facades.Schedule().Run()
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=false
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8888
|
||||
|
||||
JWT_SECRET=
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package helpers
|
||||
package tools
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -19,5 +19,5 @@ func IsRHEL() bool {
|
||||
|
||||
// IsArm 判断是否是 ARM 架构
|
||||
func IsArm() bool {
|
||||
return runtime.GOARCH == "arm"
|
||||
return runtime.GOARCH == "arm" || runtime.GOARCH == "arm64"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user