Compare commits

...

147 Commits

Author SHA1 Message Date
耗子 81843470b7 feat: 发布v2.3.11 2024-10-23 03:13:44 +08:00
renovate[bot] a539532ae2 chore(deps): Update unocss monorepo to v0.63.6 (#288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 03:12:40 +08:00
耗子 fc1584e7da fix: 罕见情况下防火墙规则错误 2024-10-23 03:05:40 +08:00
耗子 3c72a66c1f feat: ssh多机管理 2024-10-23 02:39:43 +08:00
renovate[bot] 346ec7ab1f chore(deps): Update dependency @types/node to v20.16.14 (#287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-22 07:07:18 +00:00
renovate[bot] 7850b74966 chore(deps): Update dependency npm-run-all2 to v7 (#286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-22 10:17:28 +08:00
耗子 f2d2bad52f feat: 初始化ssh模型 2024-10-22 02:59:53 +08:00
耗子 0786451c79 docs: 添加微信群 2024-10-22 02:52:56 +08:00
renovate[bot] 0291ae7637 chore(deps): Lock file maintenance (#283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 16:01:50 +08:00
renovate[bot] de76376e97 chore(deps): Update dependency md-editor-v3 to v4.21.1 (#282)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 16:01:26 +08:00
耗子 de21179d61 fix: lint 2024-10-21 11:56:53 +08:00
renovate[bot] b81f3de01d chore(deps): Lock file maintenance (#281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 11:55:54 +08:00
renovate[bot] be3bae7d57 chore(deps): Update module github.com/klauspost/compress to v1.17.11 (#278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:39:07 +08:00
renovate[bot] ffb4e21f48 chore(deps): Update dependency eslint-plugin-vue to v9.29.1 (#279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:38:58 +08:00
renovate[bot] 39624d8e1c chore(deps): Update dependency @iconify/json to v2.2.262 (#280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:38:48 +08:00
耗子 1378413e54 fix: lint 2024-10-21 01:35:32 +08:00
耗子 730aea037c feat: 应用支持实时日志 2024-10-21 01:33:13 +08:00
耗子 5ae6135429 fix: 实时日志优化 2024-10-21 01:16:09 +08:00
耗子 84703391a3 feat: 支持实时日志流 2024-10-21 01:08:10 +08:00
耗子 354705ec5b fix: cron添加脚本不保存 2024-10-21 01:07:37 +08:00
耗子 f182eab2dc fix: test 2024-10-20 22:29:25 +08:00
耗子 2828795fb0 fix: test 2024-10-20 22:21:31 +08:00
耗子 e6aad21631 fix: lint 2024-10-20 22:15:23 +08:00
耗子 ff239c467a refactor: 面板websocket 2024-10-20 22:14:11 +08:00
耗子 892f91be3e feat: 开发服务器代理支持多目标 2024-10-20 19:55:14 +08:00
耗子 d51d11f460 feat: 文件远程下载 2024-10-20 19:20:39 +08:00
耗子 f9233bd36b feat: 文件搜索 2024-10-20 18:51:05 +08:00
耗子 a2ebc070ae feat: 文件管理优化 2024-10-20 17:28:02 +08:00
耗子 480a6a829d feat: 终端支持字体缩放 2024-10-20 05:14:48 +08:00
耗子 47b92a8b2a refactor: 重写ssh 2024-10-20 04:38:27 +08:00
耗子 1a7f679fca feat: 发布v2.3.10 2024-10-20 01:47:00 +08:00
耗子 8b6b94ea1f feat: 跑分插件优化 2024-10-20 01:46:25 +08:00
耗子 0d9c8b3a2d feat: 跑分插件 2024-10-20 01:34:22 +08:00
耗子 2be97a4543 feat: 优化颜色选择器 2024-10-19 21:05:25 +08:00
renovate[bot] 7b6b86f0f6 chore(deps): Update dependency @types/node to v20.16.13 (#277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 21:04:55 +08:00
耗子 6f82242d92 feat: 颜色选择器 2024-10-19 21:01:46 +08:00
耗子 7f301cec58 feat: 组件支持固定 2024-10-19 20:47:33 +08:00
耗子 c75cfe9eb6 feat: 前端路由固定 2024-10-19 20:18:23 +08:00
耗子 b94664ed87 Merge remote-tracking branch 'origin/main' 2024-10-19 18:01:26 +08:00
耗子 93fc5e08f6 feat: 前端优化 2024-10-19 18:01:18 +08:00
renovate[bot] 9bcfd4cd73 chore(deps): Update dependency sass to v1.80.3 (#276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 15:35:39 +08:00
耗子 2d10110eef fix: test 2024-10-19 15:01:44 +08:00
耗子 78e31fdc40 chore: 添加aff 2024-10-19 14:41:47 +08:00
耗子 d2b1394950 feat(HTTPS): 证书支持华为云 2024-10-19 05:14:00 +08:00
耗子 389b0a005b feat(HTTPS): 优化证书申请 2024-10-19 03:03:38 +08:00
renovate[bot] 0b1554c2c6 chore(deps): Update dependency npm-run-all2 to v6.2.4 (#274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 17:17:39 +08:00
renovate[bot] 3b580e2827 chore(deps): Update dependency sass to v1.80.2 (#273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 08:48:22 +08:00
耗子 595f5ba2d9 fix: tests 2024-10-18 02:28:08 +08:00
耗子 78543dac67 feat: 发布v2.3.9 2024-10-18 02:11:58 +08:00
耗子 e9cbc8e945 refactor: 重构防火墙 2024-10-18 02:08:55 +08:00
耗子 905e206f9c fix: test 2024-10-17 23:27:11 +08:00
耗子 b5b1992e3b fix: build 2024-10-17 23:05:38 +08:00
耗子 a7fe0c87a5 feat: 首页资源总览优化 2024-10-17 22:46:49 +08:00
耗子 5dcbac1c93 feat(工具箱): 支持设置主机名和时间 2024-10-17 22:17:20 +08:00
耗子 67a6d5cea8 feat: 添加应用图标 2024-10-17 21:22:26 +08:00
耗子 f6fec06e82 feat: 添加应用图标 2024-10-17 21:20:26 +08:00
耗子 2d74e8966e fix: 优化表格样式 2024-10-17 20:30:56 +08:00
耗子 065877547e fix: 固定按钮防止错位 2024-10-17 20:17:06 +08:00
耗子 12b6a1af35 feat: 优化首页布局 2024-10-17 18:55:08 +08:00
耗子 b36f7f3085 feat: 添加密码复杂度验证 2024-10-17 18:40:27 +08:00
耗子 2112e86329 fix: 修复报错Slug不存在 2024-10-17 18:02:48 +08:00
耗子 f2c3569447 fix: 修改部分端口报错 2024-10-17 17:54:33 +08:00
耗子 1a9fe40b80 feat: cli支持更新应用 2024-10-17 17:32:38 +08:00
耗子 df021819b4 feat: 大幅优化移动端体验 2024-10-17 17:18:54 +08:00
耗子 57749f03b9 fix: 防火墙创建规则后未关闭窗口 2024-10-17 16:30:06 +08:00
耗子 d97697efde feat: 发布v2.3.8 2024-10-17 16:25:14 +08:00
耗子 bc28719b10 fix: 修改端口报错 2024-10-17 16:22:01 +08:00
耗子 db79d17919 feat: 支持openEuler 2024-10-17 16:19:22 +08:00
耗子 4e6654af33 fix: 华为欧拉检查 2024-10-17 13:51:41 +08:00
耗子 4015c7e1bc Merge remote-tracking branch 'origin/main' 2024-10-17 13:20:30 +08:00
耗子 d30874a5d4 fix: opencloudos检查 2024-10-17 13:20:24 +08:00
renovate[bot] f4a97c1740 chore(deps): Update dependency sass to v1.80.1 (#272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 12:49:54 +08:00
耗子 7cb497929b fix: cli下安装应用 2024-10-17 12:39:12 +08:00
耗子 8cb74159f2 feat: 发布v2.3.7 2024-10-17 12:28:37 +08:00
renovate[bot] 3b75f7d2ac chore(deps): Update dependency sass to v1.80.0 (#271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 12:15:24 +08:00
耗子 87c65176aa Merge remote-tracking branch 'origin/main' 2024-10-17 12:09:06 +08:00
耗子 3b3cb1ff24 fix: CLI下不运行队列分发 2024-10-17 12:09:00 +08:00
renovate[bot] a701ebcad6 chore(deps): Update dependency @types/node to v20.16.12 (#270)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 03:18:31 +00:00
renovate[bot] 3d88983feb chore(deps): Update dependency @iconify/json to v2.2.261 (#268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 01:37:10 +00:00
耗子 39a9bad2e1 fix: 去掉翻译 2024-10-17 04:45:30 +08:00
耗子 7b0e98bf02 feat: 防火墙完善 2024-10-17 04:44:58 +08:00
耗子 324c61625d feat: 防火墙后端升级 2024-10-17 02:05:27 +08:00
耗子 b50a3f4b4e fix: 首页监控默认10条 2024-10-17 01:13:28 +08:00
耗子 3de3b40d88 feat: 首页全新设计 2024-10-17 00:50:43 +08:00
耗子 722c4f3812 feat: 首页全新设计 2024-10-16 22:54:57 +08:00
耗子 9cf3a0e2b6 fix: 防火墙删不掉 2024-10-16 21:51:37 +08:00
耗子 c80c22c133 refactor: 仪表盘实时数据接口 2024-10-16 17:04:34 +08:00
耗子 5acf1e6eb7 feat: 修改面板端口自动放行 2024-10-16 15:22:47 +08:00
耗子 cdeaf9f48f feat: 完善验证器 2024-10-16 04:09:59 +08:00
耗子 afebeb7c00 feat: 发布v2.3.6 2024-10-16 02:33:43 +08:00
耗子 0d73f2919d workflow: update auto-close 2024-10-16 02:22:12 +08:00
耗子 070ad97e95 workflow: 添加回修订的auto-close 2024-10-16 01:52:52 +08:00
耗子 b7eba92b2b Revert "workflow: 暂时移除auto-close威力太大了"
This reverts commit 2c339b9923.
2024-10-16 01:47:42 +08:00
耗子 2c339b9923 workflow: 暂时移除auto-close威力太大了 2024-10-16 01:28:16 +08:00
耗子 8d0ea50d4f workflow: 添加自动关闭issue 2024-10-16 01:19:44 +08:00
耗子 d6a1c13328 workflow: 添加自动关闭issue 2024-10-16 01:02:37 +08:00
耗子 789e6e2043 docs: 优化描述 2024-10-15 23:55:38 +08:00
耗子 d692b9ac13 docs: 添加自动挂载脚本说明 2024-10-15 23:55:15 +08:00
耗子 eb362e42e3 fix: #260 2024-10-15 18:07:31 +08:00
耗子 3ca106ec87 refactor: 重构证书目录为cert 2024-10-15 18:05:14 +08:00
耗子 377baa0783 fix: build 2024-10-15 17:52:34 +08:00
耗子 117ccabbb5 feat: 优化默认分页条数 2024-10-15 17:51:25 +08:00
耗子 b718c11f3a feat: 离线模式 2024-10-15 17:44:41 +08:00
耗子 41d3d3fb97 feat: 重启后自动调度等待中的任务 2024-10-15 17:18:28 +08:00
耗子 31ad944da8 Merge remote-tracking branch 'origin/main' 2024-10-15 16:53:52 +08:00
耗子 a25de6e4db feat: 仅在cli中打印信息 2024-10-15 16:53:42 +08:00
renovate[bot] 3af6d4d47d chore(deps): Update dependency marked to v14.1.3 (#259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 16:01:11 +08:00
耗子 59a2fef9e8 chore: 优化描述 2024-10-15 11:35:41 +08:00
耗子 3d22a1bc03 chore: 优化描述 2024-10-15 10:11:14 +08:00
耗子 e64c27a821 chore: 添加新赞助商 2024-10-15 10:09:22 +08:00
耗子 f2bdba10bb chore: 添加新赞助商 2024-10-15 10:08:12 +08:00
耗子 e18d07e694 chore: 添加新赞助商 2024-10-15 10:06:05 +08:00
耗子 50ba0ce38d feat: 优化修复流程 2024-10-15 03:46:56 +08:00
耗子 8b2c7ea778 feat: 优化备份信息终端输出 2024-10-15 03:38:52 +08:00
耗子 e640a3cedd feat: 添加更新日期 2024-10-15 03:26:31 +08:00
耗子 31ab4f0fb6 feat: 发布v2.3.5 2024-10-15 03:24:27 +08:00
耗子 8dee17b539 Merge remote-tracking branch 'origin/main' 2024-10-15 03:18:15 +08:00
renovate[bot] af3172739e chore(deps): Update dependency vite to v5.4.9 (#258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 03:16:41 +08:00
耗子 182d9b1c8c feat: 添加面板修复命令 2024-10-15 03:14:38 +08:00
耗子 d03facb556 feat: 优化升级流程 2024-10-15 02:23:24 +08:00
耗子 577fb370a7 feat: 发布v2.3.4 2024-10-15 00:44:24 +08:00
耗子 7ef8fc6787 fix: lint 2024-10-15 00:36:41 +08:00
耗子 e5ba1375b1 feat: 网站管理优化 2024-10-15 00:29:12 +08:00
耗子 e8a01e2d04 refactor: 网站nginx配置解析生成 2024-10-14 21:34:24 +08:00
耗子 bdee27541c feat: nginx解析器完成 2024-10-14 19:07:48 +08:00
renovate[bot] f807da175d chore(deps): Lock file maintenance (#256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 15:13:19 +08:00
renovate[bot] ae29541d02 chore(deps): Update dependency @iconify/json to v2.2.260 (#257)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 15:12:49 +08:00
renovate[bot] 0f844ac3ea chore(deps): Update module github.com/urfave/cli/v3 to v3.0.0-alpha9.1 (#255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 15:12:35 +08:00
耗子 744d0b9617 fix: 跨文件系统文件移动优化 2024-10-14 13:03:16 +08:00
耗子 1bea44146f feat: 初步实现nginx配置解析器 2024-10-14 03:32:51 +08:00
耗子 a7a68f6b34 feat: 添加请求验证标签 2024-10-13 23:43:59 +08:00
耗子 80bfe50335 feat: 发布v2.3.3 2024-10-13 22:48:37 +08:00
耗子 f235492f8b feat: 数据备份前端 2024-10-13 22:44:42 +08:00
renovate[bot] e1bcabec5d chore(deps): Update module github.com/go-rat/chix to v1.1.4 (#252)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-13 13:19:04 +00:00
耗子 8a03006772 fix: lint 2024-10-13 17:58:00 +08:00
耗子 e497f5f7d7 fix: build 2024-10-13 17:46:03 +08:00
耗子 5668bc1afd refactor: 备份重构 2024-10-13 17:43:36 +08:00
耗子 bf9c0bc12e feat: 首页优化 2024-10-13 13:59:56 +08:00
耗子 7677c3b50e fix: 部分接口修改 2024-10-13 13:48:18 +08:00
耗子 82a74a538b fix(toolbox): swap setting 2024-10-13 12:19:51 +08:00
耗子 d84e3f75e3 docs: update system table 2024-10-13 04:22:43 +08:00
耗子 ce155af56b docs: update system table 2024-10-13 04:22:01 +08:00
耗子 ebc5fb9796 fix: php拓展安装 2024-10-13 04:01:35 +08:00
耗子 5e237a6bce docs: update system table 2024-10-13 03:35:54 +08:00
耗子 567663a6f5 docs: update system table 2024-10-13 03:13:51 +08:00
耗子 38935607d6 fix: restart panel after update 2024-10-13 01:41:21 +08:00
耗子 0af6b22862 fix: restart panel after update 2024-10-13 01:37:43 +08:00
308 changed files with 14738 additions and 5778 deletions
+5 -5
View File
@@ -27,7 +27,7 @@ body:
required: false
- label: 这个问题可以被稳定复现 (The problem can be stably reproduced)
required: false
- label: 问题是在升级之后产生的 (The problem is generated after upgrading)
- label: 问题是在更新之后产生的 (The problem is generated after upgrading)
required: false
- type: dropdown
id: system
@@ -68,7 +68,7 @@ body:
attributes:
label: 描述问题 (Describe The Problem)
description: |
简明概要地描述遇到的问题。
简明概要地描述遇到的问题。
Briefly describe the problem you are having.
validations:
required: true
@@ -91,7 +91,7 @@ body:
attributes:
label: 预期行为 (Expected Behavior)
description: |
简明概要地描述期望发生的事情。
简明概要地描述期望发生的事情。
Briefly describe what you expect to happen.
validations:
required: true
@@ -113,7 +113,7 @@ body:
attributes:
label: 截图 (Screenshots)
description: |
如果有,添加屏幕截图可帮助更快定位的问题。
如果有,添加屏幕截图可帮助更快定位的问题。
If so, adding screenshots can help locate your issue faster.
可以复制图片后在此区域内粘贴以添加图片。
Pictures can be copied and pasted in this area to add pictures.
@@ -128,7 +128,7 @@ body:
description: |
运行环境?浏览器?软件版本?相关的配置?链接?参考资料?
Environment? Browser? Software version? Related configuration? Link? References?
任何能让我们对所遇到的问题有更多了解的东西。
任何能让我们对所遇到的问题有更多了解的东西。
Anything that can give us more insight into the problem you're having.
validations:
required: false
+1 -1
View File
@@ -25,7 +25,7 @@ body:
attributes:
label: 描述功能 (Describe Feature)
description: |
简明概要地描述的新功能,以及它将解决什么问题。
简明概要地描述的新功能,以及它将解决什么问题。
Briefly describe your new feature and what problem it will solve.
validations:
required: true
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
- name: Install dependencies
run: go mod tidy
- name: Run tests with coverage
run: go test -v -coverprofile="coverage.out" ./...
run: sudo go test -v -coverprofile="coverage.out" ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v4
with:
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
# We need to run the dev server first to generate the auto-imports files
run: |
cp .env.production .env
cp settings/proxy-config.ts.example settings/proxy-config.ts
cp settings/proxy-config.example.ts settings/proxy-config.ts
pnpm dev &
sleep 5
kill %1
+25
View File
@@ -0,0 +1,25 @@
name: Issue Auto Lock
on:
schedule:
- cron: "0 */6 * * *"
workflow_dispatch:
push:
branches:
- main
issues:
types: [ opened ]
permissions:
issues: write
contents: read
jobs:
issue-lock:
runs-on: ubuntu-latest
steps:
- name: Set GH_REPO
run: echo "GH_REPO=${{ github.repository }}" >> $GITHUB_ENV
- name: Auto Lock Issue
uses: devhaozi/issue-auto-lock@v1
with:
gh_repo: ${{ github.repository }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
issue_labels: "⭐ No Star"
+1 -1
View File
@@ -21,4 +21,4 @@ jobs:
run: |
cp config.example.yml config.yml
- name: Run tests
run: go test ./...
run: sudo go test ./...
+55 -24
View File
@@ -18,7 +18,7 @@
耗子面板是使用 Golang + Vue 开发的开源轻量 Linux 服务器运维管理面板。
交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 论坛:[tom.moe](https://tom.moe) | 赞助:[爱发电](https://afdian.com/a/TheTNB)
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)
## 优势
@@ -26,40 +26,55 @@
2. **低破坏性:** 面板的设计理念是尽可能减少对系统的额外修改,在同类面板中,我们对系统的修改最少。
3. **追随时代:** 面板所有组件均走在时代前沿,更新快,功能强大,安全性有保障。
4. **高效运维:** 面板界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
5. **离线运行:** 面板运行可不依赖任何外部服务,甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
5. **离线运行:** 面板运行可不依赖任何外部服务,甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 2 年无事故。
7. **开源开放:** 面板开源,可以自由修改、审计面板源码,安全性有保障。
7. **开源开放:** 面板开源,可以自由修改、审计面板源码,安全性有保障。
## UI 截图
![UI 截图](ui.png)
![UI 截图](.github/assets/ui.png)
## 运行环境
耗子面板支持 `amd64` | `arm64` 架构下的主流系统。
耗子面板支持 `amd64` | `arm64` 架构下的主流系统,下表中的系统均已测试 LNMP 环境安装
优先建议使用 `AlmaLinux` | `RockyLinux`
优先建议使用标注**推荐**的系统,无特殊情况不建议使用标注**不推荐**的系统
不在下表中的其他系统,可自行尝试安装,但不保证能够正常运行,且不提供技术支持。
不在下表中的其他系统,可自行尝试安装,但不提供技术支持(接受相关 PR 提交)
| 系统 | 版本 |
|------------|----|
| RHEL | 9 |
| AlmaLinux | 9 |
| RockyLinux | 9 |
| Ubuntu | 22 |
| Ubuntu | 24 |
| Debian | 11 |
| Debian | 12 |
| 系统 | 版本 | 备注 |
|---------------------|-----|-----|
| AlmaLinux | 9 | 推荐 |
| AlmaLinux | 8 | 不推荐 |
| RockyLinux | 9 | 支持 |
| RockyLinux | 8 | 不推荐 |
| CentOS Stream | 9 | 不推荐 |
| CentOS Stream | 8 | 不推荐 |
| Ubuntu | 24 | 推荐 |
| Ubuntu | 22 | 支持 |
| Debian | 12 | 推荐 |
| Debian | 11 | 支持 |
| OpenCloudOS | 9 | 支持 |
| TencentOS Server | 4 | 支持 |
| TencentOS Server | 3.1 | 不推荐 |
| Alibaba Cloud Linux | 3.2 | 不推荐 |
| Anolis | 8 | 不推荐 |
| openEuler | 22 | 不推荐 |
随着系统版本的不断更新,我们亦可能会终止部分过于老旧的系统的支持,以保证面板的健壮性。
## 安装面板
> **Warning**
> 安装面板前,需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
> 安装面板前,需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
如果你决定继续,请`root`用户登录服务器,执行以下命令安装面板:
如果您的服务器有未挂载的数据盘,可在安装前`root`用户登录服务器行以下命令自动挂载,面板安装后不支持跨目录迁移。
```shell
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
```
准备就绪后,请以`root`用户登录服务器,运行以下命令安装面板:
**当前 v2.3.x 为测试版本,欢迎帮助我们测试在不同操作系统下的兼容性。**
@@ -71,7 +86,7 @@ curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash in
优先建议备份数据重装系统,这样可以保证系统纯净。
如果无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
如果无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
```shell
curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bash uninstall.sh
@@ -97,19 +112,35 @@ panel-cli
## 赞助商
如果耗子面板对有帮助,欢迎[赞助我们](https://afdian.com/a/TheTNB),感谢以下支持者/赞助商的支持:
如果耗子面板对有帮助,欢迎[赞助我们](https://afdian.com/a/TheTNB),感谢以下支持者/赞助商的支持:
**同时接受云资源赞助,可通过QQ群咨询联系**
### 资金
- [微晓朵](https://www.weixiaoduo.com/)
<a href="https://www.weixiaoduo.com/">
<img height="80" src=".github/assets/wxd.png" alt="微晓朵">
</a>
### 服务器
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
<img height="80" src=".github/assets/dk.png" alt="林枫云">
</a>
### CDN
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
- [WAF PRO](https://waf.pro/)
- [盾云SCDN](https://scdn.ddunyun.com/)
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
</a>
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
<img height="80" src=".github/assets/wafpro.png" alt="WAFPRO">
</a>
<a href="https://scdn.ddunyun.com/">
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
</a>
### 赞助商们
<p align="center">
<a target="_blank" href="https://afdian.com/a/TheTNB">
+50 -19
View File
@@ -18,7 +18,7 @@
The Rat Panel is an open source lightweight Linux server operation and maintenance management panel developed using Golang and Vue.
Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | Forum: [tom.moe](https://tom.moe) | Sponsor: [Open Collective](https://opencollective.com/tnb)
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)
## Advantages
@@ -32,25 +32,34 @@ Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | For
## UI Screenshots
![UI Screenshots](ui.png)
![UI Screenshots](.github/assets/ui.png)
## Operating Environment
Rat Panel supports mainstream systems under the `amd64` | `arm64` architecture.
Rat Panel supports mainstream systems under the `amd64` | `arm64` architecture, and the systems in the table below have been tested for LNMP environment installation.
It is recommended to use `AlmaLinux` | `RockyLinux` first.
It is recommended to use the systems marked **Recommended** first, and not recommended to use the systems marked **Not recommended** without special circumstances.
For other systems not listed in the table below, you can try to install them by yourself, but there is no guarantee that they will run normally, and no technical support will be provided.
For other systems not listed in the table below, you can try to install them by yourself, but there is no technical support will be provided (accept related PR submissions).
| OS | Version |
|------------|---------|
| RHEL | 9 |
| RockyLinux | 9 |
| AlmaLinux | 9 |
| Ubuntu | 22 |
| Ubuntu | 24 |
| Debian | 11 |
| Debian | 12 |
| OS | Version | Note |
|---------------------|---------|-----------------|
| AlmaLinux | 9 | Recommended |
| AlmaLinux | 8 | Not recommended |
| RockyLinux | 9 | Support |
| RockyLinux | 8 | Not recommended |
| CentOS Stream | 9 | Not recommended |
| CentOS Stream | 8 | Not recommended |
| Ubuntu | 24 | Recommended |
| Ubuntu | 22 | Support |
| Debian | 12 | Recommended |
| Debian | 11 | Support |
| OpenCloudOS | 9 | Support |
| TencentOS Server | 4 | Support |
| TencentOS Server | 3.1 | Not recommended |
| Alibaba Cloud Linux | 3.2 | Not recommended |
| Anolis | 8 | Not recommended |
| openEuler | 22 | Not recommended |
As system versions are constantly updated, we may also terminate support for some older systems to ensure the robustness of the panel.
@@ -59,7 +68,13 @@ As system versions are constantly updated, we may also terminate support for som
> **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.
If you decide to continue, please log in to the server as `root` user and execute the following command to install the panel:
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.
```shell
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:
**Current v2.3.x is a test version, welcome to help us test compatibility on different operating systems.**
@@ -103,13 +118,29 @@ If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollecti
### Financial
- [微晓朵](https://www.weixiaoduo.com/)
<a href="https://www.weixiaoduo.com/">
<img height="80" src=".github/assets/wxd.png" alt="微晓朵">
</a>
### Server
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
<img height="80" src=".github/assets/dk.png" alt="林枫云">
</a>
### CDN
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
- [WAF PRO](https://waf.pro/)
- [盾云SCDN](https://scdn.ddunyun.com/)
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
</a>
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
<img height="80" src=".github/assets/wafpro.png" alt="WAFPRO">
</a>
<a href="https://scdn.ddunyun.com/">
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
</a>
### Sponsors
<p align="center">
<a target="_blank" href="https://afdian.com/a/TheTNB">
+9 -8
View File
@@ -13,7 +13,7 @@ require (
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.22.1
github.com/go-rat/chix v1.1.3
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
@@ -23,23 +23,26 @@ require (
github.com/gookit/color v1.5.4
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-version v1.7.0
github.com/klauspost/compress v1.17.11
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/lib/pq v1.10.9
github.com/libdns/alidns v1.0.3
github.com/libdns/cloudflare v0.1.1
github.com/libdns/dnspod v0.0.3
github.com/libdns/huaweicloud v0.2.2
github.com/libdns/libdns v0.2.2
github.com/libdns/tencentcloud v1.0.0
github.com/mholt/acmez/v2 v2.0.3
github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/robfig/cron/v3 v3.0.1
github.com/samber/lo v1.47.0
github.com/sethvargo/go-limiter v1.0.0
github.com/shirou/gopsutil v2.21.11+incompatible
github.com/spf13/cast v1.7.0
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v3 v3.0.0-alpha9
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8
github.com/urfave/cli/v3 v3.0.0-alpha9.1
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.28.0
golang.org/x/net v0.30.0
@@ -58,6 +61,7 @@ require (
github.com/connesc/cipherio v0.2.1 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
@@ -72,15 +76,15 @@ require (
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/gofiber/schema v1.2.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jaevor/go-nanoid v1.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
@@ -93,7 +97,6 @@ require (
github.com/morikuni/aec v1.0.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
@@ -107,7 +110,6 @@ require (
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect
@@ -124,7 +126,6 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect
modernc.org/libc v1.60.1 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
+54 -14
View File
@@ -50,6 +50,8 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3
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=
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7 h1:AjCMD0AEGHHbcIpvRcrI8rMA+Us5F4uUt3B3KSpiYN8=
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7/go.mod h1:Gz/Ng6WCvYc1Q2smuPvow9kszfyH47wtnpZzvdD2juA=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
@@ -102,8 +104,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-rat/chix v1.1.3 h1:NeDbmA3OTfhEeizn528dz8z8/lBcsziftgtSarYn7Vo=
github.com/go-rat/chix v1.1.3/go.mod h1:XeC0hmaAXlPB/V8g0QZiIAgnAJO70ZeT+k1Q4Zh9qeI=
github.com/go-rat/chix v1.1.4 h1:YVDwF4vrlkm5hmlDGWKoiWPgP98pJooBvhsto8UWq+o=
github.com/go-rat/chix v1.1.4/go.mod h1:eKUTG7GXqMS278PH0QZi8yMWZddvMfxqWu7mverM6kQ=
github.com/go-rat/gormstore v1.0.6 h1:YxmaY6n3JoTk6o9RXoZ1+cQ6HQL+ayf/6d8D76ydCAc=
github.com/go-rat/gormstore v1.0.6/go.mod h1:xG/n+Du8baWf7ptiafiHLWBZWVXrYIPC42CsoEbfr4M=
github.com/go-rat/securecookie v1.0.1 h1:HW0fpKmB+FjJzXTw8ABOwBJ+XrPmRBSZqHhmrv86lBo=
@@ -120,6 +122,8 @@ github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAp
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -166,8 +170,9 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
@@ -187,8 +192,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
@@ -216,10 +221,9 @@ github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ=
github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE=
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
github.com/libdns/dnspod v0.0.3 h1:xJHDIujgLjvZnpB8/rMoCHUqA/KxSGBqRUXxSIzNzAA=
github.com/libdns/dnspod v0.0.3/go.mod h1:XLnqMmK7QlLPEbHwcOxbRvlzRvDgaaUlthRNFOPjXPI=
github.com/libdns/huaweicloud v0.2.2 h1:DvaWLiPJ0hoOvvPafKXgWv4gG33O8cqEu87GJeXYtbc=
github.com/libdns/huaweicloud v0.2.2/go.mod h1:F9vNIca+Cq1ZJiMCSkHQUdvXEGY6UbLoaYI3Py9CFDs=
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.1/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=
@@ -245,8 +249,6 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U=
github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -269,6 +271,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4=
github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
github.com/shirou/gopsutil v2.21.11+incompatible h1:lOGOyCG67a5dv2hq5Z1BLDUqqKp3HkbjPcz5j6XMS0U=
@@ -280,11 +284,15 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
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=
@@ -295,18 +303,19 @@ github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZ
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/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo=
github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc=
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/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
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=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -347,6 +356,9 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -376,6 +388,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -394,7 +408,12 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -410,6 +429,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -432,10 +453,22 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -443,6 +476,11 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
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=
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/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -476,6 +514,8 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+11
View File
@@ -25,9 +25,20 @@ var (
Logger *zap.Logger
)
// 定义面板状态常量
const (
StatusNormal = iota
StatusMaintain
StatusClosed
StatusUpgrade
StatusFailed
)
// 面板全局变量
var (
Root string
Version string
Locale string
IsCli bool
Status = StatusNormal
)
+18
View File
@@ -0,0 +1,18 @@
package benchmark
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: "benchmark",
Route: func(r chi.Router) {
service := NewService()
r.Post("/test", service.Test)
},
})
}
+6
View File
@@ -0,0 +1,6 @@
package benchmark
type Test struct {
Name string `json:"name" validate:"required,oneof=image machine compile encryption compression physics json memory disk"`
Multi bool `json:"multi"`
}
+686
View File
@@ -0,0 +1,686 @@
package benchmark
import (
"bytes"
"crypto/aes"
"crypto/cipher"
cryptorand "crypto/rand"
"encoding/json"
"fmt"
"image"
"image/color"
"io"
"math"
"math/big"
"math/rand/v2"
"net/http"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/klauspost/compress/zstd"
"github.com/TheTNB/panel/internal/service"
)
type Service struct {
}
func NewService() *Service {
return &Service{}
}
// Test 运行测试
func (s *Service) Test(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Test](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
switch req.Name {
case "image":
result := s.imageProcessing(req.Multi)
service.Success(w, result)
case "machine":
result := s.machineLearning(req.Multi)
service.Success(w, result)
case "compile":
result := s.compileSimulationSingle(req.Multi)
service.Success(w, result)
case "encryption":
result := s.encryptionTest(req.Multi)
service.Success(w, result)
case "compression":
result := s.compressionTest(req.Multi)
service.Success(w, result)
case "physics":
result := s.physicsSimulation(req.Multi)
service.Success(w, result)
case "json":
result := s.jsonProcessing(req.Multi)
service.Success(w, result)
case "disk":
result := s.diskTestTask()
service.Success(w, result)
case "memory":
result := s.memoryTestTask()
service.Success(w, result)
default:
service.Error(w, http.StatusUnprocessableEntity, "未知测试类型")
}
}
// calculateCpuScore 计算CPU成绩
func (s *Service) calculateCpuScore(duration time.Duration) int {
score := int((10 / duration.Seconds()) * float64(3000))
if score < 0 {
score = 0
}
return score
}
// calculateScore 计算内存/硬盘成绩
func (s *Service) calculateScore(duration time.Duration) int {
score := int((20 / duration.Seconds()) * float64(30000))
if score < 0 {
score = 0
}
return score
}
// 图像处理
func (s *Service) imageProcessing(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
if err := s.imageProcessingTask(n); err != nil {
return 0
}
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *Service) imageProcessingTask(numThreads int) error {
img := image.NewRGBA(image.Rect(0, 0, 4000, 4000))
for x := 0; x < 4000; x++ {
for y := 0; y < 4000; y++ {
img.Set(x, y, color.RGBA{R: uint8(x % 256), G: uint8(y % 256), A: 255})
}
}
var wg sync.WaitGroup
dx := img.Bounds().Dx()
dy := img.Bounds().Dy()
chunkSize := dy / numThreads
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
startY := i * chunkSize
endY := startY + chunkSize
if i == numThreads-1 {
endY = dy
}
for x := 1; x < dx-1; x++ {
for y := startY + 1; y < endY-1; y++ {
// 卷积操作(模糊)
rTotal, gTotal, bTotal := 0, 0, 0
for k := -1; k <= 1; k++ {
for l := -1; l <= 1; l++ {
r, g, b, _ := img.At(x+k, y+l).RGBA()
rTotal += int(r)
gTotal += int(g)
bTotal += int(b)
}
}
rAvg := uint8(rTotal / 9 / 256)
gAvg := uint8(gTotal / 9 / 256)
bAvg := uint8(bTotal / 9 / 256)
img.Set(x, y, color.RGBA{R: rAvg, G: gAvg, B: bAvg, A: 255})
}
}
}(i)
}
wg.Wait()
return nil
}
// 机器学习(矩阵乘法)
func (s *Service) machineLearning(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
s.machineLearningTask(n)
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *Service) machineLearningTask(numThreads int) {
size := 900
a := make([][]float64, size)
b := make([][]float64, size)
for i := 0; i < size; i++ {
a[i] = make([]float64, size)
b[i] = make([]float64, size)
for j := 0; j < size; j++ {
a[i][j] = rand.Float64()
b[i][j] = rand.Float64()
}
}
c := make([][]float64, size)
for i := 0; i < size; i++ {
c[i] = make([]float64, size)
}
var wg sync.WaitGroup
chunkSize := size / numThreads
for k := 0; k < numThreads; k++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
start := k * chunkSize
end := start + chunkSize
if k == numThreads-1 {
end = size
}
for i := start; i < end; i++ {
for j := 0; j < size; j++ {
sum := 0.0
for l := 0; l < size; l++ {
sum += a[i][l] * b[l][j]
}
c[i][j] = sum
}
}
}(k)
}
wg.Wait()
}
// 数学问题(计算斐波那契数)
func (s *Service) compileSimulationSingle(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
totalCalculations := 1000
fibNumber := 20000
calculationsPerThread := totalCalculations / n
remainder := totalCalculations % n
var wg sync.WaitGroup
for i := 0; i < n; i++ {
tasks := calculationsPerThread
if i < remainder {
tasks++ // 处理无法均分的剩余任务
}
wg.Add(1)
go func(tasks int) {
defer wg.Done()
for j := 0; j < tasks; j++ {
s.fib(fibNumber)
}
}(tasks)
}
wg.Wait()
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
// 斐波那契函数
func (s *Service) fib(n int) *big.Int {
if n < 2 {
return big.NewInt(int64(n))
}
a := big.NewInt(0)
b := big.NewInt(1)
temp := big.NewInt(0)
for i := 2; i <= n; i++ {
temp.Add(a, b)
a.Set(b)
b.Set(temp)
}
return b
}
// AES加密
func (s *Service) encryptionTest(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
if err := s.encryptionTestTask(n); err != nil {
return 0
}
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *Service) encryptionTestTask(numThreads int) error {
key := []byte("abcdefghijklmnopqrstuvwxyz123456")
dataSize := 1 * 1024 * 1024 * 1024 // 1GB
plaintext := []byte(strings.Repeat("A", dataSize))
block, err := aes.NewCipher(key)
if err != nil {
return err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return err
}
chunkSize := dataSize / numThreads
var wg sync.WaitGroup
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
start := i * chunkSize
end := start + chunkSize
if i == numThreads-1 {
end = dataSize
}
nonce := make([]byte, aesGCM.NonceSize())
if _, err = cryptorand.Read(nonce); err != nil {
return
}
aesGCM.Seal(nil, nonce, plaintext[start:end], nil)
}(i)
}
wg.Wait()
return nil
}
// 压缩/解压缩
func (s *Service) compressionTest(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
s.compressionTestTask(n)
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *Service) compressionTestTask(numThreads int) {
data := []byte(strings.Repeat("耗子面板", 50000000))
chunkSize := len(data) / numThreads
var wg sync.WaitGroup
compressedChunks := make([]bytes.Buffer, numThreads)
// 压缩
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
start := i * chunkSize
end := start + chunkSize
if i == numThreads-1 {
end = len(data)
}
var buf bytes.Buffer
zw, _ := zstd.NewWriter(&buf)
_, _ = zw.Write(data[start:end])
_ = zw.Close()
compressedChunks[i] = buf
}(i)
}
wg.Wait()
// 解压缩
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
zr, err := zstd.NewReader(&compressedChunks[i])
if err != nil {
return
}
_, err = io.Copy(io.Discard, zr)
if err != nil {
return
}
zr.Close()
}(i)
}
wg.Wait()
}
// 物理仿真(N体问题)
func (s *Service) physicsSimulation(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
s.physicsSimulationTask(n)
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *Service) physicsSimulationTask(numThreads int) {
const (
numBodies = 4000
steps = 30
)
type Body struct {
x, y, z, vx, vy, vz float64
}
bodies := make([]Body, numBodies)
for i := 0; i < numBodies; i++ {
bodies[i] = Body{
x: rand.Float64(),
y: rand.Float64(),
z: rand.Float64(),
vx: rand.Float64(),
vy: rand.Float64(),
vz: rand.Float64(),
}
}
chunkSize := numBodies / numThreads
for step := 0; step < steps; step++ {
var wg sync.WaitGroup
// 更新速度
for k := 0; k < numThreads; k++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
start := k * chunkSize
end := start + chunkSize
if k == numThreads-1 {
end = numBodies
}
for i := start; i < end; i++ {
bi := &bodies[i]
for j := 0; j < numBodies; j++ {
if i == j {
continue
}
bj := &bodies[j]
dx := bj.x - bi.x
dy := bj.y - bi.y
dz := bj.z - bi.z
dist := math.Sqrt(dx*dx + dy*dy + dz*dz)
if dist == 0 {
continue
}
force := 1 / (dist * dist)
bi.vx += force * dx / dist
bi.vy += force * dy / dist
bi.vz += force * dz / dist
}
}
}(k)
}
wg.Wait()
// 更新位置
for k := 0; k < numThreads; k++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
start := k * chunkSize
end := start + chunkSize
if k == numThreads-1 {
end = numBodies
}
for i := start; i < end; i++ {
bi := &bodies[i]
bi.x += bi.vx
bi.y += bi.vy
bi.z += bi.vz
}
}(k)
}
wg.Wait()
}
}
// JSON解析
func (s *Service) jsonProcessing(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
s.jsonProcessingTask(n)
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *Service) jsonProcessingTask(numThreads int) {
numElements := 1000000
elementsPerThread := numElements / numThreads
var wg sync.WaitGroup
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
start := i * elementsPerThread
end := start + elementsPerThread
if i == numThreads-1 {
end = numElements
}
elements := make([]map[string]any, 0, end-start)
for j := start; j < end; j++ {
elements = append(elements, map[string]any{
"id": j,
"value": fmt.Sprintf("Value%d", j),
})
}
encoded, err := json.Marshal(elements)
if err != nil {
return
}
var parsed []map[string]any
err = json.Unmarshal(encoded, &parsed)
if err != nil {
return
}
}(i)
}
wg.Wait()
}
// 内存性能
func (s *Service) memoryTestTask() map[string]any {
results := make(map[string]any)
dataSize := 500 * 1024 * 1024 // 500 MB
data := make([]byte, dataSize)
_, _ = cryptorand.Read(data)
start := time.Now()
// 内存读写速度
results["bandwidth"] = s.memoryBandwidthTest(data)
// 内存访问延迟
data = data[:100*1024*1024] // 100 MB
results["latency"] = s.memoryLatencyTest(data)
duration := time.Since(start)
results["score"] = s.calculateScore(duration)
return results
}
func (s *Service) memoryBandwidthTest(data []byte) string {
dataSize := len(data)
startTime := time.Now()
for i := 0; i < dataSize; i++ {
data[i] ^= 0xFF
}
duration := time.Since(startTime).Seconds()
if duration == 0 {
return "N/A"
}
speed := float64(dataSize) / duration / (1024 * 1024)
return fmt.Sprintf("%.2f MB/s", speed)
}
func (s *Service) memoryLatencyTest(data []byte) string {
dataSize := len(data)
indices := rand.Perm(dataSize)
startTime := time.Now()
sum := byte(0)
for _, idx := range indices {
sum ^= data[idx]
}
duration := time.Since(startTime).Seconds()
if duration == 0 {
return "N/A"
}
avgLatency := duration * 1e9 / float64(dataSize)
return fmt.Sprintf("%.2f ns", avgLatency)
}
// 硬盘IO
func (s *Service) diskTestTask() map[string]any {
results := make(map[string]any)
blockSizes := []int64{4 * 1024, 64 * 1024, 512 * 1024, 1 * 1024 * 1024} // 4K, 64K, 512K, 1M
fileSize := int64(100 * 1024 * 1024) // 100MB 文件
start := time.Now()
for _, blockSize := range blockSizes {
result := s.diskIOTest(blockSize, fileSize)
results[fmt.Sprintf("%d", blockSize/1024)] = result
}
duration := time.Since(start)
results["score"] = s.calculateScore(duration)
return results
}
func (s *Service) diskIOTest(blockSize int64, fileSize int64) map[string]any {
result := make(map[string]any)
tempFile := fmt.Sprintf("tempfile_%d", blockSize)
defer os.Remove(tempFile)
// 写测试
writeSpeed, writeIOPS := s.diskWriteTest(tempFile, blockSize, fileSize)
// 读测试
readSpeed, readIOPS := s.diskReadTest(tempFile, blockSize, fileSize)
result["write_speed"] = fmt.Sprintf("%.2f MB/s", writeSpeed)
result["write_iops"] = fmt.Sprintf("%.2f IOPS", writeIOPS)
result["read_speed"] = fmt.Sprintf("%.2f MB/s", readSpeed)
result["read_iops"] = fmt.Sprintf("%.2f IOPS", readIOPS)
return result
}
func (s *Service) diskWriteTest(fileName string, blockSize int64, fileSize int64) (float64, float64) {
totalBlocks := fileSize / blockSize
data := make([]byte, blockSize)
_, _ = cryptorand.Read(data)
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_SYNC, 0644)
if err != nil {
return 0, 0
}
defer file.Close()
start := time.Now()
for i := int64(0); i < totalBlocks; i++ {
// 生成随机偏移
offset := rand.Int64N(fileSize - blockSize + 1)
_, err := file.WriteAt(data, offset)
if err != nil {
return 0, 0
}
}
_ = file.Sync()
duration := time.Since(start).Seconds()
if duration == 0 {
duration = 1
}
speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024)
iops := float64(totalBlocks) / duration
return speed, iops
}
func (s *Service) diskReadTest(fileName string, blockSize int64, fileSize int64) (float64, float64) {
totalBlocks := fileSize / blockSize
data := make([]byte, blockSize)
file, err := os.OpenFile(fileName, os.O_RDONLY|os.O_SYNC, 0644)
if err != nil {
return 0, 0
}
defer file.Close()
start := time.Now()
for i := int64(0); i < totalBlocks; i++ {
// 生成随机偏移
offset := rand.Int64N(fileSize - blockSize + 1)
_, err := file.ReadAt(data, offset)
if err != nil && err != io.EOF {
return 0, 0
}
}
duration := time.Since(start).Seconds()
if duration == 0 {
duration = 1
}
speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024)
iops := float64(totalBlocks) / duration
return speed, iops
}
+1 -1
View File
@@ -13,7 +13,7 @@ func init() {
Route: func(r chi.Router) {
service := NewService()
r.Get("/jails", service.List)
r.Post("/jails", service.Add)
r.Post("/jails", service.Create)
r.Delete("/jails", service.Delete)
r.Get("/jails/{name}", service.BanList)
r.Post("/unban", service.Unban)
+10 -10
View File
@@ -1,29 +1,29 @@
package fail2ban
type Add struct {
Name string `json:"name"`
Type string `json:"type"`
MaxRetry string `json:"maxretry"`
FindTime string `json:"findtime"`
BanTime string `json:"bantime"`
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
MaxRetry int `json:"maxretry" validate:"required"`
FindTime int `json:"findtime" validate:"required"`
BanTime int `json:"bantime" validate:"required"`
WebsiteName string `json:"website_name"`
WebsiteMode string `json:"website_mode"`
WebsitePath string `json:"website_path"`
}
type Delete struct {
Name string `json:"name"`
Name string `json:"name" validate:"required"`
}
type BanList struct {
Name string `json:"name"`
Name string `json:"name" validate:"required"`
}
type Unban struct {
Name string `json:"name"`
IP string `json:"ip"`
Name string `json:"name" validate:"required"`
IP string `json:"ip" validate:"required"`
}
type SetWhiteList struct {
IP string `json:"ip"`
IP string `json:"ip" validate:"required"`
}
+12 -9
View File
@@ -1,6 +1,7 @@
package fail2ban
import (
"fmt"
"net/http"
"regexp"
"strings"
@@ -28,7 +29,7 @@ func NewService() *Service {
}
}
// List 所有 Fail2ban 规则
// List 所有规则
func (s *Service) List(w http.ResponseWriter, r *http.Request) {
raw, err := io.Read("/etc/fail2ban/jail.local")
if err != nil {
@@ -77,8 +78,8 @@ func (s *Service) List(w http.ResponseWriter, r *http.Request) {
})
}
// Add 添加 Fail2ban 规则
func (s *Service) Add(w http.ResponseWriter, r *http.Request) {
// Create 添加规则
func (s *Service) Create(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Add](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
@@ -86,9 +87,9 @@ func (s *Service) Add(w http.ResponseWriter, r *http.Request) {
}
jailName := req.Name
jailType := req.Type
jailMaxRetry := req.MaxRetry
jailFindTime := req.FindTime
jailBanTime := req.BanTime
jailMaxRetry := cast.ToString(req.MaxRetry)
jailFindTime := cast.ToString(req.FindTime)
jailBanTime := cast.ToString(req.BanTime)
jailWebsiteName := req.WebsiteName
jailWebsiteMode := req.WebsiteMode
jailWebsitePath := req.WebsitePath
@@ -111,10 +112,12 @@ func (s *Service) Add(w http.ResponseWriter, r *http.Request) {
return
}
var ports string
for _, port := range website.Ports {
fields := strings.Fields(cast.ToString(port))
ports += fields[0] + ","
for _, listen := range website.Listens {
if port, err := cast.ToIntE(listen.Address); err == nil {
ports += fmt.Sprintf("%d", port) + ","
}
}
ports = strings.TrimSuffix(ports, ",")
rule := `
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-START
+3 -3
View File
@@ -1,10 +1,10 @@
package frp
type Name struct {
Name string `form:"name" json:"name"`
Name string `form:"name" json:"name" validate:"required"`
}
type UpdateConfig struct {
Name string `form:"name" json:"name"`
Config string `form:"config" json:"config"`
Name string `form:"name" json:"name" validate:"required"`
Config string `form:"config" json:"config" validate:"required"`
}
+1 -1
View File
@@ -1,5 +1,5 @@
package gitea
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
+1 -6
View File
@@ -17,12 +17,7 @@ func NewService() *Service {
}
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
config, err := io.Read(fmt.Sprintf("%s/server/gitea/app.ini", app.Root))
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
config, _ := io.Read(fmt.Sprintf("%s/server/gitea/app.ini", app.Root))
service.Success(w, config)
}
+1
View File
@@ -3,6 +3,7 @@ package apps
import (
"github.com/go-chi/chi/v5"
_ "github.com/TheTNB/panel/internal/apps/benchmark"
_ "github.com/TheTNB/panel/internal/apps/fail2ban"
_ "github.com/TheTNB/panel/internal/apps/frp"
_ "github.com/TheTNB/panel/internal/apps/gitea"
+2 -2
View File
@@ -1,9 +1,9 @@
package mysql
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
type SetRootPassword struct {
Password string `form:"password" json:"password"`
Password string `form:"password" json:"password" validate:"required,password"`
}
+7 -9
View File
@@ -49,13 +49,13 @@ func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
return
}
if err := io.Write(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入配置失败")
if err = io.Write(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入配置失败%v", err)
return
}
if err := systemctl.Reload("mysqld"); err != nil {
service.Error(w, http.StatusInternalServerError, "重失败")
if err = systemctl.Restart("mysqld"); err != nil {
service.Error(w, http.StatusInternalServerError, "重失败%v", err)
return
}
@@ -108,7 +108,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到盘"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到盘"},
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
@@ -144,8 +144,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
// ErrorLog 获取错误日志
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 100 %s/server/mysql/mysql-error.log", app.Root)
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/mysql/mysql-error.log", app.Root))
}
// ClearErrorLog 清空错误日志
@@ -160,8 +159,7 @@ func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
// SlowLog 获取慢查询日志
func (s *Service) SlowLog(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 100 %s/server/mysql/mysql-slow.log", app.Root)
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/mysql/mysql-slow.log", app.Root))
}
// ClearSlowLog 清空慢查询日志
+1 -1
View File
@@ -1,5 +1,5 @@
package nginx
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
+1 -2
View File
@@ -58,8 +58,7 @@ func (s *Service) SaveConfig(w http.ResponseWriter, r *http.Request) {
}
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
out, _ := shell.Execf("tail -n 100 %s/%s", app.Root, "wwwlogs/nginx-error.log")
service.Success(w, out)
service.Success(w, fmt.Sprintf("%s/%s", app.Root, "wwwlogs/nginx-error.log"))
}
func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
+2 -2
View File
@@ -1,9 +1,9 @@
package php
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
type ExtensionSlug struct {
Slug string `form:"slug" json:"slug"`
Slug string `form:"slug" json:"slug" validate:"required"`
}
+6 -7
View File
@@ -3,6 +3,7 @@ package php
import (
"fmt"
"net/http"
"net/url"
"regexp"
"slices"
"strings"
@@ -110,13 +111,11 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
}
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 500 %s/server/php/%d/var/log/php-fpm.log", app.Root, s.version)
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/php/%d/var/log/php-fpm.log", app.Root, s.version))
}
func (s *Service) SlowLog(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 500 %s/server/php/%d/var/log/slow.log", app.Root, s.version)
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/php/%d/var/log/slow.log", app.Root, s.version))
}
func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
@@ -183,7 +182,7 @@ 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" | bash -s -- "install" "%d" >> /tmp/%s.log 2>&1`, 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.QueryEscape(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)
@@ -214,7 +213,7 @@ 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" | bash -s -- "uninstall" "%d" >> /tmp/%s.log 2>&1`, 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.QueryEscape(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)
@@ -258,7 +257,7 @@ func (s *Service) getExtensions() []Extension {
{
Name: "exif",
Slug: "exif",
Description: "通过 exif 扩展,可以操作图像元数据。",
Description: "通过 exif 扩展,可以操作图像元数据。",
},
{
Name: "pdo_pgsql",
+2 -2
View File
@@ -1,9 +1,9 @@
package phpmyadmin
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
type UpdatePort struct {
Port uint `form:"port" json:"port"`
Port uint `form:"port" json:"port" validate:"required,number,gte=1,lte=65535"`
}
+5 -2
View File
@@ -78,8 +78,11 @@ func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
fw := firewall.NewFirewall()
err = fw.Port(firewall.FireInfo{
Port: req.Port,
Protocol: "tcp",
Type: firewall.TypeNormal,
PortStart: req.Port,
PortEnd: req.Port,
Direction: firewall.DirectionIn,
Strategy: firewall.StrategyAccept,
}, firewall.OperationAdd)
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
+1 -1
View File
@@ -1,5 +1,5 @@
package podman
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
+1 -1
View File
@@ -1,5 +1,5 @@
package postgresql
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
+1 -2
View File
@@ -132,8 +132,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
// Log 获取日志
func (s *Service) Log(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 100 %s/server/postgresql/logs/postgresql-%s.log", app.Root, time.Now().Format(time.DateOnly))
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/postgresql/logs/postgresql-%s.log", app.Root, time.Now().Format(time.DateOnly)))
}
// ClearLog 清空日志
+2 -2
View File
@@ -14,8 +14,8 @@ func init() {
service := NewService()
r.Get("/users", service.List)
r.Post("/users", service.Create)
r.Delete("/users/{name}", service.Delete)
r.Post("/users/{name}/password", service.ChangePassword)
r.Delete("/users/{username}", service.Delete)
r.Post("/users/{username}/password", service.ChangePassword)
r.Get("/port", service.GetPort)
r.Post("/port", service.UpdatePort)
},
+7 -7
View File
@@ -1,20 +1,20 @@
package pureftpd
type Create struct {
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
Path string `form:"path" json:"path"`
Username string `form:"username" json:"username" validate:"required"`
Password string `form:"password" json:"password" validate:"required,password"`
Path string `form:"path" json:"path" validate:"required"`
}
type Delete struct {
Username string `form:"username" json:"username"`
Username string `form:"username" json:"username" validate:"required"`
}
type ChangePassword struct {
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
Username string `form:"username" json:"username" validate:"required"`
Password string `form:"password" json:"password" validate:"required,password"`
}
type UpdatePort struct {
Port uint `form:"port" json:"port"`
Port uint `form:"port" json:"port" validate:"required"`
}
+5 -2
View File
@@ -156,8 +156,11 @@ func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
fw := firewall.NewFirewall()
err = fw.Port(firewall.FireInfo{
Port: req.Port,
Protocol: "tcp",
Type: firewall.TypeNormal,
PortStart: req.Port,
PortEnd: req.Port,
Direction: firewall.DirectionIn,
Strategy: firewall.StrategyAccept,
}, firewall.OperationAdd)
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
+1 -1
View File
@@ -1,5 +1,5 @@
package redis
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
+10 -10
View File
@@ -1,27 +1,27 @@
package rsync
type Create struct {
Name string `form:"name" json:"name"`
Path string `form:"path" json:"path"`
Name string `form:"name" json:"name" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
Comment string `form:"comment" json:"comment"`
AuthUser string `form:"auth_user" json:"auth_user"`
Secret string `form:"secret" json:"secret"`
AuthUser string `form:"auth_user" json:"auth_user" validate:"required"`
Secret string `form:"secret" json:"secret" validate:"required"`
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
}
type Delete struct {
Name string `form:"name" json:"name"`
Name string `form:"name" json:"name" validate:"required"`
}
type Update struct {
Name string `form:"name" json:"name"`
Path string `form:"path" json:"path"`
Name string `form:"name" json:"name" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
Comment string `form:"comment" json:"comment"`
AuthUser string `form:"auth_user" json:"auth_user"`
Secret string `form:"secret" json:"secret"`
AuthUser string `form:"auth_user" json:"auth_user" validate:"required"`
Secret string `form:"secret" json:"secret" validate:"required"`
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
}
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
+6 -6
View File
@@ -1,13 +1,13 @@
package s3fs
type Create struct {
Ak string `form:"ak" json:"ak"`
Sk string `form:"sk" json:"sk"`
Bucket string `form:"bucket" json:"bucket"`
URL string `form:"url" json:"url"`
Path string `form:"path" json:"path"`
Ak string `form:"ak" json:"ak" validate:"required"`
Sk string `form:"sk" json:"sk" validate:"required"`
Bucket string `form:"bucket" json:"bucket" validate:"required"`
URL string `form:"url" json:"url" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
}
type Delete struct {
ID int64 `form:"id" json:"id"`
ID int64 `form:"id" json:"id" validate:"required"`
}
+8 -9
View File
@@ -13,19 +13,18 @@ func init() {
Route: func(r chi.Router) {
service := NewService()
r.Get("/service", service.Service)
r.Get("/log", service.Log)
r.Post("/clearLog", service.ClearLog)
r.Get("/config", service.GetConfig)
r.Post("/config", service.UpdateConfig)
r.Get("/processes", service.Processes)
r.Post("/processes/{name}/start", service.StartProcess)
r.Post("/processes/{name}/stop", service.StopProcess)
r.Post("/processes/{name}/restart", service.RestartProcess)
r.Get("/processes/{name}/log", service.ProcessLog)
r.Post("/processes/{name}/clearLog", service.ClearProcessLog)
r.Get("/processes/{name}", service.ProcessConfig)
r.Post("/processes/{name}", service.UpdateProcessConfig)
r.Delete("/processes/{name}", service.DeleteProcess)
r.Post("/processes/{process}/start", service.StartProcess)
r.Post("/processes/{process}/stop", service.StopProcess)
r.Post("/processes/{process}/restart", service.RestartProcess)
r.Get("/processes/{process}/log", service.ProcessLog)
r.Post("/processes/{process}/clearLog", service.ClearProcessLog)
r.Get("/processes/{process}", service.ProcessConfig)
r.Post("/processes/{process}", service.UpdateProcessConfig)
r.Delete("/processes/{process}", service.DeleteProcess)
r.Post("/processes", service.CreateProcess)
},
})
+9 -9
View File
@@ -1,22 +1,22 @@
package supervisor
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
type UpdateProcessConfig struct {
Process string `form:"config" json:"process"`
Config string `form:"config" json:"config"`
Process string `form:"process" json:"process" validate:"required"`
Config string `form:"config" json:"config" validate:"required"`
}
type ProcessName struct {
Process string `form:"config" json:"process"`
Process string `form:"process" json:"process" validate:"required"`
}
type CreateProcess struct {
Name string `form:"name" json:"name"`
User string `form:"user" json:"user"`
Path string `form:"path" json:"path"`
Command string `form:"command" json:"command"`
Num int `form:"num" json:"num"`
Name string `form:"name" json:"name" validate:"required"`
User string `form:"user" json:"user" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
Command string `form:"command" json:"command" validate:"required"`
Num int `form:"num" json:"num" validate:"required,gte=1"`
}
+1 -8
View File
@@ -36,12 +36,6 @@ func (s *Service) Service(w http.ResponseWriter, r *http.Request) {
service.Success(w, s.name)
}
// Log 日志
func (s *Service) Log(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf(`tail -n 200 /var/log/supervisor/supervisord.log`)
service.Success(w, log)
}
// ClearLog 清空日志
func (s *Service) ClearLog(w http.ResponseWriter, r *http.Request) {
if _, err := shell.Execf(`echo "" > /var/log/supervisor/supervisord.log`); err != nil {
@@ -204,8 +198,7 @@ func (s *Service) ProcessLog(w http.ResponseWriter, r *http.Request) {
return
}
log, _ := shell.Execf(`tail -n 200 '%s'`, logPath)
service.Success(w, log)
service.Success(w, logPath)
}
// ClearProcessLog 清空进程日志
+4
View File
@@ -18,6 +18,10 @@ func init() {
r.Post("/swap", service.UpdateSWAP)
r.Get("/timezone", service.GetTimezone)
r.Post("/timezone", service.UpdateTimezone)
r.Post("/time", service.UpdateTime)
r.Post("/syncTime", service.SyncTime)
r.Get("/hostname", service.GetHostname)
r.Post("/hostname", service.UpdateHostname)
r.Get("/hosts", service.GetHosts)
r.Post("/hosts", service.UpdateHosts)
r.Post("/rootPassword", service.UpdateRootPassword)
+15 -5
View File
@@ -1,16 +1,26 @@
package toolbox
import "time"
type DNS struct {
DNS1 string `form:"dns1" json:"dns1"`
DNS2 string `form:"dns2" json:"dns2"`
DNS1 string `form:"dns1" json:"dns1" validate:"required"`
DNS2 string `form:"dns2" json:"dns2" validate:"required"`
}
type SWAP struct {
Size int64 `form:"size" json:"size"`
Size int64 `form:"size" json:"size" validate:"gte=0"`
}
type Timezone struct {
Timezone string `form:"timezone" json:"timezone"`
Timezone string `form:"timezone" json:"timezone" validate:"required"`
}
type Time struct {
Time time.Time `form:"time" json:"time" validate:"required"`
}
type Hostname struct {
Hostname string `form:"hostname" json:"hostname" validate:"required,regex=^[a-zA-Z0-9][a-zA-Z0-9-]{061}[a-zA-Z0-9]$"`
}
type Hosts struct {
@@ -18,5 +28,5 @@ type Hosts struct {
}
type Password struct {
Password string `form:"password" json:"password"`
Password string `form:"password" json:"password" validate:"required,password"`
}
+80 -36
View File
@@ -12,6 +12,7 @@ import (
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/service"
"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/types"
@@ -52,11 +53,6 @@ func (s *Service) UpdateDNS(w http.ResponseWriter, r *http.Request) {
return
}
if len(req.DNS1) == 0 || len(req.DNS2) == 0 {
service.Error(w, http.StatusUnprocessableEntity, "DNS 信息不能为空")
return
}
var dns string
dns += "nameserver " + req.DNS1 + "\n"
dns += "nameserver " + req.DNS2 + "\n"
@@ -76,7 +72,7 @@ func (s *Service) GetSWAP(w http.ResponseWriter, r *http.Request) {
if io.Exists(filepath.Join(app.Root, "swap")) {
file, err := io.FileInfo(filepath.Join(app.Root, "swap"))
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "获取 SWAP 信息失败")
service.Error(w, http.StatusInternalServerError, "获取 SWAP 信息失败")
return
}
@@ -89,12 +85,12 @@ func (s *Service) GetSWAP(w http.ResponseWriter, r *http.Request) {
raw, err := shell.Execf("free | grep Swap")
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "获取 SWAP 信息失败")
service.Error(w, http.StatusInternalServerError, "获取 SWAP 信息失败")
return
}
match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
if len(match) > 0 {
if len(match) >= 4 {
used = str.FormatBytes(cast.ToFloat64(match[2]) * 1024)
free = str.FormatBytes(cast.ToFloat64(match[3]) * 1024)
}
@@ -117,15 +113,15 @@ func (s *Service) UpdateSWAP(w http.ResponseWriter, r *http.Request) {
if io.Exists(filepath.Join(app.Root, "swap")) {
if _, err = shell.Execf("swapoff '%s'", filepath.Join(app.Root, "swap")); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if _, err = shell.Execf("rm -f '%s'", filepath.Join(app.Root, "swap")); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if _, err = shell.Execf(`sed -i '%s/d' /etc/fstab`, filepath.Join(app.Root, "swap")); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
if _, err = shell.Execf(`sed -i "\|^%s|d" /etc/fstab`, filepath.Join(app.Root, "swap")); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
}
@@ -134,40 +130,40 @@ func (s *Service) UpdateSWAP(w http.ResponseWriter, r *http.Request) {
var free string
free, err = shell.Execf("df -k %s | awk '{print $4}' | tail -n 1", app.Root)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "获取盘空间失败")
service.Error(w, http.StatusInternalServerError, "获取盘空间失败")
return
}
if cast.ToInt64(free)*1024 < req.Size*1024*1024 {
service.Error(w, http.StatusUnprocessableEntity, "盘空间不足,当前剩余%s", str.FormatBytes(cast.ToFloat64(free)))
service.Error(w, http.StatusInternalServerError, "盘空间不足,当前剩余%s", str.FormatBytes(cast.ToFloat64(free)))
return
}
btrfsCheck, _ := shell.Execf("df -T %s | awk '{print $2}' | tail -n 1", app.Root)
if strings.Contains(btrfsCheck, "btrfs") {
if _, err = shell.Execf("btrfs filesystem mkswapfile --size %dM --uuid clear %s", req.Size, filepath.Join(app.Root, "swap")); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
} else {
if _, err = shell.Execf("dd if=/dev/zero of=%s bs=1M count=%d", filepath.Join(app.Root, "swap"), req.Size); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if _, err = shell.Execf("mkswap -f '%s'", filepath.Join(app.Root, "swap")); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = io.Chmod(filepath.Join(app.Root, "swap"), 0600); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "设置 SWAP 权限失败")
service.Error(w, http.StatusInternalServerError, "设置 SWAP 权限失败")
return
}
}
if _, err = shell.Execf("swapon '%s'", filepath.Join(app.Root, "swap")); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if _, err = shell.Execf("echo '%s swap swap defaults 0 0' >> /etc/fstab", filepath.Join(app.Root, "swap")); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
}
@@ -179,19 +175,19 @@ func (s *Service) UpdateSWAP(w http.ResponseWriter, r *http.Request) {
func (s *Service) GetTimezone(w http.ResponseWriter, r *http.Request) {
raw, err := shell.Execf("timedatectl | grep zone")
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "获取时区信息失败")
service.Error(w, http.StatusInternalServerError, "获取时区信息失败")
return
}
match := regexp.MustCompile(`zone:\s+(\S+)`).FindStringSubmatch(raw)
if len(match) == 0 {
service.Error(w, http.StatusUnprocessableEntity, "找不到时区信息")
service.Error(w, http.StatusInternalServerError, "找不到时区信息")
return
}
zonesRaw, err := shell.Execf("timedatectl list-timezones")
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "获取时区列表失败")
service.Error(w, http.StatusInternalServerError, "获取时区列表失败")
return
}
zones := strings.Split(zonesRaw, "\n")
@@ -219,21 +215,74 @@ func (s *Service) UpdateTimezone(w http.ResponseWriter, r *http.Request) {
}
if _, err = shell.Execf("timedatectl set-timezone '%s'", req.Timezone); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
}
// UpdateTime 设置时间
func (s *Service) UpdateTime(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Time](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = ntp.UpdateSystemTime(req.Time); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
}
// SyncTime 同步时间
func (s *Service) SyncTime(w http.ResponseWriter, r *http.Request) {
now, err := ntp.Now()
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = ntp.UpdateSystemTime(now); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
}
// GetHostname 获取主机名
func (s *Service) GetHostname(w http.ResponseWriter, r *http.Request) {
hostname, _ := io.Read("/etc/hostname")
service.Success(w, strings.TrimSpace(hostname))
}
// UpdateHostname 设置主机名
func (s *Service) UpdateHostname(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Hostname](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if _, err = shell.Execf("hostnamectl set-hostname '%s'", req.Hostname); err != nil {
// 直接写 /etc/hostname
if err = io.Write("/etc/hostname", req.Hostname, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入主机名失败")
return
}
}
service.Success(w, nil)
}
// GetHosts 获取 hosts 信息
func (s *Service) GetHosts(w http.ResponseWriter, r *http.Request) {
hosts, err := io.Read("/etc/hosts")
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
hosts, _ := io.Read("/etc/hosts")
service.Success(w, hosts)
}
@@ -246,7 +295,7 @@ func (s *Service) UpdateHosts(w http.ResponseWriter, r *http.Request) {
}
if err = io.Write("/etc/hosts", req.Hosts, 0644); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "写入 hosts 信息失败")
service.Error(w, http.StatusInternalServerError, "写入 hosts 信息失败")
return
}
@@ -261,14 +310,9 @@ func (s *Service) UpdateRootPassword(w http.ResponseWriter, r *http.Request) {
return
}
if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(req.Password) {
service.Error(w, http.StatusUnprocessableEntity, "密码必须为 6-20 位字母、数字或特殊字符")
return
}
req.Password = strings.ReplaceAll(req.Password, `'`, `\'`)
if _, err = shell.Execf(`yes '%s' | passwd root`, req.Password); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
+24
View File
@@ -0,0 +1,24 @@
package biz
import "github.com/TheTNB/panel/pkg/types"
type BackupType string
const (
BackupTypePath BackupType = "path"
BackupTypeWebsite BackupType = "website"
BackupTypeMySQL BackupType = "mysql"
BackupTypePostgres BackupType = "postgres"
BackupTypeRedis BackupType = "redis"
BackupTypePanel BackupType = "panel"
)
type BackupRepo interface {
List(typ BackupType) ([]*types.BackupFile, error)
Create(typ BackupType, target string, path ...string) error
Delete(typ BackupType, name string) error
Restore(typ BackupType, backup, target string) error
ClearExpired(path, prefix string, save int) error
CutoffLog(path, target string) error
GetPath(typ BackupType) (string, error)
}
-1
View File
@@ -26,5 +26,4 @@ type CronRepo interface {
Update(req *request.CronUpdate) error
Delete(id uint) error
Status(id uint, status bool) error
Log(id uint) (string, error)
}
+5 -5
View File
@@ -4,14 +4,14 @@ import (
"time"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
)
type Monitor struct {
ID uint `gorm:"primaryKey" json:"id"`
Info tools.MonitoringInfo `gorm:"not null;serializer:json" json:"info"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uint `gorm:"primaryKey" json:"id"`
Info types.CurrentInfo `gorm:"not null;serializer:json" json:"info"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type MonitorRepo interface {
+3 -4
View File
@@ -17,10 +17,7 @@ const (
SettingKeyBackupPath SettingKey = "backup_path"
SettingKeyWebsitePath SettingKey = "website_path"
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
SettingKeySshHost SettingKey = "ssh_host"
SettingKeySshPort SettingKey = "ssh_port"
SettingKeySshUser SettingKey = "ssh_user"
SettingKeySshPassword SettingKey = "ssh_password"
SettingKeyOfflineMode SettingKey = "offline_mode"
)
type Setting struct {
@@ -33,9 +30,11 @@ type Setting struct {
type SettingRepo interface {
Get(key SettingKey, defaultValue ...string) (string, error)
GetBool(key SettingKey, defaultValue ...bool) (bool, error)
Set(key SettingKey, value string) error
Delete(key SettingKey) error
GetPanelSetting(ctx context.Context) (*request.PanelSetting, error)
UpdatePanelSetting(ctx context.Context, setting *request.PanelSetting) (bool, error)
UpdatePanel(version, url, checksum string) error
FixPanel() error
}
+22 -3
View File
@@ -1,8 +1,27 @@
package biz
import "github.com/TheTNB/panel/internal/http/request"
import (
"time"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/ssh"
)
type SSH struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
Host string `gorm:"not null" json:"host"`
Port uint `gorm:"not null" json:"port"`
Config ssh.ClientConfig `gorm:"not null;serializer:json" json:"config"`
Remark string `gorm:"not null" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type SSHRepo interface {
GetInfo() (map[string]any, error)
UpdateInfo(req *request.SSHUpdateInfo) error
List(page, limit uint) ([]*SSH, int64, error)
Get(id uint) (*SSH, error)
Create(req *request.SSHCreate) error
Update(req *request.SSHUpdate) error
Delete(id uint) error
}
+1 -1
View File
@@ -28,5 +28,5 @@ type TaskRepo interface {
Delete(id uint) error
UpdateStatus(id uint, status TaskStatus) error
Push(task *Task) error
DispatchWaiting() error
DispatchWaiting()
}
+1 -2
View File
@@ -12,8 +12,7 @@ type Website struct {
Name string `gorm:"not null;unique" json:"name"`
Status bool `gorm:"not null;default:true" json:"status"`
Path string `gorm:"not null" json:"path"`
PHP int `gorm:"not null" json:"php"`
SSL bool `gorm:"not null" json:"ssl"`
Https bool `gorm:"not null" json:"https"`
Remark string `gorm:"not null" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
+1 -1
View File
@@ -19,8 +19,8 @@ func BootWeb() {
boot()
initValidator()
initSession()
initCron()
initQueue()
initCron()
initHttp()
select {}
+4 -1
View File
@@ -2,6 +2,7 @@ package bootstrap
import (
"context"
"fmt"
"os"
"strings"
@@ -13,6 +14,8 @@ import (
)
func initCli() {
app.IsCli = true
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "NAME", "名称")
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "USAGE", "用法")
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "VERSION", "版本")
@@ -43,6 +46,6 @@ func initCli() {
Commands: route.Cli(),
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
color.Redln(err.Error())
color.Redln(fmt.Sprintf("|-%v", err))
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ func initConf() {
func initGlobal() {
app.Root = app.Conf.MustString("app.root")
app.Version = "2.3.0"
app.Version = "2.3.11"
app.Locale = app.Conf.MustString("app.locale")
// 初始化时区
+1
View File
@@ -23,6 +23,7 @@ func initHttp() {
// add route
route.Http(app.Http)
route.Ws(app.Http)
apps.Boot(app.Http)
srv := &http.Server{
+4
View File
@@ -9,6 +9,7 @@ import (
"github.com/go-playground/validator/v10/translations/zh"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/http/rule"
)
func initValidator() {
@@ -23,4 +24,7 @@ func initValidator() {
app.Translator = &trans
app.Validator = validate
if err := rule.RegisterRules(validate); err != nil {
log.Fatalf("failed to register validator rules: %v", err)
}
}
+16 -12
View File
@@ -14,6 +14,7 @@ import (
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/pkg/api"
"github.com/TheTNB/panel/pkg/apploader"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
)
@@ -179,16 +180,17 @@ func (r *appRepo) Install(channel, slug string) error {
return err
}
if app.IsCli {
return shell.ExecfWithOutput(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion)
}
task := new(biz.Task)
task.Name = "安装应用 " + item.Name
task.Status = biz.TaskStatusWaiting
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
task.Log = "/tmp/" + item.Slug + ".log"
if err = r.taskRepo.Push(task); err != nil {
return err
}
return err
return r.taskRepo.Push(task)
}
func (r *appRepo) UnInstall(slug string) error {
@@ -233,16 +235,17 @@ func (r *appRepo) UnInstall(slug string) error {
return err
}
if app.IsCli {
return shell.ExecfWithOutput(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion)
}
task := new(biz.Task)
task.Name = "卸载应用 " + item.Name
task.Status = biz.TaskStatusWaiting
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
task.Log = "/tmp/" + item.Slug + ".log"
if err = r.taskRepo.Push(task); err != nil {
return err
}
return err
return r.taskRepo.Push(task)
}
func (r *appRepo) Update(slug string) error {
@@ -287,16 +290,17 @@ func (r *appRepo) Update(slug string) error {
return err
}
if app.IsCli {
return shell.ExecfWithOutput(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion)
}
task := new(biz.Task)
task.Name = "更新应用 " + item.Name
task.Status = biz.TaskStatusWaiting
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
task.Log = "/tmp/" + item.Slug + ".log"
if err = r.taskRepo.Push(task); err != nil {
return err
}
return err
return r.taskRepo.Push(task)
}
func (r *appRepo) UpdateShow(slug string, show bool) error {
+549
View File
@@ -0,0 +1,549 @@
package data
import (
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"time"
"github.com/gookit/color"
"github.com/shirou/gopsutil/disk"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"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/types"
)
type backupRepo struct {
setting biz.SettingRepo
website biz.WebsiteRepo
}
func NewBackupRepo() biz.BackupRepo {
return &backupRepo{
setting: NewSettingRepo(),
website: NewWebsiteRepo(),
}
}
// List 备份列表
func (r *backupRepo) List(typ biz.BackupType) ([]*types.BackupFile, error) {
path, err := r.GetPath(typ)
if err != nil {
return nil, err
}
files, err := io.ReadDir(path)
if err != nil {
return nil, err
}
list := make([]*types.BackupFile, 0)
for _, file := range files {
info, err := file.Info()
if err != nil {
continue
}
list = append(list, &types.BackupFile{
Name: file.Name(),
Path: filepath.Join(path, file.Name()),
Size: str.FormatBytes(float64(info.Size())),
Time: info.ModTime(),
})
}
return list, nil
}
// Create 创建备份
// typ 备份类型
// target 目标名称
// path 可选备份保存路径
func (r *backupRepo) Create(typ biz.BackupType, target string, path ...string) error {
defPath, err := r.GetPath(typ)
if err != nil {
return err
}
if len(path) > 0 && path[0] != "" {
defPath = path[0]
}
switch typ {
case biz.BackupTypeWebsite:
return r.createWebsite(defPath, target)
case biz.BackupTypeMySQL:
return r.createMySQL(defPath, target)
case biz.BackupTypePostgres:
return r.createPostgres(defPath, target)
case biz.BackupTypePanel:
return r.createPanel(defPath)
}
return errors.New("未知备份类型")
}
// Delete 删除备份
func (r *backupRepo) Delete(typ biz.BackupType, name string) error {
path, err := r.GetPath(typ)
if err != nil {
return err
}
file := filepath.Join(path, name)
return io.Remove(file)
}
// Restore 恢复备份
// typ 备份类型
// backup 备份压缩包,可以是绝对路径或者相对路径
// target 目标名称
func (r *backupRepo) Restore(typ biz.BackupType, backup, target string) error {
if !io.Exists(backup) {
path, err := r.GetPath(typ)
if err != nil {
return err
}
backup = filepath.Join(path, backup)
}
switch typ {
case biz.BackupTypeWebsite:
return r.restoreWebsite(backup, target)
case biz.BackupTypeMySQL:
return r.restoreMySQL(backup, target)
case biz.BackupTypePostgres:
return r.restorePostgres(backup, target)
}
return errors.New("未知备份类型")
}
// CutoffLog 切割日志
// path 保存目录绝对路径
// target 待切割日志文件绝对路径
func (r *backupRepo) CutoffLog(path, target string) error {
if !io.Exists(target) {
return errors.New("日志文件不存在")
}
to := filepath.Join(path, fmt.Sprintf("%s_%s.zip", time.Now().Format("20060102150405"), filepath.Base(target)))
if err := io.Compress([]string{target}, to, io.Zip); err != nil {
return err
}
return io.Remove(target)
}
// ClearExpired 清理过期备份
// path 备份目录绝对路径
// prefix 目标文件前缀
// save 保存份数
func (r *backupRepo) ClearExpired(path, prefix string, save int) error {
files, err := io.ReadDir(path)
if err != nil {
return err
}
var filtered []os.FileInfo
for _, file := range files {
if strings.HasPrefix(file.Name(), prefix) && strings.HasSuffix(file.Name(), ".zip") {
info, err := os.Stat(filepath.Join(path, file.Name()))
if err != nil {
continue
}
filtered = append(filtered, info)
}
}
// 排序所有备份文件,从新到旧
slices.SortFunc(filtered, func(a, b os.FileInfo) int {
if a.ModTime().After(b.ModTime()) {
return -1
}
if a.ModTime().Before(b.ModTime()) {
return 1
}
return 0
})
if len(filtered) <= save {
return nil
}
// 切片保留 save 份,删除剩余
toDelete := filtered[save:]
for _, file := range toDelete {
filePath := filepath.Join(path, file.Name())
if app.IsCli {
color.Greenln(fmt.Sprintf("|-清理过期文件:%s", filePath))
}
if err = os.Remove(filePath); err != nil {
if app.IsCli {
color.Redln(fmt.Sprintf("|-清理失败:%v", err))
} else {
return fmt.Errorf("清理失败:%v", err)
}
}
}
return nil
}
// GetPath 获取备份路径
func (r *backupRepo) GetPath(typ biz.BackupType) (string, error) {
backupPath, err := r.setting.Get(biz.SettingKeyBackupPath)
if err != nil {
return "", err
}
if !slices.Contains([]biz.BackupType{biz.BackupTypePath, biz.BackupTypeWebsite, biz.BackupTypeMySQL, biz.BackupTypePostgres, biz.BackupTypeRedis, biz.BackupTypePanel}, typ) {
return "", errors.New("未知备份类型")
}
backupPath = filepath.Join(backupPath, string(typ))
if !io.Exists(backupPath) {
if err = io.Mkdir(backupPath, 0644); err != nil {
return "", err
}
}
return backupPath, nil
}
// createWebsite 创建网站备份
func (r *backupRepo) createWebsite(to string, name string) error {
website, err := r.website.GetByName(name)
if err != nil {
return err
}
if err = r.preCheckPath(to, website.Path); err != nil {
return err
}
var paths []string
dirs, err := io.ReadDir(website.Path)
if err != nil {
return err
}
for _, item := range dirs {
paths = append(paths, filepath.Join(website.Path, item.Name()))
}
start := time.Now()
backup := filepath.Join(to, fmt.Sprintf("%s_%s.zip", website.Name, time.Now().Format("20060102150405")))
if err = io.Compress(paths, backup, io.Zip); err != nil {
return err
}
if app.IsCli {
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup)))
}
return nil
}
// createMySQL 创建 MySQL 备份
func (r *backupRepo) createMySQL(to string, name string) error {
rootPassword, err := r.setting.Get(biz.SettingKeyMySQLRootPassword)
if err != nil {
return err
}
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
if err != nil {
return err
}
if exist, _ := mysql.DatabaseExists(name); !exist {
return fmt.Errorf("数据库不存在:%s", name)
}
size, err := mysql.DatabaseSize(name)
if err != nil {
return err
}
if err = r.preCheckDB(to, size); err != nil {
return err
}
if err = os.Setenv("MYSQL_PWD", rootPassword); err != nil {
return err
}
start := time.Now()
backup := filepath.Join(to, fmt.Sprintf("%s_%s.sql", name, time.Now().Format("20060102150405")))
if _, err = shell.Execf(`mysqldump -u root '%s' > '%s'`, name, backup); err != nil {
return err
}
if err = os.Unsetenv("MYSQL_PWD"); err != nil {
return err
}
if err = io.Compress([]string{backup}, backup+".zip", io.Zip); err != nil {
return err
}
if err = io.Remove(backup); err != nil {
return err
}
if app.IsCli {
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup+".zip")))
}
return nil
}
// 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)
if err != nil {
return err
}
if exist, _ := postgres.DatabaseExist(name); !exist {
return fmt.Errorf("数据库不存在:%s", name)
}
size, err := postgres.DatabaseSize(name)
if err != nil {
return err
}
if err = r.preCheckDB(to, size); err != nil {
return err
}
start := time.Now()
backup := filepath.Join(to, fmt.Sprintf("%s_%s.sql", name, time.Now().Format("20060102150405")))
if _, err = shell.Execf(`su - postgres -c "pg_dump '%s'" > '%s'`, name, backup); err != nil {
return err
}
if err = io.Compress([]string{backup}, backup+".zip", io.Zip); err != nil {
return err
}
if err = io.Remove(backup); err != nil {
return err
}
if app.IsCli {
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup+".zip")))
}
return nil
}
// createPanel 创建面板备份
func (r *backupRepo) createPanel(to string) error {
backup := filepath.Join(to, fmt.Sprintf("panel_%s.zip", time.Now().Format("20060102150405")))
if err := r.preCheckPath(to, filepath.Join(app.Root, "panel")); err != nil {
return err
}
start := time.Now()
if err := io.Compress([]string{
filepath.Join(app.Root, "panel"),
"/usr/local/sbin/panel-cli",
"/usr/local/etc/panel/config.yml",
}, backup, io.Zip); err != nil {
return err
}
if err := io.Chmod(backup, 0600); err != nil {
return err
}
if app.IsCli {
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", filepath.Base(backup)))
}
return nil
}
// restoreWebsite 恢复网站备份
func (r *backupRepo) restoreWebsite(backup, target string) error {
if !io.Exists(backup) {
return errors.New("备份文件不存在")
}
website, err := r.website.GetByName(target)
if err != nil {
return err
}
format, err := io.FormatArchiveByPath(backup)
if err != nil {
return err
}
if err = io.Remove(website.Path); err != nil {
return err
}
if err = io.UnCompress(backup, website.Path, format); err != nil {
return err
}
if err = io.Chmod(website.Path, 0755); err != nil {
return err
}
if err = io.Chown(website.Path, "www", "www"); err != nil {
return err
}
return nil
}
// restoreMySQL 恢复 MySQL 备份
func (r *backupRepo) restoreMySQL(backup, target string) error {
if !io.Exists(backup) {
return errors.New("备份文件不存在")
}
rootPassword, err := r.setting.Get(biz.SettingKeyMySQLRootPassword)
if err != nil {
return err
}
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
if err != nil {
return err
}
if exist, _ := mysql.DatabaseExists(target); !exist {
return fmt.Errorf("数据库不存在:%s", target)
}
if err = os.Setenv("MYSQL_PWD", rootPassword); err != nil {
return err
}
if !strings.HasSuffix(backup, ".sql") {
backup, err = r.autoUnCompressSQL(backup)
if err != nil {
return err
}
}
if _, err = shell.Execf(`mysql -u root '%s' < '%s'`, target, backup); err != nil {
return err
}
if err = os.Unsetenv("MYSQL_PWD"); err != nil {
return err
}
_ = io.Remove(filepath.Dir(backup))
return nil
}
// restorePostgres 恢复 PostgreSQL 备份
func (r *backupRepo) restorePostgres(backup, target string) error {
if !io.Exists(backup) {
return errors.New("备份文件不存在")
}
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), 5432)
if err != nil {
return err
}
if exist, _ := postgres.DatabaseExist(target); !exist {
return fmt.Errorf("数据库不存在:%s", target)
}
if !strings.HasSuffix(backup, ".sql") {
backup, err = r.autoUnCompressSQL(backup)
if err != nil {
return err
}
}
if _, err = shell.Execf(`su - postgres -c "psql '%s'" < '%s'`, target, backup); err != nil {
return err
}
_ = io.Remove(filepath.Dir(backup))
return nil
}
// preCheckPath 预检空间和 inode 是否足够
// to 备份保存目录
// path 待备份目录
func (r *backupRepo) preCheckPath(to, path string) error {
size, err := io.SizeX(path)
if err != nil {
return err
}
files, err := io.CountX(path)
if err != nil {
return err
}
usage, err := disk.Usage(to)
if err != nil {
return err
}
if app.IsCli {
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
color.Greenln(fmt.Sprintf("|-目标文件数:%d", files))
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
color.Greenln(fmt.Sprintf("|-备份目录可用Inode%d", usage.InodesFree))
}
if uint64(size) > usage.Free {
return errors.New("备份目录空间不足")
}
if uint64(files) > usage.InodesFree {
return errors.New("备份目录Inode不足")
}
return nil
}
// preCheckDB 预检空间和 inode 是否足够
// to 备份保存目录
// size 数据库大小
func (r *backupRepo) preCheckDB(to string, size int64) error {
usage, err := disk.Usage(to)
if err != nil {
return err
}
if app.IsCli {
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
color.Greenln(fmt.Sprintf("|-备份目录可用Inode%d", usage.InodesFree))
}
if uint64(size) > usage.Free {
return errors.New("备份目录空间不足")
}
return nil
}
// autoUnCompressSQL 自动处理压缩文件
func (r *backupRepo) autoUnCompressSQL(backup string) (string, error) {
temp, err := io.TempDir(backup)
if err != nil {
return "", err
}
format, err := io.FormatArchiveByPath(backup)
if err != nil {
return "", err
}
if err = io.UnCompress(backup, temp, format); err != nil {
return "", err
}
backup = "" // 置空,防止干扰后续判断
if files, err := os.ReadDir(temp); err == nil {
if len(files) != 1 {
return "", fmt.Errorf("压缩文件中包含的文件数量不为1,实际为%d", len(files))
}
if strings.HasSuffix(files[0].Name(), ".sql") {
backup = filepath.Join(temp, files[0].Name())
}
}
if backup == "" {
return "", errors.New("无法找到.sql备份文件")
}
return backup, nil
}
+11 -7
View File
@@ -93,11 +93,11 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
}
}
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
client.UseHTTP(conf, cert.Website.Path)
client.UseHTTP(conf)
}
}
ssl, err := client.ObtainSSL(context.Background(), cert.Domains, acme.KeyType(cert.Type))
ssl, err := client.ObtainCertificate(context.Background(), cert.Domains, acme.KeyType(cert.Type))
if err != nil {
return nil, err
}
@@ -126,7 +126,7 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
return nil, errors.New("请重新获取 DNS 解析记录")
}
ssl, err := r.client.ObtainSSLManual()
ssl, err := r.client.ObtainCertificateManual()
if err != nil {
return nil, err
}
@@ -172,11 +172,11 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
}
}
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
client.UseHTTP(conf, cert.Website.Path)
client.UseHTTP(conf)
}
}
ssl, err := client.RenewSSL(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
ssl, err := client.RenewCertificate(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
if err != nil {
return nil, err
}
@@ -236,10 +236,10 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
return err
}
if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.pem", app.Root, website.Name), cert.Cert, 0644); err != nil {
if err = io.Write(fmt.Sprintf("%s/server/vhost/cert/%s.pem", app.Root, website.Name), cert.Cert, 0644); err != nil {
return err
}
if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.key", app.Root, website.Name), cert.Key, 0644); err != nil {
if err = io.Write(fmt.Sprintf("%s/server/vhost/cert/%s.key", app.Root, website.Name), cert.Key, 0644); err != nil {
return err
}
if err = systemctl.Reload("nginx"); err != nil {
@@ -251,6 +251,10 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
}
func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) {
if cert.Account == nil {
return nil, errors.New("该证书没有关联账号,无法签发")
}
var ca string
var eab *acme.EAB
switch cert.Account.CA {
+22 -48
View File
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"path/filepath"
"regexp"
"strconv"
"time"
@@ -54,32 +53,28 @@ func (r *cronRepo) Get(id uint) (*biz.Cron, error) {
}
func (r *cronRepo) Create(req *request.CronCreate) error {
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) {
return errors.New("时间格式错误")
}
var script string
if req.Type == "backup" {
if len(req.BackupPath) == 0 {
req.BackupPath, _ = r.settingRepo.Get(biz.SettingKeyBackupPath)
if len(req.BackupPath) == 0 {
return errors.New("备份路径不能为空")
}
req.BackupPath = filepath.Join(req.BackupPath, req.BackupType)
}
script = fmt.Sprintf(`#!/bin/bash
if req.BackupType == "website" {
script = fmt.Sprintf(`#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子面板 - 数据备份脚本
# 耗子面板 - 网站备份脚本
type=%s
path=%s
name=%s
save=%d
panel-cli backup website -n '%s' -p '%s'
panel-cli backup clear -t website -f '%s' -s '%d' -p '%s'
`, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath)
}
if req.BackupType == "mysql" || req.BackupType == "postgres" {
script = fmt.Sprintf(`#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 执行备份
panel backup ${type} ${name} ${path} ${save}
`, req.BackupType, req.BackupPath, req.Target, req.Save)
# 耗子面板 - 数据库备份脚本
panel-cli backup database -t '%s' -n '%s' -p '%s'
panel-cli backup clear -t '%s' -f '%s' -s '%d' -p '%s'
`, req.BackupType, req.Target, req.BackupPath, req.BackupType, req.Target, req.Save, req.BackupPath)
}
}
if req.Type == "cutoff" {
script = fmt.Sprintf(`#!/bin/bash
@@ -87,12 +82,13 @@ export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子面板 - 日志切割脚本
name=%s
save=%d
# 执行切割
panel cutoff ${name} ${save}
`, req.Target, req.Save)
panel-cli cutoff website -n %s -p %s
panel-cli cutoff clear -t website -f %s -s %d -p %s
`, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath)
}
if req.Type == "shell" {
script = req.Script
}
shellDir := fmt.Sprintf("%s/server/cron/", app.Root)
@@ -135,10 +131,6 @@ func (r *cronRepo) Update(req *request.CronUpdate) error {
return err
}
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) {
return errors.New("时间格式错误")
}
if !cron.Status {
return errors.New("计划任务已禁用")
}
@@ -202,24 +194,6 @@ func (r *cronRepo) Status(id uint, status bool) error {
return app.Orm.Save(cron).Error
}
func (r *cronRepo) Log(id uint) (string, error) {
cron, err := r.Get(id)
if err != nil {
return "", err
}
if !io.Exists(cron.Log) {
return "", errors.New("日志文件不存在")
}
log, err := shell.Execf("tail -n 1000 '%s'", cron.Log)
if err != nil {
return "", err
}
return log, nil
}
// addToSystem 添加到系统
func (r *cronRepo) addToSystem(cron *biz.Cron) error {
if _, err := shell.Execf(`( crontab -l; echo "%s %s >> %s 2>&1" ) | sort - | uniq - | crontab -`, cron.Time, cron.Shell, cron.Log); err != nil {
+1 -1
View File
@@ -50,7 +50,7 @@ func (r monitorRepo) UpdateSetting(setting *request.MonitorSetting) error {
}
func (r monitorRepo) Clear() error {
return app.Orm.Delete(&biz.Monitor{}).Error
return app.Orm.Where("1 = 1").Delete(&biz.Monitor{}).Error
}
func (r monitorRepo) List(start, end time.Time) ([]*biz.Monitor, error) {
+1 -1
View File
@@ -60,7 +60,7 @@ func (r *safeRepo) UpdateSSH(port uint, status bool) error {
}
func (r *safeRepo) GetPingStatus() (bool, error) {
out, err := shell.Execf(`firewall-cmd --list-all`)
out, err := shell.Execf(`firewall-cmd --list-rich-rules`)
if err != nil {
return true, errors.New(out)
}
+312 -103
View File
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"path/filepath"
"time"
"slices"
"github.com/go-rat/utils/hash"
"github.com/goccy/go-yaml"
@@ -16,15 +16,21 @@ import (
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/firewall"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
)
type settingRepo struct{}
type settingRepo struct {
taskRepo biz.TaskRepo
}
func NewSettingRepo() biz.SettingRepo {
return &settingRepo{}
return &settingRepo{
taskRepo: NewTaskRepo(),
}
}
func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, error) {
@@ -42,6 +48,21 @@ func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, e
return setting.Value, nil
}
func (r *settingRepo) GetBool(key biz.SettingKey, defaultValue ...bool) (bool, error) {
setting := new(biz.Setting)
if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return false, err
}
}
if setting.Value == "" && len(defaultValue) > 0 {
return defaultValue[0], nil
}
return cast.ToBool(setting.Value), nil
}
func (r *settingRepo) Set(key biz.SettingKey, value string) error {
setting := new(biz.Setting)
if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil {
@@ -65,16 +86,20 @@ func (r *settingRepo) Delete(key biz.SettingKey) error {
}
func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSetting, error) {
name := new(biz.Setting)
if err := app.Orm.Where("key = ?", biz.SettingKeyName).First(name).Error; err != nil {
name, err := r.Get(biz.SettingKeyName)
if err != nil {
return nil, err
}
websitePath := new(biz.Setting)
if err := app.Orm.Where("key = ?", biz.SettingKeyWebsitePath).First(websitePath).Error; err != nil {
offlineMode, err := r.Get(biz.SettingKeyOfflineMode)
if err != nil {
return nil, err
}
backupPath := new(biz.Setting)
if err := app.Orm.Where("key = ?", biz.SettingKeyBackupPath).First(backupPath).Error; err != nil {
websitePath, err := r.Get(biz.SettingKeyWebsitePath)
if err != nil {
return nil, err
}
backupPath, err := r.Get(biz.SettingKeyBackupPath)
if err != nil {
return nil, err
}
@@ -94,11 +119,12 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
}
return &request.PanelSetting{
Name: name.Value,
Name: name,
Locale: app.Conf.String("app.locale"),
Entrance: app.Conf.String("http.entrance"),
WebsitePath: websitePath.Value,
BackupPath: backupPath.Value,
OfflineMode: cast.ToBool(offlineMode),
WebsitePath: websitePath,
BackupPath: backupPath,
Username: user.Username,
Email: user.Email,
Port: app.Conf.Int("http.port"),
@@ -112,6 +138,9 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
if err := r.Set(biz.SettingKeyName, setting.Name); err != nil {
return false, err
}
if err := r.Set(biz.SettingKeyOfflineMode, cast.ToString(setting.OfflineMode)); err != nil {
return false, err
}
if err := r.Set(biz.SettingKeyWebsitePath, setting.WebsitePath); err != nil {
return false, err
}
@@ -119,6 +148,37 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
return false, err
}
// 用户
user := new(biz.User)
userID := cast.ToUint(ctx.Value("user_id"))
if err := app.Orm.Where("id = ?", userID).First(user).Error; err != nil {
return false, err
}
user.Username = setting.Username
user.Email = setting.Email
if setting.Password != "" {
value, err := hash.NewArgon2id().Make(setting.Password)
if err != nil {
return false, err
}
user.Password = value
}
if err := app.Orm.Save(user).Error; err != nil {
return false, err
}
// 下面是需要需要重启的设置
// 面板HTTPS
restartFlag := false
oldCert, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
oldKey, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.key"))
if oldCert != setting.Cert || oldKey != setting.Key {
if r.taskRepo.HasRunningTask() {
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
}
restartFlag = true
}
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), setting.Cert, 0644); err != nil {
return false, err
}
@@ -126,7 +186,7 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
return false, err
}
restartFlag := false
// 面板主配置
config := new(types.PanelConfig)
cm := yaml.CommentMap{}
raw, err := io.Read("/usr/local/etc/panel/config.yml")
@@ -142,33 +202,30 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
config.HTTP.Entrance = setting.Entrance
config.HTTP.TLS = setting.HTTPS
// 放行端口
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 false, err
}
encoded, err := yaml.MarshalWithOptions(config, yaml.WithComment(cm))
if err != nil {
return false, err
}
if err = io.Write("/usr/local/etc/panel/config.yml", string(encoded), 0644); err != nil {
return false, err
}
if raw != string(encoded) {
if r.taskRepo.HasRunningTask() {
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
}
restartFlag = true
}
user := new(biz.User)
userID := cast.ToUint(ctx.Value("user_id"))
if err = app.Orm.Where("id = ?", userID).First(user).Error; err != nil {
return false, err
}
user.Username = setting.Username
user.Email = setting.Email
if setting.Password != "" {
value, err := hash.NewArgon2id().Make(setting.Password)
if err != nil {
return false, err
}
user.Password = value
}
if err = app.Orm.Save(user).Error; err != nil {
if err = io.Write("/usr/local/etc/panel/config.yml", string(encoded), 0644); err != nil {
return false, err
}
@@ -176,112 +233,264 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
}
func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
name := filepath.Base(url)
color.Greenln("目标版本: ", version)
color.Greenln("下载链接: ", url)
color.Greenln("文件名: ", name)
color.Greenln("前置检查...")
if io.Exists("/tmp/panel-storage.zip") {
return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败导致的,请排除后重试")
// 预先优化数据库
if err := app.Orm.Exec("VACUUM").Error; err != nil {
return err
}
if err := app.Orm.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
return err
}
color.Greenln("备份面板数据...")
name := filepath.Base(url)
if app.IsCli {
color.Greenln(fmt.Sprintf("|-目标版本:%s", version))
color.Greenln(fmt.Sprintf("|-下载链接:%s", url))
color.Greenln(fmt.Sprintf("|-文件名:%s", name))
}
if app.IsCli {
color.Greenln("|-正在下载...")
}
if _, err := shell.Execf("wget -T 120 -t 3 -O /tmp/%s %s", name, url); err != nil {
return fmt.Errorf("下载失败:%w", err)
}
if _, err := shell.Execf("wget -T 20 -t 3 -O /tmp/%s %s", name+".sha256", checksum); err != nil {
return fmt.Errorf("下载失败:%w", err)
}
if !io.Exists(filepath.Join("/tmp", name)) || !io.Exists(filepath.Join("/tmp", name+".sha256")) {
return errors.New("下载文件检查失败")
}
if app.IsCli {
color.Greenln("|-校验下载文件...")
}
if check, err := shell.Execf("cd /tmp && sha256sum -c %s --ignore-missing", name+".sha256"); check != name+": OK" || err != nil {
return errors.New("下载文件校验失败")
}
if err := io.Remove(filepath.Join("/tmp", name+".sha256")); err != nil {
if app.IsCli {
color.Redln("|-清理校验文件失败:", err)
}
return fmt.Errorf("清理校验文件失败:%w", err)
}
if app.IsCli {
color.Greenln("|-前置检查...")
}
if io.Exists("/tmp/panel-storage.zip") {
return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败所致,请运行 panel-cli fix 修复后重试")
}
if app.IsCli {
color.Greenln("|-备份面板数据...")
}
// 备份面板
if err := io.Compress([]string{filepath.Join(app.Root, "panel")}, filepath.Join(app.Root, fmt.Sprintf("backup/panel/panel-%s.zip", time.Now().Format("20060102150405"))), io.Zip); err != nil {
color.Redln("备份面板失败:", err)
return err
backup := NewBackupRepo()
if err := backup.Create(biz.BackupTypePanel, ""); err != nil {
if app.IsCli {
color.Redln("|-备份面板失败:", err)
}
return fmt.Errorf("备份面板失败:%w", err)
}
if err := io.Compress([]string{filepath.Join(app.Root, "panel/storage")}, "/tmp/panel-storage.zip", io.Zip); err != nil {
color.Redln("备份面板数据失败:", err)
return err
if app.IsCli {
color.Redln("|-备份面板数据失败:", err)
}
return fmt.Errorf("备份面板数据失败:%w", err)
}
if !io.Exists("/tmp/panel-storage.zip") {
return errors.New("已备份面板数据检查失败")
}
color.Greenln("备份完成")
color.Greenln("清理旧版本...")
if app.IsCli {
color.Greenln("|-清理旧版本...")
}
if _, err := shell.Execf("rm -rf %s/panel/*", app.Root); err != nil {
color.Redln("清理旧版本失败:", err)
return err
return fmt.Errorf("清理旧版本失败:%w", err)
}
color.Greenln("清理完成")
color.Greenln("正在下载...")
if _, err := shell.Execf("wget -T 120 -t 3 -O %s/panel/%s %s", app.Root, name, url); err != nil {
color.Redln("下载失败:", err)
return err
if app.IsCli {
color.Greenln("|-解压新版本...")
}
if _, err := shell.Execf("wget -T 20 -t 3 -O %s/panel/%s %s", app.Root, name+".sha256", checksum); err != nil {
color.Redln("下载失败:", err)
return err
}
if !io.Exists(filepath.Join(app.Root, "panel", name)) || !io.Exists(filepath.Join(app.Root, "panel", name+".sha256")) {
return errors.New("下载文件检查失败")
}
color.Greenln("下载完成")
color.Greenln("校验下载文件...")
check, err := shell.Execf("cd %s/panel && sha256sum -c %s --ignore-missing", app.Root, name+".sha256")
if check != name+": OK" || err != nil {
return errors.New("下载文件校验失败")
}
if err = io.Remove(filepath.Join(app.Root, "panel", name+".sha256")); err != nil {
color.Redln("清理校验文件失败:", err)
return err
}
color.Greenln("文件校验完成")
color.Greenln("更新新版本...")
if _, err = shell.Execf("cd %s/panel && unzip -o %s && rm -rf %s", app.Root, name, name); err != nil {
color.Redln("更新失败:", err)
return err
if err := io.UnCompress(filepath.Join("/tmp", name), filepath.Join(app.Root, "panel"), io.Zip); err != nil {
return fmt.Errorf("解压失败:%w", err)
}
if !io.Exists(filepath.Join(app.Root, "panel", "web")) {
return errors.New("更新失败,可能是下载过程中出现了问题")
return errors.New("解压失败,缺失文件")
}
color.Greenln("更新完成")
color.Greenln("恢复面板数据...")
if err = io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel"), io.Zip); err != nil {
color.Redln("恢复面板数据失败:", err)
return err
if app.IsCli {
color.Greenln("|-恢复面板数据...")
}
if err := io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel"), io.Zip); err != nil {
return fmt.Errorf("恢复面板数据失败:%w", err)
}
if !io.Exists(filepath.Join(app.Root, "panel/storage/app.db")) {
return errors.New("恢复面板数据失败")
}
color.Greenln("恢复完成")
color.Greenln("运行升级后脚本...")
if _, err = shell.Execf("curl -fsLm 10 https://dl.cdn.haozi.net/panel/auto_update.sh | bash"); err != nil {
color.Redln("运行面板升级后脚本失败:", err)
return err
if app.IsCli {
color.Greenln("|-运行更新后脚本...")
}
if _, err = shell.Execf(`wget -O /etc/systemd/system/panel.service https://dl.cdn.haozi.net/panel/panel.service && sed -i "s|/www|%s|g" /etc/systemd/system/panel.service`, app.Root); err != nil {
color.Redln("下载面板服务文件失败:", err)
return err
if _, err := shell.Execf("curl -fsLm 10 https://dl.cdn.haozi.net/panel/auto_update.sh | bash"); err != nil {
return fmt.Errorf("运行面板更新后脚本失败:%w", err)
}
if _, err = shell.Execf("panel-cli setting write version %s", version); err != nil {
color.Redln("写入面板版本号失败:", err)
return err
if _, err := shell.Execf(`wget -O /etc/systemd/system/panel.service https://dl.cdn.haozi.net/panel/panel.service && sed -i "s|/www|%s|g" /etc/systemd/system/panel.service`, app.Root); err != nil {
return fmt.Errorf("下载面板服务文件失败:%w", err)
}
if err = io.Mv(filepath.Join(app.Root, "panel/cli"), "/usr/local/sbin/panel-cli"); err != nil {
color.Redln("移动面板命令行工具失败:", err)
return err
if _, err := shell.Execf("panel-cli setting write version %s", version); err != nil {
return fmt.Errorf("写入面板版本号失败:%w", err)
}
if err := io.Mv(filepath.Join(app.Root, "panel/cli"), "/usr/local/sbin/panel-cli"); err != nil {
return fmt.Errorf("移动面板命令行工具失败:%w", err)
}
color.Greenln("设置面板文件权限...")
if app.IsCli {
color.Greenln("|-设置关键文件权限...")
}
_ = io.Chmod("/usr/local/sbin/panel-cli", 0700)
_ = io.Chmod("/etc/systemd/system/panel.service", 0700)
_ = io.Chmod(filepath.Join(app.Root, "panel"), 0700)
color.Greenln("设置完成")
color.Greenln("升级完成")
if app.IsCli {
color.Greenln("|-更新完成")
}
_, _ = shell.Execf("systemctl daemon-reload")
_ = io.Remove("/tmp/panel-storage.zip")
_ = io.Remove(filepath.Join(app.Root, "panel/config.example.yml"))
tools.RestartPanel()
return nil
}
func (r *settingRepo) FixPanel() error {
if app.IsCli {
color.Greenln("|-开始修复面板...")
}
// 检查关键文件是否正常
flag := false
if !io.Exists(filepath.Join(app.Root, "panel", "web")) {
flag = true
}
if !io.Exists(filepath.Join(app.Root, "panel", "storage", "app.db")) {
flag = true
}
if io.Exists("/tmp/panel-storage.zip") {
flag = true
}
if !flag {
return fmt.Errorf("文件正常无需修复,请运行 panel-cli update 更新面板")
}
// 再次确认是否需要修复
if io.Exists("/tmp/panel-storage.zip") {
// 文件齐全情况下只移除临时文件
if io.Exists(filepath.Join(app.Root, "panel", "web")) &&
io.Exists(filepath.Join(app.Root, "panel", "storage", "app.db")) &&
io.Exists("/usr/local/etc/panel/config.yml") {
if err := io.Remove("/tmp/panel-storage.zip"); err != nil {
return fmt.Errorf("清理临时文件失败:%w", err)
}
if app.IsCli {
color.Greenln("已清理临时文件,请运行 panel-cli update 更新面板")
}
return nil
}
}
// 从备份目录中找最新的备份文件
backup := NewBackupRepo()
list, err := backup.List(biz.BackupTypePanel)
if err != nil {
return err
}
slices.SortFunc(list, func(a *types.BackupFile, b *types.BackupFile) int {
return int(b.Time.Unix() - a.Time.Unix())
})
if len(list) == 0 {
return fmt.Errorf("未找到备份文件,无法自动修复")
}
latest := list[0]
if app.IsCli {
color.Greenln(fmt.Sprintf("|-使用备份文件:%s", latest.Name))
}
// 解压备份文件
if app.IsCli {
color.Greenln("|-解压备份文件...")
}
if err = io.Remove("/tmp/panel-fix"); err != nil {
return fmt.Errorf("清理临时目录失败:%w", err)
}
if err = io.UnCompress(latest.Path, "/tmp/panel-fix", io.Zip); err != nil {
return fmt.Errorf("解压备份文件失败:%w", err)
}
// 移动文件到对应位置
if app.IsCli {
color.Greenln("|-移动备份文件...")
}
if io.Exists(filepath.Join("/tmp/panel-fix", "panel")) && io.IsDir(filepath.Join("/tmp/panel-fix", "panel")) {
if err = io.Remove(filepath.Join(app.Root, "panel")); err != nil {
return fmt.Errorf("删除目录失败:%w", err)
}
if err = io.Mv(filepath.Join("/tmp/panel-fix", "panel"), filepath.Join(app.Root)); err != nil {
return fmt.Errorf("移动目录失败:%w", err)
}
}
if io.Exists(filepath.Join("/tmp/panel-fix", "config.yml")) {
if err = io.Mv(filepath.Join("/tmp/panel-fix", "config.yml"), "/usr/local/etc/panel/config.yml"); err != nil {
return fmt.Errorf("移动文件失败:%w", err)
}
}
if io.Exists(filepath.Join("/tmp/panel-fix", "panel-cli")) {
if err = io.Mv(filepath.Join("/tmp/panel-fix", "panel-cli"), "/usr/local/sbin/panel-cli"); err != nil {
return fmt.Errorf("移动文件失败:%w", err)
}
}
// tmp目录下如果有storage备份,则解压回去
if app.IsCli {
color.Greenln("|-恢复面板数据...")
}
if io.Exists("/tmp/panel-storage.zip") {
if err = io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel"), io.Zip); err != nil {
return fmt.Errorf("恢复面板数据失败:%w", err)
}
if err = io.Remove("/tmp/panel-storage.zip"); err != nil {
return fmt.Errorf("清理临时文件失败:%w", err)
}
}
// 下载服务文件
if !io.Exists("/etc/systemd/system/panel.service") {
if _, err = shell.Execf(`wget -O /etc/systemd/system/panel.service https://dl.cdn.haozi.net/panel/panel.service && sed -i "s|/www|%s|g" /etc/systemd/system/panel.service`, app.Root); err != nil {
return err
}
}
// 处理权限
if app.IsCli {
color.Greenln("|-设置关键文件权限...")
}
if err = io.Chmod("/usr/local/etc/panel/config.yml", 0600); err != nil {
return err
}
if err = io.Chmod("/etc/systemd/system/panel.service", 0700); err != nil {
return err
}
if err = io.Chmod("/usr/local/sbin/panel-cli", 0700); err != nil {
return err
}
if err = io.Chmod(filepath.Join(app.Root, "panel"), 0700); err != nil {
return err
}
if app.IsCli {
color.Greenln("|-修复完成")
}
tools.RestartPanel()
return nil
}
+66 -31
View File
@@ -1,12 +1,12 @@
package data
import (
"errors"
"github.com/spf13/cast"
"fmt"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
pkgssh "github.com/TheTNB/panel/pkg/ssh"
)
type sshRepo struct {
@@ -19,36 +19,71 @@ func NewSSHRepo() biz.SSHRepo {
}
}
func (r *sshRepo) GetInfo() (map[string]any, error) {
host, _ := r.settingRepo.Get(biz.SettingKeySshHost)
port, _ := r.settingRepo.Get(biz.SettingKeySshPort)
user, _ := r.settingRepo.Get(biz.SettingKeySshUser)
password, _ := r.settingRepo.Get(biz.SettingKeySshPassword)
if len(host) == 0 || len(user) == 0 || len(password) == 0 {
return nil, errors.New("SSH 配置不完整")
}
return map[string]any{
"host": host,
"port": cast.ToInt(port),
"user": user,
"password": password,
}, nil
func (r *sshRepo) List(page, limit uint) ([]*biz.SSH, int64, error) {
var ssh []*biz.SSH
var total int64
err := app.Orm.Model(&biz.SSH{}).Omit("Hosts").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&ssh).Error
return ssh, total, err
}
func (r *sshRepo) UpdateInfo(req *request.SSHUpdateInfo) error {
if err := r.settingRepo.Set(biz.SettingKeySshHost, req.Host); err != nil {
return err
}
if err := r.settingRepo.Set(biz.SettingKeySshPort, cast.ToString(req.Port)); err != nil {
return err
}
if err := r.settingRepo.Set(biz.SettingKeySshUser, req.User); err != nil {
return err
}
if err := r.settingRepo.Set(biz.SettingKeySshPassword, req.Password); err != nil {
return err
func (r *sshRepo) Get(id uint) (*biz.SSH, error) {
ssh := new(biz.SSH)
if err := app.Orm.Where("id = ?", id).First(ssh).Error; err != nil {
return nil, err
}
return nil
return ssh, nil
}
func (r *sshRepo) Create(req *request.SSHCreate) error {
conf := pkgssh.ClientConfig{
AuthMethod: pkgssh.AuthMethod(req.AuthMethod),
Host: fmt.Sprintf("%s:%d", req.Host, req.Port),
User: req.User,
Password: req.Password,
Key: req.Key,
}
_, err := pkgssh.NewSSHClient(conf)
if err != nil {
return fmt.Errorf("failed to check ssh connection: %v", err)
}
ssh := &biz.SSH{
Name: req.Name,
Host: req.Host,
Port: req.Port,
Config: conf,
Remark: req.Remark,
}
return app.Orm.Create(ssh).Error
}
func (r *sshRepo) Update(req *request.SSHUpdate) error {
conf := pkgssh.ClientConfig{
AuthMethod: pkgssh.AuthMethod(req.AuthMethod),
Host: fmt.Sprintf("%s:%d", req.Host, req.Port),
User: req.User,
Password: req.Password,
Key: req.Key,
}
_, err := pkgssh.NewSSHClient(conf)
if err != nil {
return fmt.Errorf("failed to check ssh connection: %v", err)
}
ssh := &biz.SSH{
ID: req.ID,
Name: req.Name,
Host: req.Host,
Port: req.Port,
Config: conf,
Remark: req.Remark,
}
return app.Orm.Model(ssh).Updates(ssh).Error
}
func (r *sshRepo) Delete(id uint) error {
return app.Orm.Delete(&biz.SSH{}, id).Error
}
+21 -6
View File
@@ -1,14 +1,22 @@
package data
import (
"sync"
"go.uber.org/zap"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/queuejob"
)
var taskDispatchOnce sync.Once
type taskRepo struct{}
func NewTaskRepo() biz.TaskRepo {
task := &taskRepo{}
taskDispatchOnce.Do(task.DispatchWaiting)
return &taskRepo{}
}
@@ -48,17 +56,24 @@ func (r *taskRepo) Push(task *biz.Task) error {
})
}
func (r *taskRepo) DispatchWaiting() error {
func (r *taskRepo) DispatchWaiting() {
// cli下不处理
if app.IsCli {
return
}
var tasks []biz.Task
if err := app.Orm.Where("status = ?", biz.TaskStatusWaiting).Find(&tasks).Error; err != nil {
return err
app.Logger.Error("获取待处理任务失败", zap.Error(err))
return
}
for _, task := range tasks {
if err := r.Push(&task); err != nil {
return err
if err := app.Queue.Push(queuejob.NewProcessTask(r), []any{
task.ID,
}); err != nil {
app.Logger.Error("推送任务失败", zap.Error(err))
return
}
}
return nil
}
+334 -408
View File
@@ -4,12 +4,9 @@ import (
"errors"
"fmt"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"github.com/spf13/cast"
"github.com/samber/lo"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
@@ -18,8 +15,9 @@ import (
"github.com/TheTNB/panel/pkg/cert"
"github.com/TheTNB/panel/pkg/db"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/nginx"
"github.com/TheTNB/panel/pkg/punycode"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/systemctl"
"github.com/TheTNB/panel/pkg/types"
)
@@ -59,78 +57,84 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
if err := app.Orm.Where("id", id).First(website).Error; err != nil {
return nil, err
}
// 解析nginx配置
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
if err != nil {
return nil, err
}
p, err := nginx.NewParser(config)
if err != nil {
return nil, err
}
setting := new(types.WebsiteSetting)
setting.ID = website.ID
setting.Name = website.Name
setting.Path = website.Path
setting.SSL = website.SSL
setting.PHP = website.PHP
setting.HTTPS = website.Https
setting.PHP = p.GetPHP()
setting.Raw = config
portStr := str.Cut(config, "# port标记位开始", "# port标记位结束")
matches := regexp.MustCompile(`listen\s+([^;]*);?`).FindAllStringSubmatch(portStr, -1)
for _, match := range matches {
if len(match) < 2 {
continue
}
// 跳过 ipv6
if strings.Contains(match[1], "[::]") {
continue
}
// 处理 443 ssl 之类的情况
ports := strings.Fields(match[1])
if len(ports) == 0 {
continue
}
if !slices.Contains(setting.Ports, cast.ToUint(ports[0])) {
setting.Ports = append(setting.Ports, cast.ToUint(ports[0]))
}
if len(ports) > 1 && ports[1] == "ssl" {
setting.SSLPorts = append(setting.SSLPorts, cast.ToUint(ports[0]))
} else if len(ports) > 1 && ports[1] == "quic" {
setting.QUICPorts = append(setting.QUICPorts, cast.ToUint(ports[0]))
}
// 监听地址
listens, err := p.GetListen()
if err != nil {
return nil, err
}
serverName := str.Cut(config, "# server_name标记位开始", "# server_name标记位结束")
match := regexp.MustCompile(`server_name\s+([^;]*);?`).FindStringSubmatch(serverName)
if len(match) > 1 {
setting.Domains = strings.Split(match[1], " ")
setting.Listens = lo.Map(
lo.UniqBy(listens, func(listen []string) string {
if len(listen) == 0 {
return ""
}
return listen[0]
}),
func(listen []string, _ int) types.WebsiteListen {
addr := listen[0]
grouped := lo.GroupBy(listens, func(listen []string) string {
if len(listen) == 0 {
return ""
}
return listen[0]
})[addr]
return types.WebsiteListen{
Address: addr,
HTTPS: lo.SomeBy(grouped, func(listen []string) bool { return lo.Contains(listen, "ssl") }),
QUIC: lo.SomeBy(grouped, func(listen []string) bool { return lo.Contains(listen, "quic") }),
}
},
)
// 域名
domains, err := p.GetServerName()
if err != nil {
return nil, err
}
root := str.Cut(config, "# root标记位开始", "# root标记位结束")
match = regexp.MustCompile(`root\s+([^;]*);?`).FindStringSubmatch(root)
if len(match) > 1 {
setting.Root = match[1]
domains, err = punycode.DecodeDomains(domains)
if err != nil {
return nil, err
}
index := str.Cut(config, "# index标记位开始", "# index标记位结束")
match = regexp.MustCompile(`index\s+([^;]*);?`).FindStringSubmatch(index)
if len(match) > 1 {
setting.Index = match[1]
}
setting.Domains = domains
// 运行目录
root, _ := p.GetRoot()
setting.Root = root
// 默认文档
index, _ := p.GetIndex()
setting.Index = index
// 防跨站
if io.Exists(filepath.Join(setting.Root, ".user.ini")) {
userIni, _ := io.Read(filepath.Join(setting.Root, ".user.ini"))
if strings.Contains(userIni, "open_basedir") {
setting.OpenBasedir = true
}
}
crt, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"))
setting.SSLCertificate = crt
key, _ := io.Read(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"))
setting.SSLCertificateKey = key
if setting.SSL {
ssl := str.Cut(config, "# ssl标记位开始", "# ssl标记位结束")
setting.HTTPRedirect = strings.Contains(ssl, "# http重定向标记位")
setting.HSTS = strings.Contains(ssl, "# hsts标记位")
setting.OCSP = strings.Contains(ssl, "# ocsp标记位")
// HTTPS
if setting.HTTPS {
setting.HTTPRedirect = p.GetHTTPSRedirect()
setting.HSTS = p.GetHSTS()
setting.OCSP = p.GetOCSP()
}
// 证书
crt, _ := io.Read(filepath.Join(app.Root, "server/vhost/cert", website.Name+".pem"))
setting.SSLCertificate = crt
key, _ := io.Read(filepath.Join(app.Root, "server/vhost/cert", website.Name+".key"))
setting.SSLCertificateKey = key
// 解析证书信息
if decode, err := cert.ParseCert(crt); err == nil {
setting.SSLNotBefore = decode.NotBefore.Format("2006-01-02 15:04:05")
@@ -139,11 +143,11 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
setting.SSLOCSPServer = decode.OCSPServer
setting.SSLDNSNames = decode.DNSNames
}
// 伪静态
rewrite, _ := io.Read(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
setting.Rewrite = rewrite
log, _ := shell.Execf(`tail -n 100 '%s/wwwlogs/%s.log'`, app.Root, website.Name)
setting.Log = log
// 访问日志
setting.Log = fmt.Sprintf("%s/wwwlogs/%s.log", app.Root, website.Name)
return setting, err
}
@@ -174,21 +178,59 @@ func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) {
}
func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
w := &biz.Website{
Name: req.Name,
Status: true,
Path: req.Path,
PHP: req.PHP,
SSL: false,
// 初始化nginx配置
p, err := nginx.NewParser()
if err != nil {
return nil, err
}
if err := app.Orm.Create(w).Error; err != nil {
// 监听地址
var listens [][]string
for _, listen := range req.Listens {
listens = append(listens, []string{listen})
}
if err = p.SetListen(listens); err != nil {
return nil, err
}
// 域名
domains, err := punycode.EncodeDomains(req.Domains)
if err != nil {
return nil, err
}
if err = p.SetServerName(domains); err != nil {
return nil, err
}
// 运行目录
if err = p.SetRoot(req.Path); err != nil {
return nil, err
}
// PHP
if err = p.SetPHP(req.PHP); err != nil {
return nil, err
}
// 伪静态和acme
includes, comments, err := p.GetIncludes()
if err != nil {
return nil, err
}
includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"))
includes = append(includes, filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"))
comments = append(comments, []string{"# 伪静态规则"})
comments = append(comments, []string{"# acme http-01"})
if err = p.SetIncludes(includes, comments); err != nil {
return nil, err
}
// 日志
if err = p.SetAccessLog(filepath.Join(app.Root, "wwwlogs", req.Name+".log")); err != nil {
return nil, err
}
if err = p.SetErrorLog(filepath.Join(app.Root, "wwwlogs", req.Name+".error.log")); err != nil {
return nil, err
}
if err := io.Mkdir(req.Path, 0755); err != nil {
// 初始化网站目录
if err = io.Mkdir(req.Path, 0755); err != nil {
return nil, err
}
index, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "index.html"))
if err != nil {
return nil, fmt.Errorf("获取index模板文件失败: %w", err)
@@ -196,92 +238,16 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
if err = io.Write(filepath.Join(req.Path, "index.html"), string(index), 0644); err != nil {
return nil, err
}
notFound, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "404.html"))
if err != nil {
return nil, fmt.Errorf("获取404模板文件失败: %w", err)
}
if err = io.Write(req.Path+"/404.html", string(notFound), 0644); err != nil {
if err = io.Write(filepath.Join(req.Path, "404.html"), string(notFound), 0644); err != nil {
return nil, err
}
portList := ""
domainList := ""
portUsed := make(map[uint]bool)
domainUsed := make(map[string]bool)
for i, port := range req.Ports {
if _, ok := portUsed[port]; !ok {
if i == len(req.Ports)-1 {
portList += " listen " + cast.ToString(port) + ";\n"
portList += " listen [::]:" + cast.ToString(port) + ";"
} else {
portList += " listen " + cast.ToString(port) + ";\n"
portList += " listen [::]:" + cast.ToString(port) + ";\n"
}
portUsed[port] = true
}
}
for _, domain := range req.Domains {
if _, ok := domainUsed[domain]; !ok {
domainList += " " + domain
domainUsed[domain] = true
}
}
nginxConf := fmt.Sprintf(`# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
server
{
# port标记位开始
%s
# port标记位结束
# server_name标记位开始
server_name%s;
# server_name标记位结束
# index标记位开始
index index.php index.html;
# index标记位结束
# root标记位开始
root %s;
# root标记位结束
# ssl标记位开始
# ssl标记位结束
# php标记位开始
include enable-php-%d.conf;
# php标记位结束
# 错误页配置,可自行设置
error_page 404 /404.html;
#error_page 502 /502.html;
# acme证书签发配置,不可修改
include %s/server/vhost/acme/%s.conf;
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
include %s/server/vhost/rewrite/%s.conf;
# 面板默认禁止访问部分敏感目录,可自行修改
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
{
return 404;
}
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
location ~ .*\.(js|css)$
{
expires 1h;
error_log /dev/null;
access_log /dev/null;
}
access_log %s/wwwlogs/%s.log;
error_log %s/wwwlogs/%s.log;
}
`, portList, domainList, req.Path, req.PHP, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name, app.Root, req.Name)
if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), nginxConf, 0644); err != nil {
// 写nginx配置
if err = io.Write(filepath.Join(app.Root, "server/vhost", req.Name+".conf"), p.Dump(), 0644); err != nil {
return nil, err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"), "", 0644); err != nil {
@@ -290,13 +256,14 @@ server
if err = io.Write(filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"), "", 0644); err != nil {
return nil, err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", req.Name+".pem"), "", 0644); err != nil {
if err = io.Write(filepath.Join(app.Root, "server/vhost/cert", req.Name+".pem"), "", 0644); err != nil {
return nil, err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", req.Name+".key"), "", 0644); err != nil {
if err = io.Write(filepath.Join(app.Root, "server/vhost/cert", req.Name+".key"), "", 0644); err != nil {
return nil, err
}
// 设置目录权限
if err = io.Chmod(req.Path, 0755); err != nil {
return nil, err
}
@@ -304,11 +271,23 @@ server
return nil, err
}
// 创建面板网站
w := &biz.Website{
Name: req.Name,
Status: true,
Path: req.Path,
Https: false,
}
if err = app.Orm.Create(w).Error; err != nil {
return nil, err
}
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return nil, err
}
// 创建数据库
rootPassword, err := r.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
if err == nil && req.DB && req.DBType == "mysql" {
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
@@ -343,17 +322,17 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
if err := app.Orm.Where("id", req.ID).First(website).Error; err != nil {
return err
}
if !website.Status {
return errors.New("网站已停用,请先启用")
}
// 原文
raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
// 解析nginx配置
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
if err != nil {
return err
}
if strings.TrimSpace(raw) != strings.TrimSpace(req.Raw) {
// 如果修改了原文,直接写入返回
if strings.TrimSpace(config) != strings.TrimSpace(req.Raw) {
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), req.Raw, 0644); err != nil {
return err
}
@@ -361,188 +340,133 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
_, err = shell.Execf("nginx -t")
return err
}
return nil
}
// 目录
path := req.Path
if !io.Exists(path) {
// 初始化nginx配置
p, err := nginx.NewParser(config)
if err != nil {
return err
}
// 监听地址
var listens [][]string
for _, listen := range req.Listens {
if !listen.HTTPS && !listen.QUIC {
listens = append(listens, []string{listen.Address})
}
if listen.HTTPS {
listens = append(listens, []string{listen.Address, "ssl"})
}
if listen.QUIC {
listens = append(listens, []string{listen.Address, "quic"})
}
}
if err = p.SetListen(listens); err != nil {
return err
}
// 域名
domains, err := punycode.EncodeDomains(req.Domains)
if err != nil {
return err
}
if err = p.SetServerName(domains); err != nil {
return err
}
// 首页文件
if err = p.SetIndex(req.Index); err != nil {
return err
}
// 运行目录
if !io.Exists(req.Root) {
return errors.New("运行目录不存在")
}
if err = p.SetRoot(req.Root); err != nil {
return err
}
// 运行目录
if !io.Exists(req.Path) {
return errors.New("网站目录不存在")
}
website.Path = path
// 域名
domain := "server_name"
domains := req.Domains
for _, v := range domains {
if v == "" {
continue
website.Path = req.Path
// PHP
if err = p.SetPHP(req.PHP); err != nil {
return err
}
// HTTPS
certPath := filepath.Join(app.Root, "server/vhost/cert", website.Name+".pem")
keyPath := filepath.Join(app.Root, "server/vhost/cert", website.Name+".key")
if err = io.Write(certPath, req.SSLCertificate, 0644); err != nil {
return err
}
if err = io.Write(keyPath, req.SSLCertificateKey, 0644); err != nil {
return err
}
website.Https = req.HTTPS
if req.HTTPS {
if err = p.SetHTTPS(certPath, keyPath); err != nil {
return err
}
domain += " " + v
}
domain += ";"
domainConfigOld := str.Cut(raw, "# server_name标记位开始", "# server_name标记位结束")
if len(strings.TrimSpace(domainConfigOld)) == 0 {
return errors.New("配置文件中缺少server_name标记位")
}
raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1)
// 端口
var portConf strings.Builder
ports := req.Ports
for _, port := range ports {
https := ""
quic := false
if slices.Contains(req.SSLPorts, port) {
https = " ssl"
if slices.Contains(req.QUICPorts, port) {
quic = true
}
if err = p.SetHTTPRedirect(req.HTTPRedirect); err != nil {
return err
}
if err = p.SetHSTS(req.HSTS); err != nil {
return err
}
if err = p.SetOCSP(req.OCSP); err != nil {
return err
}
portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, https))
portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, https))
if quic {
portConf.WriteString(fmt.Sprintf(" listen %d%s;\n", port, " quic"))
portConf.WriteString(fmt.Sprintf(" listen [::]:%d%s;\n", port, " quic"))
} else {
if err = p.ClearSetHTTPS(); err != nil {
return err
}
if err = p.SetHTTPRedirect(false); err != nil {
return err
}
if err = p.SetHSTS(false); err != nil {
return err
}
if err = p.SetOCSP(false); err != nil {
return err
}
}
portConf.WriteString(" ")
portConfNew := portConf.String()
portConfOld := str.Cut(raw, "# port标记位开始", "# port标记位结束")
if len(strings.TrimSpace(portConfOld)) == 0 {
return errors.New("配置文件中缺少port标记位")
}
raw = strings.Replace(raw, portConfOld, "\n"+portConfNew, -1)
// 运行目录
root := str.Cut(raw, "# root标记位开始", "# root标记位结束")
if len(strings.TrimSpace(root)) == 0 {
return errors.New("配置文件中缺少root标记位")
}
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root)
if len(match) != 2 {
return errors.New("配置文件中root标记位格式错误")
}
rootNew := strings.Replace(root, match[1], req.Root, -1)
raw = strings.Replace(raw, root, rootNew, -1)
// 默认文件
index := str.Cut(raw, "# index标记位开始", "# index标记位结束")
if len(strings.TrimSpace(index)) == 0 {
return errors.New("配置文件中缺少index标记位")
}
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index)
if len(match) != 2 {
return errors.New("配置文件中index标记位格式错误")
}
indexNew := strings.Replace(index, match[1], req.Index, -1)
raw = strings.Replace(raw, index, indexNew, -1)
// 防跨站
if !strings.HasSuffix(req.Root, "/") {
req.Root += "/"
}
userIni := filepath.Join(req.Root, ".user.ini")
if req.OpenBasedir {
if err = io.Write(req.Root+".user.ini", "open_basedir="+path+":/tmp/", 0644); err != nil {
_, _ = shell.Execf(`chattr -i '%s'`, userIni)
if err = io.Write(userIni, fmt.Sprintf("open_basedir=%s:/tmp/", req.Root), 0644); err != nil {
return err
}
_, _ = shell.Execf(`chattr +i '%s'`, userIni)
} else {
if io.Exists(req.Root + ".user.ini") {
if err = io.Remove(req.Root + ".user.ini"); err != nil {
if io.Exists(userIni) {
_, _ = shell.Execf(`chattr -i '%s'`, userIni)
if err = io.Remove(userIni); err != nil {
return err
}
}
}
// SSL
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"), req.SSLCertificate, 0644); err != nil {
return err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"), req.SSLCertificateKey, 0644); err != nil {
return err
}
website.SSL = req.SSL
if req.SSL {
if _, err = cert.ParseCert(req.SSLCertificate); err != nil {
return errors.New("TLS证书格式错误")
}
if _, err = cert.ParseKey(req.SSLCertificateKey); err != nil {
return errors.New("TLS私钥格式错误")
}
sslConfig := fmt.Sprintf(`# ssl标记位开始
ssl_certificate %s/server/vhost/ssl/%s.pem;
ssl_certificate_key %s/server/vhost/ssl/%s.key;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_early_data on;
`, app.Root, website.Name, app.Root, website.Name)
if req.HTTPRedirect {
sslConfig += `# http重定向标记位开始
if ($server_port !~ 443){
return 301 https://$host$request_uri;
}
error_page 497 https://$host$request_uri;
# http重定向标记位结束
`
}
if req.HSTS {
sslConfig += `# hsts标记位开始
add_header Strict-Transport-Security "max-age=63072000" always;
# hsts标记位结束
`
}
if req.OCSP {
sslConfig += `# ocsp标记位开始
ssl_stapling on;
ssl_stapling_verify on;
# ocsp标记位结束
`
}
sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
if len(strings.TrimSpace(sslConfigOld)) != 0 {
raw = strings.Replace(raw, sslConfigOld, "", -1)
}
raw = strings.Replace(raw, "# ssl标记位开始", sslConfig, -1)
} else {
sslConfigOld := str.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
if len(strings.TrimSpace(sslConfigOld)) != 0 {
raw = strings.Replace(raw, sslConfigOld, "\n ", -1)
}
}
if website.PHP != req.PHP {
website.PHP = req.PHP
phpConfigOld := str.Cut(raw, "# php标记位开始", "# php标记位结束")
phpConfig := `
include enable-php-` + strconv.Itoa(website.PHP) + `.conf;
`
if len(strings.TrimSpace(phpConfigOld)) != 0 {
raw = strings.Replace(raw, phpConfigOld, phpConfig, -1)
}
}
if err = app.Orm.Save(website).Error; err != nil {
return err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil {
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil {
return err
}
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), req.Rewrite, 0644); err != nil {
return err
}
err = systemctl.Reload("nginx")
if err != nil {
_, err = shell.Execf("nginx -t")
if err = app.Orm.Save(website).Error; err != nil {
return err
}
return err
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return err
}
return nil
}
func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
@@ -550,20 +474,15 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
if err := app.Orm.Preload("Cert").Where("id", req.ID).First(website).Error; err != nil {
return err
}
if website.Cert != nil {
return errors.New("网站" + website.Name + "已绑定SSL证书,请先删除证书")
}
if err := app.Orm.Delete(website).Error; err != nil {
return err
return errors.New("网站" + website.Name + "已绑定证书,请先删除证书")
}
_ = io.Remove(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
_ = io.Remove(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
_ = io.Remove(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"))
_ = io.Remove(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".pem"))
_ = io.Remove(filepath.Join(app.Root, "server/vhost/ssl", website.Name+".key"))
_ = io.Remove(filepath.Join(app.Root, "server/vhost/cert", website.Name+".pem"))
_ = io.Remove(filepath.Join(app.Root, "server/vhost/cert", website.Name+".key"))
if req.Path {
_ = io.Remove(website.Path)
@@ -583,12 +502,16 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
_, _ = shell.Execf(`echo "DROP USER IF EXISTS '%s';" | su - postgres -c "psql"`, website.Name)
}
err := systemctl.Reload("nginx")
if err != nil {
_, err = shell.Execf("nginx -t")
if err := app.Orm.Delete(website).Error; err != nil {
return err
}
return err
if err := systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return err
}
return nil
}
func (r *websiteRepo) ClearLog(id uint) error {
@@ -617,75 +540,52 @@ func (r *websiteRepo) ResetConfig(id uint) error {
return err
}
website.Status = true
website.SSL = false
if err := app.Orm.Save(website).Error; err != nil {
// 初始化nginx配置
p, err := nginx.NewParser()
if err != nil {
return err
}
// 运行目录
if err = p.SetRoot(website.Path); err != nil {
return err
}
// 伪静态
includes, comments, err := p.GetIncludes()
if err != nil {
return err
}
includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
includes = append(includes, filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"))
comments = append(comments, []string{"# 伪静态规则"})
comments = append(comments, []string{"# acme http-01"})
if err = p.SetIncludes(includes, comments); err != nil {
return err
}
// 日志
if err = p.SetAccessLog(filepath.Join(app.Root, "wwwlogs", website.Name+".log")); err != nil {
return err
}
if err = p.SetErrorLog(filepath.Join(app.Root, "wwwlogs", website.Name+".error.log")); err != nil {
return err
}
raw := fmt.Sprintf(`
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
server
{
# port标记位开始
listen 80;
# port标记位结束
# server_name标记位开始
server_name localhost;
# server_name标记位结束
# index标记位开始
index index.php index.html;
# index标记位结束
# root标记位开始
root %s;
# root标记位结束
# ssl标记位开始
# ssl标记位结束
# php标记位开始
include enable-php-%d.conf;
# php标记位结束
# 错误页配置,可自行设置
error_page 404 /404.html;
#error_page 502 /502.html;
# acme证书签发配置,不可修改
include %s/server/vhost/acme/%s.conf;
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
include %s/server/vhost/rewrite/%s.conf;
# 面板默认禁止访问部分敏感目录,可自行修改
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
{
return 404;
}
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
location ~ .*\.(js|css)$
{
expires 1h;
error_log /dev/null;
access_log /dev/null;
}
access_log %s/wwwlogs/%s.log;
error_log %s/wwwlogs/%s.log;
}
`, website.Path, website.PHP, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name, app.Root, website.Name)
if err := io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil {
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil {
return nil
}
if err := io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil {
if err = io.Write(filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"), "", 0644); err != nil {
return nil
}
if err := io.Write(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"), "", 0644); err != nil {
return nil
if err = io.Write(filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"), "", 0644); err != nil {
return err
}
if err := systemctl.Reload("nginx"); err != nil {
website.Status = true
website.Https = false
if err = app.Orm.Save(website).Error; err != nil {
return err
}
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return err
}
@@ -699,43 +599,69 @@ func (r *websiteRepo) UpdateStatus(id uint, status bool) error {
return err
}
website.Status = status
if err := app.Orm.Save(website).Error; err != nil {
// 解析nginx配置
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
if err != nil {
return err
}
raw, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
p, err := nginx.NewParser(config)
if err != nil {
return err
}
// 运行目录
rootConfig := str.Cut(raw, "# root标记位开始\n", "# root标记位结束")
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig)
if len(match) == 2 {
if website.Status {
root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig)
raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s;\n ", root[1]))
} else {
raw = strings.ReplaceAll(raw, rootConfig, fmt.Sprintf(" root %s/server/nginx/html;\n # root %s;\n ", app.Root, match[1]))
}
}
// 默认文件
indexConfig := str.Cut(raw, "# index标记位开始\n", "# index标记位结束")
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig)
if len(match) == 2 {
if website.Status {
index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig)
raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ")
} else {
raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ")
}
}
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), raw, 0644); err != nil {
// 运行目录和默认文档
root, rootComment, err := p.GetRootWithComment()
if err != nil {
return err
}
index, indexComment, err := p.GetIndexWithComment()
if err != nil {
return err
}
indexStr := strings.Join(index, " ")
if status {
if len(rootComment) == 0 {
return fmt.Errorf("未找到运行目录注释")
}
if len(rootComment) != 1 {
return fmt.Errorf("运行目录注释数量不正确,预期1个,实际%d个", len(rootComment))
}
rootComment[0] = strings.TrimPrefix(rootComment[0], "# ")
if !io.Exists(rootComment[0]) {
return fmt.Errorf("运行目录不存在")
}
if err = p.SetRoot(rootComment[0]); err != nil {
return err
}
if len(indexComment) == 0 {
return fmt.Errorf("未找到默认文档注释")
}
if len(indexComment) != 1 {
return fmt.Errorf("默认文档注释数量不正确,预期1个,实际%d个", len(indexComment))
}
indexComment[0] = strings.TrimPrefix(indexComment[0], "# ")
if err = p.SetIndex(strings.Fields(indexComment[0])); err != nil {
return err
}
} else {
if err = p.SetRootWithComment(filepath.Join(app.Root, "server/nginx/html"), []string{"# " + root}); err != nil {
return err
}
if err = p.SetIndexWithComment([]string{"stop.html"}, []string{"# " + indexStr}); err != nil {
return err
}
}
if err = io.Write(filepath.Join(app.Root, "server/vhost", website.Name+".conf"), p.Dump(), 0644); err != nil {
return err
}
website.Status = status
if err = app.Orm.Save(website).Error; err != nil {
return err
}
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return err
+7 -7
View File
@@ -5,35 +5,35 @@ import (
"github.com/go-rat/chix"
"github.com/TheTNB/panel/pkg/types"
"github.com/TheTNB/panel/internal/app"
)
// Status 检查程序状态
func Status(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch types.Status {
case types.StatusUpgrade:
switch app.Status {
case app.StatusUpgrade:
render := chix.NewRender(w)
render.Status(http.StatusServiceUnavailable)
render.JSON(chix.M{
"message": "面板升级中,请稍后刷新",
"message": "面板更新中,请稍后刷新",
})
return
case types.StatusMaintain:
case app.StatusMaintain:
render := chix.NewRender(w)
render.Status(http.StatusServiceUnavailable)
render.JSON(chix.M{
"message": "面板正在运行维护任务,请稍后刷新",
})
return
case types.StatusClosed:
case app.StatusClosed:
render := chix.NewRender(w)
render.Status(http.StatusForbidden)
render.JSON(chix.M{
"message": "面板已关闭",
})
return
case types.StatusFailed:
case app.StatusFailed:
render := chix.NewRender(w)
render.Status(http.StatusInternalServerError)
render.JSON(chix.M{
+4 -4
View File
@@ -1,15 +1,15 @@
package request
type App struct {
Slug string `json:"slug" form:"slug"`
Channel string `json:"channel" form:"channel"`
Slug string `json:"slug" form:"slug" validate:"required,not_exists=apps slug"`
Channel string `json:"channel" form:"channel" validate:"required"`
}
type AppSlug struct {
Slug string `json:"slug" form:"slug"`
Slug string `json:"slug" form:"slug" validate:"required"`
}
type AppUpdateShow struct {
Slug string `json:"slug" form:"slug"`
Slug string `json:"slug" form:"slug" validate:"required,exists=apps slug"`
Show bool `json:"show" form:"show"`
}
+22
View File
@@ -0,0 +1,22 @@
package request
type BackupList struct {
Type string `json:"type" form:"type" validate:"required,oneof=path website mysql postgres redis panel"`
}
type BackupCreate struct {
Type string `json:"type" form:"type" validate:"required,oneof=website mysql postgres redis panel"`
Target string `json:"target" form:"target" validate:"required"`
Path string `json:"path" form:"path"`
}
type BackupFile struct {
Type string `json:"type" form:"type" validate:"required,oneof=website mysql postgres redis panel"`
File string `json:"file" form:"file" validate:"required"`
}
type BackupRestore struct {
Type string `json:"type" form:"type" validate:"required,oneof=website mysql postgres redis panel"`
File string `json:"file" form:"file" validate:"required"`
Target string `json:"target" form:"target" validate:"required"`
}
+7 -7
View File
@@ -1,8 +1,8 @@
package request
type CertCreate struct {
Type string `form:"type" json:"type"`
Domains []string `form:"domains" json:"domains"`
Type string `form:"type" json:"type" validate:"required"`
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
AccountID uint `form:"account_id" json:"account_id"`
DNSID uint `form:"dns_id" json:"dns_id"`
@@ -10,9 +10,9 @@ type CertCreate struct {
}
type CertUpdate struct {
ID uint `form:"id" json:"id"`
Type string `form:"type" json:"type"`
Domains []string `form:"domains" json:"domains"`
ID uint `form:"id" json:"id" validate:"required,exists=certs id"`
Type string `form:"type" json:"type" validate:"required"`
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
AccountID uint `form:"account_id" json:"account_id"`
DNSID uint `form:"dns_id" json:"dns_id"`
@@ -20,6 +20,6 @@ type CertUpdate struct {
}
type CertDeploy struct {
ID uint `form:"id" json:"id"`
WebsiteID uint `form:"website_id" json:"website_id"`
ID uint `form:"id" json:"id" validate:"required,exists=certs id"`
WebsiteID uint `form:"website_id" json:"website_id" validate:"required"`
}
+7 -7
View File
@@ -1,18 +1,18 @@
package request
type CertAccountCreate struct {
CA string `form:"ca" json:"ca"`
Email string `form:"email" json:"email"`
CA string `form:"ca" json:"ca" validate:"required"`
Email string `form:"email" json:"email" validate:"required"`
Kid string `form:"kid" json:"kid"`
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
KeyType string `form:"key_type" json:"key_type"`
KeyType string `form:"key_type" json:"key_type" validate:"required"`
}
type CertAccountUpdate struct {
ID uint `form:"id" json:"id"`
CA string `form:"ca" json:"ca"`
Email string `form:"email" json:"email"`
ID uint `form:"id" json:"id" validate:"required,exists=cert_accounts id"`
CA string `form:"ca" json:"ca" validate:"required"`
Email string `form:"email" json:"email" validate:"required"`
Kid string `form:"kid" json:"kid"`
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
KeyType string `form:"key_type" json:"key_type"`
KeyType string `form:"key_type" json:"key_type" validate:"required"`
}
+5 -5
View File
@@ -3,14 +3,14 @@ package request
import "github.com/TheTNB/panel/pkg/acme"
type CertDNSCreate struct {
Type string `form:"type" json:"type"`
Name string `form:"name" json:"name"`
Type string `form:"type" json:"type" validate:"required"`
Name string `form:"name" json:"name" validate:"required"`
Data acme.DNSParam `form:"data" json:"data"`
}
type CertDNSUpdate struct {
ID uint `form:"id" json:"id"`
Type string `form:"type" json:"type"`
Name string `form:"name" json:"name"`
ID uint `form:"id" json:"id" validate:"required,exists=cert_dns id"`
Type string `form:"type" json:"type" validate:"required"`
Name string `form:"name" json:"name" validate:"required"`
Data acme.DNSParam `form:"data" json:"data"`
}
+5 -5
View File
@@ -3,17 +3,17 @@ package request
import "github.com/TheTNB/panel/pkg/types"
type ContainerID struct {
ID string `json:"id" form:"id"`
ID string `json:"id" form:"id" validate:"required"`
}
type ContainerRename struct {
ID string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
ID string `form:"id" json:"id" validate:"required"`
Name string `form:"name" json:"name" validate:"required"`
}
type ContainerCreate struct {
Name string `form:"name" json:"name"`
Image string `form:"image" json:"image"`
Name string `form:"name" json:"name" validate:"required"`
Image string `form:"image" json:"image" validate:"required"`
Ports []types.ContainerPort `form:"ports" json:"ports"`
Network string `form:"network" json:"network"`
Volumes []types.ContainerVolume `form:"volumes" json:"volumes"`
+1 -1
View File
@@ -5,7 +5,7 @@ type ContainerImageID struct {
}
type ContainerImagePull struct {
Name string `form:"name" json:"name"`
Name string `form:"name" json:"name" validate:"required"`
Auth bool `form:"auth" json:"auth"`
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
+4 -4
View File
@@ -3,11 +3,11 @@ package request
import "github.com/TheTNB/panel/pkg/types"
type ContainerNetworkID struct {
ID string `json:"id" form:"id"`
ID string `json:"id" form:"id" validate:"required"`
}
type ContainerNetworkCreate struct {
Name string `form:"name" json:"name"`
Name string `form:"name" json:"name" validate:"required"`
Driver string `form:"driver" json:"driver"`
Ipv4 types.ContainerNetwork `form:"ipv4" json:"ipv4"`
Ipv6 types.ContainerNetwork `form:"ipv6" json:"ipv6"`
@@ -16,6 +16,6 @@ type ContainerNetworkCreate struct {
}
type ContainerNetworkConnect struct {
Network string `form:"network" json:"network"`
Container string `form:"container" json:"container"`
Network string `form:"network" json:"network" validate:"required"`
Container string `form:"container" json:"container" validate:"required"`
}
+2 -2
View File
@@ -3,11 +3,11 @@ package request
import "github.com/TheTNB/panel/pkg/types"
type ContainerVolumeID struct {
ID string `json:"id" form:"id"`
ID string `json:"id" form:"id" validate:"required"`
}
type ContainerVolumeCreate struct {
Name string `form:"name" json:"name"`
Name string `form:"name" json:"name" validate:"required"`
Driver string `form:"driver" json:"driver"`
Labels []types.KV `form:"labels" json:"labels"`
Options []types.KV `form:"options" json:"options"`
+11 -11
View File
@@ -1,24 +1,24 @@
package request
type CronCreate struct {
Name string `form:"name" json:"name"`
Type string `form:"type" json:"type"`
Time string `form:"time" json:"time"`
Name string `form:"name" json:"name" validate:"required,not_exists=crons name"`
Type string `form:"type" json:"type" validate:"required"`
Time string `form:"time" json:"time" validate:"required,cron"`
Script string `form:"script" json:"script"`
BackupType string `form:"backup_type" json:"backup_type"`
BackupType string `form:"backup_type" json:"backup_type" validate:"required"`
BackupPath string `form:"backup_path" json:"backup_path"`
Target string `form:"target" json:"target"`
Save int `form:"save" json:"save"`
Target string `form:"target" json:"target" validate:"required"`
Save int `form:"save" json:"save" validate:"required"`
}
type CronUpdate struct {
ID uint `form:"id" json:"id"`
Name string `form:"name" json:"name"`
Time string `form:"time" json:"time"`
Script string `form:"script" json:"script"`
ID uint `form:"id" json:"id" validate:"required,exists=crons id"`
Name string `form:"name" json:"name" validate:"required"`
Time string `form:"time" json:"time" validate:"required,cron"`
Script string `form:"script" json:"script" validate:"required"`
}
type CronStatus struct {
ID uint `form:"id" json:"id"`
ID uint `form:"id" json:"id" validate:"required,exists=crons id"`
Status bool `form:"status" json:"status"`
}
+6
View File
@@ -0,0 +1,6 @@
package request
type DashboardCurrent struct {
Nets []string `json:"nets" form:"nets"`
Disks []string `json:"disks" form:"disks"`
}
+40 -23
View File
@@ -1,54 +1,71 @@
package request
import (
"net/http"
"github.com/spf13/cast"
)
type FileList struct {
Path string `json:"path" form:"path" validate:"required"`
Sort string `json:"sort" form:"sort"`
}
type FilePath struct {
Path string `json:"path" form:"path"`
Path string `json:"path" form:"path" validate:"required"`
}
type FileCreate struct {
Dir bool `json:"dir" form:"dir"`
Path string `json:"path" form:"path"`
Path string `json:"path" form:"path" validate:"required"`
}
type FileSave struct {
Path string `form:"path" json:"path"`
Content string `form:"content" json:"content"`
}
type FileUpload struct {
Path string `json:"path" form:"path"`
File []byte `json:"file" form:"file"`
Path string `form:"path" json:"path" validate:"required"`
Content string `form:"content" json:"content" validate:"required"`
}
type FileMove struct {
Source string `form:"source" json:"source"`
Target string `form:"target" json:"target"`
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 {
Source string `form:"source" json:"source"`
Target string `form:"target" json:"target"`
Source string `form:"source" json:"source" validate:"required"`
Target string `form:"target" json:"target" validate:"required"`
Force bool `form:"force" json:"force"`
}
type FileRemoteDownload struct {
Path string `form:"path" json:"path" validate:"required"`
URL string `form:"url" json:"url" validate:"required"`
}
type FilePermission struct {
Path string `form:"path" json:"path"`
Mode string `form:"mode" json:"mode"`
Owner string `form:"owner" json:"owner"`
Group string `form:"group" json:"group"`
Path string `form:"path" json:"path" validate:"required"`
Mode string `form:"mode" json:"mode" validate:"required"`
Owner string `form:"owner" json:"owner" validate:"required"`
Group string `form:"group" json:"group" validate:"required"`
}
type FileCompress struct {
Paths []string `form:"paths" json:"paths"`
File string `form:"file" json:"file"`
Paths []string `form:"paths" json:"paths" validate:"min=1,dive,required"`
File string `form:"file" json:"file" validate:"required"`
}
type FileUnCompress struct {
File string `form:"file" json:"file"`
Path string `form:"path" json:"path"`
File string `form:"file" json:"file" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
}
type FileSearch struct {
Path string `form:"path" json:"path"`
KeyWord string `form:"keyword" json:"keyword"`
Path string `form:"path" json:"path" validate:"required"`
Keyword string `form:"keyword" json:"keyword" validate:"required"`
Sub bool `form:"sub" json:"sub"`
}
func (r *FileSearch) Prepare(req *http.Request) error {
r.Sub = cast.ToBool(req.FormValue("sub"))
return nil
}
+24 -3
View File
@@ -4,7 +4,28 @@ type FirewallStatus struct {
Status bool `json:"status" form:"status"`
}
type FirewallCreateRule struct {
Port uint `json:"port"`
Protocol string `json:"protocol"`
type FirewallRule struct {
Type string `json:"type"`
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
PortStart uint `json:"port_start" validate:"required,gte=1,lte=65535"`
PortEnd uint `json:"port_end" validate:"required,gte=1,lte=65535"`
Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"`
Address string `json:"address"`
Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"`
Direction string `json:"direction"`
}
type FirewallIPRule struct {
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"`
Address string `json:"address"`
Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"`
Direction string `json:"direction"`
}
type FirewallForward struct {
Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"`
Port uint `json:"port" validate:"required,gte=1,lte=65535"`
TargetIP string `json:"target_ip" validate:"required"`
TargetPort uint `json:"target_port" validate:"required,gte=1,lte=65535"`
}
+1 -1
View File
@@ -1,7 +1,7 @@
package request
type SafeUpdateSSH struct {
Port uint `json:"port" form:"port"`
Port uint `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
Status bool `json:"status" form:"status"`
}
+12 -11
View File
@@ -1,16 +1,17 @@
package request
type PanelSetting struct {
Name string `json:"name"`
Locale string `json:"locale"`
Entrance string `json:"entrance"`
WebsitePath string `json:"website_path"`
BackupPath string `json:"backup_path"`
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Port int `json:"port"`
Name string `json:"name" validate:"required"`
Locale string `json:"locale" validate:"required"`
Entrance string `json:"entrance" validate:"required"`
OfflineMode bool `json:"offline_mode"`
WebsitePath string `json:"website_path" validate:"required"`
BackupPath string `json:"backup_path" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"password"`
Email string `json:"email" validate:"required"`
Port int `json:"port" validate:"required,number,gte=1,lte=65535"`
HTTPS bool `json:"https"`
Cert string `json:"cert"`
Key string `json:"key"`
Cert string `json:"cert" validate:"required"`
Key string `json:"key" validate:"required"`
}
+21 -5
View File
@@ -1,8 +1,24 @@
package request
type SSHUpdateInfo struct {
Host string `json:"host" form:"host"`
Port int `json:"port" form:"port"`
User string `json:"user" form:"user"`
Password string `json:"password" form:"password"`
type SSHCreate struct {
Name string `json:"name" form:"name"`
Host string `json:"host" form:"host" validate:"required"`
Port uint `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
AuthMethod string `json:"auth_method" form:"auth_method" validate:"required,oneof=password publickey"`
User string `json:"user" form:"user" validate:"required_if=AuthMethod password"`
Password string `json:"password" form:"password" validate:"required_if=AuthMethod password"`
Key string `json:"key" form:"key" validate:"required_if=AuthMethod publickey"`
Remark string `json:"remark" form:"remark"`
}
type SSHUpdate struct {
ID uint `form:"id" json:"id" validate:"required,exists=sshes id"`
Name string `json:"name" form:"name"`
Host string `json:"host" form:"host" validate:"required"`
Port uint `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
AuthMethod string `json:"auth_method" form:"auth_method" validate:"required,oneof=password publickey"`
User string `json:"user" form:"user" validate:"required_if=AuthMethod password"`
Password string `json:"password" form:"password" validate:"required_if=AuthMethod password"`
Key string `json:"key" form:"key" validate:"required_if=AuthMethod publickey"`
Remark string `json:"remark" form:"remark"`
}
+1 -1
View File
@@ -1,5 +1,5 @@
package request
type SystemctlService struct {
Service string `json:"service"`
Service string `json:"service" validate:"required"`
}
-8
View File
@@ -4,11 +4,3 @@ type UserLogin struct {
Username string `json:"username" form:"username" validate:"required,min=3,max=255"`
Password string `json:"password" form:"password" validate:"required,min=6,max=255"`
}
type UserID struct {
ID uint `uri:"id" validate:"required,number"`
}
type AddUser struct {
Name string `json:"name" form:"name" validate:"required,min=3,max=255" comment:"用户名"`
}
+28 -28
View File
@@ -1,56 +1,56 @@
package request
import "github.com/TheTNB/panel/pkg/types"
type WebsiteDefaultConfig struct {
Index string `json:"index" form:"index"`
Stop string `json:"stop" form:"stop"`
Index string `json:"index" form:"index" validate:"required"`
Stop string `json:"stop" form:"stop" validate:"required"`
}
type WebsiteCreate struct {
Name string `form:"name" json:"name"`
Domains []string `form:"domains" json:"domains"`
Ports []uint `form:"ports" json:"ports"`
Name string `form:"name" json:"name" validate:"required,not_exists=websites name"`
Listens []string `form:"listens" json:"listens" validate:"min=1,dive,required"`
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
Path string `form:"path" json:"path"`
PHP int `form:"php" json:"php"`
PHP int `form:"php" json:"php" validate:"number,gte=0"`
DB bool `form:"db" json:"db"`
DBType string `form:"db_type" json:"db_type"`
DBName string `form:"db_name" json:"db_name"`
DBUser string `form:"db_user" json:"db_user"`
DBPassword string `form:"db_password" json:"db_password"`
DBPassword string `form:"db_password" json:"db_password" validate:"password"`
}
type WebsiteDelete struct {
ID uint `form:"id" json:"id"`
ID uint `form:"id" json:"id" validate:"required,exists=websites id"`
Path bool `form:"path" json:"path"`
DB bool `form:"db" json:"db"`
}
type WebsiteUpdate struct {
ID uint `form:"id" json:"id"`
Domains []string `form:"domains" json:"domains"`
Ports []uint `form:"ports" json:"ports"`
SSLPorts []uint `form:"ssl_ports" json:"ssl_ports"`
QUICPorts []uint `form:"quic_ports" json:"quic_ports"`
OCSP bool `form:"ocsp" json:"ocsp"`
HSTS bool `form:"hsts" json:"hsts"`
SSL bool `form:"ssl" json:"ssl"`
HTTPRedirect bool `form:"http_redirect" json:"http_redirect"`
OpenBasedir bool `form:"open_basedir" json:"open_basedir"`
Index string `form:"index" json:"index"`
Path string `form:"path" json:"path"`
Root string `form:"root" json:"root"`
Raw string `form:"raw" json:"raw"`
Rewrite string `form:"rewrite" json:"rewrite"`
PHP int `form:"php" json:"php"`
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"`
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"`
ID uint `form:"id" json:"id" validate:"required,exists=websites id"`
Listens []types.WebsiteListen `form:"listens" json:"listens" validate:"min=1"`
Domains []string `form:"domains" json:"domains" validate:"min=1,dive,required"`
HTTPS bool `form:"https" json:"https"`
OCSP bool `form:"ocsp" json:"ocsp"`
HSTS bool `form:"hsts" json:"hsts"`
HTTPRedirect bool `form:"http_redirect" json:"http_redirect"`
OpenBasedir bool `form:"open_basedir" json:"open_basedir"`
Index []string `form:"index" json:"index" validate:"min=1,dive,required"`
Path string `form:"path" json:"path" validate:"required"` // 网站目录
Root string `form:"root" json:"root" validate:"required"` // 运行目录
Raw string `form:"raw" json:"raw"`
Rewrite string `form:"rewrite" json:"rewrite"`
PHP int `form:"php" json:"php"`
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate" validate:"required_if=HTTPS true"`
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key" validate:"required_if=HTTPS true"`
}
type WebsiteUpdateRemark struct {
ID uint `form:"id" json:"id"`
ID uint `form:"id" json:"id" validate:"required,exists=websites id"`
Remark string `form:"remark" json:"remark"`
}
type WebsiteUpdateStatus struct {
ID uint `json:"id" form:"id"`
ID uint `json:"id" form:"id" validate:"required,exists=websites id"`
Status bool `json:"status" form:"status"`
}
+1
View File
@@ -0,0 +1 @@
package request
+42
View File
@@ -0,0 +1,42 @@
package rule
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"gorm.io/gorm"
)
type Exists struct {
DB *gorm.DB
}
func NewExists(db *gorm.DB) *Exists {
return &Exists{DB: db}
}
// Exists 格式 `exists=categories id other_field`
func (r *Exists) Exists(fl validator.FieldLevel) bool {
requestValue := fl.Field().Interface()
params := strings.Fields(fl.Param())
if len(params) < 2 {
return false
}
tableName := params[0]
fieldNames := params[1:]
query := r.DB.Table(tableName).Where(fmt.Sprintf("%s = ?", fieldNames[0]), requestValue)
for _, fieldName := range fieldNames[1:] {
query = query.Or(fmt.Sprintf("%s = ?", fieldName), requestValue)
}
var count int64
err := query.Count(&count).Error
if err != nil {
return false
}
return count != 0
}
+42
View File
@@ -0,0 +1,42 @@
package rule
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"gorm.io/gorm"
)
type NotExists struct {
DB *gorm.DB
}
func NewNotExists(db *gorm.DB) *NotExists {
return &NotExists{DB: db}
}
// NotExists 格式 `not_exists=categories id other_field`
func (r *NotExists) NotExists(fl validator.FieldLevel) bool {
requestValue := fl.Field().Interface()
params := strings.Fields(fl.Param())
if len(params) < 2 {
return false
}
tableName := params[0]
fieldNames := params[1:]
query := r.DB.Table(tableName).Where(fmt.Sprintf("%s = ?", fieldNames[0]), requestValue)
for _, fieldName := range fieldNames[1:] {
query = query.Or(fmt.Sprintf("%s = ?", fieldName), requestValue)
}
var count int64
err := query.Count(&count).Error
if err != nil {
return false
}
return count == 0
}
+50
View File
@@ -0,0 +1,50 @@
package rule
import (
"unicode"
"github.com/go-playground/validator/v10"
)
type Password struct{}
func NewPassword() *Password {
return &Password{}
}
// Password 密码复杂度校验
func (r *Password) Password(fl validator.FieldLevel) bool {
password := fl.Field().String()
// 不对空密码进行校验,有需要可以使用 required 标签
if password == "" {
return true
}
var hasUpper, hasLower, hasNumber, hasSpecial bool
if len(password) < 8 || len(password) > 20 {
return false
}
for _, char := range password {
switch {
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsLower(char):
hasLower = true
case unicode.IsNumber(char):
hasNumber = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
// 至少包含两类字符组合
valid := (hasUpper && hasLower) ||
(hasUpper && hasNumber) ||
(hasUpper && hasSpecial) ||
(hasLower && hasNumber) ||
(hasLower && hasSpecial) ||
(hasNumber && hasSpecial)
return valid
}
+29
View File
@@ -0,0 +1,29 @@
package rule
import (
"regexp"
"strings"
"github.com/go-playground/validator/v10"
)
type Regex struct{}
func NewRegex() *Regex {
return &Regex{}
}
func (r *Regex) Regex(fl validator.FieldLevel) bool {
// 从标签中获取正则,格式类似于 `regex=^[a-zA-Z0-9_]+$`
pattern := fl.Param()
// 替换转义字符
pattern = strings.ReplaceAll(pattern, "", ",")
re, err := regexp.Compile(pattern)
if err != nil {
return false
}
value := fl.Field().String()
return re.MatchString(value)
}
+66
View File
@@ -0,0 +1,66 @@
package rule
import (
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"github.com/TheTNB/panel/internal/app"
)
func RegisterRules(v *validator.Validate) error {
if err := v.RegisterValidation("exists", NewExists(app.Orm).Exists); err != nil {
return err
}
if err := v.RegisterValidation("not_exists", NewNotExists(app.Orm).NotExists); err != nil {
return err
}
if err := v.RegisterValidation("regex", NewRegex().Regex); err != nil {
return err
}
if err := v.RegisterValidation("password", NewPassword().Password); err != nil {
return err
}
if err := v.RegisterTranslation("exists", *app.Translator,
func(ut ut.Translator) error {
return ut.Add("exists", "{0} 不存在", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("exists", fe.Field())
return t
}); err != nil {
return err
}
if err := v.RegisterTranslation("not_exists", *app.Translator,
func(ut ut.Translator) error {
return ut.Add("not_exists", "{0} 已存在", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("not_exists", fe.Field())
return t
}); err != nil {
return err
}
if err := v.RegisterTranslation("regex", *app.Translator,
func(ut ut.Translator) error {
return ut.Add("regex", "{0} 格式不正确", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("regex", fe.Field())
return t
}); err != nil {
return err
}
if err := v.RegisterTranslation("password", *app.Translator,
func(ut ut.Translator) error {
return ut.Add("password", "密码不满足要求(8-20位,至少包含字母、数字、特殊字符中的两种)", true)
},
func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("password")
return t
}); err != nil {
return err
}
return nil
}
+3 -4
View File
@@ -9,7 +9,6 @@ import (
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/data"
pkgcert "github.com/TheTNB/panel/pkg/cert"
"github.com/TheTNB/panel/pkg/types"
)
// CertRenew 证书续签
@@ -23,8 +22,8 @@ func NewCertRenew() *CertRenew {
}
}
func (receiver *CertRenew) Run() {
if types.Status != types.StatusNormal {
func (r *CertRenew) Run() {
if app.Status != app.StatusNormal {
return
}
@@ -50,7 +49,7 @@ func (receiver *CertRenew) Run() {
continue
}
_, err = receiver.certRepo.Renew(cert.ID)
_, err = r.certRepo.Renew(cert.ID)
if err != nil {
app.Logger.Error("续签证书失败", zap.Error(err))
}
+7 -8
View File
@@ -10,7 +10,6 @@ import (
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/data"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
)
// Monitoring 系统监控
@@ -24,8 +23,8 @@ func NewMonitoring() *Monitoring {
}
}
func (receiver *Monitoring) Run() {
if types.Status != types.StatusNormal {
func (r *Monitoring) Run() {
if app.Status != app.StatusNormal {
return
}
@@ -33,18 +32,18 @@ func (receiver *Monitoring) Run() {
//task := data.NewTaskRepo()
//_ = task.DispatchWaiting()
monitor, err := receiver.settingRepo.Get(biz.SettingKeyMonitor)
monitor, err := r.settingRepo.Get(biz.SettingKeyMonitor)
if err != nil || !cast.ToBool(monitor) {
return
}
info := tools.GetMonitoringInfo()
info := tools.CurrentInfo(nil, nil)
// 去除部分数据以减少数据库存储
info.Disk = nil
info.Cpus = nil
if types.Status != types.StatusNormal {
if app.Status != app.StatusNormal {
return
}
@@ -54,12 +53,12 @@ func (receiver *Monitoring) Run() {
}
// 删除过期数据
dayStr, err := receiver.settingRepo.Get(biz.SettingKeyMonitorDays)
dayStr, err := r.settingRepo.Get(biz.SettingKeyMonitorDays)
if err != nil {
return
}
day := cast.ToInt(dayStr)
if day <= 0 || types.Status != types.StatusNormal {
if day <= 0 || app.Status != app.StatusNormal {
return
}
if err = app.Orm.Where("created_at < ?", time.Now().AddDate(0, 0, -day).Format("2006-01-02 15:04:05")).Delete(&biz.Monitor{}).Error; err != nil {

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