Compare commits

..

58 Commits

Author SHA1 Message Date
耗子 a8b8a1e789 fix: cdn domain 2023-06-21 14:01:03 +08:00
耗子 4b9abaf9df 特性(命令行):优化数据库备份流程 2023-02-09 17:24:43 +08:00
耗子 75dbf21148 特性(更新日志):修改描述 2023-01-30 11:36:33 +08:00
耗子 d4fd16060c 特性(主页):新增检查更新 2023-01-30 11:29:54 +08:00
耗子 b0bbd0f43e 特性(主页):2023新年快乐 2023-01-30 11:08:23 +08:00
耗子 9175f4c074 修复(命令行):修复备份过程中逻辑错误 2023-01-30 11:07:47 +08:00
耗子 e11c4a4456 特性:规范HTML标签格式及更新许可证 2023-01-15 23:22:39 +08:00
耗子 a77f8c6ecc 特性(命令行):优化更新和备份流程 2023-01-13 10:59:11 +08:00
耗子 0864e2fa1d 特性(网站管理):代码格式化 2023-01-08 00:46:54 +08:00
耗子 8a82b01af0 特性(网站管理):调整删除网站顺序 2023-01-08 00:30:10 +08:00
耗子 255892a0bf 特性(网站管理):修复网站删除报错的Bug及规范网站名称 2023-01-08 00:25:58 +08:00
耗子 abe00725e4 特性(网站管理):修复网站删除报错的Bug及规范网站名称 2023-01-08 00:25:51 +08:00
耗子 861077bfee 特性(全局):增强面板安全性 2023-01-08 00:21:42 +08:00
耗子 d7e7cd1485 特性(全局):增强面板安全性 2023-01-04 22:47:53 +08:00
耗子 45161c522c 特性(全局):一波更新 2022-12-22 22:46:32 +08:00
耗子 330d9bbb87 修复(网站管理):创建网站可能重复添加listen的Bug 2022-12-13 00:09:06 +08:00
耗子 6e7cb261e3 修复(命令行):修复用户判断bug 2022-12-10 13:24:35 +08:00
耗子 a1a8659258 特性(全局):增强安全性 2022-12-10 13:18:13 +08:00
耗子 f91d079296 特性(全局):增强安全性 2022-12-10 13:16:31 +08:00
耗子 d4de5c6697 特性(全局):增强安全性 2022-12-09 01:24:37 +08:00
耗子 7a01ee6f45 特性(网站管理):新增免费SSL证书申请 2022-12-08 00:11:07 +08:00
耗子 66957ca86d 特性(网站管理):新增免费SSL证书申请 2022-12-08 00:10:28 +08:00
耗子 ab85294dd6 特性(系统安全):防火墙支持端口范围添加 2022-12-07 17:13:38 +08:00
耗子 ed36715d90 特性(插件中心):优化首页显示开关显示 2022-12-06 17:41:15 +08:00
耗子 99df59eb1d 修复(命令行):优化更新流程 2022-12-05 10:09:19 +08:00
耗子 371612e6c0 特性(全局):更新版本号 2022-12-05 00:22:04 +08:00
耗子 c28c79ac62 特性(全局):新增备份支持 2022-12-05 00:21:29 +08:00
耗子 182d811664 特性(资源监控):新增时间选择 2022-12-03 02:10:02 +08:00
耗子 beb333e74f 特性(资源监控):新增时间选择 2022-12-03 02:09:22 +08:00
耗子 7df1fab7ff 特性(资源监控):新增时间选择 2022-12-03 02:09:01 +08:00
耗子 642d3339fc 特性(网站):新增备份功能及问题修复 2022-12-02 23:55:45 +08:00
耗子 b4b43cb4c4 修复(系统安全):禁用Ping不生效的Bug 2022-12-02 15:25:38 +08:00
耗子 939867d100 特性(计划任务):新增计划任务模块 2022-12-02 03:56:43 +08:00
耗子 53baea1422 特性(设置):面板API 2022-12-01 21:25:10 +08:00
耗子 3b5eb5ecf3 修复(网站):网站名可以编辑的bug 2022-12-01 20:23:50 +08:00
耗子 85997100ed 修复(网站):目录必须为/开头 2022-12-01 20:19:59 +08:00
耗子 408d486e76 修复(网站):添加网站数据库不能为空的Bug 2022-12-01 19:44:22 +08:00
耗子 5a22d0c966 特性(自述文件):更新安装脚本 2022-12-01 15:45:29 +08:00
耗子 b32f8ececf 特性(命令行):优化升级流程 2022-12-01 15:04:31 +08:00
耗子 0e4f5cc29d 特性(命令行):优化升级流程 2022-12-01 14:43:00 +08:00
耗子 d5eed03dae 特性(设置):支持修改面板端口 2022-12-01 14:41:14 +08:00
耗子 3c2c928e83 特性(全局):修复登录后表格无法鉴权的bug 2022-12-01 14:09:45 +08:00
耗子 268d9fe2f0 特性(网站):删除不必要的控制台输出 2022-12-01 13:57:11 +08:00
耗子 289831540b 特性(监控):重构系统状态获取 2022-12-01 13:56:55 +08:00
耗子 73510f5972 特性(全局):更新版本号 2022-12-01 02:47:25 +08:00
耗子 6faf2950b8 特性(设置):新增修改用户信息 2022-12-01 02:44:40 +08:00
耗子 a52f9029f2 特性(网站):新增PostgreSQL支持,调整编辑器CSS兼容性 2022-12-01 02:44:16 +08:00
耗子 ae3addbc40 特性(监控):CPU和内存图表设置y轴为其最大值 2022-12-01 02:00:39 +08:00
耗子 bfe2cf3472 修复(监控):在窗口大小改变时,重置图表大小 2022-12-01 01:44:40 +08:00
耗子 1aafa756c6 特性:新增清理所有运行中和等待中的任务命令行 2022-12-01 00:56:40 +08:00
耗子 36b80c4b6f 特性:调整任务队列与取消API限流 2022-12-01 00:16:20 +08:00
耗子 329f58da4b 特性:发布 20221130 版本 2022-11-30 22:15:32 +08:00
耗子 699562d504 修复:新建网站PHP选择00时报错的bug 2022-11-22 01:23:36 +08:00
耗子 c500dd292e 修复:新建网站不会新建数据库的bug 2022-11-22 01:16:53 +08:00
耗子 b0008444cd 特性:修复线上问题 2022-11-22 01:07:18 +08:00
耗子 397d96d0ed 特性:修复一堆问题 2022-11-21 23:22:39 +08:00
耗子 e9ffcac3b1 特性:调整部分shell命令以兼容新安装脚本 2022-11-18 14:46:01 +08:00
耗子 9c63a56b14 特性:首次提交 2022-11-17 00:18:56 +08:00
375 changed files with 24940 additions and 18216 deletions
-32
View File
@@ -1,32 +0,0 @@
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
@@ -1,143 +0,0 @@
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
@@ -1,5 +0,0 @@
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)
@@ -1,64 +0,0 @@
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
@@ -1,25 +0,0 @@
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: gomod
directory: /
labels:
- "🤖 Dependencies"
schedule:
interval: daily
groups:
go-modules:
patterns:
- "*"
open-pull-requests-limit: 100
- package-ecosystem: github-actions
directory: /
labels:
- "🤖 Dependencies"
schedule:
interval: daily
groups:
github-actions:
patterns:
- "*"
open-pull-requests-limit: 100
-32
View File
@@ -1,32 +0,0 @@
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
@@ -1,23 +0,0 @@
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
@@ -1,22 +0,0 @@
name: Code Review
permissions:
contents: read
pull-requests: write
on:
pull_request:
types: [ opened, reopened, synchronize, labeled ]
jobs:
test:
if: ${{ contains(github.event.pull_request.labels.*.name, '🚀 Review Ready') }}
runs-on: ubuntu-latest
steps:
- uses: anc95/ChatGPT-CodeReview@main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_API_ENDPOINT: https://api.openai-sb.com/v1
MODEL: gpt-3.5-turbo
PROMPT: "Below is a code patch, please help me do a brief code review on it. Only answer important bug risks and/or important improvement suggestions, Answer in Chinese and answer must be concisely and short: "
-26
View File
@@ -1,26 +0,0 @@
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
@@ -1,44 +0,0 @@
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-tools@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-tools@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
@@ -1,25 +0,0 @@
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-tools@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
@@ -1,26 +0,0 @@
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
@@ -1,11 +0,0 @@
name: PR Check Title
on:
pull_request:
jobs:
pr-check-title:
runs-on: ubuntu-latest
steps:
- uses: Slashgear/action-check-pr-title@v4.3.0
with:
regexp: "^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release|revert)(\\(.+\\))?!?: .+"
helpMessage: "Your PR title is invalid. Please follow the Conventional Commits specification: https://www.conventionalcommits.org/zh-hans/v1.0.0/"
-18
View File
@@ -1,18 +0,0 @@
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 ./...
+2 -74
View File
@@ -1,75 +1,3 @@
tmp
.env
.history
.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/
# 系统生成文件 #
.idea
.DS_Store
.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/
vendor/
-52
View File
@@ -1,52 +0,0 @@
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
@@ -1,33 +0,0 @@
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
@@ -0,0 +1,98 @@
# 更新日志
所有重要的更改都将在此文件中记录。
## [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 2022 HaoZi Technology Co., Ltd.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+20 -54
View File
@@ -1,69 +1,35 @@
# 耗子Linux面板
耗子Linux面板
===============
[![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)
这是耗子Linux面板的开源仓库,基于Apache License 2.0协议进行开源,目前处于积极开发状态。
#### 这是耗子Linux面板的开源仓库,基于Apache License 2.0协议进行开源,目前处于积极开发状态。
#### 我们正在进行基于 Golang 的 V2 版本的开发,加入我们的 交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[耗子](https://pd.qq.com/s/fyol46wfy) 获取最新动态。
广告: [`WeAvatar` —统一头像服务](https://weavatar.com)
## 运行环境
面板仅支持主流 `amd64` | `arm64` 系统的最新版本,且随着系统版本的更新,面板也会逐步停止对旧版本的支持。
| 系统 | 版本 |
|---------------|-----|
| CentOS Stream | 9 |
| CentOS Stream | 8 |
| RockyLinux | 9 |
| RockyLinux | 8 |
| AlmaLinux | 9 |
| AlmaLinux | 8 |
| 其他RHEL发行版 | 8 |
| 其他RHEL发行版 | 9 |
不支持 `Ubuntu`,因为其发版太频繁,难以维护。
低配机器建议使用 `Debian`,资源占用较 `RHEL` 系更低。其他机器建议使用 `RockyLinux` | `AlmaLinux`,维护周期更长。
不在下表中的其他同版本的 `RHEL` 系统(OpenCloudOS、Anolis、CentOS Stream等),可自行尝试安装,但不保证能够正常运行,且不提供技术支持。
| 系统 | 版本 |
|------------|----|
| RockyLinux | 9 |
| AlmaLinux | 9 |
| Debian | 12 |
## 安装面板
广告: [`WeAvatar` —统一头像服务](https://weavatar.com)
服务器赞助:
[![](https://img-cdn.haozi.xyz/2022/12/09/54a1b368700423a992789eca4af8b7e2.jpg)](http://www.ddunyun.com/aff/PNYAXMKI)
## 安装说明
```shell
TODO
wget -O install_panel.sh https://dl.panel.haozi.xyz/script/install_panel.sh && bash install_panel.sh
```
## 日常维护
## 更新说明
```shell
TODO
panel update
```
## 问题反馈
使用类问题,可在 [WePublish社区论坛](https://wepublish.cn/forums) 提问寻求帮助。
对于面板自身问题,可在 GitHub 的`Issues`
页面提交问题反馈,注意[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。
交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[耗子](https://pd.qq.com/s/fyol46wfy)
## 赞助商
### 服务器
- [盾云](https://www.ddunyun.com/)
### CDN
- [盾云CDN](http://cdn.ddunyun.com/)
- [又拍云](https://www.upyun.com/)
#### 接受云资源和资金赞助,可通过QQ群咨询联系
## Star 趋势
[![Star 趋势](https://starchart.cc/HaoZi-Team/Panel.svg)](https://starchart.cc/HaoZi-Team/Panel)
+142
View File
@@ -0,0 +1,142 @@
<?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
@@ -0,0 +1,537 @@
<?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
@@ -0,0 +1,59 @@
<?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
@@ -0,0 +1,50 @@
<?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) {
//
});
}
}
@@ -0,0 +1,212 @@
<?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);
}
}
@@ -0,0 +1,504 @@
<?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']);
}
}
@@ -0,0 +1,326 @@
<?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);
}
}
@@ -0,0 +1,109 @@
<?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);
}
}
@@ -0,0 +1,323 @@
<?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);
}
}
@@ -0,0 +1,280 @@
<?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);
}
}
@@ -0,0 +1,165 @@
<?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);
}
}
@@ -0,0 +1,120 @@
<?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);
}
}
@@ -0,0 +1,62 @@
<?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
@@ -0,0 +1,13 @@
<?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
@@ -0,0 +1,67 @@
<?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
@@ -0,0 +1,21 @@
<?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
@@ -0,0 +1,27 @@
<?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
@@ -0,0 +1,17 @@
<?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 = [
//
];
}
@@ -0,0 +1,17 @@
<?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 = [
//
];
}
@@ -0,0 +1,32 @@
<?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
@@ -0,0 +1,19 @@
<?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
@@ -0,0 +1,20 @@
<?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
@@ -0,0 +1,28 @@
<?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
@@ -0,0 +1,22 @@
<?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
@@ -0,0 +1,17 @@
<?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
@@ -0,0 +1,73 @@
<?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
@@ -0,0 +1,22 @@
<?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
@@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Database extends Model
{
use HasFactory;
}
+18
View File
@@ -0,0 +1,18 @@
<?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
@@ -0,0 +1,14 @@
<?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
@@ -0,0 +1,14 @@
<?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
@@ -0,0 +1,22 @@
<?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
@@ -0,0 +1,43 @@
<?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
@@ -0,0 +1,14 @@
<?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
@@ -0,0 +1,28 @@
<?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
@@ -0,0 +1,30 @@
<?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();
//
}
}
@@ -0,0 +1,21 @@
<?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
@@ -0,0 +1,42 @@
<?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
@@ -0,0 +1,69 @@
<?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
@@ -0,0 +1,52 @@
<?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
@@ -0,0 +1,165 @@
<?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
@@ -1,77 +0,0 @@
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
}
-305
View File
@@ -1,305 +0,0 @@
package commands
import (
"os"
"github.com/gookit/color"
"github.com/spf13/cast"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/facades"
"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)
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":
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 "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 "cleanRunningTask":
_, err := facades.Orm().Query().Model(&models.Task{}).Where("status", models.TaskStatusRunning).Update("status", models.TaskStatusFailed)
if err != nil {
color.Redln("清理正在运行的任务失败")
return nil
}
color.Greenln("清理正在运行的任务成功")
case "backup":
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 cleanRunningTask 强制清理面板正在运行的任务")
color.Greenln("panel backup {website/mysql/postgresql} {name} {path} 备份网站/MySQL数据库/PostgreSQL数据库到指定目录")
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
@@ -1,25 +0,0 @@
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
@@ -0,0 +1,70 @@
<?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);
}
-249
View File
@@ -1,249 +0,0 @@
package controllers
import (
"regexp"
"strconv"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type CronController struct {
cron services.Cron
}
func NewCronController() *CronController {
return &CronController{
cron: services.NewCronImpl(),
}
}
func (r *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 (r *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",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().All())
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
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", ctx.Request().Input("script"), 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 = "shell"
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
}
r.cron.AddToSystem(cron)
Success(ctx, http.Json{
"id": cron.ID,
})
}
// Script 获取脚本内容
func (r *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 (r *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().All())
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)
r.cron.DeleteFromSystem(cron)
if cron.Status {
r.cron.AddToSystem(cron)
}
Success(ctx, nil)
}
func (r *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
}
r.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 (r *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
}
r.cron.DeleteFromSystem(cron)
if cron.Status {
r.cron.AddToSystem(cron)
}
Success(ctx, nil)
}
func (r *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
@@ -1,18 +0,0 @@
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
@@ -1,73 +0,0 @@
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
}
-174
View File
@@ -1,174 +0,0 @@
package controllers
import (
"fmt"
"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: "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 {
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) {
proxy := ctx.Request().InputBool("proxy")
err := tools.UpdatePanel(proxy)
if err != nil {
Error(ctx, http.StatusInternalServerError, "更新失败")
return
}
Success(ctx, nil)
}
-168
View File
@@ -1,168 +0,0 @@
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 {
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 {
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)))
data.Net.Recv = append(data.Net.Recv, fmt.Sprintf("%.2f", float64(bytesRecv2/1024)))
// 监控频率为 1 分钟,所以这里除以 60 即可得到每秒的流量
data.Net.Tx = append(data.Net.Tx, fmt.Sprintf("%.2f", float64(bytesSent2-bytesSent)/60/1024))
data.Net.Rx = append(data.Net.Rx, fmt.Sprintf("%.2f", float64(bytesRecv2-bytesRecv)/60/1024))
bytesSent = bytesSent2
bytesRecv = bytesRecv2
}
Success(ctx, data)
}
-287
View File
@@ -1,287 +0,0 @@
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"`
Author string `json:"author"`
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,
Author: item.Author,
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, "操作成功")
}
@@ -1,748 +0,0 @@
package mysql57
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"golang.org/x/exp/slices"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Mysql57Controller struct {
setting services.Setting
}
func NewMysql57Controller() *Mysql57Controller {
return &Mysql57Controller{
setting: services.NewSettingImpl(),
}
}
// Status 获取运行状态
func (c *Mysql57Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
status := tools.ExecShell("systemctl status mysql | 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 mysql")
status := tools.ExecShell("systemctl status mysql | 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 mysql")
status := tools.ExecShell("systemctl status mysql | 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 mysql")
status := tools.ExecShell("systemctl status mysql | 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 mysql")
status := tools.ExecShell("systemctl status mysql | 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("mysql57")
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("mysql57", 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("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索引命中率"},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
{`Open_tables\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "锁表次数"},
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, ""},
}
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 mysql | 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("mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\"")
tools.ExecShell("mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\"")
err := c.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
if err != nil {
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
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
}
out := tools.ExecShell("mysql -uroot -p" + c.setting.Get(models.SettingKeyMysqlRootPassword) + " -e \"show databases;\"")
databases := strings.Split(out, "\n")
databases = databases[1 : len(databases)-1]
systemDatabases := []string{"information_schema", "mysql", "performance_schema", "sys"}
var userDatabases []string
for _, db := range databases {
if !slices.Contains(systemDatabases, db) {
userDatabases = append(userDatabases, db)
}
}
type Database struct {
Name string
}
var dbStructs []Database
for _, db := range userDatabases {
dbStructs = append(dbStructs, Database{Name: db})
}
controllers.Success(ctx, dbStructs)
}
// 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|regex:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*(_|[^\\w])).+$",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
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().All())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\"")
controllers.Success(ctx, "删除数据库成功")
}
// BackupList 获取备份列表
func (c *Mysql57Controller) BackupList(ctx http.Context) {
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
files, err := os.ReadDir(backupPath)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
return
}
var backupFiles []map[string]string
for _, file := range files {
if file.IsDir() {
continue
}
info, err := file.Info()
if err != nil {
continue
}
backupFiles = append(backupFiles, map[string]string{
"file": file.Name(),
"size": tools.FormatBytes(float64(info.Size())),
})
}
controllers.Success(ctx, backupFiles)
}
// 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().All())
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
backupFile := backupPath + "/" + database + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
err = os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
facades.Log().Error("[MySQL57] 设置环境变量 MYSQL_PWD 失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "备份失败")
return
}
tools.ExecShell("mysqldump -uroot " + database + " > " + backupFile)
tools.ExecShell("zip -c " + backupFile + ".zip " + backupFile)
tools.RemoveFile(backupFile)
_ = os.Unsetenv("MYSQL_PWD")
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{
"file": "required|min_len:1|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
file := ctx.Request().Input("file")
tools.RemoveFile(backupPath + "/" + file)
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{
"file": "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().All())
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
file := ctx.Request().Input("file")
backupFile := backupPath + "/" + file
if !tools.Exists(backupFile) {
controllers.Error(ctx, http.StatusBadRequest, "备份文件不存在")
return
}
err = os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
facades.Log().Error("[MYSQL57] 设置环境变量 MYSQL_PWD 失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "还原失败")
return
}
// 获取文件拓展名
ext := filepath.Ext(file)
switch ext {
case ".zip":
tools.ExecShell("unzip -o " + backupFile + " -d " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".gz":
if strings.HasSuffix(file, ".tar.gz") {
// 解压.tar.gz文件
tools.ExecShell("tar -zxvf " + backupFile + " -C " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ".tar.gz")
} else {
// 解压.gz文件
tools.ExecShell("gzip -d " + backupFile)
backupFile = strings.TrimSuffix(backupFile, ext)
}
case ".bz2":
tools.ExecShell("bzip2 -d " + backupFile)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".tar":
tools.ExecShell("tar -xvf " + backupFile + " -C " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".rar":
tools.ExecShell("unrar x " + backupFile + " " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
}
if !tools.Exists(backupFile) {
controllers.Error(ctx, http.StatusBadRequest, "自动解压备份文件失败,请手动解压")
return
}
tools.ExecShell("mysql -uroot " + ctx.Request().Input("database") + " < " + backupFile)
_ = os.Unsetenv("MYSQL_PWD")
controllers.Success(ctx, "还原成功")
}
// UserList 用户列表
func (c *Mysql57Controller) UserList(ctx http.Context) {
if !controllers.Check(ctx, "mysql57") {
return
}
type User struct {
Username string `json:"username"`
Host string `json:"host"`
Privileges string `json:"privileges"`
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
out := tools.ExecShell("mysql -uroot -p" + rootPassword + " -e 'select user,host from mysql.user'")
rawUsers := strings.Split(out, "\n")
users := make([]User, 0)
for _, rawUser := range rawUsers {
user := strings.Split(rawUser, "\t")
if user[0] == "root" || user[0] == "mysql.sys" || user[0] == "mysql.infoschema" || user[0] == "mysql.session" {
continue
}
out := tools.ExecShell("mysql -uroot -p" + rootPassword + " -e 'show grants for " + user[0] + "@" + user[1] + "'")
rawPrivileges := strings.Split(out, "\n")
privileges := make([]string, 0)
for _, rawPrivilege := range rawPrivileges {
if rawPrivilege == "" {
continue
}
privilege := rawPrivilege[6:strings.Index(rawPrivilege, " TO")]
privileges = append(privileges, privilege)
}
users = append(users, User{Username: user[0], Host: user[1], Privileges: strings.Join(privileges, " | ")})
}
controllers.Success(ctx, users)
}
// 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|regex:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*(_|[^\\w])).+$",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
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().All())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
tools.ExecShell("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|regex:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*(_|[^\\w])).+$",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
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().All())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
database := ctx.Request().Input("database")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "修改成功")
}
@@ -1,748 +0,0 @@
package mysql80
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"golang.org/x/exp/slices"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Mysql80Controller struct {
setting services.Setting
}
func NewMysql80Controller() *Mysql80Controller {
return &Mysql80Controller{
setting: services.NewSettingImpl(),
}
}
// Status 获取运行状态
func (c *Mysql80Controller) Status(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
status := tools.ExecShell("systemctl status mysql | 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 mysql")
status := tools.ExecShell("systemctl status mysql | 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 mysql")
status := tools.ExecShell("systemctl status mysql | 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 mysql")
status := tools.ExecShell("systemctl status mysql | 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 mysql")
status := tools.ExecShell("systemctl status mysql | 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("mysql80")
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("mysql80", 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("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索引命中率"},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
{`Open_tables\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "锁表次数"},
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, ""},
}
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 mysql | 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("mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\"")
tools.ExecShell("mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\"")
err := c.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
if err != nil {
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
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
}
out := tools.ExecShell("mysql -uroot -p" + c.setting.Get(models.SettingKeyMysqlRootPassword) + " -e \"show databases;\"")
databases := strings.Split(out, "\n")
databases = databases[1 : len(databases)-1]
systemDatabases := []string{"information_schema", "mysql", "performance_schema", "sys"}
var userDatabases []string
for _, db := range databases {
if !slices.Contains(systemDatabases, db) {
userDatabases = append(userDatabases, db)
}
}
type Database struct {
Name string
}
var dbStructs []Database
for _, db := range userDatabases {
dbStructs = append(dbStructs, Database{Name: db})
}
controllers.Success(ctx, dbStructs)
}
// 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|regex:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*(_|[^\\w])).+$",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
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().All())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\"")
controllers.Success(ctx, "删除数据库成功")
}
// BackupList 获取备份列表
func (c *Mysql80Controller) BackupList(ctx http.Context) {
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
files, err := os.ReadDir(backupPath)
if err != nil {
controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
return
}
var backupFiles []map[string]string
for _, file := range files {
if file.IsDir() {
continue
}
info, err := file.Info()
if err != nil {
continue
}
backupFiles = append(backupFiles, map[string]string{
"file": file.Name(),
"size": tools.FormatBytes(float64(info.Size())),
})
}
controllers.Success(ctx, backupFiles)
}
// 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().All())
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
backupFile := backupPath + "/" + database + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
if !tools.Exists(backupPath) {
tools.Mkdir(backupPath, 0644)
}
err = os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
facades.Log().Error("[MySQL80] 设置环境变量 MYSQL_PWD 失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "备份失败")
return
}
tools.ExecShell("mysqldump -uroot " + database + " > " + backupFile)
tools.ExecShell("zip -c " + backupFile + ".zip " + backupFile)
tools.RemoveFile(backupFile)
_ = os.Unsetenv("MYSQL_PWD")
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{
"file": "required|min_len:1|max_len:255",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
file := ctx.Request().Input("file")
tools.RemoveFile(backupPath + "/" + file)
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{
"file": "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().All())
return
}
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
file := ctx.Request().Input("file")
backupFile := backupPath + "/" + file
if !tools.Exists(backupFile) {
controllers.Error(ctx, http.StatusBadRequest, "备份文件不存在")
return
}
err = os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
facades.Log().Error("[MySQL80] 设置环境变量 MYSQL_PWD 失败:" + err.Error())
controllers.Error(ctx, http.StatusInternalServerError, "还原失败")
return
}
// 获取文件拓展名
ext := filepath.Ext(file)
switch ext {
case ".zip":
tools.ExecShell("unzip -o " + backupFile + " -d " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".gz":
if strings.HasSuffix(file, ".tar.gz") {
// 解压.tar.gz文件
tools.ExecShell("tar -zxvf " + backupFile + " -C " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ".tar.gz")
} else {
// 解压.gz文件
tools.ExecShell("gzip -d " + backupFile)
backupFile = strings.TrimSuffix(backupFile, ext)
}
case ".bz2":
tools.ExecShell("bzip2 -d " + backupFile)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".tar":
tools.ExecShell("tar -xvf " + backupFile + " -C " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
case ".rar":
tools.ExecShell("unrar x " + backupFile + " " + backupPath)
backupFile = strings.TrimSuffix(backupFile, ext)
}
if !tools.Exists(backupFile) {
controllers.Error(ctx, http.StatusBadRequest, "自动解压备份文件失败,请手动解压")
return
}
tools.ExecShell("mysql -uroot " + ctx.Request().Input("database") + " < " + backupFile)
_ = os.Unsetenv("MYSQL_PWD")
controllers.Success(ctx, "还原成功")
}
// UserList 用户列表
func (c *Mysql80Controller) UserList(ctx http.Context) {
if !controllers.Check(ctx, "mysql80") {
return
}
type User struct {
Username string `json:"username"`
Host string `json:"host"`
Privileges string `json:"privileges"`
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
out := tools.ExecShell("mysql -uroot -p" + rootPassword + " -e 'select user,host from mysql.user'")
rawUsers := strings.Split(out, "\n")
users := make([]User, 0)
for _, rawUser := range rawUsers {
user := strings.Split(rawUser, "\t")
if user[0] == "root" || user[0] == "mysql.sys" || user[0] == "mysql.infoschema" || user[0] == "mysql.session" {
continue
}
out := tools.ExecShell("mysql -uroot -p" + rootPassword + " -e 'show grants for " + user[0] + "@" + user[1] + "'")
rawPrivileges := strings.Split(out, "\n")
privileges := make([]string, 0)
for _, rawPrivilege := range rawPrivileges {
if rawPrivilege == "" {
continue
}
privilege := rawPrivilege[6:strings.Index(rawPrivilege, " TO")]
privileges = append(privileges, privilege)
}
users = append(users, User{Username: user[0], Host: user[1], Privileges: strings.Join(privileges, " | ")})
}
controllers.Success(ctx, users)
}
// 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|regex:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*(_|[^\\w])).+$",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
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().All())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
tools.ExecShell("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|regex:^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*(_|[^\\w])).+$",
})
if err != nil {
controllers.Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
controllers.Error(ctx, http.StatusBadRequest, validator.Errors().All())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
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().All())
return
}
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
database := ctx.Request().Input("database")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
tools.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
controllers.Success(ctx, "修改成功")
}
@@ -1,260 +0,0 @@
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)
}
@@ -1,372 +0,0 @@
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
}
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) 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_" + c.version + "_status")
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].Name == item {
extensions[i].Installed = true
}
}
}
}
return extensions
}
@@ -1,372 +0,0 @@
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
}
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) 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_" + c.version + "_status")
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].Name == item {
extensions[i].Installed = true
}
}
}
}
return extensions
}
-258
View File
@@ -1,258 +0,0 @@
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
}
if tools.IsRHEL() {
out := tools.ExecShell("firewall-cmd --list-all 2>&1")
match := regexp.MustCompile(`ports: (.*)`).FindStringSubmatch(out)
if len(match) == 0 {
Success(ctx, nil)
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],
})
}
Success(ctx, rules)
} else {
out := tools.ExecShell("ufw status | grep -v '(v6)' | grep ALLOW | awk '{print $1}'")
if len(out) == 0 {
Success(ctx, nil)
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],
})
}
Success(ctx, rules)
}
}
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 --query-rich-rule='rule protocol value=icmp drop' 2>&1")
if out == "no" {
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
@@ -1,141 +0,0 @@
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)
}
-80
View File
@@ -1,80 +0,0 @@
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
@@ -1,78 +0,0 @@
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.StatusUnauthorized, "用户名或密码错误")
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,
})
}
-627
View File
@@ -1,627 +0,0 @@
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(),
}
}
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,
})
}
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": "required",
"db_type": "required_if:db,true",
"db_name": "required_if:db,true",
"db_user": "required_if:db,true",
"db_password": "required_if:db,true",
})
if err != nil {
Error(ctx, http.StatusBadRequest, err.Error())
return
}
if validator.Fails() {
Error(ctx, http.StatusBadRequest, validator.Errors().All())
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)
}
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)
}
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,
})
}
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)
}
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)
}
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], path, -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)
// 防跨站
if !strings.HasSuffix(path, "/") {
path += "/"
}
if ctx.Request().InputBool("open_basedir") {
tools.WriteFile(path+".user.ini", "open_basedir="+path+":/tmp/", 0644)
} else {
if tools.Exists(path + ".user.ini") {
tools.RemoveFile(path + ".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
if ssl {
tools.WriteFile("/www/server/vhost/ssl/"+website.Name+".pem", ctx.Request().Input("ssl_certificate"), 0644)
tools.WriteFile("/www/server/vhost/ssl/"+website.Name+".key", ctx.Request().Input("ssl_certificate_key"), 0644)
sslConfig := `# ssl标记位开始
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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
@@ -1,18 +0,0 @@
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
@@ -1,63 +0,0 @@
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", ""))
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.StatusUnauthorized, http.Json{
"code": 401,
"message": "登录已过期",
})
return
}
token = "Bearer " + token
} else {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, 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
@@ -1,18 +0,0 @@
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
@@ -1,43 +0,0 @@
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
@@ -1,84 +0,0 @@
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
@@ -1,17 +0,0 @@
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
@@ -1,16 +0,0 @@
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
@@ -1,14 +0,0 @@
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
@@ -1,13 +0,0 @@
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"`
}
-21
View File
@@ -1,21 +0,0 @@
package models
import "github.com/goravel/framework/support/carbon"
const (
SettingKeyName = "name"
SettingKeyMonitor = "monitor"
SettingKeyMonitorDays = "monitor_days"
SettingKeyBackupPath = "backup_path"
SettingKeyWebsitePath = "website_path"
SettingKeyEntrance = "entrance"
SettingKeyMysqlRootPassword = "mysql_root_password"
)
type Setting struct {
ID uint `gorm:"primaryKey" json:"id"`
Key string `gorm:"unique;not null" json:"key"`
Value string `gorm:"default:''" json:"value"`
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
}
-22
View File
@@ -1,22 +0,0 @@
package models
import (
"github.com/goravel/framework/support/carbon"
)
const (
TaskStatusWaiting = "waiting"
TaskStatusRunning = "running"
TaskStatusSuccess = "finished"
TaskStatusFailed = "failed"
)
type Task struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
Status string `gorm:"not null;default:'waiting'" json:"status"`
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"`
}
-12
View File
@@ -1,12 +0,0 @@
package models
import "github.com/goravel/framework/support/carbon"
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"unique;not null" json:"username"`
Password string `gorm:"not null" json:"password"`
Email string `gorm:"default:''" json:"email"`
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
}
-17
View File
@@ -1,17 +0,0 @@
package models
import (
"github.com/goravel/framework/support/carbon"
)
type Website struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"unique;not null" json:"name"`
Status bool `gorm:"default:true;not null;index" json:"status"`
Path string `gorm:"not null" json:"path"`
Php int `gorm:"default:0;not null;index" json:"php"`
Ssl bool `gorm:"default:false;not null;index" json:"ssl"`
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
@@ -1,14 +0,0 @@
package mysql57
var (
Name = "MySQL-5.7"
Author = "耗子"
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。"
Slug = "mysql57"
Version = "5.7.42"
Requires = []string{}
Excludes = []string{"mysql80"}
Install = `bash /www/panel/scripts/mysql/install.sh 57`
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 57`
Update = `echo "not support now"`
)
-14
View File
@@ -1,14 +0,0 @@
package mysql80
var (
Name = "MySQL-8.0"
Author = "耗子"
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。(内存 < 4G 无法安装)"
Slug = "mysql80"
Version = "8.0.33"
Requires = []string{}
Excludes = []string{"mysql57"}
Install = `bash /www/panel/scripts/mysql/install.sh 80`
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 80`
Update = `echo "not support now"`
)
-14
View File
@@ -1,14 +0,0 @@
package openresty
var (
Name = "OpenResty"
Author = "耗子"
Description = "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台。"
Slug = "openresty"
Version = "1.21.4.1"
Requires = []string{}
Excludes = []string{}
Install = "bash /www/panel/scripts/openresty/install.sh"
Uninstall = "bash /www/panel/scripts/openresty/uninstall.sh"
Update = "bash /www/panel/scripts/openresty/install.sh"
)
-14
View File
@@ -1,14 +0,0 @@
package php74
var (
Name = "PHP-7.4"
Author = "耗子"
Description = "PHP 是世界上最好的语言!"
Slug = "php74"
Version = "7.4.33"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/php/install.sh 74`
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 74`
Update = `bash /www/panel/scripts/php/install.sh 74`
)
-14
View File
@@ -1,14 +0,0 @@
package php80
var (
Name = "PHP-8.0"
Author = "耗子"
Description = "PHP 是世界上最好的语言!"
Slug = "php80"
Version = "8.0.29"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/php/install.sh 80`
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 80`
Update = `bash /www/panel/scripts/php/install.sh 80`
)

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