mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da44f1a8fe | |||
| 0317aa1401 | |||
| 90fc48f478 | |||
| a8178cbcd6 | |||
| 91ecd04c27 | |||
| 6743ac9621 | |||
| 5606d980be | |||
| 34300431a4 | |||
| 6f0f4c69ef | |||
| 5fba45e3a4 | |||
| bb140ade7b | |||
| 1316c50a57 | |||
| f14b4f5c54 | |||
| b08eb6bf1f | |||
| 7f5869f805 | |||
| ecadaa9bbc | |||
| 60b9350625 | |||
| d64f099f8e | |||
| 294a78cd95 | |||
| 8294a1a74e | |||
| c403efd762 | |||
| cf8dfca4c0 | |||
| e552caa38f | |||
| ddff13990a | |||
| 27f79ea7a3 | |||
| 04d570197a | |||
| 8e74428023 | |||
| f4c09324b7 | |||
| ee6a12fac2 | |||
| ba429068ad | |||
| 711ccb4504 | |||
| 7d6e078e20 | |||
| d3a7c9e29c | |||
| 10d22bfbc0 | |||
| 724d05d980 | |||
| 690e7f0391 | |||
| a28fc592d1 | |||
| ff7e76ac2b | |||
| f2eb3a180c | |||
| 020a4ad870 | |||
| b5e7cfc069 | |||
| 52f8a6f045 | |||
| 3831d47a23 | |||
| 3a579f2346 | |||
| e85e8dffb3 | |||
| e18d40b0ab | |||
| c308b8e42c | |||
| cafded526f | |||
| 99aa6c99d8 | |||
| e0daee4766 | |||
| bf8c832b32 | |||
| 2d35cbc176 | |||
| 6b51f58856 | |||
| f58c680ced | |||
| 9c6d0b5837 | |||
| d069cbec75 | |||
| 9b252aa8f6 | |||
| a52d06e3cb | |||
| c25c78bfef | |||
| 567f3ab693 | |||
| aa162e12db | |||
| 7169b90c1d | |||
| 5e1001f10f | |||
| 8e26589c80 | |||
| 27ec65650d | |||
| e2345a8285 | |||
| 4ebe20d7f8 | |||
| 8f5872b284 | |||
| 75c6522453 | |||
| ff34cc72cc | |||
| e2fb1080da | |||
| b96c6ad899 | |||
| c1a4fb0904 | |||
| 5c50da27c5 | |||
| 336a9bc675 | |||
| ea3bf37687 | |||
| 79e09f9cb7 | |||
| d73c988073 | |||
| 9641ae7f34 | |||
| fd2c89a480 | |||
| 526f192200 | |||
| 6423953aeb | |||
| 1091ec8a23 | |||
| 2cbc641b2d | |||
| f4d23211e3 | |||
| 1b6086bb0b | |||
| f399e9fcec | |||
| 17cf1ada0e |
@@ -19,9 +19,7 @@ body:
|
||||
options:
|
||||
- label: 我已经搜索了现有的 Issues, Discussions 和 Google (I've searched the existing Issues, Discussions and Google)
|
||||
required: true
|
||||
- label: 我已经阅读了 README 中除版本记录以外的所有内容 (I've read everything in the README except the version notes)
|
||||
required: true
|
||||
- label: 我在提问题之前至少花费了 5 分钟来思考和准备 (I spend at least 5 minutes thinking and preparing before asking a question)
|
||||
- label: 我已经阅读了文档中除版本记录以外的所有内容 (I've read everything in the documentation except the version notes)
|
||||
required: true
|
||||
- label: 我没有改动过面板及系统的任何文件 (I have not changed any files of the panel and system)
|
||||
required: false
|
||||
@@ -37,9 +35,10 @@ body:
|
||||
请提供安装面板的操作系统。
|
||||
Please provide the operating system on which the panel is installed.
|
||||
options:
|
||||
- RockyLinux
|
||||
- AlmaLinux
|
||||
- Debian
|
||||
- Ubuntu
|
||||
- AlmaLinux
|
||||
- RockyLinux
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
@@ -50,7 +49,7 @@ body:
|
||||
description: |
|
||||
请提供操作系统的版本号。
|
||||
Please provide the version number of the operating system.
|
||||
placeholder: "9.0"
|
||||
placeholder: "12"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -60,7 +59,7 @@ body:
|
||||
description: |
|
||||
请提供面板的版本号。
|
||||
Please provide the version number of the panel.
|
||||
placeholder: "2.0.0"
|
||||
placeholder: "2.3.0"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.1 KiB |
@@ -1,26 +0,0 @@
|
||||
name: Codecov
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
codecov:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Run tests with coverage
|
||||
run: sudo go test -v -coverprofile="coverage.out" ./...
|
||||
- name: Upload coverage report to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.out
|
||||
token: ${{ secrets.CODECOV }}
|
||||
@@ -3,9 +3,6 @@ on:
|
||||
schedule:
|
||||
- cron: "0 */6 * * *"
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
issues:
|
||||
types: [ opened ]
|
||||
permissions:
|
||||
|
||||
@@ -21,4 +21,9 @@ jobs:
|
||||
run: |
|
||||
cp config.example.yml config.yml
|
||||
- name: Run tests
|
||||
run: sudo go test ./...
|
||||
run: sudo go test -v -coverprofile="coverage.out" ./...
|
||||
- name: Upload coverage report to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
file: ./coverage.out
|
||||
token: ${{ secrets.CODECOV }}
|
||||
@@ -16,7 +16,7 @@
|
||||
<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>
|
||||
|
||||
耗子面板是使用 Golang + Vue 开发的开源轻量 Linux 服务器运维管理面板。
|
||||
耗子面板是新一代全能服务器运维管理面板。简单轻量,高效运维。
|
||||
|
||||
QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 微信群:[复制此链接](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | 论坛:[tom.moe](https://tom.moe) | 赞助:[爱发电](https://afdian.com/a/TheTNB)
|
||||
|
||||
@@ -63,10 +63,7 @@ QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 微信群:[复制
|
||||
|
||||
随着系统版本的不断更新,我们亦可能会终止部分过于老旧的系统的支持,以保证面板的健壮性。
|
||||
|
||||
## 安装面板
|
||||
|
||||
> **Warning**
|
||||
> 安装面板前,您需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
|
||||
## 挂载硬盘
|
||||
|
||||
如果您的服务器有未挂载的数据盘,可在安装前以`root`用户登录服务器运行以下命令自动挂载,面板安装后不支持跨目录迁移。
|
||||
|
||||
@@ -74,9 +71,12 @@ QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 微信群:[复制
|
||||
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
|
||||
```
|
||||
|
||||
准备就绪后,请以`root`用户登录服务器,运行以下命令安装面板:
|
||||
## 安装面板
|
||||
|
||||
**当前 v2.3.x 为测试版本,欢迎帮助我们测试在不同操作系统下的兼容性。**
|
||||
> **Warning**
|
||||
> 安装面板前,您需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
|
||||
|
||||
以`root`用户登录服务器,运行以下命令安装面板:
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash install.sh
|
||||
@@ -140,6 +140,12 @@ panel-cli
|
||||
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
|
||||
</a>
|
||||
|
||||
### 合作伙伴
|
||||
|
||||
<a href="https://1ms.run">
|
||||
<img height="80" src=".github/assets/1ms.svg" alt="毫秒镜像提供经过审核的 Docker 镜像加速服务">
|
||||
</a>
|
||||
|
||||
### 赞助商们
|
||||
|
||||
<p align="center">
|
||||
@@ -152,19 +158,25 @@ panel-cli
|
||||
|
||||
这个项目的存在要归功于所有做出贡献的人,参与贡献请先查看贡献代码部分。
|
||||
|
||||
<a href="https://github.com/TheTNB/panel/graphs/contributors">
|
||||
<img alt="contributors" src="https://contrib.rocks/image?repo=TheTNB/panel"/>
|
||||
</a>
|
||||
<p align="center">
|
||||
<a href="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors?repo_id=572922963&limit=30" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=dark" width="655" height="auto">
|
||||
<img alt="Active Contributors of TheTNB/panel - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=light" width="655" height="auto">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Star 历史
|
||||
|
||||
<a href="https://star-history.com/#TheTNB/panel&Date">
|
||||
<picture>
|
||||
<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>
|
||||
<p align="center">
|
||||
<a href="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history?repo_id=572922963" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history/thumbnail.png?repo_id=572922963&image_size=auto&color_scheme=dark" width="721" height="auto">
|
||||
<img alt="Star History of TheTNB/panel" src="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history/thumbnail.png?repo_id=572922963&image_size=auto&color_scheme=light" width="721" height="auto">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 免责声明
|
||||
|
||||
|
||||
+29
-17
@@ -16,7 +16,7 @@
|
||||
<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 Rat Panel is an open source lightweight Linux server operation and maintenance management panel developed using Golang and Vue.
|
||||
Rat Panel is a new generation of all-in-one server operation and maintenance management panel. Simple and lightweight, efficient operation and maintenance.
|
||||
|
||||
QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | WeChat group: [Copy this link](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | Forum: [tom.moe](https://tom.moe) | Sponsor: [Open Collective](https://opencollective.com/tnb)
|
||||
|
||||
@@ -63,10 +63,7 @@ For other systems not listed in the table below, you can try to install them by
|
||||
|
||||
As system versions are constantly updated, we may also terminate support for some older systems to ensure the robustness of the panel.
|
||||
|
||||
## Install Panel
|
||||
|
||||
> **Warning**
|
||||
> Before installing the panel, you need to understand the basic knowledge of the LNMP environment and how to deal with common LNMP environment problems. We are not recommended for users with zero basic knowledge to install and use Rat Panel.
|
||||
## Mount Disk
|
||||
|
||||
If your server has an unmounted data disk, you can run the following command as the `root` user to automatically mount it before installation. The panel does not support cross-directory migration after installation.
|
||||
|
||||
@@ -74,9 +71,12 @@ If your server has an unmounted data disk, you can run the following command as
|
||||
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
|
||||
```
|
||||
|
||||
After you are ready, log in to the server as the `root` user and run the following command to install the panel:
|
||||
## Install Panel
|
||||
|
||||
**Current v2.3.x is a test version, welcome to help us test compatibility on different operating systems.**
|
||||
> **Warning**
|
||||
> Before installing the panel, you need to understand the basic knowledge of the LNMP environment and how to deal with common LNMP environment problems. We are not recommended for users with zero basic knowledge to install and use Rat Panel.
|
||||
|
||||
Login to the server as the `root` user and run the following command to install the panel:
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash install.sh
|
||||
@@ -140,6 +140,12 @@ If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollecti
|
||||
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
|
||||
</a>
|
||||
|
||||
### Partners
|
||||
|
||||
<a href="https://1ms.run">
|
||||
<img height="80" src=".github/assets/1ms.svg" alt="毫秒镜像提供经过审核的 Docker 镜像加速服务">
|
||||
</a>
|
||||
|
||||
### Sponsors
|
||||
|
||||
<p align="center">
|
||||
@@ -152,19 +158,25 @@ If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollecti
|
||||
|
||||
This project owes its existence to all those who have contributed. To contribute, please check the contributed code section first.
|
||||
|
||||
<a href="https://github.com/TheTNB/panel/graphs/contributors">
|
||||
<img alt="contributors" src="https://contrib.rocks/image?repo=TheTNB/panel"/>
|
||||
</a>
|
||||
<p align="center">
|
||||
<a href="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors?repo_id=572922963&limit=30" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=dark" width="655" height="auto">
|
||||
<img alt="Active Contributors of TheTNB/panel - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=light" width="655" height="auto">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#TheTNB/panel&Date">
|
||||
<picture>
|
||||
<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>
|
||||
<p align="center">
|
||||
<a href="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history?repo_id=572922963" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history/thumbnail.png?repo_id=572922963&image_size=auto&color_scheme=dark" width="721" height="auto">
|
||||
<img alt="Star History of TheTNB/panel" src="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history/thumbnail.png?repo_id=572922963&image_size=auto&color_scheme=light" width="721" height="auto">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ go 1.23
|
||||
require (
|
||||
github.com/bddjr/hlfhr v1.1.3
|
||||
github.com/beevik/ntp v1.4.3
|
||||
github.com/creack/pty v1.1.23
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/expr-lang/expr v1.16.9
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
@@ -16,21 +16,22 @@ require (
|
||||
github.com/go-rat/chix v1.1.4
|
||||
github.com/go-rat/gormstore v1.0.6
|
||||
github.com/go-rat/sessions v1.0.11
|
||||
github.com/go-rat/utils v1.0.3
|
||||
github.com/go-resty/resty/v2 v2.15.3
|
||||
github.com/go-rat/utils v1.0.7
|
||||
github.com/go-resty/resty/v2 v2.16.0
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a
|
||||
github.com/gomodule/redigo v1.9.2
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/knadh/koanf/parsers/yaml v0.1.0
|
||||
github.com/knadh/koanf/providers/file v1.1.2
|
||||
github.com/knadh/koanf/v2 v2.1.1
|
||||
github.com/knadh/koanf/v2 v2.1.2
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/libdns/alidns v1.0.3
|
||||
github.com/libdns/cloudflare v0.1.1
|
||||
github.com/libdns/huaweicloud v0.2.2
|
||||
github.com/libdns/libdns v0.2.2
|
||||
github.com/libdns/tencentcloud v1.0.0
|
||||
github.com/libdns/tencentcloud v1.1.0
|
||||
github.com/mholt/acmez/v2 v2.0.3
|
||||
github.com/orandin/slog-gorm v1.4.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
@@ -40,11 +41,11 @@ require (
|
||||
github.com/spf13/cast v1.7.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.3
|
||||
go.uber.org/zap v1.27.0
|
||||
go.uber.org/zap/exp v0.3.0
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/net v0.30.0
|
||||
golang.org/x/crypto v0.29.0
|
||||
golang.org/x/net v0.31.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/gorm v1.25.12
|
||||
@@ -56,11 +57,11 @@ require (
|
||||
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-rat/securecookie v1.0.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/gofiber/schema v1.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jaevor/go-nanoid v1.4.0 // indirect
|
||||
@@ -75,13 +76,15 @@ require (
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1033 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1033 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
modernc.org/libc v1.60.1 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
|
||||
@@ -6,8 +6,8 @@ github.com/bddjr/hlfhr v1.1.3 h1:O1Vxi7Qf0tOs9oGoSb3Q9KAGTSfh/EXyVMVSO9V1m90=
|
||||
github.com/bddjr/hlfhr v1.1.3/go.mod h1:oyIv4Q9JpCgZFdtH3KyTNWp7YYRWl4zl8k4ozrMAB4g=
|
||||
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
|
||||
github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q=
|
||||
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
|
||||
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -21,8 +21,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
@@ -49,18 +49,20 @@ github.com/go-rat/securecookie v1.0.1 h1:HW0fpKmB+FjJzXTw8ABOwBJ+XrPmRBSZqHhmrv8
|
||||
github.com/go-rat/securecookie v1.0.1/go.mod h1:tP/ObWYyjmcpabQ7WTon/i2lBSip/Aolliw2llXuPDU=
|
||||
github.com/go-rat/sessions v1.0.11 h1:2eTUvhEBtymrAtwv71LlGHNylz+Ad2SgY3jg/sSUboA=
|
||||
github.com/go-rat/sessions v1.0.11/go.mod h1:Ray/GCbuhm4U9xpjFFSCfOTCEn91puEhAXX5creHE9g=
|
||||
github.com/go-rat/utils v1.0.3 h1:SqH/O0KYq4SBv8naSAjJA4hC45/d8NLNrTCONfqjbFM=
|
||||
github.com/go-rat/utils v1.0.3/go.mod h1:4WNPlrF57KmeGZN3HOeBgdBVLJL3xgba4QRP/t6pKrE=
|
||||
github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8=
|
||||
github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/go-rat/utils v1.0.7 h1:hZUe+n1W/tvF3j+oP/TeAHX3AF98SjVB1FuF11zkCAI=
|
||||
github.com/go-rat/utils v1.0.7/go.mod h1:b0S/vd2bXWvPUQpw8z6RmJrUG7G7BkMuNSrsz6JRBHs=
|
||||
github.com/go-resty/resty/v2 v2.16.0 h1:qpKalHWI2bpp9BIKlyT8TYWEJXOk1NuKbfiT3RRnzWc=
|
||||
github.com/go-resty/resty/v2 v2.16.0/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg=
|
||||
github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
|
||||
github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a h1:BAyyIK6rc6Tq9vT2KVlPJXD8EVv/LpaFwn4fgfLwtHg=
|
||||
github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a/go.mod h1:bgk4Ij/0OQ89UeoFFAQrSNhbbr4rKJ0fwWfo7wc+TCc=
|
||||
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
|
||||
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
@@ -85,8 +87,8 @@ github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP
|
||||
github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
|
||||
github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w=
|
||||
github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
|
||||
github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
|
||||
github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
|
||||
github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ=
|
||||
github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -106,8 +108,8 @@ github.com/libdns/huaweicloud v0.2.2/go.mod h1:F9vNIca+Cq1ZJiMCSkHQUdvXEGY6UbLoa
|
||||
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/libdns/tencentcloud v1.0.0 h1:u4LXnYu/lu/9P5W+MCVPeSDnwI+6w+DxYhQ1wSnQOuU=
|
||||
github.com/libdns/tencentcloud v1.0.0/go.mod h1:NlCgPumzUsZWSOo1+Q/Hfh8G6TNRAaTUeWQdg6LbtUI=
|
||||
github.com/libdns/tencentcloud v1.1.0 h1:UvrU9HR3Cc87vIkj/FrTGkaZigGcRyZFdLCYqKDac1c=
|
||||
github.com/libdns/tencentcloud v1.1.0/go.mod h1:PuP62hTGHSI3OqSUsrzuvsCWMD+9BlPtVjKasOrAtJ4=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
@@ -144,16 +146,18 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 h1:C0GHdLTfikLVoEzfhgPfrZ7LwlG0xiCmk6iwNKE+xs0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1033 h1:g263/dapUpOAZJa1Y9x07WgfOl7Yy+FM5Mrf4WyttS8=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1033/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1033 h1:bRCo+X6i/6B+5Q/G4+rexcMRxpSq+9XS/8+Usn6OcOA=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1033/go.mod h1:ta9FOsQP/SvQAhDD6IhVhYWyY+eboPEbPWq7zpiNwZA=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8 h1:7yw1yHkylDcu/CwY4hEEe8MycDLo7B9LjcqwhoL8NrM=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8/go.mod h1:itu4KWRgrfEwGcfNka+rV4houuirUau53i0diN4lG5g=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1 h1:1fJU+bltkwN8lF4Sni/X0i1d8XwPIrS82ivZ8qsp/q4=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.3 h1:RfQlgUHMRxDMwEEmGsrHd+mXYJpWpXlcJM8w86cpjGs=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.3/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
@@ -169,25 +173,27 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -199,16 +205,16 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -216,16 +222,16 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
|
||||
@@ -37,6 +37,7 @@ const (
|
||||
|
||||
// 面板全局变量
|
||||
var (
|
||||
Key string
|
||||
Root string
|
||||
Version string
|
||||
Locale string
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/go-rat/utils/str"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
@@ -16,7 +17,6 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
_ "github.com/TheTNB/panel/internal/apps/fail2ban"
|
||||
_ "github.com/TheTNB/panel/internal/apps/frp"
|
||||
_ "github.com/TheTNB/panel/internal/apps/gitea"
|
||||
_ "github.com/TheTNB/panel/internal/apps/memcached"
|
||||
_ "github.com/TheTNB/panel/internal/apps/mysql"
|
||||
_ "github.com/TheTNB/panel/internal/apps/nginx"
|
||||
_ "github.com/TheTNB/panel/internal/apps/php"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package memcached
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "memcached",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package memcached
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package memcached
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("memcached")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "failed to get Memcached status: %v", err)
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:11211")
|
||||
if err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write([]byte("stats\nquit\n"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "failed to write to Memcached: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := make([]types.NV, 0)
|
||||
re := regexp.MustCompile(`STAT\s(\S+)\s(\S+)`)
|
||||
scanner := bufio.NewScanner(conn)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if matches := re.FindStringSubmatch(line); len(matches) == 3 {
|
||||
data = append(data, types.NV{
|
||||
Name: matches[1],
|
||||
Value: matches[2],
|
||||
})
|
||||
}
|
||||
if line == "END" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "failed to read from Memcached: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/systemd/system/memcached.service")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/systemd/system/memcached.service", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("memcached"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
@@ -123,7 +123,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
if len(matches) > 1 {
|
||||
d := map[string]string{"name": expression.name, "value": matches[1]}
|
||||
if expression.name == "发送" || expression.name == "接收" {
|
||||
d["value"] = str.FormatBytes(cast.ToFloat64(matches[1]))
|
||||
d["value"] = tools.FormatBytes(cast.ToFloat64(matches[1]))
|
||||
}
|
||||
|
||||
load = append(load, d)
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
@@ -95,7 +95,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Error(w, http.StatusInternalServerError, "获取负载失败")
|
||||
return
|
||||
}
|
||||
mem := str.FormatBytes(cast.ToFloat64(out))
|
||||
mem := tools.FormatBytes(cast.ToFloat64(out))
|
||||
data = append(data, types.NV{
|
||||
Name: "内存占用",
|
||||
Value: mem,
|
||||
|
||||
+24
-72
@@ -1,6 +1,8 @@
|
||||
package php
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
@@ -8,76 +10,26 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "php80",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService(80)
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/fpmConfig", service.GetFPMConfig)
|
||||
r.Post("/fpmConfig", service.UpdateFPMConfig)
|
||||
r.Get("/errorLog", service.ErrorLog)
|
||||
r.Get("/slowLog", service.SlowLog)
|
||||
r.Post("/clearErrorLog", service.ClearErrorLog)
|
||||
r.Post("/clearSlowLog", service.ClearSlowLog)
|
||||
r.Get("/extensions", service.ExtensionList)
|
||||
r.Post("/extensions", service.InstallExtension)
|
||||
r.Delete("/extensions", service.UninstallExtension)
|
||||
},
|
||||
})
|
||||
apploader.Register(&types.App{
|
||||
Slug: "php81",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService(81)
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/fpmConfig", service.GetFPMConfig)
|
||||
r.Post("/fpmConfig", service.UpdateFPMConfig)
|
||||
r.Get("/errorLog", service.ErrorLog)
|
||||
r.Get("/slowLog", service.SlowLog)
|
||||
r.Post("/clearErrorLog", service.ClearErrorLog)
|
||||
r.Post("/clearSlowLog", service.ClearSlowLog)
|
||||
r.Get("/extensions", service.ExtensionList)
|
||||
r.Post("/extensions", service.InstallExtension)
|
||||
r.Delete("/extensions", service.UninstallExtension)
|
||||
},
|
||||
})
|
||||
apploader.Register(&types.App{
|
||||
Slug: "php82",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService(82)
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/fpmConfig", service.GetFPMConfig)
|
||||
r.Post("/fpmConfig", service.UpdateFPMConfig)
|
||||
r.Get("/errorLog", service.ErrorLog)
|
||||
r.Get("/slowLog", service.SlowLog)
|
||||
r.Post("/clearErrorLog", service.ClearErrorLog)
|
||||
r.Post("/clearSlowLog", service.ClearSlowLog)
|
||||
r.Get("/extensions", service.ExtensionList)
|
||||
r.Post("/extensions", service.InstallExtension)
|
||||
r.Delete("/extensions", service.UninstallExtension)
|
||||
},
|
||||
})
|
||||
apploader.Register(&types.App{
|
||||
Slug: "php83",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService(83)
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/fpmConfig", service.GetFPMConfig)
|
||||
r.Post("/fpmConfig", service.UpdateFPMConfig)
|
||||
r.Get("/errorLog", service.ErrorLog)
|
||||
r.Get("/slowLog", service.SlowLog)
|
||||
r.Post("/clearErrorLog", service.ClearErrorLog)
|
||||
r.Post("/clearSlowLog", service.ClearSlowLog)
|
||||
r.Get("/extensions", service.ExtensionList)
|
||||
r.Post("/extensions", service.InstallExtension)
|
||||
r.Delete("/extensions", service.UninstallExtension)
|
||||
},
|
||||
})
|
||||
php := []uint{74, 80, 81, 82, 83}
|
||||
for _, version := range php {
|
||||
apploader.Register(&types.App{
|
||||
Slug: fmt.Sprintf("php%d", version),
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService(version)
|
||||
r.Post("/setCli", service.SetCli)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/fpmConfig", service.GetFPMConfig)
|
||||
r.Post("/fpmConfig", service.UpdateFPMConfig)
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/errorLog", service.ErrorLog)
|
||||
r.Get("/slowLog", service.SlowLog)
|
||||
r.Post("/clearErrorLog", service.ClearErrorLog)
|
||||
r.Post("/clearSlowLog", service.ClearSlowLog)
|
||||
r.Get("/extensions", service.ExtensionList)
|
||||
r.Post("/extensions", service.InstallExtension)
|
||||
r.Delete("/extensions", service.UninstallExtension)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,15 @@ func NewService(version uint) *Service {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SetCli(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf("ln -sf %s/server/php/%d/bin/php /usr/bin/php", app.Root, s.version); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, s.version))
|
||||
if err != nil {
|
||||
@@ -138,17 +147,6 @@ func (s *Service) ClearSlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Service) ExtensionList(w http.ResponseWriter, r *http.Request) {
|
||||
extensions := s.getExtensions()
|
||||
|
||||
// ionCube 只支持 PHP 8.3 以下版本
|
||||
if cast.ToUint(s.version) < 83 {
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的 PHP 加密解密工具。",
|
||||
Installed: false,
|
||||
})
|
||||
}
|
||||
|
||||
raw, err := shell.Execf("%s/server/php/%d/bin/php -m", app.Root, s.version)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -182,10 +180,10 @@ func (s *Service) InstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/%s.sh" | bash -s -- "install" "%d" >> /tmp/%s.log 2>&1`, url.QueryEscape(req.Slug), s.version, req.Slug)
|
||||
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/%s.sh' | bash -s -- 'install' '%d' >> '/tmp/%s.log' 2>&1`, url.PathEscape(req.Slug), s.version, req.Slug)
|
||||
officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"}
|
||||
if slices.Contains(officials, req.Slug) {
|
||||
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/official.sh" | bash -s -- "install" "%d" "%s" >> /tmp/%s.log 2>&1`, s.version, req.Slug, req.Slug)
|
||||
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/official.sh' | bash -s -- 'install' '%d' '%s' >> '/tmp/%s.log' 2>&1`, s.version, req.Slug, req.Slug)
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
@@ -213,10 +211,10 @@ func (s *Service) UninstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/%s.sh" | bash -s -- "uninstall" "%d" >> /tmp/%s.log 2>&1`, url.QueryEscape(req.Slug), s.version, req.Slug)
|
||||
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/%s.sh' | bash -s -- 'uninstall' '%d' >> '/tmp/%s.log' 2>&1`, url.PathEscape(req.Slug), s.version, req.Slug)
|
||||
officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"}
|
||||
if slices.Contains(officials, req.Slug) {
|
||||
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/official.sh" | bash -s -- "uninstall" "%d" "%s" >> /tmp/%s.log 2>&1`, s.version, req.Slug, req.Slug)
|
||||
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/official.sh' | bash -s -- 'uninstall' '%d' '%s' >> '/tmp/%s.log' 2>&1`, s.version, req.Slug, req.Slug)
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
@@ -237,137 +235,141 @@ func (s *Service) getExtensions() []Extension {
|
||||
{
|
||||
Name: "fileinfo",
|
||||
Slug: "fileinfo",
|
||||
Description: "Fileinfo 是一个用于识别文件类型的库。",
|
||||
Description: "Fileinfo 是一个用于识别文件类型的库",
|
||||
},
|
||||
{
|
||||
Name: "OPcache",
|
||||
Slug: "Zend OPcache",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销",
|
||||
},
|
||||
{
|
||||
Name: "PhpRedis",
|
||||
Name: "Redis",
|
||||
Slug: "redis",
|
||||
Description: "PhpRedis 是一个用 C 语言编写的 PHP 模块,用来连接并操作 Redis 数据库上的数据。",
|
||||
Description: "PhpRedis 是一个用 C 语言编写的 PHP 模块,用来连接并操作 Redis 数据库上的数据",
|
||||
},
|
||||
{
|
||||
Name: "Memcached",
|
||||
Slug: "memcached",
|
||||
Description: "Memcached 使用 libmemcached 库连接 Memcached 服务器",
|
||||
},
|
||||
{
|
||||
Name: "ImageMagick",
|
||||
Slug: "imagick",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件",
|
||||
},
|
||||
{
|
||||
Name: "exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,您可以操作图像元数据。",
|
||||
Description: "通过 exif 扩展,您可以操作图像元数据",
|
||||
},
|
||||
{
|
||||
Name: "pdo_pgsql",
|
||||
Slug: "pdo_pgsql",
|
||||
Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
|
||||
Description: "pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问(需先安装PostgreSQL)",
|
||||
},
|
||||
{
|
||||
Name: "imap",
|
||||
Slug: "imap",
|
||||
Description: "IMAP 扩展允许 PHP 读取、搜索、删除、下载和管理邮件。",
|
||||
Description: "IMAP 扩展允许 PHP 读取、搜索、删除、下载和管理邮件",
|
||||
},
|
||||
{
|
||||
Name: "zip",
|
||||
Slug: "zip",
|
||||
Description: "Zip 是一个用于处理 ZIP 文件的库。",
|
||||
Description: "Zip 是一个用于处理 ZIP 文件的库",
|
||||
},
|
||||
{
|
||||
Name: "bz2",
|
||||
Slug: "bz2",
|
||||
Description: "Bzip2 是一个用于压缩和解压缩文件的库。",
|
||||
Description: "Bzip2 是一个用于压缩和解压缩文件的库",
|
||||
},
|
||||
{
|
||||
Name: "readline",
|
||||
Slug: "readline",
|
||||
Description: "Readline 是一个库,它提供了一种用于处理文本的接口。",
|
||||
Description: "Readline 是一个库,它提供了一种用于处理文本的接口",
|
||||
},
|
||||
{
|
||||
Name: "snmp",
|
||||
Slug: "snmp",
|
||||
Description: "SNMP 是一种用于网络管理的协议。",
|
||||
Description: "SNMP 是一种用于网络管理的协议",
|
||||
},
|
||||
{
|
||||
Name: "ldap",
|
||||
Slug: "ldap",
|
||||
Description: "LDAP 是一种用于访问目录服务的协议。",
|
||||
Description: "LDAP 是一种用于访问目录服务的协议",
|
||||
},
|
||||
{
|
||||
Name: "enchant",
|
||||
Slug: "enchant",
|
||||
Description: "Enchant 是一个拼写检查库。",
|
||||
Description: "Enchant 是一个拼写检查库",
|
||||
},
|
||||
{
|
||||
Name: "pspell",
|
||||
Slug: "pspell",
|
||||
Description: "Pspell 是一个拼写检查库。",
|
||||
Description: "Pspell 是一个拼写检查库",
|
||||
},
|
||||
{
|
||||
Name: "calendar",
|
||||
Slug: "calendar",
|
||||
Description: "Calendar 是一个用于处理日期的库。",
|
||||
Description: "Calendar 是一个用于处理日期的库",
|
||||
},
|
||||
{
|
||||
Name: "gmp",
|
||||
Slug: "gmp",
|
||||
Description: "GMP 是一个用于处理大整数的库。",
|
||||
Description: "GMP 是一个用于处理大整数的库",
|
||||
},
|
||||
{
|
||||
Name: "sysvmsg",
|
||||
Slug: "sysvmsg",
|
||||
Description: "Sysvmsg 是一个用于处理 System V 消息队列的库。",
|
||||
Description: "Sysvmsg 是一个用于处理 System V 消息队列的库",
|
||||
},
|
||||
{
|
||||
Name: "sysvsem",
|
||||
Slug: "sysvsem",
|
||||
Description: "Sysvsem 是一个用于处理 System V 信号量的库。",
|
||||
Description: "Sysvsem 是一个用于处理 System V 信号量的库",
|
||||
},
|
||||
{
|
||||
Name: "sysvshm",
|
||||
Slug: "sysvshm",
|
||||
Description: "Sysvshm 是一个用于处理 System V 共享内存的库。",
|
||||
Description: "Sysvshm 是一个用于处理 System V 共享内存的库",
|
||||
},
|
||||
{
|
||||
Name: "xsl",
|
||||
Slug: "xsl",
|
||||
Description: "XSL 是一个用于处理 XML 文档的库。",
|
||||
Description: "XSL 是一个用于处理 XML 文档的库",
|
||||
},
|
||||
{
|
||||
Name: "intl",
|
||||
Slug: "intl",
|
||||
Description: "Intl 是一个用于处理国际化和本地化的库。",
|
||||
Description: "Intl 是一个用于处理国际化和本地化的库",
|
||||
},
|
||||
{
|
||||
Name: "gettext",
|
||||
Slug: "gettext",
|
||||
Description: "Gettext 是一个用于处理多语言的库。",
|
||||
Description: "Gettext 是一个用于处理多语言的库",
|
||||
},
|
||||
{
|
||||
Name: "igbinary",
|
||||
Slug: "igbinary",
|
||||
Description: "Igbinary 是一个用于序列化和反序列化数据的库。",
|
||||
Description: "Igbinary 是一个用于序列化和反序列化数据的库",
|
||||
},
|
||||
{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的 PHP 加密解密工具(需在 OPcache 之后安装)",
|
||||
},
|
||||
{
|
||||
Name: "Swoole",
|
||||
Slug: "swoole",
|
||||
Description: "Swoole 是一个用于构建高性能的异步并发服务器的 PHP 扩展。",
|
||||
},
|
||||
{
|
||||
Name: "Swow",
|
||||
Slug: "Swow",
|
||||
Description: "Swow 是一个用于构建高性能的异步并发服务器的 PHP 扩展。",
|
||||
Description: "Swoole 是一个用于构建高性能的异步并发服务器的 PHP 扩展",
|
||||
},
|
||||
}
|
||||
|
||||
// ionCube 只支持 PHP 8.3 以下版本
|
||||
if cast.ToUint(s.version) < 83 {
|
||||
// Swow 不支持 PHP 8.0 以下版本
|
||||
if cast.ToUint(s.version) >= 80 {
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的 PHP 加密解密工具。",
|
||||
Installed: false,
|
||||
Name: "Swow",
|
||||
Slug: "Swow",
|
||||
Description: "Swow 是一个用于构建高性能的异步并发服务器的 PHP 扩展。",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -16,5 +16,5 @@ type ChangePassword struct {
|
||||
}
|
||||
|
||||
type UpdatePort struct {
|
||||
Port uint `form:"port" json:"port" validate:"required"`
|
||||
Port uint `form:"port" json:"port" validate:"required,number,gte=1,lte=65535"`
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/go-rat/utils/str"
|
||||
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/ntp"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
@@ -77,7 +77,7 @@ func (s *Service) GetSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
size = file.Size() / 1024 / 1024
|
||||
total = str.FormatBytes(float64(file.Size()))
|
||||
total = tools.FormatBytes(float64(file.Size()))
|
||||
} else {
|
||||
size = 0
|
||||
total = "0.00 B"
|
||||
@@ -91,8 +91,8 @@ func (s *Service) GetSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) >= 4 {
|
||||
used = str.FormatBytes(cast.ToFloat64(match[2]) * 1024)
|
||||
free = str.FormatBytes(cast.ToFloat64(match[3]) * 1024)
|
||||
used = tools.FormatBytes(cast.ToFloat64(match[2]) * 1024)
|
||||
free = tools.FormatBytes(cast.ToFloat64(match[3]) * 1024)
|
||||
}
|
||||
|
||||
service.Success(w, chix.M{
|
||||
@@ -134,7 +134,7 @@ func (s *Service) UpdateSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if cast.ToInt64(free)*1024 < req.Size*1024*1024 {
|
||||
service.Error(w, http.StatusInternalServerError, "硬盘空间不足,当前剩余%s", str.FormatBytes(cast.ToFloat64(free)))
|
||||
service.Error(w, http.StatusInternalServerError, "硬盘空间不足,当前剩余%s", tools.FormatBytes(cast.ToFloat64(free)))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+64
-11
@@ -1,16 +1,69 @@
|
||||
package biz
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/utils/crypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type DatabaseStatus string
|
||||
|
||||
const (
|
||||
DatabaseStatusNormal DatabaseStatus = "normal"
|
||||
DatabaseStatusInvalid DatabaseStatus = "invalid"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;unique" json:"name"`
|
||||
Type string `gorm:"not null" json:"type"`
|
||||
Host string `gorm:"not null" json:"host"`
|
||||
Port int `gorm:"not null" json:"port"`
|
||||
Username string `gorm:"not null" json:"username"`
|
||||
Password string `gorm:"not null" json:"password"`
|
||||
Remark string `gorm:"not null" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ServerID uint `gorm:"not null" json:"server_id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Status DatabaseStatus `gorm:"not null" json:"status"`
|
||||
Username string `gorm:"not null" json:"username"`
|
||||
Password string `gorm:"not null" json:"password"`
|
||||
Remark string `gorm:"not null" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
Server *DatabaseServer `gorm:"foreignKey:ServerID" json:"server"`
|
||||
}
|
||||
|
||||
func (r *Database) BeforeSave(tx *gorm.DB) error {
|
||||
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Password, err = crypter.Encrypt([]byte(r.Password))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Database) AfterFind(tx *gorm.DB) error {
|
||||
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := crypter.Decrypt(r.Password)
|
||||
if err == nil {
|
||||
r.Password = string(password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DatabaseRepo interface {
|
||||
Count() (int64, error)
|
||||
List(page, limit uint) ([]*Database, int64, error)
|
||||
Get(id uint) (*Database, error)
|
||||
Create(req *request.DatabaseCreate) error
|
||||
Update(req *request.DatabaseUpdate) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/utils/crypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type DatabaseType string
|
||||
|
||||
const (
|
||||
DatabaseTypeMysql DatabaseType = "mysql"
|
||||
DatabaseTypePostgresql DatabaseType = "postgresql"
|
||||
DatabaseTypeRedis DatabaseType = "redis"
|
||||
)
|
||||
|
||||
type DatabaseServer struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;unique" json:"name"`
|
||||
Type DatabaseType `gorm:"not null" json:"type"`
|
||||
Host string `gorm:"not null" json:"host"`
|
||||
Port uint `gorm:"not null" json:"port"`
|
||||
Username string `gorm:"not null" json:"username"`
|
||||
Password string `gorm:"not null" json:"password"`
|
||||
Remark string `gorm:"not null" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
Databases []*Database `gorm:"foreignKey:ServerID" json:"-"`
|
||||
}
|
||||
|
||||
func (r *DatabaseServer) BeforeSave(tx *gorm.DB) error {
|
||||
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Password, err = crypter.Encrypt([]byte(r.Password))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (r *DatabaseServer) AfterFind(tx *gorm.DB) error {
|
||||
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password, err := crypter.Decrypt(r.Password)
|
||||
if err == nil {
|
||||
r.Password = string(password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DatabaseServerRepo interface {
|
||||
Count() (int64, error)
|
||||
List(page, limit uint) ([]*DatabaseServer, int64, error)
|
||||
Get(id uint) (*DatabaseServer, error)
|
||||
Create(req *request.DatabaseServerCreate) error
|
||||
Update(req *request.DatabaseServerUpdate) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package biz
|
||||
|
||||
type FirewallRepo interface {
|
||||
}
|
||||
@@ -3,6 +3,10 @@ package biz
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/utils/crypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/ssh"
|
||||
)
|
||||
@@ -18,6 +22,43 @@ type SSH struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (r *SSH) BeforeSave(tx *gorm.DB) error {
|
||||
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Config.Key, err = crypter.Encrypt([]byte(r.Config.Key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Config.Password, err = crypter.Encrypt([]byte(r.Config.Password))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (r *SSH) AfterFind(tx *gorm.DB) error {
|
||||
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := crypter.Decrypt(r.Config.Key)
|
||||
if err == nil {
|
||||
r.Config.Key = string(key)
|
||||
}
|
||||
password, err := crypter.Decrypt(r.Config.Password)
|
||||
if err == nil {
|
||||
r.Config.Password = string(password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type SSHRepo interface {
|
||||
List(page, limit uint) ([]*SSH, int64, error)
|
||||
Get(id uint) (*SSH, error)
|
||||
|
||||
@@ -25,8 +25,13 @@ func initConf() {
|
||||
}
|
||||
|
||||
func initGlobal() {
|
||||
app.Key = app.Conf.MustString("app.key")
|
||||
if len(app.Key) != 32 {
|
||||
log.Fatalf("app key must be 32 characters")
|
||||
}
|
||||
|
||||
app.Root = app.Conf.MustString("app.root")
|
||||
app.Version = "2.3.16"
|
||||
app.Version = "2.3.19"
|
||||
app.Locale = app.Conf.MustString("app.locale")
|
||||
|
||||
// 初始化时区
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/apps"
|
||||
"github.com/TheTNB/panel/internal/http/middleware"
|
||||
"github.com/TheTNB/panel/internal/route"
|
||||
)
|
||||
@@ -25,7 +24,6 @@ func initHttp() {
|
||||
// add route
|
||||
route.Http(app.Http)
|
||||
route.Ws(app.Http)
|
||||
apps.Boot(app.Http)
|
||||
|
||||
srv := hlfhr.New(&http.Server{
|
||||
Addr: fmt.Sprintf(":%d", app.Conf.MustInt("http.port")),
|
||||
@@ -38,7 +36,7 @@ func initHttp() {
|
||||
|
||||
if app.Conf.Bool("http.tls") {
|
||||
srv.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
cert := filepath.Join(app.Root, "panel/storage/cert.pem")
|
||||
|
||||
@@ -37,4 +37,5 @@ func initLogger() {
|
||||
logger := zap.New(core)
|
||||
zap.ReplaceGlobals(logger)
|
||||
app.Logger = slog.New(zapslog.NewHandler(logger.Core()))
|
||||
slog.SetDefault(app.Logger)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ import (
|
||||
)
|
||||
|
||||
func initQueue() {
|
||||
app.Queue = queue.New(40)
|
||||
app.Queue = queue.New(100)
|
||||
go app.Queue.Run(context.Background())
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func initSession() {
|
||||
// initialize session manager
|
||||
manager, err := sessions.NewManager(&sessions.ManagerOptions{
|
||||
Key: app.Conf.String("app.key"),
|
||||
Key: app.Key,
|
||||
Lifetime: 120,
|
||||
GcInterval: 30,
|
||||
DisableDefaultDriver: true,
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/go-rat/utils/collect"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
@@ -15,7 +16,6 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/api"
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
)
|
||||
|
||||
type appRepo struct {
|
||||
@@ -65,7 +65,7 @@ func (r *appRepo) UpdateExist(slug string) bool {
|
||||
|
||||
for channel := range slices.Values(item.Channels) {
|
||||
if channel.Slug == installed.Channel {
|
||||
current := str.FirstElement(channel.Subs)
|
||||
current := collect.First(channel.Subs)
|
||||
if current != nil && current.Version != installed.Version {
|
||||
return true
|
||||
}
|
||||
@@ -109,7 +109,7 @@ func (r *appRepo) GetHomeShow() ([]map[string]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var filtered []map[string]string
|
||||
filtered := make([]map[string]string, 0)
|
||||
for item := range slices.Values(apps) {
|
||||
loaded, err := r.Get(item.Slug)
|
||||
if err != nil {
|
||||
@@ -168,7 +168,7 @@ func (r *appRepo) Install(channel, slug string) error {
|
||||
}
|
||||
shellUrl = ch.Install
|
||||
shellChannel = ch.Slug
|
||||
shellVersion = str.FirstElement(ch.Subs).Version
|
||||
shellVersion = collect.First(ch.Subs).Version
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -278,7 +278,7 @@ func (r *appRepo) Update(slug string) error {
|
||||
}
|
||||
shellUrl = ch.Update
|
||||
shellChannel = ch.Slug
|
||||
shellVersion = str.FirstElement(ch.Subs).Version
|
||||
shellVersion = collect.First(ch.Subs).Version
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
@@ -53,7 +53,7 @@ func (r *backupRepo) List(typ biz.BackupType) ([]*types.BackupFile, error) {
|
||||
list = append(list, &types.BackupFile{
|
||||
Name: file.Name(),
|
||||
Path: filepath.Join(path, file.Name()),
|
||||
Size: str.FormatBytes(float64(info.Size())),
|
||||
Size: tools.FormatBytes(float64(info.Size())),
|
||||
Time: info.ModTime(),
|
||||
})
|
||||
}
|
||||
@@ -288,7 +288,7 @@ func (r *backupRepo) createMySQL(to string, name string) error {
|
||||
|
||||
// createPostgres 创建 PostgreSQL 备份
|
||||
func (r *backupRepo) createPostgres(to string, name string) error {
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), 5432)
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", 5432, fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -436,7 +436,7 @@ func (r *backupRepo) restorePostgres(backup, target string) error {
|
||||
return errors.New("备份文件不存在")
|
||||
}
|
||||
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), 5432)
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", 5432, fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -478,9 +478,9 @@ func (r *backupRepo) preCheckPath(to, path string) error {
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
fmt.Printf("|-目标大小:%s\n", str.FormatBytes(float64(size)))
|
||||
fmt.Printf("|-目标大小:%s\n", tools.FormatBytes(float64(size)))
|
||||
fmt.Printf("|-目标文件数:%d\n", files)
|
||||
fmt.Printf("|-备份目录可用空间:%s\n", str.FormatBytes(float64(usage.Free)))
|
||||
fmt.Printf("|-备份目录可用空间:%s\n", tools.FormatBytes(float64(usage.Free)))
|
||||
fmt.Printf("|-备份目录可用Inode:%d\n", usage.InodesFree)
|
||||
}
|
||||
|
||||
@@ -504,8 +504,8 @@ func (r *backupRepo) preCheckDB(to string, size int64) error {
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
fmt.Printf("|-目标大小:%s\n", str.FormatBytes(float64(size)))
|
||||
fmt.Printf("|-备份目录可用空间:%s\n", str.FormatBytes(float64(usage.Free)))
|
||||
fmt.Printf("|-目标大小:%s\n", tools.FormatBytes(float64(size)))
|
||||
fmt.Printf("|-备份目录可用空间:%s\n", tools.FormatBytes(float64(usage.Free)))
|
||||
fmt.Printf("|-备份目录可用Inode:%d\n", usage.InodesFree)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
"github.com/TheTNB/panel/pkg/types/docker/image"
|
||||
)
|
||||
@@ -57,7 +57,7 @@ func (r *containerImageRepo) List() ([]types.ContainerImage, error) {
|
||||
Containers: item.Containers,
|
||||
RepoTags: item.RepoTags,
|
||||
RepoDigests: item.RepoDigests,
|
||||
Size: str.FormatBytes(float64(item.Size)),
|
||||
Size: tools.FormatBytes(float64(item.Size)),
|
||||
Labels: types.MapToKV(item.Labels),
|
||||
CreatedAt: time.Unix(item.Created, 0),
|
||||
})
|
||||
@@ -76,7 +76,7 @@ func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error {
|
||||
|
||||
if req.Auth {
|
||||
sb.WriteString(fmt.Sprintf("docker login -u %s -p %s", req.Username, req.Password))
|
||||
if _, err := shell.ExecfWithTimeout(1*time.Minute, sb.String()); err != nil { // nolint: govet
|
||||
if _, err := shell.Exec(sb.String()); err != nil {
|
||||
return fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
sb.Reset()
|
||||
@@ -84,7 +84,7 @@ func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error {
|
||||
|
||||
sb.WriteString(fmt.Sprintf("docker pull %s", req.Name))
|
||||
|
||||
if _, err := shell.Execf(sb.String()); err != nil { // nolint: govet
|
||||
if _, err := shell.Exec(sb.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ func (r *containerNetworkRepo) Create(req *request.ContainerNetworkCreate) (stri
|
||||
sb.WriteString(fmt.Sprintf(" --opt %s=%s", option.Key, option.Value))
|
||||
}
|
||||
|
||||
return shell.ExecfWithTimeout(120*time.Second, sb.String()) // nolint: govet
|
||||
return shell.Exec(sb.String())
|
||||
}
|
||||
|
||||
// Remove 删除网络
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
"github.com/TheTNB/panel/pkg/types/docker/volume"
|
||||
)
|
||||
@@ -61,7 +61,7 @@ func (r *containerVolumeRepo) List() ([]types.ContainerVolume, error) {
|
||||
Labels: types.MapToKV(item.Labels),
|
||||
Options: types.MapToKV(item.Options),
|
||||
RefCount: item.UsageData.RefCount,
|
||||
Size: str.FormatBytes(float64(item.UsageData.Size)),
|
||||
Size: tools.FormatBytes(float64(item.UsageData.Size)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ func (r *containerVolumeRepo) Create(req *request.ContainerVolumeCreate) (string
|
||||
sb.WriteString(fmt.Sprintf(" --opt %s=%s", option.Key, option.Value))
|
||||
}
|
||||
|
||||
return shell.ExecfWithTimeout(120*time.Second, sb.String()) // nolint: govet
|
||||
return shell.Exec(sb.String())
|
||||
}
|
||||
|
||||
// Remove 删除存储卷
|
||||
|
||||
@@ -7,13 +7,14 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/utils/str"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
@@ -99,7 +100,7 @@ panel-cli cutoff clear -t website -f %s -s %d -p %s
|
||||
if !io.Exists(shellLogDir) {
|
||||
return errors.New("计划任务日志目录不存在")
|
||||
}
|
||||
shellFile := strconv.Itoa(int(time.Now().Unix())) + str.RandomString(16)
|
||||
shellFile := strconv.Itoa(int(time.Now().Unix())) + str.Random(16)
|
||||
if err := io.Write(filepath.Join(shellDir, shellFile+".sh"), script, 0700); err != nil {
|
||||
return errors.New(err.Error())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type databaseRepo struct{}
|
||||
|
||||
func NewDatabaseRepo() biz.DatabaseRepo {
|
||||
return &databaseRepo{}
|
||||
}
|
||||
|
||||
func (d databaseRepo) Count() (int64, error) {
|
||||
var count int64
|
||||
if err := app.Orm.Model(&biz.Database{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (d databaseRepo) List(page, limit uint) ([]*biz.Database, int64, error) {
|
||||
var database []*biz.Database
|
||||
var total int64
|
||||
err := app.Orm.Model(&biz.Database{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&database).Error
|
||||
return database, total, err
|
||||
}
|
||||
|
||||
func (d databaseRepo) Get(id uint) (*biz.Database, error) {
|
||||
database := new(biz.Database)
|
||||
if err := app.Orm.Where("id = ?", id).First(database).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return database, nil
|
||||
}
|
||||
|
||||
func (d databaseRepo) Create(req *request.DatabaseCreate) error {
|
||||
database := &biz.Database{
|
||||
Name: req.Name,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
ServerID: req.ServerID,
|
||||
Status: biz.DatabaseStatusInvalid,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
return app.Orm.Create(database).Error
|
||||
}
|
||||
|
||||
func (d databaseRepo) Update(req *request.DatabaseUpdate) error {
|
||||
database := &biz.Database{
|
||||
Name: req.Name,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
return app.Orm.Model(database).Where("id = ?", req.ID).Omit("ServerID").Updates(database).Error
|
||||
}
|
||||
|
||||
func (d databaseRepo) Delete(id uint) error {
|
||||
return app.Orm.Delete(&biz.Database{}, id).Error
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
)
|
||||
|
||||
type databaseServerRepo struct{}
|
||||
|
||||
func NewDatabaseServerRepo() biz.DatabaseServerRepo {
|
||||
return &databaseServerRepo{}
|
||||
}
|
||||
|
||||
func (d databaseServerRepo) Count() (int64, error) {
|
||||
var count int64
|
||||
if err := app.Orm.Model(&biz.DatabaseServer{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (d databaseServerRepo) List(page, limit uint) ([]*biz.DatabaseServer, int64, error) {
|
||||
var databaseServer []*biz.DatabaseServer
|
||||
var total int64
|
||||
err := app.Orm.Model(&biz.DatabaseServer{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&databaseServer).Error
|
||||
return databaseServer, total, err
|
||||
}
|
||||
|
||||
func (d databaseServerRepo) Get(id uint) (*biz.DatabaseServer, error) {
|
||||
databaseServer := new(biz.DatabaseServer)
|
||||
if err := app.Orm.Where("id = ?", id).First(databaseServer).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return databaseServer, nil
|
||||
}
|
||||
|
||||
func (d databaseServerRepo) Create(req *request.DatabaseServerCreate) error {
|
||||
switch biz.DatabaseType(req.Type) {
|
||||
case biz.DatabaseTypeMysql:
|
||||
if _, err := db.NewMySQL(req.Username, req.Password, fmt.Sprintf("%s:%d", req.Host, req.Port)); err != nil {
|
||||
return err
|
||||
}
|
||||
case biz.DatabaseTypePostgresql:
|
||||
if _, err := db.NewPostgres(req.Username, req.Password, req.Host, req.Port, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
case biz.DatabaseTypeRedis:
|
||||
if _, err := db.NewRedis(req.Username, req.Password, fmt.Sprintf("%s:%d", req.Host, req.Port)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
databaseServer := &biz.DatabaseServer{
|
||||
Name: req.Name,
|
||||
Type: biz.DatabaseType(req.Type),
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
return app.Orm.Create(databaseServer).Error
|
||||
}
|
||||
|
||||
func (d databaseServerRepo) Update(req *request.DatabaseServerUpdate) error {
|
||||
switch biz.DatabaseType(req.Type) {
|
||||
case biz.DatabaseTypeMysql:
|
||||
if _, err := db.NewMySQL(req.Username, req.Password, fmt.Sprintf("%s:%d", req.Host, req.Port)); err != nil {
|
||||
return err
|
||||
}
|
||||
case biz.DatabaseTypePostgresql:
|
||||
if _, err := db.NewPostgres(req.Username, req.Password, req.Host, req.Port, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
case biz.DatabaseTypeRedis:
|
||||
if _, err := db.NewRedis(req.Username, req.Password, fmt.Sprintf("%s:%d", req.Host, req.Port)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return app.Orm.Model(&biz.DatabaseServer{}).Where("id = ?", req.ID).Select("*").Updates(&biz.DatabaseServer{
|
||||
Name: req.Name,
|
||||
Type: biz.DatabaseType(req.Type),
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Remark: req.Remark,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (d databaseServerRepo) Delete(id uint) error {
|
||||
ds := new(biz.DatabaseServer)
|
||||
if err := app.Orm.Where("id = ?", id).First(ds).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if slices.Contains([]string{"local_mysql", "local_postgresql", "local_redis"}, ds.Name) && !app.IsCli {
|
||||
return errors.New("can't delete " + ds.Name + ", if you must delete it, please uninstall " + strings.TrimPrefix(ds.Name, "local_"))
|
||||
}
|
||||
|
||||
return app.Orm.Delete(&biz.DatabaseServer{}, id).Error
|
||||
}
|
||||
@@ -398,7 +398,7 @@ func (r *settingRepo) FixPanel() error {
|
||||
return fmt.Errorf("清理临时文件失败:%w", err)
|
||||
}
|
||||
if app.IsCli {
|
||||
fmt.Println("已清理临时文件,请运行 panel-cli update 更新面板")
|
||||
fmt.Println("|-已清理临时文件,请运行 panel-cli update 更新面板")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+17
-2
@@ -1,6 +1,7 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
@@ -47,9 +48,18 @@ func (r *taskRepo) UpdateStatus(id uint, status biz.TaskStatus) error {
|
||||
}
|
||||
|
||||
func (r *taskRepo) Push(task *biz.Task) error {
|
||||
var count int64
|
||||
if err := app.Orm.Model(&biz.Task{}).Where("shell = ? and (status = ? or status = ?)", task.Shell, biz.TaskStatusWaiting, biz.TaskStatusRunning).Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return fmt.Errorf("duplicate submission, please wait for the previous task to end")
|
||||
}
|
||||
|
||||
if err := app.Orm.Create(task).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Queue.Push(queuejob.NewProcessTask(r), []any{
|
||||
task.ID,
|
||||
})
|
||||
@@ -61,9 +71,14 @@ func (r *taskRepo) DispatchWaiting() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := app.Orm.Model(&biz.Task{}).Where("status = ?", biz.TaskStatusRunning).Update("status", biz.TaskStatusFailed).Error; err != nil {
|
||||
app.Logger.Warn("failed to mark running tasks as failed", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
|
||||
var tasks []biz.Task
|
||||
if err := app.Orm.Where("status = ?", biz.TaskStatusWaiting).Find(&tasks).Error; err != nil {
|
||||
app.Logger.Warn("获取待处理任务失败", slog.Any("err", err))
|
||||
app.Logger.Warn("failed to get pending tasks", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -71,7 +86,7 @@ func (r *taskRepo) DispatchWaiting() {
|
||||
if err := app.Queue.Push(queuejob.NewProcessTask(r), []any{
|
||||
task.ID,
|
||||
}); err != nil {
|
||||
app.Logger.Warn("推送任务失败", slog.Any("err", err))
|
||||
app.Logger.Warn("failed to push task", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,9 @@ func GlobalMiddleware() []func(http.Handler) http.Handler {
|
||||
LogRequestHeaders: []string{"User-Agent"},
|
||||
}),
|
||||
middleware.Recoverer,
|
||||
Entrance,
|
||||
Status,
|
||||
Entrance,
|
||||
MustLogin,
|
||||
MustInstall,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package middleware
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/spf13/cast"
|
||||
@@ -12,6 +14,14 @@ import (
|
||||
|
||||
// MustLogin 确保已登录
|
||||
func MustLogin(next http.Handler) http.Handler {
|
||||
// 白名单
|
||||
whiteList := []string{
|
||||
"/api/user/login",
|
||||
"/api/user/logout",
|
||||
"/api/user/isLogin",
|
||||
"/api/dashboard/panel",
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sess, err := app.Session.GetSession(r)
|
||||
if err != nil {
|
||||
@@ -22,6 +32,12 @@ func MustLogin(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// 对白名单和非 API 请求放行
|
||||
if slices.Contains(whiteList, r.URL.Path) || !strings.HasPrefix(r.URL.Path, "/api") {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if sess.Missing("user_id") {
|
||||
render := chix.NewRender(w)
|
||||
render.Status(http.StatusUnauthorized)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package request
|
||||
|
||||
type DatabaseCreate struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" validate:"required,exists=database_servers id"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
Remark string `form:"remark" json:"remark"`
|
||||
}
|
||||
|
||||
type DatabaseUpdate struct {
|
||||
ID string `form:"id" json:"id" validate:"required,exists=databases id"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
Remark string `form:"remark" json:"remark"`
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package request
|
||||
|
||||
type DatabaseServerCreate struct {
|
||||
Name string `form:"name" json:"name" validate:"required,not_exists=database_servers name"`
|
||||
Type string `form:"type" json:"type" validate:"required,oneof=mysql postgresql redis"`
|
||||
Host string `form:"host" json:"host" validate:"required"`
|
||||
Port uint `form:"port" json:"port" validate:"required,number,gte=1,lte=65535"`
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
Remark string `form:"remark" json:"remark"`
|
||||
}
|
||||
|
||||
type DatabaseServerUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=database_servers id"`
|
||||
Name string `form:"name" json:"name" validate:"required,not_exists=database_servers name"`
|
||||
Type string `form:"type" json:"type" validate:"required,oneof=mysql postgresql redis"`
|
||||
Host string `form:"host" json:"host" validate:"required"`
|
||||
Port uint `form:"port" json:"port" validate:"required,number,gte=1,lte=65535"`
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
Remark string `form:"remark" json:"remark"`
|
||||
}
|
||||
@@ -25,13 +25,7 @@ type FileSave struct {
|
||||
Content string `form:"content" json:"content"`
|
||||
}
|
||||
|
||||
type FileMove struct {
|
||||
Source string `form:"source" json:"source" validate:"required"`
|
||||
Target string `form:"target" json:"target" validate:"required"`
|
||||
Force bool `form:"force" json:"force"`
|
||||
}
|
||||
|
||||
type FileCopy struct {
|
||||
type FileControl struct {
|
||||
Source string `form:"source" json:"source" validate:"required"`
|
||||
Target string `form:"target" json:"target" validate:"required"`
|
||||
Force bool `form:"force" json:"force"`
|
||||
|
||||
@@ -17,7 +17,6 @@ func init() {
|
||||
&biz.CertDNS{},
|
||||
&biz.CertAccount{},
|
||||
&biz.Cron{},
|
||||
&biz.Database{},
|
||||
&biz.Monitor{},
|
||||
&biz.App{},
|
||||
&biz.Setting{},
|
||||
@@ -32,7 +31,6 @@ func init() {
|
||||
&biz.CertDNS{},
|
||||
&biz.CertAccount{},
|
||||
&biz.Cron{},
|
||||
&biz.Database{},
|
||||
&biz.Monitor{},
|
||||
&biz.App{},
|
||||
&biz.Setting{},
|
||||
@@ -55,4 +53,20 @@ func init() {
|
||||
)
|
||||
},
|
||||
})
|
||||
Migrations = append(Migrations, &gormigrate.Migration{
|
||||
ID: "20241107-database",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
_ = tx.Migrator().DropTable(&biz.Database{})
|
||||
return tx.AutoMigrate(
|
||||
&biz.DatabaseServer{},
|
||||
&biz.Database{},
|
||||
)
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Migrator().DropTable(
|
||||
&biz.DatabaseServer{},
|
||||
&biz.Database{},
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package queuejob
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log/slog"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
)
|
||||
@@ -36,7 +38,7 @@ func (r *ProcessTask) Handle(args ...any) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(task.Shell); err != nil { // nolint: govet
|
||||
if _, err = shell.Exec(task.Shell); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,5 +50,6 @@ func (r *ProcessTask) Handle(args ...any) error {
|
||||
}
|
||||
|
||||
func (r *ProcessTask) ErrHandle(err error) {
|
||||
app.Logger.Warn("background task failed", slog.Any("task_id", r.taskID), slog.Any("err", err))
|
||||
_ = r.taskRepo.UpdateStatus(r.taskID, biz.TaskStatusFailed)
|
||||
}
|
||||
|
||||
+32
-24
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/internal/apps"
|
||||
"github.com/TheTNB/panel/internal/embed"
|
||||
"github.com/TheTNB/panel/internal/http/middleware"
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
@@ -20,25 +21,24 @@ func Http(r chi.Router) {
|
||||
r.With(middleware.Throttle(5, time.Minute)).Post("/login", user.Login)
|
||||
r.Post("/logout", user.Logout)
|
||||
r.Get("/isLogin", user.IsLogin)
|
||||
r.With(middleware.MustLogin).Get("/info", user.Info)
|
||||
r.Get("/info", user.Info)
|
||||
})
|
||||
|
||||
r.Route("/dashboard", func(r chi.Router) {
|
||||
dashboard := service.NewDashboardService()
|
||||
r.Get("/panel", dashboard.Panel)
|
||||
r.With(middleware.MustLogin).Get("/homeApps", dashboard.HomeApps)
|
||||
r.With(middleware.MustLogin).Post("/current", dashboard.Current)
|
||||
r.With(middleware.MustLogin).Get("/systemInfo", dashboard.SystemInfo)
|
||||
r.With(middleware.MustLogin).Get("/countInfo", dashboard.CountInfo)
|
||||
r.With(middleware.MustLogin).Get("/installedDbAndPhp", dashboard.InstalledDbAndPhp)
|
||||
r.With(middleware.MustLogin).Get("/checkUpdate", dashboard.CheckUpdate)
|
||||
r.With(middleware.MustLogin).Get("/updateInfo", dashboard.UpdateInfo)
|
||||
r.With(middleware.MustLogin).Post("/update", dashboard.Update)
|
||||
r.With(middleware.MustLogin).Post("/restart", dashboard.Restart)
|
||||
r.Get("/homeApps", dashboard.HomeApps)
|
||||
r.Post("/current", dashboard.Current)
|
||||
r.Get("/systemInfo", dashboard.SystemInfo)
|
||||
r.Get("/countInfo", dashboard.CountInfo)
|
||||
r.Get("/installedDbAndPhp", dashboard.InstalledDbAndPhp)
|
||||
r.Get("/checkUpdate", dashboard.CheckUpdate)
|
||||
r.Get("/updateInfo", dashboard.UpdateInfo)
|
||||
r.Post("/update", dashboard.Update)
|
||||
r.Post("/restart", dashboard.Restart)
|
||||
})
|
||||
|
||||
r.Route("/task", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
task := service.NewTaskService()
|
||||
r.Get("/status", task.Status)
|
||||
r.Get("/", task.List)
|
||||
@@ -47,7 +47,6 @@ func Http(r chi.Router) {
|
||||
})
|
||||
|
||||
r.Route("/website", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
website := service.NewWebsiteService()
|
||||
r.Get("/defaultConfig", website.GetDefaultConfig)
|
||||
r.Post("/defaultConfig", website.UpdateDefaultConfig)
|
||||
@@ -63,8 +62,23 @@ func Http(r chi.Router) {
|
||||
r.Post("/{id}/obtainCert", website.ObtainCert)
|
||||
})
|
||||
|
||||
r.Route("/database", func(r chi.Router) {
|
||||
database := service.NewDatabaseService()
|
||||
r.Get("/", database.List)
|
||||
r.Post("/", database.Create)
|
||||
r.Put("/{id}", database.Update)
|
||||
r.Delete("/{id}", database.Delete)
|
||||
})
|
||||
|
||||
r.Route("/databaseServer", func(r chi.Router) {
|
||||
database := service.NewDatabaseService()
|
||||
r.Get("/", database.List)
|
||||
r.Post("/", database.Create)
|
||||
r.Put("/{id}", database.Update)
|
||||
r.Delete("/{id}", database.Delete)
|
||||
})
|
||||
|
||||
r.Route("/backup", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
backup := service.NewBackupService()
|
||||
r.Get("/{type}", backup.List)
|
||||
r.Post("/{type}", backup.Create)
|
||||
@@ -74,7 +88,6 @@ func Http(r chi.Router) {
|
||||
})
|
||||
|
||||
r.Route("/cert", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
cert := service.NewCertService()
|
||||
r.Get("/caProviders", cert.CAProviders)
|
||||
r.Get("/dnsProviders", cert.DNSProviders)
|
||||
@@ -112,7 +125,6 @@ func Http(r chi.Router) {
|
||||
})
|
||||
|
||||
r.Route("/app", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
app := service.NewAppService()
|
||||
r.Get("/list", app.List)
|
||||
r.Post("/install", app.Install)
|
||||
@@ -124,7 +136,6 @@ func Http(r chi.Router) {
|
||||
})
|
||||
|
||||
r.Route("/cron", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
cron := service.NewCronService()
|
||||
r.Get("/", cron.List)
|
||||
r.Post("/", cron.Create)
|
||||
@@ -135,7 +146,6 @@ func Http(r chi.Router) {
|
||||
})
|
||||
|
||||
r.Route("/safe", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
safe := service.NewSafeService()
|
||||
r.Get("/ssh", safe.GetSSH)
|
||||
r.Post("/ssh", safe.UpdateSSH)
|
||||
@@ -144,7 +154,6 @@ func Http(r chi.Router) {
|
||||
})
|
||||
|
||||
r.Route("/firewall", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
firewall := service.NewFirewallService()
|
||||
r.Get("/status", firewall.GetStatus)
|
||||
r.Post("/status", firewall.UpdateStatus)
|
||||
@@ -160,7 +169,6 @@ func Http(r chi.Router) {
|
||||
})
|
||||
|
||||
r.Route("/ssh", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
ssh := service.NewSSHService()
|
||||
r.Get("/", ssh.List)
|
||||
r.Post("/", ssh.Create)
|
||||
@@ -170,7 +178,6 @@ func Http(r chi.Router) {
|
||||
})
|
||||
|
||||
r.Route("/container", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
r.Route("/container", func(r chi.Router) {
|
||||
container := service.NewContainerService()
|
||||
r.Get("/", container.List)
|
||||
@@ -211,13 +218,13 @@ func Http(r chi.Router) {
|
||||
})
|
||||
|
||||
r.Route("/file", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
file := service.NewFileService()
|
||||
r.Post("/create", file.Create)
|
||||
r.Get("/content", file.Content)
|
||||
r.Post("/save", file.Save)
|
||||
r.Post("/delete", file.Delete)
|
||||
r.Post("/upload", file.Upload)
|
||||
r.Post("/exist", file.Exist)
|
||||
r.Post("/move", file.Move)
|
||||
r.Post("/copy", file.Copy)
|
||||
r.Get("/download", file.Download)
|
||||
@@ -231,7 +238,6 @@ func Http(r chi.Router) {
|
||||
})
|
||||
|
||||
r.Route("/monitor", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
monitor := service.NewMonitorService()
|
||||
r.Get("/setting", monitor.GetSetting)
|
||||
r.Post("/setting", monitor.UpdateSetting)
|
||||
@@ -240,14 +246,12 @@ func Http(r chi.Router) {
|
||||
})
|
||||
|
||||
r.Route("/setting", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
setting := service.NewSettingService()
|
||||
r.Get("/", setting.Get)
|
||||
r.Post("/", setting.Update)
|
||||
})
|
||||
|
||||
r.Route("/systemctl", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
systemctl := service.NewSystemctlService()
|
||||
r.Get("/status", systemctl.Status)
|
||||
r.Get("/isEnabled", systemctl.IsEnabled)
|
||||
@@ -258,6 +262,10 @@ func Http(r chi.Router) {
|
||||
r.Post("/start", systemctl.Start)
|
||||
r.Post("/stop", systemctl.Stop)
|
||||
})
|
||||
|
||||
r.Route("/apps", func(r chi.Router) {
|
||||
apps.Boot(r)
|
||||
})
|
||||
})
|
||||
|
||||
r.NotFound(func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
+24
-9
@@ -7,7 +7,9 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/utils/collect"
|
||||
"github.com/go-rat/utils/hash"
|
||||
"github.com/go-rat/utils/str"
|
||||
"github.com/spf13/cast"
|
||||
"github.com/urfave/cli/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -19,9 +21,9 @@ import (
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/api"
|
||||
"github.com/TheTNB/panel/pkg/cert"
|
||||
"github.com/TheTNB/panel/pkg/firewall"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/ntp"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
@@ -84,7 +86,7 @@ func (s *CliService) Update(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("获取最新版本失败:%v", err)
|
||||
}
|
||||
|
||||
download := str.FirstElement(panel.Downloads)
|
||||
download := collect.First(panel.Downloads)
|
||||
if download == nil {
|
||||
return fmt.Errorf("下载地址为空")
|
||||
}
|
||||
@@ -103,15 +105,15 @@ func (s *CliService) Info(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("获取管理员信息失败:%v", err)
|
||||
}
|
||||
|
||||
password := str.RandomString(16)
|
||||
password := str.Random(16)
|
||||
hashed, err := s.hash.Make(password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("密码生成失败:%v", err)
|
||||
}
|
||||
user.Username = str.RandomString(8)
|
||||
user.Username = str.Random(8)
|
||||
user.Password = hashed
|
||||
if user.Email == "" {
|
||||
user.Email = str.RandomString(8) + "@example.com"
|
||||
user.Email = str.Random(8) + "@example.com"
|
||||
}
|
||||
|
||||
if err = app.Orm.Save(user).Error; err != nil {
|
||||
@@ -326,7 +328,7 @@ func (s *CliService) EntranceOn(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
config.HTTP.Entrance = "/" + str.RandomString(6)
|
||||
config.HTTP.Entrance = "/" + str.Random(6)
|
||||
|
||||
encoded, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
@@ -389,6 +391,19 @@ func (s *CliService) Port(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 放行端口
|
||||
fw := firewall.NewFirewall()
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: uint(config.HTTP.Port),
|
||||
PortEnd: uint(config.HTTP.Port),
|
||||
Direction: firewall.DirectionIn,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Write("/usr/local/etc/panel/config.yml", string(encoded), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -755,7 +770,7 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
return fmt.Errorf("初始化失败:%v", err)
|
||||
}
|
||||
|
||||
value, err := hash.NewArgon2id().Make(str.RandomString(32))
|
||||
value, err := hash.NewArgon2id().Make(str.Random(32))
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化失败:%v", err)
|
||||
}
|
||||
@@ -779,8 +794,8 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
config.App.Key = str.RandomString(32)
|
||||
config.HTTP.Entrance = "/" + str.RandomString(6)
|
||||
config.App.Key = str.Random(32)
|
||||
config.HTTP.Entrance = "/" + str.Random(6)
|
||||
|
||||
encoded, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/go-rat/utils/collect"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/shirou/gopsutil/disk"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
@@ -20,7 +21,6 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/api"
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
@@ -140,7 +140,7 @@ func (s *DashboardService) CountInfo(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if postgresqlInstalled {
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), 5432)
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", 5432, fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root))
|
||||
if err == nil {
|
||||
defer postgres.Close()
|
||||
databases, err := postgres.Databases()
|
||||
@@ -295,7 +295,7 @@ func (s *DashboardService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
download := str.FirstElement(panel.Downloads)
|
||||
download := collect.First(panel.Downloads)
|
||||
if download == nil {
|
||||
Error(w, http.StatusInternalServerError, "获取下载链接失败")
|
||||
return
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
databaseRepo biz.DatabaseRepo
|
||||
}
|
||||
|
||||
func NewDatabaseService() *Database {
|
||||
return &Database{
|
||||
databaseRepo: data.NewDatabaseRepo(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Database) List(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.Paginate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
certs, total, err := s.databaseRepo.List(req.Page, req.Limit)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": certs,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Database) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.DatabaseCreate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.databaseRepo.Create(req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *Database) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.DatabaseUpdate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.databaseRepo.Update(req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *Database) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.databaseRepo.Delete(req.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type DatabaseServer struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewDatabaseServerService() *DatabaseServer {
|
||||
return &DatabaseServer{
|
||||
databaseServerRepo: data.NewDatabaseServerRepo(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DatabaseServer) List(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.Paginate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
certs, total, err := s.databaseServerRepo.List(req.Page, req.Limit)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": certs,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DatabaseServer) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.DatabaseServerCreate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.databaseServerRepo.Create(req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *DatabaseServer) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.DatabaseServerUpdate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.databaseServerRepo.Update(req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *DatabaseServer) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.databaseServerRepo.Delete(req.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
+67
-33
@@ -23,7 +23,7 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
)
|
||||
|
||||
type FileService struct {
|
||||
@@ -107,7 +107,6 @@ func (s *FileService) Save(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
s.setPermission(req.Path, 0755, "www", "www")
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -167,51 +166,77 @@ func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *FileService) Exist(w http.ResponseWriter, r *http.Request) {
|
||||
binder := chix.NewBind(r)
|
||||
defer binder.Release()
|
||||
|
||||
var paths []string
|
||||
if err := binder.Body(&paths); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var results []bool
|
||||
for item := range slices.Values(paths) {
|
||||
results = append(results, io.Exists(item))
|
||||
}
|
||||
|
||||
Success(w, results)
|
||||
}
|
||||
|
||||
func (s *FileService) Move(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FileMove](r)
|
||||
if err != nil {
|
||||
binder := chix.NewBind(r)
|
||||
defer binder.Release()
|
||||
|
||||
var req []request.FileControl
|
||||
if err := binder.Body(&req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if io.Exists(req.Target) && !req.Force {
|
||||
Error(w, http.StatusForbidden, "target path already exists") // no translate, frontend will use it to determine whether to continue
|
||||
return
|
||||
}
|
||||
for item := range slices.Values(req) {
|
||||
if io.Exists(item.Target) && !item.Force {
|
||||
continue
|
||||
}
|
||||
|
||||
if io.IsDir(req.Source) && strings.HasPrefix(req.Target, req.Source) {
|
||||
Error(w, http.StatusForbidden, "you can't do this, it will be broken")
|
||||
return
|
||||
}
|
||||
if io.IsDir(item.Source) && strings.HasPrefix(item.Target, item.Source) {
|
||||
Error(w, http.StatusForbidden, "you can't do this, it will be broken")
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Mv(req.Source, req.Target); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
if err := io.Mv(item.Source, item.Target); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *FileService) Copy(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FileCopy](r)
|
||||
if err != nil {
|
||||
binder := chix.NewBind(r)
|
||||
defer binder.Release()
|
||||
|
||||
var req []request.FileControl
|
||||
if err := binder.Body(&req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if io.Exists(req.Target) && !req.Force {
|
||||
Error(w, http.StatusForbidden, "target path already exists") // no translate, frontend will use it to determine whether to continue
|
||||
return
|
||||
}
|
||||
for item := range slices.Values(req) {
|
||||
if io.Exists(item.Target) && !item.Force {
|
||||
continue
|
||||
}
|
||||
|
||||
if io.IsDir(req.Source) && strings.HasPrefix(req.Target, req.Source) {
|
||||
Error(w, http.StatusForbidden, "you can't do this, it will be broken")
|
||||
return
|
||||
}
|
||||
if io.IsDir(item.Source) && strings.HasPrefix(item.Target, item.Source) {
|
||||
Error(w, http.StatusForbidden, "you can't do this, it will be broken")
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Cp(req.Source, req.Target); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
if err := io.Cp(item.Source, item.Target); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
@@ -276,7 +301,7 @@ func (s *FileService) Info(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Success(w, chix.M{
|
||||
"name": info.Name(),
|
||||
"size": str.FormatBytes(float64(info.Size())),
|
||||
"size": tools.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
"dir": info.IsDir(),
|
||||
@@ -338,7 +363,16 @@ func (s *FileService) UnCompress(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
s.setPermission(req.Path, 0755, "www", "www")
|
||||
list, err := io.ListCompress(req.File)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for item := range slices.Values(list) {
|
||||
s.setPermission(filepath.Join(req.Path, item), 0755, "www", "www")
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -407,7 +441,7 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
|
||||
// formatDir 格式化目录信息
|
||||
func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any {
|
||||
var paths []any
|
||||
for _, file := range entries {
|
||||
for file := range slices.Values(entries) {
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -417,7 +451,7 @@ func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any {
|
||||
paths = append(paths, map[string]any{
|
||||
"name": info.Name(),
|
||||
"full": filepath.Join(base, info.Name()),
|
||||
"size": str.FormatBytes(float64(info.Size())),
|
||||
"size": tools.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
"owner": os.GetUser(stat.Uid),
|
||||
@@ -443,7 +477,7 @@ func (s *FileService) formatInfo(infos map[string]stdos.FileInfo) []map[string]a
|
||||
paths = append(paths, map[string]any{
|
||||
"name": info.Name(),
|
||||
"full": path,
|
||||
"size": str.FormatBytes(float64(info.Size())),
|
||||
"size": tools.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
"owner": os.GetUser(stat.Uid),
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
)
|
||||
|
||||
type FileService struct {
|
||||
@@ -162,8 +162,26 @@ func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *FileService) Exist(w http.ResponseWriter, r *http.Request) {
|
||||
binder := chix.NewBind(r)
|
||||
defer binder.Release()
|
||||
|
||||
var paths []string
|
||||
if err := binder.Body(&paths); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var results []bool
|
||||
for item := range slices.Values(paths) {
|
||||
results = append(results, io.Exists(item))
|
||||
}
|
||||
|
||||
Success(w, results)
|
||||
}
|
||||
|
||||
func (s *FileService) Move(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FileMove](r)
|
||||
req, err := Bind[request.FileControl](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -183,7 +201,7 @@ func (s *FileService) Move(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *FileService) Copy(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FileCopy](r)
|
||||
req, err := Bind[request.FileControl](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -242,7 +260,7 @@ func (s *FileService) Info(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Success(w, chix.M{
|
||||
"name": info.Name(),
|
||||
"size": str.FormatBytes(float64(info.Size())),
|
||||
"size": tools.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
"dir": info.IsDir(),
|
||||
@@ -379,7 +397,7 @@ func (s *FileService) formatDir(base string, entries []stdos.DirEntry) []any {
|
||||
paths = append(paths, map[string]any{
|
||||
"name": info.Name(),
|
||||
"full": filepath.Join(base, info.Name()),
|
||||
"size": str.FormatBytes(float64(info.Size())),
|
||||
"size": tools.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
"owner": "",
|
||||
@@ -404,7 +422,7 @@ func (s *FileService) formatInfo(infos map[string]stdos.FileInfo) []map[string]a
|
||||
paths = append(paths, map[string]any{
|
||||
"name": info.Name(),
|
||||
"full": path,
|
||||
"size": str.FormatBytes(float64(info.Size())),
|
||||
"size": tools.FormatBytes(float64(info.Size())),
|
||||
"mode_str": info.Mode().String(),
|
||||
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
|
||||
"owner": "",
|
||||
|
||||
+1
-2
@@ -5,10 +5,9 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/utils/copier"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/copier"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
|
||||
@@ -47,7 +47,7 @@ func All() []*types.App {
|
||||
func Boot(r chi.Router) {
|
||||
apps.Range(func(_, app any) bool {
|
||||
if p, ok := app.(*types.App); ok {
|
||||
r.Route(fmt.Sprintf("/api/apps/%s", p.Slug), p.Route)
|
||||
r.Route(fmt.Sprintf("/%s", p.Slug), p.Route)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// Package chattr https://github.com/g0rbe/go-chattr/pull/3
|
||||
package chattr
|
||||
|
||||
/*
|
||||
A package for change attribute of a file on Linux, similar to the chattr command.
|
||||
|
||||
Example to set the immutable attribute to a file:
|
||||
|
||||
file, err := os.OpenFile("file.txt", os.O_RDONLY, 0666)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
err = chattr.SetAttr(file, chattr.FS_IMMUTABLE_FL)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
*/
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// from /usr/include/linux/fs.h
|
||||
const (
|
||||
FS_SECRM_FL uint32 = 0x00000001 /* Secure deletion */
|
||||
FS_UNRM_FL uint32 = 0x00000002 /* Undelete */
|
||||
FS_COMPR_FL uint32 = 0x00000004 /* Compress file */
|
||||
FS_SYNC_FL uint32 = 0x00000008 /* Synchronous updates */
|
||||
FS_IMMUTABLE_FL uint32 = 0x00000010 /* Immutable file */
|
||||
FS_APPEND_FL uint32 = 0x00000020 /* writes to file may only append */
|
||||
FS_NODUMP_FL uint32 = 0x00000040 /* do not dump file */
|
||||
FS_NOATIME_FL uint32 = 0x00000080 /* do not update atime */
|
||||
FS_DIRTY_FL uint32 = 0x00000100
|
||||
FS_COMPRBLK_FL uint32 = 0x00000200 /* One or more compressed clusters */
|
||||
FS_NOCOMP_FL uint32 = 0x00000400 /* Don't compress */
|
||||
FS_ENCRYPT_FL uint32 = 0x00000800 /* Encrypted file */
|
||||
FS_BTREE_FL uint32 = 0x00001000 /* btree format dir */
|
||||
FS_INDEX_FL uint32 = 0x00001000 /* hash-indexed directory */
|
||||
FS_IMAGIC_FL uint32 = 0x00002000 /* AFS directory */
|
||||
FS_JOURNAL_DATA_FL uint32 = 0x00004000 /* Reserved for ext3 */
|
||||
FS_NOTAIL_FL uint32 = 0x00008000 /* file tail should not be merged */
|
||||
FS_DIRSYNC_FL uint32 = 0x00010000 /* dirsync behaviour (directories only) */
|
||||
FS_TOPDIR_FL uint32 = 0x00020000 /* Top of directory hierarchies*/
|
||||
FS_HUGE_FILE_FL uint32 = 0x00040000 /* Reserved for ext4 */
|
||||
FS_EXTENT_FL uint32 = 0x00080000 /* Extents */
|
||||
FS_EA_INODE_FL uint32 = 0x00200000 /* Inode used for large EA */
|
||||
FS_EOFBLOCKS_FL uint32 = 0x00400000 /* Reserved for ext4 */
|
||||
FS_NOCOW_FL uint32 = 0x00800000 /* Do not cow file */
|
||||
FS_INLINE_DATA_FL uint32 = 0x10000000 /* Reserved for ext4 */
|
||||
FS_PROJINHERIT_FL uint32 = 0x20000000 /* Create with parents projid */
|
||||
FS_RESERVED_FL uint32 = 0x80000000 /* reserved for ext2 lib */
|
||||
)
|
||||
|
||||
// from ioctl_list manpage
|
||||
const (
|
||||
FS_IOC_GETFLAGS uintptr = 0x80086601
|
||||
FS_IOC_SETFLAGS uintptr = 0x40086602
|
||||
)
|
||||
|
||||
func ioctl(f *os.File, request uintptr, attrp *uint32) error {
|
||||
argp := uintptr(unsafe.Pointer(attrp))
|
||||
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), request, argp)
|
||||
if errno != 0 {
|
||||
return os.NewSyscallError("ioctl", errno)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAttrs retrieves the attributes of a file.
|
||||
func GetAttrs(f *os.File) (uint32, error) {
|
||||
attr := uint32(1)
|
||||
err := ioctl(f, FS_IOC_GETFLAGS, &attr)
|
||||
|
||||
return attr, err
|
||||
}
|
||||
|
||||
// SetAttr sets the given attribute.
|
||||
func SetAttr(f *os.File, attr uint32) error {
|
||||
attrs, err := GetAttrs(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs |= attr
|
||||
|
||||
return ioctl(f, FS_IOC_SETFLAGS, &attrs)
|
||||
}
|
||||
|
||||
// UnsetAttr unsets the given attribute.
|
||||
func UnsetAttr(f *os.File, attr uint32) error {
|
||||
attrs, err := GetAttrs(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs ^= attrs & attr
|
||||
|
||||
return ioctl(f, FS_IOC_SETFLAGS, &attrs)
|
||||
}
|
||||
|
||||
// IsAttr checks whether the given attribute is set.
|
||||
func IsAttr(f *os.File, attr uint32) (bool, error) {
|
||||
attrs, err := GetAttrs(f)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if (attrs & attr) != 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package copier
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func Copy[T any](from any) (*T, error) {
|
||||
to := new(T)
|
||||
b, err := json.Marshal(from)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("copier: marshal data err: %w", err)
|
||||
}
|
||||
if err = json.Unmarshal(b, to); err != nil {
|
||||
return nil, fmt.Errorf("copier: unmarshal data err: %w", err)
|
||||
}
|
||||
return to, nil
|
||||
}
|
||||
+1
-1
@@ -21,7 +21,7 @@ type Postgres struct {
|
||||
port uint
|
||||
}
|
||||
|
||||
func NewPostgres(username, password, address, hbaFile string, port uint) (*Postgres, error) {
|
||||
func NewPostgres(username, password, address string, port uint, hbaFile string) (*Postgres, error) {
|
||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=postgres sslmode=disable", address, port, username, password)
|
||||
if password == "" {
|
||||
dsn = fmt.Sprintf("host=%s port=%d user=%s dbname=postgres sslmode=disable", address, port, username)
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
type RedisKV struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
Size int64 `json:"size"`
|
||||
Length int64 `json:"length"`
|
||||
TTL int64 `json:"ttl"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Redis struct {
|
||||
conn redis.Conn
|
||||
username string
|
||||
password string
|
||||
address string
|
||||
}
|
||||
|
||||
func NewRedis(username, password, address string) (*Redis, error) {
|
||||
conn, err := redis.Dial("tcp", address, redis.DialUsername(username), redis.DialPassword(password))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Redis{
|
||||
conn: conn,
|
||||
username: username,
|
||||
password: password,
|
||||
address: address,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Redis) Close() error {
|
||||
return r.conn.Close()
|
||||
}
|
||||
|
||||
func (r *Redis) Exec(command string, args ...any) (any, error) {
|
||||
return r.conn.Do(command, args...)
|
||||
}
|
||||
|
||||
func (r *Redis) Database() (int, error) {
|
||||
return redis.Int(r.conn.Do("CONFIG", "GET", "databases"))
|
||||
}
|
||||
|
||||
func (r *Redis) Select(db int) error {
|
||||
_, err := r.conn.Do("SELECT", db)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Redis) Size() (int, error) {
|
||||
return redis.Int(r.conn.Do("DBSIZE"))
|
||||
}
|
||||
|
||||
func (r *Redis) Data(page, pageSize int) ([]RedisKV, error) {
|
||||
result := make([]RedisKV, 0)
|
||||
cursor := 0
|
||||
var keys []string
|
||||
for {
|
||||
values, err := redis.Values(r.conn.Do("SCAN", cursor, "COUNT", 100))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to SCAN: %v", err)
|
||||
}
|
||||
var batch []string
|
||||
_, err = redis.Scan(values, &cursor, &batch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse SCAN result: %v", err)
|
||||
}
|
||||
keys = append(keys, batch...)
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
start := (page - 1) * pageSize
|
||||
end := start + pageSize
|
||||
if start >= len(keys) {
|
||||
return []RedisKV{}, nil
|
||||
}
|
||||
if end > len(keys) {
|
||||
end = len(keys)
|
||||
}
|
||||
|
||||
paged := keys[start:end]
|
||||
for _, key := range paged {
|
||||
kv := RedisKV{
|
||||
Key: key,
|
||||
}
|
||||
|
||||
keyType, err := redis.String(r.conn.Do("TYPE", key))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
kv.Type = keyType
|
||||
|
||||
ttl, err := redis.Int64(r.conn.Do("TTL", key))
|
||||
if err == nil {
|
||||
kv.TTL = ttl
|
||||
}
|
||||
idleTime, err := redis.Int64(r.conn.Do("OBJECT", "IDLETIME", key))
|
||||
if err == nil {
|
||||
kv.UpdatedAt = time.Now().Add(-time.Duration(idleTime) * time.Second)
|
||||
} else {
|
||||
kv.UpdatedAt = time.Now()
|
||||
}
|
||||
memory, err := redis.Int64(r.conn.Do("MEMORY", "USAGE", key))
|
||||
if err == nil {
|
||||
kv.Size = memory
|
||||
}
|
||||
|
||||
var value any
|
||||
switch keyType {
|
||||
case "string":
|
||||
if value, err = redis.String(r.conn.Do("GET", key)); err == nil {
|
||||
kv.Length = int64(len(value.(string)))
|
||||
}
|
||||
case "list":
|
||||
if value, err = redis.Strings(r.conn.Do("LRANGE", key, 0, -1)); err == nil {
|
||||
kv.Length, _ = redis.Int64(r.conn.Do("LLEN", key))
|
||||
}
|
||||
case "set":
|
||||
if value, err = redis.Strings(r.conn.Do("SMEMBERS", key)); err == nil {
|
||||
kv.Length, _ = redis.Int64(r.conn.Do("SCARD", key))
|
||||
}
|
||||
case "zset":
|
||||
if members, err := redis.Strings(r.conn.Do("ZRANGE", key, 0, -1, "WITHSCORES")); err == nil {
|
||||
kv.Length, _ = redis.Int64(r.conn.Do("ZCARD", key))
|
||||
zsetMap := make(map[string]string)
|
||||
for i := 0; i < len(members); i += 2 {
|
||||
zsetMap[members[i]] = members[i+1]
|
||||
}
|
||||
value = zsetMap
|
||||
}
|
||||
case "hash":
|
||||
if value, err = redis.StringMap(r.conn.Do("HGETALL", key)); err == nil {
|
||||
kv.Length, _ = redis.Int64(r.conn.Do("HLEN", key))
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if kv.Length > 500 {
|
||||
value = "data is too long, can't display"
|
||||
}
|
||||
|
||||
if str, ok := value.(string); ok {
|
||||
kv.Value = str
|
||||
} else {
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
kv.Value = string(encoded)
|
||||
}
|
||||
|
||||
result = append(result, kv)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Redis) Clear() error {
|
||||
_, err := r.conn.Do("FLUSHDB")
|
||||
return err
|
||||
}
|
||||
@@ -266,7 +266,7 @@ func (r *Firewall) Forward(rule Forward, operation Operation) error {
|
||||
}
|
||||
ruleBuilder.WriteString(" --permanent")
|
||||
|
||||
_, err := shell.Execf(ruleBuilder.String()) // nolint: govet
|
||||
_, err := shell.Exec(ruleBuilder.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -89,6 +89,31 @@ func UnCompress(src string, dst string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ListCompress 获取压缩包内文件列表
|
||||
func ListCompress(src string) ([]string, error) {
|
||||
format, err := formatArchiveByPath(src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out string
|
||||
switch format {
|
||||
case Zip:
|
||||
out, err = shell.Execf("unzip -Z1 '%s'", src)
|
||||
case TGz, Bz2, Tar, Xz:
|
||||
out, err = shell.Execf("tar -tf '%s'", src)
|
||||
case SevenZip:
|
||||
out, err = shell.Execf(`7z l -ba -slt '%s' | grep "^Path = " | sed 's/^Path = //'`, src)
|
||||
default:
|
||||
return nil, errors.New("unsupported format")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return strings.Split(out, "\n"), nil
|
||||
}
|
||||
|
||||
// formatArchiveByPath 根据文件后缀获取压缩格式
|
||||
func formatArchiveByPath(path string) (FormatArchive, error) {
|
||||
switch filepath.Ext(path) {
|
||||
|
||||
+35
-2
@@ -4,6 +4,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/chattr"
|
||||
)
|
||||
|
||||
// Write 写入文件
|
||||
@@ -12,27 +14,58 @@ func Write(path string, data string, permission os.FileMode) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err := os.WriteFile(path, []byte(data), permission)
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, permission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
iFlag, _ := chattr.IsAttr(file, chattr.FS_IMMUTABLE_FL)
|
||||
aFlag, _ := chattr.IsAttr(file, chattr.FS_APPEND_FL)
|
||||
if iFlag {
|
||||
_ = chattr.UnsetAttr(file, chattr.FS_IMMUTABLE_FL)
|
||||
}
|
||||
if aFlag {
|
||||
_ = chattr.UnsetAttr(file, chattr.FS_APPEND_FL)
|
||||
}
|
||||
|
||||
_, err = file.WriteString(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if iFlag {
|
||||
_ = chattr.SetAttr(file, chattr.FS_IMMUTABLE_FL)
|
||||
}
|
||||
if aFlag {
|
||||
_ = chattr.SetAttr(file, chattr.FS_APPEND_FL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteAppend 追加写入文件
|
||||
func WriteAppend(path string, data string) error {
|
||||
file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
iFlag, _ := chattr.IsAttr(file, chattr.FS_IMMUTABLE_FL)
|
||||
if iFlag {
|
||||
_ = chattr.UnsetAttr(file, chattr.FS_IMMUTABLE_FL)
|
||||
}
|
||||
|
||||
_, err = file.WriteString(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if iFlag {
|
||||
_ = chattr.SetAttr(file, chattr.FS_IMMUTABLE_FL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -175,6 +175,60 @@ func (s *IOTestSuite) TestUnCompress() {
|
||||
s.NoError(Remove("testdata"))
|
||||
}
|
||||
|
||||
func (s *IOTestSuite) TestListCompress() {
|
||||
abs, err := filepath.Abs("testdata")
|
||||
s.NoError(err)
|
||||
src := []string{"list_archive_test1.txt", "list_archive_test2.txt"}
|
||||
err = Write(filepath.Join(abs, src[0]), "File 1", 0644)
|
||||
s.NoError(err)
|
||||
err = Write(filepath.Join(abs, src[1]), "File 2", 0644)
|
||||
s.NoError(err)
|
||||
|
||||
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.zip"))
|
||||
s.NoError(err)
|
||||
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.bz2"))
|
||||
s.NoError(err)
|
||||
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.tar"))
|
||||
s.NoError(err)
|
||||
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.gz"))
|
||||
s.NoError(err)
|
||||
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.tar.gz"))
|
||||
s.NoError(err)
|
||||
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.tgz"))
|
||||
s.NoError(err)
|
||||
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.xz"))
|
||||
s.NoError(err)
|
||||
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.7z"))
|
||||
s.NoError(err)
|
||||
|
||||
list, err := ListCompress(filepath.Join(abs, "list_archive_test.zip"))
|
||||
s.NoError(err)
|
||||
s.Len(list, 2)
|
||||
list, err = ListCompress(filepath.Join(abs, "list_archive_test.bz2"))
|
||||
s.NoError(err)
|
||||
s.Len(list, 2)
|
||||
list, err = ListCompress(filepath.Join(abs, "list_archive_test.tar"))
|
||||
s.NoError(err)
|
||||
s.Len(list, 2)
|
||||
list, err = ListCompress(filepath.Join(abs, "list_archive_test.gz"))
|
||||
s.NoError(err)
|
||||
s.Len(list, 2)
|
||||
list, err = ListCompress(filepath.Join(abs, "list_archive_test.tar.gz"))
|
||||
s.NoError(err)
|
||||
s.Len(list, 2)
|
||||
list, err = ListCompress(filepath.Join(abs, "list_archive_test.tgz"))
|
||||
s.NoError(err)
|
||||
s.Len(list, 2)
|
||||
list, err = ListCompress(filepath.Join(abs, "list_archive_test.xz"))
|
||||
s.NoError(err)
|
||||
s.Len(list, 2)
|
||||
list, err = ListCompress(filepath.Join(abs, "list_archive_test.7z"))
|
||||
s.NoError(err)
|
||||
s.Len(list, 2)
|
||||
|
||||
s.NoError(Remove("testdata"))
|
||||
}
|
||||
|
||||
func (s *IOTestSuite) TestRemoveDeletesFileOrDirectory() {
|
||||
path := "testdata/remove_test"
|
||||
s.NoError(Mkdir(path, 0755))
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
// Remove 删除文件/目录
|
||||
func Remove(path string) error {
|
||||
_, _ = shell.Execf("chattr -R -ia '%s'", path)
|
||||
return os.RemoveAll(path)
|
||||
}
|
||||
|
||||
|
||||
+17
-1
@@ -16,7 +16,23 @@ import (
|
||||
"github.com/creack/pty"
|
||||
)
|
||||
|
||||
// Execf 执行 shell 命令
|
||||
// Exec 执行 shell 命令
|
||||
func Exec(shell string) (string, error) {
|
||||
_ = os.Setenv("LC_ALL", "C")
|
||||
cmd := exec.Command("bash", "-c", shell)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return strings.TrimSpace(stdout.String()), fmt.Errorf("run %s failed, err: %s", shell, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
|
||||
return strings.TrimSpace(stdout.String()), nil
|
||||
}
|
||||
|
||||
// Execf 安全执行 shell 命令
|
||||
func Execf(shell string, args ...any) (string, error) {
|
||||
if !preCheckArg(args) {
|
||||
return "", errors.New("command contains illegal characters")
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package slice
|
||||
|
||||
import "github.com/spf13/cast"
|
||||
|
||||
// ToAny 将任意类型切片转换为 []any
|
||||
func ToAny[T any](s []T) []any {
|
||||
result := make([]any, len(s))
|
||||
for i, v := range s {
|
||||
result[i] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ToString 将任意类型切片转换为 []string
|
||||
func ToString[T any](s []T) []string {
|
||||
result := make([]string, len(s))
|
||||
for i, v := range s {
|
||||
result[i] = cast.ToString(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ToInt 将任意类型切片转换为 []int
|
||||
func ToInt[T any](s []T) []int {
|
||||
result := make([]int, len(s))
|
||||
for i, v := range s {
|
||||
result[i] = cast.ToInt(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package str
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// FirstElement 返回切片的第一个元素
|
||||
func FirstElement[T any](args []T) *T {
|
||||
if len(args) > 0 {
|
||||
return &args[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RandomNumber 生成长度为 length 随机数字字符串
|
||||
func RandomNumber(length int) string {
|
||||
table := [...]byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}
|
||||
b := make([]byte, length)
|
||||
n, err := io.ReadAtLeast(rand.Reader, b, length)
|
||||
if n != length {
|
||||
log.Panicf("failed to generate random number: %v", err)
|
||||
}
|
||||
for i := 0; i < len(b); i++ {
|
||||
b[i] = table[int(b[i])%len(table)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// RandomString 生成长度为 length 的随机字符串
|
||||
func RandomString(length int) string {
|
||||
b := make([]byte, length)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
log.Panicf("failed to generate random string: %v", err)
|
||||
}
|
||||
letters := "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
for i, v := range b {
|
||||
b[i] = letters[v%byte(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// MD5 生成字符串的 MD5 值
|
||||
func MD5(str string) string {
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(str)))
|
||||
}
|
||||
|
||||
// FormatBytes 格式化bytes
|
||||
func FormatBytes(size float64) string {
|
||||
units := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
|
||||
i := 0
|
||||
for ; size >= 1024 && i < len(units); i++ {
|
||||
size /= 1024
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.2f %s", size, units[i])
|
||||
}
|
||||
|
||||
// Cut 裁剪字符串
|
||||
func Cut(str, begin, end string) string {
|
||||
bIndex := strings.Index(str, begin)
|
||||
eIndex := strings.Index(str, end)
|
||||
if bIndex == -1 || eIndex == -1 || bIndex > eIndex {
|
||||
return ""
|
||||
}
|
||||
|
||||
b := utf8.RuneCountInString(str[:bIndex]) + utf8.RuneCountInString(begin)
|
||||
e := utf8.RuneCountInString(str[:eIndex])
|
||||
if b > e {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string([]rune(str)[b:e])
|
||||
}
|
||||
|
||||
// Escape 转义字符串
|
||||
func Escape(str string) string {
|
||||
return template.HTMLEscapeString(str)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package str
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type StringHelperTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestStringHelperTestSuite(t *testing.T) {
|
||||
suite.Run(t, &StringHelperTestSuite{})
|
||||
}
|
||||
|
||||
func (s *StringHelperTestSuite) TestFirstElement() {
|
||||
s.Equal("HaoZi", *FirstElement([]string{"HaoZi"}))
|
||||
}
|
||||
|
||||
func (s *StringHelperTestSuite) TestRandomNumber() {
|
||||
s.Len(RandomNumber(10), 10)
|
||||
}
|
||||
|
||||
func (s *StringHelperTestSuite) TestRandomString() {
|
||||
s.Len(RandomString(10), 10)
|
||||
}
|
||||
|
||||
func (s *StringHelperTestSuite) TestMD5() {
|
||||
s.Equal("e10adc3949ba59abbe56e057f20f883e", MD5("123456"))
|
||||
}
|
||||
|
||||
func (s *StringHelperTestSuite) TestFormatBytes() {
|
||||
s.Equal("1.00 B", FormatBytes(1))
|
||||
s.Equal("1.00 KB", FormatBytes(1024))
|
||||
s.Equal("1.00 MB", FormatBytes(1024*1024))
|
||||
s.Equal("1.00 GB", FormatBytes(1024*1024*1024))
|
||||
s.Equal("1.00 TB", FormatBytes(1024*1024*1024*1024))
|
||||
s.Equal("1.00 PB", FormatBytes(1024*1024*1024*1024*1024))
|
||||
s.Equal("1.00 EB", FormatBytes(1024*1024*1024*1024*1024*1024))
|
||||
s.Equal("1.00 ZB", FormatBytes(1024*1024*1024*1024*1024*1024*1024))
|
||||
s.Equal("1.00 YB", FormatBytes(1024*1024*1024*1024*1024*1024*1024*1024))
|
||||
}
|
||||
|
||||
func (s *StringHelperTestSuite) TestCut() {
|
||||
s.Equal("aoZ", Cut("HaoZi", "H", "i"))
|
||||
}
|
||||
+16
-3
@@ -4,6 +4,7 @@ package tools
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
stdnet "net"
|
||||
"net/http"
|
||||
"slices"
|
||||
@@ -96,7 +97,7 @@ func IsChina() bool {
|
||||
client.SetTimeout(3 * time.Second)
|
||||
client.SetRetryCount(3)
|
||||
|
||||
resp, err := client.R().Get("https://www.cloudflare-cn.com/cdn-cgi/trace")
|
||||
resp, err := client.R().Get("https://www.qualcomm.cn/cdn-cgi/trace")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
return false
|
||||
}
|
||||
@@ -122,7 +123,7 @@ func GetPublicIPv4() (string, error) {
|
||||
},
|
||||
})
|
||||
|
||||
resp, err := client.R().Get("https://www.cloudflare-cn.com/cdn-cgi/trace")
|
||||
resp, err := client.R().Get("https://www.qualcomm.cn/cdn-cgi/trace")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
return "", errors.New("failed to get public ipv4 address")
|
||||
}
|
||||
@@ -144,7 +145,7 @@ func GetPublicIPv6() (string, error) {
|
||||
},
|
||||
})
|
||||
|
||||
resp, err := client.R().Get("https://www.cloudflare-cn.com/cdn-cgi/trace")
|
||||
resp, err := client.R().Get("https://www.qualcomm.cn/cdn-cgi/trace")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
return "", errors.New("failed to get public ipv6 address")
|
||||
}
|
||||
@@ -175,3 +176,15 @@ func GetLocalIPv6() (string, error) {
|
||||
local := conn.LocalAddr().(*stdnet.UDPAddr)
|
||||
return local.IP.String(), nil
|
||||
}
|
||||
|
||||
// FormatBytes 格式化bytes
|
||||
func FormatBytes(size float64) string {
|
||||
units := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
|
||||
|
||||
i := 0
|
||||
for ; size >= 1024 && i < len(units); i++ {
|
||||
size /= 1024
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.2f %s", size, units[i])
|
||||
}
|
||||
|
||||
+20
-8
@@ -6,38 +6,50 @@ import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type HelperTestSuite struct {
|
||||
type ToolsTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestHelperTestSuite(t *testing.T) {
|
||||
suite.Run(t, &HelperTestSuite{})
|
||||
func TestToolsTestSuite(t *testing.T) {
|
||||
suite.Run(t, &ToolsTestSuite{})
|
||||
}
|
||||
|
||||
func (s *HelperTestSuite) TestGetMonitoringInfo() {
|
||||
func (s *ToolsTestSuite) TestGetMonitoringInfo() {
|
||||
s.NotNil(CurrentInfo(nil, nil))
|
||||
}
|
||||
|
||||
func (s *HelperTestSuite) TestGetPublicIPv4() {
|
||||
func (s *ToolsTestSuite) TestGetPublicIPv4() {
|
||||
ip, err := GetPublicIPv4()
|
||||
s.NoError(err)
|
||||
s.NotEmpty(ip)
|
||||
}
|
||||
|
||||
func (s *HelperTestSuite) TestGetPublicIPv6() {
|
||||
func (s *ToolsTestSuite) TestGetPublicIPv6() {
|
||||
ip, err := GetPublicIPv6()
|
||||
s.Error(err)
|
||||
s.Empty(ip)
|
||||
}
|
||||
|
||||
func (s *HelperTestSuite) TestGetLocalIPv4() {
|
||||
func (s *ToolsTestSuite) TestGetLocalIPv4() {
|
||||
ip, err := GetLocalIPv4()
|
||||
s.NoError(err)
|
||||
s.NotEmpty(ip)
|
||||
}
|
||||
|
||||
func (s *HelperTestSuite) TestGetLocalIPv6() {
|
||||
func (s *ToolsTestSuite) TestGetLocalIPv6() {
|
||||
ip, err := GetLocalIPv6()
|
||||
s.Error(err)
|
||||
s.Empty(ip)
|
||||
}
|
||||
|
||||
func (s *ToolsTestSuite) TestFormatBytes() {
|
||||
s.Equal("1.00 B", FormatBytes(1))
|
||||
s.Equal("1.00 KB", FormatBytes(1024))
|
||||
s.Equal("1.00 MB", FormatBytes(1024*1024))
|
||||
s.Equal("1.00 GB", FormatBytes(1024*1024*1024))
|
||||
s.Equal("1.00 TB", FormatBytes(1024*1024*1024*1024))
|
||||
s.Equal("1.00 PB", FormatBytes(1024*1024*1024*1024*1024))
|
||||
s.Equal("1.00 EB", FormatBytes(1024*1024*1024*1024*1024*1024))
|
||||
s.Equal("1.00 ZB", FormatBytes(1024*1024*1024*1024*1024*1024*1024))
|
||||
s.Equal("1.00 YB", FormatBytes(1024*1024*1024*1024*1024*1024*1024*1024))
|
||||
}
|
||||
|
||||
+5
-5
@@ -38,7 +38,7 @@
|
||||
"install": "^0.13.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.5.0",
|
||||
"marked": "^14.1.2",
|
||||
"marked": "^15.0.0",
|
||||
"mitt": "^3.0.1",
|
||||
"pinia": "^2.2.4",
|
||||
"pinia-plugin-persistedstate": "^4.1.1",
|
||||
@@ -56,11 +56,11 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.0.0",
|
||||
"@unocss/eslint-config": "^0.63.4",
|
||||
"@unocss/eslint-config": "^0.64.0",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"@vue/eslint-config-prettier": "^10.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"@vue/tsconfig": "^0.6.0",
|
||||
"colord": "^2.9.3",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-vue": "^9.29.0",
|
||||
@@ -72,9 +72,9 @@
|
||||
"prettier-plugin-organize-imports": "^4.1.0",
|
||||
"sass": "^1.79.5",
|
||||
"typescript": "^5.6.3",
|
||||
"unocss": "^0.63.4",
|
||||
"unocss": "^0.64.0",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-icons": "^0.19.3",
|
||||
"unplugin-icons": "^0.20.0",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.8",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
|
||||
Generated
+651
-615
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
import { http } from '@/utils'
|
||||
|
||||
// 负载状态
|
||||
export const getLoad = () => http.Get('/apps/memcached/load')
|
||||
// 获取配置
|
||||
export const getConfig = () => http.Get('/apps/memcached/config')
|
||||
// 保存配置
|
||||
export const updateConfig = (config: string) => http.Post('/apps/memcached/config', { config })
|
||||
@@ -3,8 +3,9 @@ import type { AxiosResponse } from 'axios'
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 负载状态
|
||||
load: (version: number): Promise<AxiosResponse<any>> => request.get(`/apps/php${version}/load`),
|
||||
// 设为 CLI 版本
|
||||
setCli: (version: number): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/apps/php${version}/setCli`),
|
||||
// 获取配置
|
||||
config: (version: number): Promise<AxiosResponse<any>> =>
|
||||
request.get(`/apps/php${version}/config`),
|
||||
@@ -17,6 +18,8 @@ export default {
|
||||
// 保存FPM配置
|
||||
saveFPMConfig: (version: number, config: string): Promise<AxiosResponse<any>> =>
|
||||
request.post(`/apps/php${version}/fpmConfig`, { config }),
|
||||
// 负载状态
|
||||
load: (version: number): Promise<AxiosResponse<any>> => request.get(`/apps/php${version}/load`),
|
||||
// 获取错误日志
|
||||
errorLog: (version: number): Promise<AxiosResponse<any>> =>
|
||||
request.get(`/apps/php${version}/errorLog`),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { AxiosResponse } from 'axios'
|
||||
|
||||
import { request } from '@/utils'
|
||||
import type { RequestConfig } from '~/types/axios'
|
||||
|
||||
export default {
|
||||
// 创建文件/文件夹
|
||||
@@ -25,12 +24,12 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
// 检查文件是否存在
|
||||
exist: (paths: string[]): Promise<AxiosResponse<any>> => request.post('/file/exist', paths),
|
||||
// 移动文件
|
||||
move: (source: string, target: string, force: boolean): Promise<AxiosResponse<any>> =>
|
||||
request.post('/file/move', { source, target, force }, { noNeedTip: true } as RequestConfig),
|
||||
move: (paths: any[]): Promise<AxiosResponse<any>> => request.post('/file/move', paths),
|
||||
// 复制文件
|
||||
copy: (source: string, target: string, force: boolean): Promise<AxiosResponse<any>> =>
|
||||
request.post('/file/copy', { source, target, force }, { noNeedTip: true } as RequestConfig),
|
||||
copy: (paths: any[]): Promise<AxiosResponse<any>> => request.post('/file/copy', paths),
|
||||
// 远程下载
|
||||
remoteDownload: (path: string, url: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/file/remoteDownload', { path, url }),
|
||||
|
||||
@@ -4,8 +4,14 @@ import { languageByPath } from '@/utils/file'
|
||||
import Editor from '@guolao/vue-monaco-editor'
|
||||
|
||||
const props = defineProps({
|
||||
path: String,
|
||||
readOnly: Boolean
|
||||
path: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const disabled = ref(false) // 在出现错误的情况下禁用保存
|
||||
@@ -13,7 +19,7 @@ const data = ref('')
|
||||
|
||||
const get = async () => {
|
||||
await file
|
||||
.content(props.path!)
|
||||
.content(props.path)
|
||||
.then((res) => {
|
||||
data.value = res.data
|
||||
window.$message.success('获取成功')
|
||||
@@ -28,7 +34,7 @@ const save = async () => {
|
||||
window.$message.error('当前状态下不可保存')
|
||||
return
|
||||
}
|
||||
await file.save(props.path!, data.value)
|
||||
await file.save(props.path, data.value)
|
||||
window.$message.success('保存成功')
|
||||
}
|
||||
|
||||
@@ -45,7 +51,7 @@ defineExpose({
|
||||
<template>
|
||||
<Editor
|
||||
v-model:value="data"
|
||||
:language="languageByPath(props.path!)"
|
||||
:language="languageByPath(props.path)"
|
||||
theme="vs-dark"
|
||||
height="60vh"
|
||||
:options="{
|
||||
|
||||
@@ -3,7 +3,10 @@ import ws from '@/api/ws'
|
||||
import type { LogInst } from 'naive-ui'
|
||||
|
||||
const props = defineProps({
|
||||
path: String
|
||||
path: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const log = ref('')
|
||||
|
||||
@@ -4,7 +4,10 @@ import type { LogInst } from 'naive-ui'
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const props = defineProps({
|
||||
path: String
|
||||
path: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const log = ref('')
|
||||
|
||||
@@ -280,7 +280,7 @@ const checkName = (name: string) => {
|
||||
}
|
||||
|
||||
const checkPath = (path: string) => {
|
||||
return /^(?!\/)(?!.*\/$)(?!.*\/\/)(?!.*\s).*$/.test(path)
|
||||
return /^(?!\/)(?!.*\/$)(?!.*\/\/).*$/.test(path)
|
||||
}
|
||||
|
||||
const getFilename = (path: string) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ defineOptions({
|
||||
|
||||
import VersionModal from '@/views/app/VersionModal.vue'
|
||||
|
||||
import { NButton, NDataTable, NPopconfirm, NSwitch } from 'naive-ui'
|
||||
import { NButton, NDataTable, NFlex, NPopconfirm, NSwitch } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import TheIcon from '@/components/custom/TheIcon.vue'
|
||||
@@ -72,98 +72,104 @@ const columns: any = [
|
||||
title: t('appIndex.columns.actions'),
|
||||
key: 'actions',
|
||||
width: 280,
|
||||
align: 'center',
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
row.installed && row.update_exist
|
||||
? h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleUpdate(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return t('appIndex.confirm.update', { app: row.name })
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'warning'
|
||||
return h(
|
||||
NFlex,
|
||||
{
|
||||
justify: 'center'
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
row.installed && row.update_exist
|
||||
? h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleUpdate(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return t('appIndex.confirm.update', { app: row.name })
|
||||
},
|
||||
{
|
||||
default: () => t('appIndex.buttons.update'),
|
||||
icon: renderIcon('material-symbols:arrow-circle-up-outline-rounded', {
|
||||
size: 14
|
||||
})
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'warning'
|
||||
},
|
||||
{
|
||||
default: () => t('appIndex.buttons.update'),
|
||||
icon: renderIcon('material-symbols:arrow-circle-up-outline-rounded', {
|
||||
size: 14
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
: null,
|
||||
row.installed
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'success',
|
||||
onClick: () => handleManage(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => t('appIndex.buttons.manage'),
|
||||
icon: renderIcon('material-symbols:settings-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
: null,
|
||||
row.installed
|
||||
? h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleUninstall(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return t('appIndex.confirm.uninstall', { app: row.name })
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px;'
|
||||
}
|
||||
)
|
||||
: null,
|
||||
row.installed
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'success',
|
||||
onClick: () => handleManage(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => t('appIndex.buttons.manage'),
|
||||
icon: renderIcon('material-symbols:settings-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
: null,
|
||||
row.installed
|
||||
? h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleUninstall(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return t('appIndex.confirm.uninstall', { app: row.name })
|
||||
},
|
||||
{
|
||||
default: () => t('appIndex.buttons.uninstall'),
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error'
|
||||
},
|
||||
{
|
||||
default: () => t('appIndex.buttons.uninstall'),
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
: null,
|
||||
!row.installed
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
versionModalShow.value = true
|
||||
versionModalOperation.value = '安装'
|
||||
versionModalInfo.value = row
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => t('appIndex.buttons.install'),
|
||||
icon: renderIcon('material-symbols:download-rounded', { size: 14 })
|
||||
}
|
||||
)
|
||||
: null
|
||||
]
|
||||
}
|
||||
)
|
||||
: null,
|
||||
!row.installed
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
versionModalShow.value = true
|
||||
versionModalOperation.value = '安装'
|
||||
versionModalInfo.value = row
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => t('appIndex.buttons.install'),
|
||||
icon: renderIcon('material-symbols:download-rounded', { size: 14 })
|
||||
}
|
||||
)
|
||||
: null
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -80,19 +80,15 @@ const handleClose = () => {
|
||||
<n-input v-model:value="model.version" placeholder="请选择渠道" readonly disabled />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button
|
||||
type="info"
|
||||
block
|
||||
:loading="doSubmit"
|
||||
:disabled="model.channel == null || doSubmit"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交
|
||||
</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-button
|
||||
type="info"
|
||||
block
|
||||
:loading="doSubmit"
|
||||
:disabled="model.channel == null || doSubmit"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交
|
||||
</n-button>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'apps-memcached-index'
|
||||
})
|
||||
|
||||
import Editor from '@guolao/vue-monaco-editor'
|
||||
import { NButton, NDataTable, NPopconfirm } from 'naive-ui'
|
||||
|
||||
import { getConfig, getLoad, updateConfig } from '@/api/apps/memcached'
|
||||
import systemctl from '@/api/panel/systemctl'
|
||||
|
||||
const currentTab = ref('status')
|
||||
const status = ref(false)
|
||||
const isEnabled = ref(false)
|
||||
|
||||
const statusType = computed(() => {
|
||||
return status.value ? 'success' : 'error'
|
||||
})
|
||||
const statusStr = computed(() => {
|
||||
return status.value ? '正常运行中' : '已停止运行'
|
||||
})
|
||||
|
||||
const loadColumns: any = [
|
||||
{
|
||||
title: '属性',
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const { data: load }: { data: any } = useRequest(getLoad, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
const getStatus = async () => {
|
||||
await systemctl.status('memcached').then((res: any) => {
|
||||
status.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getIsEnabled = async () => {
|
||||
await systemctl.isEnabled('memcached').then((res: any) => {
|
||||
isEnabled.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const { data: config }: { data: any } = useRequest(getConfig, {
|
||||
initialData: {
|
||||
config: ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
useRequest(() => updateConfig(config.value)).onSuccess(() => {
|
||||
window.$message.success('保存成功')
|
||||
})
|
||||
}
|
||||
|
||||
const handleStart = async () => {
|
||||
await systemctl.start('memcached')
|
||||
window.$message.success('启动成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleIsEnabled = async () => {
|
||||
if (isEnabled.value) {
|
||||
await systemctl.enable('memcached')
|
||||
window.$message.success('开启自启动成功')
|
||||
} else {
|
||||
await systemctl.disable('memcached')
|
||||
window.$message.success('禁用自启动成功')
|
||||
}
|
||||
await getIsEnabled()
|
||||
}
|
||||
|
||||
const handleStop = async () => {
|
||||
await systemctl.stop('memcached')
|
||||
window.$message.success('停止成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleRestart = async () => {
|
||||
await systemctl.restart('memcached')
|
||||
window.$message.success('重启成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getStatus()
|
||||
getIsEnabled()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<template #action>
|
||||
<n-button
|
||||
v-if="currentTab == 'config'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleSaveConfig"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:save-outline" />
|
||||
保存
|
||||
</n-button>
|
||||
</template>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" tab="运行状态">
|
||||
<n-space vertical>
|
||||
<n-card title="运行状态" rounded-10>
|
||||
<template #header-extra>
|
||||
<n-switch v-model:value="isEnabled" @update:value="handleIsEnabled">
|
||||
<template #checked> 自启动开 </template>
|
||||
<template #unchecked> 自启动关 </template>
|
||||
</n-switch>
|
||||
</template>
|
||||
<n-space vertical>
|
||||
<n-alert :type="statusType">
|
||||
{{ statusStr }}
|
||||
</n-alert>
|
||||
<n-space>
|
||||
<n-button type="success" @click="handleStart">
|
||||
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
|
||||
启动
|
||||
</n-button>
|
||||
<n-popconfirm @positive-click="handleStop">
|
||||
<template #trigger>
|
||||
<n-button type="error">
|
||||
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
|
||||
停止
|
||||
</n-button>
|
||||
</template>
|
||||
停止 Memcached 会导致使用 Memcached 的网站无法访问,确定要停止吗?
|
||||
</n-popconfirm>
|
||||
<n-button type="warning" @click="handleRestart">
|
||||
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
|
||||
重启
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" tab="服务配置">
|
||||
<n-space vertical>
|
||||
<Editor
|
||||
v-model:value="config"
|
||||
language="ini"
|
||||
theme="vs-dark"
|
||||
height="60vh"
|
||||
mt-8
|
||||
:options="{
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true
|
||||
}"
|
||||
/>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" tab="负载状态">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1000"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { RouteType } from '~/types/router'
|
||||
|
||||
const Layout = () => import('@/layout/IndexView.vue')
|
||||
|
||||
export default {
|
||||
name: 'memcached',
|
||||
path: '/apps/memcached',
|
||||
component: Layout,
|
||||
isHidden: true,
|
||||
children: [
|
||||
{
|
||||
name: 'apps-memcached-index',
|
||||
path: '',
|
||||
component: () => import('./IndexView.vue'),
|
||||
meta: {
|
||||
title: 'Memcached',
|
||||
icon: 'logos:memcached',
|
||||
role: ['admin'],
|
||||
requireAuth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
} as RouteType
|
||||
@@ -0,0 +1,420 @@
|
||||
<script setup lang="ts">
|
||||
import Editor from '@guolao/vue-monaco-editor'
|
||||
import { NButton, NDataTable, NPopconfirm } from 'naive-ui'
|
||||
|
||||
import php from '@/api/apps/php'
|
||||
import systemctl from '@/api/panel/systemctl'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
version: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const { version } = toRefs(props)
|
||||
|
||||
const currentTab = ref('status')
|
||||
const status = ref(false)
|
||||
const isEnabled = ref(false)
|
||||
const config = ref('')
|
||||
const fpmConfig = ref('')
|
||||
const errorLog = ref('')
|
||||
const slowLog = ref('')
|
||||
|
||||
const statusType = computed(() => {
|
||||
return status.value ? 'success' : 'error'
|
||||
})
|
||||
const statusStr = computed(() => {
|
||||
return status.value ? '正常运行中' : '已停止运行'
|
||||
})
|
||||
|
||||
const extensionColumns: any = [
|
||||
{
|
||||
title: '拓展名',
|
||||
key: 'name',
|
||||
minWidth: 250,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
key: 'description',
|
||||
resizable: true,
|
||||
minWidth: 250,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 240,
|
||||
align: 'center',
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
!row.installed
|
||||
? h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleInstallExtension(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定安装 ' + row.name + ' 吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'info'
|
||||
},
|
||||
{
|
||||
default: () => '安装',
|
||||
icon: renderIcon('material-symbols:download-rounded', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
: null,
|
||||
row.installed
|
||||
? h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleUninstallExtension(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定卸载 ' + row.name + ' 吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error'
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
: null
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const loadColumns: any = [
|
||||
{
|
||||
title: '属性',
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const extensions = ref<any[]>([])
|
||||
const load = ref<any[]>([])
|
||||
|
||||
const getLoad = async () => {
|
||||
const { data } = await php.load(version.value)
|
||||
return data
|
||||
}
|
||||
|
||||
const getExtensions = async () => {
|
||||
const { data } = await php.extensions(version.value)
|
||||
return data
|
||||
}
|
||||
|
||||
const getStatus = async () => {
|
||||
await systemctl.status(`php-fpm-${version.value}`).then((res: any) => {
|
||||
status.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getIsEnabled = async () => {
|
||||
await systemctl.isEnabled(`php-fpm-${version.value}`).then((res: any) => {
|
||||
isEnabled.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getErrorLog = async () => {
|
||||
php.errorLog(version.value).then((res: any) => {
|
||||
errorLog.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getSlowLog = async () => {
|
||||
php.slowLog(version.value).then((res: any) => {
|
||||
slowLog.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getConfig = async () => {
|
||||
php.config(version.value).then((res: any) => {
|
||||
config.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getFPMConfig = async () => {
|
||||
php.fpmConfig(version.value).then((res: any) => {
|
||||
fpmConfig.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const handleSetCli = async () => {
|
||||
await php.setCli(version.value)
|
||||
window.$message.success('设置成功')
|
||||
}
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
await php.saveConfig(version.value, config.value)
|
||||
window.$message.success('保存成功')
|
||||
}
|
||||
|
||||
const handleSaveFPMConfig = async () => {
|
||||
await php.saveFPMConfig(version.value, fpmConfig.value)
|
||||
window.$message.success('保存成功')
|
||||
await getFPMConfig()
|
||||
}
|
||||
|
||||
const handleClearErrorLog = async () => {
|
||||
await php.clearErrorLog(version.value)
|
||||
window.$message.success('清空成功')
|
||||
}
|
||||
|
||||
const handleClearSlowLog = async () => {
|
||||
await php.clearSlowLog(version.value)
|
||||
window.$message.success('清空成功')
|
||||
}
|
||||
|
||||
const handleIsEnabled = async () => {
|
||||
if (isEnabled.value) {
|
||||
await systemctl.enable(`php-fpm-${version.value}`)
|
||||
window.$message.success('开启自启动成功')
|
||||
} else {
|
||||
await systemctl.disable(`php-fpm-${version.value}`)
|
||||
window.$message.success('禁用自启动成功')
|
||||
}
|
||||
await getIsEnabled()
|
||||
}
|
||||
|
||||
const handleStart = async () => {
|
||||
await systemctl.start(`php-fpm-${version.value}`)
|
||||
window.$message.success('启动成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleStop = async () => {
|
||||
await systemctl.stop(`php-fpm-${version.value}`)
|
||||
window.$message.success('停止成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleRestart = async () => {
|
||||
await systemctl.restart(`php-fpm-${version.value}`)
|
||||
window.$message.success('重启成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleReload = async () => {
|
||||
await systemctl.reload(`php-fpm-${version.value}`)
|
||||
window.$message.success('重载成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleInstallExtension = async (slug: string) => {
|
||||
await php.installExtension(version.value, slug).then(() => {
|
||||
window.$message.success('任务已提交,请稍后查看任务进度')
|
||||
})
|
||||
}
|
||||
|
||||
const handleUninstallExtension = async (name: string) => {
|
||||
await php.uninstallExtension(version.value, name).then(() => {
|
||||
window.$message.success('任务已提交,请稍后查看任务进度')
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getStatus()
|
||||
getIsEnabled()
|
||||
getExtensions().then((res) => {
|
||||
extensions.value = res
|
||||
})
|
||||
getLoad().then((res) => {
|
||||
load.value = res
|
||||
})
|
||||
getErrorLog()
|
||||
getSlowLog()
|
||||
getConfig()
|
||||
getFPMConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<template #action>
|
||||
<n-button v-if="currentTab == 'status'" class="ml-16" type="info" @click="handleSetCli">
|
||||
设为 CLI 默认版本
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'config'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleSaveConfig"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:save-outline" />
|
||||
保存
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'fpm-config'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleSaveFPMConfig"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:save-outline" />
|
||||
保存
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'error-log'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleClearErrorLog"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:delete-outline" />
|
||||
清空错误日志
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'slow-log'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleClearSlowLog"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:delete-outline" />
|
||||
清空慢日志
|
||||
</n-button>
|
||||
</template>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" tab="运行状态">
|
||||
<n-space vertical>
|
||||
<n-card title="运行状态" rounded-10>
|
||||
<template #header-extra>
|
||||
<n-switch v-model:value="isEnabled" @update:value="handleIsEnabled">
|
||||
<template #checked> 自启动开 </template>
|
||||
<template #unchecked> 自启动关 </template>
|
||||
</n-switch>
|
||||
</template>
|
||||
<n-space vertical>
|
||||
<n-alert :type="statusType">
|
||||
{{ statusStr }}
|
||||
</n-alert>
|
||||
<n-space>
|
||||
<n-button type="success" @click="handleStart">
|
||||
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
|
||||
启动
|
||||
</n-button>
|
||||
<n-popconfirm @positive-click="handleStop">
|
||||
<template #trigger>
|
||||
<n-button type="error">
|
||||
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
|
||||
停止
|
||||
</n-button>
|
||||
</template>
|
||||
停止 PHP {{ version }} 会导致使用 PHP {{ version }} 的网站无法访问,确定要停止吗?
|
||||
</n-popconfirm>
|
||||
<n-button type="warning" @click="handleRestart">
|
||||
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
|
||||
重启
|
||||
</n-button>
|
||||
<n-button type="primary" @click="handleReload">
|
||||
<TheIcon :size="20" icon="material-symbols:refresh-rounded" />
|
||||
重载
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="extensions" tab="拓展管理">
|
||||
<n-card title="拓展列表" :segmented="true" rounded-10>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1000"
|
||||
:loading="false"
|
||||
:columns="extensionColumns"
|
||||
:data="extensions"
|
||||
:row-key="(row: any) => row.slug"
|
||||
/>
|
||||
</n-card>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" tab="主配置">
|
||||
<n-space vertical>
|
||||
<n-alert type="warning">
|
||||
此处修改的是 PHP {{ version }} 主配置文件,如果您不了解各参数的含义,请不要随意修改!
|
||||
</n-alert>
|
||||
<Editor
|
||||
v-model:value="config"
|
||||
language="ini"
|
||||
theme="vs-dark"
|
||||
height="60vh"
|
||||
mt-8
|
||||
:options="{
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true
|
||||
}"
|
||||
/>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="fpm-config" tab="FPM 配置">
|
||||
<n-space vertical>
|
||||
<n-alert type="warning">
|
||||
此处修改的是 PHP {{ version }} FPM 配置文件,如果您不了解各参数的含义,请不要随意修改!
|
||||
</n-alert>
|
||||
<Editor
|
||||
v-model:value="fpmConfig"
|
||||
language="ini"
|
||||
theme="vs-dark"
|
||||
height="60vh"
|
||||
mt-8
|
||||
:options="{
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true
|
||||
}"
|
||||
/>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" tab="负载状态">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="error-log" tab="错误日志">
|
||||
<realtime-log :path="errorLog" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="slow-log" tab="慢日志">
|
||||
<realtime-log :path="slowLog" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import PhpView from '@/views/apps/php/PhpView.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'apps-php74-index'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<php-view :version="74" />
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { RouteType } from '~/types/router'
|
||||
|
||||
const Layout = () => import('@/layout/IndexView.vue')
|
||||
|
||||
export default {
|
||||
name: 'php74',
|
||||
path: '/apps/php74',
|
||||
component: Layout,
|
||||
isHidden: true,
|
||||
children: [
|
||||
{
|
||||
name: 'apps-php74-index',
|
||||
path: '',
|
||||
component: () => import('./IndexView.vue'),
|
||||
meta: {
|
||||
title: 'PHP 7.4',
|
||||
icon: 'logos:php',
|
||||
role: ['admin'],
|
||||
requireAuth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
} as RouteType
|
||||
@@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import PhpView from '@/views/apps/php/PhpView.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'apps-php80-index'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<php-view :version="80" />
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { RouteType } from '~/types/router'
|
||||
|
||||
const Layout = () => import('@/layout/IndexView.vue')
|
||||
|
||||
export default {
|
||||
name: 'php80',
|
||||
path: '/apps/php80',
|
||||
component: Layout,
|
||||
isHidden: true,
|
||||
children: [
|
||||
{
|
||||
name: 'apps-php80-index',
|
||||
path: '',
|
||||
component: () => import('./IndexView.vue'),
|
||||
meta: {
|
||||
title: 'PHP 8.0',
|
||||
icon: 'logos:php',
|
||||
role: ['admin'],
|
||||
requireAuth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
} as RouteType
|
||||
@@ -1,397 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import PhpView from '@/views/apps/php/PhpView.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'apps-php81-index'
|
||||
})
|
||||
|
||||
import Editor from '@guolao/vue-monaco-editor'
|
||||
import { NButton, NDataTable, NPopconfirm } from 'naive-ui'
|
||||
|
||||
import php from '@/api/apps/php'
|
||||
import systemctl from '@/api/panel/systemctl'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const currentTab = ref('status')
|
||||
const version = Number(81)
|
||||
const status = ref(false)
|
||||
const isEnabled = ref(false)
|
||||
const config = ref('')
|
||||
const fpmConfig = ref('')
|
||||
const errorLog = ref('')
|
||||
const slowLog = ref('')
|
||||
|
||||
const statusType = computed(() => {
|
||||
return status.value ? 'success' : 'error'
|
||||
})
|
||||
const statusStr = computed(() => {
|
||||
return status.value ? '正常运行中' : '已停止运行'
|
||||
})
|
||||
|
||||
const extensionColumns: any = [
|
||||
{
|
||||
title: '拓展名',
|
||||
key: 'name',
|
||||
minWidth: 250,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
key: 'description',
|
||||
resizable: true,
|
||||
minWidth: 250,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 240,
|
||||
align: 'center',
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
!row.installed
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
secondary: true,
|
||||
onClick: () => handleInstallExtension(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => '安装',
|
||||
icon: renderIcon('material-symbols:download-rounded', { size: 14 })
|
||||
}
|
||||
)
|
||||
: null,
|
||||
row.installed
|
||||
? h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleUninstallExtension(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定卸载' + row.name + '吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error'
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
: null
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const loadColumns: any = [
|
||||
{
|
||||
title: '属性',
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const extensions = ref<any[]>([])
|
||||
const load = ref<any[]>([])
|
||||
|
||||
const getLoad = async () => {
|
||||
const { data } = await php.load(version)
|
||||
return data
|
||||
}
|
||||
|
||||
const getExtensions = async () => {
|
||||
const { data } = await php.extensions(version)
|
||||
return data
|
||||
}
|
||||
|
||||
const getStatus = async () => {
|
||||
await systemctl.status(`php-fpm-${version}`).then((res: any) => {
|
||||
status.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getIsEnabled = async () => {
|
||||
await systemctl.isEnabled(`php-fpm-${version}`).then((res: any) => {
|
||||
isEnabled.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getErrorLog = async () => {
|
||||
php.errorLog(version).then((res: any) => {
|
||||
errorLog.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getSlowLog = async () => {
|
||||
php.slowLog(version).then((res: any) => {
|
||||
slowLog.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getConfig = async () => {
|
||||
php.config(version).then((res: any) => {
|
||||
config.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getFPMConfig = async () => {
|
||||
php.fpmConfig(version).then((res: any) => {
|
||||
fpmConfig.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
await php.saveConfig(version, config.value)
|
||||
window.$message.success('保存成功')
|
||||
}
|
||||
|
||||
const handleSaveFPMConfig = async () => {
|
||||
await php.saveFPMConfig(version, fpmConfig.value)
|
||||
window.$message.success('保存成功')
|
||||
await getFPMConfig()
|
||||
}
|
||||
|
||||
const handleClearErrorLog = async () => {
|
||||
await php.clearErrorLog(version)
|
||||
window.$message.success('清空成功')
|
||||
}
|
||||
|
||||
const handleClearSlowLog = async () => {
|
||||
await php.clearSlowLog(version)
|
||||
window.$message.success('清空成功')
|
||||
}
|
||||
|
||||
const handleIsEnabled = async () => {
|
||||
if (isEnabled.value) {
|
||||
await systemctl.enable(`php-fpm-${version}`)
|
||||
window.$message.success('开启自启动成功')
|
||||
} else {
|
||||
await systemctl.disable(`php-fpm-${version}`)
|
||||
window.$message.success('禁用自启动成功')
|
||||
}
|
||||
await getIsEnabled()
|
||||
}
|
||||
|
||||
const handleStart = async () => {
|
||||
await systemctl.start(`php-fpm-${version}`)
|
||||
window.$message.success('启动成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleStop = async () => {
|
||||
await systemctl.stop(`php-fpm-${version}`)
|
||||
window.$message.success('停止成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleRestart = async () => {
|
||||
await systemctl.restart(`php-fpm-${version}`)
|
||||
window.$message.success('重启成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleReload = async () => {
|
||||
await systemctl.reload(`php-fpm-${version}`)
|
||||
window.$message.success('重载成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleInstallExtension = async (slug: string) => {
|
||||
await php.installExtension(version, slug).then(() => {
|
||||
window.$message.success('任务已提交,请稍后查看任务进度')
|
||||
})
|
||||
}
|
||||
|
||||
const handleUninstallExtension = async (name: string) => {
|
||||
await php.uninstallExtension(version, name).then(() => {
|
||||
window.$message.success('任务已提交,请稍后查看任务进度')
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getStatus()
|
||||
getIsEnabled()
|
||||
getExtensions().then((res) => {
|
||||
extensions.value = res
|
||||
})
|
||||
getLoad().then((res) => {
|
||||
load.value = res
|
||||
})
|
||||
getErrorLog()
|
||||
getSlowLog()
|
||||
getConfig()
|
||||
getFPMConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<template #action>
|
||||
<n-button
|
||||
v-if="currentTab == 'config'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleSaveConfig"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:save-outline" />
|
||||
保存
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'fpm-config'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleSaveFPMConfig"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:save-outline" />
|
||||
保存
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'error-log'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleClearErrorLog"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:delete-outline" />
|
||||
清空错误日志
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'slow-log'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleClearSlowLog"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:delete-outline" />
|
||||
清空慢日志
|
||||
</n-button>
|
||||
</template>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" tab="运行状态">
|
||||
<n-space vertical>
|
||||
<n-card title="运行状态" rounded-10>
|
||||
<template #header-extra>
|
||||
<n-switch v-model:value="isEnabled" @update:value="handleIsEnabled">
|
||||
<template #checked> 自启动开 </template>
|
||||
<template #unchecked> 自启动关 </template>
|
||||
</n-switch>
|
||||
</template>
|
||||
<n-space vertical>
|
||||
<n-alert :type="statusType">
|
||||
{{ statusStr }}
|
||||
</n-alert>
|
||||
<n-space>
|
||||
<n-button type="success" @click="handleStart">
|
||||
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
|
||||
启动
|
||||
</n-button>
|
||||
<n-popconfirm @positive-click="handleStop">
|
||||
<template #trigger>
|
||||
<n-button type="error">
|
||||
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
|
||||
停止
|
||||
</n-button>
|
||||
</template>
|
||||
停止 PHP {{ version }} 会导致使用 PHP {{ version }} 的网站无法访问,确定要停止吗?
|
||||
</n-popconfirm>
|
||||
<n-button type="warning" @click="handleRestart">
|
||||
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
|
||||
重启
|
||||
</n-button>
|
||||
<n-button type="primary" @click="handleReload">
|
||||
<TheIcon :size="20" icon="material-symbols:refresh-rounded" />
|
||||
重载
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="extensions" tab="拓展管理">
|
||||
<n-card title="拓展列表" :segmented="true" rounded-10>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1000"
|
||||
:loading="false"
|
||||
:columns="extensionColumns"
|
||||
:data="extensions"
|
||||
:row-key="(row: any) => row.slug"
|
||||
/>
|
||||
</n-card>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" tab="主配置">
|
||||
<n-space vertical>
|
||||
<n-alert type="warning">
|
||||
此处修改的是 PHP {{ version }} 主配置文件,如果您不了解各参数的含义,请不要随意修改!
|
||||
</n-alert>
|
||||
<Editor
|
||||
v-model:value="config"
|
||||
language="ini"
|
||||
theme="vs-dark"
|
||||
height="60vh"
|
||||
mt-8
|
||||
:options="{
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true
|
||||
}"
|
||||
/>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="fpm-config" tab="FPM 配置">
|
||||
<n-space vertical>
|
||||
<n-alert type="warning">
|
||||
此处修改的是 PHP {{ version }} FPM 配置文件,如果您不了解各参数的含义,请不要随意修改!
|
||||
</n-alert>
|
||||
<Editor
|
||||
v-model:value="fpmConfig"
|
||||
language="ini"
|
||||
theme="vs-dark"
|
||||
height="60vh"
|
||||
mt-8
|
||||
:options="{
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true
|
||||
}"
|
||||
/>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" tab="负载状态">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="error-log" tab="错误日志">
|
||||
<realtime-log :path="errorLog" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="slow-log" tab="慢日志">
|
||||
<realtime-log :path="slowLog" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
<php-view :version="81" />
|
||||
</template>
|
||||
|
||||
@@ -1,397 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import PhpView from '@/views/apps/php/PhpView.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'apps-php82-index'
|
||||
})
|
||||
|
||||
import Editor from '@guolao/vue-monaco-editor'
|
||||
import { NButton, NDataTable, NPopconfirm } from 'naive-ui'
|
||||
|
||||
import php from '@/api/apps/php'
|
||||
import systemctl from '@/api/panel/systemctl'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const currentTab = ref('status')
|
||||
const version = Number(82)
|
||||
const status = ref(false)
|
||||
const isEnabled = ref(false)
|
||||
const config = ref('')
|
||||
const fpmConfig = ref('')
|
||||
const errorLog = ref('')
|
||||
const slowLog = ref('')
|
||||
|
||||
const statusType = computed(() => {
|
||||
return status.value ? 'success' : 'error'
|
||||
})
|
||||
const statusStr = computed(() => {
|
||||
return status.value ? '正常运行中' : '已停止运行'
|
||||
})
|
||||
|
||||
const extensionColumns: any = [
|
||||
{
|
||||
title: '拓展名',
|
||||
key: 'name',
|
||||
minWidth: 250,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
key: 'description',
|
||||
resizable: true,
|
||||
minWidth: 250,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 240,
|
||||
align: 'center',
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
!row.installed
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
secondary: true,
|
||||
onClick: () => handleInstallExtension(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => '安装',
|
||||
icon: renderIcon('material-symbols:download-rounded', { size: 14 })
|
||||
}
|
||||
)
|
||||
: null,
|
||||
row.installed
|
||||
? h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleUninstallExtension(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定卸载' + row.name + '吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error'
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
: null
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const loadColumns: any = [
|
||||
{
|
||||
title: '属性',
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const extensions = ref<any[]>([])
|
||||
const load = ref<any[]>([])
|
||||
|
||||
const getLoad = async () => {
|
||||
const { data } = await php.load(version)
|
||||
return data
|
||||
}
|
||||
|
||||
const getExtensions = async () => {
|
||||
const { data } = await php.extensions(version)
|
||||
return data
|
||||
}
|
||||
|
||||
const getStatus = async () => {
|
||||
await systemctl.status(`php-fpm-${version}`).then((res: any) => {
|
||||
status.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getIsEnabled = async () => {
|
||||
await systemctl.isEnabled(`php-fpm-${version}`).then((res: any) => {
|
||||
isEnabled.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getErrorLog = async () => {
|
||||
php.errorLog(version).then((res: any) => {
|
||||
errorLog.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getSlowLog = async () => {
|
||||
php.slowLog(version).then((res: any) => {
|
||||
slowLog.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getConfig = async () => {
|
||||
php.config(version).then((res: any) => {
|
||||
config.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getFPMConfig = async () => {
|
||||
php.fpmConfig(version).then((res: any) => {
|
||||
fpmConfig.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
await php.saveConfig(version, config.value)
|
||||
window.$message.success('保存成功')
|
||||
}
|
||||
|
||||
const handleSaveFPMConfig = async () => {
|
||||
await php.saveFPMConfig(version, fpmConfig.value)
|
||||
window.$message.success('保存成功')
|
||||
await getFPMConfig()
|
||||
}
|
||||
|
||||
const handleClearErrorLog = async () => {
|
||||
await php.clearErrorLog(version)
|
||||
window.$message.success('清空成功')
|
||||
}
|
||||
|
||||
const handleClearSlowLog = async () => {
|
||||
await php.clearSlowLog(version)
|
||||
window.$message.success('清空成功')
|
||||
}
|
||||
|
||||
const handleIsEnabled = async () => {
|
||||
if (isEnabled.value) {
|
||||
await systemctl.enable(`php-fpm-${version}`)
|
||||
window.$message.success('开启自启动成功')
|
||||
} else {
|
||||
await systemctl.disable(`php-fpm-${version}`)
|
||||
window.$message.success('禁用自启动成功')
|
||||
}
|
||||
await getIsEnabled()
|
||||
}
|
||||
|
||||
const handleStart = async () => {
|
||||
await systemctl.start(`php-fpm-${version}`)
|
||||
window.$message.success('启动成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleStop = async () => {
|
||||
await systemctl.stop(`php-fpm-${version}`)
|
||||
window.$message.success('停止成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleRestart = async () => {
|
||||
await systemctl.restart(`php-fpm-${version}`)
|
||||
window.$message.success('重启成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleReload = async () => {
|
||||
await systemctl.reload(`php-fpm-${version}`)
|
||||
window.$message.success('重载成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleInstallExtension = async (slug: string) => {
|
||||
await php.installExtension(version, slug).then(() => {
|
||||
window.$message.success('任务已提交,请稍后查看任务进度')
|
||||
})
|
||||
}
|
||||
|
||||
const handleUninstallExtension = async (name: string) => {
|
||||
await php.uninstallExtension(version, name).then(() => {
|
||||
window.$message.success('任务已提交,请稍后查看任务进度')
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getStatus()
|
||||
getIsEnabled()
|
||||
getExtensions().then((res) => {
|
||||
extensions.value = res
|
||||
})
|
||||
getLoad().then((res) => {
|
||||
load.value = res
|
||||
})
|
||||
getErrorLog()
|
||||
getSlowLog()
|
||||
getConfig()
|
||||
getFPMConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<template #action>
|
||||
<n-button
|
||||
v-if="currentTab == 'config'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleSaveConfig"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:save-outline" />
|
||||
保存
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'fpm-config'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleSaveFPMConfig"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:save-outline" />
|
||||
保存
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'error-log'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleClearErrorLog"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:delete-outline" />
|
||||
清空错误日志
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'slow-log'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleClearSlowLog"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:delete-outline" />
|
||||
清空慢日志
|
||||
</n-button>
|
||||
</template>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" tab="运行状态">
|
||||
<n-space vertical>
|
||||
<n-card title="运行状态" rounded-10>
|
||||
<template #header-extra>
|
||||
<n-switch v-model:value="isEnabled" @update:value="handleIsEnabled">
|
||||
<template #checked> 自启动开 </template>
|
||||
<template #unchecked> 自启动关 </template>
|
||||
</n-switch>
|
||||
</template>
|
||||
<n-space vertical>
|
||||
<n-alert :type="statusType">
|
||||
{{ statusStr }}
|
||||
</n-alert>
|
||||
<n-space>
|
||||
<n-button type="success" @click="handleStart">
|
||||
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
|
||||
启动
|
||||
</n-button>
|
||||
<n-popconfirm @positive-click="handleStop">
|
||||
<template #trigger>
|
||||
<n-button type="error">
|
||||
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
|
||||
停止
|
||||
</n-button>
|
||||
</template>
|
||||
停止 PHP {{ version }} 会导致使用 PHP {{ version }} 的网站无法访问,确定要停止吗?
|
||||
</n-popconfirm>
|
||||
<n-button type="warning" @click="handleRestart">
|
||||
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
|
||||
重启
|
||||
</n-button>
|
||||
<n-button type="primary" @click="handleReload">
|
||||
<TheIcon :size="20" icon="material-symbols:refresh-rounded" />
|
||||
重载
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="extensions" tab="拓展管理">
|
||||
<n-card title="拓展列表" :segmented="true" rounded-10>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1000"
|
||||
:loading="false"
|
||||
:columns="extensionColumns"
|
||||
:data="extensions"
|
||||
:row-key="(row: any) => row.slug"
|
||||
/>
|
||||
</n-card>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" tab="主配置">
|
||||
<n-space vertical>
|
||||
<n-alert type="warning">
|
||||
此处修改的是 PHP {{ version }} 主配置文件,如果您不了解各参数的含义,请不要随意修改!
|
||||
</n-alert>
|
||||
<Editor
|
||||
v-model:value="config"
|
||||
language="ini"
|
||||
theme="vs-dark"
|
||||
height="60vh"
|
||||
mt-8
|
||||
:options="{
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true
|
||||
}"
|
||||
/>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="fpm-config" tab="FPM 配置">
|
||||
<n-space vertical>
|
||||
<n-alert type="warning">
|
||||
此处修改的是 PHP {{ version }} FPM 配置文件,如果您不了解各参数的含义,请不要随意修改!
|
||||
</n-alert>
|
||||
<Editor
|
||||
v-model:value="fpmConfig"
|
||||
language="ini"
|
||||
theme="vs-dark"
|
||||
height="60vh"
|
||||
mt-8
|
||||
:options="{
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true
|
||||
}"
|
||||
/>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" tab="负载状态">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="error-log" tab="错误日志">
|
||||
<realtime-log :path="errorLog" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="slow-log" tab="慢日志">
|
||||
<realtime-log :path="slowLog" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
<php-view :version="82" />
|
||||
</template>
|
||||
|
||||
@@ -1,397 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import PhpView from '@/views/apps/php/PhpView.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'apps-php83-index'
|
||||
})
|
||||
|
||||
import Editor from '@guolao/vue-monaco-editor'
|
||||
import { NButton, NDataTable, NPopconfirm } from 'naive-ui'
|
||||
|
||||
import php from '@/api/apps/php'
|
||||
import systemctl from '@/api/panel/systemctl'
|
||||
import { renderIcon } from '@/utils'
|
||||
|
||||
const currentTab = ref('status')
|
||||
const version = Number(83)
|
||||
const status = ref(false)
|
||||
const isEnabled = ref(false)
|
||||
const config = ref('')
|
||||
const fpmConfig = ref('')
|
||||
const errorLog = ref('')
|
||||
const slowLog = ref('')
|
||||
|
||||
const statusType = computed(() => {
|
||||
return status.value ? 'success' : 'error'
|
||||
})
|
||||
const statusStr = computed(() => {
|
||||
return status.value ? '正常运行中' : '已停止运行'
|
||||
})
|
||||
|
||||
const extensionColumns: any = [
|
||||
{
|
||||
title: '拓展名',
|
||||
key: 'name',
|
||||
minWidth: 250,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
key: 'description',
|
||||
resizable: true,
|
||||
minWidth: 250,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 240,
|
||||
align: 'center',
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
!row.installed
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
secondary: true,
|
||||
onClick: () => handleInstallExtension(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => '安装',
|
||||
icon: renderIcon('material-symbols:download-rounded', { size: 14 })
|
||||
}
|
||||
)
|
||||
: null,
|
||||
row.installed
|
||||
? h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleUninstallExtension(row.slug)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定卸载' + row.name + '吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error'
|
||||
},
|
||||
{
|
||||
default: () => '删除',
|
||||
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
: null
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const loadColumns: any = [
|
||||
{
|
||||
title: '属性',
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const extensions = ref<any[]>([])
|
||||
const load = ref<any[]>([])
|
||||
|
||||
const getLoad = async () => {
|
||||
const { data } = await php.load(version)
|
||||
return data
|
||||
}
|
||||
|
||||
const getExtensions = async () => {
|
||||
const { data } = await php.extensions(version)
|
||||
return data
|
||||
}
|
||||
|
||||
const getStatus = async () => {
|
||||
await systemctl.status(`php-fpm-${version}`).then((res: any) => {
|
||||
status.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getIsEnabled = async () => {
|
||||
await systemctl.isEnabled(`php-fpm-${version}`).then((res: any) => {
|
||||
isEnabled.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getErrorLog = async () => {
|
||||
php.errorLog(version).then((res: any) => {
|
||||
errorLog.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getSlowLog = async () => {
|
||||
php.slowLog(version).then((res: any) => {
|
||||
slowLog.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getConfig = async () => {
|
||||
php.config(version).then((res: any) => {
|
||||
config.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const getFPMConfig = async () => {
|
||||
php.fpmConfig(version).then((res: any) => {
|
||||
fpmConfig.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveConfig = async () => {
|
||||
await php.saveConfig(version, config.value)
|
||||
window.$message.success('保存成功')
|
||||
}
|
||||
|
||||
const handleSaveFPMConfig = async () => {
|
||||
await php.saveFPMConfig(version, fpmConfig.value)
|
||||
window.$message.success('保存成功')
|
||||
await getFPMConfig()
|
||||
}
|
||||
|
||||
const handleClearErrorLog = async () => {
|
||||
await php.clearErrorLog(version)
|
||||
window.$message.success('清空成功')
|
||||
}
|
||||
|
||||
const handleClearSlowLog = async () => {
|
||||
await php.clearSlowLog(version)
|
||||
window.$message.success('清空成功')
|
||||
}
|
||||
|
||||
const handleIsEnabled = async () => {
|
||||
if (isEnabled.value) {
|
||||
await systemctl.enable(`php-fpm-${version}`)
|
||||
window.$message.success('开启自启动成功')
|
||||
} else {
|
||||
await systemctl.disable(`php-fpm-${version}`)
|
||||
window.$message.success('禁用自启动成功')
|
||||
}
|
||||
await getIsEnabled()
|
||||
}
|
||||
|
||||
const handleStart = async () => {
|
||||
await systemctl.start(`php-fpm-${version}`)
|
||||
window.$message.success('启动成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleStop = async () => {
|
||||
await systemctl.stop(`php-fpm-${version}`)
|
||||
window.$message.success('停止成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleRestart = async () => {
|
||||
await systemctl.restart(`php-fpm-${version}`)
|
||||
window.$message.success('重启成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleReload = async () => {
|
||||
await systemctl.reload(`php-fpm-${version}`)
|
||||
window.$message.success('重载成功')
|
||||
await getStatus()
|
||||
}
|
||||
|
||||
const handleInstallExtension = async (slug: string) => {
|
||||
await php.installExtension(version, slug).then(() => {
|
||||
window.$message.success('任务已提交,请稍后查看任务进度')
|
||||
})
|
||||
}
|
||||
|
||||
const handleUninstallExtension = async (name: string) => {
|
||||
await php.uninstallExtension(version, name).then(() => {
|
||||
window.$message.success('任务已提交,请稍后查看任务进度')
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getStatus()
|
||||
getIsEnabled()
|
||||
getExtensions().then((res) => {
|
||||
extensions.value = res
|
||||
})
|
||||
getLoad().then((res) => {
|
||||
load.value = res
|
||||
})
|
||||
getErrorLog()
|
||||
getSlowLog()
|
||||
getConfig()
|
||||
getFPMConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<template #action>
|
||||
<n-button
|
||||
v-if="currentTab == 'config'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleSaveConfig"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:save-outline" />
|
||||
保存
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'fpm-config'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleSaveFPMConfig"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:save-outline" />
|
||||
保存
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'error-log'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleClearErrorLog"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:delete-outline" />
|
||||
清空错误日志
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="currentTab == 'slow-log'"
|
||||
class="ml-16"
|
||||
type="primary"
|
||||
@click="handleClearSlowLog"
|
||||
>
|
||||
<TheIcon :size="18" icon="material-symbols:delete-outline" />
|
||||
清空慢日志
|
||||
</n-button>
|
||||
</template>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" tab="运行状态">
|
||||
<n-space vertical>
|
||||
<n-card title="运行状态" rounded-10>
|
||||
<template #header-extra>
|
||||
<n-switch v-model:value="isEnabled" @update:value="handleIsEnabled">
|
||||
<template #checked> 自启动开 </template>
|
||||
<template #unchecked> 自启动关 </template>
|
||||
</n-switch>
|
||||
</template>
|
||||
<n-space vertical>
|
||||
<n-alert :type="statusType">
|
||||
{{ statusStr }}
|
||||
</n-alert>
|
||||
<n-space>
|
||||
<n-button type="success" @click="handleStart">
|
||||
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
|
||||
启动
|
||||
</n-button>
|
||||
<n-popconfirm @positive-click="handleStop">
|
||||
<template #trigger>
|
||||
<n-button type="error">
|
||||
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
|
||||
停止
|
||||
</n-button>
|
||||
</template>
|
||||
停止 PHP {{ version }} 会导致使用 PHP {{ version }} 的网站无法访问,确定要停止吗?
|
||||
</n-popconfirm>
|
||||
<n-button type="warning" @click="handleRestart">
|
||||
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
|
||||
重启
|
||||
</n-button>
|
||||
<n-button type="primary" @click="handleReload">
|
||||
<TheIcon :size="20" icon="material-symbols:refresh-rounded" />
|
||||
重载
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="extensions" tab="拓展管理">
|
||||
<n-card title="拓展列表" :segmented="true" rounded-10>
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1000"
|
||||
:loading="false"
|
||||
:columns="extensionColumns"
|
||||
:data="extensions"
|
||||
:row-key="(row: any) => row.slug"
|
||||
/>
|
||||
</n-card>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" tab="主配置">
|
||||
<n-space vertical>
|
||||
<n-alert type="warning">
|
||||
此处修改的是 PHP {{ version }} 主配置文件,如果您不了解各参数的含义,请不要随意修改!
|
||||
</n-alert>
|
||||
<Editor
|
||||
v-model:value="config"
|
||||
language="ini"
|
||||
theme="vs-dark"
|
||||
height="60vh"
|
||||
mt-8
|
||||
:options="{
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true
|
||||
}"
|
||||
/>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="fpm-config" tab="FPM 配置">
|
||||
<n-space vertical>
|
||||
<n-alert type="warning">
|
||||
此处修改的是 PHP {{ version }} FPM 配置文件,如果您不了解各参数的含义,请不要随意修改!
|
||||
</n-alert>
|
||||
<Editor
|
||||
v-model:value="fpmConfig"
|
||||
language="ini"
|
||||
theme="vs-dark"
|
||||
height="60vh"
|
||||
mt-8
|
||||
:options="{
|
||||
automaticLayout: true,
|
||||
formatOnType: true,
|
||||
formatOnPaste: true
|
||||
}"
|
||||
/>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" tab="负载状态">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="error-log" tab="错误日志">
|
||||
<realtime-log :path="errorLog" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="slow-log" tab="慢日志">
|
||||
<realtime-log :path="slowLog" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
<php-view :version="83" />
|
||||
</template>
|
||||
|
||||
@@ -330,11 +330,7 @@ onMounted(() => {
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block @click="handleAddUser">提交</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-button type="info" block @click="handleAddUser">提交</n-button>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="changePasswordModal">
|
||||
@@ -354,11 +350,7 @@ onMounted(() => {
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block @click="handleChangePassword">提交</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-button type="info" block @click="handleChangePassword">提交</n-button>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
@@ -410,11 +410,7 @@ onMounted(() => {
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block @click="handleModelAdd">提交</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-button type="info" block @click="handleModelAdd">提交</n-button>
|
||||
</n-modal>
|
||||
<n-modal
|
||||
v-model:show="editModuleModal"
|
||||
|
||||
@@ -180,11 +180,7 @@ onMounted(() => {
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block @click="handleAddMount">提交</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-button type="info" block @click="handleAddMount">提交</n-button>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
@@ -511,11 +511,7 @@ onUnmounted(() => {
|
||||
<n-input-number v-model:value="createProcessModel.num" :min="1" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block @click="handleCreateProcess">提交</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-button type="info" block @click="handleCreateProcess">提交</n-button>
|
||||
</n-modal>
|
||||
<realtime-log-modal v-model:show="processLogModal" :path="processLog" />
|
||||
<n-modal
|
||||
|
||||
@@ -76,10 +76,6 @@ const handleCreate = () => {
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block @click="handleCreate">提交</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-button type="info" block @click="handleCreate">提交</n-button>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
@@ -182,11 +182,7 @@ onUnmounted(() => {
|
||||
<n-input v-model:value="restoreModel.target" type="text" @keydown.enter.prevent />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block @click="handleRestore">提交</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-button type="info" block @click="handleRestore">提交</n-button>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -13,8 +13,14 @@ import cert from '@/api/panel/cert'
|
||||
import type { Account } from '@/views/cert/types'
|
||||
|
||||
const props = defineProps({
|
||||
caProviders: Array<any>,
|
||||
algorithms: Array<any>
|
||||
caProviders: {
|
||||
type: Array<any>,
|
||||
required: true
|
||||
},
|
||||
algorithms: {
|
||||
type: Array<any>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const { caProviders, algorithms } = toRefs(props)
|
||||
@@ -54,7 +60,7 @@ const columns: any = [
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return caProviders?.value?.find((item: any) => item.value === row.ca)?.label
|
||||
return caProviders.value?.find((item: any) => item.value === row.ca)?.label
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user