Compare commits

..

189 Commits

Author SHA1 Message Date
耗子 6e6342f0a6 feat: release v2.0.27 2023-08-04 22:45:32 +08:00
耗子 cea485133f fix: website ssl and php 2023-08-04 22:44:58 +08:00
耗子 08e14a84a5 fix: lint 2023-08-04 02:38:44 +08:00
耗子 bc179923a0 feat: optimize readme 2023-08-04 02:31:14 +08:00
耗子 67ef7ccd7a feat: optimize readme 2023-08-04 02:23:54 +08:00
耗子 1abb9691f4 fix(safe): null rule 2023-08-04 00:33:27 +08:00
耗子 4dc2895781 feat: release v2.0.26 2023-08-03 23:39:02 +08:00
耗子 e7635d4b36 fix: lint 2023-08-03 23:34:18 +08:00
耗子 e731c025b5 feat: pure-ftpd plugin 2023-08-03 23:33:31 +08:00
耗子 8234a16f9e feat: release v2.0.25 2023-07-30 03:52:56 +08:00
耗子 0e3dba7dc8 fix: lint 2023-07-30 03:51:46 +08:00
耗子 7e8ef74bee feat: fail2ban plugin 2023-07-30 03:50:55 +08:00
耗子 26ef34ef9b refactor: rename 2023-07-29 02:39:32 +08:00
耗子 4aa6dfa1ee feat(ssh): auto refresh when save 2023-07-29 02:35:21 +08:00
耗子 51989254d6 feat: release v2.0.24 2023-07-28 04:12:34 +08:00
耗子 5cfeb49468 fix(website): open_basedir 2023-07-28 04:12:06 +08:00
耗子 614a5f15b5 fix(cron): backup path 2023-07-28 03:47:43 +08:00
耗子 f5a6f17108 fix(cron): frontend bug 2023-07-27 17:15:41 +08:00
耗子 6356b82a21 feat: release v2.0.23 2023-07-27 17:03:06 +08:00
耗子 b3b1afe77e feat(task): add time 2023-07-27 16:45:07 +08:00
耗子 f9628643be feat(cron): optimize frontend 2023-07-27 16:29:47 +08:00
耗子 c8352d567d fix: php imagemagick install 2023-07-27 16:12:09 +08:00
耗子 2b92a0c693 fix(info): php version check 2023-07-27 16:09:07 +08:00
耗子 8fec8af0d7 fix(ssl): read cert 2023-07-27 16:06:45 +08:00
耗子 5b83e042c2 fix(mysql): path 2023-07-27 15:52:57 +08:00
耗子 525cb154ca fix(website): ssl problem 2023-07-27 15:52:25 +08:00
耗子 1cbd282e40 fix(safe): ping status 2023-07-27 15:14:45 +08:00
耗子 9ffa32160b feat: release v2.0.22 2023-07-27 13:09:08 +08:00
耗子 872da5f8e1 fix(safe): ping status 2023-07-27 13:08:10 +08:00
耗子 eed3651cb1 fix(cron): editor space 2023-07-27 02:31:46 +08:00
耗子 653900fd73 fix(website): root setting and clean log 2023-07-27 02:29:00 +08:00
耗子 ff1f2569dd fix: login 2023-07-27 01:04:29 +08:00
耗子 0c6354fc6e feat: release v2.0.20 2023-07-27 00:57:58 +08:00
耗子 2ed5c36890 feat: add php 81 82 2023-07-27 00:57:13 +08:00
耗子 ecaf8edf14 feat: fix status 2023-07-27 00:47:54 +08:00
耗子 2429cf023a feat: release v2.0.19 2023-07-26 19:06:32 +08:00
耗子 c7adc39887 feat: update framework 2023-07-26 18:13:09 +08:00
耗子 f4e267ef1b feat: optimize openresty install 2023-07-26 15:26:22 +08:00
耗子 98bb24d97a fix: build 2023-07-26 15:22:38 +08:00
耗子 9e603a3003 revert: use v2 version 2023-07-26 15:16:24 +08:00
耗子 7164541415 feat: use v2 version 2023-07-26 15:15:47 +08:00
耗子 a51b391634 feat: use v2 version 2023-07-26 15:15:34 +08:00
耗子 edd3b2a622 fix: openresty install in rhel9 2023-07-26 14:59:42 +08:00
耗子 c55ef5c1f2 feat: release v2.0.18 2023-07-26 02:19:20 +08:00
耗子 65a46796ea feat: supervisor plugin and optimize code 2023-07-26 02:14:26 +08:00
耗子 b4720a1c84 feat: release v2.0.17 2023-07-25 23:42:27 +08:00
耗子 5ec6a88a60 fix: lint 2023-07-25 23:36:33 +08:00
耗子 cc70ab1fde feat: web ssh 2023-07-25 20:33:25 +08:00
耗子 cf58857fd1 workflow: fix issues-helper 2023-07-25 13:31:47 +08:00
耗子 cc5bacedee feat: support restart panel 2023-07-25 13:19:08 +08:00
耗子 fe0fe810ab feat: release v2.0.15 2023-07-25 03:27:18 +08:00
耗子 826b7f65f8 docs: update README 2023-07-25 03:26:50 +08:00
耗子 ce7a380e6e feat(cron): support backup 2023-07-25 03:21:00 +08:00
耗子 a128d295ea feat(task): show failed tasks 2023-07-25 00:24:28 +08:00
耗子 85db98ec10 feat: 移除插件作者字段 2023-07-25 00:20:31 +08:00
耗子 44ae4b9d67 feat: s3fs 插件 2023-07-25 00:16:00 +08:00
耗子 d9b4162e63 feat: 更新版本号 2023-07-24 21:25:05 +08:00
耗子 a15ee96156 refactor: database backup and restore 2023-07-24 19:21:15 +08:00
耗子 4e71e8884a fix(monitor): net count error 2023-07-24 18:56:49 +08:00
耗子 346bb2d2b9 fix(monitor): net count error 2023-07-24 18:55:28 +08:00
耗子 0b476c2764 fix(monitor): net count error 2023-07-24 18:53:50 +08:00
耗子 bd5854cef3 feat: website backup and restore 2023-07-24 18:46:38 +08:00
耗子 5d7ae8d632 fix: file upload 2023-07-24 18:19:13 +08:00
耗子 adbca77687 feat: release v2.0.12 2023-07-23 04:28:44 +08:00
耗子 6377a2111c feat(tools): swap add to 4G 2023-07-23 04:27:50 +08:00
耗子 c9cc9f1fa7 feat(tools): jump /var 2023-07-23 04:04:04 +08:00
耗子 49e32beb8a fix: rhel9 build 2023-07-23 04:02:24 +08:00
耗子 f6f10aca26 feat: release v2.0.11 2023-07-22 18:02:11 +08:00
耗子 cb5d9acf3f feat: add phpmyadmin 2023-07-22 18:01:16 +08:00
耗子 903218dabb feat: auto create php conf for openresty 2023-07-22 17:39:41 +08:00
耗子 71f0e5d7cd feat: auto setting website path user 2023-07-22 17:26:05 +08:00
耗子 a5e1fbc232 feat: update v2.0.10 2023-07-22 16:44:41 +08:00
耗子 1689ef4e4a feat: add task check in update 2023-07-22 03:15:48 +08:00
耗子 fb383f9f90 feat: update v2.0.9 2023-07-22 03:09:16 +08:00
耗子 624fc9c77c feat(cmd): update cleanTask 2023-07-22 03:02:09 +08:00
耗子 793b2ee989 fix: missing file 2023-07-22 02:58:39 +08:00
耗子 e56cf991ae fix: dos2unix 2023-07-22 02:50:45 +08:00
耗子 1daaab1e93 feat: update v2.0.7 2023-07-22 02:47:56 +08:00
耗子 b9f75534ef feat: update v2.0.7 2023-07-22 02:36:08 +08:00
耗子 594dfea986 feat: update v2.0.7 2023-07-22 02:31:07 +08:00
耗子 915403e3c0 feat: update 2023-07-21 15:27:52 +08:00
耗子 0715a4e497 feat: update 2023-07-21 15:27:42 +08:00
耗子 0ab4738e1d feat: update 2023-07-21 04:32:41 +08:00
耗子 0d4f8bbea1 feat: update 2023-07-21 02:26:12 +08:00
耗子 ea59149175 fix: remove file menu 2023-07-21 00:34:42 +08:00
耗子 87a7350f9a fix: some bugs 2023-07-21 00:28:44 +08:00
耗子 59243d5203 fix(cmd): optimize getInfo 2023-07-20 15:39:19 +08:00
耗子 83e2526ce8 fix(plugin controller): script path 2023-07-20 13:39:43 +08:00
耗子 1f5718cd67 fix(cmd): conf path 2023-07-20 01:46:05 +08:00
耗子 3d8f2948be fix(conf): disable debug 2023-07-20 01:45:12 +08:00
耗子 6ef713ab3d fix(install script): oom 2023-07-20 01:30:35 +08:00
耗子 feb377a8f1 fix(install script): run migrate 2023-07-20 01:21:20 +08:00
耗子 bce2f27500 feat(website controller): optimize ssl config 2023-07-20 01:10:27 +08:00
耗子 5ce65653ad refactor: packages to pkg 2023-07-20 01:06:49 +08:00
耗子 ec7138ae2a feat: website controller 2023-07-20 01:03:05 +08:00
耗子 46644c352d feat: optimize code 2023-07-19 01:23:31 +08:00
耗子 2a3119fe9a feat: cron controller 2023-07-19 00:24:41 +08:00
耗子 6119626f50 feat: monitor controller 2023-07-18 14:38:13 +08:00
耗子 c004acbfa0 feat: safe controller 2023-07-18 00:42:37 +08:00
耗子 da58071a0a feat: task controller 2023-07-17 23:18:02 +08:00
耗子 ab599ef0f7 feat: php74 2023-07-16 17:22:49 +08:00
耗子 03f531eff4 feat: fail2ban script 2023-07-15 04:25:39 +08:00
耗子 09d4d15f72 feat: update scripts 2023-07-15 04:09:46 +08:00
耗子 a4cc6570f4 fix: lint 2023-07-11 20:08:47 +08:00
耗子 9556aac923 fix: lint 2023-07-11 20:03:08 +08:00
耗子 8ef737253c fix: lint 2023-07-11 19:55:05 +08:00
耗子 047a743aed fix: lint 2023-07-11 19:52:24 +08:00
耗子 ab78122036 feat: mysql80 plugin 2023-07-11 19:52:08 +08:00
耗子 ce918533ab feat: mysql80 plugin 2023-07-11 19:49:27 +08:00
耗子 e5226c4bf3 fix: tests 2023-07-11 01:46:28 +08:00
耗子 98c4764b95 fix: tests 2023-07-11 01:40:27 +08:00
耗子 8b31415f5c fix: tests 2023-07-11 01:37:18 +08:00
耗子 b606431305 feat: system helpers and website service 2023-07-11 01:26:04 +08:00
耗子 e2c2717852 feat: update framework 2023-07-10 18:25:47 +08:00
耗子 4ff6561942 feat: update framework 2023-07-10 18:05:15 +08:00
耗子 f5529b732f feat: update framework 2023-07-10 17:47:15 +08:00
耗子 397273012c feat: update framework 2023-07-10 17:29:58 +08:00
耗子 91f1125e6a Merge remote-tracking branch 'origin/main'
# Conflicts:
#	go.mod
#	go.sum
2023-07-10 17:10:12 +08:00
耗子 e44a783e9f feat: update framework 2023-07-10 17:09:47 +08:00
dependabot[bot] b33a35286d build(deps): bump the go-modules group with 1 update (#8) 2023-07-10 04:17:21 +00:00
耗子 200d763ba4 refactor: plugin dir 2023-07-10 01:43:16 +08:00
耗子 b9b1735d03 feat: update config 2023-07-09 00:37:20 +08:00
耗子 b88a398343 feat: optimize openresty scripts 2023-07-08 02:39:45 +08:00
耗子 c4199dad4e feat: update
1. panel entrance
2. optimize cli
3. website service init
2023-07-08 02:26:42 +08:00
耗子 9034c76096 feat: remove tests 2023-07-07 20:45:54 +08:00
耗子 bd6cf76d1d fix: tests 2023-07-07 20:26:46 +08:00
耗子 ba3a1eb29e fix: tests 2023-07-07 20:09:31 +08:00
耗子 88061496d8 fix: tests 2023-07-07 19:52:17 +08:00
耗子 f0c08d36dd fix: tests 2023-07-07 19:48:25 +08:00
耗子 7188af738b fix: tests 2023-07-07 19:46:22 +08:00
耗子 3309b79f9c feat: setting service 2023-07-07 19:43:04 +08:00
耗子 f20e942bea fix: lint 2023-07-07 19:24:47 +08:00
耗子 92f84d278e feat: panel update 2023-07-07 19:09:46 +08:00
耗子 9769715295 fix: LICENSE 2023-07-07 18:08:34 +08:00
耗子 4e83f2850a fix: build error 2023-07-07 18:02:41 +08:00
耗子 599899d960 fix: url error 2023-07-07 17:24:55 +08:00
耗子 4c5c89094c feat: update openresty install script 2023-07-07 17:16:48 +08:00
dependabot[bot] 896c6fd144 Merge pull request #7 from HaoZi-Team/dependabot/go_modules/go-modules-f84cb69f2e 2023-07-04 03:06:05 +00:00
耗子 4bb07f1a20 fix: plugin check 2023-07-04 02:25:47 +08:00
dependabot[bot] 2cd1fb832b build(deps): bump the go-modules group with 1 update
Bumps the go-modules group with 1 update: [github.com/goravel/framework](https://github.com/goravel/framework).

- [Release notes](https://github.com/goravel/framework/releases)
- [Commits](https://github.com/goravel/framework/compare/v1.12.4...v1.12.5)

---
updated-dependencies:
- dependency-name: github.com/goravel/framework
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go-modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-03 18:16:27 +00:00
耗子 9ad517863e workflow: fix build 2023-07-04 02:12:31 +08:00
耗子 7cd7aaa8d0 fix(plugin): openresty OS check 2023-07-04 02:10:17 +08:00
耗子 a46fec9016 feat: optimize views 2023-07-04 02:09:35 +08:00
耗子 86d5d22209 refactor: plugin 2023-07-04 01:36:32 +08:00
耗子 a07ac77d2b refactor: models 2023-07-04 01:31:55 +08:00
耗子 9d3417c6f3 workflow: optimize dependabot 2023-06-28 23:59:34 +08:00
耗子 9551213bfb workflow: optimize cr 2023-06-28 12:38:12 +08:00
耗子 f9f27d1d8a Merge remote-tracking branch 'origin/main' 2023-06-28 12:33:26 +08:00
耗子 2120957968 workflow: optimize cr 2023-06-28 12:32:28 +08:00
耗子 04e73859f7 workflow: optimize pr check and cr 2023-06-28 12:31:13 +08:00
dependabot[bot] 208a25899e build(deps): bump github.com/imroc/req/v3 from 3.37.1 to 3.37.2 (#5) 2023-06-28 04:10:59 +00:00
耗子 f972f849ae workflow: optimize pr check 2023-06-28 01:55:24 +08:00
耗子 6c4d95406f workflow: optimize pr check 2023-06-28 01:52:49 +08:00
耗子 4f5c6f3afe workflow: optimize pr check 2023-06-28 01:51:06 +08:00
耗子 545cf2c952 Merge remote-tracking branch 'origin/main' 2023-06-28 01:22:58 +08:00
耗子 f4beb731d8 workflow: optimize pr check 2023-06-28 01:22:16 +08:00
耗子 af6419971b workflow: add pr check 2023-06-28 01:17:29 +08:00
dependabot[bot] d49ddbeb74 Merge pull request #4 from HaoZi-Team/dependabot/go_modules/github.com/goravel/framework-1.12.4 2023-06-27 04:15:58 +00:00
dependabot[bot] 23e96aaf6d build(deps): bump github.com/goravel/framework from 1.12.3 to 1.12.4
Bumps [github.com/goravel/framework](https://github.com/goravel/framework) from 1.12.3 to 1.12.4.
- [Release notes](https://github.com/goravel/framework/releases)
- [Commits](https://github.com/goravel/framework/compare/v1.12.3...v1.12.4)

---
updated-dependencies:
- dependency-name: github.com/goravel/framework
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-06-27 04:12:12 +00:00
耗子 360e709ee9 feat: use 16k model 2023-06-27 12:10:52 +08:00
耗子 2c935023de feat: use 16k model 2023-06-26 02:15:48 +08:00
耗子 4607129933 feat: add code review 2023-06-26 02:14:13 +08:00
耗子 8f512a620e feat: add dependabot 2023-06-26 01:56:52 +08:00
耗子 6fbbd28d23 fix: goreleaser 2023-06-25 01:17:35 +08:00
耗子 175b413432 fix: goreleaser 2023-06-25 01:07:32 +08:00
耗子 e26ce4ce86 fix: goreleaser 2023-06-25 00:59:25 +08:00
耗子 6a09d1373e feat: optimize build workflow 2023-06-25 00:19:59 +08:00
耗子 6bdf850c58 feat: optimize build workflow 2023-06-25 00:11:16 +08:00
耗子 c36640c1d8 feat: optimize build workflow 2023-06-25 00:04:58 +08:00
耗子 f918f40c8d feat: build workflow 2023-06-24 23:53:13 +08:00
耗子 34077272df fix: workflow 2023-06-24 23:06:11 +08:00
耗子 ab6882e5f6 fix: lint 2023-06-24 22:44:48 +08:00
耗子 ff50f3e1de fix: lint 2023-06-24 22:40:04 +08:00
耗子 8b262cf289 feat: openresty plugin 2023-06-24 22:27:41 +08:00
耗子 1c0fb1d298 fix: tests 2023-06-23 04:26:37 +08:00
耗子 98a92da490 feat: add tests 2023-06-23 04:20:57 +08:00
耗子 5e4d02f2cc feat: readme add cards 2023-06-23 03:48:30 +08:00
耗子 3dc8f348c5 feat: home page add disk 2023-06-23 00:37:21 +08:00
耗子 674586b844 feat: home page 2023-06-22 23:00:33 +08:00
耗子 43a44b08b8 fix: actions 2023-06-22 19:56:16 +08:00
耗子 c16a30c434 feat: static use cdn 2023-06-22 19:53:24 +08:00
耗子 16d18d1052 feat: update views 2023-06-22 18:18:51 +08:00
耗子 5b72f4b09e feat: update echarts 2023-06-22 18:07:29 +08:00
耗子 4b2a722308 fix: lint 2023-06-22 17:56:24 +08:00
耗子 2e7d0b9b47 feat: github workflows 2023-06-22 17:49:15 +08:00
耗子 d2afa89d21 fix: lint 2023-06-22 17:01:56 +08:00
耗子 c6e34bdab9 feat: command and user service 2023-06-22 16:51:37 +08:00
耗子 6481c2e65e feat: captcha and login 2023-06-22 00:22:50 +08:00
耗子 c14f4de880 feat: v2 Initial commit 2023-06-22 00:09:56 +08:00
422 changed files with 28156 additions and 24520 deletions
+32
View File
@@ -0,0 +1,32 @@
root = "."
tmp_dir = "storage/temp"
[build]
bin = "./storage/temp/main.exe --env=panel.conf"
cmd = "go build -o ./storage/temp/main.exe ."
delay = 1000
exclude_dir = ["storage", "database"]
exclude_file = []
exclude_regex = []
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false
+143
View File
@@ -0,0 +1,143 @@
name: 🐛 报告问题 (Bug Report)
description: 创建一个报告以帮助我们改进 (Create a report to help us improve)
title: "🐛 [Bug] "
labels: [ "☢️ Bug", "bug" ]
body:
- type: markdown
attributes:
value: |
**请仅使用 简体中文 或 英文 进行填写**
**Please only use Simplified Chinese or English to fill in**
- type: checkboxes
id: checks
attributes:
label: 在提问之前 (Before Asking)
description: |
提问之前,先回答几个小问题。
Before asking questions, answer a few quick questions.
options:
- label: 我已经搜索了现有的 Issues, Discussions 和 Google (I've searched the existing Issues, Discussions and Google)
required: true
- label: 我已经阅读了 README 中除版本记录以外的所有内容 (I've read everything in the README except the version notes)
required: true
- label: 我在提问题之前至少花费了 5 分钟来思考和准备 (I spend at least 5 minutes thinking and preparing before asking a question)
required: true
- label: 我没有改动过面板及系统的任何文件 (I have not changed any files of the panel and system)
required: false
- label: 这个问题可以被稳定复现 (The problem can be stably reproduced)
required: false
- label: 问题是在升级之后产生的 (The problem is generated after upgrading)
required: false
- type: dropdown
id: system
attributes:
label: 操作系统 (Operating System)
description: |
请提供安装面板的操作系统。
Please provide the operating system on which the panel is installed.
options:
- RockyLinux
- AlmaLinux
- Debian
- Other
validations:
required: true
- type: input
id: system-version
attributes:
label: 操作系统版本 (Operating System Version)
description: |
请提供操作系统的版本号。
Please provide the version number of the operating system.
placeholder: "9.0"
validations:
required: true
- type: input
id: version
attributes:
label: 耗子面板版本 (HaoZi Panel Version)
description: |
请提供面板的版本号。
Please provide the version number of the panel.
placeholder: "2.0.0"
validations:
required: true
- type: textarea
id: describe
attributes:
label: 描述问题 (Describe The Problem)
description: |
简明概要地描述你遇到的问题。
Briefly describe the problem you are having.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: 如何复现 (How To Reproduce)
description: |
重现该问题的详细步骤。
Detailed steps to reproduce the problem.
value: |
1. 安装面板
2. 设置 '...'
3. 点击 '...'
4. 出现问题
validations:
required: false
- type: textarea
id: expected
attributes:
label: 预期行为 (Expected Behavior)
description: |
简明概要地描述你期望发生的事情。
Briefly describe what you expect to happen.
validations:
required: true
- type: textarea
id: logs
attributes:
label: 相关日志 (Related Logs)
description: |
请复制并粘贴任何相关的日志输出。
Please copy and paste any relevant log output.
可以把文件拖入这个区域以添加日志文件。
Files can be dragged into this area to add log files.
面板日志文件可在 `/www/panel/storage/logs` 中找到。
Panel log files can be found in `/www/panel/storage/logs`.
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: 截图 (Screenshots)
description: |
如果有,添加屏幕截图可帮助更快定位你的问题。
If so, adding screenshots can help locate your issue faster.
可以复制图片后在此区域内粘贴以添加图片。
Pictures can be copied and pasted in this area to add pictures.
如有必要,使用马赛克遮盖敏感信息。
Use a mosaic to obscure sensitive information if necessary.
validations:
required: false
- type: textarea
id: others
attributes:
label: 还有别的吗 (Anything Else)
description: |
运行环境?浏览器?软件版本?相关的配置?链接?参考资料?
Environment? Browser? Software version? Related configuration? Link? References?
任何能让我们对你所遇到的问题有更多了解的东西。
Anything that can give us more insight into the problem you're having.
validations:
required: false
- type: textarea
id: yellows
attributes:
label: 来点色图 (Porn Pictures)
description: |
如果你不提供色图而你的 Issue 中又有其他不符合标准的地方,那么你的 Issue 可能会被直接关闭。
If you don't provide some porn pictures and there are other non-conformities in your issue, then your issue may be closed directly.
validations:
required: false
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: ❓ 讨论、问答和非项目问题 (Discussions, questions, and non-project issues)
url: https://jq.qq.com/?_wv=1027&k=I1oJKSTH
about: 其他不明之处,请移步我们的QQ群 12370907 (For other unclear things, please move to our qq group 12370907)
@@ -0,0 +1,64 @@
name: ✨ 功能请求 (Feature Request)
description: 为这个项目提出一个想法 (Suggest an idea for this project)
title: "✨ [Feature] "
labels: [ "✏️ Feature", "enhancement" ]
body:
- type: markdown
attributes:
value: |
**请仅使用 简体中文 或 英文 进行填写**
**Please only use Simplified Chinese or English to fill in**
- type: checkboxes
attributes:
label: 在提问之前 (Before Asking)
description: |
提问之前,先回答几个小问题。
Before asking questions, answer a few quick questions.
options:
- label: 我已经搜索了全部 Issues 和 Commits (I have searched all Issues and Commits)
required: true
- label: 它们当中没有我将要提交的新功能 (None of them have new feature that I'm going to submit)
required: true
- type: textarea
id: feature
attributes:
label: 描述功能 (Describe Feature)
description: |
简明概要地描述你的新功能,以及它将解决什么问题。
Briefly describe your new feature and what problem it will solve.
validations:
required: true
- type: textarea
id: workflow
attributes:
label: 工作流程 (WorkFlow)
description: |
请向我们提供有关该功能的实现流程。
Please provide us with the implementation flow for this feature.
value: |
1. 新建文件 ....
2. 添加函数 ....
3. ...
validations:
required: true
- type: textarea
id: others
attributes:
label: 还有别的吗 (Anything Else)
description: |
运行环境?浏览器?软件版本?相关的配置?链接?参考资料?
Environment? Browser? Software version? Related configuration? Link? References?
任何能让我们对该功能的实现有更多帮助的东西。
Anything that can help us more with the implementation of this feature.
validations:
required: false
- type: textarea
id: yellows
attributes:
label: 来点色图 (Porn Pictures)
description: |
如果你不提供色图而你的 Issue 中又有其他不符合标准的地方,那么你的 Issue 可能会被直接关闭。
If you don't provide some porn pictures and there are other non-conformities in your issue, then your issue may be closed directly.
validations:
required: false
+25
View File
@@ -0,0 +1,25 @@
# 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
+32
View File
@@ -0,0 +1,32 @@
name: Build
on:
push:
branches:
- main
pull_request:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
goarch: [ amd64, arm64 ]
fail-fast: true
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
cache: true
go-version: 'stable'
- name: Install dependencies
run: go mod tidy
- name: Build ${{ matrix.goarch }}
env:
CGO_ENABLED: 0
GOOS: linux
GOARCH: ${{ matrix.goarch }}
run: go build -ldflags '-s -w --extldflags "-static -fpic"' -o panel-${{ matrix.goarch }}
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: panel-${{ matrix.goarch }}
path: panel-${{ matrix.goarch }}
+23
View File
@@ -0,0 +1,23 @@
name: Codecov
on:
push:
branches:
- main
pull_request:
jobs:
codecov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 'stable'
- name: Install dependencies
run: go mod tidy
- name: Run tests with coverage
run: go test -v -coverprofile="coverage.out" ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.out
token: ${{ secrets.CODECOV }}
+22
View File
@@ -0,0 +1,22 @@
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: "
+26
View File
@@ -0,0 +1,26 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 'stable'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4
with:
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+44
View File
@@ -0,0 +1,44 @@
name: Issue Auto Reply
on:
issues:
types: [ labeled ]
permissions:
contents: read
jobs:
issue-reply:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: ✏️ Feature
if: github.event.label.name == '✏️ Feature'
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Hi @${{ github.event.issue.user.login }} 👋
我们认为您的建议非常有价值!欢迎提交 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
with:
actions: 'create-comment'
token: ${{ secrets.GITHUB_TOKEN }}
issue-number: ${{ github.event.issue.number }}
body: |
Hi @${{ github.event.issue.user.login }} 👋
我们认为您的反馈非常有价值!欢迎提交 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
@@ -0,0 +1,25 @@
name: Issue Close Question
on:
schedule:
- cron: "0 0 * * *"
permissions:
contents: read
jobs:
issue-close-require:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: needs more info
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issues'
labels: 'question'
inactive-day: 3
body: |
由于该 Issue 3天未收到回应,现已被自动关闭,若有任何问题,可评论回复。
This issue has been closed automatically because it has not had recent activity for 3 days. If you have any questions, please comment here.
+26
View File
@@ -0,0 +1,26 @@
name: Lint
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
lint:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 'stable'
cache: false
- name: Lint
uses: golangci/golangci-lint-action@v3
with:
skip-cache: true
skip-pkg-cache: true
skip-build-cache: true
version: latest
args: --timeout=30m ./...
+11
View File
@@ -0,0 +1,11 @@
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/"
+18
View File
@@ -0,0 +1,18 @@
name: Test
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: 'stable'
- name: Install dependencies
run: go mod tidy
- name: Run tests
run: go test ./...
+72 -2
View File
@@ -1,3 +1,73 @@
.idea
tmp
/.air.toml
/panel.conf
# Golang #
# `go test -c` 生成的二进制文件
*.test
# go coverage 工具
*.out
*.prof
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
# 编译文件 #
*.com
*.class
*.dll
*.exe
*.o
*.so
/panel
# 压缩包 #
# Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit
*.7z
*.dmg
*.gz
*.iso
*.jar
*.rar
*.tar
*.zip
# 日志文件和数据库 #
*.log
*.sqlite
*.db
# 临时文件 #
tmp/
.tmp/
# 系统生成文件 #
.DS_Store
vendor/
.DS_Store?
.AppleDouble
.LSOverride
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
.TemporaryItems
.fseventsd
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# IDE 和编辑器 #
.idea/
/go_build_*
out/
.vscode/
.vscode/settings.json
*.sublime*
__debug_bin
.project
# 前端工具链 #
.sass-cache/*
node_modules/
+52
View File
@@ -0,0 +1,52 @@
image: golang:bookworm
# 在每个任务执行前运行
before_script:
- mkdir -p .go
- go version
- go env -w GO111MODULE=on
- go env -w GOPROXY=https://goproxy.cn,direct
.go_cache:
variables:
GOPATH: $CI_PROJECT_DIR/.go
cache:
paths:
- .go/pkg/mod/
# 全局变量
variables:
OUTPUT_NAME: "panel"
GO111MODULE: "on"
GOPROXY: "https://goproxy.cn,direct"
stages:
- prepare
- build
golangci_lint:
stage: prepare
image: golangci/golangci-lint:latest-alpine
extends: .go_cache
allow_failure: true
script:
- golangci-lint run --timeout 30m
unit_test:
stage: prepare
extends: .go_cache
allow_failure: true
script:
- go test -v -coverprofile=coverage.txt -covermode=atomic ./...
build:
stage: build
extends: .go_cache
script:
- go mod download
- CGO_ENABLED=0 go build -ldflags '-s -w --extldflags "-static -fpic"' -o $OUTPUT_NAME
artifacts:
name: "$OUTPUT_NAME"
paths:
- $OUTPUT_NAME
expire_in: 1 week
+33
View File
@@ -0,0 +1,33 @@
project_name: panel
builds:
-
id: panel
binary: panel
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- amd64
- arm64
goamd64:
- v2
- v3
ldflags:
- -s -w --extldflags "-static -fpic"
archives:
-
id: panel-archive
builds:
- panel
format: zip
wrap_in_directory: false
strip_parent_binary_folder: true
rlcp: true
files:
- LICENSE
- public/*
- storage/*
- database/*
- scripts/*
- panel-example.conf
-98
View File
@@ -1,98 +0,0 @@
# 更新日志
所有重要的更改都将在此文件中记录。
## [20230130] - 常规更新
- 优化数据库备份流程
## [20230130] - 常规更新
- 代码格式化
- 更新许可证
- 修复更新备份逻辑错误
- 主页新增新年祝福
- 主页新增检查更新
## [20230113] - 常规更新
- 优化更新流程
- 优化备份流程
## [20230108] - 常规更新
- 提升面板安全性
- 修复网站删除报错的Bug
- 规范网站名称
## [20221222] - 常规更新
- 优化计划任务日志格式
- 网站管理支持分页
- 文件管理的初步实现
- 样式微调
- 部分代码优化
- 框架版本更新
## [20221212] - 常规更新
- 修复创建网站可能重复添加listen端口的Bug
## [20221210] - 安全更新
- 增强面板安全性
- 其他微调及Bug修复
## [20221208] - 常规更新
- 新增网站免费SSL证书申请
- 前端细节优化
## [20221205] - 常规更新
- 新增自动备份插件(需配合20221203+版本的面板使用)
- 优化面板命令行显示
- 前端细节优化
## [20221203] - 常规更新
- 修复禁用Ping不生效的问题
- 修复网站设置PHP版本未赋值的问题
- 新增网站目录备份功能
- 新增面板多用户登录功能
- 新增资源监控时间选择功能
- 实装网站运行状态设置功能
- 实装网站配置文件重置功能
- 前端多处优化
## [20221202] - 常规更新
- 修复网站添加数据库不能为空的问题
- 校验网站目录必须以/开头
- 修复网站名可以编辑的问题
- 新增面板API开关
- 新增计划任务模块
- 安装脚本优化,提高在不同地区下的安装成功率
## [20221201] - 首版发布
- 修复了二测时发现的一些问题
- 首发于Hostloc,感谢各位mjj大佬的支持。
## [20221130] - 内部二测
- 内部二测
- 修复了一测发现的问题
- 新增了一些插件,基本功能已经完善
## [20221121] - 内部一测
- 内部一测
## 2022-10 - 2022-11
- 重构版本开始开发
## 2022-07 - 2022-08
- 项目启动、早期开发
+1 -1
View File
@@ -187,7 +187,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright 2022 HaoZi Technology Co., Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+118 -21
View File
@@ -1,35 +1,132 @@
耗子Linux面板
===============
# 耗子Linux面板
#### 这是耗子Linux面板的开源仓库,基于Apache License 2.0协议进行开源,目前处于积极开发状态。
[![Release](https://img.shields.io/github/release/haozi-team/panel.svg)](https://github.com/haozi-team/panel/releases)
[![Test](https://github.com/haozi-team/panel/actions/workflows/test.yml/badge.svg)](https://github.com/haozi-team/panel/actions)
[![Report Card](https://goreportcard.com/badge/github.com/haozi-team/panel)](https://goreportcard.com/report/github.com/haozi-team/panel)
[![Codecov](https://codecov.io/gh/haozi-team/panel/branch/main/graph/badge.svg?token=XFT5NGNSRG)](https://codecov.io/gh/haozi-team/panel)
![License](https://img.shields.io/github/license/haozi-team/panel)
#### 我们正在进行基于 Golang 的 V2 版本的开发,加入我们的 交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[耗子](https://pd.qq.com/s/fyol46wfy) 获取最新动态
这是耗子Linux面板,一种使用GO开发的轻量Linux服务器运维管理面板
严禁使用耗子Linux面板从事任何非法活动,非法站点请勿向我们请求任何形式的技术支持,如果在技术支持过程中发现非法内容,我们将立即停止技术支持并留存相关证据。
#### 交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[pd.qq.com/s/fyol46wfy](https://pd.qq.com/s/fyol46wfy)
广告: [`WeAvatar` 多端多元化的统一头像服务](https://weavatar.com)
## 运行环境
| 系统 | 版本 |
|---------------|-----|
| CentOS Stream | 9 |
| CentOS Stream | 8 |
| RockyLinux | 9 |
| RockyLinux | 8 |
| AlmaLinux | 9 |
| AlmaLinux | 8 |
| 其他RHEL发行版 | 8 |
| 其他RHEL发行版 | 9 |
面板仅支持主流 `amd64` | `arm64` 系统的最新版本,且随着系统版本的更新,面板也会逐步停止对旧版本系统的支持。
广告: [`WeAvatar` —统一头像服务](https://weavatar.com)
不支持 `Ubuntu`,因为其发版太过频繁,难以维护。
服务器赞助:
[![](https://img-cdn.haozi.xyz/2022/12/09/54a1b368700423a992789eca4af8b7e2.jpg)](http://www.ddunyun.com/aff/PNYAXMKI)
## 安装说明
低配机器建议使用 `Debian`,资源占用较 `RHEL` 系更低。其他机器建议使用 `RockyLinux` | `AlmaLinux`,维护周期更长。
不在下表中的其他系统(OpenCloudOS、Anolis、CentOS Stream、Debian等),可自行尝试安装,但不保证能够正常运行,且不提供技术支持。
| 系统 | 版本 |
|------------|----|
| RHEL | 9 |
| RockyLinux | 9 |
| AlmaLinux | 9 |
| Debian | 12 |
## 安装面板
安装面板前,你需要了解LNMP环境的基本知识,以及如何处理常见的LNMP环境问题,我们不建议**0基础**的用户安装和使用耗子Linux面板。
如果你决定继续,请以`root`用户登录服务器,执行以下命令安装面板:
```shell
wget -O install_panel.sh https://dl.panel.haozi.xyz/script/install_panel.sh && bash install_panel.sh
bash <(curl -sSL https://dl.cdn.haozi.net/panel/install_panel.sh)
```
## 更新说明
## 日常维护
使用`panel`命令进行日常维护:
```shell
panel update
panel
```
## 问题反馈
使用类问题,可在 [WePublish社区论坛](https://wepublish.cn/forums) 提问或QQ群`@汤姆猫`寻求帮助。
面板自身问题,可在 GitHub 的`Issues`
页面提交问题反馈,注意[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。
## 贡献代码
### 寻找/创建 Issue
您可以在 [Issue 列表](https://github.com/haozi-team/panel/issues) 中寻找或创建一个 Issue,留言表达想要处理该 Issue
的意愿,得到维护者的确认后,即可开始处理。
### 创建 PR
- 如果您对该流程不太了解,可以查看[这篇文章](https://docs.github.com/en/get-started/quickstart/contributing-to-projects)
- 在开发过程中,如果遇到问题可以随时在 Issue 中详尽描述该问题,以进一步沟通,但在此之前请确保自己已通过 Google
等方式尽可能的尝试解决问题;
- 当 PR 开发完毕后,请为其添加 `🚀 Review Ready` 标签,维护者将及时进行评审;
- PR 合并后,如果描述设置正确,Issue 将被自动关闭;
- 我们非常欢迎您的贡献,将在下次发版时将您添加到首页贡献者中;❤️
## 赞助商
### 服务器
- [盾云](https://www.ddunyun.com/aff/PNYAXMKI)
### CDN
- [盾云CDN](http://cdn.ddunyun.com/)
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
- [AnyCast.Ai](https://www.anycast.ai/)
- [迅捷盾](https://console.fastdun.com/)
### DevOps
- [极狐GitLab](https://www.jihulab.com/)
#### 接受云资源和资金赞助,可通过QQ群咨询联系
## 行为准则
耗子Linux面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
- 参与者将容忍反对意见。
- 参与者必须确保他们的语言和行为没有人身攻击和贬低个人言论。
- 在解释他人的言行时,参与者应始终保持良好的意图。
- 不能容忍可合理视为骚扰的行为。
## 安全说明
安全性是我们最关心的问题之一,我们已在生产环境广泛应用耗子Linux面板。
耗子Linux面板采用业界多种方案尽可能保证面板的安全性,但是我们不能保证面板的绝对安全性,**因此我们不对面板的安全性做任何保证
**
如果您在使用面板的过程中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
- 邮箱:i@haozi.net
- QQ826896000
## 贡献者
这个项目的存在要归功于所有做出贡献的人,参与贡献请先查看贡献代码部分。
<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>
-142
View File
@@ -1,142 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Monitor as MonitorModel;
use App\Models\Setting;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
class Monitor extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monitor';
/**
* The console command description.
*
* @var string
*/
protected $description = '耗子Linux面板 - 系统监控';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (Setting::query()->where('name', 'monitor')->value('value')) {
$info = self::getNowMonitor();
MonitorModel::query()->create(['info' => json_encode($info)]);
// 删除过期的记录
$days = Setting::query()->where('name', 'monitor_days')->value('value');
MonitorModel::query()->where('created_at', '<', Carbon::now()->subDays($days))->delete();
$this->info(time().' 监控完成');
} else {
$this->info('监控未开启');
}
return Command::SUCCESS;
}
/**
* 系统资源统计
* @return array
*/
private function getNowMonitor(): array
{
// 第一次获取网络信息
$netInfo1 = getNetInfo();
// 卡它一秒钟
sleep(1);
// 第二次获取网络信息
$netInfo2 = getNetInfo();
// CPU统计信息及负载
$cpuInfoRaw = file_get_contents('/proc/cpuinfo');
$physicalArr = array();
$siblingsSum = 0;
preg_match("/(\d+\.\d+), (\d+\.\d+), (\d+\.\d+)/", exec('uptime'), $uptime);
$uptime1 = $uptime[1] ?? 0;
$processorArr = explode("\nprocessor", $cpuInfoRaw);
foreach ($processorArr as $v) {
preg_match("/physical id\s*:\s(.*)/", $v, $physical);
preg_match("/siblings\s*:\s(.*)/", $v, $siblings);
if (isset($physical[1])) {
if (!in_array($physical[1], $physicalArr)) {
if (isset($siblings[1])) {
$siblingsSum += $siblings[1];
}
}
$physicalArr[] = $physical[1];
}
}
// CPU使用率
$cpuUse = 0.1;
$cpuRaw = explode("\n", shell_exec('ps aux'));
// 弹出第一项和最后一项
array_pop($cpuRaw);
array_shift($cpuRaw);
// 获取当前php进程的pid
$pid = getmypid();
foreach ($cpuRaw as $v) {
$v = preg_replace("/\s+/", " ", $v);
$v = (explode(' ', $v));
// 排除当前进程
if ($v[1] == $pid) {
continue;
}
$cpuUse += isset($v[2]) ? (float) $v[2] : 0;
}
$cpuUse = $siblingsSum > 0 ? ($cpuUse / $siblingsSum) : $cpuUse;
$cpuUse = round($cpuUse, 2);
$cpuUse = min($cpuUse, 100);
// 内存使用率
$memRaw = explode("\n", shell_exec('free -m'));
foreach ($memRaw as $v) {
if (str_contains($v, 'Mem')) {
$memList = preg_replace("/\s+/", " ", $v);
} elseif (str_contains($v, 'Swap')) {
$swapList = preg_replace("/\s+/", " ", $v);
}
}
$memArr = explode(' ', $memList);
$swapArr = explode(' ', $swapList);
// 内存大小MB
$memTotal = $memArr[1];
// Swap大小MB
$swapTotal = $swapArr[1];
// 使用中MB
$memUse = (str_contains($memRaw[0], 'buff/cache')) ? $memArr[2] : ($memArr[2] - $memArr[5] - $memArr[6]);
// Swap使用中MB
$swapUse = $swapArr[2];
// 使用中%
$memUseP = round($memUse / $memTotal, 2) * 100;
// Swap使用中%
$swapUseP = round($swapUse / $swapTotal, 2) * 100;
// 1分钟负载%
$uptime1P = round(min($uptime1 * 10, 100), 2);
// 构建返回数组
$res['cpu_use'] = $cpuUse;
$res['uptime'] = $uptime1;
$res['uptime_p'] = $uptime1P;
$res['mem_total'] = $memTotal;
$res['mem_use'] = $memUse;
$res['mem_use_p'] = $memUseP;
$res['swap_total'] = $swapTotal;
$res['swap_use'] = $swapUse;
$res['swap_use_p'] = $swapUseP;
$res['tx_now'] = $netInfo2['tx'] - $netInfo1['tx'];
$res['rx_now'] = $netInfo2['rx'] - $netInfo1['rx'];
return $res;
}
}
-537
View File
@@ -1,537 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Plugin;
use App\Models\Setting;
use App\Models\Task;
use App\Models\User;
use App\Models\Website;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Symfony\Component\Console\Command\Command as CommandAlias;
class Panel extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'panel {action?} {a1?} {a2?} {a3?} {a4?} {a5?} {a6?} {a7?} {a8?} {a9?} {a10?}';
/**
* The console command description.
*
* @var string
*/
protected $description = '耗子Linux面板命令行工具';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
// 检测是否以root用户运行
if (trim(shell_exec('whoami')) != 'root') {
$this->error('耗子Linux面板:请以root用户运行');
return 1;
}
$action = $this->argument('action');
switch ($action) {
case 'init':
$this->init();
break;
case 'update':
$this->update();
break;
case 'getInfo':
$this->getInfo();
break;
case 'getPort':
$this->getPort();
break;
case 'writePluginInstall':
$this->writePluginInstall();
break;
case 'writePluginUnInstall':
$this->writePluginUnInstall();
break;
case 'writeMysqlPassword':
$this->writeMysqlPassword();
break;
case 'cleanRunningTask':
$this->cleanRunningTask();
break;
case 'backup':
$this->backup();
break;
case 'writeSite':
$this->writeSite();
break;
case 'deleteSite':
$this->deleteSite();
break;
case 'writeSetting':
$this->writeSetting();
break;
case 'deleteSetting':
$this->deleteSetting();
break;
default:
$this->info('耗子Linux面板命令行工具');
$this->info('请使用以下命令:');
$this->info('panel update 更新/修复面板到最新版本');
$this->info('panel getInfo 重新初始化面板账号信息');
$this->info('panel getPort 获取面板访问端口');
$this->info('panel cleanRunningTask 强制清理面板正在运行的任务');
$this->info('panel backup {website/mysql/postgresql} {name} {path} 备份网站/MySQL数据库/PostgreSQL数据库到指定目录');
$this->warn('以下命令请在开发者指导下使用:');
$this->info('panel init 初始化面板');
$this->info('panel writePluginInstall {slug} 写入插件安装状态');
$this->info('panel writePluginUnInstall {slug} 移除插件安装状态');
$this->info('panel writeMysqlPassword {password} 写入MySQL root密码');
$this->info('panel writeSite {name} {status} {path} {php} {ssl} 写入网站数据到面板');
$this->info('panel deleteSite {name} 删除面板网站数据');
$this->info('panel writeSetting {name} {value} 写入/更新面板设置数据');
$this->info('panel deleteSetting {name} 删除面板设置数据');
break;
}
return CommandAlias::SUCCESS;
}
/**
* 初始化
* @return void
*/
private function init(): void
{
Setting::query()->updateOrCreate(['name' => 'name'], ['value' => '耗子Linux面板']);
Setting::query()->updateOrCreate(['name' => 'monitor'], ['value' => '1']);
Setting::query()->updateOrCreate(['name' => 'monitor_days'], ['value' => '30']);
Setting::query()->updateOrCreate(['name' => 'mysql_root_password'], ['value' => '']);
Setting::query()->updateOrCreate(['name' => 'postgresql_root_password'], ['value' => '']);
User::query()->create([
'id' => 1,
'username' => 'admin',
'email' => 'panel@haozi.net',
'password' => Hash::make(Str::random()),
]);
}
/**
* 更新面板
* @return void
*/
private function update(): void
{
/**
* 检查当前是否有任务正在运行
*/
if (Task::query()->where('status', '!=', 'finished')->count()) {
$this->error('当前有任务正在运行,请稍后再试');
$this->info('如需强制更新,请先执行:panel cleanRunningTask');
return;
}
$this->info('正在下载面板...');
shell_exec('wget -O /tmp/panel.zip https://api.panel.haozi.xyz/api/version/latest');
// 检查下载是否成功
if (!file_exists('/tmp/panel.zip') || filesize('/tmp/panel.zip') < 4096) {
$this->error('检测到面板新版本下载失败,已终止更新,请加QQ群:12370907 反馈处理');
return;
}
$this->info('正在备份数据库...');
shell_exec('rm -rf /tmp/database.sqlite');
if (!copy('/www/panel/database/database.sqlite', '/tmp/database.sqlite')) {
$this->error('备份数据库失败,已终止更新,请加QQ群:12370907 反馈处理');
return;
}
$this->info('正在备份插件...');
shell_exec('rm -rf /tmp/plugins');
shell_exec('\cp -rf /www/panel/plugins /tmp/plugins');
if (!is_dir('/tmp/plugins/Openresty')) {
$this->error('检测到面板插件备份失败,已终止更新,请加QQ群:12370907 反馈处理');
return;
}
$this->info('正在删除旧版本...');
shell_exec('rm -rf /www/panel');
shell_exec('mkdir /www/panel');
$this->info('正在解压新版本...');
shell_exec('unzip -o /tmp/panel.zip -d /www/panel');
if (!file_exists('/www/panel/artisan')) {
$this->error('检测到面板新版本解压失败,请加QQ群:12370907 反馈处理');
return;
}
$this->info('正在恢复数据库...');
shell_exec('rm -rf /www/panel/database/database.sqlite');
if (!copy('/tmp/database.sqlite', '/www/panel/database/database.sqlite')) {
$this->error('检测到面板数据库恢复失败,请加QQ群:12370907 反馈处理');
return;
}
$this->info('正在恢复插件...');
shell_exec('rm -rf /www/panel/plugins');
shell_exec('\cp -rf /tmp/plugins /www/panel/plugins');
if (!is_dir('/www/panel/plugins/Openresty')) {
$this->error('检测到面板插件恢复失败,请加QQ群:12370907 反馈处理');
return;
}
$this->info('正在更新面板数据库...');
shell_exec('cd /www/panel && php-panel artisan migrate');
$this->info('正在设置面板权限...');
shell_exec('chown -R root:root /www/panel');
shell_exec('chmod -R 600 /www/panel');
shell_exec('chmod 755 /www/panel');
shell_exec('chmod -R 755 /www/panel/public');
shell_exec('chown -R root:root /www/server/cron');
shell_exec('chmod -R 700 /www/server/cron');
shell_exec('chmod -R 600 /www/server/cron/logs');
shell_exec('chown -R root:root /www/server/vhost');
shell_exec('chmod -R 644 /www/server/vhost');
$this->info('正在重载面板服务...');
$reloadCheck = shell_exec('systemctl reload panel.service 2>&1');
// 检查重启是否成功
if (!empty($reloadCheck)) {
$this->error('检测到面板服务重载失败,请加QQ群:12370907 反馈处理');
} else {
$this->info('正在清理临时文件...');
shell_exec('rm -rf /tmp/panel.zip');
shell_exec('rm -rf /tmp/database.sqlite');
shell_exec('rm -rf /tmp/plugins');
$this->info('面板更新成功');
}
}
/**
* 获取面板信息
* @return void
*/
private function getInfo(): void
{
$user = User::query()->where('id', 1);
// 生成唯一信息
$username = Str::random(6);
$password = Str::random(12);
// 入库
$user->update([
'username' => $username,
'password' => Hash::make($password),
]);
// 从nginx配置文件中获取面板端口
$nginxConf = file_get_contents('/www/server/nginx/conf/nginx.conf');
preg_match('/listen\s+(\d+)/', $nginxConf, $matches);
if (!isset($matches[1])) {
$this->info('获取面板端口失败,请检查nginx主配置文件');
}
$this->info('面板用户名:'.$username);
$this->info('面板密码:'.$password);
$this->info('访问地址:http://IP:'.$matches[1]);
}
/**
* 获取端口
* @return void
*/
private function getPort(): void
{
// 从nginx配置文件中获取面板端口
$nginxConf = file_get_contents('/www/server/nginx/conf/nginx.conf');
preg_match('/listen\s+(\d+)/', $nginxConf, $matches);
if (isset($matches[1])) {
$this->info('面板访问端口为:'.$matches[1]);
} else {
$this->error('获取面板端口失败,请检查nginx主配置文件');
}
}
/**
* 写入插件安装状态
* @return void
*/
private function writePluginInstall(): void
{
$pluginSlug = $this->argument('a1');
// 判空
if (empty($pluginSlug)) {
$this->error('参数错误');
return;
}
// 入库
Plugin::query()->updateOrCreate(
['slug' => $pluginSlug],
['show' => 0]
);
$this->info('成功');
}
/**
* 写入插件卸载状态
* @return void
*/
private function writePluginUnInstall(): void
{
$pluginSlug = $this->argument('a1');
// 判空
if (empty($pluginSlug)) {
$this->error('参数错误');
return;
}
if ($pluginSlug == 'openresty') {
$this->error('耗子Linux面板:请不要花样作死!');
return;
}
// 入库
Plugin::query()->where('slug', $pluginSlug)->delete();
$this->info('成功');
}
/**
* 写入MySQL密码
* @return void
*/
private function writeMysqlPassword(): void
{
$password = $this->argument('a1');
// 判空
if (empty($password)) {
$this->error('参数错误');
return;
}
// 入库
Setting::query()->where('name', 'mysql_root_password')->updateOrCreate([
'name' => 'mysql_root_password',
], [
'value' => $password,
]);
$this->info('成功');
}
/**
* 清理所有运行中和等待中的任务
* @return void
*/
private function cleanRunningTask(): void
{
// 更新任务状态
Task::query()->update(['status' => 'finished']);
// 将所有队列任务清空
shell_exec('php-panel /www/panel/artisan queue:clear');
$this->info('成功');
}
/**
* 备份网站/MySQL数据库/PostgreSQL数据库到指定目录
* @return void
*/
private function backup(): void
{
$type = $this->argument('a1');
$name = $this->argument('a2');
$path = $this->argument('a3');
// 判空
if (empty($type) || empty($name) || empty($path)) {
$this->error('参数错误');
return;
}
// 判断目录是否存在
if (!is_dir($path)) {
$this->error('目录不存在');
return;
}
// 判断目录是否可写
if (!is_writable($path)) {
$this->error('目录不可写');
return;
}
// 判断备份目录是否以/结尾,有则去掉
if (str_ends_with($path, '/')) {
$path = substr($path, 0, -1);
}
// 判断类型
if ($type == 'website') {
// 备份网站
// 从数据库中获取网站目录
$sitePath = Website::query()->where('name', $name)->value('path');
if (empty($sitePath)) {
$this->error('网站不存在');
return;
}
$backupFile = $path.'/'.$name.'_'.date('YmdHis').'.zip';
shell_exec('zip -r '.$backupFile.' '.escapeshellarg($sitePath).' 2>&1');
$this->info('成功');
} elseif ($type == 'mysql') {
// 备份MySQL数据库
$password = Setting::query()->where('name', 'mysql_root_password')->value('value');
$backupFile = $path.'/'.$name.'_'.date('YmdHis').'.sql.zip';
$tempFile = $name.'_'.date('YmdHis').'.sql';
// 判断数据库是否存在
$name = escapeshellarg($name);
$check = shell_exec("mysql -u root -p".$password." -e 'use ".$name."' 2>&1");
if (str_contains($check, 'ERROR')) {
$this->error('数据库不存在');
return;
}
// 傻逼MySQL,瞎jb输出警告
//shell_exec("mysqldump -u root -p".$password." ".$name." 2> /dev/null > /tmp/".$tempFile);
putenv('MYSQL_PWD='.$password);
shell_exec("mysqldump -u root ".$name." > /tmp/".$tempFile);
// zip压缩
shell_exec('cd /tmp && zip -r '.$tempFile.'.zip '.escapeshellarg($tempFile).' 2>&1');
// 移动文件
if (file_exists($backupFile)) {
$this->error('检测到备份已存在,已跳过此次备份');
unlink('/tmp/'.$tempFile);
return;
}
rename('/tmp/'.$tempFile.'.zip', $backupFile);
// 删除临时文件
unlink('/tmp/'.$tempFile);
$this->info('成功');
} elseif ($type == 'postgresql') {
// 备份PostgreSQL数据库
$backupFile = $path.'/'.$name.'_'.date('YmdHis').'.sql.zip';
$tempFile = $name.'_'.date('YmdHis').'.sql';
// 判断数据库是否存在
$check = shell_exec('su - postgres -c "psql -l" 2>&1');
if (!str_contains($check, $name)) {
$this->error('数据库不存在');
return;
}
$name = escapeshellarg($name);
shell_exec('su - postgres -c "pg_dump '.$name.'" > /tmp/'.$tempFile.' 2>&1');
// zip压缩
shell_exec('cd /tmp && zip -r '.$tempFile.'.zip '.escapeshellarg($tempFile).' 2>&1');
// 移动文件
if (file_exists($backupFile)) {
$this->error('检测到备份已存在,已跳过此次备份');
unlink('/tmp/'.$tempFile);
return;
}
rename('/tmp/'.$tempFile.'.zip', $backupFile);
// 删除临时文件
unlink('/tmp/'.$tempFile);
$this->info('成功');
} else {
$this->error('参数错误');
}
}
/**
* 写入网站数据到面板
* @return void
*/
private function writeSite(): void
{
$name = $this->argument('a1');
$status = $this->argument('a2');
$path = $this->argument('a3');
$php = $this->argument('a4');
$ssl = $this->argument('a5');
// 判空
if (empty($name) || empty($status) || empty($path) || empty($php) || empty($ssl)) {
$this->error('参数错误');
return;
}
// 判断网站是否存在
if (Website::query()->where('name', $name)->exists()) {
$this->error('网站已存在');
return;
}
// 判断目录是否存在
if (!is_dir($path)) {
$this->error('目录不存在');
return;
}
// 写入网站
Website::query()->create([
'name' => $name,
'status' => $status,
'path' => $path,
'php' => $php,
'ssl' => $ssl,
]);
}
/**
* 删除面板网站数据
* @return void
*/
private function deleteSite(): void
{
$name = $this->argument('a1');
// 判空
if (empty($name)) {
$this->error('参数错误');
return;
}
// 判断网站是否存在
if (!Website::query()->where('name', $name)->exists()) {
$this->error('网站不存在');
return;
}
// 删除网站
Website::query()->where('name', $name)->delete();
}
/**
* 写入/更新面板设置数据
* @return void
*/
private function writeSetting(): void
{
$name = $this->argument('a1');
$value = $this->argument('a2');
// 判空
if (empty($name) || empty($value)) {
$this->error('参数错误');
return;
}
Setting::query()->updateOrCreate(['name' => $name], ['value' => $value]);
}
/**
* 删除面板设置数据
* @return void
*/
private function deleteSetting(): void
{
$name = $this->argument('a1');
// 判空
if (empty($name)) {
$this->error('参数错误');
return;
}
// 判断设置是否存在
if (!Setting::query()->where('name', $name)->exists()) {
$this->error('设置不存在');
return;
}
// 删除设置
Setting::query()->where('name', $name)->delete();
}
}
-59
View File
@@ -1,59 +0,0 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Models\Cron;
use Illuminate\Support\Carbon;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('monitor')->everyMinute();
// 查询所有计划任务
$crons = Cron::all();
foreach ($crons as $cron) {
$file = '/www/server/cron/'.$cron->shell;
// 检查文件是否存在,及所有者是否为root
if (!file_exists($file) || fileowner($file) != 0) {
file_put_contents('/www/server/cron/logs/'.$cron->id.'.log',
PHP_EOL.'耗子Linux面板:检测到脚本文件异常,为确保安全已终止运行,如果你不知道发生了什么,这通常意味着服务器已被入侵。'.PHP_EOL,
FILE_APPEND);
continue;
}
$schedule->exec('bash '.escapeshellarg($file))->withoutOverlapping()->cron($cron->time)->appendOutputTo('/www/server/cron/logs/'.$cron->id.'.log')->when(function (
) use ($cron) {
return (boolean) $cron->status;
})->after(function () use ($cron) {
$cron->updated_at = now();
$cron->save();
})->onSuccess(function () use ($cron) {
file_put_contents('/www/server/cron/logs/'.$cron->id.'.log',
PHP_EOL.Carbon::now()->toDateTimeString().' 任务执行成功'.PHP_EOL, FILE_APPEND);
})->onFailure(function () use ($cron) {
file_put_contents('/www/server/cron/logs/'.$cron->id.'.log',
PHP_EOL.Carbon::now()->toDateTimeString().' 任务执行失败'.PHP_EOL, FILE_APPEND);
});
}
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}
-50
View File
@@ -1,50 +0,0 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
];
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
{
$this->reportable(function (Throwable $e) {
//
});
}
}
@@ -1,212 +0,0 @@
<?php
/**
* 耗子Linux面板 - 计划任务控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Cron;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Validation\ValidationException;
class CronsController extends Controller
{
/**
* 面板计划任务列表
*/
public function getList(Request $request): JsonResponse
{
$limit = $request->input('limit', 10);
$crons = Cron::query()->orderBy('id', 'desc')->paginate($limit);
$cronData = [];
foreach ($crons as $k => $v) {
// 格式化时间
$cronData[$k]['id'] = $v['id'];
$cronData[$k]['name'] = $v['name'];
$cronData[$k]['status'] = $v['status'];
$cronData[$k]['type'] = $v['type'];
$cronData[$k]['time'] = $v['time'];
$cronData[$k]['shell'] = $v['shell'];
$cronData[$k]['script'] = @file_get_contents('/www/server/cron/'.$v['shell']);
$cronData[$k]['created_at'] = Carbon::create($v['created_at'])->toDateTimeString();
$cronData[$k]['updated_at'] = Carbon::create($v['updated_at'])->toDateTimeString();
}
$data['code'] = 0;
$data['msg'] = 'success';
$data['count'] = $crons->total();
$data['data'] = $cronData;
return response()->json($data);
}
/**
* 添加计划任务
*/
public function add(Request $request): JsonResponse
{
// 消毒
try {
$credentials = $this->validate($request, [
'name' => 'required|max:255',
'time' => [
'required',
'regex:/^((\*|\d+|\d+-\d+|\d+\/\d+|\d+-\d+\/\d+|\*\/\d+)(\,(\*|\d+|\d+-\d+|\d+\/\d+|\d+-\d+\/\d+|\*\/\d+))*\s?){5}$/'
],
'script' => 'required',
]);
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
// 将script写入shell文件
$shellDir = '/www/server/cron/';
$shellLogDir = '/www/server/cron/logs/';
if (!is_dir($shellDir)) {
mkdir($shellDir, 700, true);
}
if (!is_dir($shellLogDir)) {
mkdir($shellLogDir, 600, true);
}
$shellFile = uniqid().'.sh';
file_put_contents($shellDir.$shellFile, $credentials['script']);
$cron = new Cron();
$cron->name = $credentials['name'];
$cron->status = 1;
$cron->type = '脚本';
$cron->time = $credentials['time'];
$cron->shell = $shellFile;
$cron->save();
$data['code'] = 0;
$data['msg'] = 'success';
return response()->json($data);
}
/**
* 修改计划任务
*/
public function edit(Request $request): JsonResponse
{
// 消毒
try {
$credentials = $this->validate($request, [
'id' => 'required|integer',
'name' => 'required|max:255',
'time' => ['required', 'regex:/^((\*|\d+|\d+-\d+|\d+\/\d+)(\,(\*|\d+|\d+-\d+|\d+\/\d+))*\s?){5}$/'],
'script' => 'required',
]);
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
$cron = Cron::query()->find($credentials['id']);
$cron->name = $credentials['name'];
$cron->time = $credentials['time'];
// 将script写入shell文件
$shellDir = '/www/server/cron/';
$shellLogDir = '/www/server/cron/logs/';
if (!is_dir($shellDir)) {
mkdir($shellDir, 700, true);
}
if (!is_dir($shellLogDir)) {
mkdir($shellLogDir, 600, true);
}
$shellFile = $cron->shell;
file_put_contents($shellDir.$shellFile, $credentials['script']);
// 将文件转为unix格式
exec('dos2unix '.$shellDir.$shellFile);
// 设置文件权限
exec('chmod 700 '.$shellDir.$shellFile);
$cron->save();
$data['code'] = 0;
$data['msg'] = 'success';
return response()->json($data);
}
/**
* 删除计划任务
*/
public function delete(Request $request): JsonResponse
{
// 消毒
try {
$credentials = $this->validate($request, [
'id' => 'required|integer',
]);
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
$cron = Cron::query()->find($credentials['id']);
// 删除shell文件
$shellDir = '/www/server/cron/';
$shellFile = $cron->shell;
@unlink($shellDir.$shellFile);
// 删除日志文件
$shellLogDir = '/www/server/cron/logs/';
$shellLogFile = $shellFile.'.log';
@unlink($shellLogDir.$shellLogFile);
$cron->delete();
$data['code'] = 0;
$data['msg'] = 'success';
return response()->json($data);
}
/**
* 修改计划任务状态
*/
public function setStatus(Request $request): JsonResponse
{
// 消毒
try {
$credentials = $this->validate($request, [
'id' => 'required|integer',
'status' => 'required|integer',
]);
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
$cron = Cron::query()->find($credentials['id']);
$cron->status = $credentials['status'];
$cron->save();
$data['code'] = 0;
$data['msg'] = 'success';
return response()->json($data);
}
/**
* 获取计划任务日志
*/
public function getLog(Request $request): JsonResponse
{
// 消毒
try {
$credentials = $this->validate($request, [
'id' => 'required|integer',
]);
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
$log = @file_get_contents('/www/server/cron/logs/'.$credentials['id'].'.log');
if ($log === false) {
$log = '暂无日志';
}
$data['code'] = 0;
$data['msg'] = 'success';
$data['data'] = htmlspecialchars($log);
return response()->json($data);
}
}
@@ -1,504 +0,0 @@
<?php
/**
* 耗子Linux面板 - 文件控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class FilesController extends Controller
{
/**
* 获取某个目录下的文件(夹)列表
* @param Request $request
* @return JsonResponse
*/
public function getList(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
'limit' => 'required|integer',
'page' => 'required|integer',
]);
$path = $credentials['path'];
$limit = $credentials['limit'];
$page = $credentials['page'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_dir($path)) {
return response()->json(['code' => 1, 'msg' => '目录不存在']);
}
$files = scandir($path);
$fileData = [];
foreach ($files as $k => $v) {
if ($v == '.' || $v == '..') {
continue;
}
$fileData[$k]['name'] = $v;
$fileData[$k]['path'] = $path.'/'.$v;
$fileData[$k]['type'] = filetype($path.'/'.$v);
$fileData[$k]['size'] = filesize($path.'/'.$v);
$fileData[$k]['mtime'] = Carbon::createFromTimestamp(filemtime($path.'/'.$v))->toDateTimeString();
}
// 分页
$total = count($fileData);
$filesArr = array_slice($fileData, ($page - 1) * $limit, $limit);
$data['code'] = 0;
$data['msg'] = 'success';
$data['count'] = $total;
$data['data'] = $filesArr;
return response()->json($data);
}
/**
* 获取文件内容
* @param Request $request
* @return JsonResponse
*/
public function getFileContent(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'file' => ['required', 'regex:/^\/.*$/'],
]);
$file = $credentials['file'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_file($file)) {
return response()->json(['code' => 1, 'msg' => '文件不存在']);
}
$content = @file_get_contents($file);
$data['code'] = 0;
$data['msg'] = 'success';
$data['data'] = $content;
return response()->json($data);
}
/**
* 保存文件内容
* @param Request $request
* @return JsonResponse
*/
public function saveFileContent(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'file' => ['required', 'regex:/^\/.*$/'],
'content' => 'required',
]);
$file = $credentials['file'];
$content = $credentials['content'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_file($file)) {
return response()->json(['code' => 1, 'msg' => '文件不存在']);
}
$res = @file_put_contents($file, $content);
if ($res === false) {
return response()->json(['code' => 1, 'msg' => '保存失败']);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
/**
* 创建目录
* @param Request $request
* @return JsonResponse
*/
public function createDir(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
'name' => 'required',
]);
$path = $credentials['path'];
$name = $credentials['name'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_dir($path)) {
return response()->json(['code' => 1, 'msg' => '目录不存在']);
}
$res = @mkdir($path.'/'.$name);
if ($res === false) {
return response()->json(['code' => 1, 'msg' => '创建失败']);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
/**
* 重命名文件或目录
* @param Request $request
* @return JsonResponse
*/
public function rename(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
'name' => 'required',
]);
$path = $credentials['path'];
$name = $credentials['name'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_file($path) && !is_dir($path)) {
return response()->json(['code' => 1, 'msg' => '文件或目录不存在']);
}
$res = @rename($path, dirname($path).'/'.$name);
if ($res === false) {
return response()->json(['code' => 1, 'msg' => '重命名失败']);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
/**
* 上传文件
* @param Request $request
* @return JsonResponse
*/
public function uploadFile(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
'file' => 'required|file',
]);
$path = $credentials['path'];
$file = $credentials['file'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_dir($path)) {
return response()->json(['code' => 1, 'msg' => '目录不存在']);
}
$res = @move_uploaded_file($file->getRealPath(), $path.'/'.$file->getClientOriginalName());
if ($res === false) {
return response()->json(['code' => 1, 'msg' => '上传失败']);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
/**
* 下载文件
* @param Request $request
* @return JsonResponse|BinaryFileResponse
*/
public function downloadFile(Request $request): BinaryFileResponse|JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
]);
$path = $credentials['path'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_file($path)) {
return response()->json(['code' => 1, 'msg' => '文件不存在']);
}
return response()->download($path);
}
/**
* 创建文件
*/
public function createFile(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
'name' => 'required',
]);
$path = $credentials['path'];
$name = $credentials['name'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_dir($path)) {
return response()->json(['code' => 1, 'msg' => '目录不存在']);
}
$res = @file_put_contents($path.'/'.$name, '');
if ($res === false) {
return response()->json(['code' => 1, 'msg' => '创建失败']);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
/**
* 设置权限
* @param Request $request
* @return JsonResponse
*/
public function chmod(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
'mode' => 'required',
]);
$path = $credentials['path'];
$mode = $credentials['mode'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_file($path) && !is_dir($path)) {
return response()->json(['code' => 1, 'msg' => '文件或目录不存在']);
}
$res = @chmod($path, $mode);
if ($res === false) {
return response()->json(['code' => 1, 'msg' => '设置失败']);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
/**
* 设置所有者
*/
public function chown(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
'user' => 'required',
]);
$path = $credentials['path'];
$user = $credentials['user'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_file($path) && !is_dir($path)) {
return response()->json(['code' => 1, 'msg' => '文件或目录不存在']);
}
$res = @chown($path, $user);
if ($res === false) {
return response()->json(['code' => 1, 'msg' => '设置失败']);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
/**
* 设置所有者组
* @param Request $request
* @return JsonResponse
*/
public function chgrp(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
'group' => 'required',
]);
$path = $credentials['path'];
$group = $credentials['group'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_file($path) && !is_dir($path)) {
return response()->json(['code' => 1, 'msg' => '文件或目录不存在']);
}
$res = @chgrp($path, $group);
if ($res === false) {
return response()->json(['code' => 1, 'msg' => '设置失败']);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
/**
* 复制文件/目录
* @param Request $request
* @return JsonResponse
*/
public function copy(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
'dest' => ['required', 'regex:/^\/.*$/'],
]);
$path = $credentials['path'];
$dest = $credentials['dest'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_file($path) && !is_dir($path)) {
return response()->json(['code' => 1, 'msg' => '文件或目录不存在']);
}
if (!is_dir($dest)) {
return response()->json(['code' => 1, 'msg' => '目标目录不存在']);
}
$res = @copy($path, $dest.'/'.basename($path));
if ($res === false) {
return response()->json(['code' => 1, 'msg' => '复制失败']);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
/**
* 移动文件/目录
* @param Request $request
* @return JsonResponse
*/
public function move(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
'dest' => ['required', 'regex:/^\/.*$/'],
]);
$path = $credentials['path'];
$dest = $credentials['dest'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_file($path) && !is_dir($path)) {
return response()->json(['code' => 1, 'msg' => '文件或目录不存在']);
}
if (!is_dir($dest)) {
return response()->json(['code' => 1, 'msg' => '目标目录不存在']);
}
$res = @rename($path, $dest.'/'.basename($path));
if ($res === false) {
return response()->json(['code' => 1, 'msg' => '移动失败']);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
/**
* 删除文件/目录
* @param Request $request
* @return JsonResponse
*/
public function delete(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
]);
$path = $credentials['path'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_file($path) && !is_dir($path)) {
return response()->json(['code' => 1, 'msg' => '文件或目录不存在']);
}
$res = @shell_exec('rm -rf '.escapeshellarg($path).' 2>&1');
if (!empty($res)) {
return response()->json(['code' => 1, 'msg' => '删除失败:'.$res]);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
/**
* 压缩文件/目录
* @param Request $request
* @return JsonResponse
*/
public function zip(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
]);
$path = $credentials['path'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_file($path) && !is_dir($path)) {
return response()->json(['code' => 1, 'msg' => '文件或目录不存在']);
}
$zipPath = dirname($path).'/'.basename($path).'.zip';
$res = @shell_exec('zip -r '.escapeshellarg($zipPath).' '.escapeshellarg($path).' 2>&1');
if (!empty($res)) {
return response()->json(['code' => 1, 'msg' => '压缩失败:'.$res]);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
/**
* 解压文件/目录
* @param Request $request
* @return JsonResponse
*/
public function unzip(Request $request): JsonResponse
{
try {
$credentials = $this->validate($request, [
'path' => ['required', 'regex:/^\/.*$/'],
]);
$path = $credentials['path'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
if (!is_file($path)) {
return response()->json(['code' => 1, 'msg' => '文件不存在']);
}
$res = @shell_exec('unzip -o '.escapeshellarg($path).' -d '.escapeshellarg(dirname($path)).' 2>&1');
if (!empty($res)) {
return response()->json(['code' => 1, 'msg' => '解压失败:'.$res]);
}
return response()->json(['code' => 0, 'msg' => 'success']);
}
}
@@ -1,326 +0,0 @@
<?php
/**
* 耗子Linux面板 - 信息控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Plugin;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class InfosController extends Controller
{
public function getMenu(): JsonResponse
{
$menu = array(
"code" => 0,
"msg" => "",
"data" => array(
array(
"name" => "home",
"title" => "主页",
"icon" => "layui-icon-home",
"jump" => "/"
),
array(
"name" => "website",
"title" => "网站管理",
"icon" => "layui-icon-website",
"jump" => "website/list"
),
array(
"name" => "monitor",
"title" => "资源监控",
"icon" => "layui-icon-chart-screen",
"jump" => "monitor"
),
array(
"name" => "safe",
"title" => "系统安全",
"icon" => "layui-icon-auz",
"jump" => "safe"
),
array(
"name" => "file",
"title" => "文件管理",
"icon" => "layui-icon-file",
"jump" => "file"
),
array(
"name" => "cron",
"title" => "计划任务",
"icon" => "layui-icon-date",
"jump" => "cron"
),
array(
"name" => "plugin",
"title" => "插件中心",
"icon" => "layui-icon-app",
"jump" => "plugin"
),
array(
"name" => "setting",
"title" => "面板设置",
"icon" => "layui-icon-set",
"jump" => "setting"
)
)
);
return response()->json($menu);
}
/**
* 系统资源统计
* @return JsonResponse
*/
public function getNowMonitor(): JsonResponse
{
// 第一次获取网络信息
$netInfo1 = getNetInfo();
// 卡它一秒钟
sleep(1);
// 第二次获取网络信息
$netInfo2 = getNetInfo();
// CPU统计信息及负载
$cpuInfoRaw = file_get_contents('/proc/cpuinfo');
$physicalArr = array();
$physicalSum = 0;
$coresSum = 0;
$siblingsSum = 0;
preg_match("/model name\s*:\s(.*)/", $cpuInfoRaw, $cpuName);
preg_match("/vendor_id\s*:\s(.*)/", $cpuInfoRaw, $cpuVendor);
preg_match("/cpu family\s*:\s(.*)/", $cpuInfoRaw, $cpuFamily);
preg_match("/cpu MHz\s*:\s(.*)/", $cpuInfoRaw, $cpuFreq);
preg_match("/cache size\s*:\s(.*)/", $cpuInfoRaw, $cpuCache);
preg_match("/(\d+\.\d+), (\d+\.\d+), (\d+\.\d+)/", exec('uptime'), $uptime);
$cpuName = $cpuName[1] ?? 'No';
$cpuVendor = $cpuVendor[1] ?? 'No';
$cpuFamily = $cpuFamily[1] ?? 'No';
$cpuFreq = isset($cpuFreq[1]) ? round($cpuFreq[1], 2) : 'No';
$cpuCache = $cpuCache[1] ?? 'No';
$uptime1 = $uptime[1] ?? 0;
$uptime5 = $uptime[2] ?? 0;
$uptime15 = $uptime[3] ?? 0;
$processorArr = explode("\nprocessor", $cpuInfoRaw);
foreach ($processorArr as $v) {
preg_match("/physical id\s*:\s(.*)/", $v, $physical);
preg_match("/cpu cores\s*:\s(.*)/", $v, $cores);
preg_match("/siblings\s*:\s(.*)/", $v, $siblings);
if (isset($physical[1])) {
if (!in_array($physical[1], $physicalArr)) {
$physicalSum += 1;
if (isset($cores[1])) {
$coresSum += $cores[1];
}
if (isset($siblings[1])) {
$siblingsSum += $siblings[1];
}
}
$physicalArr[] = $physical[1];
}
}
// CPU使用率
$cpuUse = 0.1;
$cpuRaw = explode("\n", shell_exec('ps aux'));
// 弹出第一项和最后一项
array_pop($cpuRaw);
array_shift($cpuRaw);
// 获取当前php进程的pid
$pid = getmypid();
foreach ($cpuRaw as $v) {
$v = preg_replace("/\s+/", " ", $v);
$v = (explode(' ', $v));
// 排除当前进程
if ($v[1] == $pid) {
continue;
}
$cpuUse += isset($v[2]) ? (float) $v[2] : 0;
}
$cpuUse = $siblingsSum > 0 ? ($cpuUse / $siblingsSum) : $cpuUse;
$cpuUse = round($cpuUse, 2);
$cpuUse = $cpuUse > 100 ? 100 .'%' : $cpuUse.'%';
// 内存使用率
$memRaw = explode("\n", shell_exec('free -m'));
foreach ($memRaw as $v) {
if (str_contains($v, 'Mem')) {
$memList = preg_replace("/\s+/", " ", $v);
}
}
$memArr = explode(' ', $memList);
// 内存大小MB
$memTotal = $memArr[1];
// 使用中MB
$memUse = (str_contains($memRaw[0], 'buff/cache')) ? $memArr[2] : ($memArr[2] - $memArr[5] - $memArr[6]);
// 使用中%
$memUseP = round($memUse / $memTotal, 2) * 100 .'%';
// 1分钟负载%
$uptime1P = $uptime1 * 10;
$uptime1P = $uptime1P > 100 ? 100 .'%' : $uptime1P.'%';
// 5分钟负载%
$uptime5P = $uptime5 * 10;
$uptime5P = $uptime5P > 100 ? 100 .'%' : $uptime5P.'%';
// 15分钟负载%
$uptime15P = $uptime15 * 10;
$uptime15P = $uptime15P > 100 ? 100 .'%' : $uptime15P.'%';
// 构建返回数组
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = [
'cpu_use' => $cpuUse,
'uptime_1' => $uptime1,
'uptime_1_p' => $uptime1P,
'uptime_5' => $uptime5,
'uptime_5_p' => $uptime5P,
'uptime_15' => $uptime15,
'uptime_15_p' => $uptime15P,
'mem_total' => $memTotal,
'mem_use' => $memUse,
'mem_use_p' => $memUseP,
'tx_total' => formatBytes($netInfo1['tx']),
'rx_total' => formatBytes($netInfo1['rx']),
'tx_now' => formatBytes($netInfo2['tx'] - $netInfo1['tx']),
'rx_now' => formatBytes($netInfo2['rx'] - $netInfo1['rx']),
'cpu_info' => [
'name' => $cpuName,
'cores' => $coresSum,
'physical' => $physicalSum,
'siblings' => $siblingsSum,
'vendor' => $cpuVendor,
'family' => $cpuFamily,
'freq' => $cpuFreq,
'cache' => $cpuCache,
],
];
return response()->json($res);
}
/**
* 获取系统信息
* @return JsonResponse
*/
public function getSystemInfo(): JsonResponse
{
$os_name = file_get_contents('/etc/redhat-release');
$uptime = file_get_contents('/proc/uptime');
$uptime = explode(' ', $uptime)[0];
$uptime = round($uptime / 86400, 1);
$panel_version = config('panel.version');
// 构建返回数组
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = [
'os_name' => $os_name,
'uptime' => $uptime,
'panel_version' => $panel_version,
];
return response()->json($res);
}
/**
* 获取首页插件列表
*/
public function getHomePlugins(): JsonResponse
{
$plugins = Plugin::query()->where('show', 1)->get();
// 判空
if ($plugins->isEmpty()) {
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = [];
} else {
$plugins = $plugins->toArray();
$plugins = array_map(function ($item) {
$item['name'] = PLUGINS[$item['slug']]['name'];
return $item;
}, $plugins);
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = $plugins;
}
return response()->json($res);
}
/**
* 获取已安装的数据库和PHP版本
*/
public function getInstalledDbAndPhp(): JsonResponse
{
$dbVersions = [];
// 判断mysql插件是否安装
if (isset(PLUGINS['mysql'])) {
$dbVersions['mysql'] = PLUGINS['mysql']['version'];
} else {
$dbVersions['mysql'] = false;
}
// 判断postgresql插件是否安装
if (isset(PLUGINS['postgresql'])) {
$dbVersions['postgresql'] = PLUGINS['postgresql']['version'];
} else {
$dbVersions['postgresql'] = false;
}
// 循环获取已安装的PHP版本
$php_versions = Plugin::query()->where('slug', 'like', 'php%')->get();
$php_versions = $php_versions->toArray();
$php_versions = array_column($php_versions, 'slug');
$php_versions = array_map(function ($item) {
return str_replace('php', '', $item);
}, $php_versions);
$php_version = shell_exec('ls /www/server/php');
$php_version = trim($php_version);
if (!empty($php_version)) {
$php_versions = explode("\n", $php_version);
}
$php_versions[] = '00';
unset($php_versions[array_search('panel', $php_versions)]);
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = array(
'db_version' => $dbVersions,
'php_version' => $php_versions
);
return response()->json($res);
}
/**
* 检查更新
*/
public function checkUpdate(): JsonResponse
{
$version = config('panel.version');
$remoteVersion = Http::get('https://api.panel.haozi.xyz/api/version/info')->json();
if ($remoteVersion['code'] != 0) {
$res['code'] = 1;
$res['msg'] = '获取远程版本信息失败';
$res['data'] = [];
return response()->json($res);
}
$res['code'] = 0;
$res['msg'] = 'success';
if (version_compare($version, $remoteVersion['data']['version'], '<')) {
$res['data'] = [
'version' => $remoteVersion['data']['version'],
'describe' => $remoteVersion['data']['describe'],
];
} else {
$res['data'] = [];
}
return response()->json($res);
}
}
@@ -1,109 +0,0 @@
<?php
/**
* 耗子Linux面板 - 监控控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Monitor;
use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
class MonitorsController extends Controller
{
/**
* 修改监控开关
*/
public function setMonitorSwitch(Request $request): JsonResponse
{
$switch = $request->input('switch');
if ($switch) {
$status = true;
} else {
$status = false;
}
Setting::query()->where('name', 'monitor')->update(['value' => $status]);
return response()->json(['code' => 0, 'msg' => '修改成功']);
}
/**
* 修改保存天数
*/
public function setMonitorSaveDays(Request $request): JsonResponse
{
$days = $request->input('days');
Setting::query()->where('name', 'monitor_days')->update(['value' => $days]);
return response()->json(['code' => 0, 'msg' => '修改成功']);
}
/**
* 清空监控数据
*/
public function clearMonitorData(): JsonResponse
{
Monitor::query()->truncate();
return response()->json(['code' => 0, 'msg' => '清空成功']);
}
/**
* 获取监控开关和保存天数
*/
public function getMonitorSwitchAndDays(): JsonResponse
{
$monitor = Setting::query()->where('name', 'monitor')->first();
$monitor_days = Setting::query()->where('name', 'monitor_days')->first();
return response()->json([
'code' => 0, 'msg' => '获取成功',
'data' => ['monitor' => $monitor->value, 'monitor_days' => $monitor_days->value]
]);
}
/**
* 获取监控数据
*/
public function getMonitorData(Request $request): JsonResponse
{
$start = $request->input('start') ?? now();
$end = $request->input('end') ?? now();
$start = Carbon::create($start);
$end = Carbon::create($end);
$data = Monitor::query()->where('created_at', '>=', $start)->where('created_at', '<=', $end)->get()->toArray();
$res['code'] = 0;
$res['msg'] = 'success';
if (empty($data)) {
$res['code'] = 1;
$res['msg'] = '暂无数据';
return response()->json($res);
}
foreach ($data as $key => $value) {
$info = json_decode($value['info'], true);
$res['data']['times'][] = Carbon::create($value['created_at'])->tz(config('app.timezone',
'PRC'))->isoFormat('MM-DD HH:mm');
$res['data']['uptime']['uptime'][] = round($info['uptime'], 2);
$res['data']['cpu']['use'][] = round($info['cpu_use'], 2);
$res['data']['memory']['mem_use'][] = round($info['mem_use'], 2);
$res['data']['memory']['mem_use_p'][] = round($info['mem_use_p'], 2);
$res['data']['memory']['swap_use'][] = round($info['swap_use'], 2);
$res['data']['memory']['swap_use_p'][] = round($info['swap_use_p'], 2);
$res['data']['network']['tx_now'][] = round($info['tx_now'] / 1024, 2);
$res['data']['network']['rx_now'][] = round($info['rx_now'] / 1024, 2);
}
// 插入总内存大小
$result = explode("\n", shell_exec('free -m'));
foreach ($result as $key => $val) {
if (str_contains($val, 'Mem')) {
$mem_list = preg_replace("/\s+/", " ", $val);
break;
}
}
$mem_arr = explode(' ', $mem_list);
// 内存大小MB
$mem_total = $mem_arr[1];
$res['data']['mem_total'] = round($mem_total, 2);
return response()->json($res);
}
}
@@ -1,323 +0,0 @@
<?php
/**
* 耗子Linux面板 - 插件控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Jobs\ProcessShell;
use App\Models\Plugin;
use App\Models\Task;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Validation\ValidationException;
class PluginsController extends Controller
{
/**
* 面板插件列表
* @return JsonResponse
*/
public function getList(): JsonResponse
{
$data['code'] = 0;
$data['msg'] = 'success';
$data['data'] = $this->pluginList();
// 获取首页显示状态
$shows = Plugin::query()->pluck('show', 'slug');
foreach ($data['data'] as $k => $v) {
// 如果本地已安装,则显示本地名称
$data['data'][$k]['name'] = PLUGINS[$v['slug']]['name'] ?? $data['data'][$k]['name'] ?? '无名';
// 作者名称
$data['data'][$k]['author'] = PLUGINS[$v['slug']]['author'] ?? $data['data'][$k]['author'] ?? '耗子';
// 已装版本
$data['data'][$k]['install_version'] = PLUGINS[$v['slug']]['version'] ?? '';
// 首页显示
$data['data'][$k]['show'] = $shows[$v['slug']] ?? 0;
// 去除不需要的字段
unset($data['data'][$k]['url']);
unset($data['data'][$k]['install']);
unset($data['data'][$k]['uninstall']);
unset($data['data'][$k]['update']);
if (isset(PLUGINS[$v['slug']])) {
$data['data'][$k]['control']['installed'] = true;
$data['data'][$k]['control']['allow_uninstall'] = true;
// 判断是否有更新
$data['data'][$k]['control']['update'] = version_compare($v['version'],
$data['data'][$k]['install_version'], '>');
if ($v['slug'] == 'openresty') {
$data['data'][$k]['control']['allow_uninstall'] = false;
}
} else {
$data['data'][$k]['control']['installed'] = false;
$data['data'][$k]['control']['allow_uninstall'] = false;
}
}
// 插入本地插件数据
$slugs = array_column($data['data'], 'slug');
foreach (PLUGINS as $v) {
// 如果本地已安装,则不显示
if (in_array($v['slug'], $slugs)) {
continue;
}
// 插入插件数据
$data['data'][] = [
'name' => $v['name'],
'author' => $v['author'],
'slug' => $v['slug'],
'version' => $v['version'],
'install_version' => $v['version'],
'describe' => $v['describe'],
'show' => $shows[$v['slug']] ?? 0,
'control' => [
'installed' => true,
'allow_uninstall' => false,
],
];
}
return response()->json($data);
}
/**
* 安装插件
* @param Request $request
* @return JsonResponse
*/
public function install(Request $request): JsonResponse
{
// 消毒
try {
$credentials = $this->validate($request, [
'slug' => 'required|max:255',
]);
$slug = $credentials['slug'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
// 通过slug获取插件信息
$pluginList = $this->pluginList();
// 循环插件列表查找对应的插件信息
$pluginCheck = false;
$pluginData = [];
foreach ($pluginList as $v) {
if ($v['slug'] == $slug) {
$pluginData = $v;
$pluginCheck = true;
}
}
// 判断插件是否存在
if (!$pluginCheck) {
return response()->json(['code' => 1, 'msg' => '插件不存在']);
}
// 判断插件是否已经安装
$installed = isset(PLUGINS[$slug]);
if ($installed) {
$data['code'] = 1;
$data['msg'] = '请不要重复安装!';
return response()->json($data);
}
// 入库等待安装
$task = new Task();
$task->name = '安装 '.$pluginData['name'];
$task->shell = $pluginData['install'];
$task->status = 'waiting';
$task->log = '/tmp/'.$pluginData['slug'].'.log';
$task->save();
// 塞入队列
ProcessShell::dispatch($task->id)->delay(1);
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = '任务添加成功';
return response()->json($res);
}
/**
* 卸载插件
* @param Request $request
* @return JsonResponse
*/
public function uninstall(Request $request): JsonResponse
{
// 消毒
try {
$credentials = $this->validate($request, [
'slug' => 'required|max:255',
]);
$slug = $credentials['slug'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
// 通过slug获取插件信息
$pluginList = $this->pluginList();
// 循环插件列表查找对应的插件信息
$pluginCheck = false;
$pluginData = [];
foreach ($pluginList as $v) {
if ($v['slug'] == $slug) {
$pluginData = $v;
$pluginCheck = true;
}
}
// 判断插件是否存在
if (!$pluginCheck) {
return response()->json(['code' => 1, 'msg' => '插件不存在']);
}
// 判断是否是操作openresty
if ($slug == 'openresty') {
$data['code'] = 1;
$data['msg'] = '请不要花样作死!';
return response()->json($data);
}
// 入库等待卸载
$task = new Task();
$task->name = '卸载 '.$pluginData['name'];
$task->shell = $pluginData['uninstall'];
$task->status = 'waiting';
$task->log = '/tmp/'.$pluginData['slug'].'.log';
$task->save();
// 塞入队列
ProcessShell::dispatch($task->id)->delay(1);
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = '任务添加成功';
return response()->json($res);
}
/**
* 更新插件
*/
public function update(Request $request): JsonResponse
{
// 消毒
try {
$credentials = $this->validate($request, [
'slug' => 'required|max:255',
]);
$slug = $credentials['slug'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
// 通过slug获取插件信息
$pluginList = $this->pluginList();
// 循环插件列表查找对应的插件信息
$pluginCheck = false;
$pluginData = [];
foreach ($pluginList as $v) {
if ($v['slug'] == $slug) {
$pluginData = $v;
$pluginCheck = true;
break;
}
}
// 判断插件是否存在
if (!$pluginCheck) {
return response()->json(['code' => 1, 'msg' => '插件不存在']);
}
// 判断插件是否已经安装
$installed = isset(PLUGINS[$slug]);
if (!$installed) {
$data['code'] = 1;
$data['msg'] = '插件未安装!';
return response()->json($data);
}
// 入库等待更新
$task = new Task();
$task->name = '更新 '.$pluginData['name'];
$task->shell = $pluginData['update'];
$task->status = 'waiting';
$task->log = '/tmp/'.$pluginData['slug'].'.log';
$task->save();
// 塞入队列
ProcessShell::dispatch($task->id)->delay(1);
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = '任务添加成功';
return response()->json($res);
}
/**
* 读取插件列表
*/
public function pluginList($cache = true)
{
// 判断刷新缓存
if (!$cache) {
Cache::forget('pluginList');
}
if (!Cache::has('pluginList')) {
return Cache::remember('pluginList', 3600, function () {
$response = Http::get('https://api.panel.haozi.xyz/api/plugin/list');
// 判断请求是否成功,如果不成功则抛出异常
if ($response->failed()) {
throw new Exception('获取插件列表失败,请求错误');
}
// 判断返回的JSON数据中code是否为0,如果不为0则抛出异常
if (!$response->json('code') == 0) {
throw new Exception('获取插件列表失败,服务器未返回正确的状态码');
}
return $response->json('data');
});
} else {
// 从缓存中获取access_token
return Cache::get('pluginList');
}
}
/**
* 设置插件首页显示
*/
public function setShowHome(Request $request): JsonResponse
{
// 消毒
try {
$credentials = $this->validate($request, [
'slug' => 'required|max:255',
'show' => 'required|boolean',
]);
$slug = $credentials['slug'];
$show = $credentials['show'];
} catch (ValidationException $e) {
return response()->json(['code' => 1, 'msg' => $e->getMessage()]);
}
// 判断插件是否已经安装
$installed = isset(PLUGINS[$slug]);
if (!$installed) {
$data['code'] = 1;
$data['msg'] = '插件未安装!';
return response()->json($data);
}
Plugin::query()->where('slug', $slug)->updateOrInsert(
['slug' => $slug],
['show' => $show]
);
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = '设置成功';
return response()->json($res);
}
}
@@ -1,280 +0,0 @@
<?php
/**
* 耗子Linux面板 - 安全控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class SafesController extends Controller
{
/**
* 获取防火墙状态
* @return JsonResponse
*/
public function getFirewallStatus(): JsonResponse
{
$firewallStatus = trim(shell_exec("systemctl status firewalld | grep Active | awk '{print $3}'"));
$res['code'] = 0;
$res['msg'] = 'success';
if ($firewallStatus == '(running)') {
$res['data'] = 1;
} else {
$res['data'] = 0;
}
return response()->json($res);
}
/**
* 设置防火墙状态
* @param Request $request
* @return JsonResponse
*/
public function setFirewallStatus(Request $request): JsonResponse
{
$status = $request->input('status');
if ($status) {
shell_exec("systemctl enable firewalld");
shell_exec("systemctl start firewalld");
} else {
shell_exec("systemctl stop firewalld");
shell_exec("systemctl disable firewalld");
}
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 获取SSH状态
* @return JsonResponse
*/
public function getSshStatus(): JsonResponse
{
$sshStatus = trim(shell_exec("systemctl status sshd | grep Active | awk '{print $3}'"));
$res['code'] = 0;
$res['msg'] = 'success';
if ($sshStatus == '(running)') {
$res['data'] = 1;
} else {
$res['data'] = 0;
}
return response()->json($res);
}
/**
* 设置SSH状态
* @param Request $request
* @return JsonResponse
*/
public function setSshStatus(Request $request): JsonResponse
{
$status = $request->input('status');
if ($status) {
shell_exec("systemctl enable sshd");
shell_exec("systemctl start sshd");
} else {
shell_exec("systemctl stop sshd");
shell_exec("systemctl disable sshd");
}
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 获取SSH端口
* @return JsonResponse
*/
public function getSshPort(): JsonResponse
{
$sshPort = trim(shell_exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'"));
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = $sshPort;
return response()->json($res);
}
/**
* 设置SSH端口
* @param Request $request
* @return JsonResponse
*/
public function setSshPort(Request $request): JsonResponse
{
$port = $request->input('port');
// 只能是数字
if (!is_numeric($port)) {
$res['code'] = 1;
$res['msg'] = '端口只能是数字';
return response()->json($res);
}
$oldPort = trim(shell_exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'"));
shell_exec("sed -i 's/#Port ".$oldPort."/Port ".$port."/g' /etc/ssh/sshd_config");
shell_exec("sed -i 's/Port ".$oldPort."/Port ".$port."/g' /etc/ssh/sshd_config");
// 判断ssh是否开启
$sshStatus = trim(shell_exec("systemctl status sshd | grep Active | awk '{print $3}'"));
if ($sshStatus == '(running)') {
shell_exec("systemctl restart sshd");
}
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 获取ping状态
* @return JsonResponse
*/
public function getPingStatus(): JsonResponse
{
$pingStatus = trim(shell_exec("firewall-cmd --query-rich-rule='rule protocol value=icmp drop' 2>&1"));
$res['code'] = 0;
$res['msg'] = 'success';
if ($pingStatus == 'yes') {
$res['data'] = 0;
} else {
$res['data'] = 1;
}
return response()->json($res);
}
/**
* 设置ping状态
* @param Request $request
* @return JsonResponse
*/
public function setPingStatus(Request $request): JsonResponse
{
$status = $request->input('status');
if ($status) {
shell_exec("firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop' 2>&1");
} else {
shell_exec("firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop' 2>&1");
}
shell_exec("firewall-cmd --reload");
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 获取防火墙规则
* @return JsonResponse
*/
public function getFirewallRules(): JsonResponse
{
$firewallRules = trim(shell_exec("firewall-cmd --list-all 2>&1"));
// 判断是否开启
if (str_contains($firewallRules, 'not running')) {
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = [];
return response()->json($res);
}
// 正则匹配出ports
preg_match('/ports: (.*)/', $firewallRules, $matches);
$rawPorts = $matches[1];
$ports = explode(' ', $rawPorts);
// 对ports进行分割为port=>protocol形式
$rules = [];
foreach ($ports as $port) {
$rule = explode('/', $port);
$rules[] = [
'port' => $rule[0],
'protocol' => $rule[1],
];
}
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = $rules;
return response()->json($res);
}
/**
* 添加防火墙规则
* @param Request $request
* @return JsonResponse
*/
public function addFirewallRule(Request $request): JsonResponse
{
// 消毒
try {
$input = $this->validate($request, [
'port' => ['required','regex:/^([0-9]+)(-([0-9]+))?$/'],
'protocol' => 'required|in:tcp,udp',
]);
$port = $input['port'];
$protocol = $input['protocol'];
} catch (ValidationException $e) {
return response()->json([
'code' => 1,
'msg' => '参数错误:'.$e->getMessage(),
'errors' => $e->errors()
], 200);
}
// 判断是否开启
$firewallStatus = trim(shell_exec("firewall-cmd --state 2>&1"));
if ($firewallStatus != 'running') {
$res['code'] = 1;
$res['msg'] = '防火墙未开启';
return response()->json($res);
}
// 清空当前规则
shell_exec("firewall-cmd --remove-port=".$port."/".$protocol." --permanent");
// 添加新的防火墙规则
shell_exec("firewall-cmd --add-port=".$port."/".$protocol." --permanent");
// 重启防火墙
shell_exec("firewall-cmd --reload");
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
/**
* 删除防火墙规则
* @param Request $request
* @return JsonResponse
*/
public function deleteFirewallRule(Request $request): JsonResponse
{
// 消毒
try {
$input = $this->validate($request, [
'port' => ['required','regex:/^([0-9]+)(-([0-9]+))?$/'],
'protocol' => 'required|in:tcp,udp',
]);
$port = $input['port'];
$protocol = $input['protocol'];
} catch (ValidationException $e) {
return response()->json([
'code' => 1,
'msg' => '参数错误:'.$e->getMessage(),
'errors' => $e->errors()
], 200);
}
// 判断是否开启
$firewallStatus = trim(shell_exec("firewall-cmd --state 2>&1"));
if ($firewallStatus != 'running') {
$res['code'] = 1;
$res['msg'] = '防火墙未开启';
return response()->json($res);
}
// 清空当前规则
shell_exec("firewall-cmd --remove-port=".$port."/".$protocol." --permanent");
// 重启防火墙
shell_exec("firewall-cmd --reload");
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
}
@@ -1,165 +0,0 @@
<?php
/**
* 耗子Linux面板 - 设置控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class SettingsController extends Controller
{
/**
* 获取面板设置
* @param Request $request
* @return JsonResponse
*/
public function get(Request $request): JsonResponse
{
$settings = Setting::all();
// 隐藏字段
$settings->makeHidden('id');
$settings->makeHidden('created_at');
$settings->makeHidden('updated_at');
$settingArr = $settings->pluck('value', 'name');
// 从nginx配置文件中获取面板端口
$nginxConf = file_get_contents('/www/server/nginx/conf/nginx.conf');
preg_match('/listen\s+(\d+)/', $nginxConf, $matches);
if (!isset($matches[1])) {
$res['code'] = 1;
$res['msg'] = '获取面板端口失败,请检查nginx主配置文件';
return response()->json($res);
}
// API
$api = 0;
$apiToken = '';
if (isset($settingArr['api']) && $settingArr['api'] == 1) {
$api = 1;
$apiToken = $settingArr['api_token'] ?? '';
}
// 多设备登录
$multiLogin = 0;
if (isset($settingArr['multi_login']) && $settingArr['multi_login'] == 1) {
$multiLogin = 1;
}
$data = [
'name' => $settingArr['name'],
'username' => $request->user()->username,
'email' => $request->user()->email,
'password' => '',
'port' => $matches[1],
'api' => $api,
'api_token' => $apiToken,
'multi_login' => $multiLogin,
];
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = $data;
return response()->json($res);
}
/**
* 保存面板设置
* @param Request $request
* @return JsonResponse
*/
public function save(Request $request): JsonResponse
{
// 获取前端传递过来的数据
$settings = $request->all();
// 将数据入库
foreach ($settings as $key => $value) {
if ($key == 'access_token' || $key == 'username' || $key == 'email' || $key == 'password' || $key == 'api_token' || $key == 'api' || $key == 'port') {
continue;
}
// 创建或更新
Setting::query()->updateOrCreate(['name' => $key], ['value' => $value]);
}
// 单独处理用户名、密码、邮箱
if ($request->input('username') != $request->user()->username) {
$request->user()->update(['username' => $request->input('username')]);
}
if ($request->input('password') != '') {
$request->user()->update(['password' => Hash::make($request->input('password'))]);
}
if ($request->input('email') != $request->user()->email) {
$request->user()->update(['email' => $request->input('email')]);
}
// 处理面板端口
$port = $request->input('port');
$nginxConf = file_get_contents('/www/server/nginx/conf/nginx.conf');
preg_match('/listen\s+(\d+)/', $nginxConf, $matches);
if (!isset($matches[1])) {
$res['code'] = 1;
$res['msg'] = '获取面板端口失败,请检查nginx主配置文件';
return response()->json($res);
}
if ($port != $matches[1]) {
$nginxConf = preg_replace('/listen\s+'.$matches[1].'/', 'listen '.$port, $nginxConf, 1);
file_put_contents('/www/server/nginx/conf/nginx.conf', $nginxConf);
// 重载nginx
shell_exec('systemctl reload nginx');
// 防火墙放行端口
$port = escapeshellarg($port);
shell_exec('firewall-cmd --permanent --zone=public --add-port='.$port.'/tcp >/dev/null 2>&1');
shell_exec('firewall-cmd --reload');
}
// 处理api
$api = $request->input('api', 0);
$apiCheck = Setting::query()->where('name', 'api')->value('value');
if (empty($apiCheck)) {
$apiCheck = 0;
}
if ($api != $apiCheck) {
if ($api) {
Setting::query()->insert([
'name' => 'api',
'value' => 1,
]);
// 生成api用户
$username = 'api_'.Str::random();
$apiUser = User::query()->create([
'username' => $username,
'password' => Hash::make(Str::random()),
'email' => 'panel_api@haozi.net',
]);
// 生成api token
$apiToken = $apiUser->createToken('api')->plainTextToken;
Setting::query()->insert([
'name' => 'api_user',
'value' => $username,
]);
Setting::query()->insert([
'name' => 'api_token',
'value' => $apiToken,
]);
} else {
Setting::query()->where('name', 'api')->delete();
Setting::query()->where('name', 'api_token')->delete();
$username = Setting::query()->where('name', 'api_user')->value('value');
Setting::query()->where('name', 'api_user')->delete();
Setting::query()->where('name', 'api')->delete();
$apiUser = User::query()->where('username', $username)->first();
// 删除api用户的所有token
$apiUser->tokens()->delete();
// 删除api用户
$apiUser->delete();
}
}
$res['code'] = 0;
$res['msg'] = 'success';
return response()->json($res);
}
}
@@ -1,120 +0,0 @@
<?php
/**
* 耗子Linux面板 - 任务控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Task;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class TasksController extends Controller
{
/**
* 获取是否有进行中/未完成的任务
* 前台会根据这个返回渲染顶部工具栏的图标
* @return JsonResponse
*/
public function getStatus(): JsonResponse
{
// 获取任务表中的等待中/进行中的任务
$task = Task::query()->where('status', 'running')->orWhere('status', 'waiting')->first();
$res['code'] = 0;
$res['msg'] = 'success';
if (empty($task)) {
$res['data'] = false;
} else {
$res['data'] = true;
}
return response()->json($res);
}
/**
* 获取进行中的那个任务
* @return JsonResponse
*/
public function getListRunning(): JsonResponse
{
// 获取进行中的任务列表
$task = Task::query()->where('status', 'running')->get()->toArray();
// 判断任务是否存在
if (empty($task)) {
$task[0] = "";
}
// 构建返回数据
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = $task[0];
return response()->json($res);
}
/**
* 获取等待中的任务列表
* @return JsonResponse
*/
public function getListWaiting(): JsonResponse
{
$task_list = Task::query()->where('status', 'waiting')->get()->toArray();
$res['code'] = 0;
$res['msg'] = 'success';
// 判断任务是否存在
if (empty($task_list)) {
$res['code'] = 1;
$res['msg'] = '无任务!';
}
$res['data'] = $task_list;
return response()->json($res);
}
/**
* 获取已完成的任务列表
* @return JsonResponse
*/
public function getListFinished(): JsonResponse
{
$task_list = Task::query()->where('status', 'finished')->get()->toArray();
$res['code'] = 0;
$res['msg'] = 'success';
if (empty($task_list)) {
$res['code'] = 1;
$res['msg'] = '无任务!';
}
$res['data'] = $task_list;
return response()->json($res);
}
/**
* 获取单个任务的log
* @param Request $request
* @return JsonResponse
*/
public function getTaskLog(Request $request): JsonResponse
{
$name = $request->get('name');
$log_file = Task::query()->where('name', $name)->value('log');
$log = shell_exec('tail -n 30 '.$log_file);
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = $log;
return response()->json($res);
}
/**
* 删除任务
* @param Request $request
* @return JsonResponse
*/
public function deleteTask(Request $request): JsonResponse
{
$name = $request->get('name');
$res['code'] = 0;
$res['msg'] = 'success';
$res['data'] = Task::query()->where('name', $name)->delete();
return response()->json($res);
}
}
@@ -1,62 +0,0 @@
<?php
/**
* 耗子Linux面板 - 用户控制器
* @author 耗子
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class UsersController extends Controller
{
/**
* 登录
*
* @param Request $request
* @return JsonResponse
*/
public function login(Request $request): JsonResponse
{
// 消毒数据
try {
$credentials = $this->validate($request, [
'username' => 'required|max:255',
'password' => 'required|max:255',
'remember' => 'required|boolean'
]);
} catch (ValidationException $e) {
return response()->json([
'code' => 1,
'msg' => '参数错误:'.$e->getMessage(),
'errors' => $e->errors()
], 422);
}
if (auth()->attempt(['username' => $credentials['username'], 'password' => $credentials['password']],
$credentials['remember'])) {
$user = auth()->user();
// 多设备登录
$multiLogin = Setting::query()->where('name', 'multi_login')->value('value');
if ($multiLogin != 1) {
$user->tokens()->delete();
}
$token = $user->createToken('token')->plainTextToken;
return response()->json(['code' => 0, 'msg' => '登录成功', 'data' => ['access_token' => $token]]);
} else {
return response()->json(['code' => 1, 'msg' => '登录失败,用户名或密码错误']);
}
}
public function getInfo(Request $request): JsonResponse
{
$user = $request->user();
$res['code'] = 0;
$res['msg'] = 'success';
$res['data']['username'] = $user->username;
return response()->json($res);
}
}
File diff suppressed because it is too large Load Diff
-13
View File
@@ -1,13 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
-67
View File
@@ -1,67 +0,0 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\AddAccessToken::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}
-21
View File
@@ -1,21 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
class AddAccessToken
{
/**
* Handle an incoming request.
*/
public function handle($request, Closure $next, ...$guards)
{
// 获取请求头中的token
$token = $request->header('access_token') ?? $request->input('access_token');
// 将token放入请求中
$request->headers->set('Authorization', 'Bearer '.$token);
return $next($request);
}
}
-27
View File
@@ -1,27 +0,0 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
*
* @return string|null
*/
protected function redirectTo($request)
{
abort(response()->json([
'code' => 1001,
'msg' => '登录状态失效'
]));
}
}
-17
View File
@@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
@@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
@@ -1,32 +0,0 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @param string|null ...$guards
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next, ...$guards)
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}
-19
View File
@@ -1,19 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}
-20
View File
@@ -1,20 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts()
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}
-28
View File
@@ -1,28 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}
-22
View File
@@ -1,22 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}
-17
View File
@@ -1,17 +0,0 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
-73
View File
@@ -1,73 +0,0 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\Task;
class ProcessShell implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* 任务最大尝试次数
* @var int
*/
public int $tries = 1;
/**
* 任务运行的超时时间
* @var int
*/
public int $timeout = 7200;
/**
* 任务名
*/
public string $task_id;
/**
* 任务脚本
*/
public string $shell;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($task_id)
{
$this->task_id = $task_id;
}
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
// 检查当前是否有任务正在运行
$taskCheck = Task::query()->where('status', 'running')->get();
if ($taskCheck->isNotEmpty()) {
$this->release(10);
return;
}
// 查询任务
$task = Task::query()->where('id', $this->task_id)->first();
echo $task->name."开始执行".PHP_EOL;
// 更新任务状态为running
$task->job_id = $this->job->getJobId();
$task->status = 'running';
$task->save();
shell_exec($task->shell);
// 更新任务状态
$task->status = 'finished';
$task->save();
echo $task->name."执行完毕".PHP_EOL;
}
}
-22
View File
@@ -1,22 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Cron extends Model
{
use HasFactory;
// 白名单
protected $fillable = [
'name',
'status',
'type',
'time',
'shell',
'created_at',
'updated_at',
];
}
-11
View File
@@ -1,11 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Database extends Model
{
use HasFactory;
}
-18
View File
@@ -1,18 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Monitor extends Model
{
use HasFactory;
// 白名单
protected $fillable = [
'info',
'created_at',
'updated_at',
];
}
-14
View File
@@ -1,14 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Plugin extends Model
{
use HasFactory;
// 白名单
protected $fillable = ['slug', 'name', 'version', 'show', 'created_at', 'updated_at'];
}
-14
View File
@@ -1,14 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Setting extends Model
{
use HasFactory;
// 白名单
protected $fillable = ['name', 'value', 'created_at', 'updated_at'];
}
-22
View File
@@ -1,22 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
use HasFactory;
// 白名单
protected $fillable = [
'job_id',
'name',
'shell',
'log',
'status',
'created_at',
'updated_at',
];
}
-43
View File
@@ -1,43 +0,0 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'username',
'password',
'email'
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
];
}
-14
View File
@@ -1,14 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Website extends Model
{
use HasFactory;
// 白名单
protected $fillable = ['name', 'status', 'path', 'php', 'ssl', 'ssl_date', 'note', 'created_at', 'updated_at'];
}
-28
View File
@@ -1,28 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
//
}
}
-30
View File
@@ -1,30 +0,0 @@
<?php
namespace App\Providers;
// use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
//
}
}
@@ -1,21 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Broadcast::routes();
require base_path('routes/channels.php');
}
}
-42
View File
@@ -1,42 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
/**
* Register any events for your application.
*
* @return void
*/
public function boot()
{
//
}
/**
* Determine if events and listeners should be automatically discovered.
*
* @return bool
*/
public function shouldDiscoverEvents()
{
return false;
}
}
-69
View File
@@ -1,69 +0,0 @@
<?php
namespace App\Providers;
use Exception;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use App\Services\Plugin;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
class PluginServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @param Plugin $plugins
* @return void
* @throws BindingResolutionException
* @throws FileNotFoundException
*/
public function boot(Plugin $plugins): void
{
$loader = $this->app->make('translation.loader');
// Make view instead of view.finder since the finder is defined as not a singleton
$finder = $this->app->make('view');
foreach ($plugins->getPlugins() as $plugin) {
// 加载视图路径
$finder->addNamespace($plugin['slug'], $plugin['path']."/views");
// 加载语言包
$loader->addNamespace($plugin['slug'], $plugin['path']."/lang");
}
// 加载插件Composer装载文件
try {
foreach ($plugins->getComposerLoaders() as $autoloader) {
require $autoloader;
}
} catch (Exception $e) {
throw new BindingResolutionException($e->getMessage());
}
// 加载插件主文件
try {
foreach ($plugins->getPluginMainFiles() as $file) {
require_once $file;
}
} catch (Exception $e) {
throw new BindingResolutionException($e->getMessage());
}
}
/**
* 注册插件服务
*
* @return void
*/
public function register(): void
{
$this->app->singleton('plugins', Plugin::class);
// 设置面板名称
$name = @DB::table('settings')->where('name', 'name')->value('value');
$this->app['config']['panel.name'] = !empty($name) ? $name : config('panel.name');
}
}
-52
View File
@@ -1,52 +0,0 @@
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to the "home" route for your application.
*
* Typically, users are redirected here after authentication.
*
* @var string
*/
public const HOME = '/';
/**
* Define your route model bindings, pattern filters, and other route configuration.
*
* @return void
*/
public function boot()
{
$this->configureRateLimiting();
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
/**
* Configure the rate limiters for the application.
*
* @return void
*/
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}
}
-165
View File
@@ -1,165 +0,0 @@
<?php
namespace App\Services;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Foundation\Application;
class Plugin
{
/**
* @var Application
*/
protected Application $app;
/**
* @var Dispatcher
*/
protected Dispatcher $dispatcher;
/**
* @var Filesystem
*/
protected Filesystem $filesystem;
/**
* @var Collection|null
*/
protected ?Collection $plugins;
public function __construct(
Application $app,
Dispatcher $dispatcher,
Filesystem $filesystem,
Collection $plugins = new Collection()
) {
$this->app = $app;
$this->dispatcher = $dispatcher;
$this->filesystem = $filesystem;
$this->plugins = $plugins;
}
/**
* 读取所有插件
*
* @return Collection|null
* @throws FileNotFoundException
* @throws Exception
*/
public function getPlugins(): ?Collection
{
if ($this->plugins->isEmpty()) {
$plugins = new Collection();
$installed = [];
try {
$resource = opendir($this->getPluginsDir());
} catch (Exception $e) {
// 输出错误信息
throw new Exception($e->getMessage());
}
// 读取插件目录
while ($filename = @readdir($resource)) {
if ($filename == '.' || $filename == '..') {
continue;
}
$path = $this->getPluginsDir().DIRECTORY_SEPARATOR.$filename;
if (is_dir($path)) {
$pluginJsonPath = $path.DIRECTORY_SEPARATOR.'plugin.json';
if (file_exists($pluginJsonPath)) {
// 读取插件配置文件
$installed[$filename] = json_decode($this->filesystem->get($pluginJsonPath), true);
}
}
}
closedir($resource);
foreach ($installed as $dirname => $package) {
// 初始化插件信息
$plugin = [];
$plugin['name'] = (Arr::get($package, 'name'));
$plugin['author'] = (Arr::get($package, 'author'));
$plugin['describe'] = (Arr::get($package, 'describe'));
$plugin['slug'] = (Arr::get($package, 'slug'));
$plugin['version'] = (Arr::get($package, 'version'));
$plugin['path'] = $this->getPluginsDir().DIRECTORY_SEPARATOR.$dirname;
if ($plugins->has($plugin['slug'])) {
continue;
}
$plugins->put($plugin['slug'], $plugin);
}
define('PLUGINS', $plugins->toArray());
$this->plugins = $plugins;
}
return $this->plugins;
}
/**
* 获取全部插件的主文件
*
* @return Collection
* @throws Exception
*/
public function getPluginMainFiles(): Collection
{
$pluginMainFiles = new Collection;
try {
foreach ($this->getPlugins() as $plugin) {
if ($this->filesystem->exists($file = $plugin['path'].'/plugin.php')) {
$pluginMainFiles->push($file);
}
}
} catch (Exception $e) {
// 输出错误信息
throw new Exception($e->getMessage());
}
return $pluginMainFiles;
}
/**
* 获取全部插件的Composer装载文件
*
* @return Collection
* @throws Exception
*/
public function getComposerLoaders(): Collection
{
$composerLoaders = new Collection;
foreach ($this->getPlugins() as $plugin) {
if ($this->filesystem->exists($file = $plugin['path'].'/vendor/autoload.php')) {
$composerLoaders->push($file);
}
}
return $composerLoaders;
}
/**
* 插件目录
*
* @return string
*/
public function getPluginsDir(): string
{
return config('plugins.directory') ?: base_path('plugins');
}
}
+77
View File
@@ -0,0 +1,77 @@
package commands
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/app/services"
"panel/pkg/tools"
)
type Monitoring struct {
}
// Signature The name and signature of the console command.
func (receiver *Monitoring) Signature() string {
return "panel:monitoring"
}
// Description The console command description.
func (receiver *Monitoring) Description() string {
return "[面板] 系统监控"
}
// Extend The console command extend.
func (receiver *Monitoring) Extend() command.Extend {
return command.Extend{
Category: "panel",
}
}
// Handle Execute the console command.
func (receiver *Monitoring) Handle(ctx console.Context) error {
var setting models.Setting
monitor := services.NewSettingImpl().Get(models.SettingKeyMonitor)
if !cast.ToBool(monitor) {
return nil
}
info := tools.GetMonitoringInfo()
err := facades.Orm().Query().Create(&models.Monitor{
Info: info,
})
if err != nil {
facades.Log().Errorf("[面板] 系统监控保存失败: %s", err.Error())
color.Redf("[面板] 系统监控保存失败: %s", err.Error())
return nil
}
// 删除过期数据
err = facades.Orm().Query().Where("key", "monitor_days").First(&setting)
if err != nil {
return nil
}
if setting.Value == "0" || len(setting.Value) == 0 {
return nil
}
days, err := strconv.Atoi(setting.Value)
if err != nil {
return nil
}
_, err = facades.Orm().Query().Where("created_at < ?", carbon.Now().SubDays(days).ToDateTimeString()).Delete(&models.Monitor{})
if err != nil {
facades.Log().Errorf("[面板] 系统监控删除过期数据失败: %s", err.Error())
return nil
}
return nil
}
+498
View File
@@ -0,0 +1,498 @@
package commands
import (
"os"
"path/filepath"
"sort"
"strings"
"github.com/gookit/color"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Panel struct {
}
// Signature The name and signature of the console command.
func (receiver *Panel) Signature() string {
return "panel"
}
// Description The console command description.
func (receiver *Panel) Description() string {
return "[面板] 命令行"
}
// Extend The console command extend.
func (receiver *Panel) Extend() command.Extend {
return command.Extend{
Category: "panel",
}
}
// Handle Execute the console command.
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":
var check models.User
err := facades.Orm().Query().FirstOrFail(&check)
if err == nil {
color.Redln("面板已初始化")
return nil
}
settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子Linux面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyEntrance, Value: "/"}}
err = facades.Orm().Query().Create(&settings)
if err != nil {
color.Redln("初始化失败")
return nil
}
hash, err := facades.Hash().Make(tools.RandomString(32))
if err != nil {
color.Redln("初始化失败")
return nil
}
user := services.NewUserImpl()
_, err = user.Create("admin", hash)
if err != nil {
color.Redln("创建管理员失败")
return nil
}
color.Greenln("初始化成功")
case "update":
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
}
color.Greenln("更新成功")
case "getInfo":
var user models.User
err := facades.Orm().Query().Where("id", 1).FirstOrFail(&user)
if err != nil {
color.Redln("获取管理员信息失败")
return nil
}
password := tools.RandomString(16)
hash, err := facades.Hash().Make(password)
if err != nil {
color.Redln("生成密码失败")
return nil
}
user.Username = tools.RandomString(8)
user.Password = hash
err = facades.Orm().Query().Save(&user)
if err != nil {
color.Redln("保存管理员信息失败")
return nil
}
port := tools.ExecShell(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
color.Greenln("用户名: " + user.Username)
color.Greenln("密码: " + password)
color.Greenln("面板端口: " + port)
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
case "getPort":
port := tools.ExecShell("cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}'")
color.Greenln("面板端口: " + port)
case "getEntrance":
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
case "deleteEntrance":
err := services.NewSettingImpl().Set(models.SettingKeyEntrance, "/")
if err != nil {
color.Redln("删除面板入口失败")
return nil
}
color.Greenln("删除面板入口成功")
case "writePlugin":
slug := arg1
version := arg2
if len(slug) == 0 || len(version) == 0 {
color.Redln("参数错误")
return nil
}
var plugin models.Plugin
err := facades.Orm().Query().UpdateOrCreate(&plugin, models.Plugin{
Slug: slug,
}, models.Plugin{
Version: version,
})
if err != nil {
color.Redln("写入插件安装状态失败")
return nil
}
color.Greenln("写入插件安装状态成功")
case "deletePlugin":
slug := arg1
if len(slug) == 0 {
color.Redln("参数错误")
return nil
}
_, err := facades.Orm().Query().Where("slug", slug).Delete(&models.Plugin{})
if err != nil {
color.Redln("移除插件安装状态失败")
return nil
}
color.Greenln("移除插件安装状态成功")
case "writeMysqlPassword":
password := arg1
if len(password) == 0 {
color.Redln("参数错误")
return nil
}
var setting models.Setting
err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{
Key: models.SettingKeyMysqlRootPassword,
}, models.Setting{
Value: password,
})
if err != nil {
color.Redln("写入MySQL root密码失败")
return nil
}
color.Greenln("写入MySQL root密码成功")
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("清理任务失败")
return nil
}
color.Greenln("清理任务成功")
case "backup":
backupType := arg1
name := arg2
path := arg3
save := arg4
hr := `+----------------------------------------------------`
if len(backupType) == 0 || len(name) == 0 || len(path) == 0 || len(save) == 0 {
color.Redln("参数错误")
return nil
}
color.Greenln(hr)
color.Greenln("★ 开始备份 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
if !tools.Exists(path) {
tools.Mkdir(path, 0644)
}
switch backupType {
case "website":
color.Yellowln("|-目标网站: " + name)
var website models.Website
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil {
color.Redln("|-网站不存在")
color.Greenln(hr)
return nil
}
backupFile := path + "/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".zip"
tools.ExecShell(`cd '` + website.Path + `' && zip -r '` + backupFile + `' .`)
color.Greenln("|-备份成功")
case "mysql":
rootPassword := services.NewSettingImpl().Get(models.SettingKeyMysqlRootPassword)
backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
err := os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
color.Redln("|-备份MySQL数据库失败: " + err.Error())
color.Greenln(hr)
return nil
}
color.Greenln("|-目标MySQL数据库: " + name)
color.Greenln("|-开始导出")
tools.ExecShell(`mysqldump -uroot ` + name + ` > /tmp/` + backupFile + ` 2>&1`)
color.Greenln("|-导出成功")
color.Greenln("|-开始压缩")
tools.ExecShell("cd /tmp && zip -r " + backupFile + ".zip " + backupFile)
tools.RemoveFile("/tmp/" + backupFile)
color.Greenln("|-压缩成功")
color.Greenln("|-开始移动")
tools.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip")
color.Greenln("|-移动成功")
_ = os.Unsetenv("MYSQL_PWD")
color.Greenln("|-备份成功")
case "postgresql":
backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
check := tools.ExecShell(`su - postgres -c "psql -l" 2>&1`)
if strings.Contains(check, name) {
color.Redln("|-数据库不存在")
color.Greenln(hr)
return nil
}
color.Greenln("|-目标PostgreSQL数据库: " + name)
color.Greenln("|-开始导出")
tools.ExecShell(`su - postgres -c "pg_dump '` + name + `'" > /tmp/` + backupFile + ` 2>&1`)
color.Greenln("|-导出成功")
color.Greenln("|-开始压缩")
tools.ExecShell("cd /tmp && zip -r " + backupFile + ".zip " + backupFile)
tools.RemoveFile("/tmp/" + backupFile)
color.Greenln("|-压缩成功")
color.Greenln("|-开始移动")
tools.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip")
color.Greenln("|-移动成功")
color.Greenln("|-备份成功")
}
color.Greenln(hr)
files, err := os.ReadDir(path)
if err != nil {
color.Redln("|-清理失败: " + err.Error())
return nil
}
var filteredFiles []os.FileInfo
for _, file := range files {
if strings.HasPrefix(file.Name(), name) && strings.HasSuffix(file.Name(), ".zip") {
fileInfo, err := os.Stat(filepath.Join(path, file.Name()))
if err != nil {
continue
}
filteredFiles = append(filteredFiles, fileInfo)
}
}
sort.Slice(filteredFiles, func(i, j int) bool {
return filteredFiles[i].ModTime().After(filteredFiles[j].ModTime())
})
for i := cast.ToInt(save); i < len(filteredFiles); i++ {
fileToDelete := filepath.Join(path, filteredFiles[i].Name())
color.Yellowln("|-清理备份: " + fileToDelete)
tools.RemoveFile(fileToDelete)
}
color.Greenln("|-清理完成")
color.Greenln(hr)
color.Greenln("☆ 备份完成 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
case "cutoff":
name := arg1
save := arg2
hr := `+----------------------------------------------------`
if len(name) == 0 || len(save) == 0 {
color.Redln("参数错误")
return nil
}
color.Greenln(hr)
color.Greenln("★ 开始切割 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
color.Yellowln("|-目标网站: " + name)
var website models.Website
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil {
color.Redln("|-网站不存在")
color.Greenln(hr)
return nil
}
logPath := "/www/wwwlogs/" + website.Name + ".log"
if !tools.Exists(logPath) {
color.Redln("|-日志文件不存在")
color.Greenln(hr)
return nil
}
backupPath := "/www/wwwlogs/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".log.zip"
tools.ExecShell(`cd /www/wwwlogs && zip -r ` + backupPath + ` ` + website.Name + ".log")
tools.ExecShell(`echo "" > ` + logPath)
color.Greenln("|-切割成功")
color.Greenln(hr)
files, err := os.ReadDir("/www/wwwlogs")
if err != nil {
color.Redln("|-清理失败: " + err.Error())
return nil
}
var filteredFiles []os.FileInfo
for _, file := range files {
if strings.HasPrefix(file.Name(), website.Name) && strings.HasSuffix(file.Name(), ".log.zip") {
fileInfo, err := os.Stat(filepath.Join("/www/wwwlogs", file.Name()))
if err != nil {
continue
}
filteredFiles = append(filteredFiles, fileInfo)
}
}
sort.Slice(filteredFiles, func(i, j int) bool {
return filteredFiles[i].ModTime().After(filteredFiles[j].ModTime())
})
for i := cast.ToInt(save); i < len(filteredFiles); i++ {
fileToDelete := filepath.Join("/www/wwwlogs", filteredFiles[i].Name())
color.Yellowln("|-清理日志: " + fileToDelete)
tools.RemoveFile(fileToDelete)
}
color.Greenln("|-清理完成")
color.Greenln(hr)
color.Greenln("☆ 切割完成 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
case "writeSite":
name := arg1
status := cast.ToBool(arg2)
path := ctx.Argument(3)
php := cast.ToInt(ctx.Argument(4))
ssl := cast.ToBool(ctx.Argument(5))
if len(name) == 0 || len(path) == 0 {
color.Redln("参数错误")
return nil
}
var website models.Website
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err == nil {
color.Redln("网站已存在")
return nil
}
_, err := os.Stat(path)
if os.IsNotExist(err) {
color.Redln("网站目录不存在")
return nil
}
err = facades.Orm().Query().Create(&models.Website{
Name: name,
Status: status,
Path: path,
Php: php,
Ssl: ssl,
})
if err != nil {
color.Redln("写入网站失败")
return nil
}
color.Greenln("写入网站成功")
case "deleteSite":
name := arg1
if len(name) == 0 {
color.Redln("参数错误")
return nil
}
_, err := facades.Orm().Query().Where("name", name).Delete(&models.Website{})
if err != nil {
color.Redln("删除网站失败")
return nil
}
color.Greenln("删除网站成功")
case "writeSetting":
key := arg1
value := arg2
if len(key) == 0 || len(value) == 0 {
color.Redln("参数错误")
return nil
}
var setting models.Setting
err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{
Key: key,
}, models.Setting{
Value: value,
})
if err != nil {
color.Redln("写入设置失败")
return nil
}
color.Greenln("写入设置成功")
case "deleteSetting":
key := arg1
if len(key) == 0 {
color.Redln("参数错误")
return nil
}
_, err := facades.Orm().Query().Where("key", key).Delete(&models.Setting{})
if err != nil {
color.Redln("删除设置失败")
return nil
}
color.Greenln("删除设置成功")
default:
color.Yellowln(facades.Config().GetString("panel.name") + "命令行工具 - " + facades.Config().GetString("panel.version"))
color.Greenln("请使用以下命令:")
color.Greenln("panel update {proxy} 更新 / 修复面板到最新版本")
color.Greenln("panel getInfo 重新初始化面板账号信息")
color.Greenln("panel getPort 获取面板访问端口")
color.Greenln("panel getEntrance 获取面板访问入口")
color.Greenln("panel deleteEntrance 删除面板访问入口")
color.Greenln("panel cleanTask 清理面板运行中和等待中的任务[任务卡住时使用]")
color.Greenln("panel backup {website/mysql/postgresql} {name} {path} {save_copies} 备份网站 / MySQL数据库 / PostgreSQL数据库到指定目录并保留指定数量")
color.Greenln("panel cutoff {website_name} {save_copies} 切割网站日志并保留指定数量")
color.Redln("以下命令请在开发者指导下使用:")
color.Yellowln("panel init 初始化面板")
color.Yellowln("panel writePlugin {slug} {version} 写入插件安装状态")
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 deleteSetting {name} 删除面板设置数据")
}
return nil
}
+25
View File
@@ -0,0 +1,25 @@
package console
import (
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/schedule"
"github.com/goravel/framework/facades"
"panel/app/console/commands"
)
type Kernel struct {
}
func (kernel *Kernel) Schedule() []schedule.Event {
return []schedule.Event{
facades.Schedule().Command("panel:monitoring").EveryMinute().SkipIfStillRunning(),
}
}
func (kernel *Kernel) Commands() []console.Command {
return []console.Command{
&commands.Panel{},
&commands.Monitoring{},
}
}
-70
View File
@@ -1,70 +0,0 @@
<?php
/**
* 耗子Linux面板 - 帮助函数
* @author 耗子
*/
/**
* 获取网络统计信息
* @return array
*/
function getNetInfo(): array
{
$networkRaw = file_get_contents('/proc/net/dev');
$networkArr = explode("\n", $networkRaw);
foreach ($networkArr as $key => $val) {
if ($key < 2) {
continue;
}
$val = str_replace(':', ' ', trim($val));
$val = preg_replace("/[ ]+/", " ", $val);
$arr = explode(' ', $val);
if (!empty($arr[0])) {
$arr = array($arr[0], $arr[1], $arr[9]);
$allRs[$arr[0].$key] = $arr;
}
}
ksort($allRs);
$tx = 0;
$rx = 0;
foreach ($allRs as $key => $val) {
// 排除本地lo
if (str_contains($key, 'lo')) {
continue;
}
$tx += $val[2];
$rx += $val[1];
}
$res['tx'] = $tx;
$res['rx'] = $rx;
return $res;
}
/**
* 格式化bytes
* @param $size
* @return string
*/
function formatBytes($size): string
{
$size = is_numeric($size) ? $size : 0;
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) {
$size /= 1024;
}
return round($size, 2).$units[$i];
}
/**
* 裁剪字符串
* @param $begin
* @param $end
* @param $str
* @return string
*/
function cut($begin, $end, $str): string
{
$b = mb_strpos($str, $begin) + mb_strlen($begin);
$e = mb_strpos($str, $end) - $b;
return mb_substr($str, $b, $e);
}
+295
View File
@@ -0,0 +1,295 @@
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(),
}
}
func (c *CronController) List(ctx http.Context) {
limit := ctx.Request().QueryInt("limit")
page := ctx.Request().QueryInt("page")
var crons []models.Cron
var total int64
err := facades.Orm().Query().Paginate(page, limit, &crons, &total)
if err != nil {
facades.Log().Error("[面板][CronController] 查询计划任务列表失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, http.Json{
"total": total,
"items": crons,
})
}
func (c *CronController) Add(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
"time": "required",
"script": "required",
"type": "required|in:shell,backup,cutoff",
"backup_type": "required_if:type,backup|in:website,mysql,postgresql",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
// 单独验证时间格式
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) {
Error(ctx, http.StatusBadRequest, "时间格式错误")
return
}
shell := 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] 计划任务目录不存在")
Error(ctx, http.StatusInternalServerError, "计划任务目录不存在")
return
}
if !tools.Exists(shellLogDir) {
facades.Log().Error("[面板][CronController] 计划任务日志目录不存在")
Error(ctx, http.StatusInternalServerError, "计划任务日志目录不存在")
return
}
shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + tools.RandomString(16)
if !tools.WriteFile(shellDir+shellFile+".sh", shell, 0700) {
facades.Log().Error("[面板][CronController] 创建计划任务脚本失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
tools.ExecShell("dos2unix " + shellDir + shellFile + ".sh")
var cron models.Cron
cron.Name = ctx.Request().Input("name")
cron.Type = 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)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
c.cron.AddToSystem(cron)
Success(ctx, http.Json{
"id": cron.ID,
})
}
// Script 获取脚本内容
func (c *CronController) Script(ctx http.Context) {
var cron models.Cron
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
Error(ctx, http.StatusBadRequest, "计划任务不存在")
return
}
Success(ctx, tools.ReadFile(cron.Shell))
}
func (c *CronController) Update(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
"time": "required",
"script": "required",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
// 单独验证时间格式
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) {
Error(ctx, http.StatusBadRequest, "时间格式错误")
return
}
var cron models.Cron
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
Error(ctx, http.StatusBadRequest, "计划任务不存在")
return
}
if !cron.Status {
Error(ctx, http.StatusBadRequest, "计划任务已禁用")
return
}
cron.Time = ctx.Request().Input("time")
cron.Name = ctx.Request().Input("name")
err = facades.Orm().Query().Save(&cron)
if err != nil {
facades.Log().Error("[面板][CronController] 更新计划任务失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if !tools.WriteFile(cron.Shell, ctx.Request().Input("script"), 0644) {
facades.Log().Error("[面板][CronController] 更新计划任务脚本失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
tools.ExecShell("dos2unix " + cron.Shell)
c.cron.DeleteFromSystem(cron)
if cron.Status {
c.cron.AddToSystem(cron)
}
Success(ctx, nil)
}
func (c *CronController) Delete(ctx http.Context) {
var cron models.Cron
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
Error(ctx, http.StatusBadRequest, "计划任务不存在")
return
}
c.cron.DeleteFromSystem(cron)
tools.RemoveFile(cron.Shell)
_, err = facades.Orm().Query().Delete(&cron)
if err != nil {
facades.Log().Error("[面板][CronController] 删除计划任务失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
func (c *CronController) Status(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"status": "bool",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
var cron models.Cron
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
Error(ctx, http.StatusBadRequest, "计划任务不存在")
return
}
cron.Status = ctx.Request().InputBool("status")
err = facades.Orm().Query().Save(&cron)
if err != nil {
facades.Log().Error("[面板][CronController] 更新计划任务状态失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
c.cron.DeleteFromSystem(cron)
if cron.Status {
c.cron.AddToSystem(cron)
}
Success(ctx, nil)
}
func (c *CronController) Log(ctx http.Context) {
var cron models.Cron
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
Error(ctx, http.StatusBadRequest, "计划任务不存在")
return
}
if !tools.Exists(cron.Log) {
Error(ctx, http.StatusBadRequest, "日志文件不存在")
return
}
Success(ctx, tools.ReadFile(cron.Log))
}
+18
View File
@@ -0,0 +1,18 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
)
type FileController struct {
// Dependent services
}
func NewFileController() *FileController {
return &FileController{
// Inject services
}
}
func (r *FileController) Index(ctx http.Context) {
}
+73
View File
@@ -0,0 +1,73 @@
package controllers
import (
"sync"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/services"
)
func Success(ctx http.Context, data any) {
ctx.Response().Success().Json(http.Json{
"code": 0,
"message": "success",
"data": data,
})
}
func Error(ctx http.Context, code int, message any) {
ctx.Response().Json(http.StatusOK, http.Json{
"code": code,
"message": message,
})
}
// Check 检查插件是否可用
func Check(ctx http.Context, slug string) bool {
plugin := services.NewPluginImpl().GetBySlug(slug)
installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug)
installedPlugins, err := services.NewPluginImpl().AllInstalled()
if err != nil {
facades.Log().Error("[面板][插件] 获取已安装插件失败")
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return false
}
if installedPlugin.Version != plugin.Version || installedPlugin.Slug != plugin.Slug {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要更新至 "+plugin.Version+" 版本")
return false
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
return false
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
return false
}
}
return true
}
+201
View File
@@ -0,0 +1,201 @@
package controllers
import (
"fmt"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type MenuItem struct {
Name string `json:"name"`
Title string `json:"title"`
Icon string `json:"icon"`
Jump string `json:"jump"`
}
type InfoController struct {
plugin services.Plugin
}
func NewInfoController() *InfoController {
return &InfoController{
plugin: services.NewPluginImpl(),
}
}
func (c *InfoController) Name(ctx http.Context) {
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
}
Success(ctx, http.Json{
"name": setting.Value,
})
}
func (c *InfoController) Menu(ctx http.Context) {
Success(ctx, []MenuItem{
{Name: "home", Title: "主页", Icon: "layui-icon-home", Jump: "/"},
{Name: "website", Title: "网站管理", Icon: "layui-icon-website", Jump: "website/list"},
{Name: "monitor", Title: "资源监控", Icon: "layui-icon-chart-screen", Jump: "monitor"},
{Name: "safe", Title: "系统安全", Icon: "layui-icon-auz", Jump: "safe"},
/*{Name: "file", Title: "文件管理", Icon: "layui-icon-file", Jump: "file"},*/
{Name: "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 (c *InfoController) HomePlugins(ctx http.Context) {
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
}
type pluginsData struct {
models.Plugin
Name string `json:"name"`
}
var pluginsJson []pluginsData
for _, plugin := range plugins {
pluginsJson = append(pluginsJson, pluginsData{
Plugin: plugin,
Name: services.NewPluginImpl().GetBySlug(plugin.Slug).Name,
})
}
Success(ctx, pluginsJson)
}
func (c *InfoController) NowMonitor(ctx http.Context) {
Success(ctx, tools.GetMonitoringInfo())
}
func (c *InfoController) SystemInfo(ctx http.Context) {
monitorInfo := tools.GetMonitoringInfo()
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 (c *InfoController) InstalledDbAndPhp(ctx http.Context) {
var php []models.Plugin
err := facades.Orm().Query().Where("slug like ?", "php%").Find(&php)
if err != nil {
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
var mysql models.Plugin
mysqlInstalled := true
err = facades.Orm().Query().Where("slug like ?", "mysql%").FirstOrFail(&mysql)
if err != nil {
mysqlInstalled = false
}
var postgresql models.Plugin
postgresqlInstalled := true
err = facades.Orm().Query().Where("slug like ?", "postgresql%").FirstOrFail(&postgresql)
if err != nil {
postgresqlInstalled = false
}
type data struct {
Slug string `json:"slug"`
Name string `json:"name"`
}
var phpData []data
phpData = append(phpData, data{Slug: "0", Name: "不使用"})
for _, p := range php {
match := regexp.MustCompile(`php(\d+)`).FindStringSubmatch(p.Slug)
if len(match) == 0 {
continue
}
phpData = append(phpData, data{Slug: strings.ReplaceAll(p.Slug, "php", ""), Name: c.plugin.GetBySlug(p.Slug).Name})
}
Success(ctx, http.Json{
"php": phpData,
"mysql": mysqlInstalled,
"postgresql": postgresqlInstalled,
})
}
func (c *InfoController) CheckUpdate(ctx http.Context) {
version := facades.Config().GetString("panel.version")
remote, err := tools.GetLatestPanelVersion()
if err != nil {
Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
return
}
if version == remote.Version {
Success(ctx, http.Json{
"update": false,
"version": remote.Version,
"name": remote.Name,
"body": remote.Body,
"date": remote.Date,
})
return
}
Success(ctx, http.Json{
"update": true,
"version": remote.Version,
"name": remote.Name,
"body": remote.Body,
"date": remote.Date,
})
}
func (c *InfoController) Update(ctx http.Context) {
var task models.Task
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
if err == nil {
Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止更新")
return
}
proxy := ctx.Request().InputBool("proxy")
err = tools.UpdatePanel(proxy)
if err != nil {
facades.Log().Error("[面板][InfoController] 更新面板失败 ", err.Error())
Error(ctx, http.StatusInternalServerError, "更新失败: "+err.Error())
return
}
Success(ctx, nil)
}
func (c *InfoController) Restart(ctx http.Context) {
var task models.Task
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
if err == nil {
Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止重启")
return
}
tools.ExecShell("systemctl restart panel")
Success(ctx, nil)
}
+176
View File
@@ -0,0 +1,176 @@
package controllers
import (
"fmt"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
)
type MonitorController struct {
setting services.Setting
}
func NewMonitorController() *MonitorController {
return &MonitorController{
setting: services.NewSettingImpl(),
}
}
// Switch 监控开关
func (r *MonitorController) Switch(ctx http.Context) {
value := ctx.Request().InputBool("switch")
err := r.setting.Set(models.SettingKeyMonitor, cast.ToString(value))
if err != nil {
facades.Log().Error("[面板][MonitorController] 更新监控开关失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
// SaveDays 保存监控天数
func (r *MonitorController) SaveDays(ctx http.Context) {
days := ctx.Request().Input("days")
err := r.setting.Set(models.SettingKeyMonitorDays, days)
if err != nil {
facades.Log().Error("[面板][MonitorController] 更新监控天数失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
// SwitchAndDays 监控开关和监控天数
func (r *MonitorController) SwitchAndDays(ctx http.Context) {
monitor := r.setting.Get(models.SettingKeyMonitor)
monitorDays := r.setting.Get(models.SettingKeyMonitorDays)
Success(ctx, http.Json{
"switch": cast.ToBool(monitor),
"days": cast.ToInt(monitorDays),
})
}
// Clear 清空监控数据
func (r *MonitorController) Clear(ctx http.Context) {
_, err := facades.Orm().Query().Where("1 = 1").Delete(&models.Monitor{})
if err != nil {
facades.Log().Error("[面板][MonitorController] 清空监控数据失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
// List 监控数据列表
func (r *MonitorController) List(ctx http.Context) {
start := ctx.Request().InputInt64("start")
end := ctx.Request().InputInt64("end")
startTime := carbon.FromTimestampMilli(start)
endTime := carbon.FromTimestampMilli(end)
var monitors []models.Monitor
err := facades.Orm().Query().Where("created_at >= ?", startTime.ToDateTimeString()).Where("created_at <= ?", endTime.ToDateTimeString()).Get(&monitors)
if err != nil {
facades.Log().Error("[面板][MonitorController] 查询监控数据失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if len(monitors) == 0 {
Error(ctx, http.StatusNotFound, "监控数据为空")
return
}
type load struct {
Load1 []float64 `json:"load1"`
Load5 []float64 `json:"load5"`
Load15 []float64 `json:"load15"`
}
type cpu struct {
Percent []string `json:"percent"`
}
type mem struct {
Total string `json:"total"`
Available []string `json:"available"`
Used []string `json:"used"`
}
type swap struct {
Total string `json:"total"`
Used []string `json:"used"`
Free []string `json:"free"`
}
type network struct {
Sent []string `json:"sent"`
Recv []string `json:"recv"`
Tx []string `json:"tx"`
Rx []string `json:"rx"`
}
type monitorData struct {
Times []string `json:"times"`
Load load `json:"load"`
Cpu cpu `json:"cpu"`
Mem mem `json:"mem"`
Swap swap `json:"swap"`
Net network `json:"net"`
}
var data monitorData
var bytesSent uint64
var bytesRecv uint64
var bytesSent2 uint64
var bytesRecv2 uint64
for _, net := range monitors[0].Info.Net {
if net.Name == "lo" {
continue
}
bytesSent += net.BytesSent
bytesRecv += net.BytesRecv
}
for i, monitor := range monitors {
// 跳过第一条数据,因为第一条数据的流量为 0
if i == 0 {
// MB
data.Mem.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Total)/1024/1024)
data.Swap.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Total)/1024/1024)
continue
}
for _, net := range monitor.Info.Net {
if net.Name == "lo" {
continue
}
bytesSent2 += net.BytesSent
bytesRecv2 += net.BytesRecv
}
data.Times = append(data.Times, monitor.CreatedAt.ToDateTimeString())
data.Load.Load1 = append(data.Load.Load1, monitor.Info.Load.Load1)
data.Load.Load5 = append(data.Load.Load5, monitor.Info.Load.Load5)
data.Load.Load15 = append(data.Load.Load15, monitor.Info.Load.Load15)
data.Cpu.Percent = append(data.Cpu.Percent, fmt.Sprintf("%.2f", monitor.Info.Percent[0]))
data.Mem.Available = append(data.Mem.Available, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Available)/1024/1024))
data.Mem.Used = append(data.Mem.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Used)/1024/1024))
data.Swap.Used = append(data.Swap.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024))
data.Swap.Free = append(data.Swap.Free, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Free)/1024/1024))
data.Net.Sent = append(data.Net.Sent, fmt.Sprintf("%.2f", float64(bytesSent2/1024/1024)))
data.Net.Recv = append(data.Net.Recv, fmt.Sprintf("%.2f", float64(bytesRecv2/1024/1024)))
// 监控频率为 1 分钟,所以这里除以 60 即可得到每秒的流量
data.Net.Tx = append(data.Net.Tx, fmt.Sprintf("%.2f", float64(bytesSent2-bytesSent)/60/1024/1024))
data.Net.Rx = append(data.Net.Rx, fmt.Sprintf("%.2f", float64(bytesRecv2-bytesRecv)/60/1024/1024))
bytesSent = bytesSent2
bytesRecv = bytesRecv2
bytesSent2 = 0
bytesRecv2 = 0
}
Success(ctx, data)
}
+285
View File
@@ -0,0 +1,285 @@
package controllers
import (
"sync"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/models"
"panel/app/services"
)
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) {
plugins := r.plugin.All()
installedPlugins, err := r.plugin.AllInstalled()
if err != nil {
Error(ctx, http.StatusInternalServerError, "系统内部错误")
}
var lock sync.RWMutex
installedPluginsMap := make(map[string]models.Plugin)
for _, p := range installedPlugins {
lock.Lock()
installedPluginsMap[p.Slug] = p
lock.Unlock()
}
type plugin struct {
Name string `json:"name"`
Description string `json:"description"`
Slug string `json:"slug"`
Version string `json:"version"`
Requires []string `json:"requires"`
Excludes []string `json:"excludes"`
Installed bool `json:"installed"`
InstalledVersion string `json:"installed_version"`
Show bool `json:"show"`
}
var p []plugin
for _, item := range plugins {
installed, installedVersion, show := false, "", false
if _, ok := installedPluginsMap[item.Slug]; ok {
installed = true
installedVersion = installedPluginsMap[item.Slug].Version
show = installedPluginsMap[item.Slug].Show
}
p = append(p, plugin{
Name: item.Name,
Description: item.Description,
Slug: item.Slug,
Version: item.Version,
Requires: item.Requires,
Excludes: item.Excludes,
Installed: installed,
InstalledVersion: installedVersion,
Show: show,
})
}
Success(ctx, p)
}
// Install 安装插件
func (r *PluginController) Install(ctx http.Context) {
slug := ctx.Request().Input("slug")
plugin := r.plugin.GetBySlug(slug)
installedPlugin := r.plugin.GetInstalledBySlug(slug)
installedPlugins, err := r.plugin.AllInstalled()
if err != nil {
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if installedPlugin.ID != 0 {
Error(ctx, http.StatusBadRequest, "插件已安装")
return
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
return
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
return
}
}
var task models.Task
task.Name = "安装插件 " + plugin.Name
task.Status = models.TaskStatusWaiting
task.Shell = 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
}
r.task.Process(task.ID)
Success(ctx, "任务已提交")
}
// Uninstall 卸载插件
func (r *PluginController) Uninstall(ctx http.Context) {
slug := ctx.Request().Input("slug")
plugin := r.plugin.GetBySlug(slug)
installedPlugin := r.plugin.GetInstalledBySlug(slug)
installedPlugins, err := r.plugin.AllInstalled()
if err != nil {
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if installedPlugin.ID == 0 {
Error(ctx, http.StatusBadRequest, "插件未安装")
return
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
return
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
return
}
}
var task models.Task
task.Name = "卸载插件 " + plugin.Name
task.Status = models.TaskStatusWaiting
task.Shell = 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
}
r.task.Process(task.ID)
Success(ctx, "任务已提交")
}
// Update 更新插件
func (r *PluginController) Update(ctx http.Context) {
slug := ctx.Request().Input("slug")
plugin := r.plugin.GetBySlug(slug)
installedPlugin := r.plugin.GetInstalledBySlug(slug)
installedPlugins, err := r.plugin.AllInstalled()
if err != nil {
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if installedPlugin.ID == 0 {
Error(ctx, http.StatusBadRequest, "插件未安装")
return
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
return
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
return
}
}
var task models.Task
task.Name = "更新插件 " + plugin.Name
task.Status = models.TaskStatusWaiting
task.Shell = 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
}
r.task.Process(task.ID)
Success(ctx, "任务已提交")
}
// UpdateShow 更新插件首页显示状态
func (r *PluginController) UpdateShow(ctx http.Context) {
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
}
if plugin.ID == 0 {
Error(ctx, http.StatusBadRequest, "插件未安装")
return
}
plugin.Show = show
if err := facades.Orm().Query().Save(&plugin); err != nil {
facades.Log().Error("[面板][PluginController] 更新插件失败: " + err.Error())
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, "操作成功")
}
@@ -0,0 +1,459 @@
package fail2ban
import (
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Fail2banController struct {
website services.Website
}
func NewFail2banController() *Fail2banController {
return &Fail2banController{
website: services.NewWebsiteImpl(),
}
}
type Jail struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
LogPath string `json:"log_path"`
MaxRetry int `json:"max_retry"`
FindTime int `json:"find_time"`
BanTime int `json:"ban_time"`
}
// Status 获取运行状态
func (c *Fail2banController) Status(ctx http.Context) {
if !controllers.Check(ctx, "fail2ban") {
return
}
status := tools.ExecShell("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Reload 重载配置
func (c *Fail2banController) Reload(ctx http.Context) {
if !controllers.Check(ctx, "fail2ban") {
return
}
tools.ExecShell("systemctl reload fail2ban")
status := tools.ExecShell("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Restart 重启服务
func (c *Fail2banController) Restart(ctx http.Context) {
if !controllers.Check(ctx, "fail2ban") {
return
}
tools.ExecShell("systemctl restart fail2ban")
status := tools.ExecShell("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Start 启动服务
func (c *Fail2banController) Start(ctx http.Context) {
if !controllers.Check(ctx, "fail2ban") {
return
}
tools.ExecShell("systemctl start fail2ban")
status := tools.ExecShell("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Stop 停止服务
func (c *Fail2banController) Stop(ctx http.Context) {
if !controllers.Check(ctx, "fail2ban") {
return
}
tools.ExecShell("systemctl stop fail2ban")
status := tools.ExecShell("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
return
}
if status != "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// List 所有 Fail2ban 规则
func (c *Fail2banController) List(ctx http.Context) {
if !controllers.Check(ctx, "fail2ban") {
return
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
raw := tools.ReadFile("/etc/fail2ban/jail.local")
if len(raw) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "Fail2ban 规则为空")
return
}
jailList := regexp.MustCompile(`\[(.*?)]`).FindAllStringSubmatch(raw, -1)
if len(jailList) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "Fail2ban 规则为空")
return
}
var jails []Jail
for i, jail := range jailList {
if i == 0 {
continue
}
jailName := jail[1]
jailRaw := tools.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
if len(jailRaw) == 0 {
continue
}
jailEnabled := strings.Contains(jailRaw, "enabled = true")
jailLogPath := regexp.MustCompile(`logpath = (.*)`).FindStringSubmatch(jailRaw)
jailMaxRetry := regexp.MustCompile(`maxretry = (.*)`).FindStringSubmatch(jailRaw)
jailFindTime := regexp.MustCompile(`findtime = (.*)`).FindStringSubmatch(jailRaw)
jailBanTime := regexp.MustCompile(`bantime = (.*)`).FindStringSubmatch(jailRaw)
jails = append(jails, Jail{
Name: jailName,
Enabled: jailEnabled,
LogPath: jailLogPath[1],
MaxRetry: cast.ToInt(jailMaxRetry[1]),
FindTime: cast.ToInt(jailFindTime[1]),
BanTime: cast.ToInt(jailBanTime[1]),
})
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(jails) {
controllers.Success(ctx, http.Json{
"total": 0,
"items": []Jail{},
})
return
}
if endIndex > len(jails) {
endIndex = len(jails)
}
pagedJails := jails[startIndex:endIndex]
controllers.Success(ctx, http.Json{
"total": len(jails),
"items": pagedJails,
})
}
// Add 添加 Fail2ban 规则
func (c *Fail2banController) Add(ctx http.Context) {
if !controllers.Check(ctx, "fail2ban") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"name": "required",
"type": "required|in:website,service",
"maxretry": "required",
"findtime": "required",
"bantime": "required",
"website_mode": "required_if:type,website",
"website_path": "required_if:type,website",
})
if err != nil {
controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
return
}
jailName := ctx.Request().Input("name")
jailType := ctx.Request().Input("type")
jailMaxRetry := ctx.Request().Input("maxretry")
jailFindTime := ctx.Request().Input("findtime")
jailBanTime := ctx.Request().Input("bantime")
jailWebsiteMode := ctx.Request().Input("website_mode")
jailWebsitePath := ctx.Request().Input("website_path")
raw := tools.ReadFile("/etc/fail2ban/jail.local")
if strings.Contains(raw, "["+jailName+"]") || (strings.Contains(raw, "["+jailName+"]"+"-cc") && jailWebsiteMode == "cc") || (strings.Contains(raw, "["+jailName+"]"+"-path") && jailWebsiteMode == "path") {
controllers.Error(ctx, http.StatusUnprocessableEntity, "规则已存在")
return
}
switch jailType {
case "website":
var website models.Website
err := facades.Orm().Query().Where("name", jailName).FirstOrFail(&website)
if err != nil {
controllers.Error(ctx, http.StatusUnprocessableEntity, "网站不存在")
return
}
config, err := c.website.GetConfig(int(website.ID))
if err != nil {
controllers.Error(ctx, http.StatusUnprocessableEntity, "获取网站配置失败")
return
}
var ports string
for _, port := range config.Ports {
if len(strings.Split(port, " ")) > 1 {
ports += strings.Split(port, " ")[0] + ","
} else {
ports += port + ","
}
}
rule := `
# ` + jailName + `-` + jailWebsiteMode + `-START
[` + jailName + `-` + jailWebsiteMode + `]
enabled = true
filter = haozi-` + jailName + `-` + jailWebsiteMode + `
port = ` + ports + `
maxretry = ` + jailMaxRetry + `
findtime = ` + jailFindTime + `
bantime = ` + jailBanTime + `
action = %(action_mwl)s
logpath = /www/wwwlogs/` + website.Name + `.log
# ` + jailName + `-` + jailWebsiteMode + `-END
`
raw += rule
tools.WriteFile("/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.WriteFile("/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.ExecShell("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
case "mysql":
logPath = "/www/server/mysql/mysql-error.log"
filter = "mysqld-auth"
port = tools.ExecShell("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.ExecShell(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
default:
controllers.Error(ctx, http.StatusUnprocessableEntity, "未知服务")
return
}
if len(port) == 0 {
controllers.Error(ctx, http.StatusUnprocessableEntity, "获取服务端口失败,请检查是否安装")
return
}
rule := `
# ` + jailName + `-START
[` + jailName + `]
enabled = true
filter = ` + filter + `
port = ` + port + `
maxretry = ` + jailMaxRetry + `
findtime = ` + jailFindTime + `
bantime = ` + jailBanTime + `
action = %(action_mwl)s
logpath = ` + logPath + `
# ` + jailName + `-END
`
raw += rule
tools.WriteFile("/etc/fail2ban/jail.local", raw, 0644)
}
tools.ExecShell("fail2ban-client reload")
controllers.Success(ctx, nil)
}
// Delete 删除规则
func (c *Fail2banController) Delete(ctx http.Context) {
if !controllers.Check(ctx, "fail2ban") {
return
}
jailName := ctx.Request().Input("name")
raw := tools.ReadFile("/etc/fail2ban/jail.local")
if !strings.Contains(raw, "["+jailName+"]") {
controllers.Error(ctx, http.StatusUnprocessableEntity, "规则不存在")
return
}
rule := tools.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
raw = strings.Replace(raw, "\n# "+jailName+"-START"+rule+"# "+jailName+"-END", "", -1)
raw = strings.TrimSpace(raw)
tools.WriteFile("/etc/fail2ban/jail.local", raw, 0644)
tools.ExecShell("fail2ban-client reload")
controllers.Success(ctx, nil)
}
// BanList 获取封禁列表
func (c *Fail2banController) BanList(ctx http.Context) {
if !controllers.Check(ctx, "fail2ban") {
return
}
name := ctx.Request().Query("name")
if len(name) == 0 {
controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
return
}
currentlyBan := tools.ExecShell(`fail2ban-client status ` + name + ` | grep "Currently banned" | awk '{print $4}'`)
totalBan := tools.ExecShell(`fail2ban-client status ` + name + ` | grep "Total banned" | awk '{print $4}'`)
bannedIp := tools.ExecShell(`fail2ban-client status ` + name + ` | grep "Banned IP list" | awk -F ":" '{print $2}'`)
bannedIpList := strings.Split(bannedIp, " ")
var list []map[string]string
for _, ip := range bannedIpList {
if len(ip) > 0 {
list = append(list, map[string]string{
"name": name,
"ip": ip,
})
}
}
controllers.Success(ctx, http.Json{
"currentlyBan": currentlyBan,
"totalBan": totalBan,
"bannedIpList": list,
})
}
// Unban 解封
func (c *Fail2banController) Unban(ctx http.Context) {
if !controllers.Check(ctx, "fail2ban") {
return
}
name := ctx.Request().Input("name")
ip := ctx.Request().Input("ip")
if len(name) == 0 || len(ip) == 0 {
controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
return
}
tools.ExecShell("fail2ban-client set " + name + " unbanip " + ip)
controllers.Success(ctx, nil)
}
// SetWhiteList 设置白名单
func (c *Fail2banController) SetWhiteList(ctx http.Context) {
if !controllers.Check(ctx, "fail2ban") {
return
}
ip := ctx.Request().Input("ip")
if len(ip) == 0 {
controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
return
}
raw := tools.ReadFile("/etc/fail2ban/jail.local")
// 正则替换
reg := regexp.MustCompile(`ignoreip\s*=\s*.*\n`)
if reg.MatchString(raw) {
raw = reg.ReplaceAllString(raw, "ignoreip = "+ip+"\n")
} else {
controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
return
}
tools.WriteFile("/etc/fail2ban/jail.local", raw, 0644)
tools.ExecShell("fail2ban-client reload")
controllers.Success(ctx, nil)
}
// GetWhiteList 获取白名单
func (c *Fail2banController) GetWhiteList(ctx http.Context) {
if !controllers.Check(ctx, "fail2ban") {
return
}
raw := tools.ReadFile("/etc/fail2ban/jail.local")
reg := regexp.MustCompile(`ignoreip\s*=\s*(.*)\n`)
if reg.MatchString(raw) {
ignoreIp := reg.FindStringSubmatch(raw)[1]
controllers.Success(ctx, ignoreIp)
} else {
controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
}
}
@@ -0,0 +1,781 @@
package mysql57
import (
"database/sql"
"fmt"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Mysql57Controller struct {
setting services.Setting
backup services.Backup
}
func NewMysql57Controller() *Mysql57Controller {
return &Mysql57Controller{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// Status 获取运行状态
func (c *Mysql57Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Reload 重载配置
func (c *Mysql57Controller) Reload(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
tools.ExecShell("systemctl reload mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Restart 重启服务
func (c *Mysql57Controller) Restart(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
tools.ExecShell("systemctl restart mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Start 启动服务
func (c *Mysql57Controller) Start(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
tools.ExecShell("systemctl start mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Stop 停止服务
func (c *Mysql57Controller) Stop(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
tools.ExecShell("systemctl stop mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status != "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// GetConfig 获取配置
func (c *Mysql57Controller) GetConfig(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
// 获取配置
config := tools.ReadFile("/www/server/mysql/conf/my.cnf")
if len(config) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
return
}
controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (c *Mysql57Controller) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
config := ctx.Request().Input("config")
if len(config) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
return
}
if !tools.WriteFile("/www/server/mysql/conf/my.cnf", config, 0644) {
controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
return
}
controllers.Success(ctx, "保存MySQL配置成功")
}
// Load 获取负载
func (c *Mysql57Controller) Load(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
return
}
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if status != "active" {
controllers.Error(ctx, http.StatusInternalServerError, "MySQL 已停止运行")
return
}
raw := tools.ExecShell("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
if strings.Contains(raw, "Access denied for user") {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码错误")
return
}
if !strings.Contains(raw, "Uptime") {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
return
}
data := make(map[int]map[string]string)
expressions := []struct {
regex string
name string
}{
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
}
for i, expression := range expressions {
re := regexp.MustCompile(expression.regex)
matches := re.FindStringSubmatch(raw)
if len(matches) > 1 {
data[i] = make(map[string]string)
data[i] = map[string]string{"name": expression.name, "value": matches[1]}
if expression.name == "发送" || expression.name == "接收" {
data[i]["value"] = tools.FormatBytes(cast.ToFloat64(matches[1]))
}
}
}
// 索引命中率
readRequests := cast.ToFloat64(data[9]["value"])
reads := cast.ToFloat64(data[10]["value"])
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
// Innodb 索引命中率
bufferPoolReads := cast.ToFloat64(data[11]["value"])
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
controllers.Success(ctx, data)
}
// ErrorLog 获取错误日志
func (c *Mysql57Controller) ErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
log := tools.ExecShell("tail -n 100 /www/server/mysql/mysql-error.log")
controllers.Success(ctx, log)
}
// ClearErrorLog 清空错误日志
func (c *Mysql57Controller) ClearErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
tools.ExecShell("echo '' > /www/server/mysql/mysql-error.log")
controllers.Success(ctx, "清空错误日志成功")
}
// SlowLog 获取慢查询日志
func (c *Mysql57Controller) SlowLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
log := tools.ExecShell("tail -n 100 /www/server/mysql/mysql-slow.log")
controllers.Success(ctx, log)
}
// ClearSlowLog 清空慢查询日志
func (c *Mysql57Controller) ClearSlowLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
tools.ExecShell("echo '' > /www/server/mysql/mysql-slow.log")
controllers.Success(ctx, "清空慢查询日志成功")
}
// GetRootPassword 获取root密码
func (c *Mysql57Controller) GetRootPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
return
}
controllers.Success(ctx, rootPassword)
}
// SetRootPassword 设置root密码
func (c *Mysql57Controller) SetRootPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status != "active" {
controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
return
}
rootPassword := ctx.Request().Input(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码不能为空")
return
}
oldRootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
if oldRootPassword != rootPassword {
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\"")
err := c.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
if err != nil {
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
return
}
}
controllers.Success(ctx, "设置root密码成功")
}
// DatabaseList 获取数据库列表
func (c *Mysql57Controller) DatabaseList(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
type database struct {
Name string `json:"name"`
}
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
facades.Log().Error("[MySQL57] 连接数据库失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
return
}
defer db.Close()
rows, err := db.Query("SHOW DATABASES")
if err != nil {
facades.Log().Error("[MySQL57] 获取数据库列表失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
return
}
defer rows.Close()
var databases []database
for rows.Next() {
var d database
err := rows.Scan(&d.Name)
if err != nil {
continue
}
databases = append(databases, d)
}
if err := rows.Err(); err != nil {
facades.Log().Error("[MySQL57] 获取数据库列表失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
return
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(databases) {
controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
return
}
if endIndex > len(databases) {
endIndex = len(databases)
}
pagedDatabases := databases[startIndex:endIndex]
controllers.Success(ctx, http.Json{
"total": len(databases),
"items": pagedDatabases,
})
}
// AddDatabase 添加数据库
func (c *Mysql57Controller) AddDatabase(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "添加数据库成功")
}
// DeleteDatabase 删除数据库
func (c *Mysql57Controller) DeleteDatabase(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\"")
controllers.Success(ctx, "删除数据库成功")
}
// BackupList 获取备份列表
func (c *Mysql57Controller) BackupList(ctx http.Context) {
backupList, err := c.backup.MysqlList()
if err != nil {
facades.Log().Error("[MySQL57] 获取备份列表失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
return
}
controllers.Success(ctx, backupList)
}
// UploadBackup 上传备份
func (c *Mysql57Controller) UploadBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
file, err := ctx.Request().File("file")
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
controllers.Success(ctx, "上传文件成功")
}
// CreateBackup 创建备份
func (c *Mysql57Controller) CreateBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
database := ctx.Request().Input("database")
err = c.backup.MysqlBackup(database)
if err != nil {
facades.Log().Error("[MYSQL57] 创建备份失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
return
}
controllers.Success(ctx, "备份成功")
}
// DeleteBackup 删除备份
func (c *Mysql57Controller) DeleteBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
fileName := ctx.Request().Input("name")
tools.RemoveFile(backupPath + "/" + fileName)
controllers.Success(ctx, "删除备份成功")
}
// RestoreBackup 还原备份
func (c *Mysql57Controller) RestoreBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
err = c.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
if err != nil {
facades.Log().Error("[MYSQL57] 还原失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
return
}
controllers.Success(ctx, "还原成功")
}
// UserList 用户列表
func (c *Mysql57Controller) UserList(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
type user struct {
User string `json:"user"`
Host string `json:"host"`
Grants []string `json:"grants"`
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
facades.Log().Error("[MYSQL57] 连接数据库失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
}
defer db.Close()
rows, err := db.Query("SELECT user, host FROM mysql.user")
if err != nil {
facades.Log().Error("[MYSQL57] 查询数据库失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "查询数据库失败")
}
defer rows.Close()
var userGrants []user
for rows.Next() {
var u user
err := rows.Scan(&u.User, &u.Host)
if err != nil {
continue
}
// 查询用户权限
grantsRows, err := db.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", u.User, u.Host))
if err != nil {
continue
}
defer grantsRows.Close()
for grantsRows.Next() {
var grant string
err := grantsRows.Scan(&grant)
if err != nil {
continue
}
u.Grants = append(u.Grants, grant)
}
if err := grantsRows.Err(); err != nil {
continue
}
userGrants = append(userGrants, u)
}
if err := rows.Err(); err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
return
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(userGrants) {
controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
})
return
}
if endIndex > len(userGrants) {
endIndex = len(userGrants)
}
pagedUserGrants := userGrants[startIndex:endIndex]
controllers.Success(ctx, http.Json{
"total": len(userGrants),
"items": pagedUserGrants,
})
}
// AddUser 添加用户
func (c *Mysql57Controller) AddUser(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "添加成功")
}
// DeleteUser 删除用户
func (c *Mysql57Controller) DeleteUser(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\"")
controllers.Success(ctx, "删除成功")
}
// SetUserPassword 设置用户密码
func (c *Mysql57Controller) SetUserPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "修改成功")
}
// SetUserPrivileges 设置用户权限
func (c *Mysql57Controller) SetUserPrivileges(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"database": "required|min_len:1|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
database := ctx.Request().Input("database")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "修改成功")
}
@@ -0,0 +1,781 @@
package mysql80
import (
"database/sql"
"fmt"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Mysql80Controller struct {
setting services.Setting
backup services.Backup
}
func NewMysql80Controller() *Mysql80Controller {
return &Mysql80Controller{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// Status 获取运行状态
func (c *Mysql80Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Reload 重载配置
func (c *Mysql80Controller) Reload(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
tools.ExecShell("systemctl reload mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Restart 重启服务
func (c *Mysql80Controller) Restart(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
tools.ExecShell("systemctl restart mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Start 启动服务
func (c *Mysql80Controller) Start(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
tools.ExecShell("systemctl start mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Stop 停止服务
func (c *Mysql80Controller) Stop(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
tools.ExecShell("systemctl stop mysqld")
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status != "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// GetConfig 获取配置
func (c *Mysql80Controller) GetConfig(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
// 获取配置
config := tools.ReadFile("/www/server/mysql/conf/my.cnf")
if len(config) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
return
}
controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (c *Mysql80Controller) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
config := ctx.Request().Input("config")
if len(config) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
return
}
if !tools.WriteFile("/www/server/mysql/conf/my.cnf", config, 0644) {
controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
return
}
controllers.Success(ctx, "保存MySQL配置成功")
}
// Load 获取负载
func (c *Mysql80Controller) Load(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
return
}
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if status != "active" {
controllers.Error(ctx, http.StatusInternalServerError, "MySQL 已停止运行")
return
}
raw := tools.ExecShell("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
if strings.Contains(raw, "Access denied for user") {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码错误")
return
}
if !strings.Contains(raw, "Uptime") {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
return
}
data := make(map[int]map[string]string)
expressions := []struct {
regex string
name string
}{
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
}
for i, expression := range expressions {
re := regexp.MustCompile(expression.regex)
matches := re.FindStringSubmatch(raw)
if len(matches) > 1 {
data[i] = make(map[string]string)
data[i] = map[string]string{"name": expression.name, "value": matches[1]}
if expression.name == "发送" || expression.name == "接收" {
data[i]["value"] = tools.FormatBytes(cast.ToFloat64(matches[1]))
}
}
}
// 索引命中率
readRequests := cast.ToFloat64(data[9]["value"])
reads := cast.ToFloat64(data[10]["value"])
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
// Innodb 索引命中率
bufferPoolReads := cast.ToFloat64(data[11]["value"])
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
controllers.Success(ctx, data)
}
// ErrorLog 获取错误日志
func (c *Mysql80Controller) ErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
log := tools.ExecShell("tail -n 100 /www/server/mysql/mysql-error.log")
controllers.Success(ctx, log)
}
// ClearErrorLog 清空错误日志
func (c *Mysql80Controller) ClearErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
tools.ExecShell("echo '' > /www/server/mysql/mysql-error.log")
controllers.Success(ctx, "清空错误日志成功")
}
// SlowLog 获取慢查询日志
func (c *Mysql80Controller) SlowLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
log := tools.ExecShell("tail -n 100 /www/server/mysql/mysql-slow.log")
controllers.Success(ctx, log)
}
// ClearSlowLog 清空慢查询日志
func (c *Mysql80Controller) ClearSlowLog(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
tools.ExecShell("echo '' > /www/server/mysql/mysql-slow.log")
controllers.Success(ctx, "清空慢查询日志成功")
}
// GetRootPassword 获取root密码
func (c *Mysql80Controller) GetRootPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
return
}
controllers.Success(ctx, rootPassword)
}
// SetRootPassword 设置root密码
func (c *Mysql80Controller) SetRootPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
status := tools.ExecShell("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
return
}
if status != "active" {
controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
return
}
rootPassword := ctx.Request().Input(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码不能为空")
return
}
oldRootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
if oldRootPassword != rootPassword {
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\"")
err := c.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
if err != nil {
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
return
}
}
controllers.Success(ctx, "设置root密码成功")
}
// DatabaseList 获取数据库列表
func (c *Mysql80Controller) DatabaseList(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
type database struct {
Name string `json:"name"`
}
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
facades.Log().Error("[MySQL80] 连接数据库失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
return
}
defer db.Close()
rows, err := db.Query("SHOW DATABASES")
if err != nil {
facades.Log().Error("[MySQL80] 获取数据库列表失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
return
}
defer rows.Close()
var databases []database
for rows.Next() {
var d database
err := rows.Scan(&d.Name)
if err != nil {
continue
}
databases = append(databases, d)
}
if err := rows.Err(); err != nil {
facades.Log().Error("[MySQL80] 获取数据库列表失败" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
return
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(databases) {
controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
return
}
if endIndex > len(databases) {
endIndex = len(databases)
}
pagedDatabases := databases[startIndex:endIndex]
controllers.Success(ctx, http.Json{
"total": len(databases),
"items": pagedDatabases,
})
}
// AddDatabase 添加数据库
func (c *Mysql80Controller) AddDatabase(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "添加数据库成功")
}
// DeleteDatabase 删除数据库
func (c *Mysql80Controller) DeleteDatabase(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\"")
controllers.Success(ctx, "删除数据库成功")
}
// BackupList 获取备份列表
func (c *Mysql80Controller) BackupList(ctx http.Context) {
backupList, err := c.backup.MysqlList()
if err != nil {
facades.Log().Error("[MySQL80] 获取备份列表失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
return
}
controllers.Success(ctx, backupList)
}
// UploadBackup 上传备份
func (c *Mysql80Controller) UploadBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
file, err := ctx.Request().File("file")
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
controllers.Success(ctx, "上传文件成功")
}
// CreateBackup 创建备份
func (c *Mysql80Controller) CreateBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
database := ctx.Request().Input("database")
err = c.backup.MysqlBackup(database)
if err != nil {
facades.Log().Error("[MYSQL80] 创建备份失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
return
}
controllers.Success(ctx, "备份成功")
}
// DeleteBackup 删除备份
func (c *Mysql80Controller) DeleteBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
fileName := ctx.Request().Input("name")
tools.RemoveFile(backupPath + "/" + fileName)
controllers.Success(ctx, "删除备份成功")
}
// RestoreBackup 还原备份
func (c *Mysql80Controller) RestoreBackup(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
err = c.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
if err != nil {
facades.Log().Error("[MYSQL80] 还原失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
return
}
controllers.Success(ctx, "还原成功")
}
// UserList 用户列表
func (c *Mysql80Controller) UserList(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
type user struct {
User string `json:"user"`
Host string `json:"host"`
Grants []string `json:"grants"`
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
facades.Log().Error("[MYSQL80] 连接数据库失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
}
defer db.Close()
rows, err := db.Query("SELECT user, host FROM mysql.user")
if err != nil {
facades.Log().Error("[MYSQL80] 查询数据库失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "查询数据库失败")
}
defer rows.Close()
var userGrants []user
for rows.Next() {
var u user
err := rows.Scan(&u.User, &u.Host)
if err != nil {
continue
}
// 查询用户权限
grantsRows, err := db.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", u.User, u.Host))
if err != nil {
continue
}
defer grantsRows.Close()
for grantsRows.Next() {
var grant string
err := grantsRows.Scan(&grant)
if err != nil {
continue
}
u.Grants = append(u.Grants, grant)
}
if err := grantsRows.Err(); err != nil {
continue
}
userGrants = append(userGrants, u)
}
if err := rows.Err(); err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
return
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(userGrants) {
controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
})
return
}
if endIndex > len(userGrants) {
endIndex = len(userGrants)
}
pagedUserGrants := userGrants[startIndex:endIndex]
controllers.Success(ctx, http.Json{
"total": len(userGrants),
"items": pagedUserGrants,
})
}
// AddUser 添加用户
func (c *Mysql80Controller) AddUser(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "添加成功")
}
// DeleteUser 删除用户
func (c *Mysql80Controller) DeleteUser(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\"")
controllers.Success(ctx, "删除成功")
}
// SetUserPassword 设置用户密码
func (c *Mysql80Controller) SetUserPassword(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "修改成功")
}
// SetUserPrivileges 设置用户权限
func (c *Mysql80Controller) SetUserPrivileges(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"database": "required|min_len:1|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
database := ctx.Request().Input("database")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
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 (r *OpenRestyController) Status(ctx http.Context) {
if !controllers.Check(ctx, "openresty") {
return
}
status := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Reload 重载配置
func (r *OpenRestyController) Reload(ctx http.Context) {
if !controllers.Check(ctx, "openresty") {
return
}
tools.ExecShell("systemctl reload openresty")
status := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
}
if status == "active" {
controllers.Success(ctx, "重载OpenResty成功")
} else {
controllers.Error(ctx, 1, "重载OpenResty失败: "+status)
}
}
// Start 启动OpenResty
func (r *OpenRestyController) Start(ctx http.Context) {
if !controllers.Check(ctx, "openresty") {
return
}
tools.ExecShell("systemctl start openresty")
status := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
}
if status == "active" {
controllers.Success(ctx, "启动OpenResty成功")
} else {
controllers.Error(ctx, 1, "启动OpenResty失败: "+status)
}
}
// Stop 停止OpenResty
func (r *OpenRestyController) Stop(ctx http.Context) {
if !controllers.Check(ctx, "openresty") {
return
}
tools.ExecShell("systemctl stop openresty")
status := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
}
if status != "active" {
controllers.Success(ctx, "停止OpenResty成功")
} else {
controllers.Error(ctx, 1, "停止OpenResty失败: "+status)
}
}
// Restart 重启OpenResty
func (r *OpenRestyController) Restart(ctx http.Context) {
if !controllers.Check(ctx, "openresty") {
return
}
tools.ExecShell("systemctl restart openresty")
status := tools.ExecShell("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
return
}
if status == "active" {
controllers.Success(ctx, "重启OpenResty成功")
} else {
controllers.Error(ctx, 1, "重启OpenResty失败: "+status)
}
}
// GetConfig 获取配置
func (r *OpenRestyController) GetConfig(ctx http.Context) {
if !controllers.Check(ctx, "openresty") {
return
}
config := tools.ReadFile("/www/server/openresty/conf/nginx.conf")
if len(config) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty配置失败")
return
}
controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *OpenRestyController) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "openresty") {
return
}
config := ctx.Request().Input("config")
if len(config) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "配置不能为空")
return
}
if !tools.WriteFile("/www/server/openresty/conf/nginx.conf", config, 0644) {
controllers.Error(ctx, http.StatusInternalServerError, "保存OpenResty配置失败")
return
}
controllers.Success(ctx, "保存OpenResty配置成功")
}
// ErrorLog 获取错误日志
func (r *OpenRestyController) ErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "openresty") {
return
}
if !tools.Exists("/www/wwwlogs/nginx_error.log") {
controllers.Success(ctx, "")
return
}
out := tools.ExecShell("tail -n 100 /www/wwwlogs/nginx_error.log")
controllers.Success(ctx, out)
}
// ClearErrorLog 清空错误日志
func (r *OpenRestyController) ClearErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "openresty") {
return
}
tools.ExecShell("echo '' > /www/wwwlogs/nginx_error.log")
controllers.Success(ctx, "清空OpenResty错误日志成功")
}
// Load 获取负载
func (r *OpenRestyController) Load(ctx http.Context) {
if !controllers.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()
type nginxStatus struct {
Name string `json:"name"`
Value string `json:"value"`
}
var data []nginxStatus
workers := tools.ExecShell("ps aux | grep nginx | grep 'worker process' | wc -l")
data = append(data, nginxStatus{
Name: "工作进程",
Value: workers,
})
out := tools.ExecShell("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
mem := tools.FormatBytes(cast.ToFloat64(out))
data = append(data, nginxStatus{
Name: "内存占用",
Value: mem,
})
match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 2 {
data = 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],
})
}
controllers.Success(ctx, data)
}
@@ -0,0 +1,371 @@
package php74
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/imroc/req/v3"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Php74Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp74Controller() *Php74Controller {
return &Php74Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "74",
}
}
func (c *Php74Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
status := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php74Controller) Reload(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl reload php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php74Controller) Start(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl start php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php74Controller) Stop(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl stop php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status != "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php74Controller) Restart(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl restart php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php74Controller) GetConfig(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
config := tools.ReadFile("/www/server/php/" + c.version + "/etc/php.ini")
controllers.Success(ctx, config)
}
func (c *Php74Controller) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
config := ctx.Request().Input("config")
tools.WriteFile("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
c.Reload(ctx)
}
func (c *Php74Controller) Load(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
client := req.C().SetTimeout(10 * time.Second)
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + c.version)
if err != nil || !resp.IsSuccessState() {
facades.Log().Error("获取PHP-" + c.version + "运行状态失败")
controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
return
}
raw := resp.String()
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
type Data struct {
Name string `json:"name"`
Value string `json:"value"`
}
data := make([]Data, len(dataKeys))
for i := range dataKeys {
data[i].Name = dataKeys[i]
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
match := r.FindStringSubmatch(raw)
if len(match) > 1 {
data[i].Value = strings.TrimSpace(match[1])
}
}
controllers.Success(ctx, data)
}
func (c *Php74Controller) ErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
log := tools.ExecShell("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log")
controllers.Success(ctx, log)
}
func (c *Php74Controller) SlowLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
log := tools.ExecShell("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log")
controllers.Success(ctx, log)
}
func (c *Php74Controller) ClearErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
controllers.Success(ctx, true)
}
func (c *Php74Controller) ClearSlowLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
controllers.Success(ctx, true)
}
type Extension struct {
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Installed bool `json:"installed"`
}
func (c *Php74Controller) GetExtensionList(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
extensions := c.GetExtensions()
controllers.Success(ctx, extensions)
}
func (c *Php74Controller) InstallExtension(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "参数错误")
return
}
extensions := c.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if item.Installed {
controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
return
}
var task models.Task
task.Name = "安装PHP-" + c.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[PHP-" + c.version + "] 创建安装拓展任务失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
c.task.Process(task.ID)
controllers.Success(ctx, true)
return
}
}
controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
}
func (c *Php74Controller) UninstallExtension(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "参数错误")
return
}
extensions := c.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if !item.Installed {
controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
return
}
var task models.Task
task.Name = "卸载PHP-" + c.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[PHP-" + c.version + "] 创建卸载拓展任务失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
c.task.Process(task.ID)
controllers.Success(ctx, true)
return
}
}
controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
}
func (c *Php74Controller) GetExtensions() []Extension {
var extensions []Extension
extensions = append(extensions, Extension{
Name: "OPcache",
Slug: "Zend OPcache",
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "PhpRedis",
Slug: "redis",
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "ImageMagick",
Slug: "imagick",
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "Exif",
Slug: "exif",
Description: "通过 exif 扩展,你可以操作图像元数据。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "pdo_pgsql",
Slug: "pdo_pgsql",
Description: "(需先安装PostgreSQLpdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "ionCube",
Slug: "ionCube Loader",
Description: "ionCube 是一个专业级的PHP加密解密工具。",
Installed: false,
})
raw := tools.ExecShell("/www/server/php/" + c.version + "/bin/php -m")
rawExtensionList := strings.Split(raw, "\n")
for _, item := range rawExtensionList {
if !strings.Contains(item, "[") && item != "" {
for i := range extensions {
if extensions[i].Slug == item {
extensions[i].Installed = true
}
}
}
}
return extensions
}
@@ -0,0 +1,371 @@
package php80
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/imroc/req/v3"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Php80Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp80Controller() *Php80Controller {
return &Php80Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "80",
}
}
func (c *Php80Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
status := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php80Controller) Reload(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl reload php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php80Controller) Start(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl start php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php80Controller) Stop(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl stop php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status != "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php80Controller) Restart(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl restart php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php80Controller) GetConfig(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
config := tools.ReadFile("/www/server/php/" + c.version + "/etc/php.ini")
controllers.Success(ctx, config)
}
func (c *Php80Controller) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
config := ctx.Request().Input("config")
tools.WriteFile("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
c.Reload(ctx)
}
func (c *Php80Controller) Load(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
client := req.C().SetTimeout(10 * time.Second)
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + c.version)
if err != nil || !resp.IsSuccessState() {
facades.Log().Error("获取PHP-" + c.version + "运行状态失败")
controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
return
}
raw := resp.String()
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
type Data struct {
Name string `json:"name"`
Value string `json:"value"`
}
data := make([]Data, len(dataKeys))
for i := range dataKeys {
data[i].Name = dataKeys[i]
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
match := r.FindStringSubmatch(raw)
if len(match) > 1 {
data[i].Value = strings.TrimSpace(match[1])
}
}
controllers.Success(ctx, data)
}
func (c *Php80Controller) ErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
log := tools.ExecShell("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log")
controllers.Success(ctx, log)
}
func (c *Php80Controller) SlowLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
log := tools.ExecShell("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log")
controllers.Success(ctx, log)
}
func (c *Php80Controller) ClearErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
controllers.Success(ctx, true)
}
func (c *Php80Controller) ClearSlowLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
controllers.Success(ctx, true)
}
type Extension struct {
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Installed bool `json:"installed"`
}
func (c *Php80Controller) GetExtensionList(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
extensions := c.GetExtensions()
controllers.Success(ctx, extensions)
}
func (c *Php80Controller) InstallExtension(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "参数错误")
return
}
extensions := c.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if item.Installed {
controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
return
}
var task models.Task
task.Name = "安装PHP-" + c.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[PHP-" + c.version + "] 创建安装拓展任务失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
c.task.Process(task.ID)
controllers.Success(ctx, true)
return
}
}
controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
}
func (c *Php80Controller) UninstallExtension(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "参数错误")
return
}
extensions := c.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if !item.Installed {
controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
return
}
var task models.Task
task.Name = "卸载PHP-" + c.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[PHP-" + c.version + "] 创建卸载拓展任务失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
c.task.Process(task.ID)
controllers.Success(ctx, true)
return
}
}
controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
}
func (c *Php80Controller) GetExtensions() []Extension {
var extensions []Extension
extensions = append(extensions, Extension{
Name: "OPcache",
Slug: "Zend OPcache",
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "PhpRedis",
Slug: "redis",
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "ImageMagick",
Slug: "imagick",
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "Exif",
Slug: "exif",
Description: "通过 exif 扩展,你可以操作图像元数据。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "pdo_pgsql",
Slug: "pdo_pgsql",
Description: "(需先安装PostgreSQLpdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "ionCube",
Slug: "ionCube Loader",
Description: "ionCube 是一个专业级的PHP加密解密工具。",
Installed: false,
})
raw := tools.ExecShell("/www/server/php/" + c.version + "/bin/php -m")
rawExtensionList := strings.Split(raw, "\n")
for _, item := range rawExtensionList {
if !strings.Contains(item, "[") && item != "" {
for i := range extensions {
if extensions[i].Slug == item {
extensions[i].Installed = true
}
}
}
}
return extensions
}
@@ -0,0 +1,371 @@
package php81
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/imroc/req/v3"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Php81Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp81Controller() *Php81Controller {
return &Php81Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "81",
}
}
func (c *Php81Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
status := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php81Controller) Reload(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl reload php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php81Controller) Start(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl start php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php81Controller) Stop(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl stop php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status != "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php81Controller) Restart(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl restart php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php81Controller) GetConfig(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
config := tools.ReadFile("/www/server/php/" + c.version + "/etc/php.ini")
controllers.Success(ctx, config)
}
func (c *Php81Controller) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
config := ctx.Request().Input("config")
tools.WriteFile("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
c.Reload(ctx)
}
func (c *Php81Controller) Load(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
client := req.C().SetTimeout(10 * time.Second)
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + c.version)
if err != nil || !resp.IsSuccessState() {
facades.Log().Error("获取PHP-" + c.version + "运行状态失败")
controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
return
}
raw := resp.String()
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
type Data struct {
Name string `json:"name"`
Value string `json:"value"`
}
data := make([]Data, len(dataKeys))
for i := range dataKeys {
data[i].Name = dataKeys[i]
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
match := r.FindStringSubmatch(raw)
if len(match) > 1 {
data[i].Value = strings.TrimSpace(match[1])
}
}
controllers.Success(ctx, data)
}
func (c *Php81Controller) ErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
log := tools.ExecShell("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log")
controllers.Success(ctx, log)
}
func (c *Php81Controller) SlowLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
log := tools.ExecShell("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log")
controllers.Success(ctx, log)
}
func (c *Php81Controller) ClearErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
controllers.Success(ctx, true)
}
func (c *Php81Controller) ClearSlowLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
controllers.Success(ctx, true)
}
type Extension struct {
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Installed bool `json:"installed"`
}
func (c *Php81Controller) GetExtensionList(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
extensions := c.GetExtensions()
controllers.Success(ctx, extensions)
}
func (c *Php81Controller) InstallExtension(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "参数错误")
return
}
extensions := c.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if item.Installed {
controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
return
}
var task models.Task
task.Name = "安装PHP-" + c.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[PHP-" + c.version + "] 创建安装拓展任务失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
c.task.Process(task.ID)
controllers.Success(ctx, true)
return
}
}
controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
}
func (c *Php81Controller) UninstallExtension(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "参数错误")
return
}
extensions := c.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if !item.Installed {
controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
return
}
var task models.Task
task.Name = "卸载PHP-" + c.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[PHP-" + c.version + "] 创建卸载拓展任务失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
c.task.Process(task.ID)
controllers.Success(ctx, true)
return
}
}
controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
}
func (c *Php81Controller) GetExtensions() []Extension {
var extensions []Extension
extensions = append(extensions, Extension{
Name: "OPcache",
Slug: "Zend OPcache",
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "PhpRedis",
Slug: "redis",
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "ImageMagick",
Slug: "imagick",
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "Exif",
Slug: "exif",
Description: "通过 exif 扩展,你可以操作图像元数据。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "pdo_pgsql",
Slug: "pdo_pgsql",
Description: "(需先安装PostgreSQLpdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "ionCube",
Slug: "ionCube Loader",
Description: "ionCube 是一个专业级的PHP加密解密工具。",
Installed: false,
})
raw := tools.ExecShell("/www/server/php/" + c.version + "/bin/php -m")
rawExtensionList := strings.Split(raw, "\n")
for _, item := range rawExtensionList {
if !strings.Contains(item, "[") && item != "" {
for i := range extensions {
if extensions[i].Slug == item {
extensions[i].Installed = true
}
}
}
}
return extensions
}
@@ -0,0 +1,365 @@
package php82
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/imroc/req/v3"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Php82Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp82Controller() *Php82Controller {
return &Php82Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "82",
}
}
func (c *Php82Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
status := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php82Controller) Reload(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl reload php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php82Controller) Start(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl start php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php82Controller) Stop(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl stop php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status != "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php82Controller) Restart(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("systemctl restart php-fpm-" + c.version)
out := tools.ExecShell("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
status := strings.TrimSpace(out)
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
func (c *Php82Controller) GetConfig(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
config := tools.ReadFile("/www/server/php/" + c.version + "/etc/php.ini")
controllers.Success(ctx, config)
}
func (c *Php82Controller) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
config := ctx.Request().Input("config")
tools.WriteFile("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
c.Reload(ctx)
}
func (c *Php82Controller) Load(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
client := req.C().SetTimeout(10 * time.Second)
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + c.version)
if err != nil || !resp.IsSuccessState() {
facades.Log().Error("获取PHP-" + c.version + "运行状态失败")
controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
return
}
raw := resp.String()
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
type Data struct {
Name string `json:"name"`
Value string `json:"value"`
}
data := make([]Data, len(dataKeys))
for i := range dataKeys {
data[i].Name = dataKeys[i]
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
match := r.FindStringSubmatch(raw)
if len(match) > 1 {
data[i].Value = strings.TrimSpace(match[1])
}
}
controllers.Success(ctx, data)
}
func (c *Php82Controller) ErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
log := tools.ExecShell("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log")
controllers.Success(ctx, log)
}
func (c *Php82Controller) SlowLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
log := tools.ExecShell("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log")
controllers.Success(ctx, log)
}
func (c *Php82Controller) ClearErrorLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
controllers.Success(ctx, true)
}
func (c *Php82Controller) ClearSlowLog(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
tools.ExecShell("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
controllers.Success(ctx, true)
}
type Extension struct {
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Installed bool `json:"installed"`
}
func (c *Php82Controller) GetExtensionList(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
extensions := c.GetExtensions()
controllers.Success(ctx, extensions)
}
func (c *Php82Controller) InstallExtension(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "参数错误")
return
}
extensions := c.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if item.Installed {
controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
return
}
var task models.Task
task.Name = "安装PHP-" + c.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[PHP-" + c.version + "] 创建安装拓展任务失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
c.task.Process(task.ID)
controllers.Success(ctx, true)
return
}
}
controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
}
func (c *Php82Controller) UninstallExtension(ctx http.Context) {
if !controllers.Check(ctx, "php"+c.version) {
return
}
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "参数错误")
return
}
extensions := c.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if !item.Installed {
controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
return
}
var task models.Task
task.Name = "卸载PHP-" + c.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Error("[PHP-" + c.version + "] 创建卸载拓展任务失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
c.task.Process(task.ID)
controllers.Success(ctx, true)
return
}
}
controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
}
func (c *Php82Controller) GetExtensions() []Extension {
var extensions []Extension
extensions = append(extensions, Extension{
Name: "OPcache",
Slug: "Zend OPcache",
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "PhpRedis",
Slug: "redis",
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "ImageMagick",
Slug: "imagick",
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "Exif",
Slug: "exif",
Description: "通过 exif 扩展,你可以操作图像元数据。",
Installed: false,
})
extensions = append(extensions, Extension{
Name: "pdo_pgsql",
Slug: "pdo_pgsql",
Description: "(需先安装PostgreSQLpdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
Installed: false,
})
raw := tools.ExecShell("/www/server/php/" + c.version + "/bin/php -m")
rawExtensionList := strings.Split(raw, "\n")
for _, item := range rawExtensionList {
if !strings.Contains(item, "[") && item != "" {
for i := range extensions {
if extensions[i].Slug == item {
extensions[i].Installed = true
}
}
}
}
return extensions
}
@@ -0,0 +1,81 @@
package phpmyadmin
import (
"os"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"panel/app/http/controllers"
"panel/pkg/tools"
)
type PhpMyAdminController struct {
}
func NewPhpMyAdminController() *PhpMyAdminController {
return &PhpMyAdminController{}
}
func (c *PhpMyAdminController) Info(ctx http.Context) {
if !controllers.Check(ctx, "phpmyadmin") {
return
}
files, err := os.ReadDir("/www/server/phpmyadmin")
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 目录")
return
}
var phpmyadmin string
for _, f := range files {
if strings.HasPrefix(f.Name(), "phpmyadmin_") {
phpmyadmin = f.Name()
}
}
if len(phpmyadmin) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 目录")
return
}
conf := tools.ReadFile("/www/server/vhost/phpmyadmin.conf")
match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf)
if len(match) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 端口")
return
}
controllers.Success(ctx, http.Json{
"phpmyadmin": phpmyadmin,
"port": match[1],
})
}
func (c *PhpMyAdminController) SetPort(ctx http.Context) {
if !controllers.Check(ctx, "phpmyadmin") {
return
}
port := ctx.Request().Input("port")
if len(port) == 0 {
controllers.Error(ctx, http.StatusBadRequest, "端口不能为空")
return
}
conf := tools.ReadFile("/www/server/vhost/phpmyadmin.conf")
conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+port+";")
tools.WriteFile("/www/server/vhost/phpmyadmin.conf", conf, 0644)
if tools.IsRHEL() {
tools.ExecShell("firewall-cmd --zone=public --add-port=" + port + "/tcp --permanent")
tools.ExecShell("firewall-cmd --reload")
} else {
tools.ExecShell("ufw allow " + port + "/tcp")
tools.ExecShell("ufw reload")
}
tools.ExecShell("systemctl reload openresty")
controllers.Success(ctx, nil)
}
@@ -0,0 +1,314 @@
package pureftpd
import (
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"panel/app/http/controllers"
"panel/pkg/tools"
)
type PureFtpdController struct {
}
type User struct {
Username string `json:"username"`
Path string `json:"path"`
}
func NewPureFtpdController() *PureFtpdController {
return &PureFtpdController{}
}
// Status 获取运行状态
func (c *PureFtpdController) Status(ctx http.Context) {
if !controllers.Check(ctx, "pureftpd") {
return
}
status := tools.ExecShell("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Reload 重载配置
func (c *PureFtpdController) Reload(ctx http.Context) {
if !controllers.Check(ctx, "pureftpd") {
return
}
tools.ExecShell("systemctl reload pure-ftpd")
status := tools.ExecShell("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Restart 重启服务
func (c *PureFtpdController) Restart(ctx http.Context) {
if !controllers.Check(ctx, "pureftpd") {
return
}
tools.ExecShell("systemctl restart pure-ftpd")
status := tools.ExecShell("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Start 启动服务
func (c *PureFtpdController) Start(ctx http.Context) {
if !controllers.Check(ctx, "pureftpd") {
return
}
tools.ExecShell("systemctl start pure-ftpd")
status := tools.ExecShell("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
return
}
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Stop 停止服务
func (c *PureFtpdController) Stop(ctx http.Context) {
if !controllers.Check(ctx, "pureftpd") {
return
}
tools.ExecShell("systemctl stop pure-ftpd")
status := tools.ExecShell("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
if len(status) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
return
}
if status != "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// List 获取用户列表
func (c *PureFtpdController) List(ctx http.Context) {
if !controllers.Check(ctx, "pureftpd") {
return
}
listRaw := tools.ExecShell("pure-pw list")
if len(listRaw) == 0 {
controllers.Success(ctx, http.Json{
"total": 0,
"items": []User{},
})
return
}
listArr := strings.Split(listRaw, "\n")
var users []User
for _, v := range listArr {
if len(v) == 0 {
continue
}
match := regexp.MustCompile(`(\S+)\s+(\S+)`).FindStringSubmatch(v)
users = append(users, User{
Username: match[1],
Path: strings.Replace(match[2], "/./", "/", 1),
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(users) {
controllers.Success(ctx, http.Json{
"total": 0,
"items": []User{},
})
return
}
if endIndex > len(users) {
endIndex = len(users)
}
pagedUsers := users[startIndex:endIndex]
controllers.Success(ctx, http.Json{
"total": len(users),
"items": pagedUsers,
})
}
// Add 添加用户
func (c *PureFtpdController) Add(ctx http.Context) {
if !controllers.Check(ctx, "pureftpd") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"username": "required",
"password": "required|min_len:6",
"path": "required",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
username := ctx.Request().Input("username")
password := ctx.Request().Input("password")
path := ctx.Request().Input("path")
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
if !tools.Exists(path) {
controllers.Error(ctx, http.StatusBadRequest, "目录不存在")
return
}
tools.Chmod(path, 0755)
tools.Chown(path, "www", "www")
tools.ExecShell(`yes '` + password + `' | pure-pw useradd ` + username + ` -u www -g www -d ` + path)
tools.ExecShell("pure-pw mkdb")
controllers.Success(ctx, nil)
}
// Delete 删除用户
func (c *PureFtpdController) Delete(ctx http.Context) {
if !controllers.Check(ctx, "pureftpd") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"username": "required",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
username := ctx.Request().Input("username")
tools.ExecShell("pure-pw userdel " + username + " -m")
tools.ExecShell("pure-pw mkdb")
controllers.Success(ctx, nil)
}
// ChangePassword 修改密码
func (c *PureFtpdController) ChangePassword(ctx http.Context) {
if !controllers.Check(ctx, "pureftpd") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"username": "required",
"password": "required|min_len:6",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
username := ctx.Request().Input("username")
password := ctx.Request().Input("password")
tools.ExecShell(`yes '` + password + `' | pure-pw passwd ` + username + ` -m`)
tools.ExecShell("pure-pw mkdb")
controllers.Success(ctx, nil)
}
// GetPort 获取端口
func (c *PureFtpdController) GetPort(ctx http.Context) {
if !controllers.Check(ctx, "pureftpd") {
return
}
port := tools.ExecShell(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
if len(port) == 0 {
controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd端口失败")
return
}
controllers.Success(ctx, port)
}
// SetPort 设置端口
func (c *PureFtpdController) SetPort(ctx http.Context) {
if !controllers.Check(ctx, "pureftpd") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"port": "required",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
port := ctx.Request().Input("port")
tools.ExecShell(`sed -i "s/Bind.*/Bind 0.0.0.0,` + port + `/g" /www/server/pure-ftpd/etc/pure-ftpd.conf`)
if tools.IsRHEL() {
tools.ExecShell("firewall-cmd --zone=public --add-port=" + port + "/tcp --permanent")
tools.ExecShell("firewall-cmd --reload")
} else {
tools.ExecShell("ufw allow " + port + "/tcp")
tools.ExecShell("ufw reload")
}
tools.ExecShell("systemctl restart pure-ftpd")
controllers.Success(ctx, nil)
}
@@ -0,0 +1,222 @@
package s3fs
import (
"strings"
"github.com/bytedance/sonic"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/services"
"panel/pkg/tools"
)
type S3fsController struct {
setting services.Setting
}
type s3fs struct {
ID int64 `json:"id"`
Path string `json:"path"`
Bucket string `json:"bucket"`
Url string `json:"url"`
}
func NewS3fsController() *S3fsController {
return &S3fsController{
setting: services.NewSettingImpl(),
}
}
// List 所有 S3fs 挂载
func (c *S3fsController) List(ctx http.Context) {
if !controllers.Check(ctx, "s3fs") {
return
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
var s3fsList []s3fs
err := sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList)
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, "获取 S3fs 挂载失败")
return
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(s3fsList) {
controllers.Success(ctx, http.Json{
"total": 0,
"items": []s3fs{},
})
return
}
if endIndex > len(s3fsList) {
endIndex = len(s3fsList)
}
pagedS3fsList := s3fsList[startIndex:endIndex]
controllers.Success(ctx, http.Json{
"total": len(s3fsList),
"items": pagedS3fsList,
})
}
// Add 添加 S3fs 挂载
func (c *S3fsController) Add(ctx http.Context) {
if !controllers.Check(ctx, "s3fs") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"ak": "required|regex:^[a-zA-Z0-9]*$",
"sk": "required|regex:^[a-zA-Z0-9]*$",
"bucket": "required|regex:^[a-zA-Z0-9_-]*$",
"url": "required|full_url",
"path": "required|regex:^/[a-zA-Z0-9_-]+$",
})
if err != nil {
controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
return
}
ak := ctx.Request().Input("ak")
sk := ctx.Request().Input("sk")
path := ctx.Request().Input("path")
bucket := ctx.Request().Input("bucket")
url := ctx.Request().Input("url")
// 检查下地域节点中是否包含bucket,如果包含了,肯定是错误的
if strings.Contains(url, bucket) {
controllers.Error(ctx, http.StatusUnprocessableEntity, "地域节点不能包含 Bucket 名称")
return
}
// 检查挂载目录是否存在且为空
if !tools.Exists(path) {
tools.Mkdir(path, 0755)
}
if !tools.Empty(path) {
controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录必须为空")
return
}
var s3fsList []s3fs
err = sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
return
}
for _, s := range s3fsList {
if s.Path == path {
controllers.Error(ctx, http.StatusUnprocessableEntity, "路径已存在")
return
}
}
id := carbon.Now().TimestampMilli()
password := ak + ":" + sk
tools.WriteFile("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600)
tools.ExecShell(`echo 's3fs#` + bucket + ` ` + path + ` fuse _netdev,allow_other,nonempty,url=` + url + `,passwd_file=/etc/passwd-s3fs-` + cast.ToString(id) + ` 0 0' >> /etc/fstab`)
check := tools.ExecShell("mount -a 2>&1")
if len(check) != 0 {
tools.ExecShell(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+check)
return
}
check2 := tools.ExecShell("df -h | grep " + path + " 2>&1")
if len(check2) == 0 {
tools.ExecShell(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
controllers.Error(ctx, http.StatusInternalServerError, "挂载失败,请检查配置是否正确")
return
}
s3fsList = append(s3fsList, s3fs{
ID: id,
Path: path,
Bucket: bucket,
Url: url,
})
json, err := sonic.MarshalString(s3fsList)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败")
return
}
err = c.setting.Set("s3fs", json)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败")
return
}
controllers.Success(ctx, nil)
}
// Delete 删除 S3fs 挂载
func (c *S3fsController) Delete(ctx http.Context) {
if !controllers.Check(ctx, "s3fs") {
return
}
id := ctx.Request().Input("id")
if len(id) == 0 {
controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不能为空")
return
}
var s3fsList []s3fs
err := sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
return
}
var mount s3fs
for _, s := range s3fsList {
if cast.ToString(s.ID) == id {
mount = s
break
}
}
if mount.ID == 0 {
controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不存在")
return
}
tools.ExecShell(`fusermount -u '` + mount.Path + `' 2>&1`)
tools.ExecShell(`umount '` + mount.Path + `' 2>&1`)
tools.ExecShell(`sed -i 's@^s3fs#` + mount.Bucket + `\s` + mount.Path + `.*$@@g' /etc/fstab`)
check := tools.ExecShell("mount -a 2>&1")
if len(check) != 0 {
controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+check)
return
}
tools.RemoveFile("/etc/passwd-s3fs-" + cast.ToString(mount.ID))
var newS3fsList []s3fs
for _, s := range s3fsList {
if s.ID != mount.ID {
newS3fsList = append(newS3fsList, s)
}
}
json, err := sonic.MarshalString(newS3fsList)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败")
return
}
err = c.setting.Set("s3fs", json)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败")
return
}
controllers.Success(ctx, nil)
}
@@ -0,0 +1,390 @@
package supervisor
import (
"strconv"
"strings"
"github.com/goravel/framework/contracts/http"
"panel/pkg/tools"
"panel/app/http/controllers"
)
type SupervisorController struct {
ServiceName string
}
func NewSupervisorController() *SupervisorController {
var serviceName string
if tools.IsRHEL() {
serviceName = "supervisord"
} else {
serviceName = "supervisor"
}
return &SupervisorController{
ServiceName: serviceName,
}
}
// Status 状态
func (r *SupervisorController) Status(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
status := tools.ExecShell(`systemctl status ` + r.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Start 启动
func (r *SupervisorController) Start(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
tools.ExecShell(`systemctl start ` + r.ServiceName)
status := tools.ExecShell(`systemctl status ` + r.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Stop 停止
func (r *SupervisorController) Stop(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
tools.ExecShell(`systemctl stop ` + r.ServiceName)
status := tools.ExecShell(`systemctl status ` + r.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
if status != "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Restart 重启
func (r *SupervisorController) Restart(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
tools.ExecShell(`systemctl restart ` + r.ServiceName)
status := tools.ExecShell(`systemctl status ` + r.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Reload 重载
func (r *SupervisorController) Reload(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
tools.ExecShell(`systemctl reload ` + r.ServiceName)
status := tools.ExecShell(`systemctl status ` + r.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
if status == "active" {
controllers.Success(ctx, true)
} else {
controllers.Success(ctx, false)
}
}
// Log 日志
func (r *SupervisorController) Log(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
log := tools.ExecShell(`tail -n 200 /var/log/supervisor/supervisord.log`)
controllers.Success(ctx, log)
}
// ClearLog 清空日志
func (r *SupervisorController) ClearLog(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
tools.ExecShell(`echo "" > /var/log/supervisor/supervisord.log`)
controllers.Success(ctx, nil)
}
// Config 获取配置
func (r *SupervisorController) Config(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
var config string
if tools.IsRHEL() {
config = tools.ReadFile(`/etc/supervisord.conf`)
} else {
config = tools.ReadFile(`/etc/supervisor/supervisord.conf`)
}
controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *SupervisorController) SaveConfig(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
config := ctx.Request().Input("config")
if tools.IsRHEL() {
tools.WriteFile(`/etc/supervisord.conf`, config, 0644)
} else {
tools.WriteFile(`/etc/supervisor/supervisord.conf`, config, 0644)
}
tools.ExecShell(`systemctl restart ` + r.ServiceName)
controllers.Success(ctx, nil)
}
// Processes 进程列表
func (r *SupervisorController) Processes(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
type process struct {
Name string `json:"name"`
Status string `json:"status"`
Pid string `json:"pid"`
Uptime string `json:"uptime"`
}
out := tools.ExecShell(`supervisorctl status | awk '{print $1}'`)
var processList []process
for _, line := range strings.Split(out, "\n") {
if len(line) == 0 {
continue
}
var p process
p.Name = line
p.Status = tools.ExecShell(`supervisorctl status ` + line + ` | awk '{print $2}'`)
if p.Status == "RUNNING" {
p.Pid = strings.ReplaceAll(tools.ExecShell(`supervisorctl status `+line+` | awk '{print $4}'`), ",", "")
p.Uptime = tools.ExecShell(`supervisorctl status ` + line + ` | awk '{print $6}'`)
} else {
p.Pid = "-"
p.Uptime = "-"
}
processList = append(processList, p)
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(processList) {
controllers.Success(ctx, http.Json{
"total": 0,
"items": []process{},
})
return
}
if endIndex > len(processList) {
endIndex = len(processList)
}
pagedProcessList := processList[startIndex:endIndex]
controllers.Success(ctx, http.Json{
"total": len(processList),
"items": pagedProcessList,
})
}
// StartProcess 启动进程
func (r *SupervisorController) StartProcess(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
tools.ExecShell(`supervisorctl start ` + process)
controllers.Success(ctx, nil)
}
// StopProcess 停止进程
func (r *SupervisorController) StopProcess(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
tools.ExecShell(`supervisorctl stop ` + process)
controllers.Success(ctx, nil)
}
// RestartProcess 重启进程
func (r *SupervisorController) RestartProcess(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
tools.ExecShell(`supervisorctl restart ` + process)
controllers.Success(ctx, nil)
}
// ProcessLog 进程日志
func (r *SupervisorController) ProcessLog(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
var logPath string
if tools.IsRHEL() {
logPath = tools.ExecShell(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
} else {
logPath = tools.ExecShell(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
}
log := tools.ExecShell(`tail -n 200 ` + logPath)
controllers.Success(ctx, log)
}
// ClearProcessLog 清空进程日志
func (r *SupervisorController) ClearProcessLog(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
var logPath string
if tools.IsRHEL() {
logPath = tools.ExecShell(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
} else {
logPath = tools.ExecShell(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
}
tools.ExecShell(`echo "" > ` + logPath)
controllers.Success(ctx, nil)
}
// ProcessConfig 获取进程配置
func (r *SupervisorController) ProcessConfig(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Query("process")
var config string
if tools.IsRHEL() {
config = tools.ReadFile(`/etc/supervisord.d/` + process + `.conf`)
} else {
config = tools.ReadFile(`/etc/supervisor/conf.d/` + process + `.conf`)
}
controllers.Success(ctx, config)
}
// SaveProcessConfig 保存进程配置
func (r *SupervisorController) SaveProcessConfig(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
config := ctx.Request().Input("config")
if tools.IsRHEL() {
tools.WriteFile(`/etc/supervisord.d/`+process+`.conf`, config, 0644)
} else {
tools.WriteFile(`/etc/supervisor/conf.d/`+process+`.conf`, config, 0644)
}
tools.ExecShell(`supervisorctl reread`)
tools.ExecShell(`supervisorctl update`)
tools.ExecShell(`supervisorctl start ` + process)
controllers.Success(ctx, nil)
}
// AddProcess 添加进程
func (r *SupervisorController) AddProcess(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|alpha_dash",
"user": "required|alpha_dash",
"path": "required",
"command": "required",
"num": "required",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
name := ctx.Request().Input("name")
user := ctx.Request().Input("user")
path := ctx.Request().Input("path")
command := ctx.Request().Input("command")
num := ctx.Request().InputInt("num", 1)
config := `[program:` + name + `]
command=` + command + `
process_name=%(program_name)s
directory=` + path + `
autostart=true
autorestart=true
user=` + user + `
numprocs=` + strconv.Itoa(num) + `
redirect_stderr=true
stdout_logfile=/var/log/supervisor/` + name + `.log
stdout_logfile_maxbytes=2MB
`
if tools.IsRHEL() {
tools.WriteFile(`/etc/supervisord.d/`+name+`.conf`, config, 0644)
} else {
tools.WriteFile(`/etc/supervisor/conf.d/`+name+`.conf`, config, 0644)
}
tools.ExecShell(`supervisorctl reread`)
tools.ExecShell(`supervisorctl update`)
tools.ExecShell(`supervisorctl start ` + name)
controllers.Success(ctx, nil)
}
// DeleteProcess 删除进程
func (r *SupervisorController) DeleteProcess(ctx http.Context) {
if !controllers.Check(ctx, "supervisor") {
return
}
process := ctx.Request().Input("process")
tools.ExecShell(`supervisorctl stop ` + process)
var logPath string
if tools.IsRHEL() {
logPath = tools.ExecShell(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
tools.RemoveFile(`/etc/supervisord.d/` + process + `.conf`)
} else {
logPath = tools.ExecShell(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
tools.RemoveFile(`/etc/supervisor/conf.d/` + process + `.conf`)
}
tools.RemoveFile(logPath)
tools.ExecShell(`supervisorctl reread`)
tools.ExecShell(`supervisorctl update`)
controllers.Success(ctx, nil)
}
+300
View File
@@ -0,0 +1,300 @@
package controllers
import (
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
"panel/pkg/tools"
)
type SafeController struct {
// Dependent services
}
func NewSafeController() *SafeController {
return &SafeController{
// Inject services
}
}
func (r *SafeController) GetFirewallStatus(ctx http.Context) {
Success(ctx, r.firewallStatus())
}
func (r *SafeController) SetFirewallStatus(ctx http.Context) {
var out string
if ctx.Request().InputBool("status") {
if tools.IsRHEL() {
out = tools.ExecShell("systemctl start firewalld")
} else {
out = tools.ExecShell("echo y | ufw enable")
}
} else {
if tools.IsRHEL() {
out = tools.ExecShell("systemctl stop firewalld")
} else {
out = tools.ExecShell("ufw disable")
}
}
Success(ctx, out)
}
func (r *SafeController) GetFirewallRules(ctx http.Context) {
if !r.firewallStatus() {
Success(ctx, nil)
return
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
if tools.IsRHEL() {
out := tools.ExecShell("firewall-cmd --list-all 2>&1")
match := regexp.MustCompile(`ports: (.*)`).FindStringSubmatch(out)
if len(match) == 0 {
Success(ctx, http.Json{
"total": 0,
"items": []map[string]string{},
})
return
}
ports := strings.Split(match[1], " ")
var rules []map[string]string
for _, port := range ports {
rule := strings.Split(port, "/")
rules = append(rules, map[string]string{
"port": rule[0],
"protocol": rule[1],
})
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(rules) {
Success(ctx, http.Json{
"total": 0,
"items": []map[string]string{},
})
return
}
if endIndex > len(rules) {
endIndex = len(rules)
}
pagedRules := rules[startIndex:endIndex]
Success(ctx, http.Json{
"total": len(rules),
"items": pagedRules,
})
} else {
out := tools.ExecShell("ufw status | grep -v '(v6)' | grep ALLOW | awk '{print $1}'")
if len(out) == 0 {
Success(ctx, http.Json{
"total": 0,
"items": []map[string]string{},
})
return
}
var rules []map[string]string
for _, port := range strings.Split(out, "\n") {
rule := strings.Split(port, "/")
rules = append(rules, map[string]string{
"port": rule[0],
"protocol": rule[1],
})
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(rules) {
Success(ctx, http.Json{
"total": 0,
"items": []map[string]string{},
})
return
}
if endIndex > len(rules) {
endIndex = len(rules)
}
pagedRules := rules[startIndex:endIndex]
Success(ctx, http.Json{
"total": len(rules),
"items": pagedRules,
})
}
}
func (r *SafeController) AddFirewallRule(ctx http.Context) {
if !r.firewallStatus() {
Error(ctx, http.StatusBadRequest, "防火墙未启动")
return
}
port := ctx.Request().InputInt("port", 0)
protocol := ctx.Request().Input("protocol", "")
if port == 0 || protocol == "" {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
if tools.IsRHEL() {
tools.ExecShell("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
tools.ExecShell("firewall-cmd --add-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
tools.ExecShell("firewall-cmd --reload")
} else {
tools.ExecShell("ufw delete allow " + cast.ToString(port) + "/" + protocol)
tools.ExecShell("ufw allow " + cast.ToString(port) + "/" + protocol)
tools.ExecShell("ufw reload")
}
Success(ctx, nil)
}
func (r *SafeController) DeleteFirewallRule(ctx http.Context) {
if !r.firewallStatus() {
Error(ctx, http.StatusBadRequest, "防火墙未启动")
return
}
port := ctx.Request().InputInt("port", 0)
protocol := ctx.Request().Input("protocol", "")
if port == 0 || protocol == "" {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
if tools.IsRHEL() {
tools.ExecShell("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
tools.ExecShell("firewall-cmd --reload")
} else {
tools.ExecShell("ufw delete allow " + cast.ToString(port) + "/" + protocol)
tools.ExecShell("ufw reload")
}
Success(ctx, nil)
}
func (r *SafeController) firewallStatus() bool {
var out string
var running bool
if tools.IsRHEL() {
out = tools.ExecShell("systemctl status firewalld | grep Active | awk '{print $3}'")
if out == "(running)" {
running = true
} else {
running = false
}
} else {
out = tools.ExecShell("ufw status | grep Status | awk '{print $2}'")
if out == "active" {
running = true
} else {
running = false
}
}
return running
}
func (r *SafeController) GetSshStatus(ctx http.Context) {
var out string
if tools.IsRHEL() {
out = tools.ExecShell("systemctl status sshd | grep Active | awk '{print $3}'")
} else {
out = tools.ExecShell("systemctl status ssh | grep Active | awk '{print $3}'")
}
running := false
if out == "(running)" {
running = true
}
Success(ctx, running)
}
func (r *SafeController) SetSshStatus(ctx http.Context) {
if ctx.Request().InputBool("status") {
if tools.IsRHEL() {
tools.ExecShell("systemctl enable sshd")
tools.ExecShell("systemctl start sshd")
} else {
tools.ExecShell("systemctl enable ssh")
tools.ExecShell("systemctl start ssh")
}
} else {
if tools.IsRHEL() {
tools.ExecShell("systemctl stop sshd")
tools.ExecShell("systemctl disable sshd")
} else {
tools.ExecShell("systemctl stop ssh")
tools.ExecShell("systemctl disable ssh")
}
}
Success(ctx, nil)
}
func (r *SafeController) GetSshPort(ctx http.Context) {
out := tools.ExecShell("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
Success(ctx, out)
}
func (r *SafeController) SetSshPort(ctx http.Context) {
port := ctx.Request().InputInt("port", 0)
if port == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
oldPort := tools.ExecShell("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
tools.ExecShell("sed -i 's/#Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
tools.ExecShell("sed -i 's/Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
if status := tools.ExecShell("systemctl status sshd | grep Active | awk '{print $3}'"); status == "(running)" {
tools.ExecShell("systemctl restart sshd")
}
Success(ctx, nil)
}
func (r *SafeController) GetPingStatus(ctx http.Context) {
if tools.IsRHEL() {
out := tools.ExecShell(`firewall-cmd --list-all 2>&1`)
if !strings.Contains(out, `rule protocol value="icmp" drop`) {
Success(ctx, true)
} else {
Success(ctx, false)
}
} else {
config := tools.ReadFile("/etc/ufw/before.rules")
if strings.Contains(config, "-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT") {
Success(ctx, true)
} else {
Success(ctx, false)
}
}
}
func (r *SafeController) SetPingStatus(ctx http.Context) {
if tools.IsRHEL() {
if ctx.Request().InputBool("status") {
tools.ExecShell(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`)
} else {
tools.ExecShell(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`)
}
tools.ExecShell(`firewall-cmd --reload`)
} else {
if ctx.Request().InputBool("status") {
tools.ExecShell(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/g' /etc/ufw/before.rules`)
} else {
tools.ExecShell(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules`)
}
tools.ExecShell(`ufw reload`)
}
Success(ctx, nil)
}
+141
View File
@@ -0,0 +1,141 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/pkg/tools"
"panel/app/models"
"panel/app/services"
)
type SettingController struct {
setting services.Setting
}
func NewSettingController() *SettingController {
return &SettingController{
setting: services.NewSettingImpl(),
}
}
func (r *SettingController) List(ctx http.Context) {
var settings []models.Setting
err := facades.Orm().Query().Get(&settings)
if err != nil {
facades.Log().Error("[面板][SettingController] 查询设置列表失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
var result = make(map[string]string)
for _, setting := range settings {
if setting.Key == models.SettingKeyMysqlRootPassword {
continue
}
result[setting.Key] = setting.Value
}
var user models.User
err = facades.Auth().User(ctx, &user)
if err != nil {
facades.Log().Error("[面板][SettingController] 获取用户失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
result["username"] = user.Username
result["email"] = user.Email
result["port"] = tools.ExecShell(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
Success(ctx, result)
}
func (r *SettingController) Save(ctx http.Context) {
name := ctx.Request().Input("name")
port := ctx.Request().Input("port")
backupPath := ctx.Request().Input("backup_path")
websitePath := ctx.Request().Input("website_path")
entrance := ctx.Request().Input("entrance")
username := ctx.Request().Input("username")
email := ctx.Request().Input("email")
password := ctx.Request().Input("password")
err := r.setting.Set(models.SettingKeyName, name)
if err != nil {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
oldPort := tools.ExecShell(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
if oldPort != port {
tools.ExecShell("sed -i 's/APP_PORT=" + oldPort + "/APP_PORT=" + port + "/g' /www/panel/panel.conf")
}
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
err = r.setting.Set(models.SettingKeyBackupPath, backupPath)
if err != nil {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if !tools.Exists(websitePath) {
tools.Mkdir(websitePath, 0755)
tools.Chown(websitePath, "www", "www")
}
err = r.setting.Set(models.SettingKeyWebsitePath, websitePath)
if err != nil {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
err = r.setting.Set(models.SettingKeyEntrance, entrance)
if err != nil {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
var user models.User
err = facades.Auth().User(ctx, &user)
if err != nil {
facades.Log().Error("[面板][SettingController] 获取用户失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if len(username) > 0 {
user.Username = username
}
if len(email) > 0 {
user.Email = email
}
if len(password) > 0 {
hash, err := facades.Hash().Make(password)
if err != nil {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
user.Password = hash
}
if err = facades.Orm().Query().Save(&user); err != nil {
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
+164
View File
@@ -0,0 +1,164 @@
package controllers
import (
"bytes"
"context"
nethttp "net/http"
"sync"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/gorilla/websocket"
"panel/app/models"
"panel/app/services"
"panel/pkg/ssh"
)
type SshController struct {
AuthMethod ssh.AuthMethod
setting services.Setting
}
func NewSshController() *SshController {
return &SshController{
AuthMethod: ssh.PASSWORD,
setting: services.NewSettingImpl(),
}
}
func (r *SshController) GetInfo(ctx http.Context) {
host := r.setting.Get(models.SettingKeySshHost)
port := r.setting.Get(models.SettingKeySshPort)
user := r.setting.Get(models.SettingKeySshUser)
password := r.setting.Get(models.SettingKeySshPassword)
if len(host) == 0 || len(user) == 0 || len(password) == 0 {
Error(ctx, http.StatusInternalServerError, "SSH 配置不完整")
return
}
Success(ctx, http.Json{
"host": host,
"port": port,
"user": user,
"password": password,
})
}
func (r *SshController) UpdateInfo(ctx http.Context) {
validator, err := ctx.Request().Validate(map[string]string{
"host": "required",
"port": "required",
"user": "required",
"password": "required",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
host := ctx.Request().Input("host")
port := ctx.Request().Input("port")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
err = r.setting.Set(models.SettingKeySshHost, host)
if err != nil {
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
err = r.setting.Set(models.SettingKeySshPort, port)
if err != nil {
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
err = r.setting.Set(models.SettingKeySshUser, user)
if err != nil {
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
err = r.setting.Set(models.SettingKeySshPassword, password)
if err != nil {
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
func (r *SshController) Session(ctx http.Context) {
upGrader := websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(r *nethttp.Request) bool {
return true
},
Subprotocols: []string{ctx.Request().Header("Sec-WebSocket-Protocol")},
}
ws, err := upGrader.Upgrade(ctx.Response().Writer(), ctx.Request().Origin(), nil)
if err != nil {
facades.Log().Error("[面板][SSH] 建立连接失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
defer ws.Close()
config := ssh.SSHClientConfigPassword(
r.setting.Get(models.SettingKeySshHost)+":"+r.setting.Get(models.SettingKeySshPort),
r.setting.Get(models.SettingKeySshUser),
r.setting.Get(models.SettingKeySshPassword),
)
client, err := ssh.NewSSHClient(config)
if err != nil {
_ = ws.WriteControl(websocket.CloseMessage,
[]byte(err.Error()), time.Now().Add(time.Second))
return
}
defer client.Close()
turn, err := ssh.NewTurn(ws, client)
if err != nil {
_ = ws.WriteControl(websocket.CloseMessage,
[]byte(err.Error()), time.Now().Add(time.Second))
return
}
defer turn.Close()
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
var logBuff = bufPool.Get().(*bytes.Buffer)
logBuff.Reset()
defer bufPool.Put(logBuff)
ctx2, cancel := context.WithCancel(context.Background())
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
err := turn.LoopRead(logBuff, ctx2)
if err != nil {
facades.Log().Error("[面板][SSH] 读取数据失败 ", err.Error())
}
}()
go func() {
defer wg.Done()
err := turn.SessionWait()
if err != nil {
facades.Log().Error("[面板][SSH] 会话失败 ", err.Error())
}
cancel()
}()
wg.Wait()
}
+80
View File
@@ -0,0 +1,80 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/models"
"panel/pkg/tools"
)
type TaskController struct {
// Dependent services
}
func NewTaskController() *TaskController {
return &TaskController{
// Inject services
}
}
func (r *TaskController) Status(ctx http.Context) {
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{
"task": true,
})
return
}
Success(ctx, http.Json{
"task": false,
})
}
func (r *TaskController) List(ctx http.Context) {
status := ctx.Request().Query("status")
if len(status) == 0 {
status = models.TaskStatusWaiting
}
var tasks []models.Task
var total int64
err := facades.Orm().Query().Where("status", status).Paginate(ctx.Request().QueryInt("page"), ctx.Request().QueryInt("limit"), &tasks, &total)
if err != nil {
facades.Log().Error("[面板][TaskController] 查询任务列表失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, http.Json{
"total": total,
"items": tasks,
})
}
func (r *TaskController) Log(ctx http.Context) {
var task models.Task
err := facades.Orm().Query().Where("id", ctx.Request().QueryInt("id")).FirstOrFail(&task)
if err != nil {
facades.Log().Error("[面板][TaskController] 查询任务失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
log := tools.ExecShell("tail -n 30 " + task.Log)
Success(ctx, log)
}
func (r *TaskController) Delete(ctx http.Context) {
var task models.Task
_, err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).Delete(&task)
if err != nil {
facades.Log().Error("[面板][TaskController] 删除任务失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
+78
View File
@@ -0,0 +1,78 @@
package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/http/requests"
"panel/app/models"
)
type UserController struct {
// Dependent services
}
func NewUserController() *UserController {
return &UserController{
// Inject services
}
}
func (r *UserController) Login(ctx http.Context) {
var loginRequest requests.LoginRequest
errors, err := ctx.Request().ValidateRequest(&loginRequest)
if err != nil {
Error(ctx, http.StatusUnprocessableEntity, err.Error())
return
}
if errors != nil {
Error(ctx, http.StatusUnprocessableEntity, errors.All())
return
}
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
}
if user.ID == 0 || !facades.Hash().Check(loginRequest.Password, user.Password) {
Error(ctx, http.StatusForbidden, "用户名或密码错误")
return
}
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
}
}
token, loginErr := facades.Auth().LoginUsingID(ctx, user.ID)
if loginErr != nil {
facades.Log().Error("[面板][UserController] 登录失败 ", loginErr)
Error(ctx, http.StatusInternalServerError, loginErr.Error())
return
}
Success(ctx, http.Json{
"access_token": token,
})
}
func (r *UserController) Info(ctx http.Context) {
user, ok := ctx.Value("user").(models.User)
if !ok {
Error(ctx, http.StatusUnauthorized, "登录已过期")
return
}
Success(ctx, http.Json{
"username": user.Username,
"email": user.Email,
})
}
+715
View File
@@ -0,0 +1,715 @@
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 {
website services.Website
setting services.Setting
backup services.Backup
}
func NewWebsiteController() *WebsiteController {
return &WebsiteController{
website: services.NewWebsiteImpl(),
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// List 网站列表
func (c *WebsiteController) List(ctx http.Context) {
limit := ctx.Request().QueryInt("limit")
page := ctx.Request().QueryInt("page")
total, websites, err := c.website.List(page, limit)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站列表失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, http.Json{
"total": total,
"items": websites,
})
}
// Add 添加网站
func (c *WebsiteController) Add(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$",
"domain": "required",
"php": "required",
"db": "bool",
"db_type": "required_if:db,true",
"db_name": "required_if:db,true",
"db_user": "required_if:db,true",
"db_password": "required_if:db,true",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
var website services.PanelWebsite
website.Name = ctx.Request().Input("name")
website.Domain = ctx.Request().Input("domain")
website.Php = ctx.Request().InputInt("php")
website.Db = ctx.Request().InputBool("db")
website.DbType = ctx.Request().Input("db_type")
website.DbName = ctx.Request().Input("db_name")
website.DbUser = ctx.Request().Input("db_user")
website.DbPassword = ctx.Request().Input("db_password")
newSite, err := c.website.Add(website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 添加网站失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, newSite)
}
// Delete 删除网站
func (c *WebsiteController) Delete(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
err := c.website.Delete(id)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 删除网站失败 ", err)
Error(ctx, http.StatusInternalServerError, "删除网站失败: "+err.Error())
return
}
Success(ctx, nil)
}
// GetDefaultConfig 获取默认配置
func (c *WebsiteController) GetDefaultConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
index := tools.ReadFile("/www/server/openresty/html/index.html")
stop := tools.ReadFile("/www/server/openresty/html/stop.html")
Success(ctx, http.Json{
"index": index,
"stop": stop,
})
}
// SaveDefaultConfig 保存默认配置
func (c *WebsiteController) SaveDefaultConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
index := ctx.Request().Input("index")
stop := ctx.Request().Input("stop")
if !tools.WriteFile("/www/server/openresty/html/index.html", index, 0644) {
facades.Log().Error("[面板][WebsiteController] 保存默认配置失败")
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
if !tools.WriteFile("/www/server/openresty/html/stop.html", stop, 0644) {
facades.Log().Error("[面板][WebsiteController] 保存默认配置失败")
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
// GetConfig 获取配置
func (c *WebsiteController) GetConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
config, err := c.website.GetConfig(id)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, config)
}
// SaveConfig 保存配置
func (c *WebsiteController) SaveConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
validator, err := ctx.Request().Validate(map[string]string{
"id": "required",
"domains": "required",
"ports": "required",
"hsts": "bool",
"ssl": "bool",
"http_redirect": "bool",
"open_basedir": "bool",
"waf": "required",
"waf_cache": "required",
"waf_mode": "required",
"waf_cc_deny": "required",
"index": "required",
"path": "required",
"root": "required",
"raw": "required",
"php": "required",
"ssl_certificate": "required_if:ssl,true",
"ssl_certificate_key": "required_if:ssl,true",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().One())
return
}
var website models.Website
if facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&website) != nil {
Error(ctx, http.StatusBadRequest, "网站不存在")
return
}
if !website.Status {
Error(ctx, http.StatusBadRequest, "网站已停用,请先启用")
return
}
// 原文
raw := tools.ReadFile("/www/server/vhost/" + website.Name + ".conf")
if strings.TrimSpace(raw) != strings.TrimSpace(ctx.Request().Input("raw")) {
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", ctx.Request().Input("raw"), 0644)
Success(ctx, nil)
return
}
// 目录
path := ctx.Request().Input("path")
if !tools.Exists(path) {
Error(ctx, http.StatusBadRequest, "网站目录不存在")
return
}
website.Path = path
// 域名
domain := "server_name"
domains := strings.Split(ctx.Request().Input("domains"), "\n")
if len(domains) == 0 {
Error(ctx, http.StatusBadRequest, "域名不能为空")
return
}
for _, v := range domains {
if v == "" {
continue
}
domain += " " + v
}
domain += ";"
domainConfigOld := tools.Cut(raw, "# server_name标记位开始", "# server_name标记位结束")
if len(strings.TrimSpace(domainConfigOld)) == 0 {
Error(ctx, http.StatusBadRequest, "配置文件中缺少server_name标记位")
return
}
raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1)
// 端口
var port strings.Builder
ports := strings.Split(ctx.Request().Input("ports"), "\n")
if len(ports) == 0 {
Error(ctx, http.StatusBadRequest, "端口不能为空")
return
}
for i, v := range ports {
if _, err := strconv.Atoi(v); err != nil && v != "443 ssl http2" {
Error(ctx, http.StatusBadRequest, "端口格式错误")
return
}
if v == "443" && ctx.Request().InputBool("ssl") {
v = "443 ssl http2"
}
if i != len(ports)-1 {
port.WriteString(" listen " + v + ";\n")
} else {
port.WriteString(" listen " + v + ";")
}
}
portConfigOld := tools.Cut(raw, "# port标记位开始", "# port标记位结束")
if len(strings.TrimSpace(portConfigOld)) == 0 {
Error(ctx, http.StatusBadRequest, "配置文件中缺少port标记位")
return
}
raw = strings.Replace(raw, portConfigOld, "\n"+port.String()+"\n ", -1)
// 运行目录
root := tools.Cut(raw, "# root标记位开始", "# root标记位结束")
if len(strings.TrimSpace(root)) == 0 {
Error(ctx, http.StatusBadRequest, "配置文件中缺少root标记位")
return
}
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root)
if len(match) != 2 {
Error(ctx, http.StatusBadRequest, "配置文件中root标记位格式错误")
return
}
rootNew := strings.Replace(root, match[1], ctx.Request().Input("root"), -1)
raw = strings.Replace(raw, root, rootNew, -1)
// 默认文件
index := tools.Cut(raw, "# index标记位开始", "# index标记位结束")
if len(strings.TrimSpace(index)) == 0 {
Error(ctx, http.StatusBadRequest, "配置文件中缺少index标记位")
return
}
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index)
if len(match) != 2 {
Error(ctx, http.StatusBadRequest, "配置文件中index标记位格式错误")
return
}
indexNew := strings.Replace(index, match[1], ctx.Request().Input("index"), -1)
raw = strings.Replace(raw, index, indexNew, -1)
// 防跨站
root = ctx.Request().Input("root")
if !strings.HasSuffix(root, "/") {
root += "/"
}
if ctx.Request().InputBool("open_basedir") {
tools.WriteFile(root+".user.ini", "open_basedir="+path+":/tmp/", 0644)
} else {
if tools.Exists(root + ".user.ini") {
tools.RemoveFile(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.WriteFile("/www/server/vhost/ssl/"+website.Name+".pem", ctx.Request().Input("ssl_certificate"), 0644)
tools.WriteFile("/www/server/vhost/ssl/"+website.Name+".key", ctx.Request().Input("ssl_certificate_key"), 0644)
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)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", raw, 0644)
tools.WriteFile("/www/server/vhost/rewrite/"+website.Name+".conf", ctx.Request().Input("rewrite"), 0644)
tools.ExecShell("systemctl reload openresty")
Success(ctx, nil)
}
// ClearLog 清空日志
func (c *WebsiteController) ClearLog(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
website := models.Website{}
err := facades.Orm().Query().Where("id", id).Get(&website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
tools.RemoveFile("/www/wwwlogs/" + website.Name + ".log")
Success(ctx, nil)
}
// UpdateRemark 更新备注
func (c *WebsiteController) UpdateRemark(ctx http.Context) {
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
website := models.Website{}
err := facades.Orm().Query().Where("id", id).Get(&website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
website.Remark = ctx.Request().Input("remark")
err = facades.Orm().Query().Save(&website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 保存网站备注失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, nil)
}
// BackupList 备份列表
func (c *WebsiteController) BackupList(ctx http.Context) {
backupList, err := c.backup.WebsiteList()
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站备份列表失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
Success(ctx, backupList)
}
// CreateBackup 创建备份
func (c *WebsiteController) CreateBackup(ctx http.Context) {
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
website := models.Website{}
err := facades.Orm().Query().Where("id", id).Get(&website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
Error(ctx, http.StatusInternalServerError, "获取网站信息失败: "+err.Error())
return
}
err = c.backup.WebSiteBackup(website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 备份网站失败 ", err)
Error(ctx, http.StatusInternalServerError, "备份网站失败: "+err.Error())
return
}
Success(ctx, nil)
}
// UploadBackup 上传备份
func (c *WebsiteController) UploadBackup(ctx http.Context) {
file, err := ctx.Request().File("file")
if err != nil {
Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/website"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
Error(ctx, http.StatusBadRequest, "上传文件失败")
return
}
Success(ctx, "上传文件成功")
}
// RestoreBackup 还原备份
func (c *WebsiteController) RestoreBackup(ctx http.Context) {
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
fileName := ctx.Request().Input("name")
if len(fileName) == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
website := models.Website{}
err := facades.Orm().Query().Where("id", id).Get(&website)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
Error(ctx, http.StatusInternalServerError, "获取网站信息失败: "+err.Error())
return
}
err = c.backup.WebsiteRestore(website, fileName)
if err != nil {
facades.Log().Error("[面板][WebsiteController] 还原网站失败 ", err)
Error(ctx, http.StatusInternalServerError, "还原网站失败: "+err.Error())
return
}
Success(ctx, nil)
}
// DeleteBackup 删除备份
func (c *WebsiteController) DeleteBackup(ctx http.Context) {
fileName := ctx.Request().Input("name")
if len(fileName) == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/website"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
if !tools.RemoveFile(backupPath + "/" + fileName) {
Error(ctx, http.StatusInternalServerError, "删除备份失败")
return
}
Success(ctx, nil)
}
// ResetConfig 重置配置
func (c *WebsiteController) ResetConfig(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
website := models.Website{}
if err := facades.Orm().Query().Where("id", id).Get(&website); err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
website.Status = true
website.Ssl = false
if err := facades.Orm().Query().Save(&website); err != nil {
facades.Log().Error("[面板][WebsiteController] 保存网站配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
raw := fmt.Sprintf(`
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
server
{
# port标记位开始
listen 80;
# port标记位结束
# server_name标记位开始
server_name localhost;
# server_name标记位结束
# index标记位开始
index index.php index.html;
# index标记位结束
# root标记位开始
root %s;
# root标记位结束
# ssl标记位开始
# ssl标记位结束
# php标记位开始
include enable-php-%d.conf;
# php标记位结束
# waf标记位开始
waf on;
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
waf_mode DYNAMIC;
waf_cc_deny rate=1000r/m duration=60m;
waf_cache capacity=50;
# waf标记位结束
# 错误页配置,可自行设置
#error_page 404 /404.html;
#error_page 502 /502.html;
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
include /www/server/vhost/rewrite/%s.conf;
# 面板默认禁止访问部分敏感目录,可自行修改
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
{
return 404;
}
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
location ~ .*\.(js|css)$
{
expires 1h;
error_log /dev/null;
access_log /dev/null;
}
access_log /www/wwwlogs/%s.log;
error_log /www/wwwlogs/%s.log;
}
`, website.Path, website.Php, website.Name, website.Name, website.Name)
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", raw, 0644)
tools.WriteFile("/www/server/vhost/rewrite"+website.Name+".conf", "", 0644)
tools.ExecShell("systemctl reload openresty")
Success(ctx, nil)
}
// Status 网站状态
func (c *WebsiteController) Status(ctx http.Context) {
if !Check(ctx, "openresty") {
return
}
id := ctx.Request().InputInt("id")
if id == 0 {
Error(ctx, http.StatusBadRequest, "参数错误")
return
}
website := models.Website{}
if err := facades.Orm().Query().Where("id", id).Get(&website); err != nil {
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
website.Status = ctx.Request().InputBool("status")
if err := facades.Orm().Query().Save(&website); err != nil {
facades.Log().Error("[面板][WebsiteController] 保存网站配置失败 ", err)
Error(ctx, http.StatusInternalServerError, "系统内部错误")
return
}
raw := tools.ReadFile("/www/server/vhost/" + website.Name + ".conf")
// 运行目录
rootConfig := tools.Cut(raw, "# root标记位开始\n", "# root标记位结束")
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig)
if len(match) == 2 {
if website.Status {
root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig)
raw = strings.ReplaceAll(raw, rootConfig, " root "+root[1]+";\n ")
} else {
raw = strings.ReplaceAll(raw, rootConfig, " root /www/server/openresty/html;\n # root "+match[1]+";\n ")
}
}
// 默认文件
indexConfig := tools.Cut(raw, "# index标记位开始\n", "# index标记位结束")
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig)
if len(match) == 2 {
if website.Status {
index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig)
raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ")
} else {
raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ")
}
}
tools.WriteFile("/www/server/vhost/"+website.Name+".conf", raw, 0644)
tools.ExecShell("systemctl reload openresty")
Success(ctx, nil)
}
+18
View File
@@ -0,0 +1,18 @@
package http
import (
"github.com/goravel/framework/contracts/http"
"panel/app/http/middleware"
)
type Kernel struct {
}
// The application's global HTTP middleware stack.
// These middleware are run during every request to your application.
func (kernel Kernel) Middleware() []http.Middleware {
return []http.Middleware{
middleware.Static(),
}
}
+63
View File
@@ -0,0 +1,63 @@
package middleware
import (
"errors"
"github.com/goravel/framework/auth"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/models"
)
// Jwt 确保通过 JWT 鉴权
func Jwt() http.Middleware {
return func(ctx http.Context) {
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,
"message": "未登录",
})
return
}
// JWT 鉴权
if _, err := facades.Auth().Parse(ctx, token); err != nil {
if errors.Is(err, auth.ErrorTokenExpired) {
token, err = facades.Auth().Refresh(ctx)
if err != nil {
// Refresh time exceeded
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": 401,
"message": "登录已过期",
})
return
}
token = "Bearer " + token
} else {
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": 401,
"message": "登录已过期",
})
return
}
}
// 取出用户信息
var user models.User
if err := facades.Auth().User(ctx, &user); err != nil {
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
"code": 403,
"message": "用户不存在",
})
return
}
ctx.WithValue("user", user)
ctx.Response().Header("Authorization", token)
ctx.Request().Next()
}
}
+18
View File
@@ -0,0 +1,18 @@
package middleware
import (
"github.com/gin-contrib/static"
contractshttp "github.com/goravel/framework/contracts/http"
frameworkhttp "github.com/goravel/framework/http"
"panel/app/services"
)
func Static() contractshttp.Middleware {
return func(ctx contractshttp.Context) {
static.Serve(services.NewSettingImpl().Get("entrance", "/"), static.LocalFile("/www/panel/public", false))(ctx.(*frameworkhttp.GinContext).Instance())
ctx.Request().Next()
}
}
+43
View File
@@ -0,0 +1,43 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type LoginRequest struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
CaptchaID string `json:"captcha_id" form:"captcha_id"`
Captcha string `json:"captcha" form:"captcha"`
}
func (r *LoginRequest) Authorize(ctx http.Context) error {
return nil
}
func (r *LoginRequest) Rules(ctx http.Context) map[string]string {
return map[string]string{
"username": "required",
"password": "required|min_len:8",
"captcha": "captcha:true",
}
}
func (r *LoginRequest) Messages(ctx http.Context) map[string]string {
return map[string]string{
"username.required": "登录名不能为空",
"password.required": "密码不能为空",
"password.min_len": "密码长度不能小于 8 位",
"captcha.captcha": "验证码错误",
}
}
func (r *LoginRequest) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *LoginRequest) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
+84
View File
@@ -0,0 +1,84 @@
package jobs
import (
"os/exec"
"time"
"github.com/goravel/framework/facades"
"panel/app/models"
)
// ProcessTask 处理面板任务
type ProcessTask struct {
}
// Signature The name and signature of the job.
func (receiver *ProcessTask) Signature() string {
return "process_task"
}
// Handle Execute the job.
func (receiver *ProcessTask) Handle(args ...any) error {
taskID, ok := args[0].(uint)
if !ok {
facades.Log().Error("[面板][ProcessTask] 任务ID参数错误")
return nil
}
for {
if !haveRunningTask() {
break
}
time.Sleep(5 * time.Second)
}
var task models.Task
if err := facades.Orm().Query().Where("id = ?", taskID).Get(&task); err != nil {
facades.Log().Errorf("[面板][ProcessTask] 获取任务%d失败: %s", taskID, err.Error())
return nil
}
task.Status = models.TaskStatusRunning
if err := facades.Orm().Query().Save(&task); err != nil {
facades.Log().Errorf("[面板][ProcessTask] 更新任务%d失败: %s", taskID, err.Error())
return nil
}
facades.Log().Infof("[面板][ProcessTask] 开始执行任务%d", taskID)
cmd := exec.Command("bash", "-c", task.Shell)
err := cmd.Run()
if err != nil {
task.Status = models.TaskStatusFailed
if err := facades.Orm().Query().Save(&task); err != nil {
facades.Log().Errorf("[面板][ProcessTask] 更新任务%d失败: %s", taskID, err.Error())
return nil
}
facades.Log().Errorf("[面板][ProcessTask] 任务%d执行失败: %s", taskID, err.Error())
return nil
}
task.Status = models.TaskStatusSuccess
if err := facades.Orm().Query().Save(&task); err != nil {
facades.Log().Errorf("[面板][ProcessTask] 更新任务%d失败: %s", taskID, err.Error())
return nil
}
facades.Log().Infof("[面板][ProcessTask] 任务%d执行成功", taskID)
return nil
}
// haveRunningTask 是否有任务正在执行
func haveRunningTask() bool {
var task models.Task
if err := facades.Orm().Query().Where("status = ?", models.TaskStatusRunning).Get(&task); err != nil {
facades.Log().Error("[面板][ProcessTask] 获取任务失败: " + err.Error())
return true
}
if task.ID != 0 {
return true
}
return false
}
+17
View File
@@ -0,0 +1,17 @@
package models
import (
"github.com/goravel/framework/support/carbon"
)
type Cron struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null;unique" json:"name"`
Status bool `gorm:"not null;default:false" json:"status"`
Type string `gorm:"not null" json:"type"`
Time string `gorm:"not null" json:"time"`
Shell string `gorm:"default:''" json:"shell"`
Log string `gorm:"default:''" json:"log"`
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
}
+16
View File
@@ -0,0 +1,16 @@
package models
import "github.com/goravel/framework/support/carbon"
type Database struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"unique;not null" json:"name"`
Type string `gorm:"not null;index" json:"type"`
Host string `gorm:"not null" json:"host"`
Port int `gorm:"not null" json:"port"`
Username string `gorm:"not null" json:"username"`
Password string `gorm:"default:''" json:"password"`
Remark string `gorm:"default:''" json:"remark"`
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
}
+14
View File
@@ -0,0 +1,14 @@
package models
import (
"github.com/goravel/framework/support/carbon"
"panel/pkg/tools"
)
type Monitor struct {
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"`
}
+13
View File
@@ -0,0 +1,13 @@
package models
import "github.com/goravel/framework/support/carbon"
type Plugin struct {
ID uint `gorm:"primaryKey" json:"id"`
Slug string `gorm:"unique;not null" json:"slug"`
Version string `gorm:"not null" json:"version"`
Show bool `gorm:"default:false;not null" json:"show"`
ShowOrder int `gorm:"default:0;not null" json:"show_order"`
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
}

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