mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
Compare commits
196 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45ed3f2ff9 | |||
| 5703e3354f | |||
| da2e66b7c6 | |||
| 8dca4c1cbf | |||
| a295eb9a65 | |||
| 868ee73d51 | |||
| ef7d38f2af | |||
| 801ff28854 | |||
| e0a401f688 | |||
| f50f2f7f2b | |||
| 3df1388326 | |||
| 5390ef3140 | |||
| 7cb5a3db4b | |||
| 082b0d2755 | |||
| 10ba87b63a | |||
| ec4c187a11 | |||
| 2e250dbb39 | |||
| 69938cf9bc | |||
| 3bc3efe59c | |||
| 9b6c9484e4 | |||
| e127572300 | |||
| c757dd9555 | |||
| 97c3f4c7c7 | |||
| 1e8c4945d5 | |||
| 90ef29acc4 | |||
| 89a6c3733b | |||
| 26d8aacabd | |||
| e7e5d4b283 | |||
| db0e722ea3 | |||
| ccd0aa3505 | |||
| 43f19c802f | |||
| a86d8a7490 | |||
| 08d906e27c | |||
| 70d65b2b2c | |||
| 3de92c9603 | |||
| deb9d9fbd2 | |||
| 71903c1100 | |||
| 9853c1792b | |||
| 18f0257ff2 | |||
| 0754fcaf6a | |||
| ea7bb34be7 | |||
| 383b1652cf | |||
| ca36c7c62a | |||
| c42fc0c70d | |||
| a89f5e3ca3 | |||
| 64537e1c75 | |||
| 19a57f9951 | |||
| 95a90db230 | |||
| 5dcfe4126c | |||
| d3e758f61a | |||
| d921939186 | |||
| b0a5221aed | |||
| dd4db4899d | |||
| 7f0b7b349a | |||
| 2a483bc9cd | |||
| 731a1f13a9 | |||
| 95025267aa | |||
| 4dd1bd3d46 | |||
| 633fa61738 | |||
| 35c998f402 | |||
| 1a82279304 | |||
| ac4380ef14 | |||
| 6b8228a048 | |||
| 67927eef06 | |||
| 429a027c79 | |||
| 284f6fb953 | |||
| 8194028a76 | |||
| c173f57c6d | |||
| 639159f150 | |||
| 89bd5d4b36 | |||
| 165424b4b2 | |||
| d519ede9e9 | |||
| ebe2700cfd | |||
| bf56151db5 | |||
| 53e13d1257 | |||
| 2732a476fd | |||
| e8b7128f7a | |||
| ec9bd0ebb3 | |||
| e1da619460 | |||
| a90b800ede | |||
| d64403f49a | |||
| 527ef39a92 | |||
| 82d2fab895 | |||
| bdf33163f0 | |||
| 705620ab13 | |||
| 6e2d3eed06 | |||
| 053de1b848 | |||
| 721d579512 | |||
| 8b8fbaa3ec | |||
| 4a8a4dbcde | |||
| cd241f88af | |||
| 064bf7e1ed | |||
| c1aa6e2e17 | |||
| 246e53c519 | |||
| 0810eee37a | |||
| c003b11ff3 | |||
| d39743cdb9 | |||
| 94751d474f | |||
| 63033dfa15 | |||
| 8dc41d4222 | |||
| 396fb1207e | |||
| 0ce8b15aca | |||
| 55fc516049 | |||
| a9c19115d4 | |||
| 9375860203 | |||
| 52aaadb2f9 | |||
| c7ad1a1435 | |||
| c0db1cf993 | |||
| bad23197c1 | |||
| f6dc38e61b | |||
| 40ce8a2aad | |||
| 999fda846d | |||
| 958369b5ce | |||
| dc86e2efa3 | |||
| bb23be565a | |||
| 2a40cb11f7 | |||
| 4da6969636 | |||
| bb5dd8a835 | |||
| 7af5ab3c30 | |||
| 6a328c3a42 | |||
| 51ec5e137b | |||
| 8684727267 | |||
| c884dd6fd7 | |||
| 35d2838036 | |||
| 0acf996981 | |||
| 2be12f20f2 | |||
| 07b0c7a010 | |||
| cf4bc94e6d | |||
| d89f9553e9 | |||
| e4536360bd | |||
| 8bfef8030c | |||
| 69e972fa5e | |||
| a33bd8309c | |||
| dacdfc6d45 | |||
| 1c4e34e8fe | |||
| c9f650ff66 | |||
| 255f94d588 | |||
| a3aaaa37ef | |||
| f31e3a7d8e | |||
| 1e462bf0e6 | |||
| 1ed79a86b2 | |||
| 0e4d593d6c | |||
| 849e6602dd | |||
| 4dc16b1e03 | |||
| 599f3b5cb4 | |||
| ccc50ed6ee | |||
| 8e84d2553f | |||
| d731efb082 | |||
| 11ad8e524e | |||
| 87e975df28 | |||
| 00d0cadb03 | |||
| 10db77ffd3 | |||
| 3c82631d7b | |||
| cd27c05d1c | |||
| 3a8f850a50 | |||
| 06e66eaa0a | |||
| 2fcd3db4a4 | |||
| 2ec74e774b | |||
| b26f75f3a3 | |||
| 7e685dd43c | |||
| 3d3dcbef8d | |||
| bc2166ee66 | |||
| 7cf577ff5b | |||
| 87cdda782e | |||
| 24106288cb | |||
| 3f258a5f81 | |||
| ce8addfecd | |||
| d969360b54 | |||
| 1f43a12f81 | |||
| 930b86a671 | |||
| 790e56003e | |||
| c38736abb6 | |||
| befad1d3ac | |||
| 53d8c2ff6d | |||
| d07314b5be | |||
| f04cafbcee | |||
| 86199b9eec | |||
| e4e3ad5821 | |||
| a80ddfaebc | |||
| 026e1319fe | |||
| b532064cf0 | |||
| f900ada2c6 | |||
| 468b20348a | |||
| 862686e4e2 | |||
| 5a3a1e694b | |||
| a368117e00 | |||
| 596cd68728 | |||
| d55bb5ffe1 | |||
| 94ff9e2dbc | |||
| f4cb1a850a | |||
| 7d1acae966 | |||
| 18b524bddc | |||
| 70596a7906 | |||
| fbf0b7acce | |||
| 12ad6e8879 | |||
| b4ea470099 |
+1
-1
@@ -1,4 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
open_collective: haozi-panel
|
||||
custom: ['https://afdian.net/a/haozi-team']
|
||||
custom: ['https://afdian.net/a/TheTNB']
|
||||
|
||||
@@ -13,10 +13,10 @@ jobs:
|
||||
fail-fast: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
go-version: '1.22'
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Build ${{ matrix.goarch }}
|
||||
@@ -24,9 +24,9 @@ jobs:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: linux
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: go build -ldflags '-s -w --extldflags "-static -fpic"' -o panel-${{ matrix.goarch }}
|
||||
run: go build -ldflags '-s -w --extldflags "-static"' -o panel-${{ matrix.goarch }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: panel-${{ matrix.goarch }}
|
||||
path: panel-${{ matrix.goarch }}
|
||||
|
||||
@@ -9,15 +9,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
go-version: '1.22'
|
||||
- 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
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.out
|
||||
token: ${{ secrets.CODECOV }}
|
||||
|
||||
@@ -14,13 +14,13 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
go-version: '1.22'
|
||||
- name: Fetch Frontend
|
||||
run: |
|
||||
sudo apt-get install -y curl jq unzip zip
|
||||
curl -sSL https://api.github.com/repos/haozi-team/panel-frontend/releases/latest | jq -r ".assets[] | select(.name | contains(\"dist\")) | .browser_download_url" | xargs curl -L -o frontend.zip
|
||||
curl -sSL https://api.github.com/repos/TheTNB/panel-frontend/releases/latest | jq -r ".assets[] | select(.name | contains(\"dist\")) | .browser_download_url" | xargs curl -L -o frontend.zip
|
||||
rm -rf public
|
||||
unzip frontend.zip
|
||||
mv dist public
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
我们认为您的建议非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献!
|
||||
We think your suggestion is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution!
|
||||
|
||||

|
||||

|
||||
- name: ☢️ Bug
|
||||
if: github.event.label.name == '☢️ Bug'
|
||||
uses: actions-cool/issues-helper@v3
|
||||
@@ -41,4 +41,4 @@ jobs:
|
||||
我们认为您的反馈非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献!
|
||||
We think your feedback is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution!
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -12,12 +12,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
go-version: '1.22'
|
||||
cache: false
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
with:
|
||||
skip-cache: true
|
||||
skip-pkg-cache: true
|
||||
|
||||
@@ -9,15 +9,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
go-version: '1.22'
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y curl jq
|
||||
- name: Set up environment
|
||||
run: |
|
||||
cp panel-example.conf .env
|
||||
echo "DB_FILE=$(pwd)/database/panel.db" >> .env
|
||||
echo "DB_FILE=$(pwd)/storage/panel.db" >> .env
|
||||
go run . artisan key:generate
|
||||
go run . artisan migrate
|
||||
- name: Run tests
|
||||
|
||||
+8
-14
@@ -1,4 +1,4 @@
|
||||
image: golang:bookworm
|
||||
image: golang:1.22-bookworm
|
||||
|
||||
# 在每个任务执行前运行
|
||||
before_script:
|
||||
@@ -14,12 +14,6 @@ before_script:
|
||||
paths:
|
||||
- .go/pkg/mod/
|
||||
|
||||
# 全局变量
|
||||
variables:
|
||||
OUTPUT_NAME: "panel"
|
||||
GO111MODULE: "on"
|
||||
GOPROXY: "https://goproxy.cn,direct"
|
||||
|
||||
stages:
|
||||
- prepare
|
||||
- build
|
||||
@@ -44,7 +38,7 @@ unit_test:
|
||||
- apt-get update
|
||||
- apt-get install -y curl jq
|
||||
- cp panel-example.conf .env
|
||||
- echo "DB_FILE=$(pwd)/database/panel.db" >> .env
|
||||
- echo "DB_FILE=$(pwd)/storage/panel.db" >> .env
|
||||
- go run . artisan key:generate
|
||||
- go run . artisan migrate
|
||||
- go test -v -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
@@ -54,21 +48,21 @@ build:
|
||||
extends: .go_cache
|
||||
script:
|
||||
- go mod download
|
||||
- CGO_ENABLED=0 go build -ldflags '-s -w --extldflags "-static -fpic"' -o $OUTPUT_NAME
|
||||
- CGO_ENABLED=0 go build -ldflags '-s -w --extldflags "-static"' -o panel
|
||||
artifacts:
|
||||
name: "$OUTPUT_NAME"
|
||||
name: "panel"
|
||||
paths:
|
||||
- $OUTPUT_NAME
|
||||
- panel
|
||||
expire_in: 3 days
|
||||
|
||||
fetch:
|
||||
stage: build
|
||||
image: alpine:latest
|
||||
before_script:
|
||||
- sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
|
||||
- sed -i 's/dl-cdn.alpinelinux.org/mirror.sjtu.edu.cn/g' /etc/apk/repositories
|
||||
- apk add --no-cache curl jq unzip zip
|
||||
script:
|
||||
- curl -sSL "https://jihulab.com/api/v4/projects/haozi-team%2Fpanel-frontend/releases" | jq -r '.[0].assets.links[] | select(.name | contains("dist")) | .direct_asset_url' | xargs curl -L -o frontend.zip
|
||||
- curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel-frontend/releases" | jq -r '.[0].assets.links[] | select(.name | contains("dist")) | .direct_asset_url' | xargs curl -L -o frontend.zip
|
||||
- rm -rf public
|
||||
- unzip frontend.zip
|
||||
- mv dist public
|
||||
@@ -84,7 +78,7 @@ release:
|
||||
- build
|
||||
- fetch
|
||||
image:
|
||||
name: goreleaser/goreleaser
|
||||
name: goreleaser/goreleaser:nightly
|
||||
entrypoint: [ '' ]
|
||||
only:
|
||||
- tags
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@ builds:
|
||||
goamd64:
|
||||
- v2
|
||||
ldflags:
|
||||
- -s -w --extldflags "-static -fpic"
|
||||
- -s -w --extldflags "-static"
|
||||
archives:
|
||||
-
|
||||
id: panel-archive
|
||||
@@ -33,8 +33,8 @@ archives:
|
||||
- panel-example.conf
|
||||
|
||||
gitlab_urls:
|
||||
api: https://jihulab.com/api/v4/
|
||||
download: https://jihulab.com
|
||||
api: https://git.haozi.net/api/v4/
|
||||
download: https://git.haozi.net
|
||||
|
||||
# set to true if you use a self-signed certificate
|
||||
skip_tls_verify: false
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
## 行为准则
|
||||
|
||||
耗子Linux面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
|
||||
耗子 Linux 面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
|
||||
|
||||
- 参与者将容忍反对意见。
|
||||
- 参与者必须确保他们的语言和行为没有人身攻击和贬低个人言论。
|
||||
|
||||
@@ -5,12 +5,11 @@
|
||||
<h1 align="center">耗子 Linux 面板</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/haozi-team/panel/releases"><img src="https://img.shields.io/github/release/haozi-team/panel.svg"></a>
|
||||
<a href="https://github.com/haozi-team/panel/actions"><img src="https://github.com/haozi-team/panel/actions/workflows/test.yml/badge.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/haozi-team/panel"><img src="https://goreportcard.com/badge/github.com/haozi-team/panel"></a>
|
||||
<a href="https://codecov.io/gh/haozi-team/panel"><img src="https://codecov.io/gh/haozi-team/panel/branch/main/graph/badge.svg?token=XFT5NGNSRG"></a>
|
||||
<a href="https://img.shields.io/github/license/haozi-team/panel"><img src="https://img.shields.io/github/license/haozi-team/panel"></a>
|
||||
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2Fhaozi-team%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Fhaozi-team%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
|
||||
<a href="https://github.com/TheTNB/panel/releases"><img src="https://img.shields.io/github/release/TheTNB/panel.svg"></a>
|
||||
<a href="https://github.com/TheTNB/panel/actions"><img src="https://github.com/TheTNB/panel/actions/workflows/test.yml/badge.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/TheTNB/panel"><img src="https://goreportcard.com/badge/github.com/TheTNB/panel"></a>
|
||||
<a href="https://img.shields.io/github/license/TheTNB/panel"><img src="https://img.shields.io/github/license/TheTNB/panel"></a>
|
||||
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2FTheTNB%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2FTheTNB%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
|
||||
</p>
|
||||
|
||||
耗子 Linux 面板是使用 Golang + Vue 开发的轻量 Linux 服务器运维管理面板,以 GNU Affero General Public License v3.0 开源。
|
||||
@@ -20,10 +19,12 @@
|
||||
## 优势
|
||||
|
||||
1. **极低占用:** 在 Debian 12 下部署面板 + LNMP 环境,内存占用不到 500 MB,遥遥领先于使用容器化的其他面板。
|
||||
2. **高效运维:** 面板 UI 界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
|
||||
3. **离线运行:** 面板运行可不依赖任何外部服务,你甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
|
||||
4. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 1 年无事故。
|
||||
5. **开源开放:** 面板开源,你可以自由修改、审计面板源码,安全性有保障。
|
||||
2. **低破坏性:** 面板的设计理念是尽可能减少对系统的额外修改,在同类面板中,我们对系统的修改最少。
|
||||
3. **追随时代:** 面板所有组件均走在时代前沿,更新快,功能强大,安全性有保障。
|
||||
4. **高效运维:** 面板 UI 界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
|
||||
5. **离线运行:** 面板运行可不依赖任何外部服务,你甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
|
||||
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 1 年以上无事故。
|
||||
7. **开源开放:** 面板开源,你可以自由修改、审计面板源码,安全性有保障。
|
||||
|
||||
## UI 截图
|
||||
|
||||
@@ -31,13 +32,13 @@
|
||||
|
||||
## 运行环境
|
||||
|
||||
耗子Linux面板仅支持 `amd64` | `arm64` 架构下的主流系统的最新版本,不支持 `Ubuntu`,因为其发版太过频繁,难以维护。
|
||||
耗子 Linux 面板仅支持 `amd64` | `arm64` 架构下的主流系统的最新版本,不支持 `Ubuntu`,因为其发版太过频繁,难以维护。
|
||||
|
||||
低配机器建议使用 `Debian`,资源占用较 `RHEL` 系更低。其他机器建议使用 `AlmaLinux` | `RockyLinux`,维护周期更长也更稳定。
|
||||
|
||||
不在下表中的其他系统(OpenCloudOS 8、Anolis 8、CentOS Stream 8/9、Debian 11等),可自行尝试安装,但不保证能够正常运行,且不提供无偿技术支持(理论上不会有大问题)。
|
||||
不在下表中的其他 RHEL 9.x 系统,可自行尝试安装,但不保证能够正常运行,且不提供无偿技术支持(理论上不会有大问题)。
|
||||
|
||||
CentOS Stream 可使用迁移脚本迁移至支持的系统: [CentOS 8/9 迁移脚本](https://github.com/haozi-team/byecentos)
|
||||
CentOS Stream 可使用迁移脚本迁移至支持的系统: [CentOS 8/9 迁移脚本](https://github.com/TheTNB/byecentos)
|
||||
|
||||
| 系统 | 版本 |
|
||||
|------------|----|
|
||||
@@ -51,12 +52,12 @@ CentOS Stream 可使用迁移脚本迁移至支持的系统: [CentOS 8/9 迁移
|
||||
## 安装面板
|
||||
|
||||
> **Warning**
|
||||
> 安装面板前,你需要了解LNMP环境的基本知识,以及如何处理常见的LNMP环境问题,不建议0基础的用户安装和使用耗子Linux面板。
|
||||
> 安装面板前,你需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,不建议 0 基础的用户安装和使用耗子 Linux 面板。
|
||||
|
||||
如果你决定继续,请以`root`用户登录服务器,执行以下命令安装面板:
|
||||
|
||||
```shell
|
||||
HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
|
||||
HAOZI_DL_URL="https://git.haozi.net/opensource/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
|
||||
```
|
||||
|
||||
## 卸载面板
|
||||
@@ -66,7 +67,7 @@ HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -s
|
||||
如果你无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
|
||||
|
||||
```shell
|
||||
HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
|
||||
HAOZI_DL_URL="https://git.haozi.net/opensource/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
|
||||
```
|
||||
|
||||
卸载面板前请务必备份好所有数据,提前卸载面板全部插件。卸载后数据将**无法恢复**!
|
||||
@@ -79,38 +80,28 @@ HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -s
|
||||
panel
|
||||
```
|
||||
|
||||
在 [Wiki](https://github.com/haozi-team/panel/wiki) 中查看更多使用方法和技巧。
|
||||
在 [Wiki](https://github.com/TheTNB/panel/wiki) 中查看更多使用方法和技巧。
|
||||
|
||||
## 问题反馈
|
||||
|
||||
使用类问题,可在 [WePublish 社区论坛](https://wepublish.cn/forums) 提问或QQ群`@坤坤`寻求 AI 帮助,亦可在群里寻求付费支持。
|
||||
使用类问题,可在 [鼠片社区](https://forum.haozi.net) 提问或QQ群`@坤坤`寻求 AI 帮助,亦可在群里寻求付费支持。
|
||||
|
||||
面板自身问题,可在 GitHub 的`Issues`页面提交问题反馈,注意[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。
|
||||
|
||||
## 贡献代码
|
||||
|
||||
### 寻找/创建 Issue
|
||||
|
||||
您可以在 [Issue 列表](https://github.com/haozi-team/panel/issues) 中寻找或创建一个 Issue,留言表达想要处理该 Issue 的意愿,得到维护者的确认后,即可开始处理。
|
||||
|
||||
### 创建 PR
|
||||
|
||||
- 在开发过程中,如果遇到问题可以随时在 Issue 中详尽描述该问题,以进一步沟通,但在此之前请确保自己已通过 Google 等方式尽可能的尝试解决问题;
|
||||
- PR 须提交至我们的极狐 GitLab 仓库[https://jihulab.com/haozi-team/panel](https://jihulab.com/haozi-team/panel),勿在 GitHub 上提交;
|
||||
- 当 PR 开发完毕后,请为其添加 `🚀 Review Ready` 标签,维护者将及时进行评审;
|
||||
- 我们非常欢迎您的贡献,将在下次发版时将您添加到首页贡献者中;❤️
|
||||
|
||||
## 赞助商
|
||||
|
||||
### 资金
|
||||
|
||||
- [微晓朵](https://www.weixiaoduo.com/)
|
||||
|
||||
### CDN
|
||||
|
||||
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
|
||||
|
||||
- [盾云CDN](http://cdn.ddunyun.com/)
|
||||
|
||||
### DevOps
|
||||
|
||||
- [极狐 GitLab](https://www.jihulab.com/)
|
||||
- [JetBrains](https://www.jetbrains.com/)
|
||||
|
||||
**接受云资源和资金赞助,可通过QQ群咨询联系**
|
||||
|
||||
@@ -122,11 +113,11 @@ panel
|
||||
|
||||
## Star 历史
|
||||
|
||||
<a href="https://star-history.com/#haozi-team/panel&Date">
|
||||
<a href="https://star-history.com/#TheTNB/panel&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
+26
-35
@@ -5,12 +5,11 @@
|
||||
<h1 align="center">HaoZi Linux Panel</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/haozi-team/panel/releases"><img src="https://img.shields.io/github/release/haozi-team/panel.svg"></a>
|
||||
<a href="https://github.com/haozi-team/panel/actions"><img src="https://github.com/haozi-team/panel/actions/workflows/test.yml/badge.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/haozi-team/panel"><img src="https://goreportcard.com/badge/github.com/haozi-team/panel"></a>
|
||||
<a href="https://codecov.io/gh/haozi-team/panel"><img src="https://codecov.io/gh/haozi-team/panel/branch/main/graph/badge.svg?token=XFT5NGNSRG"></a>
|
||||
<a href="https://img.shields.io/github/license/haozi-team/panel"><img src="https://img.shields.io/github/license/haozi-team/panel"></a>
|
||||
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2Fhaozi-team%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Fhaozi-team%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
|
||||
<a href="https://github.com/TheTNB/panel/releases"><img src="https://img.shields.io/github/release/TheTNB/panel.svg"></a>
|
||||
<a href="https://github.com/TheTNB/panel/actions"><img src="https://github.com/TheTNB/panel/actions/workflows/test.yml/badge.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/TheTNB/panel"><img src="https://goreportcard.com/badge/github.com/TheTNB/panel"></a>
|
||||
<a href="https://img.shields.io/github/license/TheTNB/panel"><img src="https://img.shields.io/github/license/TheTNB/panel"></a>
|
||||
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2FTheTNB%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2FTheTNB%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
|
||||
</p>
|
||||
|
||||
The HaoZi Linux Panel is a lightweight Linux server operation and maintenance management panel developed using Golang and Vue. It is open source under the GNU Affero General Public License v3.0 protocol.
|
||||
@@ -20,10 +19,12 @@ Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ
|
||||
## Advantages
|
||||
|
||||
1. **Extremely low usage:** Deploying the panel + LNMP environment under Debian 12, the memory usage is less than 500 MB, far ahead of other panels using containerization.
|
||||
2. **Efficient operation and maintenance:** The panel UI interface is simple and easy to operate, and you can quickly deploy various environments and adjust application settings without complicated configuration.
|
||||
3. **Offline operation:** The panel can run without relying on any external services. You can even stop the panel process after deployment is complete, and it will not affect the deployed services.
|
||||
4. **Tested by time:** We have been using it in production environment since 2022, and it has been running stably for 1 year without any accidents.
|
||||
5. **Open source and open:** The panel is open source, you can freely modify and audit the panel source code, and the security is guaranteed.
|
||||
2. **Low destructiveness:** The design concept of the panel is to minimize the additional modifications to the system. Among similar panels, we have made the least modifications to the system.
|
||||
3. **Follow the times:** All components of the panel are at the forefront of the times, updated quickly, powerful functions, and guaranteed security.
|
||||
4. **Efficient operation and maintenance:** The panel UI interface is simple and easy to operate, and you can quickly deploy various environments and adjust application settings without complicated configuration.
|
||||
5. **Offline operation:** The panel can run without relying on any external services. You can even stop the panel process after deployment is complete, and it will not affect the deployed services.
|
||||
6. **Tested by time:** We have been using it in production environment since 2022, and it has been running stably for more than 1 year without any accidents.
|
||||
7. **Open source and open:** The panel is open source, you can freely modify and audit the panel source code, and the security is guaranteed.
|
||||
|
||||
## UI Screenshots
|
||||
|
||||
@@ -35,9 +36,9 @@ HaoZi Linux Panel only supports the latest version of mainstream systems under t
|
||||
|
||||
Recommended to use `Debian` for low-configuration machines, as its resource usage is lower than that of the `RHEL` system. For other machines, recommended to use `RockyLinux` | `AlmaLinux`, which has a longer maintenance cycle and is more stable.
|
||||
|
||||
For other systems not in the table below (OpenCloudOS 8, Anolis 8, CentOS Stream 8/9, Debian 11, etc.), you can try to install it yourself, but normal operation is not guaranteed, and free technical support is not provided (theoretically there will be no major question).
|
||||
For other RHEL 9.x systems not in the table, you can try to install it yourself, but normal operation is not guaranteed, and free technical support is not provided (theoretically there will be no major question).
|
||||
|
||||
CentOS Stream can be migrated to a supported system using the migration script: [CentOS 8/9 Migration Script](https://github.com/haozi-team/byecentos)
|
||||
CentOS Stream can be migrated to a supported system using the migration script: [CentOS 8/9 Migration Script](https://github.com/TheTNB/byecentos)
|
||||
|
||||
| OS | Version |
|
||||
|------------|---------|
|
||||
@@ -56,7 +57,7 @@ As system versions are constantly updated, we may also terminate support for som
|
||||
If you decide to continue, please log in to the server as `root` user and execute the following command to install the panel:
|
||||
|
||||
```shell
|
||||
HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
|
||||
HAOZI_DL_URL="https://git.haozi.net/opensource/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
|
||||
```
|
||||
|
||||
## Uninstall Panel
|
||||
@@ -66,7 +67,7 @@ Recommended to back up data and reinstall the system first, so that the system c
|
||||
If you are unable to reinstall the system, log in to the server as the `root` user and execute the following command to uninstall the panel:
|
||||
|
||||
```shell
|
||||
HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
|
||||
HAOZI_DL_URL="https://git.haozi.net/opensource/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
|
||||
```
|
||||
|
||||
Before uninstalling the panel, please be sure to back up all data and uninstall all panel plugins in advance. The data will **not be recoverable** after uninstallation!
|
||||
@@ -79,38 +80,28 @@ Use `panel` command for daily maintenance:
|
||||
panel
|
||||
```
|
||||
|
||||
See more usage methods and tips in [Wiki](https://github.com/haozi-team/panel/wiki).
|
||||
See more usage methods and tips in [Wiki](https://github.com/TheTNB/panel/wiki).
|
||||
|
||||
## Feedback
|
||||
|
||||
For usage issues, you can ask questions in the [WePublish Community Forum](https://wepublish.cn/forums) or seek AI help in the QQ group `@坤坤`. You can also seek paid support in the group.
|
||||
For usage issues, you can ask questions in the [HaoZi Community Forum](https://forum.haozi.net) or seek AI help in the QQ group `@坤坤`. You can also seek paid support in the group.
|
||||
|
||||
For issues with the panel itself, you can submit feedback on the `Issues` page of GitHub. Please note [the wisdom of asking questions](http://www.catb.org/~esr/faqs/smart-questions.html).
|
||||
|
||||
## Contribute code
|
||||
|
||||
### Find/Create Issue
|
||||
|
||||
You can find or create an Issue in [Issue List](https://github.com/haozi-team/panel/issues), leave a message to express your willingness to deal with the Issue, and get confirmation from the maintainer. Start processing.
|
||||
|
||||
### Create PR
|
||||
|
||||
- During the development process, if you encounter a problem, you can describe the problem in detail in the Issue at any time for further communication, but before doing so, please make sure that you have tried your best to solve the problem through Google and other methods;
|
||||
- PRs must be submitted to our JiHu GitLab repository [https://jihulab.com/haozi-team/panel](https://jihulab.com/haozi-team/panel). Do not submit on GitHub;
|
||||
- When the PR is completed, please add the `🚀 Review Ready` tag to it, and the maintainer will review it in time;
|
||||
- We very much welcome your contributions and will add you to the homepage contributors in the next release; ❤️
|
||||
|
||||
## Sponsor
|
||||
|
||||
### Funding
|
||||
|
||||
- [微晓朵](https://www.weixiaoduo.com/)
|
||||
|
||||
### CDN
|
||||
|
||||
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
|
||||
|
||||
- [盾云CDN](http://cdn.ddunyun.com/)
|
||||
|
||||
### DevOps
|
||||
|
||||
- [JiHu GitLab](https://www.jihulab.com/)
|
||||
- [JetBrains](https://www.jetbrains.com/)
|
||||
|
||||
**Accept cloud resources and financial sponsorship, you can contact us through QQ group**
|
||||
|
||||
@@ -122,11 +113,11 @@ This project owes its existence to all those who have contributed. To contribute
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#haozi-team/panel&Date">
|
||||
<a href="https://star-history.com/#TheTNB/panel&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
## 安全说明
|
||||
|
||||
安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子Linux面板,至今无一例安全事故。
|
||||
安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子 Linux 面板,至今无一例安全事故。
|
||||
|
||||
耗子Linux面板采用业界多种方案尽可能保证面板的安全性,但是我们不能保证面板的绝对安全性,**因此我们不对面板的安全性做任何保证**。
|
||||
耗子 Linux 面板采用业界多种方案尽可能保证面板的安全性,但是我们不能保证面板的绝对安全性,**因此我们不对面板的安全性做任何保证**。
|
||||
|
||||
如果您在使用面板的过程中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
|
||||
|
||||
|
||||
@@ -10,9 +10,11 @@ import (
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
)
|
||||
|
||||
// CertRenew 证书续签
|
||||
type CertRenew struct {
|
||||
}
|
||||
|
||||
@@ -35,6 +37,10 @@ func (receiver *CertRenew) Extend() command.Extend {
|
||||
|
||||
// Handle Execute the console command.
|
||||
func (receiver *CertRenew) Handle(ctx console.Context) error {
|
||||
if internal.Status != internal.StatusNormal {
|
||||
return nil
|
||||
}
|
||||
|
||||
var certs []models.Cert
|
||||
err := facades.Orm().Query().With("Website").With("User").With("DNS").Find(&certs)
|
||||
if err != nil {
|
||||
|
||||
@@ -11,10 +11,12 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
// Monitoring 系统监控
|
||||
type Monitoring struct {
|
||||
}
|
||||
|
||||
@@ -37,14 +39,32 @@ func (receiver *Monitoring) Extend() command.Extend {
|
||||
|
||||
// Handle Execute the console command.
|
||||
func (receiver *Monitoring) Handle(ctx console.Context) error {
|
||||
var setting models.Setting
|
||||
monitor := services.NewSettingImpl().Get(models.SettingKeyMonitor)
|
||||
if internal.Status != internal.StatusNormal {
|
||||
return nil
|
||||
}
|
||||
|
||||
setting := services.NewSettingImpl()
|
||||
monitor := setting.Get(models.SettingKeyMonitor)
|
||||
if !cast.ToBool(monitor) {
|
||||
return nil
|
||||
}
|
||||
|
||||
info := tools.GetMonitoringInfo()
|
||||
|
||||
// 去除部分数据以减少数据库存储
|
||||
info.Disk = nil
|
||||
for _, cpu := range info.Cpus {
|
||||
cpu.VendorID = ""
|
||||
cpu.Family = ""
|
||||
cpu.Model = ""
|
||||
cpu.PhysicalID = ""
|
||||
cpu.ModelName = ""
|
||||
cpu.Flags = nil
|
||||
}
|
||||
|
||||
if internal.Status != internal.StatusNormal {
|
||||
return nil
|
||||
}
|
||||
err := facades.Orm().Query().Create(&models.Monitor{
|
||||
Info: info,
|
||||
})
|
||||
@@ -55,20 +75,16 @@ func (receiver *Monitoring) Handle(ctx console.Context) error {
|
||||
}
|
||||
|
||||
// 删除过期数据
|
||||
err = facades.Orm().Query().Where("key", "monitor_days").First(&setting)
|
||||
monitorDays := setting.Get(models.SettingKeyMonitorDays)
|
||||
days, err := strconv.Atoi(monitorDays)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if setting.Value == "0" || len(setting.Value) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
days, err := strconv.Atoi(setting.Value)
|
||||
if err != nil {
|
||||
if days <= 0 || internal.Status != internal.StatusNormal {
|
||||
return nil
|
||||
}
|
||||
_, err = facades.Orm().Query().Where("created_at < ?", carbon.Now().SubDays(days).ToDateTimeString()).Delete(&models.Monitor{})
|
||||
if err != nil {
|
||||
if _, err = facades.Orm().Query().Where("created_at < ?", carbon.Now().SubDays(days).ToDateTimeString()).Delete(&models.Monitor{}); err != nil {
|
||||
facades.Log().Infof("[面板] 系统监控删除过期数据失败: %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
// Panel 面板命令行
|
||||
type Panel struct {
|
||||
}
|
||||
|
||||
@@ -46,6 +48,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
arg2 := ctx.Argument(2)
|
||||
arg3 := ctx.Argument(3)
|
||||
arg4 := ctx.Argument(4)
|
||||
arg5 := ctx.Argument(5)
|
||||
|
||||
switch action {
|
||||
case "init":
|
||||
@@ -56,7 +59,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子Linux面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyVersion, Value: facades.Config().GetString("panel.version")}}
|
||||
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.SettingKeyVersion, Value: facades.Config().GetString("panel.version")}}
|
||||
err = facades.Orm().Query().Create(&settings)
|
||||
if err != nil {
|
||||
color.Redln("初始化失败")
|
||||
@@ -92,12 +95,14 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tools.UpdatePanel(panel)
|
||||
if err != nil {
|
||||
internal.Status = internal.StatusUpgrade
|
||||
if err = tools.UpdatePanel(panel); err != nil {
|
||||
internal.Status = internal.StatusFailed
|
||||
color.Redln("更新失败: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
internal.Status = internal.StatusNormal
|
||||
color.Greenln("更新成功")
|
||||
tools.RestartPanel()
|
||||
|
||||
@@ -115,9 +120,11 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
color.Redln("生成密码失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
user.Username = tools.RandomString(8)
|
||||
user.Password = hash
|
||||
if user.Email == "" {
|
||||
user.Email = tools.RandomString(8) + "@example.com"
|
||||
}
|
||||
|
||||
err = facades.Orm().Query().Save(&user)
|
||||
if err != nil {
|
||||
@@ -130,11 +137,20 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
color.Redln("获取面板端口失败")
|
||||
return nil
|
||||
}
|
||||
ip, err := tools.GetPublicIP()
|
||||
if err != nil {
|
||||
ip = "127.0.0.1"
|
||||
}
|
||||
protocol := "http"
|
||||
if facades.Config().GetBool("panel.ssl") {
|
||||
protocol = "https"
|
||||
}
|
||||
|
||||
color.Greenln("用户名: " + user.Username)
|
||||
color.Greenln("密码: " + password)
|
||||
color.Greenln("面板端口: " + port)
|
||||
color.Greenln("面板入口: " + facades.Config().GetString("http.entrance"))
|
||||
color.Greenln("面板地址: " + protocol + "://" + ip + ":" + port + facades.Config().GetString("http.entrance"))
|
||||
|
||||
case "getPort":
|
||||
port, err := tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
@@ -312,7 +328,7 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
color.Greenln(hr)
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(check, name) {
|
||||
if !strings.Contains(check, name) {
|
||||
color.Redln("|-数据库不存在")
|
||||
color.Greenln(hr)
|
||||
return nil
|
||||
@@ -450,8 +466,8 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
case "writeSite":
|
||||
name := arg1
|
||||
status := cast.ToBool(arg2)
|
||||
path := ctx.Argument(3)
|
||||
php := cast.ToInt(ctx.Argument(4))
|
||||
path := arg3
|
||||
php := cast.ToInt(arg4)
|
||||
ssl := cast.ToBool(ctx.Argument(5))
|
||||
if len(name) == 0 || len(path) == 0 {
|
||||
color.Redln("参数错误")
|
||||
@@ -549,6 +565,127 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
|
||||
color.Greenln("删除设置成功")
|
||||
|
||||
case "addSite":
|
||||
name := arg1
|
||||
domain := arg2
|
||||
port := arg3
|
||||
path := arg4
|
||||
php := arg5
|
||||
if len(name) == 0 || len(domain) == 0 || len(port) == 0 || len(path) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
domains := strings.Split(domain, ",")
|
||||
ports := strings.Split(port, ",")
|
||||
if len(domains) == 0 || len(ports) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
var uintPorts []uint
|
||||
for _, p := range ports {
|
||||
uintPorts = append(uintPorts, cast.ToUint(p))
|
||||
}
|
||||
|
||||
website := services.NewWebsiteImpl()
|
||||
id, err := website.GetIDByName(name)
|
||||
if err != nil {
|
||||
color.Redln(err.Error())
|
||||
return nil
|
||||
}
|
||||
if id != 0 {
|
||||
color.Redln("网站名已存在")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = website.Add(internal.PanelWebsite{
|
||||
Name: name,
|
||||
Status: true,
|
||||
Domains: domains,
|
||||
Ports: uintPorts,
|
||||
Path: path,
|
||||
Php: php,
|
||||
Ssl: false,
|
||||
Db: false,
|
||||
})
|
||||
if err != nil {
|
||||
color.Redln(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("网站添加成功")
|
||||
|
||||
case "removeSite":
|
||||
name := arg1
|
||||
if len(name) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
website := services.NewWebsiteImpl()
|
||||
id, err := website.GetIDByName(name)
|
||||
if err != nil {
|
||||
color.Redln(err.Error())
|
||||
return nil
|
||||
}
|
||||
if id == 0 {
|
||||
color.Redln("网站名不存在")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = website.Delete(id); err != nil {
|
||||
color.Redln(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("网站删除成功")
|
||||
|
||||
case "installPlugin":
|
||||
slug := arg1
|
||||
if len(slug) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
plugin := services.NewPluginImpl()
|
||||
if err := plugin.Install(slug); err != nil {
|
||||
color.Redln(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("任务已提交")
|
||||
|
||||
case "uninstallPlugin":
|
||||
slug := arg1
|
||||
if len(slug) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
plugin := services.NewPluginImpl()
|
||||
if err := plugin.Uninstall(slug); err != nil {
|
||||
color.Redln(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("任务已提交")
|
||||
|
||||
case "updatePlugin":
|
||||
slug := arg1
|
||||
if len(slug) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
plugin := services.NewPluginImpl()
|
||||
if err := plugin.Update(slug); err != nil {
|
||||
color.Redln(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("任务已提交")
|
||||
|
||||
default:
|
||||
color.Yellowln(facades.Config().GetString("panel.name") + "命令行工具 - " + facades.Config().GetString("panel.version"))
|
||||
color.Greenln("请使用以下命令:")
|
||||
@@ -560,6 +697,11 @@ func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
color.Greenln("panel cleanTask 清理面板运行中和等待中的任务[任务卡住时使用]")
|
||||
color.Greenln("panel backup {website/mysql/postgresql} {name} {path} {save_copies} 备份网站 / MySQL数据库 / PostgreSQL数据库到指定目录并保留指定数量")
|
||||
color.Greenln("panel cutoff {website_name} {save_copies} 切割网站日志并保留指定数量")
|
||||
color.Greenln("panel installPlugin {slug} 安装插件")
|
||||
color.Greenln("panel uninstallPlugin {slug} 卸载插件")
|
||||
color.Greenln("panel updatePlugin {slug} 更新插件")
|
||||
color.Greenln("panel addSite {name} {domain} {port} {path} {php} 添加网站[域名和端口用英文逗号分隔]")
|
||||
color.Greenln("panel removeSite {name} 删除网站")
|
||||
color.Redln("以下命令请在开发者指导下使用:")
|
||||
color.Yellowln("panel init 初始化面板")
|
||||
color.Yellowln("panel writePlugin {slug} {version} 写入插件安装状态")
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/console/command"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
"panel/internal"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
// PanelTask 面板每日任务
|
||||
type PanelTask struct {
|
||||
}
|
||||
|
||||
// Signature The name and signature of the console command.
|
||||
func (receiver *PanelTask) Signature() string {
|
||||
return "panel:task"
|
||||
}
|
||||
|
||||
// Description The console command description.
|
||||
func (receiver *PanelTask) Description() string {
|
||||
return "[面板] 每日任务"
|
||||
}
|
||||
|
||||
// Extend The console command extend.
|
||||
func (receiver *PanelTask) Extend() command.Extend {
|
||||
return command.Extend{
|
||||
Category: "panel",
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Execute the console command.
|
||||
func (receiver *PanelTask) Handle(ctx console.Context) error {
|
||||
internal.Status = internal.StatusMaintain
|
||||
|
||||
// 优化数据库
|
||||
if _, err := facades.Orm().Query().Exec("VACUUM"); err != nil {
|
||||
facades.Log().Tags("面板", "每日任务").
|
||||
With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Error("优化面板数据库失败")
|
||||
return err
|
||||
}
|
||||
|
||||
// 备份面板
|
||||
if err := tools.Archive([]string{"/www/panel"}, "/www/backup/panel/panel-"+carbon.Now().ToShortDateTimeString()+".zip"); err != nil {
|
||||
facades.Log().Tags("面板", "每日任务").
|
||||
With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Error("备份面板失败")
|
||||
return err
|
||||
}
|
||||
|
||||
// 清理 7 天前的备份
|
||||
if _, err := tools.Exec(`find /www/backup/panel -mtime +7 -name "*.zip" -exec rm -rf {} \;`); err != nil {
|
||||
facades.Log().Tags("面板", "每日任务").
|
||||
With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Error("清理面板备份失败")
|
||||
return err
|
||||
}
|
||||
|
||||
internal.Status = internal.StatusNormal
|
||||
return nil
|
||||
}
|
||||
@@ -14,7 +14,8 @@ type Kernel struct {
|
||||
func (kernel *Kernel) Schedule() []schedule.Event {
|
||||
return []schedule.Event{
|
||||
facades.Schedule().Command("panel:monitoring").EveryMinute().SkipIfStillRunning(),
|
||||
facades.Schedule().Command("panel:cert-renew").Daily().SkipIfStillRunning(),
|
||||
facades.Schedule().Command("panel:cert-renew").DailyAt("04:00").SkipIfStillRunning(),
|
||||
facades.Schedule().Command("panel:task").Daily().SkipIfStillRunning(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,5 +24,6 @@ func (kernel *Kernel) Commands() []console.Command {
|
||||
&commands.Panel{},
|
||||
&commands.Monitoring{},
|
||||
&commands.CertRenew{},
|
||||
&commands.PanelTask{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type AssetController struct {
|
||||
setting services.Setting
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewAssetController() *AssetController {
|
||||
@@ -33,7 +34,7 @@ func (r *AssetController) Index(ctx http.Context) http.Response {
|
||||
}
|
||||
// 拒绝访问非入口文件
|
||||
if !strings.HasPrefix(ctx.Request().Path(), entrance) {
|
||||
return ctx.Response().Status(http.StatusNotFound).String(http.StatusText(http.StatusNotFound))
|
||||
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
}
|
||||
|
||||
path := strings.TrimPrefix(ctx.Request().Path(), entrance)
|
||||
@@ -43,21 +44,21 @@ func (r *AssetController) Index(ctx http.Context) http.Response {
|
||||
}
|
||||
|
||||
if !tools.Exists("public" + path) {
|
||||
return ctx.Response().Status(http.StatusNotFound).String(http.StatusText(http.StatusNotFound))
|
||||
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
}
|
||||
|
||||
file, err := os.Open("public" + path)
|
||||
if err != nil {
|
||||
return ctx.Response().Status(http.StatusInternalServerError).String(http.StatusText(http.StatusInternalServerError))
|
||||
return Error(ctx, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
return ctx.Response().Status(http.StatusInternalServerError).String(http.StatusText(http.StatusInternalServerError))
|
||||
return Error(ctx, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
return ctx.Response().Status(http.StatusForbidden).String(http.StatusText(http.StatusForbidden))
|
||||
return Error(ctx, http.StatusForbidden, http.StatusText(http.StatusForbidden))
|
||||
}
|
||||
|
||||
return ctx.Response().Header("Cache-Control", "no-cache").File("public" + path)
|
||||
@@ -72,5 +73,5 @@ func (r *AssetController) Robots(ctx http.Context) http.Response {
|
||||
}
|
||||
|
||||
func (r *AssetController) NotFound(ctx http.Context) http.Response {
|
||||
return ctx.Response().Status(http.StatusNotFound).String(http.StatusText(http.StatusNotFound))
|
||||
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
}
|
||||
|
||||
@@ -6,15 +6,15 @@ import (
|
||||
|
||||
requests "panel/app/http/requests/cert"
|
||||
commonrequests "panel/app/http/requests/common"
|
||||
responses "panel/app/http/responses/cert"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/acme"
|
||||
)
|
||||
|
||||
type CertController struct {
|
||||
cron services.Cron
|
||||
cert services.Cert
|
||||
cron internal.Cron
|
||||
cert internal.Cert
|
||||
}
|
||||
|
||||
func NewCertController() *CertController {
|
||||
@@ -121,8 +121,8 @@ func (r *CertController) Algorithms(ctx http.Context) http.Response {
|
||||
// @Tags 证书管理
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse{data=responses.CertList}
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/users [get]
|
||||
func (r *CertController) UserList(ctx http.Context) http.Response {
|
||||
var paginateRequest commonrequests.Paginate
|
||||
@@ -141,9 +141,9 @@ func (r *CertController) UserList(ctx http.Context) http.Response {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, responses.UserList{
|
||||
Total: total,
|
||||
Items: users,
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": users,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -273,8 +273,8 @@ func (r *CertController) UserDestroy(ctx http.Context) http.Response {
|
||||
// @Tags 证书管理
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse{data=responses.DNSList}
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/dns [get]
|
||||
func (r *CertController) DNSList(ctx http.Context) http.Response {
|
||||
var paginateRequest commonrequests.Paginate
|
||||
@@ -293,9 +293,9 @@ func (r *CertController) DNSList(ctx http.Context) http.Response {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, responses.DNSList{
|
||||
Total: total,
|
||||
Items: dns,
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": dns,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -425,8 +425,8 @@ func (r *CertController) DNSDestroy(ctx http.Context) http.Response {
|
||||
// @Tags 证书管理
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse{data=responses.CertList}
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/certs [get]
|
||||
func (r *CertController) CertList(ctx http.Context) http.Response {
|
||||
var paginateRequest commonrequests.Paginate
|
||||
@@ -445,9 +445,9 @@ func (r *CertController) CertList(ctx http.Context) http.Response {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, responses.CertList{
|
||||
Total: total,
|
||||
Items: certs,
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": certs,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -650,7 +650,7 @@ func (r *CertController) Renew(ctx http.Context) http.Response {
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Obtain true "request"
|
||||
// @Success 200 {object} SuccessResponse{data=map[string]acme.Resolve}
|
||||
// @Success 200 {object} SuccessResponse{data=[]acme.DNSRecord}
|
||||
// @Router /panel/cert/manualDNS [post]
|
||||
func (r *CertController) ManualDNS(ctx http.Context) http.Response {
|
||||
var obtainRequest requests.Obtain
|
||||
@@ -669,3 +669,33 @@ func (r *CertController) ManualDNS(ctx http.Context) http.Response {
|
||||
|
||||
return Success(ctx, resolves)
|
||||
}
|
||||
|
||||
// Deploy
|
||||
//
|
||||
// @Summary 部署证书
|
||||
// @Description 部署面板证书管理的证书
|
||||
// @Tags 证书管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.CertDeploy true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/cert/deploy [post]
|
||||
func (r *CertController) Deploy(ctx http.Context) http.Response {
|
||||
var deployRequest requests.CertDeploy
|
||||
sanitize := Sanitize(ctx, &deployRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
err := r.cert.Deploy(deployRequest.ID, deployRequest.WebsiteID)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
|
||||
"certID": deployRequest.ID,
|
||||
"error": err.Error(),
|
||||
}).Info("部署证书失败")
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,21 +6,18 @@ import (
|
||||
|
||||
// SuccessResponse 通用成功响应
|
||||
type SuccessResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
// ErrorResponse 通用错误响应
|
||||
type ErrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Success 响应成功
|
||||
func Success(ctx http.Context, data any) http.Response {
|
||||
return ctx.Response().Success().Json(&SuccessResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: data,
|
||||
})
|
||||
@@ -28,16 +25,14 @@ func Success(ctx http.Context, data any) http.Response {
|
||||
|
||||
// Error 响应错误
|
||||
func Error(ctx http.Context, code int, message string) http.Response {
|
||||
return ctx.Response().Json(http.StatusOK, &ErrorResponse{
|
||||
Code: code,
|
||||
return ctx.Response().Json(code, &ErrorResponse{
|
||||
Message: "错误: " + message,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorSystem 响应系统错误
|
||||
func ErrorSystem(ctx http.Context) http.Response {
|
||||
return ctx.Response().Json(http.StatusOK, &ErrorResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
return ctx.Response().Json(http.StatusInternalServerError, &ErrorResponse{
|
||||
Message: "系统内部错误",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,14 +8,16 @@ import (
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type CronController struct {
|
||||
cron services.Cron
|
||||
setting services.Setting
|
||||
cron internal.Cron
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewCronController() *CronController {
|
||||
@@ -71,7 +73,7 @@ func (r *CronController) Add(ctx http.Context) http.Response {
|
||||
cronType := ctx.Request().Input("type")
|
||||
if cronType == "backup" {
|
||||
backupType := ctx.Request().Input("backup_type")
|
||||
backupName := ctx.Request().Input("backup_database")
|
||||
backupName := ctx.Request().Input("database")
|
||||
if backupType == "website" {
|
||||
backupName = ctx.Request().Input("website")
|
||||
}
|
||||
@@ -83,7 +85,7 @@ func (r *CronController) Add(ctx http.Context) http.Response {
|
||||
shell = `#!/bin/bash
|
||||
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
# 耗子Linux面板 - 数据备份脚本
|
||||
# 耗子 Linux 面板 - 数据备份脚本
|
||||
|
||||
type=` + backupType + `
|
||||
path=` + backupPath + `
|
||||
@@ -100,7 +102,7 @@ panel backup ${type} ${name} ${path} ${save} 2>&1
|
||||
shell = `#!/bin/bash
|
||||
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
# 耗子Linux面板 - 日志切割脚本
|
||||
# 耗子 Linux 面板 - 日志切割脚本
|
||||
|
||||
name=` + website + `
|
||||
save=` + cast.ToString(save) + `
|
||||
|
||||
@@ -1,11 +1,492 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
requests "panel/app/http/requests/file"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type FileController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewFileController() *FileController {
|
||||
return &FileController{
|
||||
// Inject services
|
||||
}
|
||||
return &FileController{}
|
||||
}
|
||||
|
||||
// Create
|
||||
//
|
||||
// @Summary 创建文件/目录
|
||||
// @Description 创建文件/目录到给定路径
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.NotExist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/create [post]
|
||||
func (r *FileController) Create(ctx http.Context) http.Response {
|
||||
var request requests.NotExist
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
isDir := ctx.Request().InputBool("dir")
|
||||
if !isDir {
|
||||
if out, err := tools.Exec("touch " + request.Path); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
} else {
|
||||
if err := tools.Mkdir(request.Path, 0755); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
r.setPermission(request.Path, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Content
|
||||
//
|
||||
// @Summary 获取文件内容
|
||||
// @Description 获取给定路径的文件内容
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query requests.Exist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/content [get]
|
||||
func (r *FileController) Content(ctx http.Context) http.Response {
|
||||
var request requests.Exist
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
fileInfo, err := tools.FileInfo(request.Path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if fileInfo.IsDir() {
|
||||
return Error(ctx, http.StatusInternalServerError, "目标路径不是文件")
|
||||
}
|
||||
if fileInfo.Size() > 10*1024*1024 {
|
||||
return Error(ctx, http.StatusInternalServerError, "文件大小超过 10M")
|
||||
}
|
||||
|
||||
content, err := tools.Read(request.Path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, content)
|
||||
}
|
||||
|
||||
// Save
|
||||
//
|
||||
// @Summary 保存文件内容
|
||||
// @Description 保存给定路径的文件内容
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Save true "request"
|
||||
// @Param content body string true "content"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/save [post]
|
||||
func (r *FileController) Save(ctx http.Context) http.Response {
|
||||
var request requests.Save
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
fileInfo, err := tools.FileInfo(request.Path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err = tools.Write(request.Path, request.Content, fileInfo.Mode()); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
r.setPermission(request.Path, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete
|
||||
//
|
||||
// @Summary 删除文件/目录
|
||||
// @Description 删除给定路径的文件/目录
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Exist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/delete [post]
|
||||
func (r *FileController) Delete(ctx http.Context) http.Response {
|
||||
var request requests.Exist
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := tools.Remove(request.Path); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Upload
|
||||
//
|
||||
// @Summary 上传文件
|
||||
// @Description 上传文件到给定路径
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param file formData file true "file"
|
||||
// @Param path formData string true "path"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/upload [post]
|
||||
func (r *FileController) Upload(ctx http.Context) http.Response {
|
||||
var request requests.Upload
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
src, err := request.File.Open()
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
if tools.Exists(request.Path) && !ctx.Request().InputBool("force") {
|
||||
return Error(ctx, http.StatusForbidden, "目标路径已存在,是否覆盖?")
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(src)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err = tools.Write(request.Path, string(data), 0755); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
r.setPermission(request.Path, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Move
|
||||
//
|
||||
// @Summary 移动文件/目录
|
||||
// @Description 移动文件/目录到给定路径,等效于重命名
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Move true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/move [post]
|
||||
func (r *FileController) Move(ctx http.Context) http.Response {
|
||||
var request requests.Move
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if tools.Exists(request.New) && !ctx.Request().InputBool("force") {
|
||||
return Error(ctx, http.StatusForbidden, "目标路径已存在,是否覆盖?")
|
||||
}
|
||||
|
||||
if err := tools.Mv(request.Old, request.New); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
r.setPermission(request.New, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Copy
|
||||
//
|
||||
// @Summary 复制文件/目录
|
||||
// @Description 复制文件/目录到给定路径
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Copy true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/copy [post]
|
||||
func (r *FileController) Copy(ctx http.Context) http.Response {
|
||||
var request requests.Copy
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if tools.Exists(request.New) && !ctx.Request().InputBool("force") {
|
||||
return Error(ctx, http.StatusForbidden, "目标路径已存在,是否覆盖?")
|
||||
}
|
||||
|
||||
if err := tools.Cp(request.Old, request.New); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
r.setPermission(request.New, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Download
|
||||
//
|
||||
// @Summary 下载文件
|
||||
// @Description 下载给定路径的文件
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query requests.NotExist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/download [get]
|
||||
func (r *FileController) Download(ctx http.Context) http.Response {
|
||||
var request requests.Exist
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
info, err := tools.FileInfo(request.Path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if info.IsDir() {
|
||||
return Error(ctx, http.StatusInternalServerError, "不能下载目录")
|
||||
}
|
||||
|
||||
return ctx.Response().Download(request.Path, info.Name())
|
||||
}
|
||||
|
||||
// RemoteDownload
|
||||
//
|
||||
// @Summary 下载远程文件
|
||||
// @Description 下载远程文件到给定路径
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.NotExist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/remoteDownload [post]
|
||||
func (r *FileController) RemoteDownload(ctx http.Context) http.Response {
|
||||
var request requests.NotExist
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
// TODO 使用异步任务下载文件
|
||||
return nil
|
||||
}
|
||||
|
||||
// Info
|
||||
//
|
||||
// @Summary 获取文件/目录信息
|
||||
// @Description 获取给定路径的文件/目录信息
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query requests.Exist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/info [get]
|
||||
func (r *FileController) Info(ctx http.Context) http.Response {
|
||||
var request requests.Exist
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
fileInfo, err := tools.FileInfo(request.Path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"name": fileInfo.Name(),
|
||||
"size": tools.FormatBytes(float64(fileInfo.Size())),
|
||||
"mode_str": fileInfo.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", fileInfo.Mode().Perm()),
|
||||
"dir": fileInfo.IsDir(),
|
||||
"modify": carbon.FromStdTime(fileInfo.ModTime()).ToDateTimeString(),
|
||||
})
|
||||
}
|
||||
|
||||
// Permission
|
||||
//
|
||||
// @Summary 修改文件/目录权限
|
||||
// @Description 修改给定路径的文件/目录权限
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Permission true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/permission [post]
|
||||
func (r *FileController) Permission(ctx http.Context) http.Response {
|
||||
var request requests.Permission
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := tools.Chmod(request.Path, os.FileMode(request.Mode)); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if err := tools.Chown(request.Path, request.Owner, request.Group); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Archive
|
||||
//
|
||||
// @Summary 压缩文件/目录
|
||||
// @Description 压缩文件/目录到给定路径
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Archive true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/archive [post]
|
||||
func (r *FileController) Archive(ctx http.Context) http.Response {
|
||||
var request requests.Archive
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := tools.Archive(request.Paths, request.File); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
r.setPermission(request.File, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UnArchive
|
||||
//
|
||||
// @Summary 解压文件/目录
|
||||
// @Description 解压文件/目录到给定路径
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.UnArchive true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/unArchive [post]
|
||||
func (r *FileController) UnArchive(ctx http.Context) http.Response {
|
||||
var request requests.UnArchive
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := tools.UnArchive(request.File, request.Path); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
r.setPermission(request.Path, 0755, "www", "www")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Search
|
||||
//
|
||||
// @Summary 搜索文件/目录
|
||||
// @Description 通过关键词搜索给定路径的文件/目录
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data body requests.Search true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/search [post]
|
||||
func (r *FileController) Search(ctx http.Context) http.Response {
|
||||
var request requests.Search
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
paths := make(map[string]os.FileInfo)
|
||||
err := filepath.Walk(request.Path, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(info.Name(), request.KeyWord) {
|
||||
paths[path] = info
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, paths)
|
||||
}
|
||||
|
||||
// List
|
||||
//
|
||||
// @Summary 获取文件/目录列表
|
||||
// @Description 获取给定路径的文件/目录列表
|
||||
// @Tags 文件管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param data query requests.Exist true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/file/list [get]
|
||||
func (r *FileController) List(ctx http.Context) http.Response {
|
||||
var request requests.Exist
|
||||
sanitize := Sanitize(ctx, &request)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
fileInfoList, err := os.ReadDir(request.Path)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
var paths []any
|
||||
for _, fileInfo := range fileInfoList {
|
||||
info, _ := fileInfo.Info()
|
||||
paths = append(paths, map[string]any{
|
||||
"name": info.Name(),
|
||||
"size": tools.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
"dir": info.IsDir(),
|
||||
"modify": carbon.FromStdTime(info.ModTime()).ToDateTimeString(),
|
||||
})
|
||||
}
|
||||
|
||||
return Success(ctx, paths)
|
||||
}
|
||||
|
||||
// setPermission
|
||||
func (r *FileController) setPermission(path string, mode uint, owner, group string) {
|
||||
_ = tools.Chmod(path, os.FileMode(mode))
|
||||
_ = tools.Chown(path, owner, group)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
@@ -22,8 +23,8 @@ type MenuItem struct {
|
||||
}
|
||||
|
||||
type InfoController struct {
|
||||
plugin services.Plugin
|
||||
setting services.Setting
|
||||
plugin internal.Plugin
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewInfoController() *InfoController {
|
||||
@@ -60,7 +61,7 @@ func (r *InfoController) HomePlugins(ctx http.Context) http.Response {
|
||||
for _, plugin := range plugins {
|
||||
pluginsJson = append(pluginsJson, pluginsData{
|
||||
Plugin: plugin,
|
||||
Name: services.NewPluginImpl().GetBySlug(plugin.Slug).Name,
|
||||
Name: r.plugin.GetBySlug(plugin.Slug).Name,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -308,14 +309,16 @@ func (r *InfoController) Update(ctx http.Context) http.Response {
|
||||
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
|
||||
}
|
||||
|
||||
err = tools.UpdatePanel(panel)
|
||||
if err != nil {
|
||||
internal.Status = internal.StatusUpgrade
|
||||
if err = tools.UpdatePanel(panel); err != nil {
|
||||
internal.Status = internal.StatusFailed
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("更新面板失败")
|
||||
return Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
internal.Status = internal.StatusNormal
|
||||
tools.RestartPanel()
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
)
|
||||
|
||||
type MonitorController struct {
|
||||
setting services.Setting
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewMonitorController() *MonitorController {
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
)
|
||||
|
||||
type PluginController struct {
|
||||
plugin services.Plugin
|
||||
task services.Task
|
||||
plugin internal.Plugin
|
||||
task internal.Task
|
||||
}
|
||||
|
||||
func NewPluginController() *PluginController {
|
||||
@@ -30,13 +29,10 @@ func (r *PluginController) List(ctx http.Context) http.Response {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -96,183 +92,33 @@ func (r *PluginController) List(ctx http.Context) http.Response {
|
||||
// Install 安装插件
|
||||
func (r *PluginController) Install(ctx http.Context) http.Response {
|
||||
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().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
|
||||
"slug": slug,
|
||||
}).Info("检查插件安装状态失败")
|
||||
|
||||
if err := r.plugin.Install(slug); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if installedPlugin.ID != 0 {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "插件已安装")
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
pluginsMap[p.Slug] = true
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
lock.RLock()
|
||||
_, requireFound := pluginsMap[require]
|
||||
lock.RUnlock()
|
||||
if !requireFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
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().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
|
||||
"slug": slug,
|
||||
"err": err.Error(),
|
||||
}).Info("创建任务失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
return Success(ctx, "任务已提交")
|
||||
}
|
||||
|
||||
// Uninstall 卸载插件
|
||||
func (r *PluginController) Uninstall(ctx http.Context) http.Response {
|
||||
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().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
|
||||
"slug": slug,
|
||||
}).Info("检查插件安装状态失败")
|
||||
|
||||
if err := r.plugin.Uninstall(slug); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if installedPlugin.ID == 0 {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "插件未安装")
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
pluginsMap[p.Slug] = true
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
lock.RLock()
|
||||
_, requireFound := pluginsMap[require]
|
||||
lock.RUnlock()
|
||||
if !requireFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
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().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
|
||||
"slug": slug,
|
||||
"err": err.Error(),
|
||||
}).Info("创建任务失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
return Success(ctx, "任务已提交")
|
||||
}
|
||||
|
||||
// Update 更新插件
|
||||
func (r *PluginController) Update(ctx http.Context) http.Response {
|
||||
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().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
|
||||
"slug": slug,
|
||||
}).Info("检查插件安装状态失败")
|
||||
|
||||
if err := r.plugin.Update(slug); err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
if installedPlugin.ID == 0 {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "插件未安装")
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
pluginsMap[p.Slug] = true
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
lock.RLock()
|
||||
_, requireFound := pluginsMap[require]
|
||||
lock.RUnlock()
|
||||
if !requireFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
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().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
|
||||
"slug": slug,
|
||||
"err": err.Error(),
|
||||
}).Info("创建任务失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
return Success(ctx, "任务已提交")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package plugins
|
||||
|
||||
type PHPExtension struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type LoadInfo struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type Fail2banJail struct {
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
LogPath string `json:"log_path"`
|
||||
MaxRetry int `json:"max_retry"`
|
||||
FindTime int `json:"find_time"`
|
||||
BanTime int `json:"ban_time"`
|
||||
}
|
||||
|
||||
type S3fsMount struct {
|
||||
ID int64 `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Bucket string `json:"bucket"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
@@ -10,12 +10,14 @@ import (
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
"panel/types"
|
||||
)
|
||||
|
||||
type Fail2banController struct {
|
||||
website services.Website
|
||||
website internal.Website
|
||||
}
|
||||
|
||||
func NewFail2banController() *Fail2banController {
|
||||
@@ -88,7 +90,7 @@ func (r *Fail2banController) List(ctx http.Context) http.Response {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "Fail2ban 规则为空")
|
||||
}
|
||||
|
||||
var jails []Fail2banJail
|
||||
var jails []types.Fail2banJail
|
||||
for i, jail := range jailList {
|
||||
if i == 0 {
|
||||
continue
|
||||
@@ -105,7 +107,7 @@ func (r *Fail2banController) List(ctx http.Context) http.Response {
|
||||
jailFindTime := regexp.MustCompile(`findtime = (.*)`).FindStringSubmatch(jailRaw)
|
||||
jailBanTime := regexp.MustCompile(`bantime = (.*)`).FindStringSubmatch(jailRaw)
|
||||
|
||||
jails = append(jails, Fail2banJail{
|
||||
jails = append(jails, types.Fail2banJail{
|
||||
Name: jailName,
|
||||
Enabled: jailEnabled,
|
||||
LogPath: jailLogPath[1],
|
||||
@@ -120,7 +122,7 @@ func (r *Fail2banController) List(ctx http.Context) http.Response {
|
||||
if startIndex > len(jails) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []Fail2banJail{},
|
||||
"items": []types.Fail2banJail{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(jails) {
|
||||
@@ -128,7 +130,7 @@ func (r *Fail2banController) List(ctx http.Context) http.Response {
|
||||
}
|
||||
pagedJails := jails[startIndex:endIndex]
|
||||
if pagedJails == nil {
|
||||
pagedJails = []Fail2banJail{}
|
||||
pagedJails = []types.Fail2banJail{}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Mysql57Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
setting internal.Setting
|
||||
backup internal.Backup
|
||||
}
|
||||
|
||||
func NewMysql57Controller() *Mysql57Controller {
|
||||
@@ -376,7 +377,7 @@ func (r *Mysql57Controller) BackupList(ctx http.Context) http.Response {
|
||||
if startIndex > len(backupList) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []services.BackupFile{},
|
||||
"items": []internal.BackupFile{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(backupList) {
|
||||
@@ -384,7 +385,7 @@ func (r *Mysql57Controller) BackupList(ctx http.Context) http.Response {
|
||||
}
|
||||
pagedBackupList := backupList[startIndex:endIndex]
|
||||
if pagedBackupList == nil {
|
||||
pagedBackupList = []services.BackupFile{}
|
||||
pagedBackupList = []internal.BackupFile{}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Mysql80Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
setting internal.Setting
|
||||
backup internal.Backup
|
||||
}
|
||||
|
||||
func NewMysql80Controller() *Mysql80Controller {
|
||||
@@ -376,7 +377,7 @@ func (r *Mysql80Controller) BackupList(ctx http.Context) http.Response {
|
||||
if startIndex > len(backupList) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []services.BackupFile{},
|
||||
"items": []internal.BackupFile{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(backupList) {
|
||||
@@ -384,7 +385,7 @@ func (r *Mysql80Controller) BackupList(ctx http.Context) http.Response {
|
||||
}
|
||||
pagedBackupList := backupList[startIndex:endIndex]
|
||||
if pagedBackupList == nil {
|
||||
pagedBackupList = []services.BackupFile{}
|
||||
pagedBackupList = []internal.BackupFile{}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
|
||||
@@ -98,7 +98,7 @@ func (r *OpenRestyController) ErrorLog(ctx http.Context) http.Response {
|
||||
return controllers.Success(ctx, "")
|
||||
}
|
||||
|
||||
out, err := tools.Exec("tail -n 100 /www/wwwlogs/nginx_error.log")
|
||||
out, err := tools.Exec("tail -n 100 /www/wwwlogs/openresty_error.log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func (r *OpenRestyController) ErrorLog(ctx http.Context) http.Response {
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (r *OpenRestyController) ClearErrorLog(ctx http.Context) http.Response {
|
||||
if out, err := tools.Exec("echo '' > /www/wwwlogs/nginx_error.log"); err != nil {
|
||||
if out, err := tools.Exec("echo '' > /www/wwwlogs/openresty_error.log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
package plugins
|
||||
|
||||
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 (r *Php74Controller) Status(ctx http.Context) http.Response {
|
||||
status, err := tools.ServiceStatus("php-fpm-" + r.version)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"运行状态失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, status)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) Reload(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceReload("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) Start(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceStart("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "启动PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) Stop(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceStop("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "停止PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) Restart(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceRestart("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重启PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) GetConfig(ctx http.Context) http.Response {
|
||||
config, err := tools.Read("/www/server/php/" + r.version + "/etc/php.ini")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
config := ctx.Request().Input("config")
|
||||
if err := tools.Write("/www/server/php/"+r.version+"/etc/php.ini", config, 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
return r.Reload(ctx)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) Load(ctx http.Context) http.Response {
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + r.version)
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Info("获取PHP-" + r.version + "运行状态失败")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+r.version+"] 获取运行状态失败")
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
|
||||
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
|
||||
|
||||
type Data struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
data := make([]Data, len(dataKeys))
|
||||
for i := range dataKeys {
|
||||
data[i].Name = dataKeys[i]
|
||||
|
||||
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
|
||||
match := r.FindStringSubmatch(raw)
|
||||
|
||||
if len(match) > 1 {
|
||||
data[i].Value = strings.TrimSpace(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/php-fpm.log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) SlowLog(ctx http.Context) http.Response {
|
||||
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/slow.log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/php-fpm.log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/slow.log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) GetExtensionList(ctx http.Context) http.Response {
|
||||
extensions := r.GetExtensions()
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (r *Php74Controller) InstallExtension(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
extensions := r.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展已安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "安装PHP-" + r.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Info("[PHP-" + r.version + "] 创建安装拓展任务失败:" + err.Error())
|
||||
return controllers.ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
|
||||
}
|
||||
|
||||
func (r *Php74Controller) UninstallExtension(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
extensions := r.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展未安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "卸载PHP-" + r.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Info("[PHP-" + r.version + "] 创建卸载拓展任务失败:" + err.Error())
|
||||
return controllers.ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
|
||||
}
|
||||
|
||||
func (r *Php74Controller) GetExtensions() []PHPExtension {
|
||||
var extensions []PHPExtension
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "OPcache",
|
||||
Slug: "Zend OPcache",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "PhpRedis",
|
||||
Slug: "redis",
|
||||
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "ImageMagick",
|
||||
Slug: "imagick",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "Exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "pdo_pgsql",
|
||||
Slug: "pdo_pgsql",
|
||||
Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的PHP加密解密工具。",
|
||||
Installed: false,
|
||||
})
|
||||
|
||||
raw, err := tools.Exec("/www/server/php/" + r.version + "/bin/php -m")
|
||||
if err != nil {
|
||||
return extensions
|
||||
}
|
||||
|
||||
rawExtensionList := strings.Split(raw, "\n")
|
||||
for _, item := range rawExtensionList {
|
||||
if !strings.Contains(item, "[") && item != "" {
|
||||
for i := range extensions {
|
||||
if extensions[i].Slug == item {
|
||||
extensions[i].Installed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
package plugins
|
||||
|
||||
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 (r *Php80Controller) Status(ctx http.Context) http.Response {
|
||||
status, err := tools.ServiceStatus("php-fpm-" + r.version)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"运行状态失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, status)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) Reload(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceReload("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) Start(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceStart("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "启动PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) Stop(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceStop("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "停止PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) Restart(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceRestart("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重启PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) GetConfig(ctx http.Context) http.Response {
|
||||
config, err := tools.Read("/www/server/php/" + r.version + "/etc/php.ini")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
config := ctx.Request().Input("config")
|
||||
if err := tools.Write("/www/server/php/"+r.version+"/etc/php.ini", config, 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
return r.Reload(ctx)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) Load(ctx http.Context) http.Response {
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + r.version)
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Info("获取PHP-" + r.version + "运行状态失败")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+r.version+"] 获取运行状态失败")
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
|
||||
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
|
||||
|
||||
type Data struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
data := make([]Data, len(dataKeys))
|
||||
for i := range dataKeys {
|
||||
data[i].Name = dataKeys[i]
|
||||
|
||||
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
|
||||
match := r.FindStringSubmatch(raw)
|
||||
|
||||
if len(match) > 1 {
|
||||
data[i].Value = strings.TrimSpace(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/php-fpm.log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) SlowLog(ctx http.Context) http.Response {
|
||||
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/slow.log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/php-fpm.log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/slow.log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) GetExtensionList(ctx http.Context) http.Response {
|
||||
extensions := r.GetExtensions()
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (r *Php80Controller) InstallExtension(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
extensions := r.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展已安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "安装PHP-" + r.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Info("[PHP-" + r.version + "] 创建安装拓展任务失败:" + err.Error())
|
||||
return controllers.ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
|
||||
}
|
||||
|
||||
func (r *Php80Controller) UninstallExtension(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
extensions := r.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展未安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "卸载PHP-" + r.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Info("[PHP-" + r.version + "] 创建卸载拓展任务失败:" + err.Error())
|
||||
return controllers.ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
|
||||
}
|
||||
|
||||
func (r *Php80Controller) GetExtensions() []PHPExtension {
|
||||
var extensions []PHPExtension
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "OPcache",
|
||||
Slug: "Zend OPcache",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "PhpRedis",
|
||||
Slug: "redis",
|
||||
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "ImageMagick",
|
||||
Slug: "imagick",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "Exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "pdo_pgsql",
|
||||
Slug: "pdo_pgsql",
|
||||
Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的PHP加密解密工具。",
|
||||
Installed: false,
|
||||
})
|
||||
|
||||
raw, err := tools.Exec("/www/server/php/" + r.version + "/bin/php -m")
|
||||
if err != nil {
|
||||
return extensions
|
||||
}
|
||||
|
||||
rawExtensionList := strings.Split(raw, "\n")
|
||||
for _, item := range rawExtensionList {
|
||||
if !strings.Contains(item, "[") && item != "" {
|
||||
for i := range extensions {
|
||||
if extensions[i].Slug == item {
|
||||
extensions[i].Installed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Php81Controller struct {
|
||||
setting services.Setting
|
||||
task services.Task
|
||||
version string
|
||||
}
|
||||
|
||||
func NewPhp81Controller() *Php81Controller {
|
||||
return &Php81Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
task: services.NewTaskImpl(),
|
||||
version: "81",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Php81Controller) Status(ctx http.Context) http.Response {
|
||||
status, err := tools.ServiceStatus("php-fpm-" + r.version)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"运行状态失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, status)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) Reload(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceReload("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) Start(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceStart("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "启动PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) Stop(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceStop("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "停止PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) Restart(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceRestart("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重启PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) GetConfig(ctx http.Context) http.Response {
|
||||
config, err := tools.Read("/www/server/php/" + r.version + "/etc/php.ini")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
config := ctx.Request().Input("config")
|
||||
if err := tools.Write("/www/server/php/"+r.version+"/etc/php.ini", config, 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
return r.Reload(ctx)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) Load(ctx http.Context) http.Response {
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + r.version)
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Info("获取PHP-" + r.version + "运行状态失败")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+r.version+"] 获取运行状态失败")
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
|
||||
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
|
||||
|
||||
type Data struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
data := make([]Data, len(dataKeys))
|
||||
for i := range dataKeys {
|
||||
data[i].Name = dataKeys[i]
|
||||
|
||||
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
|
||||
match := r.FindStringSubmatch(raw)
|
||||
|
||||
if len(match) > 1 {
|
||||
data[i].Value = strings.TrimSpace(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/php-fpm.log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) SlowLog(ctx http.Context) http.Response {
|
||||
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/slow.log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/php-fpm.log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/slow.log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) GetExtensionList(ctx http.Context) http.Response {
|
||||
extensions := r.GetExtensions()
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (r *Php81Controller) InstallExtension(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
extensions := r.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展已安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "安装PHP-" + r.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Info("[PHP-" + r.version + "] 创建安装拓展任务失败:" + err.Error())
|
||||
return controllers.ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
|
||||
}
|
||||
|
||||
func (r *Php81Controller) UninstallExtension(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
extensions := r.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展未安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "卸载PHP-" + r.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Info("[PHP-" + r.version + "] 创建卸载拓展任务失败:" + err.Error())
|
||||
return controllers.ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
|
||||
}
|
||||
|
||||
func (r *Php81Controller) GetExtensions() []PHPExtension {
|
||||
var extensions []PHPExtension
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "OPcache",
|
||||
Slug: "Zend OPcache",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "PhpRedis",
|
||||
Slug: "redis",
|
||||
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "ImageMagick",
|
||||
Slug: "imagick",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "Exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "pdo_pgsql",
|
||||
Slug: "pdo_pgsql",
|
||||
Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的PHP加密解密工具。",
|
||||
Installed: false,
|
||||
})
|
||||
|
||||
raw, err := tools.Exec("/www/server/php/" + r.version + "/bin/php -m")
|
||||
if err != nil {
|
||||
return extensions
|
||||
}
|
||||
|
||||
rawExtensionList := strings.Split(raw, "\n")
|
||||
for _, item := range rawExtensionList {
|
||||
if !strings.Contains(item, "[") && item != "" {
|
||||
for i := range extensions {
|
||||
if extensions[i].Slug == item {
|
||||
extensions[i].Installed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Php82Controller struct {
|
||||
setting services.Setting
|
||||
task services.Task
|
||||
version string
|
||||
}
|
||||
|
||||
func NewPhp82Controller() *Php82Controller {
|
||||
return &Php82Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
task: services.NewTaskImpl(),
|
||||
version: "82",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Php82Controller) Status(ctx http.Context) http.Response {
|
||||
status, err := tools.ServiceStatus("php-fpm-" + r.version)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"运行状态失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, status)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) Reload(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceReload("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重载PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) Start(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceStart("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "启动PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) Stop(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceStop("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "停止PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) Restart(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceRestart("php-fpm-" + r.version); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重启PHP-"+r.version+"失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) GetConfig(ctx http.Context) http.Response {
|
||||
config, err := tools.Read("/www/server/php/" + r.version + "/etc/php.ini")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
config := ctx.Request().Input("config")
|
||||
if err := tools.Write("/www/server/php/"+r.version+"/etc/php.ini", config, 0644); err != nil {
|
||||
return nil
|
||||
}
|
||||
return r.Reload(ctx)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) Load(ctx http.Context) http.Response {
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + r.version)
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Info("获取PHP-" + r.version + "运行状态失败")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+r.version+"] 获取运行状态失败")
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
|
||||
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
|
||||
|
||||
type Data struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
data := make([]Data, len(dataKeys))
|
||||
for i := range dataKeys {
|
||||
data[i].Name = dataKeys[i]
|
||||
|
||||
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
|
||||
match := r.FindStringSubmatch(raw)
|
||||
|
||||
if len(match) > 1 {
|
||||
data[i].Value = strings.TrimSpace(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/php-fpm.log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) SlowLog(ctx http.Context) http.Response {
|
||||
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/slow.log")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, log)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/php-fpm.log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/slow.log"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) GetExtensionList(ctx http.Context) http.Response {
|
||||
extensions := r.GetExtensions()
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (r *Php82Controller) InstallExtension(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
extensions := r.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展已安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "安装PHP-" + r.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Info("[PHP-" + r.version + "] 创建安装拓展任务失败:" + err.Error())
|
||||
return controllers.ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
|
||||
}
|
||||
|
||||
func (r *Php82Controller) UninstallExtension(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
extensions := r.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展未安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "卸载PHP-" + r.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Info("[PHP-" + r.version + "] 创建卸载拓展任务失败:" + err.Error())
|
||||
return controllers.ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
|
||||
}
|
||||
|
||||
func (r *Php82Controller) GetExtensions() []PHPExtension {
|
||||
var extensions []PHPExtension
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "OPcache",
|
||||
Slug: "Zend OPcache",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "PhpRedis",
|
||||
Slug: "redis",
|
||||
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "ImageMagick",
|
||||
Slug: "imagick",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "Exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, PHPExtension{
|
||||
Name: "pdo_pgsql",
|
||||
Slug: "pdo_pgsql",
|
||||
Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
|
||||
Installed: false,
|
||||
})
|
||||
|
||||
raw, err := tools.Exec("/www/server/php/" + r.version + "/bin/php -m")
|
||||
if err != nil {
|
||||
return extensions
|
||||
}
|
||||
|
||||
rawExtensionList := strings.Split(raw, "\n")
|
||||
for _, item := range rawExtensionList {
|
||||
if !strings.Contains(item, "[") && item != "" {
|
||||
for i := range extensions {
|
||||
if extensions[i].Slug == item {
|
||||
extensions[i].Installed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"panel/app/http/controllers"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
)
|
||||
|
||||
type PHPController struct {
|
||||
service internal.PHP
|
||||
}
|
||||
|
||||
func NewPHPController(version uint) *PHPController {
|
||||
return &PHPController{
|
||||
service: services.NewPHPImpl(version),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PHPController) Status(ctx http.Context) http.Response {
|
||||
status, err := r.service.Status()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, status)
|
||||
}
|
||||
|
||||
func (r *PHPController) Reload(ctx http.Context) http.Response {
|
||||
if err := r.service.Reload(); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *PHPController) Start(ctx http.Context) http.Response {
|
||||
if err := r.service.Start(); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *PHPController) Stop(ctx http.Context) http.Response {
|
||||
if err := r.service.Stop(); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *PHPController) Restart(ctx http.Context) http.Response {
|
||||
if err := r.service.Restart(); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *PHPController) GetConfig(ctx http.Context) http.Response {
|
||||
config, err := r.service.GetConfig()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (r *PHPController) SaveConfig(ctx http.Context) http.Response {
|
||||
config := ctx.Request().Input("config")
|
||||
if err := r.service.SaveConfig(config); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *PHPController) GetFPMConfig(ctx http.Context) http.Response {
|
||||
config, err := r.service.GetFPMConfig()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (r *PHPController) SaveFPMConfig(ctx http.Context) http.Response {
|
||||
config := ctx.Request().Input("config")
|
||||
if err := r.service.SaveFPMConfig(config); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *PHPController) Load(ctx http.Context) http.Response {
|
||||
load, err := r.service.Load()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, load)
|
||||
}
|
||||
|
||||
func (r *PHPController) ErrorLog(ctx http.Context) http.Response {
|
||||
log, _ := r.service.GetErrorLog()
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (r *PHPController) SlowLog(ctx http.Context) http.Response {
|
||||
log, _ := r.service.GetSlowLog()
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (r *PHPController) ClearErrorLog(ctx http.Context) http.Response {
|
||||
err := r.service.ClearErrorLog()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *PHPController) ClearSlowLog(ctx http.Context) http.Response {
|
||||
err := r.service.ClearSlowLog()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *PHPController) GetExtensionList(ctx http.Context) http.Response {
|
||||
extensions, err := r.service.GetExtensions()
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (r *PHPController) InstallExtension(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
if err := r.service.InstallExtension(slug); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
func (r *PHPController) UninstallExtension(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
|
||||
}
|
||||
|
||||
if err := r.service.UninstallExtension(slug); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -8,13 +8,15 @@ import (
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
"panel/types"
|
||||
)
|
||||
|
||||
type Postgresql15Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
setting internal.Setting
|
||||
backup internal.Backup
|
||||
}
|
||||
|
||||
func NewPostgresql15Controller() *Postgresql15Controller {
|
||||
@@ -151,12 +153,12 @@ func (r *Postgresql15Controller) Load(ctx http.Context) http.Response {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL空间占用失败")
|
||||
}
|
||||
|
||||
data := []LoadInfo{
|
||||
{"启动时间", carbon.Parse(time).ToDateTimeString()},
|
||||
{"进程 PID", pid},
|
||||
{"进程数", process},
|
||||
{"总连接数", connections},
|
||||
{"空间占用", storage},
|
||||
data := []types.NV{
|
||||
{Name: "启动时间", Value: carbon.Parse(time).ToDateTimeString()},
|
||||
{Name: "进程 PID", Value: pid},
|
||||
{Name: "进程数", Value: process},
|
||||
{Name: "总连接数", Value: connections},
|
||||
{Name: "空间占用", Value: storage},
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
@@ -319,7 +321,7 @@ func (r *Postgresql15Controller) BackupList(ctx http.Context) http.Response {
|
||||
if startIndex > len(backupList) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []services.BackupFile{},
|
||||
"items": []internal.BackupFile{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(backupList) {
|
||||
@@ -327,7 +329,7 @@ func (r *Postgresql15Controller) BackupList(ctx http.Context) http.Response {
|
||||
}
|
||||
pagedBackupList := backupList[startIndex:endIndex]
|
||||
if pagedBackupList == nil {
|
||||
pagedBackupList = []services.BackupFile{}
|
||||
pagedBackupList = []internal.BackupFile{}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
|
||||
@@ -8,13 +8,15 @@ import (
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
"panel/types"
|
||||
)
|
||||
|
||||
type Postgresql16Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
setting internal.Setting
|
||||
backup internal.Backup
|
||||
}
|
||||
|
||||
func NewPostgresql16Controller() *Postgresql16Controller {
|
||||
@@ -151,12 +153,12 @@ func (r *Postgresql16Controller) Load(ctx http.Context) http.Response {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL空间占用失败")
|
||||
}
|
||||
|
||||
data := []LoadInfo{
|
||||
{"启动时间", carbon.Parse(time).ToDateTimeString()},
|
||||
{"进程 PID", pid},
|
||||
{"进程数", process},
|
||||
{"总连接数", connections},
|
||||
{"空间占用", storage},
|
||||
data := []types.NV{
|
||||
{Name: "启动时间", Value: carbon.Parse(time).ToDateTimeString()},
|
||||
{Name: "进程 PID", Value: pid},
|
||||
{Name: "进程数", Value: process},
|
||||
{Name: "总连接数", Value: connections},
|
||||
{Name: "空间占用", Value: storage},
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
@@ -319,7 +321,7 @@ func (r *Postgresql16Controller) BackupList(ctx http.Context) http.Response {
|
||||
if startIndex > len(backupList) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []services.BackupFile{},
|
||||
"items": []internal.BackupFile{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(backupList) {
|
||||
@@ -327,7 +329,7 @@ func (r *Postgresql16Controller) BackupList(ctx http.Context) http.Response {
|
||||
}
|
||||
pagedBackupList := backupList[startIndex:endIndex]
|
||||
if pagedBackupList == nil {
|
||||
pagedBackupList = []services.BackupFile{}
|
||||
pagedBackupList = []internal.BackupFile{}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/pkg/tools"
|
||||
"panel/types"
|
||||
)
|
||||
|
||||
type RedisController struct {
|
||||
@@ -103,20 +104,20 @@ func (r *RedisController) Load(ctx http.Context) http.Response {
|
||||
}
|
||||
}
|
||||
|
||||
data := []LoadInfo{
|
||||
{"TCP 端口", dataRaw["tcp_port"]},
|
||||
{"已运行天数", dataRaw["uptime_in_days"]},
|
||||
{"连接的客户端数", dataRaw["connected_clients"]},
|
||||
{"已分配的内存总量", dataRaw["used_memory_human"]},
|
||||
{"占用内存总量", dataRaw["used_memory_rss_human"]},
|
||||
{"占用内存峰值", dataRaw["used_memory_peak_human"]},
|
||||
{"内存碎片比率", dataRaw["mem_fragmentation_ratio"]},
|
||||
{"运行以来连接过的客户端的总数", dataRaw["total_connections_received"]},
|
||||
{"运行以来执行过的命令的总数", dataRaw["total_commands_processed"]},
|
||||
{"每秒执行的命令数", dataRaw["instantaneous_ops_per_sec"]},
|
||||
{"查找数据库键成功次数", dataRaw["keyspace_hits"]},
|
||||
{"查找数据库键失败次数", dataRaw["keyspace_misses"]},
|
||||
{"最近一次 fork() 操作耗费的毫秒数", dataRaw["latest_fork_usec"]},
|
||||
data := []types.NV{
|
||||
{Name: "TCP 端口", Value: dataRaw["tcp_port"]},
|
||||
{Name: "已运行天数", Value: dataRaw["uptime_in_days"]},
|
||||
{Name: "连接的客户端数", Value: dataRaw["connected_clients"]},
|
||||
{Name: "已分配的内存总量", Value: dataRaw["used_memory_human"]},
|
||||
{Name: "占用内存总量", Value: dataRaw["used_memory_rss_human"]},
|
||||
{Name: "占用内存峰值", Value: dataRaw["used_memory_peak_human"]},
|
||||
{Name: "内存碎片比率", Value: dataRaw["mem_fragmentation_ratio"]},
|
||||
{Name: "运行以来连接过的客户端的总数", Value: dataRaw["total_connections_received"]},
|
||||
{Name: "运行以来执行过的命令的总数", Value: dataRaw["total_commands_processed"]},
|
||||
{Name: "每秒执行的命令数", Value: dataRaw["instantaneous_ops_per_sec"]},
|
||||
{Name: "查找数据库键成功次数", Value: dataRaw["keyspace_hits"]},
|
||||
{Name: "查找数据库键失败次数", Value: dataRaw["keyspace_misses"]},
|
||||
{Name: "最近一次 fork() 操作耗费的毫秒数", Value: dataRaw["latest_fork_usec"]},
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
commonrequests "panel/app/http/requests/common"
|
||||
requests "panel/app/http/requests/plugins/rsync"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
"panel/types"
|
||||
)
|
||||
|
||||
type RsyncController struct {
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewRsyncController() *RsyncController {
|
||||
return &RsyncController{
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Status
|
||||
//
|
||||
// @Summary 服务状态
|
||||
// @Description 获取 Rsync 服务状态
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/status [get]
|
||||
func (r *RsyncController) Status(ctx http.Context) http.Response {
|
||||
status, err := tools.ServiceStatus("rsyncd")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, status)
|
||||
}
|
||||
|
||||
// Restart
|
||||
//
|
||||
// @Summary 重启服务
|
||||
// @Description 重启 Rsync 服务
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/restart [post]
|
||||
func (r *RsyncController) Restart(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceRestart("rsyncd"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "重启服务失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Start
|
||||
//
|
||||
// @Summary 启动服务
|
||||
// @Description 启动 Rsync 服务
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/start [post]
|
||||
func (r *RsyncController) Start(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceStart("rsyncd"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "启动服务失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Stop
|
||||
//
|
||||
// @Summary 停止服务
|
||||
// @Description 停止 Rsync 服务
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/stop [post]
|
||||
func (r *RsyncController) Stop(ctx http.Context) http.Response {
|
||||
if err := tools.ServiceStop("rsyncd"); err != nil {
|
||||
return nil
|
||||
}
|
||||
status, err := tools.ServiceStatus("rsyncd")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, !status)
|
||||
}
|
||||
|
||||
// List
|
||||
//
|
||||
// @Summary 列出模块
|
||||
// @Description 列出所有 Rsync 模块
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/modules [get]
|
||||
func (r *RsyncController) List(ctx http.Context) http.Response {
|
||||
var paginateRequest commonrequests.Paginate
|
||||
sanitize := controllers.Sanitize(ctx, &paginateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
config, err := tools.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
var modules []types.RsyncModule
|
||||
lines := strings.Split(config, "\n")
|
||||
var currentModule *types.RsyncModule
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
if currentModule != nil {
|
||||
modules = append(modules, *currentModule)
|
||||
}
|
||||
moduleName := line[1 : len(line)-1]
|
||||
currentModule = &types.RsyncModule{
|
||||
Name: moduleName,
|
||||
}
|
||||
} else if currentModule != nil {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
switch key {
|
||||
case "path":
|
||||
currentModule.Path = value
|
||||
case "comment":
|
||||
currentModule.Comment = value
|
||||
case "read only":
|
||||
currentModule.ReadOnly = value == "yes" || value == "true"
|
||||
case "auth users":
|
||||
currentModule.AuthUser = value
|
||||
currentModule.Secret, err = tools.Exec("grep -E '^" + currentModule.AuthUser + ":.*$' /etc/rsyncd.secrets | awk -F ':' '{print $2}'")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取模块"+currentModule.AuthUser+"的密钥失败")
|
||||
}
|
||||
case "hosts allow":
|
||||
currentModule.HostsAllow = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentModule != nil {
|
||||
modules = append(modules, *currentModule)
|
||||
}
|
||||
|
||||
startIndex := (paginateRequest.Page - 1) * paginateRequest.Limit
|
||||
endIndex := paginateRequest.Page * paginateRequest.Limit
|
||||
if startIndex > len(modules) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []types.RsyncModule{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(modules) {
|
||||
endIndex = len(modules)
|
||||
}
|
||||
pagedModules := modules[startIndex:endIndex]
|
||||
if pagedModules == nil {
|
||||
pagedModules = []types.RsyncModule{}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(modules),
|
||||
"items": pagedModules,
|
||||
})
|
||||
}
|
||||
|
||||
// Create
|
||||
//
|
||||
// @Summary 添加模块
|
||||
// @Description 添加 Rsync 模块
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.Create true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/modules [post]
|
||||
func (r *RsyncController) Create(ctx http.Context) http.Response {
|
||||
var createRequest requests.Create
|
||||
sanitize := controllers.Sanitize(ctx, &createRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
config, err := tools.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if strings.Contains(config, "["+createRequest.Name+"]") {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "模块 "+createRequest.Name+" 已存在")
|
||||
}
|
||||
|
||||
conf := `# ` + createRequest.Name + `-START
|
||||
[` + createRequest.Name + `]
|
||||
path = ` + createRequest.Path + `
|
||||
comment = ` + createRequest.Comment + `
|
||||
read only = no
|
||||
auth users = ` + createRequest.AuthUser + `
|
||||
hosts allow = ` + createRequest.HostsAllow + `
|
||||
secrets file = /etc/rsyncd.secrets
|
||||
# ` + createRequest.Name + `-END
|
||||
`
|
||||
|
||||
if err := tools.WriteAppend("/etc/rsyncd.conf", conf); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if out, err := tools.Exec("echo '" + createRequest.AuthUser + ":" + createRequest.Secret + "' >> /etc/rsyncd.secrets"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
if err := tools.ServiceRestart("rsyncd"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Destroy
|
||||
//
|
||||
// @Summary 删除模块
|
||||
// @Description 删除 Rsync 模块
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param name path string true "模块名称"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/modules/{name} [delete]
|
||||
func (r *RsyncController) Destroy(ctx http.Context) http.Response {
|
||||
name := ctx.Request().Input("name")
|
||||
if len(name) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "name 不能为空")
|
||||
}
|
||||
|
||||
config, err := tools.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if !strings.Contains(config, "["+name+"]") {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "模块 "+name+" 不存在")
|
||||
}
|
||||
|
||||
module := tools.Cut(config, "# "+name+"-START", "# "+name+"-END")
|
||||
config = strings.Replace(config, "\n# "+name+"-START"+module+"# "+name+"-END", "", -1)
|
||||
|
||||
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
|
||||
if len(match) == 2 {
|
||||
authUser := match[1]
|
||||
if out, err := tools.Exec("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
|
||||
if err = tools.Write("/etc/rsyncd.conf", config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err = tools.ServiceRestart("rsyncd"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Update
|
||||
//
|
||||
// @Summary 更新模块
|
||||
// @Description 更新 Rsync 模块
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param name path string true "模块名称"
|
||||
// @Param data body requests.Update true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/modules/{name} [post]
|
||||
func (r *RsyncController) Update(ctx http.Context) http.Response {
|
||||
var updateRequest requests.Update
|
||||
sanitize := controllers.Sanitize(ctx, &updateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
config, err := tools.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if !strings.Contains(config, "["+updateRequest.Name+"]") {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "模块 "+updateRequest.Name+" 不存在")
|
||||
}
|
||||
|
||||
newConf := `# ` + updateRequest.Name + `-START
|
||||
[` + updateRequest.Name + `]
|
||||
path = ` + updateRequest.Path + `
|
||||
comment = ` + updateRequest.Comment + `
|
||||
read only = no
|
||||
auth users = ` + updateRequest.AuthUser + `
|
||||
hosts allow = ` + updateRequest.HostsAllow + `
|
||||
secrets file = /etc/rsyncd.secrets
|
||||
# ` + updateRequest.Name + `-END`
|
||||
|
||||
module := tools.Cut(config, "# "+updateRequest.Name+"-START", "# "+updateRequest.Name+"-END")
|
||||
config = strings.Replace(config, "# "+updateRequest.Name+"-START"+module+"# "+updateRequest.Name+"-END", newConf, -1)
|
||||
|
||||
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
|
||||
if len(match) == 2 {
|
||||
authUser := match[1]
|
||||
if out, err := tools.Exec("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
|
||||
if err = tools.Write("/etc/rsyncd.conf", config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if out, err := tools.Exec("echo '" + updateRequest.AuthUser + ":" + updateRequest.Secret + "' >> /etc/rsyncd.secrets"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
|
||||
if err = tools.ServiceRestart("rsyncd"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetConfig
|
||||
//
|
||||
// @Summary 获取配置
|
||||
// @Description 获取 Rsync 配置
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/config [get]
|
||||
func (r *RsyncController) GetConfig(ctx http.Context) http.Response {
|
||||
config, err := tools.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// UpdateConfig
|
||||
//
|
||||
// @Summary 更新配置
|
||||
// @Description 更新 Rsync 配置
|
||||
// @Tags 插件-Rsync
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body requests.UpdateConfig true "request"
|
||||
// @Success 200 {object} controllers.SuccessResponse
|
||||
// @Router /plugins/rsync/config [post]
|
||||
func (r *RsyncController) UpdateConfig(ctx http.Context) http.Response {
|
||||
var updateRequest requests.UpdateConfig
|
||||
sanitize := controllers.Sanitize(ctx, &updateRequest)
|
||||
if sanitize != nil {
|
||||
return sanitize
|
||||
}
|
||||
|
||||
if err := tools.Write("/etc/rsyncd.conf", updateRequest.Config, 0644); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err := tools.ServiceRestart("rsyncd"); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -9,12 +9,14 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
"panel/types"
|
||||
)
|
||||
|
||||
type S3fsController struct {
|
||||
setting services.Setting
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewS3fsController() *S3fsController {
|
||||
@@ -28,7 +30,7 @@ func (r *S3fsController) List(ctx http.Context) http.Response {
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
|
||||
var s3fsList []S3fsMount
|
||||
var s3fsList []types.S3fsMount
|
||||
err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
@@ -39,7 +41,7 @@ func (r *S3fsController) List(ctx http.Context) http.Response {
|
||||
if startIndex > len(s3fsList) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []S3fsMount{},
|
||||
"items": []types.S3fsMount{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(s3fsList) {
|
||||
@@ -47,7 +49,7 @@ func (r *S3fsController) List(ctx http.Context) http.Response {
|
||||
}
|
||||
pagedS3fsList := s3fsList[startIndex:endIndex]
|
||||
if pagedS3fsList == nil {
|
||||
pagedS3fsList = []S3fsMount{}
|
||||
pagedS3fsList = []types.S3fsMount{}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
@@ -93,7 +95,7 @@ func (r *S3fsController) Add(ctx http.Context) http.Response {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录必须为空")
|
||||
}
|
||||
|
||||
var s3fsList []S3fsMount
|
||||
var s3fsList []types.S3fsMount
|
||||
err = json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
@@ -123,7 +125,7 @@ func (r *S3fsController) Add(ctx http.Context) http.Response {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "挂载失败,请检查配置是否正确")
|
||||
}
|
||||
|
||||
s3fsList = append(s3fsList, S3fsMount{
|
||||
s3fsList = append(s3fsList, types.S3fsMount{
|
||||
ID: id,
|
||||
Path: path,
|
||||
Bucket: bucket,
|
||||
@@ -148,13 +150,13 @@ func (r *S3fsController) Delete(ctx http.Context) http.Response {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不能为空")
|
||||
}
|
||||
|
||||
var s3fsList []S3fsMount
|
||||
var s3fsList []types.S3fsMount
|
||||
err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
var mount S3fsMount
|
||||
var mount types.S3fsMount
|
||||
for _, s := range s3fsList {
|
||||
if cast.ToString(s.ID) == id {
|
||||
mount = s
|
||||
@@ -181,7 +183,7 @@ func (r *S3fsController) Delete(ctx http.Context) http.Response {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
var newS3fsList []S3fsMount
|
||||
var newS3fsList []types.S3fsMount
|
||||
for _, s := range s3fsList {
|
||||
if s.ID != mount.ID {
|
||||
newS3fsList = append(newS3fsList, s)
|
||||
|
||||
@@ -296,15 +296,9 @@ func (r *SupervisorController) SaveProcessConfig(ctx http.Context) http.Response
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
|
||||
if out, err := tools.Exec(`supervisorctl reread`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := tools.Exec(`supervisorctl update`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := tools.Exec(`supervisorctl start ` + process); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
_, _ = tools.Exec(`supervisorctl reread`)
|
||||
_, _ = tools.Exec(`supervisorctl update`)
|
||||
_, _ = tools.Exec(`supervisorctl restart ` + process)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -352,15 +346,9 @@ stdout_logfile_maxbytes=2MB
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
|
||||
if out, err := tools.Exec(`supervisorctl reread`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := tools.Exec(`supervisorctl update`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := tools.Exec(`supervisorctl start ` + name); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
_, _ = tools.Exec(`supervisorctl reread`)
|
||||
_, _ = tools.Exec(`supervisorctl update`)
|
||||
_, _ = tools.Exec(`supervisorctl start ` + name)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -393,12 +381,8 @@ func (r *SupervisorController) DeleteProcess(ctx http.Context) http.Response {
|
||||
if err := tools.Remove(logPath); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if out, err := tools.Exec(`supervisorctl reread`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
if out, err := tools.Exec(`supervisorctl update`); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
_, _ = tools.Exec(`supervisorctl reread`)
|
||||
_, _ = tools.Exec(`supervisorctl update`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
requests "panel/app/http/requests/setting"
|
||||
responses "panel/app/http/responses/setting"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type SettingController struct {
|
||||
setting services.Setting
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewSettingController() *SettingController {
|
||||
@@ -29,7 +29,7 @@ func NewSettingController() *SettingController {
|
||||
// @Tags 面板设置
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} SuccessResponse{data=responses.Settings}
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/setting/list [get]
|
||||
func (r *SettingController) List(ctx http.Context) http.Response {
|
||||
var settings []models.Setting
|
||||
@@ -41,24 +41,16 @@ func (r *SettingController) List(ctx http.Context) http.Response {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
var result responses.Settings
|
||||
result.Name = r.setting.Get(models.SettingKeyName)
|
||||
result.Entrance = facades.Config().GetString("http.entrance")
|
||||
result.WebsitePath = r.setting.Get(models.SettingKeyWebsitePath)
|
||||
result.BackupPath = r.setting.Get(models.SettingKeyBackupPath)
|
||||
|
||||
var user models.User
|
||||
err = facades.Auth().User(ctx, &user)
|
||||
err = facades.Auth(ctx).User(&user)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("获取用户信息失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
result.Username = user.Username
|
||||
result.Email = user.Email
|
||||
|
||||
result.Port, err = tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
port, err := tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
@@ -66,7 +58,17 @@ func (r *SettingController) List(ctx http.Context) http.Response {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, result)
|
||||
return Success(ctx, http.Json{
|
||||
"name": r.setting.Get(models.SettingKeyName),
|
||||
"entrance": facades.Config().GetString("http.entrance"),
|
||||
"ssl": facades.Config().GetBool("panel.ssl"),
|
||||
"website_path": r.setting.Get(models.SettingKeyWebsitePath),
|
||||
"backup_path": r.setting.Get(models.SettingKeyBackupPath),
|
||||
"username": user.Username,
|
||||
"password": "",
|
||||
"email": user.Email,
|
||||
"port": port,
|
||||
})
|
||||
}
|
||||
|
||||
// Update
|
||||
@@ -124,7 +126,7 @@ func (r *SettingController) Update(ctx http.Context) http.Response {
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err = facades.Auth().User(ctx, &user)
|
||||
err = facades.Auth(ctx).User(&user)
|
||||
if err != nil {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
@@ -195,7 +197,17 @@ func (r *SettingController) Update(ctx http.Context) http.Response {
|
||||
}
|
||||
}
|
||||
|
||||
if oldPort != port || oldEntrance != entrance {
|
||||
if updateRequest.SSL {
|
||||
if out, err := tools.Exec("sed -i 's/APP_SSL=false/APP_SSL=true/g' /www/panel/panel.conf"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
} else {
|
||||
if out, err := tools.Exec("sed -i 's/APP_SSL=true/APP_SSL=false/g' /www/panel/panel.conf"); err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, out)
|
||||
}
|
||||
}
|
||||
|
||||
if oldPort != port || oldEntrance != entrance || updateRequest.SSL != facades.Config().GetBool("panel.ssl") {
|
||||
tools.RestartPanel()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,24 +3,24 @@ package controllers
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
nethttp "net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fasthttp/websocket"
|
||||
"github.com/goravel/fiber"
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/ssh"
|
||||
)
|
||||
|
||||
type SshController struct {
|
||||
AuthMethod ssh.AuthMethod
|
||||
setting services.Setting
|
||||
setting internal.Setting
|
||||
}
|
||||
|
||||
func NewSshController() *SshController {
|
||||
@@ -85,15 +85,24 @@ func (r *SshController) UpdateInfo(ctx http.Context) http.Response {
|
||||
|
||||
// Session SSH 会话
|
||||
func (r *SshController) Session(ctx http.Context) http.Response {
|
||||
upGrader := websocket.FastHTTPUpgrader{
|
||||
upGrader := websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(ctx *fasthttp.RequestCtx) bool {
|
||||
CheckOrigin: func(r *nethttp.Request) bool {
|
||||
return true
|
||||
},
|
||||
Subprotocols: []string{ctx.Request().Header("Sec-WebSocket-Protocol")},
|
||||
}
|
||||
|
||||
ws, err := upGrader.Upgrade(ctx.Response().Writer(), ctx.Request().Origin(), nil)
|
||||
if err != nil {
|
||||
facades.Log().Tags("面板", "SSH").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Infof("建立连接失败")
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
config := ssh.SSHClientConfigPassword(
|
||||
r.setting.Get(models.SettingKeySshHost)+":"+r.setting.Get(models.SettingKeySshPort),
|
||||
r.setting.Get(models.SettingKeySshUser),
|
||||
@@ -101,59 +110,51 @@ func (r *SshController) Session(ctx http.Context) http.Response {
|
||||
)
|
||||
client, err := ssh.NewSSHClient(config)
|
||||
|
||||
err = upGrader.Upgrade(ctx.(*fiber.Context).Instance().Context(), func(conn *websocket.Conn) {
|
||||
if err != nil {
|
||||
_ = conn.WriteControl(websocket.CloseMessage,
|
||||
[]byte(err.Error()), time.Now().Add(time.Second))
|
||||
return
|
||||
}
|
||||
|
||||
defer client.Close()
|
||||
|
||||
turn, err := ssh.NewTurn(conn, client)
|
||||
if err != nil {
|
||||
_ = conn.WriteControl(websocket.CloseMessage,
|
||||
[]byte(err.Error()), time.Now().Add(time.Second))
|
||||
return
|
||||
}
|
||||
defer turn.Close()
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
var logBuff = bufPool.Get().(*bytes.Buffer)
|
||||
logBuff.Reset()
|
||||
defer bufPool.Put(logBuff)
|
||||
|
||||
ctx2, cancel := context.WithCancel(context.Background())
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := turn.LoopRead(logBuff, ctx2)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "SSH").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("SSH 会话错误")
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := turn.SessionWait()
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "SSH").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Info("SSH 会话错误")
|
||||
}
|
||||
cancel()
|
||||
}()
|
||||
wg.Wait()
|
||||
})
|
||||
if err != nil {
|
||||
_ = ws.WriteControl(websocket.CloseMessage,
|
||||
[]byte(err.Error()), time.Now().Add(time.Second))
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
turn, err := ssh.NewTurn(ws, client)
|
||||
if err != nil {
|
||||
_ = ws.WriteControl(websocket.CloseMessage,
|
||||
[]byte(err.Error()), time.Now().Add(time.Second))
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
defer turn.Close()
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
var logBuff = bufPool.Get().(*bytes.Buffer)
|
||||
logBuff.Reset()
|
||||
defer bufPool.Put(logBuff)
|
||||
|
||||
sshCtx, cancel := context.WithCancel(context.Background())
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err = turn.LoopRead(logBuff, sshCtx); err != nil {
|
||||
facades.Log().Tags("面板", "SSH").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Infof("读取数据失败")
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err = turn.SessionWait(); err != nil {
|
||||
facades.Log().Tags("面板", "SSH").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Infof("会话错误")
|
||||
}
|
||||
cancel()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,35 +1,19 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
swaggerFiles "github.com/swaggo/files/v2"
|
||||
"github.com/swaggo/swag"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/swaggo/http-swagger/v2"
|
||||
|
||||
_ "panel/docs"
|
||||
)
|
||||
|
||||
type SwaggerController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
// Config stores fiberSwagger configuration variables.
|
||||
type Config struct {
|
||||
URL string
|
||||
InstanceName string
|
||||
DocExpansion string
|
||||
DomID string
|
||||
DeepLinking bool
|
||||
PersistAuthorization bool
|
||||
}
|
||||
|
||||
func NewSwaggerController() *SwaggerController {
|
||||
return &SwaggerController{
|
||||
// Inject services
|
||||
}
|
||||
return &SwaggerController{}
|
||||
}
|
||||
|
||||
// Index
|
||||
@@ -41,149 +25,12 @@ func NewSwaggerController() *SwaggerController {
|
||||
// @Failure 500
|
||||
// @Router /swagger [get]
|
||||
func (r *SwaggerController) Index(ctx http.Context) http.Response {
|
||||
config := Config{
|
||||
URL: "swagger.json",
|
||||
DocExpansion: "list",
|
||||
DomID: "swagger-ui",
|
||||
InstanceName: swag.Name,
|
||||
DeepLinking: true,
|
||||
if !facades.Config().GetBool("app.debug") {
|
||||
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
|
||||
}
|
||||
|
||||
// create a template with name
|
||||
index, _ := template.New("swagger_index.html").Parse(indexTemplate)
|
||||
handler := httpSwagger.Handler()
|
||||
handler(ctx.Response().Writer(), ctx.Request().Origin())
|
||||
|
||||
var re = regexp.MustCompile(`^(.*/)([^?].*)?[?|.]*$`)
|
||||
|
||||
matches := re.FindStringSubmatch(ctx.Request().Path())
|
||||
path := matches[2]
|
||||
|
||||
fileExt := filepath.Ext(path)
|
||||
|
||||
switch path {
|
||||
case "":
|
||||
return ctx.Response().Redirect(http.StatusMovedPermanently, filepath.Join(matches[1], "index.html"))
|
||||
|
||||
case "index.html":
|
||||
var builder strings.Builder
|
||||
err := index.Execute(&builder, config)
|
||||
if err != nil {
|
||||
return ctx.Response().Status(http.StatusInternalServerError).String(http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
return ctx.Response().Success().String("html", builder.String())
|
||||
case "swagger.json":
|
||||
return ctx.Response().File("docs/swagger.json")
|
||||
}
|
||||
|
||||
file, err := swaggerFiles.FS.Open(path)
|
||||
if err != nil {
|
||||
return ctx.Response().Status(http.StatusInternalServerError).String(http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
content, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return ctx.Response().Status(http.StatusInternalServerError).String(http.StatusText(http.StatusInternalServerError))
|
||||
}
|
||||
|
||||
return ctx.Response().Success().String(fileExt[0:], string(content))
|
||||
return nil
|
||||
}
|
||||
|
||||
const indexTemplate = `<!-- HTML for static distribution bundle build -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<link href="https://gfont.cdn.haozi.net/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
|
||||
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
|
||||
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
|
||||
<style>
|
||||
html
|
||||
{
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
*,
|
||||
*:before,
|
||||
*:after
|
||||
{
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin:0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
|
||||
<defs>
|
||||
<symbol viewBox="0 0 20 20" id="unlocked">
|
||||
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" id="locked">
|
||||
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" id="close">
|
||||
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" id="large-arrow">
|
||||
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 20 20" id="large-arrow-down">
|
||||
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
|
||||
</symbol>
|
||||
|
||||
|
||||
<symbol viewBox="0 0 24 24" id="jump-to">
|
||||
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 24 24" id="expand">
|
||||
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
|
||||
</symbol>
|
||||
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="./swagger-ui-bundle.js"> </script>
|
||||
<script src="./swagger-ui-standalone-preset.js"> </script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Build a system
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "{{.URL}}",
|
||||
deepLinking: {{.DeepLinking}},
|
||||
docExpansion: "{{.DocExpansion}}",
|
||||
dom_id: "#{{.DomID}}",
|
||||
persistAuthorization: {{.PersistAuthorization}},
|
||||
validatorUrl: null,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
})
|
||||
window.ui = ui
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
`
|
||||
|
||||
@@ -3,6 +3,7 @@ package controllers
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
@@ -62,9 +63,9 @@ func (r *TaskController) Log(ctx http.Context) http.Response {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
log, err := tools.Exec("tail -n 1000 " + task.Log)
|
||||
log, err := tools.Exec(`tail -n 500 '` + task.Log + `'`)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, log)
|
||||
return Error(ctx, http.StatusInternalServerError, "日志已被清理")
|
||||
}
|
||||
|
||||
return Success(ctx, log)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/http/requests/user"
|
||||
responses "panel/app/http/responses/user"
|
||||
"panel/app/models"
|
||||
)
|
||||
|
||||
@@ -61,7 +60,7 @@ func (r *UserController) Login(ctx http.Context) http.Response {
|
||||
}
|
||||
}
|
||||
|
||||
token, loginErr := facades.Auth().LoginUsingID(ctx, user.ID)
|
||||
token, loginErr := facades.Auth(ctx).LoginUsingID(user.ID)
|
||||
if loginErr != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
@@ -81,11 +80,11 @@ func (r *UserController) Login(ctx http.Context) http.Response {
|
||||
// @Tags 用户鉴权
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Success 200 {object} SuccessResponse{data=responses.Info}
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/user/info [get]
|
||||
func (r *UserController) Info(ctx http.Context) http.Response {
|
||||
var user models.User
|
||||
err := facades.Auth().User(ctx, &user)
|
||||
err := facades.Auth(ctx).User(&user)
|
||||
if err != nil {
|
||||
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
|
||||
"error": err.Error(),
|
||||
@@ -93,10 +92,10 @@ func (r *UserController) Info(ctx http.Context) http.Response {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, responses.Info{
|
||||
ID: user.ID,
|
||||
Role: []string{"admin"},
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
return Success(ctx, http.Json{
|
||||
"id": user.ID,
|
||||
"role": []string{"admin"},
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,16 +10,16 @@ import (
|
||||
|
||||
commonrequests "panel/app/http/requests/common"
|
||||
requests "panel/app/http/requests/website"
|
||||
responses "panel/app/http/responses/website"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/internal"
|
||||
"panel/internal/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type WebsiteController struct {
|
||||
website services.Website
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
website internal.Website
|
||||
setting internal.Setting
|
||||
backup internal.Backup
|
||||
}
|
||||
|
||||
func NewWebsiteController() *WebsiteController {
|
||||
@@ -37,8 +37,8 @@ func NewWebsiteController() *WebsiteController {
|
||||
// @Tags 网站管理
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse{data=responses.List}
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse
|
||||
// @Router /panel/websites [get]
|
||||
func (r *WebsiteController) List(ctx http.Context) http.Response {
|
||||
var paginateRequest commonrequests.Paginate
|
||||
@@ -55,9 +55,9 @@ func (r *WebsiteController) List(ctx http.Context) http.Response {
|
||||
return ErrorSystem(ctx)
|
||||
}
|
||||
|
||||
return Success(ctx, responses.List{
|
||||
Total: total,
|
||||
Items: websites,
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": websites,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func (r *WebsiteController) Add(ctx http.Context) http.Response {
|
||||
addRequest.Path = r.setting.Get(models.SettingKeyWebsitePath) + "/" + addRequest.Name
|
||||
}
|
||||
|
||||
website := services.PanelWebsite{
|
||||
website := internal.PanelWebsite{
|
||||
Name: addRequest.Name,
|
||||
Status: true,
|
||||
Domains: addRequest.Domains,
|
||||
@@ -205,7 +205,7 @@ func (r *WebsiteController) SaveDefaultConfig(ctx http.Context) http.Response {
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param id path int true "网站 ID"
|
||||
// @Success 200 {object} SuccessResponse{data=services.PanelWebsite}
|
||||
// @Success 200 {object} SuccessResponse{data=internal.PanelWebsite}
|
||||
// @Router /panel/websites/{id}/config [get]
|
||||
func (r *WebsiteController) GetConfig(ctx http.Context) http.Response {
|
||||
var idRequest requests.ID
|
||||
@@ -327,8 +327,8 @@ func (r *WebsiteController) UpdateRemark(ctx http.Context) http.Response {
|
||||
// @Tags 网站管理
|
||||
// @Produce json
|
||||
// @Security BearerToken
|
||||
// @Param data body commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse{data=[]services.BackupFile}
|
||||
// @Param data query commonrequests.Paginate true "request"
|
||||
// @Success 200 {object} SuccessResponse{data=[]internal.BackupFile}
|
||||
// @Router /panel/website/backupList [get]
|
||||
func (r *WebsiteController) BackupList(ctx http.Context) http.Response {
|
||||
var paginateRequest commonrequests.Paginate
|
||||
@@ -350,7 +350,7 @@ func (r *WebsiteController) BackupList(ctx http.Context) http.Response {
|
||||
if startIndex > len(backupList) {
|
||||
return Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []services.BackupFile{},
|
||||
"items": []internal.BackupFile{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(backupList) {
|
||||
@@ -358,7 +358,7 @@ func (r *WebsiteController) BackupList(ctx http.Context) http.Response {
|
||||
}
|
||||
pagedBackupList := backupList[startIndex:endIndex]
|
||||
if pagedBackupList == nil {
|
||||
pagedBackupList = []services.BackupFile{}
|
||||
pagedBackupList = []internal.BackupFile{}
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
@@ -567,7 +567,7 @@ server
|
||||
# php标记位结束
|
||||
|
||||
# waf标记位开始
|
||||
waf on;
|
||||
waf off;
|
||||
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
|
||||
waf_mode DYNAMIC;
|
||||
waf_cc_deny rate=1000r/m duration=60m;
|
||||
|
||||
+5
-1
@@ -2,6 +2,8 @@ package http
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"panel/app/http/middleware"
|
||||
)
|
||||
|
||||
type Kernel struct {
|
||||
@@ -10,5 +12,7 @@ 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{}
|
||||
return []http.Middleware{
|
||||
middleware.Status(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,21 +13,19 @@ func Jwt() http.Middleware {
|
||||
return func(ctx http.Context) {
|
||||
token := ctx.Request().Header("Authorization", ctx.Request().Header("Sec-WebSocket-Protocol"))
|
||||
if len(token) == 0 {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
|
||||
"code": 401,
|
||||
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
|
||||
"message": "未登录",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// JWT 鉴权
|
||||
if _, err := facades.Auth().Parse(ctx, token); err != nil {
|
||||
if _, err := facades.Auth(ctx).Parse(token); err != nil {
|
||||
if errors.Is(err, auth.ErrorTokenExpired) {
|
||||
token, err = facades.Auth().Refresh(ctx)
|
||||
token, err = facades.Auth(ctx).Refresh()
|
||||
if err != nil {
|
||||
// 到达刷新时间上限
|
||||
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
|
||||
"code": 401,
|
||||
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
|
||||
"message": "登录已过期",
|
||||
})
|
||||
return
|
||||
@@ -35,8 +33,7 @@ func Jwt() http.Middleware {
|
||||
|
||||
token = "Bearer " + token
|
||||
} else {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
|
||||
"code": 401,
|
||||
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
|
||||
"message": "登录已过期",
|
||||
})
|
||||
return
|
||||
|
||||
@@ -2,11 +2,10 @@ package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"panel/app/services"
|
||||
"panel/internal/services"
|
||||
)
|
||||
|
||||
// MustInstall 确保已安装插件
|
||||
@@ -19,8 +18,7 @@ func MustInstall() http.Middleware {
|
||||
} else {
|
||||
pathArr := strings.Split(path, "/")
|
||||
if len(pathArr) < 4 {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
|
||||
"code": http.StatusForbidden,
|
||||
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
|
||||
"message": "插件不存在",
|
||||
})
|
||||
return
|
||||
@@ -32,37 +30,29 @@ func MustInstall() http.Middleware {
|
||||
installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug)
|
||||
installedPlugins, err := services.NewPluginImpl().AllInstalled()
|
||||
if err != nil {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
|
||||
"code": http.StatusInternalServerError,
|
||||
ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, http.Json{
|
||||
"message": "系统内部错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if installedPlugin.Version != plugin.Version || installedPlugin.Slug != plugin.Slug {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
|
||||
"code": http.StatusForbidden,
|
||||
"message": "插件 " + slug + " 需要更新至 " + plugin.Version + " 版本",
|
||||
if installedPlugin.Slug != plugin.Slug {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
|
||||
"message": "插件 " + slug + " 未安装",
|
||||
})
|
||||
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 {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
|
||||
"code": http.StatusForbidden,
|
||||
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
|
||||
"message": "插件 " + slug + " 需要依赖 " + require + " 插件",
|
||||
})
|
||||
return
|
||||
@@ -70,12 +60,9 @@ func MustInstall() http.Middleware {
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
|
||||
"code": http.StatusForbidden,
|
||||
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
|
||||
"message": "插件 " + slug + " 不兼容 " + exclude + " 插件",
|
||||
})
|
||||
return
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"panel/internal"
|
||||
)
|
||||
|
||||
// Status 检查程序状态
|
||||
func Status() http.Middleware {
|
||||
return func(ctx http.Context) {
|
||||
switch internal.Status {
|
||||
case internal.StatusUpgrade:
|
||||
ctx.Request().AbortWithStatusJson(http.StatusServiceUnavailable, http.Json{
|
||||
"message": "面板升级中,请稍后",
|
||||
})
|
||||
return
|
||||
case internal.StatusMaintain:
|
||||
ctx.Request().AbortWithStatusJson(http.StatusServiceUnavailable, http.Json{
|
||||
"message": "面板正在运行维护,请稍后",
|
||||
})
|
||||
return
|
||||
case internal.StatusClosed:
|
||||
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
|
||||
"message": "面板已关闭",
|
||||
})
|
||||
return
|
||||
case internal.StatusFailed:
|
||||
ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, http.Json{
|
||||
"message": "面板运行出错,请检查排除或联系支持",
|
||||
})
|
||||
return
|
||||
default:
|
||||
ctx.Request().Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
)
|
||||
|
||||
type CertDeploy struct {
|
||||
ID uint `form:"id" json:"id"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id"`
|
||||
ID uint `form:"id" json:"id" filter:"uint"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id" filter:"uint"`
|
||||
}
|
||||
|
||||
func (r *CertDeploy) Authorize(ctx http.Context) error {
|
||||
|
||||
@@ -26,7 +26,6 @@ func (r *DNSStore) Rules(ctx http.Context) map[string]string {
|
||||
"data.token": "required_if:type,dnspod",
|
||||
"data.access_key": "required_if:type,aliyun",
|
||||
"data.secret_key": "required_if:type,aliyun",
|
||||
"data.email": "required_if:type,cloudflare",
|
||||
"data.api_key": "required_if:type,cloudflare",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
)
|
||||
|
||||
type Paginate struct {
|
||||
Page int `form:"page" json:"page"`
|
||||
Limit int `form:"limit" json:"limit"`
|
||||
Page int `form:"page" json:"page" filter:"int"`
|
||||
Limit int `form:"limit" json:"limit" filter:"int"`
|
||||
}
|
||||
|
||||
func (r *Paginate) Authorize(ctx http.Context) error {
|
||||
@@ -16,20 +16,13 @@ func (r *Paginate) Authorize(ctx http.Context) error {
|
||||
|
||||
func (r *Paginate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"page": "required|uint|min:1",
|
||||
"limit": "required|uint|min:1",
|
||||
"page": "required|int|min:1",
|
||||
"limit": "required|int|min:1",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Paginate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"page.required": "分页参数 page 不能为空",
|
||||
"page.uint": "分页参数 page 必须是一个整数",
|
||||
"page.min": "分页参数 page 必须大于等于 1",
|
||||
"limit.required": "分页参数 limit 不能为空",
|
||||
"limit.uint": "分页参数 limit 必须是一个整数",
|
||||
"limit.min": "分页参数 limit 必须大于等于 1",
|
||||
}
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Paginate) Attributes(ctx http.Context) map[string]string {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
|
||||
"panel/types"
|
||||
)
|
||||
|
||||
type ContainerCreate struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Image string `form:"image" json:"image"`
|
||||
Ports []types.ContainerPort `form:"ports" json:"ports"`
|
||||
Network string `form:"network" json:"network"`
|
||||
Volumes []types.ContainerVolume `form:"volumes" json:"volumes"`
|
||||
Labels []types.KV `form:"labels" json:"labels"`
|
||||
Env []types.KV `form:"env" json:"env"`
|
||||
Entrypoint []string `form:"entrypoint" json:"entrypoint"`
|
||||
Command []string `form:"command" json:"command"`
|
||||
RestartPolicy string `form:"restart_policy" json:"restart_policy"`
|
||||
AutoRemove bool `form:"auto_remove" json:"auto_remove"`
|
||||
Privileged bool `form:"privileged" json:"privileged"`
|
||||
OpenStdin bool `form:"openStdin" json:"open_stdin"`
|
||||
PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"`
|
||||
Tty bool `form:"tty" json:"tty"`
|
||||
CPUShares int64 `form:"cpu_shares" json:"cpu_shares" filter:"int"`
|
||||
CPUs int64 `form:"cpus" json:"cpus" filter:"int"`
|
||||
Memory int64 `form:"memory" json:"memory" filter:"int"`
|
||||
}
|
||||
|
||||
func (r *ContainerCreate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ContainerCreate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required|string",
|
||||
"image": "required|string",
|
||||
"ports": "slice",
|
||||
/*"ports.*.host": "string",
|
||||
"ports.*.host_start": "int",
|
||||
"ports.*.host_end": "int",
|
||||
"ports.*.container_start": "int",
|
||||
"ports.*.container_end": "int",
|
||||
"ports.*.protocol": "string|in:tcp,udp",*/
|
||||
"network": "string",
|
||||
"volumes": "slice",
|
||||
/*"volumes.*.host": "string",
|
||||
"volumes.*.container": "string",
|
||||
"volumes.*.mode": "string|in:ro,rw",*/
|
||||
"labels": "slice",
|
||||
"env": "slice",
|
||||
"entrypoint": "slice",
|
||||
"command": "slice",
|
||||
"restart_policy": "string|in:always,on-failure,unless-stopped,no",
|
||||
"auto_remove": "bool",
|
||||
"privileged": "bool",
|
||||
"open_stdin": "bool",
|
||||
"publish_all_ports": "bool",
|
||||
"tty": "bool",
|
||||
"cpu_shares": "int",
|
||||
"cpus": "int",
|
||||
"memory": "int",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ContainerCreate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ContainerCreate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ContainerCreate) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type ContainerRename struct {
|
||||
ID string `form:"id" json:"id"`
|
||||
Name string `form:"name" json:"name"`
|
||||
}
|
||||
|
||||
func (r *ContainerRename) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ContainerRename) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|string",
|
||||
"name": "required|string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ContainerRename) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ContainerRename) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ContainerRename) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
|
||||
"panel/types"
|
||||
)
|
||||
|
||||
type ContainerUpdate struct {
|
||||
ID string `form:"id" json:"id"`
|
||||
Name string `form:"name" json:"name"`
|
||||
Image string `form:"image" json:"image"`
|
||||
Ports []types.ContainerPort `form:"ports" json:"ports"`
|
||||
Network string `form:"network" json:"network"`
|
||||
Volumes []types.ContainerVolume `form:"volumes" json:"volumes"`
|
||||
Labels []string `form:"labels" json:"labels"`
|
||||
Env []string `form:"env" json:"env"`
|
||||
Entrypoint []string `form:"entrypoint" json:"entrypoint"`
|
||||
Command []string `form:"command" json:"command"`
|
||||
RestartPolicy string `form:"restart_policy" json:"restart_policy"`
|
||||
AutoRemove bool `form:"auto_remove" json:"auto_remove"`
|
||||
Privileged bool `form:"privileged" json:"privileged"`
|
||||
OpenStdin bool `form:"openStdin" json:"open_stdin"`
|
||||
PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"`
|
||||
Tty bool `form:"tty" json:"tty"`
|
||||
CPUShares int64 `form:"cpu_shares" json:"cpu_shares"`
|
||||
CPUs int64 `form:"cpus" json:"cpus"`
|
||||
Memory int64 `form:"memory" json:"memory"`
|
||||
}
|
||||
|
||||
func (r *ContainerUpdate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ContainerUpdate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|string",
|
||||
"name": "required|string",
|
||||
"image": "required|string",
|
||||
"ports": "slice",
|
||||
"network": "string",
|
||||
"volumes": "slice",
|
||||
"labels": "slice",
|
||||
"env": "slice",
|
||||
"entrypoint": "slice",
|
||||
"command": "slice",
|
||||
"restart_policy": "string|in:always,on-failure,unless-stopped,no",
|
||||
"auto_remove": "bool",
|
||||
"privileged": "bool",
|
||||
"open_stdin": "bool",
|
||||
"publish_all_ports": "bool",
|
||||
"tty": "bool",
|
||||
"cpu_shares": "int",
|
||||
"cpus": "int",
|
||||
"memory": "int",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ContainerUpdate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ContainerUpdate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ContainerUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type ID struct {
|
||||
ID string `form:"id" json:"id"`
|
||||
}
|
||||
|
||||
func (r *ID) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ID) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"id": "required|string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ID) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ID) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ID) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type ImagePull struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Auth bool `form:"auth" json:"auth"`
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
}
|
||||
|
||||
func (r *ImagePull) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ImagePull) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required|string",
|
||||
"auth": "bool",
|
||||
"username": "string",
|
||||
"password": "string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ImagePull) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ImagePull) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *ImagePull) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type NetworkConnectDisConnect struct {
|
||||
Network string `form:"network" json:"network"`
|
||||
Container string `form:"container" json:"container"`
|
||||
}
|
||||
|
||||
func (r *NetworkConnectDisConnect) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *NetworkConnectDisConnect) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"network": "required|string",
|
||||
"container": "required|string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *NetworkConnectDisConnect) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *NetworkConnectDisConnect) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *NetworkConnectDisConnect) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
|
||||
"panel/types"
|
||||
)
|
||||
|
||||
type NetworkCreate struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Driver string `form:"driver" json:"driver"`
|
||||
Ipv4 types.ContainerNetwork `form:"ipv4" json:"ipv4"`
|
||||
Ipv6 types.ContainerNetwork `form:"ipv6" json:"ipv6"`
|
||||
Labels []types.KV `form:"labels" json:"labels"`
|
||||
Options []types.KV `form:"options" json:"options"`
|
||||
}
|
||||
|
||||
func (r *NetworkCreate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *NetworkCreate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required|string",
|
||||
"driver": "required|string|in:bridge,overlay,macvlan,ipvlan",
|
||||
"ipv4": "required",
|
||||
"ipv6": "required",
|
||||
"labels": "slice",
|
||||
"options": "slice",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *NetworkCreate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *NetworkCreate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *NetworkCreate) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
|
||||
"panel/types"
|
||||
)
|
||||
|
||||
type VolumeCreate struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Driver string `form:"driver" json:"driver"`
|
||||
Labels []types.KV `form:"labels" json:"labels"`
|
||||
Options []types.KV `form:"options" json:"options"`
|
||||
}
|
||||
|
||||
func (r *VolumeCreate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *VolumeCreate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required|string",
|
||||
"driver": "required|string|in:local",
|
||||
"labels": "slice",
|
||||
"options": "slice",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *VolumeCreate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *VolumeCreate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *VolumeCreate) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Archive struct {
|
||||
Paths []string `form:"paths" json:"paths"`
|
||||
File string `form:"file" json:"file"`
|
||||
}
|
||||
|
||||
func (r *Archive) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Archive) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"paths": "array",
|
||||
"paths.*": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$|path_exists",
|
||||
"file": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$|path_not_exists",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Archive) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Archive) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Archive) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Copy struct {
|
||||
Old string `form:"old" json:"old"`
|
||||
New string `form:"new" json:"new"`
|
||||
}
|
||||
|
||||
func (r *Copy) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Copy) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"old": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$|path_exists",
|
||||
"new": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Copy) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Copy) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Copy) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Exist struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
}
|
||||
|
||||
func (r *Exist) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Exist) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"path": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$|path_exists",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Exist) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Exist) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Exist) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Move struct {
|
||||
Old string `form:"old" json:"old"`
|
||||
New string `form:"new" json:"new"`
|
||||
}
|
||||
|
||||
func (r *Move) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Move) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"old": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$|path_exists",
|
||||
"new": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Move) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Move) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Move) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type NotExist struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
}
|
||||
|
||||
func (r *NotExist) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *NotExist) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"path": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$|path_not_exists",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *NotExist) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *NotExist) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *NotExist) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Permission struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
Mode uint `form:"mode" json:"mode" filter:"uint"`
|
||||
Owner string `form:"owner" json:"owner"`
|
||||
Group string `form:"group" json:"group"`
|
||||
}
|
||||
|
||||
func (r *Permission) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Permission) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"path": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$|path_exists",
|
||||
"mode": "regex:^[0-7]{3}$|uint",
|
||||
"owner": "regex:^[a-zA-Z0-9_-]+$",
|
||||
"group": "regex:^[a-zA-Z0-9_-]+$",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Permission) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Permission) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Permission) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Save struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
Content string `form:"content" json:"content"`
|
||||
}
|
||||
|
||||
func (r *Save) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Save) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"path": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$|path_exists",
|
||||
"content": "required|string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Save) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Save) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Save) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Search struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
KeyWord string `form:"keyword" json:"keyword"`
|
||||
}
|
||||
|
||||
func (r *Search) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Search) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"path": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$|path_exists",
|
||||
"keyword": "required|string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Search) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Search) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Search) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type UnArchive struct {
|
||||
File string `form:"file" json:"file"`
|
||||
Path string `form:"path" json:"path"`
|
||||
}
|
||||
|
||||
func (r *UnArchive) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UnArchive) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"file": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$|path_exists",
|
||||
"path": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$|path_not_exists",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UnArchive) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *UnArchive) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *UnArchive) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Upload struct {
|
||||
Path string `form:"path" json:"path"`
|
||||
File *multipart.FileHeader `form:"file" json:"file"`
|
||||
}
|
||||
|
||||
func (r *Upload) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Upload) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"path": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$",
|
||||
"file": "required",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Upload) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Upload) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Upload) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Create struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Path string `form:"path" json:"path"`
|
||||
Comment string `form:"comment" json:"comment"`
|
||||
AuthUser string `form:"auth_user" json:"auth_user"`
|
||||
Secret string `form:"secret" json:"secret"`
|
||||
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
||||
}
|
||||
|
||||
func (r *Create) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Create) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required|regex:^[a-zA-Z0-9-_]+$",
|
||||
"path": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$",
|
||||
"comment": "string",
|
||||
"auth_user": "required|regex:^[a-zA-Z0-9-_]+$",
|
||||
"secret": "required|min_len:8",
|
||||
"hosts_allow": "string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Create) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Create) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Create) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type Update struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
Path string `form:"path" json:"path"`
|
||||
Comment string `form:"comment" json:"comment"`
|
||||
AuthUser string `form:"auth_user" json:"auth_user"`
|
||||
Secret string `form:"secret" json:"secret"`
|
||||
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
||||
}
|
||||
|
||||
func (r *Update) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Update) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"name": "required|regex:^[a-zA-Z0-9-_]+$",
|
||||
"path": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$",
|
||||
"comment": "string",
|
||||
"auth_user": "required|regex:^[a-zA-Z0-9-_]+$",
|
||||
"secret": "required|min_len:8",
|
||||
"hosts_allow": "string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Update) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Update) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *Update) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
}
|
||||
|
||||
func (r *UpdateConfig) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UpdateConfig) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"config": "required|string",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UpdateConfig) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *UpdateConfig) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *UpdateConfig) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -11,6 +11,7 @@ type Update struct {
|
||||
BackupPath string `form:"backup_path" json:"backup_path"`
|
||||
WebsitePath string `form:"website_path" json:"website_path"`
|
||||
Entrance string `form:"entrance" json:"entrance"`
|
||||
SSL bool `form:"ssl" json:"ssl"`
|
||||
UserName string `form:"username" json:"username"`
|
||||
Email string `form:"email" json:"email"`
|
||||
Password string `form:"password" json:"password"`
|
||||
@@ -27,6 +28,7 @@ func (r *Update) Rules(ctx http.Context) map[string]string {
|
||||
"backup_path": "required|string:2,255",
|
||||
"website_path": "required|string:2,255",
|
||||
"entrance": `required|regex:^/(\w+)?$|not_in:/api`,
|
||||
"ssl": "bool",
|
||||
"username": "required|string:2,20",
|
||||
"email": "required|email",
|
||||
"password": "string:8,255",
|
||||
|
||||
@@ -10,7 +10,7 @@ type Add struct {
|
||||
Domains []string `form:"domains" json:"domains"`
|
||||
Ports []uint `form:"ports" json:"ports"`
|
||||
Path string `form:"path" json:"path"`
|
||||
Php int `form:"php" json:"php"`
|
||||
Php string `form:"php" json:"php"`
|
||||
Db bool `form:"db" json:"db"`
|
||||
DbType string `form:"db_type" json:"db_type"`
|
||||
DbName string `form:"db_name" json:"db_name"`
|
||||
@@ -27,7 +27,7 @@ func (r *Add) Rules(ctx http.Context) map[string]string {
|
||||
"name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$|not_exists:websites,name|not_in:phpmyadmin,mysql,panel,ssh",
|
||||
"domains": "required|slice",
|
||||
"ports": "required|slice",
|
||||
"path": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$",
|
||||
"path": "regex:^/[a-zA-Z0-9_.@#$%-]+(\\/[a-zA-Z0-9_.@#$%-]+)*$",
|
||||
"php": "required",
|
||||
"db": "bool",
|
||||
"db_type": "required_if:db,true|in:0,mysql,postgresql",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package responses
|
||||
|
||||
import "panel/app/models"
|
||||
|
||||
type CertList struct {
|
||||
Total int64 `json:"total"`
|
||||
Items []models.Cert `json:"items"`
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package responses
|
||||
|
||||
import "panel/app/models"
|
||||
|
||||
type DNSList struct {
|
||||
Total int64 `json:"total"`
|
||||
Items []models.CertDNS `json:"items"`
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package responses
|
||||
|
||||
import "panel/app/models"
|
||||
|
||||
type UserList struct {
|
||||
Total int64 `json:"total"`
|
||||
Items []models.CertUser `json:"items"`
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package responses
|
||||
|
||||
type Settings struct {
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
Port string `json:"port"`
|
||||
Entrance string `json:"entrance"`
|
||||
WebsitePath string `json:"website_path"`
|
||||
BackupPath string `json:"backup_path"`
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package responses
|
||||
|
||||
type Info struct {
|
||||
ID uint `json:"id"`
|
||||
Role []string `json:"role"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package responses
|
||||
|
||||
import "panel/app/models"
|
||||
|
||||
type List struct {
|
||||
Total int64 `json:"total"`
|
||||
Items []models.Website `json:"items"`
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
|
||||
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"`
|
||||
Name string `json:"name"`
|
||||
Status bool `gorm:"default:true" json:"status"`
|
||||
Path string `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"`
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package fail2ban
|
||||
|
||||
var (
|
||||
Name = "Fail2ban"
|
||||
Description = "Fail2ban 扫描系统日志文件并从中找出多次尝试失败的IP地址,将该IP地址加入防火墙的拒绝访问列表中。"
|
||||
Slug = "fail2ban"
|
||||
Version = "1.0.0"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/fail2ban/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/fail2ban/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/fail2ban/update.sh`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package mysql57
|
||||
|
||||
var (
|
||||
Name = "MySQL-5.7"
|
||||
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。"
|
||||
Slug = "mysql57"
|
||||
Version = "5.7.44"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"mysql80"}
|
||||
Install = `bash /www/panel/scripts/mysql/install.sh 57`
|
||||
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 57`
|
||||
Update = `bash /www/panel/scripts/mysql/update.sh 57`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package mysql80
|
||||
|
||||
var (
|
||||
Name = "MySQL-8.0"
|
||||
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。(内存 < 4G 无法安装)"
|
||||
Slug = "mysql80"
|
||||
Version = "8.0.35"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"mysql57"}
|
||||
Install = `bash /www/panel/scripts/mysql/install.sh 80`
|
||||
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 80`
|
||||
Update = `bash /www/panel/scripts/mysql/update.sh 80`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package openresty
|
||||
|
||||
var (
|
||||
Name = "OpenResty"
|
||||
Description = "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台。"
|
||||
Slug = "openresty"
|
||||
Version = "1.21.4.3"
|
||||
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"
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package php74
|
||||
|
||||
var (
|
||||
Name = "PHP-7.4"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php74"
|
||||
Version = "7.4.33"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/php/install.sh 74`
|
||||
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 74`
|
||||
Update = `bash /www/panel/scripts/php/install.sh 74`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package php80
|
||||
|
||||
var (
|
||||
Name = "PHP-8.0"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php80"
|
||||
Version = "8.0.30"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/php/install.sh 80`
|
||||
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 80`
|
||||
Update = `bash /www/panel/scripts/php/install.sh 80`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package php81
|
||||
|
||||
var (
|
||||
Name = "PHP-8.1"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php81"
|
||||
Version = "8.1.25"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/php/install.sh 81`
|
||||
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 81`
|
||||
Update = `bash /www/panel/scripts/php/install.sh 81`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package php82
|
||||
|
||||
var (
|
||||
Name = "PHP-8.2"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php82"
|
||||
Version = "8.2.12"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/php/install.sh 82`
|
||||
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 82`
|
||||
Update = `bash /www/panel/scripts/php/install.sh 82`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package phpmyadmin
|
||||
|
||||
var (
|
||||
Name = "phpMyAdmin"
|
||||
Description = "phpMyAdmin 是一个以 PHP 为基础,以 Web-Base 方式架构在网站主机上的 MySQL 数据库管理工具。"
|
||||
Slug = "phpmyadmin"
|
||||
Version = "5.2.1"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/phpmyadmin/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/phpmyadmin/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/phpmyadmin/uninstall.sh && bash /www/panel/scripts/phpmyadmin/install.sh`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package postgresql15
|
||||
|
||||
var (
|
||||
Name = "PostgreSQL-15"
|
||||
Description = "PostgreSQL 是世界上最先进的开源关系数据库,在类似 BSD 与 MIT 许可的 PostgreSQL 许可下发行。"
|
||||
Slug = "postgresql15"
|
||||
Version = "15.5"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"postgresql16"}
|
||||
Install = `bash /www/panel/scripts/postgresql/install.sh 15`
|
||||
Uninstall = `bash /www/panel/scripts/postgresql/uninstall.sh 15`
|
||||
Update = `bash /www/panel/scripts/postgresql/update.sh 15`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package postgresql16
|
||||
|
||||
var (
|
||||
Name = "PostgreSQL-16"
|
||||
Description = "PostgreSQL 是世界上最先进的开源关系数据库,在类似 BSD 与 MIT 许可的 PostgreSQL 许可下发行。"
|
||||
Slug = "postgresql16"
|
||||
Version = "16.1"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"postgresql15"}
|
||||
Install = `bash /www/panel/scripts/postgresql/install.sh 16`
|
||||
Uninstall = `bash /www/panel/scripts/postgresql/uninstall.sh 16`
|
||||
Update = `bash /www/panel/scripts/postgresql/update.sh 16`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package pureftpd
|
||||
|
||||
var (
|
||||
Name = "Pure-FTPd"
|
||||
Description = "Pure-Ftpd 是一个快速、高效、轻便、安全的 FTP 服务器,它以安全和配置简单为设计目标,支持虚拟主机,IPV6,PAM 等功能。"
|
||||
Slug = "pureftpd"
|
||||
Version = "1.0.50"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/pureftpd/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/pureftpd/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/pureftpd/update.sh`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package redis
|
||||
|
||||
var (
|
||||
Name = "Redis"
|
||||
Description = "Redis 是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。"
|
||||
Slug = "redis"
|
||||
Version = "7.2.3"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/redis/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/redis/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/redis/update.sh`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package s3fs
|
||||
|
||||
var (
|
||||
Name = "S3fs"
|
||||
Description = "S3fs 通过 FUSE 挂载兼容 S3 标准的存储桶,例如Amazon S3、阿里云OSS、腾讯云COS、七牛云Kodo等。"
|
||||
Slug = "s3fs"
|
||||
Version = "1.9"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/s3fs/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/s3fs/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/s3fs/install.sh`
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user