Compare commits

...

88 Commits

Author SHA1 Message Date
耗子 da44f1a8fe feat: 发布v2.3.19 2024-11-12 02:50:24 +08:00
耗子 0317aa1401 feat: redis添加清空 2024-11-12 02:46:54 +08:00
renovate[bot] 90fc48f478 chore(deps): Update dependency vite to v5.4.11 (#354)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 14:21:13 +00:00
renovate[bot] a8178cbcd6 chore(deps): Update all dependencies (#353)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 15:12:12 +08:00
耗子 91ecd04c27 feat: 优化登录中间件使用白名单 2024-11-11 14:28:00 +08:00
耗子 6743ac9621 fix: 防止首页报错 2024-11-11 14:19:36 +08:00
耗子 5606d980be fix: cli下修改端口后自动放行 2024-11-11 14:13:18 +08:00
renovate[bot] 34300431a4 chore(deps): Lock file maintenance (#352)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 08:25:44 +08:00
耗子 6f0f4c69ef feat: 初步完成redis操作方法 2024-11-11 02:22:23 +08:00
耗子 5fba45e3a4 feat: 初步完成redis操作方法 2024-11-11 02:22:17 +08:00
耗子 bb140ade7b feat: 优化app路由加载 2024-11-11 01:07:37 +08:00
耗子 1316c50a57 feat: php支持设置为cli默认版本 2024-11-11 01:04:09 +08:00
耗子 f14b4f5c54 Merge remote-tracking branch 'origin/main' 2024-11-11 00:47:48 +08:00
耗子 b08eb6bf1f feat: 优化目录输入自动清理多余的斜杠和空格 2024-11-11 00:47:09 +08:00
renovate[bot] 7f5869f805 chore(deps): Update dependency @vue/tsconfig to ^0.6.0 (#351)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 00:38:26 +08:00
耗子 ecadaa9bbc fix: lint 2024-11-10 18:37:44 +08:00
耗子 60b9350625 feat: 优化删除和写入支持自动去除ia属性 2024-11-10 18:25:01 +08:00
耗子 d64f099f8e feat: 发布v2.3.18 2024-11-10 08:37:38 +08:00
耗子 294a78cd95 feat: 优化删除检查 2024-11-10 03:26:08 +08:00
renovate[bot] 8294a1a74e chore(deps): Update module github.com/urfave/cli/v3 to v3.0.0-alpha9.3 (#349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 02:28:09 +08:00
renovate[bot] c403efd762 chore(deps): Update dependency @iconify/json to v2.2.270 (#348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-09 18:02:04 +08:00
耗子 cf8dfca4c0 Merge remote-tracking branch 'origin/main' 2024-11-09 18:00:15 +08:00
耗子 e552caa38f feat: 修改ip获取地址 2024-11-09 18:00:09 +08:00
renovate[bot] ddff13990a chore(deps): Update dependency marked to v15 (#347)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-09 06:58:36 +00:00
renovate[bot] 27f79ea7a3 chore(deps): Update dependency pinia-plugin-persistedstate to v4.1.3 (#346)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-09 01:32:12 +00:00
耗子 04d570197a feat: 添加redis驱动 2024-11-09 02:41:47 +08:00
renovate[bot] 8e74428023 chore(deps): Update all dependencies (#345)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-09 01:10:12 +08:00
renovate[bot] f4c09324b7 chore(deps): Update dependency md-editor-v3 to v4.21.3 (#344)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 11:08:41 +00:00
renovate[bot] ee6a12fac2 chore(deps): Update module golang.org/x/crypto to v0.29.0 (#343)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 03:10:34 +00:00
renovate[bot] ba429068ad chore(deps): Update dependency marked to v14.1.4 (#342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-07 21:48:01 +00:00
renovate[bot] 711ccb4504 chore(deps): Update dependency @iconify/json to v2.2.269 (#341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-07 19:09:13 +08:00
耗子 7d6e078e20 chore: 添加新合作 2024-11-07 16:58:49 +08:00
耗子 d3a7c9e29c feat: 数据库密码可以为空(本地认证) 2024-11-07 02:37:29 +08:00
耗子 10d22bfbc0 feat: 初始化数据库服务 2024-11-07 02:27:17 +08:00
耗子 724d05d980 feat: 优化数据库模型 2024-11-07 02:13:24 +08:00
耗子 690e7f0391 feat: 重命名安全为数据库 2024-11-07 01:12:38 +08:00
耗子 a28fc592d1 feat: 优化数据库模型 2024-11-07 00:52:12 +08:00
耗子 ff7e76ac2b feat: 初始化数据库条目模型 2024-11-07 00:49:11 +08:00
耗子 f2eb3a180c feat: 对部分密码进行加密 2024-11-07 00:25:09 +08:00
renovate[bot] 020a4ad870 chore(deps): Update all dependencies (#340)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-06 22:16:57 +08:00
renovate[bot] b5e7cfc069 chore(deps): Update all dependencies (#339)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 21:31:06 +08:00
renovate[bot] 52f8a6f045 chore(deps): Update dependency @types/node to v22.9.0 (#338)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 05:13:50 +00:00
耗子 3831d47a23 feat(文件): 路径切换后清空已选择 2024-11-05 02:57:30 +08:00
耗子 3a579f2346 feat: 运行等待任务前标记先前运行中的任务为失败 2024-11-04 17:59:13 +08:00
耗子 e85e8dffb3 fix: lint 2024-11-04 17:48:46 +08:00
耗子 e18d40b0ab feat: 防止任务重复提交 2024-11-04 17:47:39 +08:00
renovate[bot] c308b8e42c chore(deps): Update all dependencies (#336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 12:40:20 +08:00
renovate[bot] cafded526f chore(deps): Lock file maintenance (#335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 04:24:39 +00:00
renovate[bot] 99aa6c99d8 chore(deps): Update dependency pinia to v2.2.6 (#334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 01:11:37 +00:00
耗子 e0daee4766 feat: 移动部分方法到utils 2024-11-04 03:04:34 +08:00
耗子 bf8c832b32 feat: 去掉重复的方法 2024-11-04 02:33:15 +08:00
耗子 2d35cbc176 fix: lint 2024-11-03 16:46:05 +08:00
renovate[bot] 6b51f58856 chore(deps): Update dependency @types/node to v22.8.7 (#330)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-03 07:25:33 +00:00
renovate[bot] f58c680ced chore(deps): Update dependency @iconify/json to v2.2.267 (#329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 19:49:40 +00:00
耗子 9c6d0b5837 feat: php拓展安装添加确认弹窗 2024-11-02 21:28:29 +08:00
耗子 d069cbec75 feat: 负载条不允许超过100% 2024-11-02 21:22:35 +08:00
耗子 9b252aa8f6 docs: 更新文档 2024-11-02 20:02:43 +08:00
耗子 a52d06e3cb feat: 添加memcached php拓展 2024-11-02 19:26:38 +08:00
耗子 c25c78bfef feat: 发布v2.3.17 2024-11-02 18:32:54 +08:00
耗子 567f3ab693 fix: lint 2024-11-02 18:18:29 +08:00
耗子 aa162e12db feat: 支持memcached 2024-11-02 17:48:52 +08:00
renovate[bot] 7169b90c1d chore(deps): Update dependency sass to v1.80.6 (#328)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 03:07:42 +00:00
耗子 5e1001f10f docs: update README 2024-11-02 03:36:33 +08:00
耗子 8e26589c80 feat: 提升最低TLS到1.3 2024-11-02 03:34:21 +08:00
耗子 27ec65650d feat: 替换go默认的logger 2024-11-02 01:12:17 +08:00
耗子 e2345a8285 workflow: 合并codecov与test 2024-11-02 00:43:01 +08:00
耗子 4ebe20d7f8 workflow: issue lock修改为手动触发 2024-11-02 00:37:02 +08:00
renovate[bot] 8f5872b284 chore(deps): Update dependency alova to v3.2.0 (#327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 00:34:24 +08:00
耗子 75c6522453 fix: lint 2024-11-01 23:07:51 +08:00
耗子 ff34cc72cc fix: lint 2024-11-01 21:54:06 +08:00
耗子 e2fb1080da Merge remote-tracking branch 'origin/main' 2024-11-01 21:42:27 +08:00
耗子 b96c6ad899 feat: php支持74和80 2024-11-01 21:42:20 +08:00
renovate[bot] c1a4fb0904 chore(deps): Update all dependencies (#326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-01 09:46:35 +08:00
renovate[bot] 5c50da27c5 chore(deps): Update dependency @vue-js-cron/naive-ui to v2.0.7 (#325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 18:48:45 +00:00
耗子 336a9bc675 fix: 文件管理bug 2024-10-31 21:49:58 +08:00
耗子 ea3bf37687 fix: 7z list 2024-10-31 18:38:03 +08:00
耗子 79e09f9cb7 chore: debug 2024-10-31 18:32:08 +08:00
耗子 d73c988073 chore: debug 2024-10-31 18:32:01 +08:00
耗子 9641ae7f34 chore: debug 2024-10-31 18:31:56 +08:00
耗子 fd2c89a480 chore: debug 2024-10-31 18:27:01 +08:00
耗子 526f192200 feat(文件): 解压时只设置压缩包内文件的权限 2024-10-31 18:20:54 +08:00
renovate[bot] 6423953aeb chore(deps): Update dependency @types/node to v22.8.5 (#324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 17:07:19 +08:00
耗子 1091ec8a23 Merge remote-tracking branch 'origin/main' 2024-10-31 12:35:38 +08:00
耗子 2cbc641b2d feat(文件): 优化解压,保存时的权限设置 2024-10-31 12:35:31 +08:00
renovate[bot] f4d23211e3 chore(deps): Update dependency @vueuse/core to v11.2.0 (#323)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 01:16:52 +00:00
耗子 1b6086bb0b docs: 更新bug模板 2024-10-31 04:11:13 +08:00
耗子 f399e9fcec fix: 应用更新按钮错位 2024-10-31 04:11:03 +08:00
renovate[bot] 17cf1ada0e chore(deps): Update all dependencies (#321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 03:43:36 +08:00
130 changed files with 3424 additions and 2834 deletions
+6 -7
View File
@@ -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

-26
View File
@@ -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
View File
@@ -3,9 +3,6 @@ on:
schedule:
- cron: "0 */6 * * *"
workflow_dispatch:
push:
branches:
- main
issues:
types: [ opened ]
permissions:
+6 -1
View File
@@ -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 }}
+29 -17
View File
@@ -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
View File
@@ -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
+16 -13
View File
@@ -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
+40 -34
View File
@@ -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=
+1
View File
@@ -37,6 +37,7 @@ const (
// 面板全局变量
var (
Key string
Root string
Version string
Locale string
+1 -1
View File
@@ -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 {
+1
View File
@@ -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"
+20
View File
@@ -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)
},
})
}
+5
View File
@@ -0,0 +1,5 @@
package memcached
type UpdateConfig struct {
Config string `form:"config" json:"config" validate:"required"`
}
+97
View File
@@ -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)
}
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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
View File
@@ -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)
},
})
}
}
+53 -51
View File
@@ -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: "(需先安装PostgreSQLpdo_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 扩展。",
})
}
+1 -1
View File
@@ -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"`
}
+1 -1
View File
@@ -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"
)
+5 -5
View File
@@ -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
View File
@@ -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
}
+72
View File
@@ -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
}
-4
View File
@@ -1,4 +0,0 @@
package biz
type FirewallRepo interface {
}
+41
View File
@@ -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)
+6 -1
View File
@@ -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")
// 初始化时区
+1 -3
View File
@@ -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")
+1
View File
@@ -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)
}
+1 -1
View File
@@ -8,6 +8,6 @@ import (
)
func initQueue() {
app.Queue = queue.New(40)
app.Queue = queue.New(100)
go app.Queue.Run(context.Background())
}
+1 -1
View File
@@ -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,
+5 -5
View File
@@ -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
}
}
+8 -8
View File
@@ -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)
}
+4 -4
View File
@@ -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
}
+1 -1
View File
@@ -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 删除网络
+3 -3
View File
@@ -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 删除存储卷
+3 -2
View File
@@ -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())
}
+66
View File
@@ -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
}
+115
View File
@@ -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
}
+1 -1
View File
@@ -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
View File
@@ -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
}
}
+2 -1
View File
@@ -24,8 +24,9 @@ func GlobalMiddleware() []func(http.Handler) http.Handler {
LogRequestHeaders: []string{"User-Agent"},
}),
middleware.Recoverer,
Entrance,
Status,
Entrance,
MustLogin,
MustInstall,
}
}
+16
View File
@@ -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)
+17
View File
@@ -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"`
}
+22
View File
@@ -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"`
}
+1 -7
View File
@@ -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"`
+16 -2
View File
@@ -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{},
)
},
})
}
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+3 -3
View File
@@ -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
+85
View File
@@ -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)
}
+85
View File
@@ -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
View File
@@ -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),
+24 -6
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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
})
+121
View File
@@ -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
}
-18
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -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
}
+25
View File
@@ -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
View File
@@ -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
}
+54
View File
@@ -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))
+1
View File
@@ -12,6 +12,7 @@ import (
// Remove 删除文件/目录
func Remove(path string) error {
_, _ = shell.Execf("chattr -R -ia '%s'", path)
return os.RemoveAll(path)
}
+17 -1
View File
@@ -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")
-30
View File
@@ -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
}
-87
View File
@@ -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)
}
-47
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+651 -615
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -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 })
+5 -2
View File
@@ -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`),
+4 -5
View File
@@ -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 }),
+11 -5
View File
@@ -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="{
+4 -1
View File
@@ -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('')
+1 -1
View File
@@ -280,7 +280,7 @@ const checkName = (name: string) => {
}
const checkPath = (path: string) => {
return /^(?!\/)(?!.*\/$)(?!.*\/\/)(?!.*\s).*$/.test(path)
return /^(?!\/)(?!.*\/$)(?!.*\/\/).*$/.test(path)
}
const getFilename = (path: string) => {
+93 -87
View File
@@ -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
]
}
)
}
}
]
+9 -13
View File
@@ -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>
+180
View File
@@ -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>
+23
View File
@@ -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
+420
View File
@@ -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>
+11
View File
@@ -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>
+23
View File
@@ -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
+11
View File
@@ -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>
+23
View File
@@ -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
+3 -389
View File
@@ -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>
+3 -389
View File
@@ -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>
+3 -389
View File
@@ -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>
+2 -10
View File
@@ -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>
+1 -5
View File
@@ -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"
+1 -5
View File
@@ -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>
+1 -5
View File
@@ -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
+1 -5
View File
@@ -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>
+1 -5
View File
@@ -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>
+9 -3
View File
@@ -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