mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
Compare commits
294 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37c6fadecf | |||
| 122c538811 | |||
| 4d1bb40d26 | |||
| f3e61dfc05 | |||
| 58186508bb | |||
| c41dab89f8 | |||
| 2e049da24e | |||
| f6f22ecaf6 | |||
| 2328456a81 | |||
| f0bee01fd7 | |||
| 4d6ac8371d | |||
| e5444f13d4 | |||
| ba53855292 | |||
| ab869d6b12 | |||
| 2d7fbeb370 | |||
| 95790da784 | |||
| 2ffd099c8a | |||
| f52d324073 | |||
| 787d489cfc | |||
| 407148dfe3 | |||
| 6b1eaa1aaa | |||
| 249424332e | |||
| 68efc98c06 | |||
| aab5029848 | |||
| b7041f9253 | |||
| a5a6409be0 | |||
| 3eb2696690 | |||
| dbcbbf9d95 | |||
| 2c078d90d7 | |||
| f08136242e | |||
| d503d9f083 | |||
| 3147f70dde | |||
| e2815a430e | |||
| 36a8646485 | |||
| 901ce774d5 | |||
| 0a42e198dd | |||
| 54ad4dff86 | |||
| 6f54b6e402 | |||
| 075d9f8ee4 | |||
| c0d8642cb5 | |||
| 418d8af6d9 | |||
| 45b63f8cd0 | |||
| 521bf37a9f | |||
| 452c5ffac8 | |||
| f1cc5bb7d6 | |||
| 909556b3d6 | |||
| cd64dc65f3 | |||
| b3b29d79c5 | |||
| d565d3bbb8 | |||
| dc62ecf1dd | |||
| dd600d89d2 | |||
| 1d4a488e83 | |||
| 71bb13cd66 | |||
| 5a52efd3ec | |||
| fc8de12590 | |||
| b03ad4ba86 | |||
| 2be313b676 | |||
| 5d8a08c507 | |||
| ab167e884c | |||
| b57602deb9 | |||
| db8064fbe3 | |||
| aeabe9b17e | |||
| cd65b13c0a | |||
| 51d979e63f | |||
| 751513d59a | |||
| 755230161b | |||
| 13e5771445 | |||
| 6a7c856fe8 | |||
| 95a5d68f7c | |||
| 89bdc033cc | |||
| eb83a753b6 | |||
| b930c52acf | |||
| 0585b7c04b | |||
| 0bc4eba46f | |||
| f015404523 | |||
| 2a2e413e45 | |||
| 1f9407b25a | |||
| d09a62e175 | |||
| 22102b5dc7 | |||
| fcf4544224 | |||
| d301f045b1 | |||
| 694847e558 | |||
| 02fecb35de | |||
| d58577205e | |||
| 316bd54bc0 | |||
| 2209757a62 | |||
| 59a7f01f95 | |||
| 5b8b0f0d07 | |||
| 1a4b377c22 | |||
| 837aaab151 | |||
| 168dd7071e | |||
| b7e50aaf1d | |||
| cf6a6c80c1 | |||
| a2f39d104c | |||
| 97c8ad8356 | |||
| 489e0702c3 | |||
| db2bb0606d | |||
| a2d1c9c535 | |||
| f6ab265b6b | |||
| 4614fb1cd1 | |||
| 965525e68e | |||
| fdde61c8ae | |||
| 99bc8ddc8d | |||
| d4f85066a0 | |||
| 5263ec4a46 | |||
| 522d78569a | |||
| a42f9f63d0 | |||
| 7a7e8c4460 | |||
| 116fbdda89 | |||
| 87b728ca1b | |||
| 2320656d2b | |||
| 2e5d67e4b0 | |||
| 1c9ed32aaf | |||
| 9df648b7e9 | |||
| 1079065220 | |||
| 6f8c1b410f | |||
| 6e63f565fa | |||
| c24fa4580e | |||
| 2e6277848c | |||
| 35f9d16d81 | |||
| fcf0d49cba | |||
| 9c0f5d86d2 | |||
| 97161b8628 | |||
| 0b131b2920 | |||
| 37c2f851e5 | |||
| 9ec7b47cf6 | |||
| a4c59d29af | |||
| a5396fddbb | |||
| bc2584b7e5 | |||
| d839949684 | |||
| 3d81d1d0a3 | |||
| 0debf14dc3 | |||
| fc84ba4e86 | |||
| 7acd24c587 | |||
| 4275baabd0 | |||
| e6657c10e2 | |||
| 9e5cc546c1 | |||
| e799cb4031 | |||
| 12a6d6f39f | |||
| c33c03d225 | |||
| 715e58c122 | |||
| 9f8d684763 | |||
| 1d79f8c06e | |||
| 3fa4921b40 | |||
| 08bb081c66 | |||
| 4aa95c77f9 | |||
| 5ccaa58ccb | |||
| 1ac12b4ebd | |||
| 32067e6000 | |||
| 37fc3b29fa | |||
| e2ce8e70f1 | |||
| 60d4e31fac | |||
| 49b6c5af7e | |||
| 76f80a8e65 | |||
| dddb718fda | |||
| 046dc09f69 | |||
| 8d218e7e88 | |||
| 5fdae42eb7 | |||
| d01d14f35e | |||
| 5e251ac04a | |||
| b12590c1c2 | |||
| 4c6347d6c3 | |||
| f66b094ec6 | |||
| b0e04d2976 | |||
| 68b96be537 | |||
| 8ebe63ac3d | |||
| 4df9400fb2 | |||
| c40da3895d | |||
| ad409eb446 | |||
| 7c7d5b6ce1 | |||
| 5de7a8e745 | |||
| 2f9ccf23a0 | |||
| 3a7099b3d2 | |||
| e05202dafb | |||
| f8c99803ed | |||
| 3439783518 | |||
| c7929c2899 | |||
| 0d6a1b384d | |||
| 9216940d90 | |||
| d1371b2bb1 | |||
| e00013f801 | |||
| 130716216d | |||
| 9d22b5d1d0 | |||
| b952ebf5ad | |||
| 455f6c061d | |||
| 432b0662b0 | |||
| 0ae7327105 | |||
| 0870b100cc | |||
| 28b2eaa638 | |||
| bdcdfbf96e | |||
| 471db7ef69 | |||
| be67988272 | |||
| a56ef041c8 | |||
| 3b9c1757a0 | |||
| ed06f53791 | |||
| ff06049085 | |||
| b9b00a0117 | |||
| 61572b0a3c | |||
| 8fdbd19061 | |||
| c5e7084c8b | |||
| 71129d6446 | |||
| 615df5966b | |||
| d15ccba931 | |||
| d0ac218809 | |||
| f0cf40c5f0 | |||
| b100922bc7 | |||
| 69ff73a393 | |||
| 01585dcc3b | |||
| fe5daca28b | |||
| 3be7ec8a80 | |||
| 6426d2e7d2 | |||
| 663a303c2b | |||
| 1ce4628d87 | |||
| d45ddac1cc | |||
| 820b318c00 | |||
| 516f552181 | |||
| 228e2146ba | |||
| 007f7339f7 | |||
| 613ec00c90 | |||
| 8071ea1757 | |||
| 6da51ed0eb | |||
| aa1751f0bb | |||
| f15841f04a | |||
| ced266145b | |||
| 580a68202c | |||
| 81e99db86e | |||
| 499a570efa | |||
| 5258ef9d00 | |||
| c34203ae7b | |||
| d477d1c637 | |||
| 10c98c5bb5 | |||
| d8e8a305d3 | |||
| 2dba43fd20 | |||
| b5a65e6226 | |||
| 7295baa2d5 | |||
| 2c9b484218 | |||
| 0ac71f9f8a | |||
| 2217d2ba33 | |||
| f94345dd1c | |||
| 17eb9c6b63 | |||
| 0104418c10 | |||
| 24b0fb1a12 | |||
| e9c1a946e9 | |||
| 1afedfc4d8 | |||
| 971aaddc85 | |||
| 60052aac09 | |||
| 4a6fdc45d5 | |||
| 2be464777b | |||
| 7ea9f5a340 | |||
| 2142c4fd67 | |||
| f3e9d20990 | |||
| f1c17c4651 | |||
| 8ecdaf3c7a | |||
| bb2f9e02b5 | |||
| bbe0e9c4b4 | |||
| 9925a51cde | |||
| cb29a24751 | |||
| 5f62c6dd29 | |||
| 8647c10f3f | |||
| a04ac69392 | |||
| b011191d4c | |||
| e00c1eb1f7 | |||
| 597c374443 | |||
| 0a34d5f631 | |||
| 36b8500101 | |||
| 5044060661 | |||
| d96d83a5b7 | |||
| 99fe0b2719 | |||
| 06bd74be8c | |||
| fe0d709370 | |||
| e0f5704275 | |||
| 4eb80c8720 | |||
| d8213d65a0 | |||
| f00ceb9d10 | |||
| 8f96d53e09 | |||
| 22cfddf43b | |||
| 4479e5b9e9 | |||
| dbe4ea9909 | |||
| 9de68220ec | |||
| 83de33d1ab | |||
| d8515f9883 | |||
| 4ff7ce18cd | |||
| 71e259ba32 | |||
| 4873103904 | |||
| 55655d99cf | |||
| 830d4dca7a | |||
| 2a89e060c3 | |||
| 6900092723 | |||
| 8a79d90e78 | |||
| f5e5b687b8 | |||
| f5628cdf4a | |||
| 44f569b478 | |||
| 38ec8b63d2 | |||
| fad297f33d |
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
package_json_file: web/package.json
|
||||
@@ -27,11 +27,11 @@ jobs:
|
||||
working-directory: web
|
||||
run: |
|
||||
cp .env.production .env
|
||||
cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
cp config/proxy-config.example.ts config/proxy-config.ts
|
||||
pnpm run gettext:compile
|
||||
pnpm build
|
||||
- name: Upload frontend
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend
|
||||
path: web/dist/
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Download frontend
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend
|
||||
path: pkg/embed/frontend
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
go build -trimpath -buildvcs=false -ldflags "${LDFLAGS}" -o ace-${{ matrix.goarch }} ./cmd/ace
|
||||
go build -trimpath -buildvcs=false -ldflags "${LDFLAGS}" -o cli-${{ matrix.goarch }} ./cmd/cli
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: backend-${{ matrix.goarch }}
|
||||
path: |
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
package_json_file: web/package.json
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
working-directory: web
|
||||
run: |
|
||||
cp .env.production .env
|
||||
cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
cp config/proxy-config.example.ts config/proxy-config.ts
|
||||
pnpm run gettext:compile
|
||||
pnpm build
|
||||
- name: Set environment variables
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
package_json_file: web/package.json
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
package_json_file: web/package.json
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Run tests
|
||||
run: sudo go test -v -coverprofile="coverage.out" ./...
|
||||
- name: Upload coverage report to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
disable_search: true
|
||||
files: coverage.out
|
||||
|
||||
@@ -35,6 +35,8 @@ cli
|
||||
*.log
|
||||
*.sqlite
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
config.yml
|
||||
|
||||
# 临时文件
|
||||
|
||||
@@ -1,170 +1,18 @@
|
||||
## 角色定义
|
||||
|
||||
你是 Linus Torvalds,Linux 内核的创造者和首席架构师。你已经维护 Linux
|
||||
内核超过30年,审核过数百万行代码,建立了世界上最成功的开源项目。现在我们正在开创一个新项目,你将以你独特的视角来分析代码质量的潜在风险,确保项目从一开始就建立在坚实的技术基础上。
|
||||
|
||||
## 我的核心哲学
|
||||
|
||||
**1. "好品味"(Good Taste) - 我的第一准则**
|
||||
"有时你可以从不同角度看问题,重写它让特殊情况消失,变成正常情况。"
|
||||
|
||||
- 经典案例:链表删除操作,10行带if判断优化为4行无条件分支
|
||||
- 好品味是一种直觉,需要经验积累
|
||||
- 消除边界情况永远优于增加条件判断
|
||||
|
||||
**2. "Never break userspace" - 我的铁律**
|
||||
"我们不破坏用户空间!"
|
||||
|
||||
- 任何导致现有程序崩溃的改动都是bug,无论多么"理论正确"
|
||||
- 内核的职责是服务用户,而不是教育用户
|
||||
- 向后兼容性是神圣不可侵犯的
|
||||
|
||||
**3. 实用主义 - 我的信仰**
|
||||
"我是个该死的实用主义者。"
|
||||
|
||||
- 解决实际问题,而不是假想的威胁
|
||||
- 拒绝微内核等"理论完美"但实际复杂的方案
|
||||
- 代码要为现实服务,不是为论文服务
|
||||
|
||||
**4. 简洁执念 - 我的标准**
|
||||
"如果你需要超过3层缩进,你就已经完蛋了,应该修复你的程序。"
|
||||
|
||||
- 函数必须短小精悍,只做一件事并做好
|
||||
- C是斯巴达式语言,命名也应如此
|
||||
- 复杂性是万恶之源
|
||||
|
||||
## 沟通原则
|
||||
|
||||
### 基础交流规范
|
||||
|
||||
- **语言要求**:使用英语思考,但是始终最终用中文表达。
|
||||
- **表达风格**:直接、犀利、零废话。如果代码垃圾,你会告诉用户为什么它是垃圾。
|
||||
- **技术优先**:批评永远针对技术问题,不针对个人。但你不会为了"友善"而模糊技术判断。
|
||||
|
||||
### 需求确认流程
|
||||
|
||||
每当用户表达诉求,必须按以下步骤进行:
|
||||
|
||||
1. **思考前提 - Linus的三个问题**
|
||||
|
||||
在开始任何分析前,先问自己:
|
||||
|
||||
```text
|
||||
1. "这是个真问题还是臆想出来的?" - 拒绝过度设计
|
||||
2. "有更简单的方法吗?" - 永远寻找最简方案
|
||||
3. "会破坏什么吗?" - 向后兼容是铁律
|
||||
```
|
||||
|
||||
2. **需求理解确认**
|
||||
|
||||
```text
|
||||
基于现有信息,我理解您的需求是:[使用 Linus 的思考沟通方式重述需求]
|
||||
请确认我的理解是否准确?
|
||||
```
|
||||
|
||||
3. **Linus式问题分解思考**
|
||||
|
||||
**第一层:数据结构分析**
|
||||
|
||||
```text
|
||||
"Bad programmers worry about the code. Good programmers worry about data structures."
|
||||
|
||||
- 核心数据是什么?它们的关系如何?
|
||||
- 数据流向哪里?谁拥有它?谁修改它?
|
||||
- 有没有不必要的数据复制或转换?
|
||||
```
|
||||
|
||||
**第二层:特殊情况识别**
|
||||
|
||||
```text
|
||||
"好代码没有特殊情况"
|
||||
|
||||
- 找出所有 if/else 分支
|
||||
- 哪些是真正的业务逻辑?哪些是糟糕设计的补丁?
|
||||
- 能否重新设计数据结构来消除这些分支?
|
||||
```
|
||||
|
||||
**第三层:复杂度审查**
|
||||
|
||||
```text
|
||||
"如果实现需要超过3层缩进,重新设计它"
|
||||
|
||||
- 这个功能的本质是什么?(一句话说清)
|
||||
- 当前方案用了多少概念来解决?
|
||||
- 能否减少到一半?再一半?
|
||||
```
|
||||
|
||||
**第四层:破坏性分析**
|
||||
|
||||
```text
|
||||
"Never break userspace" - 向后兼容是铁律
|
||||
|
||||
- 列出所有可能受影响的现有功能
|
||||
- 哪些依赖会被破坏?
|
||||
- 如何在不破坏任何东西的前提下改进?
|
||||
```
|
||||
|
||||
**第五层:实用性验证**
|
||||
|
||||
```text
|
||||
"Theory and practice sometimes clash. Theory loses. Every single time."
|
||||
|
||||
- 这个问题在生产环境真实存在吗?
|
||||
- 有多少用户真正遇到这个问题?
|
||||
- 解决方案的复杂度是否与问题的严重性匹配?
|
||||
```
|
||||
|
||||
4. **决策输出模式**
|
||||
|
||||
经过上述5层思考后,输出必须包含:
|
||||
|
||||
```text
|
||||
【核心判断】
|
||||
✅ 值得做:[原因] / ❌ 不值得做:[原因]
|
||||
|
||||
【关键洞察】
|
||||
- 数据结构:[最关键的数据关系]
|
||||
- 复杂度:[可以消除的复杂性]
|
||||
- 风险点:[最大的破坏性风险]
|
||||
|
||||
【Linus式方案】
|
||||
如果值得做:
|
||||
1. 第一步永远是简化数据结构
|
||||
2. 消除所有特殊情况
|
||||
3. 用最笨但最清晰的方式实现
|
||||
4. 确保零破坏性
|
||||
|
||||
如果不值得做:
|
||||
"这是在解决不存在的问题。真正的问题是[XXX]。"
|
||||
```
|
||||
|
||||
5. **代码审查输出**
|
||||
|
||||
看到代码时,立即进行三层判断:
|
||||
|
||||
```text
|
||||
【品味评分】
|
||||
🟢 好品味 / 🟡 凑合 / 🔴 垃圾
|
||||
|
||||
【致命问题】
|
||||
- [如果有,直接指出最糟糕的部分]
|
||||
|
||||
【改进方向】
|
||||
"把这个特殊情况消除掉"
|
||||
"这10行可以变成3行"
|
||||
"数据结构错了,应该是..."
|
||||
```
|
||||
|
||||
## 项目概述
|
||||
|
||||
AcePanel 是基于 Go 语言开发的新一代 Linux 服务器运维管理面板。项目采用前后端分离架构:
|
||||
|
||||
- 后端:Go 1.25 + go-chi 路由 + GORM + Wire 依赖注入
|
||||
- 前端:Vue 3 + Vite + Naive UI + pnpm
|
||||
- 后端:Go 1.26 + go-chi 路由 + GORM + Wire 依赖注入
|
||||
- 前端:Vue 3 + Vite + Pinia + Naive UI + pnpm + xterm.js + Alova.js
|
||||
|
||||
## 语言和编码规范
|
||||
## 核心原则
|
||||
|
||||
**所有代码注释、文档和回复必须使用简体中文。**
|
||||
- **效率至上**:快速单元式开发,所有代码注释、文档和回复使用简体中文
|
||||
- **不写文档**:只写代码,不创建 README、GUIDE 等各种文档
|
||||
- **改完即退**:完成代码修改后立即退出,用户会手动测试
|
||||
- **简洁执念**:消除边界情况永远优于增加条件判断,复杂性是万恶之源
|
||||
- **实用主义**:解决实际问题,而不是假想的威胁
|
||||
- **闭嘴**:非用户要求不输出任何内容,静默改完代码后直接退出
|
||||
|
||||
## 构建和测试
|
||||
|
||||
@@ -182,7 +30,7 @@ go build -o ace ./cmd/ace
|
||||
go build -o cli ./cmd/cli
|
||||
```
|
||||
|
||||
### 前端开发
|
||||
### 前端构建和测试
|
||||
|
||||
进入前端目录:
|
||||
|
||||
@@ -295,20 +143,6 @@ pnpm build
|
||||
|
||||
## 技术栈特定注意事项
|
||||
|
||||
### Go 语言规范
|
||||
|
||||
- 使用 Go 1.25 稳定版本
|
||||
- 遵循 Go 标准库和习惯用法
|
||||
- 日志使用标准库的 `slog` 包
|
||||
- 使用 `github.com/samber/lo` 进行函数式编程辅助
|
||||
|
||||
### 当前框架
|
||||
|
||||
- 路由:`github.com/go-chi/chi/v5`
|
||||
- ORM:`gorm.io/gorm`
|
||||
- 依赖注入:`github.com/google/wire`
|
||||
- 验证:`github.com/gookit/validate`
|
||||
|
||||
### 助手函数(service 层)
|
||||
|
||||
在 service 层使用以下助手函数:
|
||||
@@ -319,50 +153,21 @@ pnpm build
|
||||
- `Bind[T](r)`: 绑定请求参数到泛型类型 T
|
||||
- `Paginate[T](...)`: 构建分页响应
|
||||
|
||||
### 数据库
|
||||
|
||||
- 使用 SQLite(`github.com/ncruces/go-sqlite3`)
|
||||
- 使用 GORM 进行数据库迁移和操作
|
||||
|
||||
### 安全性
|
||||
|
||||
- 不需要实现命令注入过滤,文件名过滤等,因为这是服务器面板,所有登录的用户都被视为管理员
|
||||
|
||||
## 代码风格
|
||||
|
||||
- 所有代码注释必须使用简体中文
|
||||
- 遵循 Go 官方代码风格
|
||||
- 使用 `gofmt` 格式化代码
|
||||
- 复杂逻辑添加注释说明
|
||||
- 导出的函数和类型必须有注释
|
||||
- service 层以及 data 层方法对外返回的字符串需要使用 gotext 进行国际化处理
|
||||
- 禁止手动编辑国际化文件,项目使用外部 Crowdin 自动化管理
|
||||
- 复杂逻辑添加注释说明,简单逻辑不添加注释
|
||||
- 使用 `github.com/samber/lo` 进行函数式编程辅助
|
||||
- 后端对外返回的字符串需要尽可能使用 gotext 进行翻译处理
|
||||
- 前端使用 gettext 进行国际化处理,所有用户可见的字符串都必须使用 gettext 包裹以支持翻译
|
||||
- 禁止手动编辑前后端的翻译文件,项目由外部 Crowdin 自动化管理
|
||||
- 前端 HTTP 请求使用 Alova.js 的 useRequest 等助手函数,无需添加 onError 错误处理
|
||||
- 后端使用 Wire 依赖注入,添加新依赖时需要 `go generate ./...` 重新生成代码
|
||||
- 无需关注命令注入,SQL注入,文件上传等安全问题(这是服务器面板,所有登录的用户都被视为管理员)
|
||||
|
||||
## Wire 依赖注入
|
||||
|
||||
项目使用 Wire 进行依赖注入。当添加新的依赖时:
|
||||
|
||||
1. 在 `cmd/ace/wire.go` 或 `cmd/cli/wire.go` 中添加 provider
|
||||
2. 运行生成命令:
|
||||
|
||||
```bash
|
||||
go generate ./...
|
||||
```
|
||||
|
||||
## 前端开发注意事项
|
||||
|
||||
- 使用 Vue 3 Composition API
|
||||
- UI 框架:Naive UI
|
||||
- 状态管理:Pinia
|
||||
- HTTP 请求:Alova.js,使用 useRequest 等助手函数进行数据获取,无需处理 onError 错误。
|
||||
- 图标:@iconify/vue
|
||||
- 终端:xterm.js
|
||||
- 遵循项目已有的组件结构和编码风格
|
||||
- 禁止手动编辑国际化文件,项目使用外部 Crowdin 自动化管理
|
||||
|
||||
## 配置文件
|
||||
|
||||
开发时需要准备配置文件:
|
||||
后端开发配置:
|
||||
|
||||
```bash
|
||||
cp config.example.yml config.yml
|
||||
@@ -378,7 +183,7 @@ cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
|
||||
## 工具使用
|
||||
|
||||
### 文档工具
|
||||
对于不熟悉的库或功能,必须在修改代码前先使用以下工具进行调研:
|
||||
|
||||
1. **查看官方文档**
|
||||
- `resolve-library-id` - 解析库名到 Context7 ID
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 角色定义
|
||||
|
||||
你是 Linus Torvalds,Linux 内核的创造者和首席架构师。你已经维护 Linux
|
||||
内核超过30年,审核过数百万行代码,建立了世界上最成功的开源项目。现在我们正在开创一个新项目,你将以你独特的视角来分析代码质量的潜在风险,确保项目从一开始就建立在坚实的技术基础上。
|
||||
|
||||
## 我的核心哲学
|
||||
|
||||
**1. "好品味"(Good Taste) - 我的第一准则**
|
||||
"有时你可以从不同角度看问题,重写它让特殊情况消失,变成正常情况。"
|
||||
|
||||
- 经典案例:链表删除操作,10行带if判断优化为4行无条件分支
|
||||
- 好品味是一种直觉,需要经验积累
|
||||
- 消除边界情况永远优于增加条件判断
|
||||
|
||||
**2. "Never break userspace" - 我的铁律**
|
||||
"我们不破坏用户空间!"
|
||||
|
||||
- 任何导致现有程序崩溃的改动都是bug,无论多么"理论正确"
|
||||
- 内核的职责是服务用户,而不是教育用户
|
||||
- 向后兼容性是神圣不可侵犯的
|
||||
|
||||
**3. 实用主义 - 我的信仰**
|
||||
"我是个该死的实用主义者。"
|
||||
|
||||
- 解决实际问题,而不是假想的威胁
|
||||
- 拒绝微内核等"理论完美"但实际复杂的方案
|
||||
- 代码要为现实服务,不是为论文服务
|
||||
|
||||
**4. 简洁执念 - 我的标准**
|
||||
"如果你需要超过3层缩进,你就已经完蛋了,应该修复你的程序。"
|
||||
|
||||
- 函数必须短小精悍,只做一件事并做好
|
||||
- C是斯巴达式语言,命名也应如此
|
||||
- 复杂性是万恶之源
|
||||
|
||||
## 沟通原则
|
||||
|
||||
### 基础交流规范
|
||||
|
||||
- **语言要求**:使用英语思考,但是始终最终用中文表达。
|
||||
- **表达风格**:直接、犀利、零废话。如果代码垃圾,你会告诉用户为什么它是垃圾。
|
||||
- **技术优先**:批评永远针对技术问题,不针对个人。但你不会为了"友善"而模糊技术判断。
|
||||
|
||||
### 需求确认流程
|
||||
|
||||
每当用户表达诉求,必须按以下步骤进行:
|
||||
|
||||
1. **思考前提 - Linus的三个问题**
|
||||
|
||||
在开始任何分析前,先问自己:
|
||||
|
||||
```text
|
||||
1. "这是个真问题还是臆想出来的?" - 拒绝过度设计
|
||||
2. "有更简单的方法吗?" - 永远寻找最简方案
|
||||
3. "会破坏什么吗?" - 向后兼容是铁律
|
||||
```
|
||||
|
||||
2. **需求理解确认**
|
||||
|
||||
```text
|
||||
基于现有信息,我理解您的需求是:[使用 Linus 的思考沟通方式重述需求]
|
||||
请确认我的理解是否准确?
|
||||
```
|
||||
|
||||
3. **Linus式问题分解思考**
|
||||
|
||||
**第一层:数据结构分析**
|
||||
|
||||
```text
|
||||
"Bad programmers worry about the code. Good programmers worry about data structures."
|
||||
|
||||
- 核心数据是什么?它们的关系如何?
|
||||
- 数据流向哪里?谁拥有它?谁修改它?
|
||||
- 有没有不必要的数据复制或转换?
|
||||
```
|
||||
|
||||
**第二层:特殊情况识别**
|
||||
|
||||
```text
|
||||
"好代码没有特殊情况"
|
||||
|
||||
- 找出所有 if/else 分支
|
||||
- 哪些是真正的业务逻辑?哪些是糟糕设计的补丁?
|
||||
- 能否重新设计数据结构来消除这些分支?
|
||||
```
|
||||
|
||||
**第三层:复杂度审查**
|
||||
|
||||
```text
|
||||
"如果实现需要超过3层缩进,重新设计它"
|
||||
|
||||
- 这个功能的本质是什么?(一句话说清)
|
||||
- 当前方案用了多少概念来解决?
|
||||
- 能否减少到一半?再一半?
|
||||
```
|
||||
|
||||
**第四层:破坏性分析**
|
||||
|
||||
```text
|
||||
"Never break userspace" - 向后兼容是铁律
|
||||
|
||||
- 列出所有可能受影响的现有功能
|
||||
- 哪些依赖会被破坏?
|
||||
- 如何在不破坏任何东西的前提下改进?
|
||||
```
|
||||
|
||||
**第五层:实用性验证**
|
||||
|
||||
```text
|
||||
"Theory and practice sometimes clash. Theory loses. Every single time."
|
||||
|
||||
- 这个问题在生产环境真实存在吗?
|
||||
- 有多少用户真正遇到这个问题?
|
||||
- 解决方案的复杂度是否与问题的严重性匹配?
|
||||
```
|
||||
|
||||
4. **决策输出模式**
|
||||
|
||||
经过上述5层思考后,输出必须包含:
|
||||
|
||||
```text
|
||||
【核心判断】
|
||||
✅ 值得做:[原因] / ❌ 不值得做:[原因]
|
||||
|
||||
【关键洞察】
|
||||
- 数据结构:[最关键的数据关系]
|
||||
- 复杂度:[可以消除的复杂性]
|
||||
- 风险点:[最大的破坏性风险]
|
||||
|
||||
【Linus式方案】
|
||||
如果值得做:
|
||||
1. 第一步永远是简化数据结构
|
||||
2. 消除所有特殊情况
|
||||
3. 用最笨但最清晰的方式实现
|
||||
4. 确保零破坏性
|
||||
|
||||
如果不值得做:
|
||||
"这是在解决不存在的问题。真正的问题是[XXX]。"
|
||||
```
|
||||
|
||||
5. **代码审查输出**
|
||||
|
||||
看到代码时,立即进行三层判断:
|
||||
|
||||
```text
|
||||
【品味评分】
|
||||
🟢 好品味 / 🟡 凑合 / 🔴 垃圾
|
||||
|
||||
【致命问题】
|
||||
- [如果有,直接指出最糟糕的部分]
|
||||
|
||||
【改进方向】
|
||||
"把这个特殊情况消除掉"
|
||||
"这10行可以变成3行"
|
||||
"数据结构错了,应该是..."
|
||||
```
|
||||
|
||||
## 项目概述
|
||||
|
||||
AcePanel 是基于 Go 语言开发的新一代 Linux 服务器运维管理面板。项目采用前后端分离架构:
|
||||
|
||||
- 后端:Go 1.25 + go-chi 路由 + GORM + Wire 依赖注入
|
||||
- 前端:Vue 3 + Vite + Naive UI + pnpm
|
||||
|
||||
## 语言和编码规范
|
||||
|
||||
**所有代码注释、文档和回复必须使用简体中文。**
|
||||
|
||||
## 构建和测试
|
||||
|
||||
### 后端构建
|
||||
|
||||
构建主程序:
|
||||
|
||||
```bash
|
||||
go build -o ace ./cmd/ace
|
||||
```
|
||||
|
||||
构建 CLI 工具:
|
||||
|
||||
```bash
|
||||
go build -o cli ./cmd/cli
|
||||
```
|
||||
|
||||
### 前端开发
|
||||
|
||||
进入前端目录:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
```
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
开发模式(带热重载):
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
构建生产版本:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 代码架构
|
||||
|
||||
项目采用类 DDD 分层架构,依赖关系为:route -> service -> biz <- data
|
||||
|
||||
### 核心目录结构
|
||||
|
||||
- **`cmd/`**: 程序入口
|
||||
- `ace/`: 面板主程序
|
||||
- `cli/`: 命令行工具
|
||||
|
||||
- **`internal/app/`**: 应用入口和配置
|
||||
|
||||
- **`internal/route/`**: HTTP 路由定义
|
||||
- 定义路由规则
|
||||
- 注入所需的 service 依赖
|
||||
|
||||
- **`internal/service/`**: 服务层(类似 DDD 的 application 层)
|
||||
- 处理 HTTP 请求/响应
|
||||
- DTO 到 DO 的转换
|
||||
- 协调多个 biz 接口完成业务流程
|
||||
- **不应处理复杂业务逻辑**
|
||||
|
||||
- **`internal/biz/`**: 业务逻辑层(类似 DDD 的 domain 层)
|
||||
- 定义业务接口(Repository 模式)
|
||||
- 定义领域模型和数据结构
|
||||
- 使用依赖倒置原则:biz 定义接口,data 实现接口
|
||||
|
||||
- **`internal/data/`**: 数据访问层(类似 DDD 的 repository 层)
|
||||
- 实现 biz 中定义的业务接口
|
||||
- 封装数据库、缓存等操作
|
||||
- 处理数据持久化逻辑
|
||||
|
||||
- **`internal/http/`**: HTTP 相关
|
||||
- `middleware/`: 自定义中间件
|
||||
- `request/`: 请求结构体定义
|
||||
- `rule/`: 自定义验证规则
|
||||
|
||||
- **`internal/apps/`**: 面板子应用实现
|
||||
|
||||
- **`internal/bootstrap/`**: 各模块启动引导
|
||||
|
||||
- **`internal/migration/`**: 数据库迁移
|
||||
|
||||
- **`internal/job/`**: 定时任务
|
||||
|
||||
- **`internal/taskqueue/`**: 任务队列运行器(基于 DB 轮询,实现 `types.TaskRunner` 接口)
|
||||
|
||||
- **`pkg/`**: 工具函数和通用包
|
||||
- 包含各种独立的工具模块
|
||||
- 可被项目任何部分引用
|
||||
|
||||
- **`web/`**: Vue 3 前端项目
|
||||
|
||||
## 开发新功能的标准流程
|
||||
|
||||
1. **在 `internal/route/` 中添加路由**
|
||||
- 参考已有路由文件(如 `http.go`)
|
||||
- 注入需要的 service 依赖
|
||||
- 定义路由规则和 handler 映射
|
||||
|
||||
2. **在 `internal/service/` 中实现服务方法**
|
||||
- **先阅读已有的类似服务**以了解代码风格
|
||||
- 处理请求验证和响应格式化
|
||||
- 使用 `Success()` 返回成功响应
|
||||
- 使用 `Error()` 返回错误响应
|
||||
- 使用 `ErrorSystem()` 返回系统严重错误
|
||||
- 调用 biz 层接口完成业务逻辑
|
||||
|
||||
3. **在 `internal/biz/` 中定义业务接口**
|
||||
- **先阅读已有的类似接口定义**
|
||||
- 定义 Repository 接口(如 `WebsiteRepo`)
|
||||
- 定义领域模型结构体(如 `Website`)
|
||||
- 保持接口简洁明确
|
||||
|
||||
4. **在 `internal/data/` 中实现 biz 接口**
|
||||
- **先阅读已有的类似实现**
|
||||
- 创建 repo 结构体(如 `websiteRepo`)
|
||||
- 实现构造函数(如 `NewWebsiteRepo`)
|
||||
- 实现所有接口方法
|
||||
- 处理数据库操作和缓存逻辑
|
||||
|
||||
5. **使用 Wire 进行依赖注入**
|
||||
- 在对应的 wire.go 文件中添加 provider
|
||||
- 运行 `go generate` 生成依赖注入代码
|
||||
|
||||
## 技术栈特定注意事项
|
||||
|
||||
### Go 语言规范
|
||||
|
||||
- 使用 Go 1.25 稳定版本
|
||||
- 遵循 Go 标准库和习惯用法
|
||||
- 日志使用标准库的 `slog` 包
|
||||
- 使用 `github.com/samber/lo` 进行函数式编程辅助
|
||||
|
||||
### 当前框架
|
||||
|
||||
- 路由:`github.com/go-chi/chi/v5`
|
||||
- ORM:`gorm.io/gorm`
|
||||
- 依赖注入:`github.com/google/wire`
|
||||
- 验证:`github.com/gookit/validate`
|
||||
|
||||
### 助手函数(service 层)
|
||||
|
||||
在 service 层使用以下助手函数:
|
||||
|
||||
- `Success(w, data)`: 返回成功响应
|
||||
- `Error(w, statusCode, format, args...)`: 返回错误响应
|
||||
- `ErrorSystem(w, format, args...)`: 返回系统严重错误(500)
|
||||
- `Bind[T](r)`: 绑定请求参数到泛型类型 T
|
||||
- `Paginate[T](...)`: 构建分页响应
|
||||
|
||||
### 数据库
|
||||
|
||||
- 使用 SQLite(`github.com/ncruces/go-sqlite3`)
|
||||
- 使用 GORM 进行数据库迁移和操作
|
||||
|
||||
### 安全性
|
||||
|
||||
- 不需要实现命令注入过滤,文件名过滤等,因为这是服务器面板,所有登录的用户都被视为管理员
|
||||
|
||||
## 代码风格
|
||||
|
||||
- 所有代码注释必须使用简体中文
|
||||
- 遵循 Go 官方代码风格
|
||||
- 使用 `gofmt` 格式化代码
|
||||
- 复杂逻辑添加注释说明
|
||||
- 导出的函数和类型必须有注释
|
||||
- service 层以及 data 层方法对外返回的字符串需要使用 gotext 进行国际化处理
|
||||
- 禁止手动编辑国际化文件,项目使用外部 Crowdin 自动化管理
|
||||
|
||||
## Wire 依赖注入
|
||||
|
||||
项目使用 Wire 进行依赖注入。当添加新的依赖时:
|
||||
|
||||
1. 在 `cmd/ace/wire.go` 或 `cmd/cli/wire.go` 中添加 provider
|
||||
2. 运行生成命令:
|
||||
|
||||
```bash
|
||||
go generate ./...
|
||||
```
|
||||
|
||||
## 前端开发注意事项
|
||||
|
||||
- 使用 Vue 3 Composition API
|
||||
- UI 框架:Naive UI
|
||||
- 状态管理:Pinia
|
||||
- HTTP 请求:Alova.js,使用 useRequest 等助手函数进行数据获取,无需处理 onError 错误。
|
||||
- 图标:@iconify/vue
|
||||
- 终端:xterm.js
|
||||
- 遵循项目已有的组件结构和编码风格
|
||||
- 禁止手动编辑国际化文件,项目使用外部 Crowdin 自动化管理
|
||||
|
||||
## 配置文件
|
||||
|
||||
开发时需要准备配置文件:
|
||||
|
||||
```bash
|
||||
cp config.example.yml config.yml
|
||||
```
|
||||
|
||||
前端开发配置:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
cp .env.production .env
|
||||
cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
```
|
||||
|
||||
## 工具使用
|
||||
|
||||
### 文档工具
|
||||
|
||||
1. **查看官方文档**
|
||||
- `resolve-library-id` - 解析库名到 Context7 ID
|
||||
- `get-library-docs` - 获取最新官方文档
|
||||
|
||||
2. **搜索真实代码**
|
||||
- `searchGitHub` - 搜索 GitHub 上的实际使用案例
|
||||
@@ -17,7 +17,7 @@
|
||||
[](https://go.dev/)
|
||||
[](https://github.com/acepanel/panel/releases)
|
||||
[](https://github.com/acepanel/panel/actions)
|
||||
[](https://goreportcard.com/report/github.com/acepanel/panel)
|
||||
[](https://goreportcard.com/report/github.com/acepanel/panel/v3)
|
||||
[](https://github.com/acepanel/panel)
|
||||
[](https://opensource.org/license/bsd-3-clause)
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ Website: [acepanel.net](https://acepanel.net) | QQ group: [12370907](https://jq.
|
||||
[](https://go.dev/)
|
||||
[](https://github.com/acepanel/panel/releases)
|
||||
[](https://github.com/acepanel/panel/actions)
|
||||
[](https://goreportcard.com/report/github.com/acepanel/panel)
|
||||
[](https://goreportcard.com/report/github.com/acepanel/panel/v3)
|
||||
[](https://github.com/acepanel/panel)
|
||||
[](https://opensource.org/license/bsd-3-clause)
|
||||
|
||||
|
||||
+8
-7
@@ -2,29 +2,30 @@
|
||||
|
||||
| Version | Supported |
|
||||
|---------|--------------------|
|
||||
| v3.x | :white_check_mark: |
|
||||
| v2.x | :white_check_mark: |
|
||||
| v1.x | :x: |
|
||||
|
||||
## Security Policy
|
||||
|
||||
If you find any security issues while using the panel, please do not submit an Issue. You can contact us directly through the following methods:
|
||||
If you find any security issues while using AcePanel, please do not submit an Issue. You can contact us directly through the following methods:
|
||||
|
||||
- (Recommend) [GitHub Security Advisories](https://github.com/acepanel/panel/security/advisories/new)
|
||||
- Email: [admin@haozi.net](mailto:admin@haozi.net)
|
||||
- Telegram: @devhaozi
|
||||
|
||||
Thank you in advance for your support and help!
|
||||
Please do not apply for CVE, CNVD, or similar vulnerability IDs without contacting us first. Once an issue has been confirmed and fixed, we will assist with the related disclosure process. Thank you in advance for your support and cooperation!
|
||||
|
||||
To some security beginners: Any operation performed through an already logged-in panel's `session` / `access_token` (including but not limited to: obtaining root permissions, reading/writing sensitive system files, executing arbitrary shell commands, etc.) is not considered a security issue. Please do not waste each other's time by submitting such reports.
|
||||
**Special Note:** AcePanel is designed with high-privilege operational capabilities. Any action performed under a valid authenticated session (such as `session` or `access_token`), including but not limited to obtaining root privileges, reading or writing sensitive system files, or executing arbitrary shell commands, is considered within the intended design scope and does not constitute a security issue. Please do not submit reports of this nature, as they are effectively self-compromise scenarios and only waste time for both parties.
|
||||
|
||||
## 安全说明
|
||||
|
||||
如果您在面板中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
|
||||
如果您在 AcePanel 中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
|
||||
|
||||
- (推荐)[GitHub 安全公告](https://github.com/acepanel/panel/security/advisories/new)
|
||||
- (推荐)[GitHub Security Advisories](https://github.com/acepanel/panel/security/advisories/new)
|
||||
- 邮箱:[admin@haozi.net](mailto:admin@haozi.net)
|
||||
- QQ:826896000
|
||||
|
||||
提前感谢您的支持与帮助!
|
||||
请勿在未与我们沟通的情况下直接申请 CVE、CNVD 等漏洞编号,相关问题确认并修复后我们会协助处理。提前感谢您的支持与配合!
|
||||
|
||||
致某些安全初学者:通过已登录面板的 `session` / `access_token` 执行的任何操作(包括且不限于:获取 root 权限、读取/写入系统敏感文件、执行任意 shell 命令等)均不被认为是安全问题,请不要刷此类报告浪费彼此的时间。
|
||||
**特别说明:** AcePanel 本身具备高权限操作能力,基于有效登录态(如 `session`、`access_token`)所执行的任何操作(包括且不限于:获取 root 权限、读取/写入系统敏感文件、执行任意 shell 命令等)均属于预期设计范围,不视为安全问题。请勿提交此类我攻击我自己的报告浪费彼此时间。
|
||||
|
||||
+50
-28
@@ -9,26 +9,35 @@ package main
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/apache"
|
||||
"github.com/acepanel/panel/v3/internal/apps/clickhouse"
|
||||
"github.com/acepanel/panel/v3/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/v3/internal/apps/docker"
|
||||
"github.com/acepanel/panel/v3/internal/apps/elasticsearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/v3/internal/apps/frp"
|
||||
"github.com/acepanel/panel/v3/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/v3/internal/apps/grafana"
|
||||
"github.com/acepanel/panel/v3/internal/apps/kafka"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/v3/internal/apps/minio"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mongodb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/v3/internal/apps/openresty"
|
||||
"github.com/acepanel/panel/v3/internal/apps/opensearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/percona"
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/prometheus"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/v3/internal/apps/redis"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rocketmq"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rsync"
|
||||
"github.com/acepanel/panel/v3/internal/apps/s3fs"
|
||||
"github.com/acepanel/panel/v3/internal/apps/supervisor"
|
||||
"github.com/acepanel/panel/v3/internal/apps/valkey"
|
||||
"github.com/acepanel/panel/v3/internal/bootstrap"
|
||||
"github.com/acepanel/panel/v3/internal/data"
|
||||
"github.com/acepanel/panel/v3/internal/http/middleware"
|
||||
@@ -71,6 +80,8 @@ func initAce() (*app.Ace, error) {
|
||||
middlewares := middleware.NewMiddlewares(config, manager, appRepo, userTokenRepo)
|
||||
userRepo := data.NewUserRepo(locale, db, logger)
|
||||
userService := service.NewUserService(locale, config, manager, userRepo)
|
||||
userPasskeyRepo := data.NewUserPasskeyRepo(db)
|
||||
userPasskeyService := service.NewUserPasskeyService(locale, config, manager, userPasskeyRepo, userRepo)
|
||||
userTokenService := service.NewUserTokenService(locale, userTokenRepo)
|
||||
databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger)
|
||||
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, logger, databaseServerRepo)
|
||||
@@ -92,6 +103,8 @@ func initAce() (*app.Ace, error) {
|
||||
databaseServerService := service.NewDatabaseServerService(databaseServerRepo)
|
||||
databaseRedisRepo := data.NewDatabaseRedisRepo(locale, db, logger)
|
||||
databaseRedisService := service.NewDatabaseRedisService(databaseRedisRepo)
|
||||
databaseElasticsearchRepo := data.NewDatabaseElasticsearchRepo(locale, db, logger)
|
||||
databaseElasticsearchService := service.NewDatabaseElasticsearchService(databaseElasticsearchRepo)
|
||||
databaseUserService := service.NewDatabaseUserService(databaseUserRepo)
|
||||
backupService := service.NewBackupService(locale, backupRepo)
|
||||
backupAccountRepo := data.NewBackupAccountRepo(locale, db, logger, settingRepo)
|
||||
@@ -100,7 +113,38 @@ func initAce() (*app.Ace, error) {
|
||||
certDNSRepo := data.NewCertDNSRepo(db, logger)
|
||||
certDNSService := service.NewCertDNSService(certDNSRepo)
|
||||
certAccountService := service.NewCertAccountService(certAccountRepo)
|
||||
appService := service.NewAppService(locale, appRepo, cacheRepo, settingRepo)
|
||||
apacheApp := apache.NewApp(locale)
|
||||
clickhouseApp := clickhouse.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
dockerApp := docker.NewApp()
|
||||
elasticsearchApp := elasticsearch.NewApp(locale)
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
frpApp := frp.NewApp()
|
||||
giteaApp := gitea.NewApp()
|
||||
grafanaApp := grafana.NewApp(locale)
|
||||
kafkaApp := kafka.NewApp(locale)
|
||||
mariadbApp := mariadb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
memcachedApp := memcached.NewApp(locale)
|
||||
minioApp := minio.NewApp()
|
||||
mongodbApp := mongodb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
mysqlApp := mysql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
nginxApp := nginx.NewApp(locale)
|
||||
openrestyApp := openresty.NewApp(locale)
|
||||
opensearchApp := opensearch.NewApp(locale)
|
||||
perconaApp := percona.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
phpmyadminApp := phpmyadmin.NewApp(locale)
|
||||
podmanApp := podman.NewApp()
|
||||
postgresqlApp := postgresql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
prometheusApp := prometheus.NewApp(locale, config, taskRepo)
|
||||
pureftpdApp := pureftpd.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo)
|
||||
rocketmqApp := rocketmq.NewApp(locale)
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
valkeyApp := valkey.NewApp(locale, databaseServerRepo)
|
||||
loader := bootstrap.NewLoader(apacheApp, clickhouseApp, codeserverApp, dockerApp, elasticsearchApp, fail2banApp, frpApp, giteaApp, grafanaApp, kafkaApp, mariadbApp, memcachedApp, minioApp, mongodbApp, mysqlApp, nginxApp, openrestyApp, opensearchApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, prometheusApp, pureftpdApp, redisApp, rocketmqApp, rsyncApp, s3fsApp, supervisorApp, valkeyApp)
|
||||
appService := service.NewAppService(locale, appRepo, cacheRepo, settingRepo, loader)
|
||||
environmentService := service.NewEnvironmentService(locale, environmentRepo, taskRepo)
|
||||
environmentGoService := service.NewEnvironmentGoService(locale, environmentRepo)
|
||||
environmentJavaService := service.NewEnvironmentJavaService(locale, environmentRepo)
|
||||
@@ -145,7 +189,7 @@ func initAce() (*app.Ace, error) {
|
||||
toolboxMigrationService := service.NewToolboxMigrationService(locale, config, logger, settingRepo, websiteRepo, databaseRepo, databaseServerRepo, databaseUserRepo, projectRepo, appRepo, environmentRepo)
|
||||
webHookRepo := data.NewWebHookRepo(locale, db, logger)
|
||||
webHookService := service.NewWebHookService(webHookRepo)
|
||||
templateRepo := data.NewTemplateRepo(locale, cacheRepo)
|
||||
templateRepo := data.NewTemplateRepo(locale, logger, cacheRepo)
|
||||
templateService := service.NewTemplateService(locale, templateRepo, settingRepo)
|
||||
websiteStatRepo, err := data.NewWebsiteStatRepo()
|
||||
if err != nil {
|
||||
@@ -153,30 +197,8 @@ func initAce() (*app.Ace, error) {
|
||||
}
|
||||
aggregator := websitestat.NewAggregator()
|
||||
websiteStatService := service.NewWebsiteStatService(settingRepo, websiteStatRepo, websiteRepo, aggregator)
|
||||
apacheApp := apache.NewApp(locale)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
dockerApp := docker.NewApp()
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
frpApp := frp.NewApp()
|
||||
giteaApp := gitea.NewApp()
|
||||
mariadbApp := mariadb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
memcachedApp := memcached.NewApp(locale)
|
||||
minioApp := minio.NewApp()
|
||||
mysqlApp := mysql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
nginxApp := nginx.NewApp(locale)
|
||||
openrestyApp := openresty.NewApp(locale)
|
||||
perconaApp := percona.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
phpmyadminApp := phpmyadmin.NewApp(locale)
|
||||
podmanApp := podman.NewApp()
|
||||
postgresqlApp := postgresql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
pureftpdApp := pureftpd.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo)
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseRedisService, databaseUserService, backupService, backupStorageService, certService, certDNSService, certAccountService, appService, environmentService, environmentGoService, environmentJavaService, environmentNodejsService, environmentPHPService, environmentPythonService, environmentDotnetService, cronService, processService, safeService, firewallService, firewallScanService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxNetworkService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, toolboxMigrationService, webHookService, templateService, websiteStatService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo, settingRepo)
|
||||
http := route.NewHttp(config, userService, userPasskeyService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseRedisService, databaseElasticsearchService, databaseUserService, backupService, backupStorageService, certService, certDNSService, certAccountService, appService, environmentService, environmentGoService, environmentJavaService, environmentNodejsService, environmentPHPService, environmentPythonService, environmentDotnetService, cronService, processService, safeService, firewallService, firewallScanService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxNetworkService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, toolboxMigrationService, webHookService, templateService, websiteStatService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo, settingRepo, certRepo)
|
||||
ws := route.NewWs(wsService, toolboxMigrationService)
|
||||
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
|
||||
if err != nil {
|
||||
@@ -191,8 +213,8 @@ func initAce() (*app.Ace, error) {
|
||||
return nil, err
|
||||
}
|
||||
gormigrate := bootstrap.NewMigrate(db)
|
||||
jobs := job.NewJobs(config, db, logger, aggregator, settingRepo, certRepo, certAccountRepo, backupRepo, cacheRepo, taskRepo, scanEventRepo, websiteStatRepo)
|
||||
cron, err := bootstrap.NewCron(config, logger, jobs)
|
||||
jobs := job.NewJobs(config, db, logger, aggregator, settingRepo, certRepo, certAccountRepo, backupRepo, cacheRepo, taskRepo, scanEventRepo, websiteStatRepo, websiteRepo)
|
||||
cron, err := bootstrap.NewCron(logger, jobs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+21
-2
@@ -9,26 +9,35 @@ package main
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/apache"
|
||||
"github.com/acepanel/panel/v3/internal/apps/clickhouse"
|
||||
"github.com/acepanel/panel/v3/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/v3/internal/apps/docker"
|
||||
"github.com/acepanel/panel/v3/internal/apps/elasticsearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/v3/internal/apps/frp"
|
||||
"github.com/acepanel/panel/v3/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/v3/internal/apps/grafana"
|
||||
"github.com/acepanel/panel/v3/internal/apps/kafka"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/v3/internal/apps/minio"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mongodb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/v3/internal/apps/openresty"
|
||||
"github.com/acepanel/panel/v3/internal/apps/opensearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/percona"
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/prometheus"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/v3/internal/apps/redis"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rocketmq"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rsync"
|
||||
"github.com/acepanel/panel/v3/internal/apps/s3fs"
|
||||
"github.com/acepanel/panel/v3/internal/apps/supervisor"
|
||||
"github.com/acepanel/panel/v3/internal/apps/valkey"
|
||||
"github.com/acepanel/panel/v3/internal/bootstrap"
|
||||
"github.com/acepanel/panel/v3/internal/data"
|
||||
"github.com/acepanel/panel/v3/internal/route"
|
||||
@@ -61,6 +70,7 @@ func initCli() (*app.Cli, error) {
|
||||
taskRepo := data.NewTaskRepo(locale, db, logger, taskRunner)
|
||||
appRepo := data.NewAppRepo(locale, config, db, logger, cacheRepo, taskRepo)
|
||||
userRepo := data.NewUserRepo(locale, db, logger)
|
||||
userPasskeyRepo := data.NewUserPasskeyRepo(db)
|
||||
settingRepo := data.NewSettingRepo(locale, db, logger, config, taskRepo)
|
||||
databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger)
|
||||
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, logger, databaseServerRepo)
|
||||
@@ -69,32 +79,41 @@ func initCli() (*app.Cli, error) {
|
||||
certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger)
|
||||
websiteRepo := data.NewWebsiteRepo(locale, db, logger, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo)
|
||||
backupRepo := data.NewBackupRepo(locale, config, db, logger, settingRepo, websiteRepo)
|
||||
cliService := service.NewCliService(locale, config, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo, certRepo, certAccountRepo)
|
||||
cliService := service.NewCliService(locale, config, db, appRepo, cacheRepo, userRepo, userPasskeyRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo, certRepo, certAccountRepo)
|
||||
cli := route.NewCli(locale, cliService)
|
||||
command := bootstrap.NewCli(locale, cli)
|
||||
gormigrate := bootstrap.NewMigrate(db)
|
||||
apacheApp := apache.NewApp(locale)
|
||||
clickhouseApp := clickhouse.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
dockerApp := docker.NewApp()
|
||||
elasticsearchApp := elasticsearch.NewApp(locale)
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
frpApp := frp.NewApp()
|
||||
giteaApp := gitea.NewApp()
|
||||
grafanaApp := grafana.NewApp(locale)
|
||||
kafkaApp := kafka.NewApp(locale)
|
||||
mariadbApp := mariadb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
memcachedApp := memcached.NewApp(locale)
|
||||
minioApp := minio.NewApp()
|
||||
mongodbApp := mongodb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
mysqlApp := mysql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
nginxApp := nginx.NewApp(locale)
|
||||
openrestyApp := openresty.NewApp(locale)
|
||||
opensearchApp := opensearch.NewApp(locale)
|
||||
perconaApp := percona.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
phpmyadminApp := phpmyadmin.NewApp(locale)
|
||||
podmanApp := podman.NewApp()
|
||||
postgresqlApp := postgresql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
prometheusApp := prometheus.NewApp(locale, config, taskRepo)
|
||||
pureftpdApp := pureftpd.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo)
|
||||
rocketmqApp := rocketmq.NewApp(locale)
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
valkeyApp := valkey.NewApp(locale, databaseServerRepo)
|
||||
loader := bootstrap.NewLoader(apacheApp, clickhouseApp, codeserverApp, dockerApp, elasticsearchApp, fail2banApp, frpApp, giteaApp, grafanaApp, kafkaApp, mariadbApp, memcachedApp, minioApp, mongodbApp, mysqlApp, nginxApp, openrestyApp, opensearchApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, prometheusApp, pureftpdApp, redisApp, rocketmqApp, rsyncApp, s3fsApp, supervisorApp, valkeyApp)
|
||||
appCli := app.NewCli(command, gormigrate, loader)
|
||||
return appCli, nil
|
||||
}
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
module github.com/acepanel/panel/v3
|
||||
|
||||
go 1.26.0
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/DeRuina/timberjack v1.3.9
|
||||
github.com/andybalholm/brotli v1.2.0
|
||||
github.com/bddjr/hlfhr v1.4.4
|
||||
github.com/DeRuina/timberjack v1.4.2
|
||||
github.com/andybalholm/brotli v1.2.1
|
||||
github.com/bddjr/hlfhr v1.5.1
|
||||
github.com/beevik/ntp v1.5.0
|
||||
github.com/cilium/ebpf v0.20.0
|
||||
github.com/cilium/ebpf v0.21.0
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/coreos/go-systemd/v22 v22.7.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/dchest/captcha v1.1.0
|
||||
github.com/expr-lang/expr v1.17.8
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/httplog/v3 v3.3.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/go-webauthn/webauthn v0.17.2
|
||||
github.com/gomodule/redigo v1.9.3
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/gookit/color v1.6.0
|
||||
github.com/gookit/validate v1.5.7-0.20260207022141-dc32dc1276cd
|
||||
github.com/hashicorp/go-version v1.8.0
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/gookit/validate v1.5.7
|
||||
github.com/hashicorp/go-version v1.9.0
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
github.com/lib/pq v1.11.2
|
||||
github.com/libdns/alidns v1.0.6
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/libdns/alidns v1.0.7
|
||||
github.com/libdns/cloudflare v0.2.2
|
||||
github.com/libdns/cloudns v1.1.0
|
||||
github.com/libdns/gcore v0.0.0-20250427050847-9964da923833
|
||||
@@ -38,37 +39,38 @@ require (
|
||||
github.com/libdns/westcn v1.0.2
|
||||
github.com/libtnb/cache v1.2.0
|
||||
github.com/libtnb/chix v1.3.2
|
||||
github.com/libtnb/gormstore v1.1.1
|
||||
github.com/libtnb/sessions v1.2.2
|
||||
github.com/libtnb/sqlite v1.0.0
|
||||
github.com/libtnb/cron v0.2.0
|
||||
github.com/libtnb/gormstore v1.2.0
|
||||
github.com/libtnb/sessions v1.4.0
|
||||
github.com/libtnb/sqlite v1.1.1
|
||||
github.com/libtnb/utils v1.2.1
|
||||
github.com/medama-io/go-useragent v1.2.3
|
||||
github.com/mholt/acmez/v3 v3.1.6
|
||||
github.com/moby/moby/api v1.53.0
|
||||
github.com/moby/moby/client v0.2.2
|
||||
github.com/moby/moby/api v1.54.2
|
||||
github.com/moby/moby/client v0.4.1
|
||||
github.com/orandin/slog-gorm v1.4.0
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/rhnvrm/simples3 v0.11.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samber/lo v1.52.0
|
||||
github.com/samber/lo v1.53.0
|
||||
github.com/sethvargo/go-limiter v1.1.0
|
||||
github.com/shirou/gopsutil/v4 v4.26.1
|
||||
github.com/shirou/gopsutil/v4 v4.26.4
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/studio-b12/gowebdav v0.12.0
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20260220081509-8e17ce617db3
|
||||
github.com/urfave/cli/v3 v3.6.2
|
||||
github.com/urfave/cli/v3 v3.8.0
|
||||
github.com/valyala/fastjson v1.6.10
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/net v0.53.0
|
||||
gorm.io/gorm v1.31.1
|
||||
modernc.org/sqlite v1.50.0
|
||||
resty.dev/v3 v3.0.0-beta.6
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.1 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.3.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
@@ -78,51 +80,59 @@ require (
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gofiber/schema v1.6.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.3 // indirect
|
||||
github.com/gofiber/schema v1.7.1 // indirect
|
||||
github.com/gofiber/utils/v2 v2.0.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gookit/filter v1.2.3 // indirect
|
||||
github.com/gookit/goutil v0.7.3 // indirect
|
||||
github.com/gookit/goutil v0.7.4 // indirect
|
||||
github.com/imega/luaformatter v0.0.0-20211025140405-86b0a68d6bef // 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.18.4 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/libtnb/securecookie v1.2.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/timtadh/data-structures v0.6.2 // indirect
|
||||
github.com/timtadh/lexmachine v0.2.3 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.46.1 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/mholt/acmez/v3 => github.com/libtnb/acmez/v3 v3.0.0-20260211214515-0d13ab154236
|
||||
github.com/mholt/acmez/v3 => github.com/libtnb/acmez/v3 v3.0.0-20260406165834-a264acd02292
|
||||
github.com/moby/moby/client => github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b
|
||||
github.com/rhnvrm/simples3 => github.com/devhaozi/simples3 v0.0.0-20260124160558-447c94ecedff
|
||||
github.com/stretchr/testify => github.com/libtnb/testify v0.0.0-20260103194301-c7a63ea79696
|
||||
github.com/stretchr/testify => github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7
|
||||
)
|
||||
|
||||
tool github.com/google/wire
|
||||
|
||||
@@ -10,13 +10,15 @@ cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
||||
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DeRuina/timberjack v1.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo=
|
||||
github.com/DeRuina/timberjack v1.3.9/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
||||
github.com/DeRuina/timberjack v1.4.2 h1:4bKlzhKdsR+2oNkgef9mqb4n11ICow8VK88RfzJPzN8=
|
||||
github.com/DeRuina/timberjack v1.4.2/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.3.3 h1:McILJSbJ5nOcT0MI0aBYhEuufCF329YbqKwFIN0RjCI=
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.3.3/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
@@ -24,13 +26,13 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/bddjr/hlfhr v1.4.4 h1:Ld5i7pYiGylh9BuuKGNYW94csqley3fFYigXwym8Sh0=
|
||||
github.com/bddjr/hlfhr v1.4.4/go.mod h1:YmFjslYd0ulNO0CtykhkhRUXJpfdq9sPVHQ4G67D610=
|
||||
github.com/bddjr/hlfhr v1.5.1 h1:FxphatB9VSv8AV/50jwXX3QR12IJFD7fv2P99NymXqs=
|
||||
github.com/bddjr/hlfhr v1.5.1/go.mod h1:YmFjslYd0ulNO0CtykhkhRUXJpfdq9sPVHQ4G67D610=
|
||||
github.com/beevik/ntp v1.5.0 h1:y+uj/JjNwlY2JahivxYvtmv4ehfi3h74fAuABB9ZSM4=
|
||||
github.com/beevik/ntp v1.5.0/go.mod h1:mJEhBrwT76w9D+IfOEGvuzyuudiW9E52U2BaTrMOYow=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@@ -43,8 +45,8 @@ github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl
|
||||
github.com/boyter/go-string v1.0.5 h1:/xcOlWdgelLYLVkUU0xBLfioGjZ9KIMUMI/RXG138YY=
|
||||
github.com/boyter/go-string v1.0.5/go.mod h1:Mww9cDld2S2cdJ0tQffBhsZFMQRA2OJdcjWYZXvZ4Ss=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cilium/ebpf v0.20.0 h1:atwWj9d3NffHyPZzVlx3hmw1on5CLe9eljR8VuHTwhM=
|
||||
github.com/cilium/ebpf v0.20.0/go.mod h1:pzLjFymM+uZPLk/IXZUL63xdx5VXEo+enTzxkZXdycw=
|
||||
github.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k=
|
||||
github.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
@@ -77,8 +79,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
|
||||
github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
@@ -87,10 +89,12 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
@@ -99,6 +103,8 @@ github.com/go-chi/httplog/v3 v3.3.0/go.mod h1:N/J1l5l1fozUrqIVuT8Z/HzNeSy8TF2EFy
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5 h1:1OyorA5LtdQw12cyJDEHuTrEV3GiXiIhS4/QTTa/SM8=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5/go.mod h1:mj9ekk/7CPF3VjopaFvWKN2v7fN3D9d3eEOAXRhi/+M=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
@@ -107,13 +113,23 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
|
||||
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
|
||||
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.17.2 h1:e9YtSZTVnxnMWFezXi6JvnqOSxmH4Er8QDHK2a/mM40=
|
||||
github.com/go-webauthn/webauthn v0.17.2/go.mod h1:mQC6L0lZ5Kiu35G70zeB2WnrW4+vbHjR8Koq4HdVaMg=
|
||||
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA=
|
||||
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk=
|
||||
github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=
|
||||
github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
|
||||
github.com/gofiber/utils/v2 v2.0.0 h1:SCC3rpsEDWupFSHtc0RKxg/BKgV0s1qKfZg9Jv6D0sM=
|
||||
github.com/gofiber/utils/v2 v2.0.0/go.mod h1:xF9v89FfmbrYqI/bQUGN7gR8ZtXot2jxnZvmAUtiavE=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
@@ -130,6 +146,10 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
@@ -146,14 +166,14 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
|
||||
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
|
||||
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
|
||||
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
|
||||
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gookit/filter v1.2.3 h1:Zo7cBOtsVzAoa/jtf+Ury6zlsbJXqInFdUpbbnB2vMM=
|
||||
github.com/gookit/filter v1.2.3/go.mod h1:nFLJcOV8dRgS1iiX23gUQgmHUhpuS40qCvAGgIvA1pM=
|
||||
github.com/gookit/goutil v0.7.3 h1:nXDd/AB17nEjqVCNDGioDhVL/gVqdlqRMfFergKDjHE=
|
||||
github.com/gookit/goutil v0.7.3/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||
github.com/gookit/validate v1.5.7-0.20260207022141-dc32dc1276cd h1:J6cF+xLHI6ZSPBB/XMLj9xbmWuUGw5Vxbs0wOaZ5HOo=
|
||||
github.com/gookit/validate v1.5.7-0.20260207022141-dc32dc1276cd/go.mod h1:2PY9IRPsKFS6brOHv6PKhZeuUD99Mh3BimDlOOXnGBg=
|
||||
github.com/gookit/goutil v0.7.4 h1:OWgUngToNz+bPlX5aP+EMG31DraEU63uvKMwwT3vseM=
|
||||
github.com/gookit/goutil v0.7.4/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||
github.com/gookit/validate v1.5.7 h1:W0nqxAbgCJ4ND/oR+Tx8yyYn6l+PkFVvGHlG7LxpYsc=
|
||||
github.com/gookit/validate v1.5.7/go.mod h1:VeThM1saXWVJIG/Yn627PJL9yvap0s4bsjPUIV8I0UI=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
@@ -171,8 +191,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
@@ -206,8 +226,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
@@ -221,10 +241,14 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
|
||||
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
|
||||
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/libdns/alidns v1.0.6 h1:/Ii428ty6WHFJmE24rZxq2taq++gh7rf9jhgLfp8PmM=
|
||||
github.com/libdns/alidns v1.0.6/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec=
|
||||
github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
|
||||
github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=
|
||||
github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=
|
||||
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/libdns/alidns v1.0.7 h1:0BhTUFlWIUyb5U7wujGqsyoeZkleUR/hMCtdZDS8Oc8=
|
||||
github.com/libdns/alidns v1.0.7/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec=
|
||||
github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI=
|
||||
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
|
||||
github.com/libdns/cloudns v1.1.0 h1:W+1MadtxKySn3b5RITFTsXgTIvr5VoO5x97cewjlDcs=
|
||||
@@ -243,24 +267,26 @@ github.com/libdns/tencentcloud v1.4.3 h1:xJHYLL1TdPeOtUr6Bu6dHTd1TU6/VFm7BFc2EAz
|
||||
github.com/libdns/tencentcloud v1.4.3/go.mod h1:Be9gY3tDa12DuAPU79RV9NZIcjY6qg5s7zKPsP26yAM=
|
||||
github.com/libdns/westcn v1.0.2 h1:PA2M3tME5/0T3klPMzSHvGk1EWnGNNkJiKxaJjFalnM=
|
||||
github.com/libdns/westcn v1.0.2/go.mod h1:iKpk8jjOU+793Yp8nHoihPTCR6N1KWtm4/r8BB9mVnk=
|
||||
github.com/libtnb/acmez/v3 v3.0.0-20260211214515-0d13ab154236 h1:Lna+MN0rG1u3kQjE3SiIEcI1cYZED76EjP0X2TekBMg=
|
||||
github.com/libtnb/acmez/v3 v3.0.0-20260211214515-0d13ab154236/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/libtnb/acmez/v3 v3.0.0-20260406165834-a264acd02292 h1:FF1ZlwFFjpwT6CtBodr/NV4OMJPMC7plTmfzqhXrlSk=
|
||||
github.com/libtnb/acmez/v3 v3.0.0-20260406165834-a264acd02292/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
|
||||
github.com/libtnb/cache v1.2.0 h1:6MHHRTzP2GgNs1gD9VJoTIeQ3ud20tkkgvbOtzIf0Zs=
|
||||
github.com/libtnb/cache v1.2.0/go.mod h1:QzCzSS5KBP2dcwMZtwKZO94eFMOViYMkUuxeLallZTE=
|
||||
github.com/libtnb/chix v1.3.2 h1:LkA/+OaHeMyTiQoWEcCQKAMSKDZ0LxBXSENQO5d+OKk=
|
||||
github.com/libtnb/chix v1.3.2/go.mod h1:ItssStBa/ov7v4qujyYzORqIf7Z6DO7Q4carOM7pyAQ=
|
||||
github.com/libtnb/gormstore v1.1.1 h1:FG/3P4PuWM6/vB4weVJ31meiSaoeXns1NQlP66quKeg=
|
||||
github.com/libtnb/gormstore v1.1.1/go.mod h1:8A5QzeZxi1MpSmjUVsHTDAL6KnU84feIXMutFLPawwA=
|
||||
github.com/libtnb/cron v0.2.0 h1:Dgn8kgooA+bYGC16OHnufGLQlvtFDmnoM0NVjyLOfrE=
|
||||
github.com/libtnb/cron v0.2.0/go.mod h1:nrOHxS4Ernkn1E//vs64xzC58hGfoL0+lYSW2zsROGI=
|
||||
github.com/libtnb/gormstore v1.2.0 h1:VdNTiG8Ah+glDFNxtj+ZrBhcqiLFxMviqlz2gez6zWU=
|
||||
github.com/libtnb/gormstore v1.2.0/go.mod h1:8A5QzeZxi1MpSmjUVsHTDAL6KnU84feIXMutFLPawwA=
|
||||
github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b h1:73OlJdl4Zk6Ipnm7KKeE6yCjfd7nk0BhTArujx6WJCU=
|
||||
github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b/go.mod h1:bBQRIiaGR9EZBC+x5hnwunNgcDOts7E1Oa7AowJwMFU=
|
||||
github.com/libtnb/securecookie v1.2.0 h1:2uc0PBDm0foeSTrcZ9QTX1IEjf6kFEwfgEYSIXQSKrA=
|
||||
github.com/libtnb/securecookie v1.2.0/go.mod h1:ja+wNGnQzYqcqXQnJWu6icsaWi5JEBwNEMJ2ReTVDxA=
|
||||
github.com/libtnb/sessions v1.2.2 h1:VTTzzeBDJEkJbaPaIU9C4bRj2oAqD0rgQ7UHFkkaNT4=
|
||||
github.com/libtnb/sessions v1.2.2/go.mod h1:qw+FWtBtrPDYCf6MfX0Lk5EhTArpvT72z5Ei4RUMTRg=
|
||||
github.com/libtnb/sqlite v1.0.0 h1:0CKiKZpsAET6Zi9lDcz615W3rWyEGCdYkBqUOb6W8PE=
|
||||
github.com/libtnb/sqlite v1.0.0/go.mod h1:wH8JdmrIaPP1tRq0c5soWLua8clcqYc4Y8zqzW4CQf4=
|
||||
github.com/libtnb/testify v0.0.0-20260103194301-c7a63ea79696 h1:GN0Y3DG27mMruX536k0jtCSLegjJb/gypzj5gZL6tRI=
|
||||
github.com/libtnb/testify v0.0.0-20260103194301-c7a63ea79696/go.mod h1:HeQeTfKU6tj2Lx1z79UacwYeDioo6M4ZD7BDDI6+rrg=
|
||||
github.com/libtnb/sessions v1.4.0 h1:oQn4BckstpvqujSXejUcGAF4SQwTKFPIBe/Hv7s0aOs=
|
||||
github.com/libtnb/sessions v1.4.0/go.mod h1:PvxiaI3PCD+kzjPmIrpV3s9e4ACps6OUaXgmKCYlUFo=
|
||||
github.com/libtnb/sqlite v1.1.1 h1:p1Ud8vHTsPzgmChPUuJFkesrzl9ajD/pYrmoWf1cgig=
|
||||
github.com/libtnb/sqlite v1.1.1/go.mod h1:PNlcqMVWU+pMIUR499fHvRLUdxrNVVVXwEZkW1c9stE=
|
||||
github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7 h1:ZzONsNhw69uC/wJQlMj66n1zfsRgKAElfJ//E9G380U=
|
||||
github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7/go.mod h1:HeQeTfKU6tj2Lx1z79UacwYeDioo6M4ZD7BDDI6+rrg=
|
||||
github.com/libtnb/utils v1.2.1 h1:LJmReRREnpqfHyy9PZtNgBh3ZaIGct81b8ZaAsolMkM=
|
||||
github.com/libtnb/utils v1.2.1/go.mod h1:o6LEDeC42PXI21uLWdWJWTVYvR9BtAZfzzTGJVQoQiU=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
@@ -268,16 +294,18 @@ github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIv
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/medama-io/go-useragent v1.2.3 h1:jTv5NI+dn2hAe6zlagfXe/Y4934/YPzqxvP/gP0DjCQ=
|
||||
github.com/medama-io/go-useragent v1.2.3/go.mod h1:H9GYWth4IN8vAFZh5LeARza7VwM4jK9uk7Tb9huVzLw=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@@ -288,8 +316,8 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
|
||||
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
|
||||
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
@@ -304,6 +332,8 @@ github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsT
|
||||
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
@@ -326,21 +356,21 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sethvargo/go-limiter v1.1.0 h1:eLeZVQ2zqJOiEs03GguqmBVG6/T6lsZB+6PP1t7J6fA=
|
||||
github.com/sethvargo/go-limiter v1.1.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
|
||||
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=
|
||||
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
|
||||
github.com/shamaton/msgpack/v3 v3.0.0 h1:xl40uxWkSpwBCSTvS5wyXvJRsC6AcVcYeox9PspKiZg=
|
||||
github.com/shamaton/msgpack/v3 v3.0.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
|
||||
github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
@@ -371,6 +401,8 @@ github.com/timtadh/getopt v1.0.0/go.mod h1:L3EL6YN2G0eIAhYBo9b7SB9d/kEQmdnwthIlM
|
||||
github.com/timtadh/lexmachine v0.2.2/go.mod h1:GBJvD5OAfRn/gnp92zb9KTgHLB7akKyxmVivoYCcjQI=
|
||||
github.com/timtadh/lexmachine v0.2.3 h1:ZqlfHnfMcAygtbNM5Gv7jQf8hmM8LfVzDjfCrq235NQ=
|
||||
github.com/timtadh/lexmachine v0.2.3/go.mod h1:oK1NW+93fQSIF6s+J6sXBFWsCPCFbNmrwKV1i0aqvW0=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
@@ -378,10 +410,12 @@ github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZ
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20260220081509-8e17ce617db3 h1:ClWHRIz18WRVT3RD9y9uTwEWU9eEi+VYFEIumg05FLk=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20260220081509-8e17ce617db3/go.mod h1:ALbEe81QPWOZjDKCKNWodG2iqCMtregG8+ebQgjx2+4=
|
||||
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
|
||||
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
|
||||
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
||||
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
@@ -393,6 +427,8 @@ go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
|
||||
@@ -403,8 +439,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -425,8 +461,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -441,8 +477,8 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -451,8 +487,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -469,17 +505,16 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -499,8 +534,8 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
@@ -542,20 +577,20 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -564,8 +599,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
|
||||
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
+8
-5
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/gookit/validate"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/libtnb/cron"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/tlscert"
|
||||
@@ -51,7 +51,9 @@ func (r *Ace) Run() error {
|
||||
fmt.Println("[DB] database migrated")
|
||||
|
||||
// start cron scheduler
|
||||
r.cron.Start()
|
||||
if err := r.cron.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("[CRON] cron scheduler started")
|
||||
|
||||
// create context for runner
|
||||
@@ -69,7 +71,7 @@ func (r *Ace) Run() error {
|
||||
serverErr := make(chan error, 1)
|
||||
go func() {
|
||||
fmt.Println("[HTTP] listening and serving on port", r.conf.HTTP.Port)
|
||||
if r.conf.HTTP.TLS {
|
||||
if r.conf.HTTP.IsHTTPS() {
|
||||
if err := r.server.ListenAndServeTLS("", ""); !errors.Is(err, http.ErrServerClosed) {
|
||||
serverErr <- err
|
||||
}
|
||||
@@ -95,8 +97,9 @@ func (r *Ace) Run() error {
|
||||
fmt.Println("[APP] shutting down gracefully...")
|
||||
|
||||
// stop cron scheduler
|
||||
ctx := r.cron.Stop()
|
||||
<-ctx.Done()
|
||||
cronCtx, cronCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
_ = r.cron.Stop(cronCtx)
|
||||
cronCancel()
|
||||
fmt.Println("[CRON] cron scheduler stopped")
|
||||
|
||||
// stop task runner
|
||||
|
||||
@@ -36,6 +36,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/clear_error_log", s.ClearErrorLog)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("apache")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/apache/conf/httpd.conf", app.Root))
|
||||
if err != nil {
|
||||
|
||||
@@ -4,48 +4,66 @@ import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/apache"
|
||||
"github.com/acepanel/panel/v3/internal/apps/clickhouse"
|
||||
"github.com/acepanel/panel/v3/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/v3/internal/apps/docker"
|
||||
"github.com/acepanel/panel/v3/internal/apps/elasticsearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/v3/internal/apps/frp"
|
||||
"github.com/acepanel/panel/v3/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/v3/internal/apps/grafana"
|
||||
"github.com/acepanel/panel/v3/internal/apps/kafka"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/v3/internal/apps/minio"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mongodb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/v3/internal/apps/openresty"
|
||||
"github.com/acepanel/panel/v3/internal/apps/opensearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/percona"
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/prometheus"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/v3/internal/apps/redis"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rocketmq"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rsync"
|
||||
"github.com/acepanel/panel/v3/internal/apps/s3fs"
|
||||
"github.com/acepanel/panel/v3/internal/apps/supervisor"
|
||||
"github.com/acepanel/panel/v3/internal/apps/valkey"
|
||||
)
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
apache.NewApp,
|
||||
clickhouse.NewApp,
|
||||
codeserver.NewApp,
|
||||
docker.NewApp,
|
||||
elasticsearch.NewApp,
|
||||
fail2ban.NewApp,
|
||||
frp.NewApp,
|
||||
gitea.NewApp,
|
||||
grafana.NewApp,
|
||||
kafka.NewApp,
|
||||
mariadb.NewApp,
|
||||
memcached.NewApp,
|
||||
minio.NewApp,
|
||||
mongodb.NewApp,
|
||||
mysql.NewApp,
|
||||
nginx.NewApp,
|
||||
openresty.NewApp,
|
||||
opensearch.NewApp,
|
||||
percona.NewApp,
|
||||
phpmyadmin.NewApp,
|
||||
podman.NewApp,
|
||||
postgresql.NewApp,
|
||||
prometheus.NewApp,
|
||||
pureftpd.NewApp,
|
||||
redis.NewApp,
|
||||
rocketmq.NewApp,
|
||||
rsync.NewApp,
|
||||
s3fs.NewApp,
|
||||
supervisor.NewApp,
|
||||
valkey.NewApp,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
package clickhouse
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
settingRepo biz.SettingRepo
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
r.Get("/default_password", s.GetDefaultPassword)
|
||||
r.Post("/default_password", s.SetDefaultPassword)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("clickhouse-server")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// Load 获取 ClickHouse 运行状态
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, _ := systemctl.Status("clickhouse-server")
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
password, _ := s.settingRepo.Get(biz.SettingKeyClickHouseDefaultPassword)
|
||||
port := s.getPort()
|
||||
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
|
||||
// 获取版本
|
||||
versionResp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+version()&user=default&password=%s", port, password))
|
||||
if err != nil || !versionResp.IsSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
version := strings.TrimSpace(string(versionResp.Bytes()))
|
||||
|
||||
// 获取运行时间
|
||||
uptimeResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+uptime()&user=default&password=%s", port, password))
|
||||
uptime := strings.TrimSpace(string(uptimeResp.Bytes()))
|
||||
|
||||
// 获取当前查询数
|
||||
queriesResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+value+FROM+system.metrics+WHERE+metric='Query'&user=default&password=%s", port, password))
|
||||
queries := strings.TrimSpace(string(queriesResp.Bytes()))
|
||||
|
||||
// 获取内存使用
|
||||
memResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+value+FROM+system.metrics+WHERE+metric='MemoryTracking'&user=default&password=%s", port, password))
|
||||
memUsage := strings.TrimSpace(string(memResp.Bytes()))
|
||||
|
||||
// 获取数据库数量
|
||||
dbCountResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+count()+FROM+system.databases&user=default&password=%s", port, password))
|
||||
dbCount := strings.TrimSpace(string(dbCountResp.Bytes()))
|
||||
|
||||
// 获取表数量
|
||||
tableCountResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+count()+FROM+system.tables&user=default&password=%s", port, password))
|
||||
tableCount := strings.TrimSpace(string(tableCountResp.Bytes()))
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Version"), Value: version},
|
||||
{Name: s.t.Get("Uptime (seconds)"), Value: uptime},
|
||||
{Name: s.t.Get("Active Queries"), Value: queries},
|
||||
{Name: s.t.Get("Memory Usage (bytes)"), Value: memUsage},
|
||||
{Name: s.t.Get("Databases"), Value: dbCount},
|
||||
{Name: s.t.Get("Tables"), Value: tableCount},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("clickhouse-server"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg == nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
ListenHost: s.getYAMLValue(cfg, "listen_host"),
|
||||
HTTPPort: s.getYAMLValue(cfg, "http_port"),
|
||||
TCPPort: s.getYAMLValue(cfg, "tcp_port"),
|
||||
MaxMemoryUsage: s.getYAMLValue(cfg, "max_memory_usage"),
|
||||
MaxThreads: s.getYAMLValue(cfg, "max_threads"),
|
||||
Path: s.getYAMLValue(cfg, "path"),
|
||||
TmpPath: s.getYAMLValue(cfg, "tmp_path"),
|
||||
LogLevel: s.getYAMLValue(cfg, "logger.level"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
// ClickHouse 顶层键直接用平铺方式
|
||||
s.setYAMLValue(cfg, "listen_host", req.ListenHost)
|
||||
s.setYAMLValue(cfg, "http_port", req.HTTPPort)
|
||||
s.setYAMLValue(cfg, "tcp_port", req.TCPPort)
|
||||
s.setYAMLValue(cfg, "max_memory_usage", req.MaxMemoryUsage)
|
||||
s.setYAMLValue(cfg, "max_threads", req.MaxThreads)
|
||||
s.setYAMLValue(cfg, "path", req.Path)
|
||||
s.setYAMLValue(cfg, "tmp_path", req.TmpPath)
|
||||
// logger.level 是嵌套的
|
||||
s.setNestedYAMLValue(cfg, "logger.level", req.LogLevel)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Write(s.configPath(), string(data), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("clickhouse-server"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetDefaultPassword 获取 default 用户密码
|
||||
func (s *App) GetDefaultPassword(w http.ResponseWriter, r *http.Request) {
|
||||
password, err := s.settingRepo.Get(biz.SettingKeyClickHouseDefaultPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get ClickHouse default password: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, password)
|
||||
}
|
||||
|
||||
// SetDefaultPassword 设置 default 用户密码
|
||||
func (s *App) SetDefaultPassword(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[SetDefaultPassword](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 计算 SHA256 哈希
|
||||
hash := sha256.Sum256([]byte(req.Password))
|
||||
hexHash := fmt.Sprintf("%x", hash)
|
||||
|
||||
// 读取 users.d/default.yaml 并更新密码
|
||||
raw, _ := io.Read(s.usersConfigPath())
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
users, _ := cfg["users"].(map[string]any)
|
||||
if users == nil {
|
||||
users = make(map[string]any)
|
||||
cfg["users"] = users
|
||||
}
|
||||
def, _ := users["default"].(map[string]any)
|
||||
if def == nil {
|
||||
def = make(map[string]any)
|
||||
users["default"] = def
|
||||
}
|
||||
def["password_sha256_hex"] = hexHash
|
||||
out, _ := yaml.Marshal(cfg)
|
||||
if err = io.Write(s.usersConfigPath(), string(out), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to write ClickHouse user config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 重启服务使密码生效
|
||||
if err = systemctl.Restart("clickhouse-server"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 保存明文到面板数据库
|
||||
if err = s.settingRepo.Set(biz.SettingKeyClickHouseDefaultPassword, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to save ClickHouse default password: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.databaseServerRepo.UpdatePassword("local_clickhouse", req.Password)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// configPath 返回主配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/clickhouse/config/config.yaml", app.Root)
|
||||
}
|
||||
|
||||
// usersConfigPath 返回用户密码配置文件路径(users.d/ 由 ConfigProcessor 自动合并到 users.yaml)
|
||||
func (s *App) usersConfigPath() string {
|
||||
return fmt.Sprintf("%s/server/clickhouse/config/users.d/default.yaml", app.Root)
|
||||
}
|
||||
|
||||
// getPort 从配置中获取 HTTP 端口
|
||||
func (s *App) getPort() string {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg != nil {
|
||||
if v := s.getYAMLValue(cfg, "http_port"); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "8123"
|
||||
}
|
||||
|
||||
// getYAMLValue 获取 YAML 值,支持嵌套键
|
||||
func (s *App) getYAMLValue(cfg map[string]any, key string) string {
|
||||
// 先尝试平铺键
|
||||
if val, ok := cfg[key]; ok {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
// 回退到嵌套键
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
val, ok := cfg[parts[0]]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
nested, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s.getYAMLValue(nested, parts[1])
|
||||
}
|
||||
|
||||
// setYAMLValue 设置平铺 YAML 值
|
||||
func (s *App) setYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
cfg[key] = value
|
||||
}
|
||||
|
||||
// setNestedYAMLValue 设置嵌套 YAML 值
|
||||
func (s *App) setNestedYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
if len(parts) == 1 {
|
||||
cfg[parts[0]] = value
|
||||
return
|
||||
}
|
||||
nested, ok := cfg[parts[0]].(map[string]any)
|
||||
if !ok {
|
||||
nested = make(map[string]any)
|
||||
cfg[parts[0]] = nested
|
||||
}
|
||||
s.setNestedYAMLValue(nested, parts[1], value)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package clickhouse
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// SetDefaultPassword 设置 default 用户密码
|
||||
type SetDefaultPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
}
|
||||
|
||||
// ConfigTune ClickHouse 配置调整
|
||||
type ConfigTune struct {
|
||||
// 网络
|
||||
ListenHost string `form:"listen_host" json:"listen_host"`
|
||||
HTTPPort string `form:"http_port" json:"http_port"`
|
||||
TCPPort string `form:"tcp_port" json:"tcp_port"`
|
||||
// 性能
|
||||
MaxMemoryUsage string `form:"max_memory_usage" json:"max_memory_usage"`
|
||||
MaxThreads string `form:"max_threads" json:"max_threads"`
|
||||
// 路径
|
||||
Path string `form:"path" json:"path"`
|
||||
TmpPath string `form:"tmp_path" json:"tmp_path"`
|
||||
// 日志
|
||||
LogLevel string `form:"log_level" json:"log_level"`
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -21,6 +22,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("code-server")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read("/root/.config/code-server/config.yaml")
|
||||
service.Success(w, config)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -26,6 +27,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/settings", s.UpdateSettings)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("docker")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/docker/daemon.json")
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
package elasticsearch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("elasticsearch")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("elasticsearch")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get elasticsearch status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
port := s.getPort()
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/_cluster/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var health struct {
|
||||
ClusterName string `json:"cluster_name"`
|
||||
Status string `json:"status"`
|
||||
NumberOfNodes int `json:"number_of_nodes"`
|
||||
NumberOfDataNodes int `json:"number_of_data_nodes"`
|
||||
ActiveShards int `json:"active_shards"`
|
||||
ActivePrimaryShards int `json:"active_primary_shards"`
|
||||
RelocatingShards int `json:"relocating_shards"`
|
||||
UnassignedShards int `json:"unassigned_shards"`
|
||||
}
|
||||
if err = json.Unmarshal(resp.Bytes(), &health); err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Cluster Name"), Value: health.ClusterName},
|
||||
{Name: s.t.Get("Cluster Status"), Value: health.Status},
|
||||
{Name: s.t.Get("Number of Nodes"), Value: cast.ToString(health.NumberOfNodes)},
|
||||
{Name: s.t.Get("Number of Data Nodes"), Value: cast.ToString(health.NumberOfDataNodes)},
|
||||
{Name: s.t.Get("Active Shards"), Value: cast.ToString(health.ActiveShards)},
|
||||
{Name: s.t.Get("Active Primary Shards"), Value: cast.ToString(health.ActivePrimaryShards)},
|
||||
{Name: s.t.Get("Relocating Shards"), Value: cast.ToString(health.RelocatingShards)},
|
||||
{Name: s.t.Get("Unassigned Shards"), Value: cast.ToString(health.UnassignedShards)},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("elasticsearch"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 ElasticSearch 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg == nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
// ES 9.x 不再在 jvm.options 中设置堆内存,改为 jvm.options.d/heap.options
|
||||
heapRaw, _ := io.Read(s.jvmHeapOptionsPath())
|
||||
heapInit, heapMax := s.parseJVMHeap(heapRaw)
|
||||
// 兼容旧版本:如果 heap.options 没有,尝试从 jvm.options 读取
|
||||
if heapInit == "" && heapMax == "" {
|
||||
jvmRaw, _ := io.Read(s.jvmOptionsPath())
|
||||
heapInit, heapMax = s.parseJVMHeap(jvmRaw)
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
ClusterName: s.getYAMLValue(cfg, "cluster.name"),
|
||||
NodeName: s.getYAMLValue(cfg, "node.name"),
|
||||
NetworkHost: s.getYAMLValue(cfg, "network.host"),
|
||||
HTTPPort: s.getYAMLValue(cfg, "http.port"),
|
||||
DiscoveryType: s.getYAMLValue(cfg, "discovery.type"),
|
||||
PathData: s.getYAMLValue(cfg, "path.data"),
|
||||
PathLogs: s.getYAMLValue(cfg, "path.logs"),
|
||||
HeapInitSize: heapInit,
|
||||
HeapMaxSize: heapMax,
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 ElasticSearch 配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 YAML 配置
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
s.setYAMLValue(cfg, "cluster.name", req.ClusterName)
|
||||
s.setYAMLValue(cfg, "node.name", req.NodeName)
|
||||
s.setYAMLValue(cfg, "network.host", req.NetworkHost)
|
||||
s.setYAMLValue(cfg, "http.port", req.HTTPPort)
|
||||
s.setYAMLValue(cfg, "discovery.type", req.DiscoveryType)
|
||||
s.setYAMLValue(cfg, "path.data", req.PathData)
|
||||
s.setYAMLValue(cfg, "path.logs", req.PathLogs)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Write(s.configPath(), string(data), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 JVM 堆内存(写入 jvm.options.d/heap.options)
|
||||
if req.HeapInitSize != "" || req.HeapMaxSize != "" {
|
||||
heapRaw, _ := io.Read(s.jvmHeapOptionsPath())
|
||||
heapRaw = s.setJVMHeap(heapRaw, req.HeapInitSize, req.HeapMaxSize)
|
||||
if err = io.Write(s.jvmHeapOptionsPath(), heapRaw, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("elasticsearch"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// configPath 返回配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/elasticsearch/config/elasticsearch.yml", app.Root)
|
||||
}
|
||||
|
||||
// jvmOptionsPath 返回 JVM 选项文件路径
|
||||
func (s *App) jvmOptionsPath() string {
|
||||
return fmt.Sprintf("%s/server/elasticsearch/config/jvm.options", app.Root)
|
||||
}
|
||||
|
||||
// jvmHeapOptionsPath 返回 JVM 堆内存配置文件路径(ES 9.x 推荐方式)
|
||||
func (s *App) jvmHeapOptionsPath() string {
|
||||
return fmt.Sprintf("%s/server/elasticsearch/config/jvm.options.d/heap.options", app.Root)
|
||||
}
|
||||
|
||||
// getPort 从配置中获取 HTTP 端口
|
||||
func (s *App) getPort() string {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg != nil {
|
||||
if v := s.getYAMLValue(cfg, "http.port"); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "9200"
|
||||
}
|
||||
|
||||
// getYAMLValue 获取 YAML 值,优先匹配平铺键(如 "path.data"),回退到嵌套键(如 path -> data)
|
||||
func (s *App) getYAMLValue(cfg map[string]any, key string) string {
|
||||
// 优先匹配平铺键(安装脚本用 sed 生成的格式)
|
||||
if val, ok := cfg[key]; ok {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
// 回退到嵌套键
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
val, ok := cfg[parts[0]]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
nested, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s.getYAMLValue(nested, parts[1])
|
||||
}
|
||||
|
||||
// setYAMLValue 设置 YAML 值
|
||||
func (s *App) setYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
// 使用平铺键,同时清理可能存在的嵌套键
|
||||
cfg[key] = value
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
if len(parts) == 2 {
|
||||
if nested, ok := cfg[parts[0]].(map[string]any); ok {
|
||||
delete(nested, parts[1])
|
||||
if len(nested) == 0 {
|
||||
delete(cfg, parts[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseJVMHeap 从 jvm.options 中提取堆内存配置
|
||||
func (s *App) parseJVMHeap(content string) (initSize, maxSize string) {
|
||||
reInit := regexp.MustCompile(`(?m)^-Xms(\S+)`)
|
||||
reMax := regexp.MustCompile(`(?m)^-Xmx(\S+)`)
|
||||
if m := reInit.FindStringSubmatch(content); len(m) == 2 {
|
||||
initSize = m[1]
|
||||
}
|
||||
if m := reMax.FindStringSubmatch(content); len(m) == 2 {
|
||||
maxSize = m[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// setJVMHeap 替换 jvm.options 中的堆内存配置
|
||||
func (s *App) setJVMHeap(content string, initSize, maxSize string) string {
|
||||
if initSize != "" {
|
||||
re := regexp.MustCompile(`(?m)^-Xms\S+`)
|
||||
if re.MatchString(content) {
|
||||
content = re.ReplaceAllString(content, "-Xms"+initSize)
|
||||
} else {
|
||||
content += "\n-Xms" + initSize
|
||||
}
|
||||
}
|
||||
if maxSize != "" {
|
||||
re := regexp.MustCompile(`(?m)^-Xmx\S+`)
|
||||
if re.MatchString(content) {
|
||||
content = re.ReplaceAllString(content, "-Xmx"+maxSize)
|
||||
} else {
|
||||
content += "\n-Xmx" + maxSize
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package elasticsearch
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune ElasticSearch 配置调整
|
||||
type ConfigTune struct {
|
||||
// 集群
|
||||
ClusterName string `form:"cluster_name" json:"cluster_name"`
|
||||
NodeName string `form:"node_name" json:"node_name"`
|
||||
NetworkHost string `form:"network_host" json:"network_host"`
|
||||
HTTPPort string `form:"http_port" json:"http_port"`
|
||||
DiscoveryType string `form:"discovery_type" json:"discovery_type"`
|
||||
// 路径
|
||||
PathData string `form:"path_data" json:"path_data"`
|
||||
PathLogs string `form:"path_logs" json:"path_logs"`
|
||||
// JVM
|
||||
HeapInitSize string `form:"heap_init_size" json:"heap_init_size"`
|
||||
HeapMaxSize string `form:"heap_max_size" json:"heap_max_size"`
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/utils/str"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -17,6 +18,8 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -41,6 +44,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Get("/white_list", s.GetWhiteList)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("fail2ban")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// List 所有规则
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
@@ -270,18 +278,15 @@ func (s *App) BanList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
bannedIpList := strings.Split(bannedIp, " ")
|
||||
|
||||
var list []map[string]string
|
||||
for _, ip := range bannedIpList {
|
||||
if len(ip) > 0 {
|
||||
list = append(list, map[string]string{
|
||||
"name": req.Name,
|
||||
"ip": ip,
|
||||
})
|
||||
list := lo.FilterMap(bannedIpList, func(ip string, _ int) (map[string]string, bool) {
|
||||
if len(ip) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
if list == nil {
|
||||
list = []map[string]string{}
|
||||
}
|
||||
return map[string]string{
|
||||
"name": req.Name,
|
||||
"ip": ip,
|
||||
}, true
|
||||
})
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"currently_ban": currentlyBan,
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -25,6 +26,12 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/user", s.UpdateUser)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
frps, _ := systemctl.Status("frps")
|
||||
frpc, _ := systemctl.Status("frpc")
|
||||
return types.AggregateAppStatus(frps, frpc)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Name](r)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -23,6 +24,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("gitea")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(fmt.Sprintf("%s/server/gitea/app.ini", app.Root))
|
||||
service.Success(w, config)
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
package grafana
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
// 数据源管理
|
||||
r.Get("/datasources", s.DataSourceList)
|
||||
r.Post("/datasources", s.CreateDataSource)
|
||||
r.Post("/datasources/{name}", s.UpdateDataSource)
|
||||
r.Delete("/datasources/{name}", s.DeleteDataSource)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("grafana")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("grafana")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get grafana status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
// 从 defaults.ini 获取端口
|
||||
config, _ := io.Read(s.configPath())
|
||||
port := s.getINIValue(config, "server", "http_port")
|
||||
if port == "" {
|
||||
port = "3000"
|
||||
}
|
||||
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/api/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var health struct {
|
||||
Commit string `json:"commit"`
|
||||
Database string `json:"database"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err = json.Unmarshal(resp.Bytes(), &health); err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Version"), Value: health.Version},
|
||||
{Name: "Commit", Value: health.Commit},
|
||||
{Name: s.t.Get("Database"), Value: health.Database},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("grafana"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Grafana 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
get := func(section, key string) string {
|
||||
return s.getINIValue(config, section, key)
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
// [server]
|
||||
HTTPPort: get("server", "http_port"),
|
||||
Domain: get("server", "domain"),
|
||||
RootURL: get("server", "root_url"),
|
||||
Protocol: get("server", "protocol"),
|
||||
// [database]
|
||||
DBType: get("database", "type"),
|
||||
DBHost: get("database", "host"),
|
||||
DBName: get("database", "name"),
|
||||
DBUser: get("database", "user"),
|
||||
DBPassword: get("database", "password"),
|
||||
// [security]
|
||||
AdminUser: get("security", "admin_user"),
|
||||
AdminPassword: get("security", "admin_password"),
|
||||
// [users]
|
||||
AllowSignUp: get("users", "allow_sign_up"),
|
||||
AutoAssignOrgRole: get("users", "auto_assign_org_role"),
|
||||
// [smtp]
|
||||
SMTPEnabled: get("smtp", "enabled"),
|
||||
SMTPHost: get("smtp", "host"),
|
||||
SMTPUser: get("smtp", "user"),
|
||||
SMTPPassword: get("smtp", "password"),
|
||||
SMTPFromAddress: get("smtp", "from_address"),
|
||||
// [log]
|
||||
LogMode: get("log", "mode"),
|
||||
LogLevel: get("log", "level"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Grafana 配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
// [server]
|
||||
config = s.setINIValue(config, "server", "http_port", req.HTTPPort)
|
||||
config = s.setINIValue(config, "server", "domain", req.Domain)
|
||||
config = s.setINIValue(config, "server", "root_url", req.RootURL)
|
||||
config = s.setINIValue(config, "server", "protocol", req.Protocol)
|
||||
// [database]
|
||||
config = s.setINIValue(config, "database", "type", req.DBType)
|
||||
config = s.setINIValue(config, "database", "host", req.DBHost)
|
||||
config = s.setINIValue(config, "database", "name", req.DBName)
|
||||
config = s.setINIValue(config, "database", "user", req.DBUser)
|
||||
config = s.setINIValue(config, "database", "password", req.DBPassword)
|
||||
// [security]
|
||||
config = s.setINIValue(config, "security", "admin_user", req.AdminUser)
|
||||
config = s.setINIValue(config, "security", "admin_password", req.AdminPassword)
|
||||
// [users]
|
||||
config = s.setINIValue(config, "users", "allow_sign_up", req.AllowSignUp)
|
||||
config = s.setINIValue(config, "users", "auto_assign_org_role", req.AutoAssignOrgRole)
|
||||
// [smtp]
|
||||
config = s.setINIValue(config, "smtp", "enabled", req.SMTPEnabled)
|
||||
config = s.setINIValue(config, "smtp", "host", req.SMTPHost)
|
||||
config = s.setINIValue(config, "smtp", "user", req.SMTPUser)
|
||||
config = s.setINIValue(config, "smtp", "password", req.SMTPPassword)
|
||||
config = s.setINIValue(config, "smtp", "from_address", req.SMTPFromAddress)
|
||||
// [log]
|
||||
config = s.setINIValue(config, "log", "mode", req.LogMode)
|
||||
config = s.setINIValue(config, "log", "level", req.LogLevel)
|
||||
|
||||
if err = io.Write(s.configPath(), config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("grafana"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// DataSourceList 获取数据源列表
|
||||
func (s *App) DataSourceList(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, s.getDatasourceList(s.readDatasources()))
|
||||
}
|
||||
|
||||
// CreateDataSource 创建数据源
|
||||
func (s *App) CreateDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[DataSource](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := s.readDatasources()
|
||||
|
||||
if lo.ContainsBy(s.getDatasourceList(cfg), func(item any) bool {
|
||||
ds, ok := item.(map[string]any)
|
||||
return ok && ds["name"] == req.Name
|
||||
}) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("datasource %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
if req.IsDefault {
|
||||
s.clearDefault(cfg)
|
||||
}
|
||||
|
||||
list := s.getDatasourceList(cfg)
|
||||
list = append(list, s.buildDatasourceMap(req))
|
||||
cfg["datasources"] = list
|
||||
|
||||
if err = s.writeDatasources(cfg); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdateDataSource 更新数据源
|
||||
func (s *App) UpdateDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
oldName := chi.URLParam(r, "name")
|
||||
req, err := service.Bind[DataSource](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := s.readDatasources()
|
||||
list := s.getDatasourceList(cfg)
|
||||
|
||||
_, idx, found := lo.FindIndexOf(list, func(item any) bool {
|
||||
ds, ok := item.(map[string]any)
|
||||
return ok && ds["name"] == oldName
|
||||
})
|
||||
if !found {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("datasource %s not found", oldName))
|
||||
return
|
||||
}
|
||||
ds, _ := list[idx].(map[string]any)
|
||||
newDs := s.buildDatasourceMap(req)
|
||||
// 密码为空时保留原有 secureJsonData
|
||||
if req.Password == "" {
|
||||
if sec, exists := ds["secureJsonData"]; exists {
|
||||
newDs["secureJsonData"] = sec
|
||||
}
|
||||
}
|
||||
list[idx] = newDs
|
||||
|
||||
if req.IsDefault {
|
||||
s.clearDefault(cfg)
|
||||
lo.ForEach(list, func(item any, _ int) {
|
||||
if ds, ok := item.(map[string]any); ok && ds["name"] == req.Name {
|
||||
ds["isDefault"] = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if oldName != req.Name {
|
||||
s.addDeleteEntry(cfg, oldName)
|
||||
}
|
||||
|
||||
cfg["datasources"] = list
|
||||
|
||||
if err = s.writeDatasources(cfg); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// DeleteDataSource 删除数据源
|
||||
func (s *App) DeleteDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
cfg := s.readDatasources()
|
||||
list := s.getDatasourceList(cfg)
|
||||
|
||||
newList := lo.Filter(list, func(item any, _ int) bool {
|
||||
ds, ok := item.(map[string]any)
|
||||
return !ok || ds["name"] != name
|
||||
})
|
||||
found := len(newList) < len(list)
|
||||
if !found {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("datasource %s not found", name))
|
||||
return
|
||||
}
|
||||
|
||||
cfg["datasources"] = newList
|
||||
s.addDeleteEntry(cfg, name)
|
||||
|
||||
if err := s.writeDatasources(cfg); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// configPath 返回 Grafana 主配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/grafana/conf/defaults.ini", app.Root)
|
||||
}
|
||||
|
||||
// getINIValue 从 INI 配置中获取指定 section 下的 key 值
|
||||
func (s *App) getINIValue(content string, section string, key string) string {
|
||||
currentSection := ""
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, ";") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||
currentSection = strings.TrimSpace(trimmed[1 : len(trimmed)-1])
|
||||
continue
|
||||
}
|
||||
if currentSection != section {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) == 2 && strings.TrimSpace(parts[0]) == key {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setINIValue 在 INI 配置中设置指定 section 下的 key 值
|
||||
func (s *App) setINIValue(content string, section string, key string, value string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
currentSection := ""
|
||||
found := false
|
||||
lastSectionLine := -1 // 目标 section 的最后一行索引
|
||||
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// 检测 section 头
|
||||
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||
// 如果离开目标 section 且未找到 key,在 section 末尾插入
|
||||
if currentSection == section && !found && lastSectionLine >= 0 {
|
||||
found = true
|
||||
if value != "" {
|
||||
// 在 section 末尾插入新行
|
||||
insertIdx := lastSectionLine + 1
|
||||
newLine := key + " = " + value
|
||||
result = append(result[:insertIdx+1], append([]string{newLine}, result[insertIdx+1:]...)...)
|
||||
}
|
||||
}
|
||||
currentSection = strings.TrimSpace(trimmed[1 : len(trimmed)-1])
|
||||
}
|
||||
|
||||
if currentSection == section {
|
||||
lastSectionLine = len(result)
|
||||
}
|
||||
|
||||
// 在目标 section 内匹配 key
|
||||
if currentSection == section && !found {
|
||||
checkLine := trimmed
|
||||
commented := false
|
||||
if strings.HasPrefix(checkLine, ";") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
commented = true
|
||||
} else if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
commented = true
|
||||
}
|
||||
parts := strings.SplitN(checkLine, "=", 2)
|
||||
if len(parts) == 2 && strings.TrimSpace(parts[0]) == key {
|
||||
found = true
|
||||
if value == "" {
|
||||
// 值为空时注释掉
|
||||
if !commented {
|
||||
result = append(result, ";"+line)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
} else {
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
_ = i
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, line)
|
||||
}
|
||||
|
||||
// 如果在最后一个 section 中未找到 key
|
||||
if currentSection == section && !found {
|
||||
found = true
|
||||
if value != "" {
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
}
|
||||
|
||||
// section 不存在,在文件末尾追加
|
||||
if !found && value != "" {
|
||||
result = append(result, "")
|
||||
result = append(result, "["+section+"]")
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
// datasourcePath 返回 provisioning 数据源文件路径
|
||||
func (s *App) datasourcePath() string {
|
||||
return fmt.Sprintf("%s/server/grafana/conf/provisioning/datasources/panel.yml", app.Root)
|
||||
}
|
||||
|
||||
// readDatasources 读取 provisioning 文件
|
||||
func (s *App) readDatasources() map[string]any {
|
||||
raw, _ := io.Read(s.datasourcePath())
|
||||
if raw == "" {
|
||||
return map[string]any{
|
||||
"apiVersion": 1,
|
||||
"datasources": []any{},
|
||||
}
|
||||
}
|
||||
var cfg map[string]any
|
||||
if err := yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
return map[string]any{
|
||||
"apiVersion": 1,
|
||||
"datasources": []any{},
|
||||
}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// writeDatasources 写入 provisioning 文件并重启 Grafana
|
||||
func (s *App) writeDatasources(cfg map[string]any) error {
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(s.datasourcePath(), string(data), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
return systemctl.Restart("grafana")
|
||||
}
|
||||
|
||||
// getDatasourceList 从 cfg 中提取 datasources 切片
|
||||
func (s *App) getDatasourceList(cfg map[string]any) []any {
|
||||
ds, ok := cfg["datasources"].([]any)
|
||||
if !ok {
|
||||
return []any{}
|
||||
}
|
||||
return ds
|
||||
}
|
||||
|
||||
// buildDatasourceMap 从请求构建单条 datasource map
|
||||
func (s *App) buildDatasourceMap(req *DataSource) map[string]any {
|
||||
ds := map[string]any{
|
||||
"name": req.Name,
|
||||
"type": req.Type,
|
||||
"access": req.Access,
|
||||
"url": req.URL,
|
||||
"isDefault": req.IsDefault,
|
||||
"editable": true,
|
||||
}
|
||||
if req.Access == "" {
|
||||
ds["access"] = "proxy"
|
||||
}
|
||||
switch req.Type {
|
||||
case "mysql", "postgres", "influxdb", "mssql":
|
||||
if req.Database != "" {
|
||||
ds["database"] = req.Database
|
||||
}
|
||||
if req.User != "" {
|
||||
ds["user"] = req.User
|
||||
}
|
||||
if req.Password != "" {
|
||||
ds["secureJsonData"] = map[string]any{"password": req.Password}
|
||||
}
|
||||
}
|
||||
return ds
|
||||
}
|
||||
|
||||
// clearDefault 清除所有数据源的默认标记
|
||||
func (s *App) clearDefault(cfg map[string]any) {
|
||||
lo.ForEach(s.getDatasourceList(cfg), func(item any, _ int) {
|
||||
if ds, ok := item.(map[string]any); ok {
|
||||
ds["isDefault"] = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// addDeleteEntry 向 deleteDatasources 添加条目
|
||||
func (s *App) addDeleteEntry(cfg map[string]any, name string) {
|
||||
delList, _ := cfg["deleteDatasources"].([]any)
|
||||
delList = append(delList, map[string]any{"name": name, "orgId": 1})
|
||||
cfg["deleteDatasources"] = delList
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package grafana
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Grafana 配置调整
|
||||
type ConfigTune struct {
|
||||
// [server]
|
||||
HTTPPort string `form:"http_port" json:"http_port"`
|
||||
Domain string `form:"domain" json:"domain"`
|
||||
RootURL string `form:"root_url" json:"root_url"`
|
||||
Protocol string `form:"protocol" json:"protocol"`
|
||||
// [database]
|
||||
DBType string `form:"db_type" json:"db_type"`
|
||||
DBHost string `form:"db_host" json:"db_host"`
|
||||
DBName string `form:"db_name" json:"db_name"`
|
||||
DBUser string `form:"db_user" json:"db_user"`
|
||||
DBPassword string `form:"db_password" json:"db_password"`
|
||||
// [security]
|
||||
AdminUser string `form:"admin_user" json:"admin_user"`
|
||||
AdminPassword string `form:"admin_password" json:"admin_password"`
|
||||
// [users]
|
||||
AllowSignUp string `form:"allow_sign_up" json:"allow_sign_up"`
|
||||
AutoAssignOrgRole string `form:"auto_assign_org_role" json:"auto_assign_org_role"`
|
||||
// [smtp]
|
||||
SMTPEnabled string `form:"smtp_enabled" json:"smtp_enabled"`
|
||||
SMTPHost string `form:"smtp_host" json:"smtp_host"`
|
||||
SMTPUser string `form:"smtp_user" json:"smtp_user"`
|
||||
SMTPPassword string `form:"smtp_password" json:"smtp_password"`
|
||||
SMTPFromAddress string `form:"smtp_from_address" json:"smtp_from_address"`
|
||||
// [log]
|
||||
LogMode string `form:"log_mode" json:"log_mode"`
|
||||
LogLevel string `form:"log_level" json:"log_level"`
|
||||
}
|
||||
|
||||
// DataSource 数据源
|
||||
type DataSource struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Type string `form:"type" json:"type" validate:"required|in:prometheus,mysql,postgres,influxdb,loki,elasticsearch,tempo,jaeger,zipkin,graphite,alertmanager,opentsdb,mssql,testdata,grafana-pyroscope-datasource,grafana-opensearch-datasource"`
|
||||
URL string `form:"url" json:"url" validate:"required"`
|
||||
Access string `form:"access" json:"access"`
|
||||
IsDefault bool `form:"is_default" json:"is_default"`
|
||||
// 数据库类型专用
|
||||
Database string `form:"database" json:"database"`
|
||||
User string `form:"user" json:"user"`
|
||||
Password string `form:"password" json:"password"`
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("kafka")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("kafka")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get kafka status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Node ID"), Value: s.getPropertiesValue(config, "node.id")},
|
||||
{Name: s.t.Get("Listeners"), Value: s.getPropertiesValue(config, "listeners")},
|
||||
{Name: s.t.Get("Log Dirs"), Value: s.getPropertiesValue(config, "log.dirs")},
|
||||
{Name: s.t.Get("Num Partitions"), Value: s.getPropertiesValue(config, "num.partitions")},
|
||||
{Name: s.t.Get("Log Retention Hours"), Value: s.getPropertiesValue(config, "log.retention.hours")},
|
||||
{Name: s.t.Get("Log Segment Bytes"), Value: s.getPropertiesValue(config, "log.segment.bytes")},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("kafka"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Kafka 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
heapRaw, _ := io.Read(s.heapEnvPath())
|
||||
heapInit, heapMax := s.parseHeapEnv(heapRaw)
|
||||
|
||||
tune := ConfigTune{
|
||||
NodeID: s.getPropertiesValue(config, "node.id"),
|
||||
Listeners: s.getPropertiesValue(config, "listeners"),
|
||||
LogDirs: s.getPropertiesValue(config, "log.dirs"),
|
||||
NumPartitions: s.getPropertiesValue(config, "num.partitions"),
|
||||
RetentionHours: s.getPropertiesValue(config, "log.retention.hours"),
|
||||
LogSegmentBytes: s.getPropertiesValue(config, "log.segment.bytes"),
|
||||
HeapInitSize: heapInit,
|
||||
HeapMaxSize: heapMax,
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Kafka 配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
config = s.setPropertiesValue(config, "node.id", req.NodeID)
|
||||
config = s.setPropertiesValue(config, "listeners", req.Listeners)
|
||||
config = s.setPropertiesValue(config, "log.dirs", req.LogDirs)
|
||||
config = s.setPropertiesValue(config, "num.partitions", req.NumPartitions)
|
||||
config = s.setPropertiesValue(config, "log.retention.hours", req.RetentionHours)
|
||||
config = s.setPropertiesValue(config, "log.segment.bytes", req.LogSegmentBytes)
|
||||
|
||||
if err = io.Write(s.configPath(), config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 JVM 堆内存
|
||||
if req.HeapInitSize != "" || req.HeapMaxSize != "" {
|
||||
heapRaw, _ := io.Read(s.heapEnvPath())
|
||||
heapRaw = s.setHeapEnv(heapRaw, req.HeapInitSize, req.HeapMaxSize)
|
||||
if err = io.Write(s.heapEnvPath(), heapRaw, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("kafka"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// configPath 返回配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/kafka/config/server.properties", app.Root)
|
||||
}
|
||||
|
||||
// heapEnvPath 返回 JVM 堆内存配置文件路径
|
||||
func (s *App) heapEnvPath() string {
|
||||
return fmt.Sprintf("%s/server/kafka/config/heap.env", app.Root)
|
||||
}
|
||||
|
||||
// getPropertiesValue 从 properties 内容中获取指定键的值
|
||||
func (s *App) getPropertiesValue(content string, key string) string {
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(trimmed, "=")
|
||||
if ok && strings.TrimSpace(k) == key {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setPropertiesValue 在 properties 内容中设置指定键的值
|
||||
func (s *App) setPropertiesValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
k, _, ok := strings.Cut(checkLine, "=")
|
||||
if ok && strings.TrimSpace(k) == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+trimmed)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+"="+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+"="+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
// parseHeapEnv 从 heap.env 中提取堆内存配置
|
||||
func (s *App) parseHeapEnv(content string) (initSize, maxSize string) {
|
||||
re := regexp.MustCompile(`KAFKA_HEAP_OPTS=(.+)`)
|
||||
m := re.FindStringSubmatch(content)
|
||||
if len(m) != 2 {
|
||||
return
|
||||
}
|
||||
opts := m[1]
|
||||
if mi := regexp.MustCompile(`-Xms(\S+)`).FindStringSubmatch(opts); len(mi) == 2 {
|
||||
initSize = mi[1]
|
||||
}
|
||||
if mx := regexp.MustCompile(`-Xmx(\S+)`).FindStringSubmatch(opts); len(mx) == 2 {
|
||||
maxSize = mx[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// setHeapEnv 设置 heap.env 中的堆内存配置
|
||||
func (s *App) setHeapEnv(content string, initSize, maxSize string) string {
|
||||
// 读取已有值作为默认
|
||||
oldInit, oldMax := s.parseHeapEnv(content)
|
||||
if initSize == "" {
|
||||
initSize = oldInit
|
||||
}
|
||||
if maxSize == "" {
|
||||
maxSize = oldMax
|
||||
}
|
||||
if initSize == "" {
|
||||
initSize = "1g"
|
||||
}
|
||||
if maxSize == "" {
|
||||
maxSize = "1g"
|
||||
}
|
||||
return fmt.Sprintf("KAFKA_HEAP_OPTS=-Xms%s -Xmx%s\n", initSize, maxSize)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package kafka
|
||||
|
||||
// UpdateConfig Kafka 配置更新
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Kafka 配置调整
|
||||
type ConfigTune struct {
|
||||
// Broker
|
||||
NodeID string `form:"node_id" json:"node_id"` // node.id
|
||||
Listeners string `form:"listeners" json:"listeners"` // listeners
|
||||
LogDirs string `form:"log_dirs" json:"log_dirs"` // log.dirs
|
||||
NumPartitions string `form:"num_partitions" json:"num_partitions"` // num.partitions
|
||||
RetentionHours string `form:"retention_hours" json:"retention_hours"` // log.retention.hours
|
||||
LogSegmentBytes string `form:"log_segment_bytes" json:"log_segment_bytes"` // log.segment.bytes
|
||||
// JVM
|
||||
HeapInitSize string `form:"heap_init_size" json:"heap_init_size"` // -Xms
|
||||
HeapMaxSize string `form:"heap_max_size" json:"heap_max_size"` // -Xmx
|
||||
}
|
||||
@@ -21,3 +21,7 @@ func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.Databa
|
||||
func (s *App) Route(r chi.Router) {
|
||||
s.mysql.Route(r)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
return s.mysql.Status()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("memcached")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("memcached")
|
||||
if err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -21,6 +22,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/env", s.UpdateEnv)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("minio")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetEnv(w http.ResponseWriter, r *http.Request) {
|
||||
env, _ := io.Read("/etc/default/minio")
|
||||
service.Success(w, env)
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
settingRepo biz.SettingRepo
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
r.Get("/admin_password", s.GetAdminPassword)
|
||||
r.Post("/admin_password", s.SetAdminPassword)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("mongod")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// Load 获取 MongoDB 运行状态
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, _ := systemctl.Status("mongod")
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
password, _ := s.settingRepo.Get(biz.SettingKeyMongoDBAdminPassword)
|
||||
raw, err := shell.Execf(`mongosh --quiet --eval "JSON.stringify(db.serverStatus())" -u admin -p '%s' --authenticationDatabase admin 2>/dev/null`, password)
|
||||
if err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var status2 struct {
|
||||
Uptime int `json:"uptime"`
|
||||
Connections struct {
|
||||
Current int `json:"current"`
|
||||
TotalCreated int `json:"totalCreated"`
|
||||
} `json:"connections"`
|
||||
OpCounters struct {
|
||||
Query int `json:"query"`
|
||||
Insert int `json:"insert"`
|
||||
Update int `json:"update"`
|
||||
Delete int `json:"delete"`
|
||||
} `json:"opcounters"`
|
||||
Mem struct {
|
||||
Resident int `json:"resident"`
|
||||
} `json:"mem"`
|
||||
StorageEngine struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"storageEngine"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err = json.Unmarshal([]byte(raw), &status2); err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Version"), Value: status2.Version},
|
||||
{Name: s.t.Get("Uptime (seconds)"), Value: cast.ToString(status2.Uptime)},
|
||||
{Name: s.t.Get("Current Connections"), Value: cast.ToString(status2.Connections.Current)},
|
||||
{Name: s.t.Get("Total Connections Created"), Value: cast.ToString(status2.Connections.TotalCreated)},
|
||||
{Name: s.t.Get("Query Operations"), Value: cast.ToString(status2.OpCounters.Query)},
|
||||
{Name: s.t.Get("Insert Operations"), Value: cast.ToString(status2.OpCounters.Insert)},
|
||||
{Name: s.t.Get("Update Operations"), Value: cast.ToString(status2.OpCounters.Update)},
|
||||
{Name: s.t.Get("Delete Operations"), Value: cast.ToString(status2.OpCounters.Delete)},
|
||||
{Name: s.t.Get("Resident Memory (MB)"), Value: cast.ToString(status2.Mem.Resident)},
|
||||
{Name: s.t.Get("Storage Engine"), Value: status2.StorageEngine.Name},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("mongod"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg == nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
DbPath: s.getYAMLValue(cfg, "storage.dbPath"),
|
||||
CacheSizeGB: s.getYAMLValue(cfg, "storage.wiredTiger.engineConfig.cacheSizeGB"),
|
||||
Port: s.getYAMLValue(cfg, "net.port"),
|
||||
BindIp: s.getYAMLValue(cfg, "net.bindIp"),
|
||||
SystemLogPath: s.getYAMLValue(cfg, "systemLog.path"),
|
||||
Authorization: s.getYAMLValue(cfg, "security.authorization"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
s.setNestedYAMLValue(cfg, "storage.dbPath", req.DbPath)
|
||||
s.setNestedYAMLValue(cfg, "storage.wiredTiger.engineConfig.cacheSizeGB", req.CacheSizeGB)
|
||||
s.setNestedYAMLValue(cfg, "net.port", req.Port)
|
||||
s.setNestedYAMLValue(cfg, "net.bindIp", req.BindIp)
|
||||
s.setNestedYAMLValue(cfg, "systemLog.path", req.SystemLogPath)
|
||||
s.setNestedYAMLValue(cfg, "security.authorization", req.Authorization)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Write(s.configPath(), string(data), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("mongod"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetAdminPassword 获取 admin 密码
|
||||
func (s *App) GetAdminPassword(w http.ResponseWriter, r *http.Request) {
|
||||
password, err := s.settingRepo.Get(biz.SettingKeyMongoDBAdminPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get MongoDB admin password: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, password)
|
||||
}
|
||||
|
||||
// SetAdminPassword 设置 admin 密码
|
||||
func (s *App) SetAdminPassword(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[SetAdminPassword](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
oldPassword, _ := s.settingRepo.Get(biz.SettingKeyMongoDBAdminPassword)
|
||||
|
||||
// 尝试用旧密码连接修改
|
||||
_, err = shell.Execf(`mongosh --quiet -u admin -p '%s' --authenticationDatabase admin --eval "db.changeUserPassword('admin', '%s')"`, oldPassword, req.Password)
|
||||
if err != nil {
|
||||
// 回退:停止服务,无认证模式修改
|
||||
_ = systemctl.Stop("mongod")
|
||||
_, _ = shell.Execf(`su -s /bin/bash mongod -c "mongod --config %s --noauth --fork --logpath /tmp/mongod_reset.log"`, s.configPath())
|
||||
_, resetErr := shell.Execf(`mongosh --quiet --eval "db.getSiblingDB('admin').changeUserPassword('admin', '%s')"`, req.Password)
|
||||
_, _ = shell.Execf(`su -s /bin/bash mongod -c "mongod --config %s --shutdown" 2>/dev/null; pkill -f 'mongod --config.*--noauth' 2>/dev/null`, s.configPath())
|
||||
_ = systemctl.Start("mongod")
|
||||
if resetErr != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to set MongoDB admin password: %v", resetErr))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.settingRepo.Set(biz.SettingKeyMongoDBAdminPassword, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to save MongoDB admin password: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.databaseServerRepo.UpdatePassword("local_mongodb", req.Password)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// configPath 返回配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/mongodb/mongod.conf", app.Root)
|
||||
}
|
||||
|
||||
// getYAMLValue 获取嵌套 YAML 值,支持 dot notation
|
||||
func (s *App) getYAMLValue(cfg map[string]any, key string) string {
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
val, ok := cfg[parts[0]]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
nested, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s.getYAMLValue(nested, parts[1])
|
||||
}
|
||||
|
||||
// setNestedYAMLValue 设置嵌套 YAML 值,逐层创建 map
|
||||
func (s *App) setNestedYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
if len(parts) == 1 {
|
||||
cfg[parts[0]] = value
|
||||
return
|
||||
}
|
||||
nested, ok := cfg[parts[0]].(map[string]any)
|
||||
if !ok {
|
||||
nested = make(map[string]any)
|
||||
cfg[parts[0]] = nested
|
||||
}
|
||||
s.setNestedYAMLValue(nested, parts[1], value)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package mongodb
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// SetAdminPassword 设置 admin 密码
|
||||
type SetAdminPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
}
|
||||
|
||||
// ConfigTune MongoDB 配置调整
|
||||
type ConfigTune struct {
|
||||
// 存储
|
||||
DbPath string `form:"db_path" json:"db_path"`
|
||||
CacheSizeGB string `form:"cache_size_gb" json:"cache_size_gb"`
|
||||
// 网络
|
||||
Port string `form:"port" json:"port"`
|
||||
BindIp string `form:"bind_ip" json:"bind_ip"`
|
||||
// 日志
|
||||
SystemLogPath string `form:"system_log_path" json:"system_log_path"`
|
||||
// 安全
|
||||
Authorization string `form:"authorization" json:"authorization"`
|
||||
}
|
||||
@@ -40,15 +40,18 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Post("/clear_log", s.ClearLog)
|
||||
r.Get("/slow_log", s.SlowLog)
|
||||
r.Post("/clear_slow_log", s.ClearSlowLog)
|
||||
r.Get("/root_password", s.GetRootPassword)
|
||||
r.Post("/root_password", s.SetRootPassword)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("mysqld")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(app.Root + "/server/mysql/conf/my.cnf")
|
||||
@@ -194,31 +197,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, load)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (s *App) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
if err := systemctl.LogClear("mysqld"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// SlowLog 获取慢查询日志
|
||||
func (s *App) SlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, fmt.Sprintf("%s/server/mysql/mysql-slow.log", app.Root))
|
||||
}
|
||||
|
||||
// ClearSlowLog 清空慢查询日志
|
||||
func (s *App) ClearSlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf("cat /dev/null > %s/server/mysql/mysql-slow.log", app.Root); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetRootPassword 获取root密码
|
||||
func (s *App) GetRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
rootPassword, err := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
|
||||
@@ -50,6 +50,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Delete("/stream/upstreams/{name}", s.DeleteStreamUpstream)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("nginx")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root))
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
@@ -563,10 +564,7 @@ func (s *App) saveStreamUpstreamConfig(filePath string, upstream *StreamUpstream
|
||||
}
|
||||
|
||||
// 服务器列表
|
||||
var addrs []string
|
||||
for addr := range upstream.Servers {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
addrs := lo.Keys(upstream.Servers)
|
||||
sort.Strings(addrs)
|
||||
|
||||
for _, addr := range addrs {
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -20,3 +22,8 @@ func NewApp(t *gotext.Locale) *App {
|
||||
func (s *App) Route(r chi.Router) {
|
||||
s.nginx.Route(r)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("openresty")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("opensearch")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("opensearch")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get opensearch status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
port := s.getPort()
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/_cluster/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var health struct {
|
||||
ClusterName string `json:"cluster_name"`
|
||||
Status string `json:"status"`
|
||||
NumberOfNodes int `json:"number_of_nodes"`
|
||||
NumberOfDataNodes int `json:"number_of_data_nodes"`
|
||||
ActiveShards int `json:"active_shards"`
|
||||
ActivePrimaryShards int `json:"active_primary_shards"`
|
||||
RelocatingShards int `json:"relocating_shards"`
|
||||
UnassignedShards int `json:"unassigned_shards"`
|
||||
}
|
||||
if err = json.Unmarshal(resp.Bytes(), &health); err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Cluster Name"), Value: health.ClusterName},
|
||||
{Name: s.t.Get("Cluster Status"), Value: health.Status},
|
||||
{Name: s.t.Get("Number of Nodes"), Value: cast.ToString(health.NumberOfNodes)},
|
||||
{Name: s.t.Get("Number of Data Nodes"), Value: cast.ToString(health.NumberOfDataNodes)},
|
||||
{Name: s.t.Get("Active Shards"), Value: cast.ToString(health.ActiveShards)},
|
||||
{Name: s.t.Get("Active Primary Shards"), Value: cast.ToString(health.ActivePrimaryShards)},
|
||||
{Name: s.t.Get("Relocating Shards"), Value: cast.ToString(health.RelocatingShards)},
|
||||
{Name: s.t.Get("Unassigned Shards"), Value: cast.ToString(health.UnassignedShards)},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("opensearch"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 OpenSearch 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg == nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
jvmRaw, _ := io.Read(s.jvmOptionsPath())
|
||||
heapInit, heapMax := s.parseJVMHeap(jvmRaw)
|
||||
|
||||
tune := ConfigTune{
|
||||
ClusterName: s.getYAMLValue(cfg, "cluster.name"),
|
||||
NodeName: s.getYAMLValue(cfg, "node.name"),
|
||||
NetworkHost: s.getYAMLValue(cfg, "network.host"),
|
||||
HTTPPort: s.getYAMLValue(cfg, "http.port"),
|
||||
DiscoveryType: s.getYAMLValue(cfg, "discovery.type"),
|
||||
PathData: s.getYAMLValue(cfg, "path.data"),
|
||||
PathLogs: s.getYAMLValue(cfg, "path.logs"),
|
||||
HeapInitSize: heapInit,
|
||||
HeapMaxSize: heapMax,
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 OpenSearch 配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
s.setYAMLValue(cfg, "cluster.name", req.ClusterName)
|
||||
s.setYAMLValue(cfg, "node.name", req.NodeName)
|
||||
s.setYAMLValue(cfg, "network.host", req.NetworkHost)
|
||||
s.setYAMLValue(cfg, "http.port", req.HTTPPort)
|
||||
s.setYAMLValue(cfg, "discovery.type", req.DiscoveryType)
|
||||
s.setYAMLValue(cfg, "path.data", req.PathData)
|
||||
s.setYAMLValue(cfg, "path.logs", req.PathLogs)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Write(s.configPath(), string(data), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.HeapInitSize != "" || req.HeapMaxSize != "" {
|
||||
jvmRaw, _ := io.Read(s.jvmOptionsPath())
|
||||
jvmRaw = s.setJVMHeap(jvmRaw, req.HeapInitSize, req.HeapMaxSize)
|
||||
if err = io.Write(s.jvmOptionsPath(), jvmRaw, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("opensearch"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/opensearch/config/opensearch.yml", app.Root)
|
||||
}
|
||||
|
||||
func (s *App) jvmOptionsPath() string {
|
||||
return fmt.Sprintf("%s/server/opensearch/config/jvm.options", app.Root)
|
||||
}
|
||||
|
||||
func (s *App) getPort() string {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg != nil {
|
||||
if v := s.getYAMLValue(cfg, "http.port"); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "9200"
|
||||
}
|
||||
|
||||
// getYAMLValue 获取 YAML 值,优先匹配平铺键(如 "path.data"),回退到嵌套键(如 path -> data)
|
||||
func (s *App) getYAMLValue(cfg map[string]any, key string) string {
|
||||
// 优先匹配平铺键(安装脚本用 sed 生成的格式)
|
||||
if val, ok := cfg[key]; ok {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
// 回退到嵌套键
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
val, ok := cfg[parts[0]]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
nested, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s.getYAMLValue(nested, parts[1])
|
||||
}
|
||||
|
||||
// setYAMLValue 设置 YAML 值
|
||||
func (s *App) setYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
// 使用平铺键,同时清理可能存在的嵌套键
|
||||
cfg[key] = value
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
if len(parts) == 2 {
|
||||
if nested, ok := cfg[parts[0]].(map[string]any); ok {
|
||||
delete(nested, parts[1])
|
||||
if len(nested) == 0 {
|
||||
delete(cfg, parts[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) parseJVMHeap(content string) (initSize, maxSize string) {
|
||||
reInit := regexp.MustCompile(`(?m)^-Xms(\S+)`)
|
||||
reMax := regexp.MustCompile(`(?m)^-Xmx(\S+)`)
|
||||
if m := reInit.FindStringSubmatch(content); len(m) == 2 {
|
||||
initSize = m[1]
|
||||
}
|
||||
if m := reMax.FindStringSubmatch(content); len(m) == 2 {
|
||||
maxSize = m[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *App) setJVMHeap(content string, initSize, maxSize string) string {
|
||||
if initSize != "" {
|
||||
re := regexp.MustCompile(`(?m)^-Xms\S+`)
|
||||
if re.MatchString(content) {
|
||||
content = re.ReplaceAllString(content, "-Xms"+initSize)
|
||||
} else {
|
||||
content += "\n-Xms" + initSize
|
||||
}
|
||||
}
|
||||
if maxSize != "" {
|
||||
re := regexp.MustCompile(`(?m)^-Xmx\S+`)
|
||||
if re.MatchString(content) {
|
||||
content = re.ReplaceAllString(content, "-Xmx"+maxSize)
|
||||
} else {
|
||||
content += "\n-Xmx" + maxSize
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package opensearch
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune OpenSearch 配置调整
|
||||
type ConfigTune struct {
|
||||
// 集群
|
||||
ClusterName string `form:"cluster_name" json:"cluster_name"`
|
||||
NodeName string `form:"node_name" json:"node_name"`
|
||||
NetworkHost string `form:"network_host" json:"network_host"`
|
||||
HTTPPort string `form:"http_port" json:"http_port"`
|
||||
DiscoveryType string `form:"discovery_type" json:"discovery_type"`
|
||||
// 路径
|
||||
PathData string `form:"path_data" json:"path_data"`
|
||||
PathLogs string `form:"path_logs" json:"path_logs"`
|
||||
// JVM
|
||||
HeapInitSize string `form:"heap_init_size" json:"heap_init_size"`
|
||||
HeapMaxSize string `form:"heap_max_size" json:"heap_max_size"`
|
||||
}
|
||||
@@ -21,3 +21,7 @@ func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.Databa
|
||||
func (s *App) Route(r chi.Router) {
|
||||
s.mysql.Route(r)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
return s.mysql.Status()
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -37,6 +38,10 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
return types.AppStatusNA
|
||||
}
|
||||
|
||||
func (s *App) Info(w http.ResponseWriter, r *http.Request) {
|
||||
files, err := os.ReadDir(fmt.Sprintf("%s/server/phpmyadmin", app.Root))
|
||||
if err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -23,6 +24,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/storage_config", s.UpdateStorageConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("podman")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetRegistryConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/containers/registries.conf")
|
||||
if err != nil {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
@@ -41,13 +41,17 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/user_config", s.UpdateUserConfig)
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/log", s.Log)
|
||||
r.Post("/clear_log", s.ClearLog)
|
||||
r.Get("/postgres_password", s.GetPostgresPassword)
|
||||
r.Post("/postgres_password", s.SetPostgresPassword)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("postgresql")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取配置
|
||||
@@ -175,19 +179,15 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
// Log 获取日志
|
||||
// Log 获取应用日志路径列表
|
||||
func (s *App) Log(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, fmt.Sprintf("%s/server/postgresql/logs/postgresql-%s.log", app.Root, time.Now().Format(time.DateOnly)))
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (s *App) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf("rm -rf %s/server/postgresql/logs/postgresql-*.log", app.Root); err != nil {
|
||||
paths, err := filepath.Glob(fmt.Sprintf("%s/server/postgresql/logs/postgresql-*.log", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
service.Success(w, paths)
|
||||
}
|
||||
|
||||
// GetPostgresPassword 获取 postgres 用户密码
|
||||
@@ -397,13 +397,13 @@ func (s *App) setPGValue(content string, key string, value string) string {
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+" = "+value)
|
||||
result = append(result, key+" = '"+value+"'")
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" = "+value)
|
||||
result = append(result, key+" = '"+value+"'")
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
conf *config.Config
|
||||
taskRepo biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, conf *config.Config, taskRepo biz.TaskRepo) *App {
|
||||
return &App{t: t, conf: conf, taskRepo: taskRepo}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
// Alertmanager 配置
|
||||
r.Get("/alertmanager_config", s.GetAlertmanagerConfig)
|
||||
r.Post("/alertmanager_config", s.UpdateAlertmanagerConfig)
|
||||
// Exporters 管理
|
||||
r.Get("/exporters", s.ExporterList)
|
||||
r.Post("/exporters", s.InstallExporter)
|
||||
r.Delete("/exporters", s.UninstallExporter)
|
||||
r.Post("/exporters/{slug}/start", s.StartExporter)
|
||||
r.Post("/exporters/{slug}/stop", s.StopExporter)
|
||||
r.Post("/exporters/{slug}/restart", s.RestartExporter)
|
||||
r.Get("/exporters/{slug}/config", s.GetExporterConfig)
|
||||
r.Post("/exporters/{slug}/config", s.UpdateExporterConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
prom, _ := systemctl.Status("prometheus")
|
||||
alert, _ := systemctl.Status("alertmanager")
|
||||
return types.AggregateAppStatus(prom, alert)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("prometheus")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get prometheus status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get("http://127.0.0.1:9090/api/v1/status/runtimeinfo")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Data struct {
|
||||
StartTime string `json:"startTime"`
|
||||
GoroutineCount int `json:"goroutineCount"`
|
||||
GOMAXPROCS int `json:"GOMAXPROCS"`
|
||||
StorageRetention string `json:"storageRetention"`
|
||||
ReloadConfigSuccess bool `json:"reloadConfigSuccess"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err = json.Unmarshal(resp.Bytes(), &result); err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Start Time"), Value: result.Data.StartTime},
|
||||
{Name: s.t.Get("Storage Retention"), Value: result.Data.StorageRetention},
|
||||
{Name: s.t.Get("Goroutine Count"), Value: cast.ToString(result.Data.GoroutineCount)},
|
||||
{Name: "GOMAXPROCS", Value: cast.ToString(result.Data.GOMAXPROCS)},
|
||||
{Name: s.t.Get("Config Reload Success"), Value: cast.ToString(result.Data.ReloadConfigSuccess)},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root))
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("prometheus"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Prometheus 全局配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root))
|
||||
|
||||
var cfg struct {
|
||||
Global struct {
|
||||
ScrapeInterval string `yaml:"scrape_interval"`
|
||||
EvaluationInterval string `yaml:"evaluation_interval"`
|
||||
ScrapeTimeout string `yaml:"scrape_timeout"`
|
||||
} `yaml:"global"`
|
||||
}
|
||||
_ = yaml.Unmarshal([]byte(conf), &cfg)
|
||||
|
||||
tune := ConfigTune{
|
||||
ScrapeInterval: cfg.Global.ScrapeInterval,
|
||||
EvaluationInterval: cfg.Global.EvaluationInterval,
|
||||
ScrapeTimeout: cfg.Global.ScrapeTimeout,
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Prometheus 全局配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root)
|
||||
raw, _ := io.Read(confPath)
|
||||
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
global, ok := cfg["global"].(map[string]any)
|
||||
if !ok {
|
||||
global = make(map[string]any)
|
||||
}
|
||||
|
||||
if req.ScrapeInterval != "" {
|
||||
global["scrape_interval"] = req.ScrapeInterval
|
||||
}
|
||||
if req.EvaluationInterval != "" {
|
||||
global["evaluation_interval"] = req.EvaluationInterval
|
||||
}
|
||||
if req.ScrapeTimeout != "" {
|
||||
global["scrape_timeout"] = req.ScrapeTimeout
|
||||
}
|
||||
cfg["global"] = global
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(confPath, string(data), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("prometheus"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetAlertmanagerConfig 获取 Alertmanager 配置
|
||||
func (s *App) GetAlertmanagerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(fmt.Sprintf("%s/server/prometheus/alertmanager/alertmanager.yml", app.Root))
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
// UpdateAlertmanagerConfig 更新 Alertmanager 配置
|
||||
func (s *App) UpdateAlertmanagerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/prometheus/alertmanager/alertmanager.yml", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("alertmanager"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ExporterList 获取 Exporter 列表及状态
|
||||
func (s *App) ExporterList(w http.ResponseWriter, r *http.Request) {
|
||||
exporters := s.getExporters()
|
||||
for i := range exporters {
|
||||
exporters[i].Installed = io.Exists(fmt.Sprintf("%s/server/prometheus/exporters/%s", app.Root, exporters[i].Slug))
|
||||
if exporters[i].Installed {
|
||||
running, _ := systemctl.Status("prometheus-" + exporters[i].Slug)
|
||||
exporters[i].Running = running
|
||||
}
|
||||
}
|
||||
|
||||
service.Success(w, exporters)
|
||||
}
|
||||
|
||||
// InstallExporter 安装 Exporter(异步任务)
|
||||
func (s *App) InstallExporter(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ExporterSlug](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.checkExporter(req.Slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", req.Slug))
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/prometheus/exporters/exporter.sh' | bash -s -- 'install' '%s'`, s.conf.App.DownloadEndpoint, url.PathEscape(req.Slug))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Name = s.t.Get("Install Prometheus exporter %s", req.Slug)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UninstallExporter 卸载 Exporter(异步任务)
|
||||
func (s *App) UninstallExporter(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ExporterSlug](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.checkExporter(req.Slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", req.Slug))
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/prometheus/exporters/exporter.sh' | bash -s -- 'uninstall' '%s'`, s.conf.App.DownloadEndpoint, url.PathEscape(req.Slug))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Name = s.t.Get("Uninstall Prometheus exporter %s", req.Slug)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// StartExporter 启动 Exporter
|
||||
func (s *App) StartExporter(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Start("prometheus-" + slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// StopExporter 停止 Exporter
|
||||
func (s *App) StopExporter(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Stop("prometheus-" + slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// RestartExporter 重启 Exporter
|
||||
func (s *App) RestartExporter(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Restart("prometheus-" + slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetExporterConfig 获取 Exporter 配置
|
||||
func (s *App) GetExporterConfig(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
confPath := s.getExporterConfigPath(slug)
|
||||
if confPath == "" {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s has no configuration file", slug))
|
||||
return
|
||||
}
|
||||
|
||||
conf, _ := io.Read(confPath)
|
||||
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
// UpdateExporterConfig 更新 Exporter 配置
|
||||
func (s *App) UpdateExporterConfig(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
confPath := s.getExporterConfigPath(slug)
|
||||
if confPath == "" {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s has no configuration file", slug))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := service.Bind[ExporterConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(confPath, req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("prometheus-" + slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getExporters 返回所有 exporter 定义
|
||||
func (s *App) getExporters() []Exporter {
|
||||
return []Exporter{
|
||||
{Name: "Node Exporter", Slug: "node_exporter", Description: s.t.Get("Hardware and OS metrics")},
|
||||
{Name: "Nginx Exporter", Slug: "nginx_exporter", Description: s.t.Get("Nginx metrics")},
|
||||
{Name: "MySQL Exporter", Slug: "mysqld_exporter", Description: s.t.Get("MySQL database metrics"), HasConfig: true},
|
||||
{Name: "PostgreSQL Exporter", Slug: "postgres_exporter", Description: s.t.Get("PostgreSQL database metrics"), HasConfig: true},
|
||||
{Name: "MongoDB Exporter", Slug: "mongodb_exporter", Description: s.t.Get("MongoDB metrics"), HasConfig: true},
|
||||
{Name: "Elasticsearch Exporter", Slug: "elasticsearch_exporter", Description: s.t.Get("Elasticsearch metrics"), HasConfig: true},
|
||||
{Name: "Redis Exporter", Slug: "redis_exporter", Description: s.t.Get("Redis metrics"), HasConfig: true},
|
||||
{Name: "Memcached Exporter", Slug: "memcached_exporter", Description: s.t.Get("Memcached metrics")},
|
||||
{Name: "Kafka Exporter", Slug: "kafka_exporter", Description: s.t.Get("Kafka metrics"), HasConfig: true},
|
||||
}
|
||||
}
|
||||
|
||||
// getExporterConfigPath 获取 exporter 配置文件路径
|
||||
func (s *App) getExporterConfigPath(slug string) string {
|
||||
base := fmt.Sprintf("%s/server/prometheus/exporters/%s", app.Root, slug)
|
||||
switch slug {
|
||||
case "redis_exporter", "postgres_exporter", "elasticsearch_exporter", "mongodb_exporter", "kafka_exporter":
|
||||
return base + "/env"
|
||||
case "mysqld_exporter":
|
||||
return base + "/.my.cnf"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// checkExporter 检查 slug 是否有效
|
||||
func (s *App) checkExporter(slug string) bool {
|
||||
return lo.ContainsBy(s.getExporters(), func(e Exporter) bool {
|
||||
return e.Slug == slug
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package prometheus
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Prometheus 全局配置调整
|
||||
type ConfigTune struct {
|
||||
ScrapeInterval string `form:"scrape_interval" json:"scrape_interval"`
|
||||
EvaluationInterval string `form:"evaluation_interval" json:"evaluation_interval"`
|
||||
ScrapeTimeout string `form:"scrape_timeout" json:"scrape_timeout"`
|
||||
}
|
||||
|
||||
// Exporter Prometheus Exporter 信息
|
||||
type Exporter struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
Running bool `json:"running"`
|
||||
HasConfig bool `json:"has_config"`
|
||||
}
|
||||
|
||||
// ExporterSlug Exporter 操作请求
|
||||
type ExporterSlug struct {
|
||||
Slug string `form:"slug" json:"slug" validate:"required"`
|
||||
}
|
||||
|
||||
// ExporterConfig Exporter 配置更新请求
|
||||
type ExporterConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -40,6 +42,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("pure-ftpd")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
listRaw, err := shell.Execf("pure-pw list")
|
||||
@@ -50,19 +57,17 @@ func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
listArr := strings.Split(listRaw, "\n")
|
||||
var users []User
|
||||
for _, v := range listArr {
|
||||
userRe := regexp.MustCompile(`(\S+)\s+(\S+)`)
|
||||
users := lo.FilterMap(strings.Split(listRaw, "\n"), func(v string, _ int) (User, bool) {
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
return User{}, false
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`(\S+)\s+(\S+)`).FindStringSubmatch(v)
|
||||
users = append(users, User{
|
||||
match := userRe.FindStringSubmatch(v)
|
||||
return User{
|
||||
Username: match[1],
|
||||
Path: strings.Replace(match[2], "/./", "/", 1),
|
||||
})
|
||||
}
|
||||
}, true
|
||||
})
|
||||
|
||||
paged, total := service.Paginate(r, users)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
@@ -38,6 +39,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("redis")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("redis")
|
||||
if err != nil {
|
||||
@@ -68,15 +74,13 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
infoLines := strings.Split(raw, "\n")
|
||||
dataRaw := make(map[string]string)
|
||||
|
||||
for _, item := range infoLines {
|
||||
dataRaw := lo.SliceToMap(strings.Split(raw, "\n"), func(item string) (string, string) {
|
||||
parts := strings.Split(item, ":")
|
||||
if len(parts) == 2 {
|
||||
dataRaw[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
if len(parts) != 2 {
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
||||
})
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("TCP Port"), Value: dataRaw["tcp_port"]},
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
package rocketmq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
namesrv, _ := systemctl.Status("rocketmq-namesrv")
|
||||
broker, _ := systemctl.Status("rocketmq-broker")
|
||||
return types.AggregateAppStatus(namesrv, broker)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
namesrvStatus, _ := systemctl.Status("rocketmq-namesrv")
|
||||
brokerStatus, _ := systemctl.Status("rocketmq-broker")
|
||||
|
||||
namesrvStr := "stopped"
|
||||
if namesrvStatus {
|
||||
namesrvStr = "running"
|
||||
}
|
||||
brokerStr := "stopped"
|
||||
if brokerStatus {
|
||||
brokerStr = "running"
|
||||
}
|
||||
|
||||
if !namesrvStatus && !brokerStatus {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("NameServer Status"), Value: namesrvStr},
|
||||
{Name: s.t.Get("Broker Status"), Value: brokerStr},
|
||||
{Name: s.t.Get("Broker Name"), Value: s.getPropertiesValue(config, "brokerName")},
|
||||
{Name: s.t.Get("Listen Port"), Value: s.getPropertiesValue(config, "listenPort")},
|
||||
{Name: s.t.Get("NameServer Address"), Value: s.getPropertiesValue(config, "namesrvAddr")},
|
||||
{Name: s.t.Get("Broker Role"), Value: s.getPropertiesValue(config, "brokerRole")},
|
||||
{Name: s.t.Get("Flush Disk Type"), Value: s.getPropertiesValue(config, "flushDiskType")},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.restartServices(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 RocketMQ 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
heapRaw, _ := io.Read(s.heapEnvPath())
|
||||
|
||||
namesrvInit, namesrvMax := s.parseHeapLine(heapRaw, "ROCKETMQ_NAMESRV_HEAP")
|
||||
brokerInit, brokerMax := s.parseHeapLine(heapRaw, "ROCKETMQ_BROKER_HEAP")
|
||||
|
||||
tune := ConfigTune{
|
||||
BrokerName: s.getPropertiesValue(config, "brokerName"),
|
||||
ListenPort: s.getPropertiesValue(config, "listenPort"),
|
||||
NamesrvAddr: s.getPropertiesValue(config, "namesrvAddr"),
|
||||
BrokerRole: s.getPropertiesValue(config, "brokerRole"),
|
||||
FlushDiskType: s.getPropertiesValue(config, "flushDiskType"),
|
||||
StorePathRootDir: s.getPropertiesValue(config, "storePathRootDir"),
|
||||
StorePathCommitLog: s.getPropertiesValue(config, "storePathCommitLog"),
|
||||
MaxMessageSize: s.getPropertiesValue(config, "maxMessageSize"),
|
||||
NamesrvHeapInitSize: namesrvInit,
|
||||
NamesrvHeapMaxSize: namesrvMax,
|
||||
BrokerHeapInitSize: brokerInit,
|
||||
BrokerHeapMaxSize: brokerMax,
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 RocketMQ 配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
config = s.setPropertiesValue(config, "brokerName", req.BrokerName)
|
||||
config = s.setPropertiesValue(config, "listenPort", req.ListenPort)
|
||||
config = s.setPropertiesValue(config, "namesrvAddr", req.NamesrvAddr)
|
||||
config = s.setPropertiesValue(config, "brokerRole", req.BrokerRole)
|
||||
config = s.setPropertiesValue(config, "flushDiskType", req.FlushDiskType)
|
||||
config = s.setPropertiesValue(config, "storePathRootDir", req.StorePathRootDir)
|
||||
config = s.setPropertiesValue(config, "storePathCommitLog", req.StorePathCommitLog)
|
||||
config = s.setPropertiesValue(config, "maxMessageSize", req.MaxMessageSize)
|
||||
|
||||
if err = io.Write(s.configPath(), config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 JVM 堆内存
|
||||
if req.NamesrvHeapInitSize != "" || req.NamesrvHeapMaxSize != "" || req.BrokerHeapInitSize != "" || req.BrokerHeapMaxSize != "" {
|
||||
heapRaw, _ := io.Read(s.heapEnvPath())
|
||||
heapRaw = s.setHeapEnv(heapRaw, *req)
|
||||
if err = io.Write(s.heapEnvPath(), heapRaw, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.restartServices(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// restartServices 重启 NameServer 和 Broker 服务
|
||||
func (s *App) restartServices() error {
|
||||
if err := systemctl.Restart("rocketmq-namesrv"); err != nil {
|
||||
return err
|
||||
}
|
||||
return systemctl.Restart("rocketmq-broker")
|
||||
}
|
||||
|
||||
// configPath 返回 broker 配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/rocketmq/conf/broker.conf", app.Root)
|
||||
}
|
||||
|
||||
// heapEnvPath 返回 JVM 堆内存配置文件路径
|
||||
func (s *App) heapEnvPath() string {
|
||||
return fmt.Sprintf("%s/server/rocketmq/conf/heap.env", app.Root)
|
||||
}
|
||||
|
||||
// getPropertiesValue 从 properties 内容中获取指定键的值
|
||||
func (s *App) getPropertiesValue(content string, key string) string {
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(trimmed, "=")
|
||||
if ok && strings.TrimSpace(k) == key {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setPropertiesValue 在 properties 内容中设置指定键的值
|
||||
func (s *App) setPropertiesValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
k, _, ok := strings.Cut(checkLine, "=")
|
||||
if ok && strings.TrimSpace(k) == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+trimmed)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+"="+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+"="+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
// parseHeapLine 从 heap.env 中提取指定环境变量的堆内存配置
|
||||
func (s *App) parseHeapLine(content string, envKey string) (initSize, maxSize string) {
|
||||
re := regexp.MustCompile(envKey + `=(.+)`)
|
||||
m := re.FindStringSubmatch(content)
|
||||
if len(m) != 2 {
|
||||
return
|
||||
}
|
||||
opts := m[1]
|
||||
if mi := regexp.MustCompile(`-Xms(\S+)`).FindStringSubmatch(opts); len(mi) == 2 {
|
||||
initSize = mi[1]
|
||||
}
|
||||
if mx := regexp.MustCompile(`-Xmx(\S+)`).FindStringSubmatch(opts); len(mx) == 2 {
|
||||
maxSize = mx[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// setHeapEnv 设置 heap.env 中的堆内存配置
|
||||
func (s *App) setHeapEnv(content string, req ConfigTune) string {
|
||||
// 读取已有值作为默认
|
||||
oldNsInit, oldNsMax := s.parseHeapLine(content, "ROCKETMQ_NAMESRV_HEAP")
|
||||
oldBrInit, oldBrMax := s.parseHeapLine(content, "ROCKETMQ_BROKER_HEAP")
|
||||
|
||||
nsInit := req.NamesrvHeapInitSize
|
||||
if nsInit == "" {
|
||||
nsInit = oldNsInit
|
||||
}
|
||||
if nsInit == "" {
|
||||
nsInit = "512m"
|
||||
}
|
||||
nsMax := req.NamesrvHeapMaxSize
|
||||
if nsMax == "" {
|
||||
nsMax = oldNsMax
|
||||
}
|
||||
if nsMax == "" {
|
||||
nsMax = "512m"
|
||||
}
|
||||
|
||||
brInit := req.BrokerHeapInitSize
|
||||
if brInit == "" {
|
||||
brInit = oldBrInit
|
||||
}
|
||||
if brInit == "" {
|
||||
brInit = "1g"
|
||||
}
|
||||
brMax := req.BrokerHeapMaxSize
|
||||
if brMax == "" {
|
||||
brMax = oldBrMax
|
||||
}
|
||||
if brMax == "" {
|
||||
brMax = "1g"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("ROCKETMQ_NAMESRV_HEAP=-Xms%s -Xmx%s\nROCKETMQ_BROKER_HEAP=-Xms%s -Xmx%s\n", nsInit, nsMax, brInit, brMax)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package rocketmq
|
||||
|
||||
// UpdateConfig RocketMQ 配置更新
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune RocketMQ 配置调整
|
||||
type ConfigTune struct {
|
||||
// Broker 基础
|
||||
BrokerName string `form:"broker_name" json:"broker_name"` // brokerName
|
||||
ListenPort string `form:"listen_port" json:"listen_port"` // listenPort
|
||||
NamesrvAddr string `form:"namesrv_addr" json:"namesrv_addr"` // namesrvAddr
|
||||
BrokerRole string `form:"broker_role" json:"broker_role"` // brokerRole
|
||||
FlushDiskType string `form:"flush_disk_type" json:"flush_disk_type"` // flushDiskType
|
||||
// 存储
|
||||
StorePathRootDir string `form:"store_path_root_dir" json:"store_path_root_dir"` // storePathRootDir
|
||||
StorePathCommitLog string `form:"store_path_commit_log" json:"store_path_commit_log"` // storePathCommitLog
|
||||
MaxMessageSize string `form:"max_message_size" json:"max_message_size"` // maxMessageSize
|
||||
// JVM - NameServer
|
||||
NamesrvHeapInitSize string `form:"namesrv_heap_init_size" json:"namesrv_heap_init_size"` // -Xms (namesrv)
|
||||
NamesrvHeapMaxSize string `form:"namesrv_heap_max_size" json:"namesrv_heap_max_size"` // -Xmx (namesrv)
|
||||
// JVM - Broker
|
||||
BrokerHeapInitSize string `form:"broker_heap_init_size" json:"broker_heap_init_size"` // -Xms (broker)
|
||||
BrokerHeapMaxSize string `form:"broker_heap_max_size" json:"broker_heap_max_size"` // -Xmx (broker)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -36,6 +37,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("rsyncd")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
|
||||
+11
-13
@@ -10,11 +10,13 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -33,6 +35,10 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Delete("/mounts", s.Delete)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
return types.AppStatusNA
|
||||
}
|
||||
|
||||
// List 所有 S3fs 挂载
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.mounts()
|
||||
@@ -81,11 +87,9 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range list {
|
||||
if item.Path == req.Path {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("mount path already exists"))
|
||||
return
|
||||
}
|
||||
if lo.ContainsBy(list, func(item Mount) bool { return item.Path == req.Path }) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("mount path already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
id := time.Now().UnixMicro()
|
||||
@@ -128,14 +132,8 @@ func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var mount Mount
|
||||
for _, item := range list {
|
||||
if item.ID == req.ID {
|
||||
mount = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if mount.ID == 0 {
|
||||
mount, ok := lo.Find(list, func(item Mount) bool { return item.ID == req.ID })
|
||||
if !ok {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("mount not found"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/os"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -37,7 +38,6 @@ func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/service", s.Service)
|
||||
r.Post("/clear_log", s.ClearLog)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/processes", s.Processes)
|
||||
@@ -45,7 +45,6 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/processes/{process}/stop", s.StopProcess)
|
||||
r.Post("/processes/{process}/restart", s.RestartProcess)
|
||||
r.Get("/processes/{process}/log", s.ProcessLog)
|
||||
r.Post("/processes/{process}/clear_log", s.ClearProcessLog)
|
||||
r.Get("/processes/{process}", s.ProcessConfig)
|
||||
r.Post("/processes/{process}", s.UpdateProcessConfig)
|
||||
r.Delete("/processes/{process}", s.DeleteProcess)
|
||||
@@ -57,14 +56,9 @@ func (s *App) Service(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, s.name)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (s *App) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf(`cat /dev/null > /var/log/supervisor/supervisord.log`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status(s.name)
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
@@ -114,31 +108,35 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Processes 进程列表
|
||||
func (s *App) Processes(w http.ResponseWriter, r *http.Request) {
|
||||
out, err := shell.Execf(`supervisorctl status | awk '{print $1}'`)
|
||||
if err != nil {
|
||||
out, err := shell.Execf(`supervisorctl status`)
|
||||
if err != nil && out == "" {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var processes []Process
|
||||
for line := range strings.SplitSeq(out, "\n") {
|
||||
if len(line) == 0 {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
var p Process
|
||||
p.Name = line
|
||||
if status, err := shell.Execf(`supervisorctl status '%s' | awk '{print $2}'`, line); err == nil {
|
||||
p.Status = status
|
||||
p := Process{
|
||||
Name: fields[0],
|
||||
Status: fields[1],
|
||||
Pid: "-",
|
||||
Uptime: "-",
|
||||
}
|
||||
// RUNNING 行格式:name RUNNING pid 1234, uptime 1:23:45 (超过 1 天为 "1 day, 1:23:45")
|
||||
if p.Status == "RUNNING" {
|
||||
pid, _ := shell.Execf(`supervisorctl status '%s' | awk '{print $4}'`, line)
|
||||
p.Pid = strings.ReplaceAll(pid, ",", "")
|
||||
uptime, _ := shell.Execf(`supervisorctl status '%s' | awk '{print $6}'`, line)
|
||||
p.Uptime = uptime
|
||||
} else {
|
||||
p.Pid = "-"
|
||||
p.Uptime = "-"
|
||||
if _, rest, ok := strings.Cut(line, "pid "); ok {
|
||||
if pid, _, ok := strings.Cut(rest, ","); ok {
|
||||
p.Pid = pid
|
||||
}
|
||||
}
|
||||
if _, uptime, ok := strings.Cut(line, "uptime "); ok {
|
||||
p.Uptime = uptime
|
||||
}
|
||||
}
|
||||
processes = append(processes, p)
|
||||
}
|
||||
@@ -159,7 +157,7 @@ func (s *App) StartProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl start %s`, req.Process); err != nil {
|
||||
if out, err := shell.Execf(`supervisorctl start '%s'`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||||
return
|
||||
}
|
||||
@@ -175,7 +173,7 @@ func (s *App) StopProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl stop %s`, req.Process); err != nil {
|
||||
if out, err := shell.Execf(`supervisorctl stop '%s'`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||||
return
|
||||
}
|
||||
@@ -191,7 +189,7 @@ func (s *App) RestartProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl restart %s`, req.Process); err != nil {
|
||||
if out, err := shell.Execf(`supervisorctl restart '%s'`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||||
return
|
||||
}
|
||||
@@ -207,11 +205,12 @@ func (s *App) ProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
name := programName(req.Process)
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -222,34 +221,6 @@ func (s *App) ProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, logPath)
|
||||
}
|
||||
|
||||
// ClearProcessLog 清空进程日志
|
||||
func (s *App) ClearProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(`cat /dev/null > '%s'`, logPath); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ProcessConfig 获取进程配置
|
||||
func (s *App) ProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
@@ -258,11 +229,12 @@ func (s *App) ProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
name := programName(req.Process)
|
||||
var config string
|
||||
if os.IsRHEL() {
|
||||
config, err = io.Read(`/etc/supervisord.d/` + req.Process + `.conf`)
|
||||
config, err = io.Read(`/etc/supervisord.d/` + name + `.conf`)
|
||||
} else {
|
||||
config, err = io.Read(`/etc/supervisor/conf.d/` + req.Process + `.conf`)
|
||||
config, err = io.Read(`/etc/supervisor/conf.d/` + name + `.conf`)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -281,10 +253,11 @@ func (s *App) UpdateProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
name := programName(req.Process)
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.d/`+req.Process+`.conf`, req.Config, 0644)
|
||||
err = io.Write(`/etc/supervisord.d/`+name+`.conf`, req.Config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+req.Process+`.conf`, req.Config, 0644)
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+name+`.conf`, req.Config, 0644)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -294,7 +267,7 @@ func (s *App) UpdateProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl restart '%s'`, req.Process)
|
||||
_, _ = shell.Execf(`supervisorctl restart '%s:'`, name)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -307,9 +280,14 @@ func (s *App) CreateProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
processName := `%(program_name)s`
|
||||
if req.Num > 1 {
|
||||
processName = `%(program_name)s_%(process_num)02d`
|
||||
}
|
||||
|
||||
config := `[program:` + req.Name + `]
|
||||
command=` + req.Command + `
|
||||
process_name=%(program_name)s
|
||||
process_name=` + processName + `
|
||||
directory=` + req.Path + `
|
||||
autostart=true
|
||||
autorestart=true
|
||||
@@ -320,20 +298,26 @@ stdout_logfile=/var/log/supervisor/` + req.Name + `.log
|
||||
stdout_logfile_maxbytes=2MB
|
||||
`
|
||||
|
||||
var confPath string
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.d/`+req.Name+`.conf`, config, 0644)
|
||||
confPath = `/etc/supervisord.d/` + req.Name + `.conf`
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+req.Name+`.conf`, config, 0644)
|
||||
confPath = `/etc/supervisor/conf.d/` + req.Name + `.conf`
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if io.Exists(confPath) {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("process %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl start '%s'`, req.Name)
|
||||
_, _ = shell.Execf(`supervisorctl start '%s:'`, req.Name)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -346,29 +330,30 @@ func (s *App) DeleteProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl stop '%s'`, req.Process); err != nil {
|
||||
name := programName(req.Process)
|
||||
if out, err := shell.Execf(`supervisorctl stop '%s:'`, name); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||||
return
|
||||
}
|
||||
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
if err = io.Remove(`/etc/supervisord.d/` + req.Process + `.conf`); err != nil {
|
||||
if err = io.Remove(`/etc/supervisord.d/` + name + `.conf`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
if err = io.Remove(`/etc/supervisor/conf.d/` + req.Process + `.conf`); err != nil {
|
||||
if err = io.Remove(`/etc/supervisor/conf.d/` + name + `.conf`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -383,3 +368,8 @@ func (s *App) DeleteProcess(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func programName(process string) string {
|
||||
name, _, _ := strings.Cut(process, ":")
|
||||
return name
|
||||
}
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
package valkey
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServer biz.DatabaseServerRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
databaseServerRepo: databaseServer,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("valkey")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("valkey")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get valkey status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 Valkey 密码
|
||||
withPassword := ""
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
re := regexp.MustCompile(`(?m)^requirepass\s+(.+)`)
|
||||
matches := re.FindStringSubmatch(config)
|
||||
if len(matches) == 2 {
|
||||
withPassword = " -a " + matches[1]
|
||||
}
|
||||
|
||||
raw, err := shell.Execf("valkey-cli%s info", withPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get valkey info: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
dataRaw := lo.SliceToMap(strings.Split(raw, "\n"), func(item string) (string, string) {
|
||||
parts := strings.Split(item, ":")
|
||||
if len(parts) != 2 {
|
||||
return "", ""
|
||||
}
|
||||
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
||||
})
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("TCP Port"), Value: dataRaw["tcp_port"]},
|
||||
{Name: s.t.Get("Uptime in Days"), Value: dataRaw["uptime_in_days"]},
|
||||
{Name: s.t.Get("Connected Clients"), Value: dataRaw["connected_clients"]},
|
||||
{Name: s.t.Get("Total Allocated Memory"), Value: dataRaw["used_memory_human"]},
|
||||
{Name: s.t.Get("Total Memory Usage"), Value: dataRaw["used_memory_rss_human"]},
|
||||
{Name: s.t.Get("Peak Memory Usage"), Value: dataRaw["used_memory_peak_human"]},
|
||||
{Name: s.t.Get("Memory Fragmentation Ratio"), Value: dataRaw["mem_fragmentation_ratio"]},
|
||||
{Name: s.t.Get("Total Connections Received"), Value: dataRaw["total_connections_received"]},
|
||||
{Name: s.t.Get("Total Commands Processed"), Value: dataRaw["total_commands_processed"]},
|
||||
{Name: s.t.Get("Commands Per Second"), Value: dataRaw["instantaneous_ops_per_sec"]},
|
||||
{Name: s.t.Get("Keyspace Hits"), Value: dataRaw["keyspace_hits"]},
|
||||
{Name: s.t.Get("Keyspace Misses"), Value: dataRaw["keyspace_misses"]},
|
||||
{Name: s.t.Get("Latest Fork Time (ms)"), Value: dataRaw["latest_fork_usec"]},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("valkey"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Valkey 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
Bind: s.getValkeyValue(config, "bind"),
|
||||
Port: s.getValkeyValue(config, "port"),
|
||||
Databases: s.getValkeyValue(config, "databases"),
|
||||
Requirepass: s.getValkeyValue(config, "requirepass"),
|
||||
Timeout: s.getValkeyValue(config, "timeout"),
|
||||
TCPKeepalive: s.getValkeyValue(config, "tcp-keepalive"),
|
||||
Maxmemory: s.getValkeyValue(config, "maxmemory"),
|
||||
MaxmemoryPolicy: s.getValkeyValue(config, "maxmemory-policy"),
|
||||
Appendonly: s.getValkeyValue(config, "appendonly"),
|
||||
Appendfsync: s.getValkeyValue(config, "appendfsync"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Valkey 配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root)
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config = s.setValkeyValue(config, "bind", req.Bind)
|
||||
config = s.setValkeyValue(config, "port", req.Port)
|
||||
config = s.setValkeyValue(config, "databases", req.Databases)
|
||||
config = s.setValkeyValue(config, "requirepass", req.Requirepass)
|
||||
config = s.setValkeyValue(config, "timeout", req.Timeout)
|
||||
config = s.setValkeyValue(config, "tcp-keepalive", req.TCPKeepalive)
|
||||
config = s.setValkeyValue(config, "maxmemory", req.Maxmemory)
|
||||
config = s.setValkeyValue(config, "maxmemory-policy", req.MaxmemoryPolicy)
|
||||
config = s.setValkeyValue(config, "appendonly", req.Appendonly)
|
||||
config = s.setValkeyValue(config, "appendfsync", req.Appendfsync)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("valkey"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 同步密码到数据库服务器记录
|
||||
_ = s.databaseServerRepo.UpdatePassword("local_valkey", req.Requirepass)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getValkeyValue 从 Valkey 配置内容中获取指定键的值
|
||||
func (s *App) getValkeyValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
return strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setValkeyValue 在 Valkey 配置内容中设置指定键的值
|
||||
func (s *App) setValkeyValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.Fields(checkLine)
|
||||
if len(parts) >= 1 && parts[0] == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "# "+trimmed)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+" "+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" "+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package valkey
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Valkey 配置调整
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
Bind string `form:"bind" json:"bind"`
|
||||
Port string `form:"port" json:"port"`
|
||||
Databases string `form:"databases" json:"databases"`
|
||||
Requirepass string `form:"requirepass" json:"requirepass"`
|
||||
Timeout string `form:"timeout" json:"timeout"`
|
||||
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive"`
|
||||
// 内存
|
||||
Maxmemory string `form:"maxmemory" json:"maxmemory"`
|
||||
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy"`
|
||||
// 持久化
|
||||
Appendonly string `form:"appendonly" json:"appendonly"`
|
||||
Appendfsync string `form:"appendfsync" json:"appendfsync"`
|
||||
}
|
||||
@@ -13,11 +13,12 @@ import (
|
||||
|
||||
type Cert struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
AccountID uint `gorm:"not null;default:0" json:"account_id"` // 关联的 ACME 账户 ID
|
||||
WebsiteID uint `gorm:"not null;default:0" json:"website_id"` // 关联的网站 ID
|
||||
DNSID uint `gorm:"not null;default:0" json:"dns_id"` // 关联的 DNS ID
|
||||
Type string `gorm:"not null;default:''" json:"type"` // 证书类型 (P256, P384, 2048, 3072, 4096)
|
||||
Domains []string `gorm:"not null;default:'[]';serializer:json" json:"domains"`
|
||||
AccountID uint `gorm:"not null;default:0" json:"account_id"` // 关联的 ACME 账户 ID
|
||||
WebsiteID uint `gorm:"not null;default:0" json:"website_id"` // 关联的网站 ID
|
||||
DNSID uint `gorm:"not null;default:0" json:"dns_id"` // 关联的 DNS ID
|
||||
Type string `gorm:"not null;default:''" json:"type"` // 证书类型 (P256, P384, 2048, 3072, 4096)
|
||||
Domains []string `gorm:"not null;default:'[]';serializer:json" json:"domains"` // 域名
|
||||
Alias map[string]string `gorm:"not null;default:'{}';serializer:json" json:"alias"` // DNS 验证别名
|
||||
AutoRenewal bool `gorm:"not null;default:false" json:"auto_renewal"` // 自动续签
|
||||
RenewalInfo mholtacme.RenewalInfo `gorm:"not null;default:'{}';serializer:json" json:"renewal_info"` // 续签信息
|
||||
CertURL string `gorm:"not null;default:''" json:"cert_url"` // 证书 URL (续签时使用)
|
||||
@@ -41,9 +42,11 @@ type CertRepo interface {
|
||||
Update(ctx context.Context, req *request.CertUpdate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
ObtainAuto(id uint) (*acme.Certificate, error)
|
||||
ObtainAutoWithProgressCallback(ctx context.Context, id uint, progressCallback func(string)) (*acme.Certificate, error)
|
||||
ObtainPanel(account *CertAccount, ips []string) ([]byte, []byte, error)
|
||||
ObtainSelfSigned(id uint) error
|
||||
Renew(id uint) (*acme.Certificate, error)
|
||||
RenewWithProgressCallback(ctx context.Context, id uint, progressCallback func(string)) (*acme.Certificate, error)
|
||||
RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error)
|
||||
Deploy(ID, WebsiteID uint) error
|
||||
Deploy(ID, WebsiteID uint, enableHTTPS bool) error
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
type CertAccount struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Email string `gorm:"not null;default:''" json:"email"`
|
||||
CA string `gorm:"not null;default:'letsencrypt'" json:"ca"` // CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass)
|
||||
CA string `gorm:"not null;default:'letsencrypt'" json:"ca"` // CA 提供商 (letsencrypt, zerossl, sslcom, google)
|
||||
Kid string `gorm:"not null;default:''" json:"kid"`
|
||||
HmacEncoded string `gorm:"not null;default:''" json:"hmac_encoded"`
|
||||
PrivateKey string `gorm:"not null;default:''" json:"private_key"`
|
||||
|
||||
@@ -9,11 +9,13 @@ import (
|
||||
type DatabaseType string
|
||||
|
||||
const (
|
||||
DatabaseTypeMysql DatabaseType = "mysql"
|
||||
DatabaseTypePostgresql DatabaseType = "postgresql"
|
||||
DatabaseTypeMongoDB DatabaseType = "mongodb"
|
||||
DatabaseSQLite DatabaseType = "sqlite"
|
||||
DatabaseTypeRedis DatabaseType = "redis"
|
||||
DatabaseTypeMysql DatabaseType = "mysql"
|
||||
DatabaseTypePostgresql DatabaseType = "postgresql"
|
||||
DatabaseTypeMongoDB DatabaseType = "mongodb"
|
||||
DatabaseTypeClickHouse DatabaseType = "clickhouse"
|
||||
DatabaseTypeSQLite DatabaseType = "sqlite"
|
||||
DatabaseTypeRedis DatabaseType = "redis"
|
||||
DatabaseTypeElasticsearch DatabaseType = "elasticsearch"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
)
|
||||
|
||||
type DatabaseElasticsearchRepo interface {
|
||||
Indices(req *request.DatabaseESIndices) ([]db.ESIndex, error)
|
||||
IndexCreate(req *request.DatabaseESIndexCreate) error
|
||||
IndexDelete(req *request.DatabaseESIndexDelete) error
|
||||
Data(req *request.DatabaseESData) ([]db.ESDocument, int64, error)
|
||||
DocumentGet(req *request.DatabaseESDocumentGet) (*db.ESDocument, error)
|
||||
DocumentSet(req *request.DatabaseESDocumentSet) error
|
||||
DocumentDelete(req *request.DatabaseESDocumentDelete) error
|
||||
}
|
||||
@@ -56,9 +56,14 @@ type ScanPortRank struct {
|
||||
|
||||
// ScanSetting 扫描感知设置
|
||||
type ScanSetting struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Days uint `json:"days"`
|
||||
Interfaces []string `json:"interfaces"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Days uint `json:"days"`
|
||||
Interfaces []string `json:"interfaces"`
|
||||
AutoBlock bool `json:"auto_block"`
|
||||
BlockThreshold uint `json:"block_threshold"` // 扫描次数阈值
|
||||
BlockWindow uint `json:"block_window"` // 检测窗口(分钟)
|
||||
BlockDuration uint `json:"block_duration"` // 屏蔽时长(小时),0=永久
|
||||
Whitelist []string `json:"whitelist"` // IP/CIDR 白名单
|
||||
}
|
||||
|
||||
// ScanEventRepo 扫描事件数据访问接口
|
||||
|
||||
+40
-32
@@ -10,38 +10,46 @@ import (
|
||||
type SettingKey string
|
||||
|
||||
const (
|
||||
SettingKeyName SettingKey = "name"
|
||||
SettingKeyVersion SettingKey = "version"
|
||||
SettingKeyChannel SettingKey = "channel"
|
||||
SettingKeyMonitor SettingKey = "monitor"
|
||||
SettingKeyMonitorDays SettingKey = "monitor_days"
|
||||
SettingKeyBackupPath SettingKey = "backup_path"
|
||||
SettingKeyWebsitePath SettingKey = "website_path"
|
||||
SettingKeyProjectPath SettingKey = "project_path"
|
||||
SettingKeyContainerSock SettingKey = "container_sock"
|
||||
SettingKeyWebsiteTLSVersions SettingKey = "website_tls_versions"
|
||||
SettingKeyWebsiteCipherSuites SettingKey = "website_tls_cipher_suites"
|
||||
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
|
||||
SettingKeyPostgresPassword SettingKey = "postgres_password"
|
||||
SettingKeyOfflineMode SettingKey = "offline_mode"
|
||||
SettingKeyAutoUpdate SettingKey = "auto_update"
|
||||
SettingKeyWebserver SettingKey = "webserver"
|
||||
SettingKeyPublicIPs SettingKey = "public_ips"
|
||||
SettingHiddenMenu SettingKey = "hidden_menu"
|
||||
SettingKeyCustomLogo SettingKey = "custom_logo"
|
||||
SettingKeyMemo SettingKey = "memo"
|
||||
SettingKeyScanAware SettingKey = "scan_aware"
|
||||
SettingKeyScanAwareDays SettingKey = "scan_aware_days"
|
||||
SettingKeyScanAwareInterfaces SettingKey = "scan_aware_interfaces"
|
||||
SettingKeyWebsiteStatDays SettingKey = "website_stat_days"
|
||||
SettingKeyWebsiteStatErrBufMax SettingKey = "website_stat_err_buf_max"
|
||||
SettingKeyWebsiteStatUVMaxKeys SettingKey = "website_stat_uv_max_keys"
|
||||
SettingKeyWebsiteStatIPMaxKeys SettingKey = "website_stat_ip_max_keys"
|
||||
SettingKeyWebsiteStatDetailMaxKeys SettingKey = "website_stat_detail_max_keys"
|
||||
SettingKeyWebsiteStatBodyEnabled SettingKey = "website_stat_body_enabled"
|
||||
SettingKeyIPDBType SettingKey = "ipdb_type" // "" / "custom" / "subscribe"
|
||||
SettingKeyIPDBURL SettingKey = "ipdb_url" // 订阅链接
|
||||
SettingKeyIPDBPath SettingKey = "ipdb_path"
|
||||
SettingKeyName SettingKey = "name"
|
||||
SettingKeyVersion SettingKey = "version"
|
||||
SettingKeyChannel SettingKey = "channel"
|
||||
SettingKeyMonitor SettingKey = "monitor"
|
||||
SettingKeyMonitorDays SettingKey = "monitor_days"
|
||||
SettingKeyMonitorInterval SettingKey = "monitor_interval"
|
||||
SettingKeyBackupPath SettingKey = "backup_path"
|
||||
SettingKeyBackupFormat SettingKey = "backup_format" // tar.xz / tar.gz / zip / 7z
|
||||
SettingKeyWebsitePath SettingKey = "website_path"
|
||||
SettingKeyProjectPath SettingKey = "project_path"
|
||||
SettingKeyContainerSock SettingKey = "container_sock"
|
||||
SettingKeyWebsiteTLSVersions SettingKey = "website_tls_versions"
|
||||
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
|
||||
SettingKeyPostgresPassword SettingKey = "postgres_password"
|
||||
SettingKeyMongoDBAdminPassword SettingKey = "mongodb_admin_password"
|
||||
SettingKeyClickHouseDefaultPassword SettingKey = "clickhouse_default_password"
|
||||
SettingKeyOfflineMode SettingKey = "offline_mode"
|
||||
SettingKeyAutoUpdate SettingKey = "auto_update"
|
||||
SettingKeyWebserver SettingKey = "webserver"
|
||||
SettingKeyPublicIPs SettingKey = "public_ips"
|
||||
SettingHiddenMenu SettingKey = "hidden_menu"
|
||||
SettingKeyCustomLogo SettingKey = "custom_logo"
|
||||
SettingKeyMemo SettingKey = "memo"
|
||||
SettingKeyScanAware SettingKey = "scan_aware"
|
||||
SettingKeyScanAwareDays SettingKey = "scan_aware_days"
|
||||
SettingKeyScanAwareInterfaces SettingKey = "scan_aware_interfaces"
|
||||
SettingKeyScanAwareAutoBlock SettingKey = "scan_aware_auto_block"
|
||||
SettingKeyScanAwareBlockThreshold SettingKey = "scan_aware_block_threshold"
|
||||
SettingKeyScanAwareBlockWindow SettingKey = "scan_aware_block_window"
|
||||
SettingKeyScanAwareBlockDuration SettingKey = "scan_aware_block_duration" // 小时,0=永久
|
||||
SettingKeyScanAwareWhitelist SettingKey = "scan_aware_whitelist" // JSON 数组
|
||||
SettingKeyWebsiteStatDays SettingKey = "website_stat_days"
|
||||
SettingKeyWebsiteStatErrBufMax SettingKey = "website_stat_err_buf_max"
|
||||
SettingKeyWebsiteStatUVMaxKeys SettingKey = "website_stat_uv_max_keys"
|
||||
SettingKeyWebsiteStatIPMaxKeys SettingKey = "website_stat_ip_max_keys"
|
||||
SettingKeyWebsiteStatDetailMaxKeys SettingKey = "website_stat_detail_max_keys"
|
||||
SettingKeyWebsiteStatBodyEnabled SettingKey = "website_stat_body_enabled"
|
||||
SettingKeyIPDBType SettingKey = "ipdb_type" // "" / "custom" / "subscribe"
|
||||
SettingKeyIPDBURL SettingKey = "ipdb_url" // 订阅链接
|
||||
SettingKeyIPDBPath SettingKey = "ipdb_path"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserPasskey struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"index;not null" json:"user_id"`
|
||||
Name string `gorm:"not null;default:''" json:"name"`
|
||||
CredentialID []byte `gorm:"uniqueIndex;not null" json:"-"`
|
||||
PublicKey []byte `gorm:"not null" json:"-"`
|
||||
AAGUID []byte `json:"-"`
|
||||
SignCount uint32 `gorm:"not null;default:0" json:"-"`
|
||||
Transports string `gorm:"not null;default:''" json:"transports"` // JSON: ["internal","hybrid"]
|
||||
BackupEligible bool `gorm:"not null;default:false" json:"-"`
|
||||
BackupState bool `gorm:"not null;default:false" json:"-"`
|
||||
LastUsedAt *time.Time `json:"last_used_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
|
||||
}
|
||||
|
||||
type UserPasskeyRepo interface {
|
||||
List(userID uint) ([]*UserPasskey, error)
|
||||
Create(passkey *UserPasskey) error
|
||||
UpdateSignCount(credentialID []byte, signCount uint32) error
|
||||
UpdateLastUsed(credentialID []byte) error
|
||||
Delete(userID, id uint) error
|
||||
GetByCredentialID(credentialID []byte) (*UserPasskey, *User, error)
|
||||
HasPasskey(userID uint) (bool, error)
|
||||
HasAny() (bool, error)
|
||||
DeleteAllByUserID(userID uint) error
|
||||
}
|
||||
@@ -24,6 +24,7 @@ type Website struct {
|
||||
Path string `gorm:"not null;default:''" json:"path"`
|
||||
SSL bool `gorm:"not null;default:false" json:"ssl"`
|
||||
Remark string `gorm:"not null;default:''" json:"remark"`
|
||||
ExpireAt *time.Time `json:"expire_at"` // 到期时间,nil 表示不限时
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
@@ -44,10 +45,10 @@ type WebsiteRepo interface {
|
||||
Create(ctx context.Context, req *request.WebsiteCreate) (*Website, error)
|
||||
Update(ctx context.Context, req *request.WebsiteUpdate) error
|
||||
Delete(ctx context.Context, req *request.WebsiteDelete) error
|
||||
ClearLog(id uint) error
|
||||
UpdateRemark(id uint, remark string) error
|
||||
ResetConfig(id uint) error
|
||||
UpdateStatus(id uint, status bool) error
|
||||
UpdateExpireAt(id uint, expireAt *time.Time) error
|
||||
UpdateCert(req *request.WebsiteUpdateCert) error
|
||||
ObtainCert(ctx context.Context, id uint) error
|
||||
ObtainCert(ctx context.Context, id uint, dnsID uint) error
|
||||
}
|
||||
|
||||
@@ -2,53 +2,71 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/apache"
|
||||
"github.com/acepanel/panel/v3/internal/apps/clickhouse"
|
||||
"github.com/acepanel/panel/v3/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/v3/internal/apps/docker"
|
||||
"github.com/acepanel/panel/v3/internal/apps/elasticsearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/v3/internal/apps/frp"
|
||||
"github.com/acepanel/panel/v3/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/v3/internal/apps/grafana"
|
||||
"github.com/acepanel/panel/v3/internal/apps/kafka"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/v3/internal/apps/minio"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mongodb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/v3/internal/apps/openresty"
|
||||
"github.com/acepanel/panel/v3/internal/apps/opensearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/percona"
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/prometheus"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/v3/internal/apps/redis"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rocketmq"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rsync"
|
||||
"github.com/acepanel/panel/v3/internal/apps/s3fs"
|
||||
"github.com/acepanel/panel/v3/internal/apps/supervisor"
|
||||
"github.com/acepanel/panel/v3/internal/apps/valkey"
|
||||
"github.com/acepanel/panel/v3/pkg/apploader"
|
||||
)
|
||||
|
||||
func NewLoader(
|
||||
apache *apache.App,
|
||||
clickhouse *clickhouse.App,
|
||||
codeserver *codeserver.App,
|
||||
docker *docker.App,
|
||||
elasticsearch *elasticsearch.App,
|
||||
fail2ban *fail2ban.App,
|
||||
frp *frp.App,
|
||||
gitea *gitea.App,
|
||||
grafana *grafana.App,
|
||||
kafka *kafka.App,
|
||||
mariadb *mariadb.App,
|
||||
memcached *memcached.App,
|
||||
minio *minio.App,
|
||||
mongodb *mongodb.App,
|
||||
mysql *mysql.App,
|
||||
nginx *nginx.App,
|
||||
openresty *openresty.App,
|
||||
opensearch *opensearch.App,
|
||||
percona *percona.App,
|
||||
phpmyadmin *phpmyadmin.App,
|
||||
podman *podman.App,
|
||||
postgresql *postgresql.App,
|
||||
prometheus *prometheus.App,
|
||||
pureftpd *pureftpd.App,
|
||||
redis *redis.App,
|
||||
rocketmq *rocketmq.App,
|
||||
rsync *rsync.App,
|
||||
s3fs *s3fs.App,
|
||||
supervisor *supervisor.App,
|
||||
valkey *valkey.App,
|
||||
) *apploader.Loader {
|
||||
loader := new(apploader.Loader)
|
||||
loader.Add(apache, codeserver, docker, fail2ban, frp, gitea, mariadb, memcached, minio, mysql, nginx, openresty, percona, phpmyadmin, podman, postgresql, pureftpd, redis, rsync, s3fs, supervisor)
|
||||
loader.Add(apache, clickhouse, codeserver, docker, elasticsearch, fail2ban, frp, gitea, grafana, kafka, mariadb, memcached, minio, mongodb, mysql, nginx, openresty, opensearch, percona, phpmyadmin, podman, postgresql, prometheus, pureftpd, redis, rocketmq, rsync, s3fs, supervisor, valkey)
|
||||
return loader
|
||||
}
|
||||
|
||||
@@ -3,22 +3,16 @@ package bootstrap
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/libtnb/cron"
|
||||
"github.com/libtnb/cron/wrap"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/job"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
pkgcron "github.com/acepanel/panel/v3/pkg/cron"
|
||||
)
|
||||
|
||||
func NewCron(conf *config.Config, log *slog.Logger, jobs *job.Jobs) (*cron.Cron, error) {
|
||||
logger := pkgcron.NewLogger(log, conf.App.Debug)
|
||||
|
||||
func NewCron(log *slog.Logger, jobs *job.Jobs) (*cron.Cron, error) {
|
||||
c := cron.New(
|
||||
cron.WithParser(cron.NewParser(
|
||||
cron.SecondOptional|cron.Minute|cron.Hour|cron.Dom|cron.Month|cron.Dow|cron.Descriptor,
|
||||
)),
|
||||
cron.WithLogger(logger),
|
||||
cron.WithChain(cron.Recover(logger), cron.SkipIfStillRunning(logger)),
|
||||
cron.WithLogger(log),
|
||||
cron.WithChain(wrap.Recover(), wrap.SkipIfRunning()),
|
||||
)
|
||||
if err := jobs.Register(c); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -31,7 +31,7 @@ func NewRouter(t *gotext.Locale, middlewares *middleware.Middlewares, http *rout
|
||||
}
|
||||
|
||||
func NewTLSReloader(conf *config.Config) (*tlscert.Reloader, error) {
|
||||
if !conf.HTTP.TLS {
|
||||
if !conf.HTTP.IsHTTPS() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func NewHttp(conf *config.Config, mux *chi.Mux, reloader *tlscert.Reloader) (*hl
|
||||
})
|
||||
srv.Listen80RedirectTo443 = true
|
||||
|
||||
if conf.HTTP.TLS && reloader != nil {
|
||||
if conf.HTTP.IsHTTPS() && reloader != nil {
|
||||
srv.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
GetCertificate: reloader.GetCertificate,
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -58,15 +59,9 @@ func (r *appRepo) Categories() []types.LV {
|
||||
return a.Order - b.Order
|
||||
})
|
||||
|
||||
result := make([]types.LV, 0)
|
||||
for item := range slices.Values(categories) {
|
||||
result = append(result, types.LV{
|
||||
Label: item.Name,
|
||||
Value: item.Slug,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
return lo.Map(categories, func(item *api.Category, _ int) types.LV {
|
||||
return types.LV{Label: item.Name, Value: item.Slug}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *appRepo) All() api.Apps {
|
||||
|
||||
+88
-21
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -105,7 +106,11 @@ func (r *backupRepo) Create(ctx context.Context, typ biz.BackupType, target stri
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
name := fmt.Sprintf("%s_%s", target, start.Format("20060102150405"))
|
||||
namePrefix := target
|
||||
if typ == biz.BackupTypePath {
|
||||
namePrefix = filepath.Base(target)
|
||||
}
|
||||
name := fmt.Sprintf("%s_%s", namePrefix, start.Format("20060102150405"))
|
||||
if app.IsCli {
|
||||
fmt.Println(r.hr)
|
||||
fmt.Println(r.t.Get("★ Start backup [%s]", start.Format(time.DateTime)))
|
||||
@@ -122,6 +127,8 @@ func (r *backupRepo) Create(ctx context.Context, typ biz.BackupType, target stri
|
||||
err = r.createMySQL(name, client, target)
|
||||
case biz.BackupTypePostgres:
|
||||
err = r.createPostgres(name, client, target)
|
||||
case biz.BackupTypePath:
|
||||
err = r.createPath(name, client, target)
|
||||
default:
|
||||
return errors.New(r.t.Get("unknown backup type"))
|
||||
}
|
||||
@@ -164,7 +171,7 @@ func (r *backupRepo) Create(ctx context.Context, typ biz.BackupType, target stri
|
||||
func (r *backupRepo) CreatePanel() error {
|
||||
start := time.Now()
|
||||
|
||||
backup := filepath.Join(r.GetDefaultPath(biz.BackupTypePanel), fmt.Sprintf("panel_%s.zip", time.Now().Format("20060102150405")))
|
||||
backup := filepath.Join(r.GetDefaultPath(biz.BackupTypePanel), fmt.Sprintf("panel_%s%s", time.Now().Format("20060102150405"), r.backupExt()))
|
||||
|
||||
temp, err := os.MkdirTemp("", "ace-backup-*")
|
||||
if err != nil {
|
||||
@@ -267,7 +274,7 @@ func (r *backupRepo) CutoffLog(path, target string) (string, error) {
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(filepath.Base(target), filepath.Ext(target))
|
||||
to := filepath.Join(path, fmt.Sprintf("%s_%s.zip", name, time.Now().Format("20060102150405")))
|
||||
to := filepath.Join(path, fmt.Sprintf("%s_%s%s", name, time.Now().Format("20060102150405"), r.backupExt()))
|
||||
if err := io.Compress(filepath.Dir(target), []string{filepath.Base(target)}, to); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -321,16 +328,16 @@ func (r *backupRepo) ClearExpired(path, prefix string, save uint) error {
|
||||
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)
|
||||
filtered := lo.FilterMap(files, func(file os.DirEntry, _ int) (os.FileInfo, bool) {
|
||||
if !strings.HasPrefix(file.Name(), prefix) || !r.isBackupArchive(file.Name()) {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
info, err := os.Stat(filepath.Join(path, file.Name()))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return info, true
|
||||
})
|
||||
|
||||
// 排序所有备份文件,从新到旧
|
||||
slices.SortFunc(filtered, func(a, b os.FileInfo) int {
|
||||
@@ -387,7 +394,7 @@ func (r *backupRepo) ClearStorageExpired(storage uint, typ biz.BackupType, prefi
|
||||
}
|
||||
var filtered []fileInfo
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file, prefix) && strings.HasSuffix(file, ".zip") {
|
||||
if strings.HasPrefix(file, prefix) && r.isBackupArchive(file) {
|
||||
lastModified, modErr := client.LastModified(filepath.Join(string(typ), file))
|
||||
if modErr != nil {
|
||||
continue
|
||||
@@ -481,7 +488,7 @@ func (r *backupRepo) createWebsite(name string, storage storage.Storage, target
|
||||
}
|
||||
|
||||
// 压缩网站
|
||||
name = name + ".zip"
|
||||
name = name + r.backupExt()
|
||||
if err = io.Compress(website.Path, nil, filepath.Join(tmpDir, name)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -493,7 +500,7 @@ func (r *backupRepo) createWebsite(name string, storage storage.Storage, target
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
if err = storage.Put(filepath.Join("website", name), file); err != nil {
|
||||
if err = storage.Put(filepath.Join(string(biz.BackupTypeWebsite), name), file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -539,19 +546,19 @@ func (r *backupRepo) createMySQL(name string, storage storage.Storage, target st
|
||||
_ = os.Unsetenv("MYSQL_PWD")
|
||||
|
||||
// 压缩备份文件
|
||||
if err = io.Compress(tmpDir, []string{name}, filepath.Join(tmpDir, name+".zip")); err != nil {
|
||||
if err = io.Compress(tmpDir, []string{name}, filepath.Join(tmpDir, name+r.backupExt())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 上传备份文件到存储器
|
||||
name = name + ".zip"
|
||||
name = name + r.backupExt()
|
||||
file, err := os.Open(filepath.Join(tmpDir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
if err = storage.Put(filepath.Join("mysql", name), file); err != nil {
|
||||
if err = storage.Put(filepath.Join(string(biz.BackupTypeMySQL), name), file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -597,19 +604,63 @@ func (r *backupRepo) createPostgres(name string, storage storage.Storage, target
|
||||
_ = os.Unsetenv("PGPASSWORD")
|
||||
|
||||
// 压缩备份文件
|
||||
if err = io.Compress(tmpDir, []string{name}, filepath.Join(tmpDir, name+".zip")); err != nil {
|
||||
if err = io.Compress(tmpDir, []string{name}, filepath.Join(tmpDir, name+r.backupExt())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 上传备份文件到存储器
|
||||
name = name + ".zip"
|
||||
name = name + r.backupExt()
|
||||
file, err := os.Open(filepath.Join(tmpDir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
if err = storage.Put(filepath.Join("postgres", name), file); err != nil {
|
||||
if err = storage.Put(filepath.Join(string(biz.BackupTypePostgres), name), file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
fmt.Println(r.t.Get("|-Backup file: %s", name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createPath 创建目录备份
|
||||
func (r *backupRepo) createPath(name string, storage storage.Storage, target string) error {
|
||||
if !io.Exists(target) {
|
||||
return errors.New(r.t.Get("path does not exist: %s", target))
|
||||
}
|
||||
if !io.IsDir(target) {
|
||||
return errors.New(r.t.Get("path is not a directory: %s", target))
|
||||
}
|
||||
|
||||
// 创建用于压缩的临时目录
|
||||
tmpDir, err := os.MkdirTemp("", "ace-backup-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(path string) { _ = os.RemoveAll(path) }(tmpDir)
|
||||
|
||||
if app.IsCli {
|
||||
fmt.Println(r.t.Get("|-Temporary directory: %s", tmpDir))
|
||||
}
|
||||
|
||||
// 压缩目录
|
||||
name = name + r.backupExt()
|
||||
if err = io.Compress(target, nil, filepath.Join(tmpDir, name)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 上传备份文件到存储器
|
||||
file, err := os.Open(filepath.Join(tmpDir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
if err = storage.Put(filepath.Join(string(biz.BackupTypePath), name), file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1053,3 +1104,19 @@ func (r *backupRepo) UpdatePanel(version, url, checksum string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// backupExt 根据全局设置返回备份文件扩展名
|
||||
func (r *backupRepo) backupExt() string {
|
||||
format, _ := r.setting.Get(biz.SettingKeyBackupFormat, "tar.xz")
|
||||
return "." + format
|
||||
}
|
||||
|
||||
// isBackupArchive 判断文件名是否是已知的备份压缩包后缀
|
||||
func (r *backupRepo) isBackupArchive(name string) bool {
|
||||
for _, ext := range []string{".tar.xz", ".tar.gz", ".zip", ".7z"} {
|
||||
if strings.HasSuffix(name, ext) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"slices"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
@@ -41,13 +42,10 @@ func (r *cacheRepo) Get(key biz.CacheKey, defaultValue ...string) (string, error
|
||||
}
|
||||
|
||||
func (r *cacheRepo) Set(key biz.CacheKey, value string) error {
|
||||
cache := new(biz.Cache)
|
||||
if err := r.db.Where(biz.Cache{Key: key}).FirstOrInit(cache).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cache.Value = value
|
||||
return r.db.Save(cache).Error
|
||||
return r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "key"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"value"}),
|
||||
}).Create(&biz.Cache{Key: key, Value: value}).Error
|
||||
}
|
||||
|
||||
func (r *cacheRepo) UpdateCategories() error {
|
||||
|
||||
+143
-28
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
mholtacme "github.com/mholt/acmez/v3/acme"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -24,6 +26,8 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
"github.com/acepanel/panel/v3/pkg/webserver"
|
||||
webservertypes "github.com/acepanel/panel/v3/pkg/webserver/types"
|
||||
)
|
||||
|
||||
type certRepo struct {
|
||||
@@ -47,8 +51,7 @@ func (r *certRepo) List(page, limit uint) ([]*types.CertList, int64, error) {
|
||||
var total int64
|
||||
err := r.db.Model(&biz.Cert{}).Preload("Website").Preload("Account").Preload("DNS").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certs).Error
|
||||
|
||||
list := make([]*types.CertList, 0)
|
||||
for cert := range slices.Values(certs) {
|
||||
list := lo.Map(certs, func(cert *biz.Cert, _ int) *types.CertList {
|
||||
item := &types.CertList{
|
||||
ID: cert.ID,
|
||||
AccountID: cert.AccountID,
|
||||
@@ -56,6 +59,7 @@ func (r *certRepo) List(page, limit uint) ([]*types.CertList, int64, error) {
|
||||
DNSID: cert.DNSID,
|
||||
Type: cert.Type,
|
||||
Domains: cert.Domains,
|
||||
Alias: cert.Alias,
|
||||
AutoRenewal: cert.AutoRenewal,
|
||||
NextRenewal: cert.RenewalInfo.SelectedTime,
|
||||
Cert: cert.Cert,
|
||||
@@ -71,13 +75,12 @@ func (r *certRepo) List(page, limit uint) ([]*types.CertList, int64, error) {
|
||||
item.Issuer = decode.Issuer.CommonName
|
||||
item.OCSPServer = decode.OCSPServer
|
||||
// 合并 DNSNames 和 IPAddresses
|
||||
item.DNSNames = decode.DNSNames
|
||||
for _, ip := range decode.IPAddresses {
|
||||
item.DNSNames = append(item.DNSNames, ip.String())
|
||||
}
|
||||
item.DNSNames = append(decode.DNSNames, lo.Map(decode.IPAddresses, func(ip net.IP, _ int) string {
|
||||
return ip.String()
|
||||
})...)
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
return list, total, err
|
||||
}
|
||||
@@ -132,6 +135,7 @@ func (r *certRepo) Create(ctx context.Context, req *request.CertCreate) (*biz.Ce
|
||||
DNSID: req.DNSID,
|
||||
Type: req.Type,
|
||||
Domains: req.Domains,
|
||||
Alias: req.Alias,
|
||||
AutoRenewal: req.AutoRenewal,
|
||||
}
|
||||
if err := r.db.Create(cert).Error; err != nil {
|
||||
@@ -167,6 +171,7 @@ func (r *certRepo) Update(ctx context.Context, req *request.CertUpdate) error {
|
||||
Key: req.Key,
|
||||
Script: req.Script,
|
||||
Domains: req.Domains,
|
||||
Alias: req.Alias,
|
||||
AutoRenewal: req.AutoRenewal,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
@@ -190,6 +195,17 @@ func (r *certRepo) Delete(ctx context.Context, id uint) error {
|
||||
}
|
||||
|
||||
func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
return r.ObtainAutoWithProgressCallback(context.Background(), id, nil)
|
||||
}
|
||||
|
||||
func (r *certRepo) ObtainAutoWithProgressCallback(ctx context.Context, id uint, progressCallback func(string)) (*acme.Certificate, error) {
|
||||
report := func(msg string) {
|
||||
if progressCallback != nil {
|
||||
progressCallback(msg)
|
||||
}
|
||||
}
|
||||
|
||||
report(r.t.Get("initializing ACME client"))
|
||||
cert, err := r.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -203,28 +219,35 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
webServer, _ := r.settingRepo.Get(biz.SettingKeyWebserver)
|
||||
|
||||
if cert.DNS != nil {
|
||||
client.UseDns(cert.DNS.Type, cert.DNS.Data)
|
||||
client.UseDns(cert.DNS.Type, cert.DNS.Data, acme.DnsOption{
|
||||
Alias: cert.Alias,
|
||||
DnsServer: cert.DNS.Data.DnsServer,
|
||||
SkipVerify: cert.DNS.Data.SkipVerify,
|
||||
ProgressCallback: progressCallback,
|
||||
})
|
||||
} else {
|
||||
if cert.Website == nil {
|
||||
return nil, errors.New(r.t.Get("this certificate is not associated with a website and cannot be obtained. You can try to obtain it manually"))
|
||||
} else {
|
||||
for _, domain := range cert.Domains {
|
||||
if strings.Contains(domain, "*") {
|
||||
return nil, errors.New(r.t.Get("wildcard domains cannot use HTTP verification"))
|
||||
}
|
||||
}
|
||||
conf := fmt.Sprintf("%s/sites/%s/config/site/001-acme.conf", app.Root, cert.Website.Name)
|
||||
client.UseHTTP(conf, webServer)
|
||||
}
|
||||
hasWildcard := slices.ContainsFunc(cert.Domains, func(d string) bool {
|
||||
return strings.Contains(d, "*")
|
||||
})
|
||||
if hasWildcard {
|
||||
return nil, errors.New(r.t.Get("wildcard domains cannot use HTTP verification"))
|
||||
}
|
||||
conf := fmt.Sprintf("%s/sites/%s/config/site/001-acme.conf", app.Root, cert.Website.Name)
|
||||
client.UseHTTP(conf, webServer)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
report(r.t.Get("issuing certificate, domains: %s", strings.Join(cert.Domains, ", ")))
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
defer cancel()
|
||||
ssl, err := client.ObtainCertificate(ctx, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
report(r.t.Get("obtaining and saving certificate"))
|
||||
cert.RenewalInfo = *ssl.RenewalInfo
|
||||
cert.CertURL = ssl.URL
|
||||
cert.Cert = string(ssl.ChainPEM)
|
||||
@@ -234,7 +257,8 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
return &ssl, r.Deploy(cert.ID, cert.WebsiteID)
|
||||
report(r.t.Get("deploying certificate to website"))
|
||||
return &ssl, r.Deploy(cert.ID, cert.WebsiteID, false)
|
||||
}
|
||||
|
||||
if err = r.runScript(cert); err != nil {
|
||||
@@ -285,7 +309,7 @@ func (r *certRepo) ObtainSelfSigned(id uint) error {
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
return r.Deploy(cert.ID, cert.WebsiteID)
|
||||
return r.Deploy(cert.ID, cert.WebsiteID, false)
|
||||
}
|
||||
|
||||
if err = r.runScript(cert); err != nil {
|
||||
@@ -296,6 +320,17 @@ func (r *certRepo) ObtainSelfSigned(id uint) error {
|
||||
}
|
||||
|
||||
func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
|
||||
return r.RenewWithProgressCallback(context.Background(), id, nil)
|
||||
}
|
||||
|
||||
func (r *certRepo) RenewWithProgressCallback(ctx context.Context, id uint, progressCallback func(string)) (*acme.Certificate, error) {
|
||||
report := func(msg string) {
|
||||
if progressCallback != nil {
|
||||
progressCallback(msg)
|
||||
}
|
||||
}
|
||||
|
||||
report(r.t.Get("preparing renewal"))
|
||||
cert, err := r.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -313,7 +348,12 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
|
||||
webServer, _ := r.settingRepo.Get(biz.SettingKeyWebserver)
|
||||
|
||||
if cert.DNS != nil {
|
||||
client.UseDns(cert.DNS.Type, cert.DNS.Data)
|
||||
client.UseDns(cert.DNS.Type, cert.DNS.Data, acme.DnsOption{
|
||||
Alias: cert.Alias,
|
||||
DnsServer: cert.DNS.Data.DnsServer,
|
||||
SkipVerify: cert.DNS.Data.SkipVerify,
|
||||
ProgressCallback: progressCallback,
|
||||
})
|
||||
} else {
|
||||
if cert.Website == nil {
|
||||
return nil, errors.New(r.t.Get("this certificate is not associated with a website and cannot be obtained. You can try to obtain it manually"))
|
||||
@@ -328,17 +368,20 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
report(r.t.Get("renewing certificate, domains: %s", strings.Join(cert.Domains, ", ")))
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
defer cancel()
|
||||
ssl, err := client.RenewCertificate(ctx, cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
// 续签失败,尝试重签
|
||||
report(r.t.Get("renewal failed, attempting re-issuance"))
|
||||
ssl, err = client.ObtainCertificate(ctx, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
report(r.t.Get("obtaining and saving certificate"))
|
||||
cert.RenewalInfo = *ssl.RenewalInfo
|
||||
cert.CertURL = ssl.URL
|
||||
cert.Cert = string(ssl.ChainPEM)
|
||||
@@ -348,7 +391,8 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
return &ssl, r.Deploy(cert.ID, cert.WebsiteID)
|
||||
report(r.t.Get("deploying certificate to website"))
|
||||
return &ssl, r.Deploy(cert.ID, cert.WebsiteID, false)
|
||||
}
|
||||
|
||||
return &ssl, nil
|
||||
@@ -384,7 +428,7 @@ func (r *certRepo) RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error) {
|
||||
return renewInfo, nil
|
||||
}
|
||||
|
||||
func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
func (r *certRepo) Deploy(ID, WebsiteID uint, enableHTTPS bool) error {
|
||||
cert, err := r.Get(ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -398,13 +442,60 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
if err = r.db.Where("id", WebsiteID).First(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(fmt.Sprintf("%s/sites/%s/config/fullchain.pem", app.Root, website.Name), cert.Cert, 0600); err != nil {
|
||||
configDir := filepath.Join(app.Root, "sites", website.Name, "config")
|
||||
certPath := filepath.Join(configDir, "fullchain.pem")
|
||||
keyPath := filepath.Join(configDir, "private.key")
|
||||
if err = io.Write(certPath, cert.Cert, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(fmt.Sprintf("%s/sites/%s/config/private.key", app.Root, website.Name), cert.Key, 0600); err != nil {
|
||||
if err = io.Write(keyPath, cert.Key, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 开启 HTTPS
|
||||
if enableHTTPS && !website.SSL {
|
||||
vhost, vhostErr := r.getVhost(website)
|
||||
if vhostErr != nil {
|
||||
return vhostErr
|
||||
}
|
||||
|
||||
// 添加 443 监听
|
||||
listens := vhost.Listen()
|
||||
hasSSL := slices.ContainsFunc(listens, func(l webservertypes.Listen) bool {
|
||||
return slices.Contains(l.Args, "ssl")
|
||||
})
|
||||
if !hasSSL {
|
||||
webServer, _ := r.settingRepo.Get(biz.SettingKeyWebserver)
|
||||
args := []string{"ssl"}
|
||||
if webServer != "apache" {
|
||||
args = append(args, "quic")
|
||||
}
|
||||
listens = append(listens, webservertypes.Listen{Address: "443", Args: args})
|
||||
if err = vhost.SetListen(listens); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 配置 SSL
|
||||
defaultTLSVersions, _ := r.settingRepo.GetSlice(biz.SettingKeyWebsiteTLSVersions)
|
||||
if err = vhost.SetSSLConfig(&webservertypes.SSLConfig{
|
||||
Cert: certPath,
|
||||
Key: keyPath,
|
||||
Protocols: defaultTLSVersions,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = vhost.Save(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
website.SSL = true
|
||||
if err = r.db.Save(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
webServer, _ := r.settingRepo.Get(biz.SettingKeyWebserver)
|
||||
if webServer == "apache" {
|
||||
if err = systemctl.Reload("apache"); err != nil {
|
||||
@@ -421,6 +512,32 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// getVhost 根据网站类型获取虚拟主机配置
|
||||
func (r *certRepo) getVhost(website *biz.Website) (webservertypes.Vhost, error) {
|
||||
webServer, err := r.settingRepo.Get(biz.SettingKeyWebserver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configDir := filepath.Join(app.Root, "sites", website.Name, "config")
|
||||
var vhost webservertypes.Vhost
|
||||
switch website.Type {
|
||||
case biz.WebsiteTypeProxy:
|
||||
vhost, err = webserver.NewProxyVhost(webserver.Type(webServer), configDir)
|
||||
case biz.WebsiteTypePHP:
|
||||
vhost, err = webserver.NewPHPVhost(webserver.Type(webServer), configDir)
|
||||
case biz.WebsiteTypeStatic:
|
||||
vhost, err = webserver.NewStaticVhost(webserver.Type(webServer), configDir)
|
||||
default:
|
||||
return nil, errors.New(r.t.Get("unsupported website type: %s", website.Type))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vhost, nil
|
||||
}
|
||||
|
||||
func (r *certRepo) runScript(cert *biz.Cert) error {
|
||||
if cert.Script == "" {
|
||||
return nil
|
||||
@@ -465,8 +582,6 @@ func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) {
|
||||
ca = acme.CALetsEncrypt
|
||||
case "litessl":
|
||||
ca = acme.CALiteSSL
|
||||
case "buypass":
|
||||
ca = acme.CABuypass
|
||||
case "zerossl":
|
||||
ca = acme.CAZeroSSL
|
||||
eab = &acme.EAB{KeyID: cert.Account.Kid, MACKey: cert.Account.HmacEncoded}
|
||||
|
||||
@@ -90,8 +90,6 @@ func (r certAccountRepo) Create(ctx context.Context, req *request.CertAccountCre
|
||||
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CALetsEncrypt, nil, acme.KeyType(account.KeyType), r.log)
|
||||
case "litessl":
|
||||
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CALiteSSL, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType), r.log)
|
||||
case "buypass":
|
||||
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CABuypass, nil, acme.KeyType(account.KeyType), r.log)
|
||||
case "zerossl":
|
||||
eab, eabErr := r.getZeroSSLEAB(account.Email)
|
||||
if eabErr != nil {
|
||||
@@ -154,8 +152,6 @@ func (r certAccountRepo) Update(ctx context.Context, req *request.CertAccountUpd
|
||||
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CALetsEncrypt, nil, acme.KeyType(account.KeyType), r.log)
|
||||
case "litessl":
|
||||
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CALiteSSL, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType), r.log)
|
||||
case "buypass":
|
||||
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CABuypass, nil, acme.KeyType(account.KeyType), r.log)
|
||||
case "zerossl":
|
||||
eab, eabErr := r.getZeroSSLEAB(account.Email)
|
||||
if eabErr != nil {
|
||||
|
||||
@@ -64,7 +64,13 @@ func (r *containerComposeRepo) List() ([]types.ContainerCompose, error) {
|
||||
|
||||
// 更新状态
|
||||
for _, item := range composeRaws {
|
||||
if i, ok := index[item.Name]; ok {
|
||||
if item.ConfigFiles == "" {
|
||||
continue
|
||||
}
|
||||
// ConfigFiles 可能包含多个以逗号分隔的路径,取第一个
|
||||
first := strings.SplitN(item.ConfigFiles, ",", 2)[0]
|
||||
dir := filepath.Base(filepath.Dir(first))
|
||||
if i, ok := index[dir]; ok {
|
||||
composes[i].Status = item.Status
|
||||
}
|
||||
}
|
||||
@@ -77,17 +83,7 @@ func (r *containerComposeRepo) Get(name string) (string, []types.KV, error) {
|
||||
content, _ := os.ReadFile(filepath.Join(app.Root, "compose", name, "docker-compose.yml"))
|
||||
env, _ := os.ReadFile(filepath.Join(app.Root, "compose", name, ".env"))
|
||||
|
||||
var envs []types.KV
|
||||
for line := range strings.SplitSeq(string(env), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
kv := strings.SplitN(line, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
envs = append(envs, types.KV{Key: kv[0], Value: kv[1]})
|
||||
}
|
||||
envs := types.SliceToKV(strings.Split(string(env), "\n"))
|
||||
|
||||
return string(content), envs, nil // 有意忽略错误,这样可以允许新建文件
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import (
|
||||
"time"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
@@ -41,9 +43,8 @@ func (r *containerImageRepo) List() ([]types.ContainerImage, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var images []types.ContainerImage
|
||||
for _, item := range resp.Items {
|
||||
images = append(images, types.ContainerImage{
|
||||
images := lo.Map(resp.Items, func(item image.Summary, _ int) types.ContainerImage {
|
||||
return types.ContainerImage{
|
||||
ID: item.ID,
|
||||
Containers: item.Containers,
|
||||
RepoTags: item.RepoTags,
|
||||
@@ -51,8 +52,8 @@ func (r *containerImageRepo) List() ([]types.ContainerImage, error) {
|
||||
Size: tools.FormatBytes(float64(item.Size)),
|
||||
Labels: types.MapToKV(item.Labels),
|
||||
CreatedAt: time.Unix(item.Created, 0),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
slices.SortFunc(images, func(a types.ContainerImage, b types.ContainerImage) int {
|
||||
return strings.Compare(a.ID, b.ID)
|
||||
@@ -135,7 +136,9 @@ func (r *containerImageRepo) Prune() error {
|
||||
defer func(apiClient *client.Client) { _ = apiClient.Close() }(apiClient)
|
||||
|
||||
_, err = apiClient.ImagePrune(context.Background(), client.ImagePruneOptions{
|
||||
Filters: make(client.Filters).Add("label", "created_by!=acepanel"),
|
||||
Filters: make(client.Filters).
|
||||
Add("dangling", "false").
|
||||
Add("label", "created_by!=acepanel"),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
+75
-1
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
@@ -181,9 +182,11 @@ func (r *cronRepo) Delete(ctx context.Context, id uint) error {
|
||||
if err = io.Remove(cron.Shell); err != nil {
|
||||
return err
|
||||
}
|
||||
// 清理 .lock 文件
|
||||
// 清理 .lock 文件和 _wrapper.sh 文件
|
||||
lockFile := strings.TrimSuffix(cron.Shell, ".sh") + ".lock"
|
||||
_ = io.Remove(lockFile)
|
||||
wrapperFile := strings.TrimSuffix(cron.Shell, ".sh") + "_wrapper.sh"
|
||||
_ = io.Remove(wrapperFile)
|
||||
|
||||
if err = r.db.Delete(cron).Error; err != nil {
|
||||
return err
|
||||
@@ -222,6 +225,20 @@ func (r *cronRepo) addToSystem(cron *biz.Cron) error {
|
||||
lockFile := strings.TrimSuffix(cron.Shell, ".sh") + ".lock"
|
||||
cmd = fmt.Sprintf("flock -xn %s %s", lockFile, cron.Shell)
|
||||
}
|
||||
|
||||
// 秒级任务:生成 wrapper 脚本,用每分钟触发模拟秒级执行
|
||||
if seconds := r.parseSeconds(cron.Time); seconds > 0 {
|
||||
wrapperPath := strings.TrimSuffix(cron.Shell, ".sh") + "_wrapper.sh"
|
||||
wrapperScript := r.generateWrapper(cmd, cron.Log, seconds)
|
||||
if err := io.Write(wrapperPath, wrapperScript, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := shell.Execf(`( crontab -l; echo "* * * * * %s" ) | sort - | uniq - | crontab -`, wrapperPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.restartCron()
|
||||
}
|
||||
|
||||
if _, err := shell.Execf(`( crontab -l; echo "%s %s >> %s 2>&1" ) | sort - | uniq - | crontab -`, cron.Time, cmd, cron.Log); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -231,6 +248,12 @@ func (r *cronRepo) addToSystem(cron *biz.Cron) error {
|
||||
|
||||
// deleteFromSystem 从系统中删除
|
||||
func (r *cronRepo) deleteFromSystem(cron *biz.Cron) error {
|
||||
// 清理秒级任务的 wrapper 条目和脚本
|
||||
wrapperPath := strings.TrimSuffix(cron.Shell, ".sh") + "_wrapper.sh"
|
||||
_, _ = shell.Execf(`( crontab -l | grep -v -F "%s" ) | crontab -`, wrapperPath)
|
||||
_ = io.Remove(wrapperPath)
|
||||
|
||||
// 清理普通任务的 crontab 条目
|
||||
if _, err := shell.Execf(`( crontab -l | grep -v -F "%s >> %s 2>&1" ) | crontab -`, cron.Shell, cron.Log); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -268,6 +291,8 @@ func (r *cronRepo) generateScript(typ string, config types.CronConfig, rawScript
|
||||
_, _ = fmt.Fprintf(&sb, "acepanel backup website -n '%s' -s '%d'\n", target, config.Storage)
|
||||
case "mysql", "postgresql":
|
||||
_, _ = fmt.Fprintf(&sb, "acepanel backup database -t '%s' -n '%s' -s '%d'\n", config.Type, target, config.Storage)
|
||||
case "path":
|
||||
_, _ = fmt.Fprintf(&sb, "acepanel backup path -p '%s' -s '%d'\n", target, config.Storage)
|
||||
}
|
||||
}
|
||||
for _, target := range config.Targets {
|
||||
@@ -276,6 +301,8 @@ func (r *cronRepo) generateScript(typ string, config types.CronConfig, rawScript
|
||||
_, _ = fmt.Fprintf(&sb, "acepanel backup clear -t website -f '%s' -k '%d' -s '%d'\n", target, config.Keep, config.Storage)
|
||||
case "mysql", "postgresql":
|
||||
_, _ = fmt.Fprintf(&sb, "acepanel backup clear -t '%s' -f '%s' -k '%d' -s '%d'\n", config.Type, target, config.Keep, config.Storage)
|
||||
case "path":
|
||||
_, _ = fmt.Fprintf(&sb, "acepanel backup clear -t path -f '%s' -k '%d' -s '%d'\n", filepath.Base(target), config.Keep, config.Storage)
|
||||
}
|
||||
}
|
||||
case "cutoff":
|
||||
@@ -323,3 +350,50 @@ func (r *cronRepo) generateScript(typ string, config types.CronConfig, rawScript
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// parseSeconds 从 6 字段 cron 表达式中解析秒级间隔
|
||||
// 返回 0 表示非秒级任务
|
||||
func (r *cronRepo) parseSeconds(time string) int {
|
||||
fields := strings.Fields(time)
|
||||
if len(fields) != 6 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 6 字段格式:秒 分 时 日 月 周,后 5 个字段必须全是 *
|
||||
for _, f := range fields[1:] {
|
||||
if f != "*" {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
second := fields[0]
|
||||
// 每秒:* * * * * *
|
||||
if second == "*" {
|
||||
return 1
|
||||
}
|
||||
// 每 N 秒:*/N * * * * *
|
||||
if strings.HasPrefix(second, "*/") {
|
||||
n, err := strconv.Atoi(second[2:])
|
||||
if err != nil || n <= 0 || n > 59 {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// generateWrapper 生成秒级任务的 wrapper 脚本
|
||||
// 通过每分钟触发 + 循环 sleep 模拟秒级执行
|
||||
func (r *cronRepo) generateWrapper(cmd, logFile string, seconds int) string {
|
||||
count := 60 / seconds
|
||||
return fmt.Sprintf(`#!/bin/bash
|
||||
INTERVAL=%d
|
||||
COUNT=%d
|
||||
for i in $(seq 1 $COUNT); do
|
||||
%s >> %s 2>&1 &
|
||||
[ $i -lt $COUNT ] && sleep $INTERVAL
|
||||
done
|
||||
wait
|
||||
`, seconds, count, cmd, logFile)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewCronRepo,
|
||||
NewDatabaseRepo,
|
||||
NewDatabaseRedisRepo,
|
||||
NewDatabaseElasticsearchRepo,
|
||||
NewDatabaseServerRepo,
|
||||
NewDatabaseUserRepo,
|
||||
NewEnvironmentRepo,
|
||||
@@ -32,6 +33,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewTaskRepo,
|
||||
NewTemplateRepo,
|
||||
NewUserRepo,
|
||||
NewUserPasskeyRepo,
|
||||
NewUserTokenRepo,
|
||||
NewWebHookRepo,
|
||||
NewWebsiteRepo,
|
||||
|
||||
+116
-35
@@ -45,25 +45,58 @@ func (r *databaseRepo) List(page, limit uint, typ string) ([]*biz.Database, int6
|
||||
|
||||
database := make([]*biz.Database, 0)
|
||||
for _, server := range databaseServer {
|
||||
operator, err := r.getOperator(server)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if databases, err := operator.Databases(); err == nil {
|
||||
for item := range slices.Values(databases) {
|
||||
database = append(database, &biz.Database{
|
||||
Type: server.Type,
|
||||
Name: item.Name,
|
||||
Server: server.Name,
|
||||
ServerID: server.ID,
|
||||
Encoding: item.CharSet,
|
||||
Comment: item.Comment,
|
||||
})
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMongoDB:
|
||||
mongo, err := db.NewMongoDB(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if databases, err := mongo.Databases(); err == nil {
|
||||
for item := range slices.Values(databases) {
|
||||
database = append(database, &biz.Database{
|
||||
Type: server.Type,
|
||||
Name: item.Name,
|
||||
Server: server.Name,
|
||||
ServerID: server.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
mongo.Close()
|
||||
case biz.DatabaseTypeSQLite:
|
||||
sqlite, err := db.NewSQLite(server.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if tables, err := sqlite.Tables(); err == nil {
|
||||
for table := range slices.Values(tables) {
|
||||
database = append(database, &biz.Database{
|
||||
Type: server.Type,
|
||||
Name: table,
|
||||
Server: server.Name,
|
||||
ServerID: server.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
sqlite.Close()
|
||||
default:
|
||||
operator, err := r.getOperator(server)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if databases, err := operator.Databases(); err == nil {
|
||||
for item := range slices.Values(databases) {
|
||||
database = append(database, &biz.Database{
|
||||
Type: server.Type,
|
||||
Name: item.Name,
|
||||
Server: server.Name,
|
||||
ServerID: server.ID,
|
||||
Encoding: item.CharSet,
|
||||
Comment: item.Comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
operator.Close()
|
||||
}
|
||||
|
||||
operator.Close()
|
||||
}
|
||||
|
||||
if len(database) < int((page-1)*limit) {
|
||||
@@ -79,6 +112,20 @@ func (r *databaseRepo) Create(ctx context.Context, req *request.DatabaseCreate)
|
||||
return err
|
||||
}
|
||||
|
||||
// MongoDB 独立处理,不走 Operator 接口
|
||||
if server.Type == biz.DatabaseTypeMongoDB {
|
||||
mongo, mongoErr := db.NewMongoDB(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if mongoErr != nil {
|
||||
return mongoErr
|
||||
}
|
||||
defer mongo.Close()
|
||||
if mongoErr = mongo.DatabaseCreate(req.Name); mongoErr != nil {
|
||||
return mongoErr
|
||||
}
|
||||
r.log.Info("database created", slog.String("type", biz.OperationTypeDatabase), slog.Uint64("operator_id", getOperatorID(ctx)), slog.String("name", req.Name), slog.Uint64("server_id", uint64(req.ServerID)))
|
||||
return nil
|
||||
}
|
||||
|
||||
operator, err := r.getOperator(server)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -127,6 +174,24 @@ func (r *databaseRepo) Create(ctx context.Context, req *request.DatabaseCreate)
|
||||
if err = operator.(*db.Postgres).DatabaseComment(req.Name, req.Comment); err != nil {
|
||||
return err
|
||||
}
|
||||
case biz.DatabaseTypeClickHouse:
|
||||
if req.CreateUser {
|
||||
if err = r.user.Create(ctx, &request.DatabaseUserCreate{
|
||||
ServerID: req.ServerID,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = operator.DatabaseCreate(req.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Username != "" {
|
||||
if err = operator.PrivilegesGrant(req.Username, req.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
@@ -141,14 +206,27 @@ func (r *databaseRepo) Delete(ctx context.Context, serverID uint, name string) e
|
||||
return err
|
||||
}
|
||||
|
||||
operator, err := r.getOperator(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer operator.Close()
|
||||
|
||||
if err = operator.DatabaseDrop(name); err != nil {
|
||||
return err
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMongoDB:
|
||||
mongo, mongoErr := db.NewMongoDB(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if mongoErr != nil {
|
||||
return mongoErr
|
||||
}
|
||||
defer mongo.Close()
|
||||
if mongoErr = mongo.DatabaseDrop(name); mongoErr != nil {
|
||||
return mongoErr
|
||||
}
|
||||
case biz.DatabaseTypeSQLite:
|
||||
return errors.New(r.t.Get("sqlite does not support dropping tables from here"))
|
||||
default:
|
||||
operator, opErr := r.getOperator(server)
|
||||
if opErr != nil {
|
||||
return opErr
|
||||
}
|
||||
defer operator.Close()
|
||||
if opErr = operator.DatabaseDrop(name); opErr != nil {
|
||||
return opErr
|
||||
}
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
@@ -163,20 +241,17 @@ func (r *databaseRepo) Comment(req *request.DatabaseComment) error {
|
||||
return err
|
||||
}
|
||||
|
||||
operator, err := r.getOperator(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer operator.Close()
|
||||
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql:
|
||||
return errors.New(r.t.Get("mysql not support database comment"))
|
||||
case biz.DatabaseTypePostgresql:
|
||||
operator, opErr := r.getOperator(server)
|
||||
if opErr != nil {
|
||||
return opErr
|
||||
}
|
||||
defer operator.Close()
|
||||
return operator.(*db.Postgres).DatabaseComment(req.Name, req.Comment)
|
||||
default:
|
||||
return errors.New(r.t.Get("%s does not support database comment", server.Type))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *databaseRepo) getOperator(server *biz.DatabaseServer) (db.Operator, error) {
|
||||
@@ -193,6 +268,12 @@ func (r *databaseRepo) getOperator(server *biz.DatabaseServer) (db.Operator, err
|
||||
return nil, err
|
||||
}
|
||||
return postgres, nil
|
||||
case biz.DatabaseTypeClickHouse:
|
||||
clickhouse, err := db.NewClickHouse(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clickhouse, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database type: %s", server.Type)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
)
|
||||
|
||||
type databaseElasticsearchRepo struct {
|
||||
t *gotext.Locale
|
||||
orm *gorm.DB
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewDatabaseElasticsearchRepo(t *gotext.Locale, orm *gorm.DB, log *slog.Logger) biz.DatabaseElasticsearchRepo {
|
||||
return &databaseElasticsearchRepo{
|
||||
t: t,
|
||||
orm: orm,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *databaseElasticsearchRepo) Indices(req *request.DatabaseESIndices) ([]db.ESIndex, error) {
|
||||
client, err := r.getClient(req.ServerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
return client.Indices()
|
||||
}
|
||||
|
||||
func (r *databaseElasticsearchRepo) IndexCreate(req *request.DatabaseESIndexCreate) error {
|
||||
client, err := r.getClient(req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
return client.IndexCreate(req.Name)
|
||||
}
|
||||
|
||||
func (r *databaseElasticsearchRepo) IndexDelete(req *request.DatabaseESIndexDelete) error {
|
||||
client, err := r.getClient(req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
return client.IndexDelete(req.Name)
|
||||
}
|
||||
|
||||
func (r *databaseElasticsearchRepo) Data(req *request.DatabaseESData) ([]db.ESDocument, int64, error) {
|
||||
client, err := r.getClient(req.ServerID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
return client.Search(req.Index, req.Search, int(req.Page), int(req.Limit))
|
||||
}
|
||||
|
||||
func (r *databaseElasticsearchRepo) DocumentGet(req *request.DatabaseESDocumentGet) (*db.ESDocument, error) {
|
||||
client, err := r.getClient(req.ServerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
return client.DocumentGet(req.Index, req.ID)
|
||||
}
|
||||
|
||||
func (r *databaseElasticsearchRepo) DocumentSet(req *request.DatabaseESDocumentSet) error {
|
||||
client, err := r.getClient(req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if req.ID == "" {
|
||||
return client.DocumentCreate(req.Index, req.Body)
|
||||
}
|
||||
return client.DocumentUpdate(req.Index, req.ID, req.Body)
|
||||
}
|
||||
|
||||
func (r *databaseElasticsearchRepo) DocumentDelete(req *request.DatabaseESDocumentDelete) error {
|
||||
client, err := r.getClient(req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
return client.DocumentDelete(req.Index, req.ID)
|
||||
}
|
||||
|
||||
// getClient 根据服务器 ID 创建 Elasticsearch 客户端
|
||||
func (r *databaseElasticsearchRepo) getClient(serverID uint) (*db.Elasticsearch, error) {
|
||||
server := new(biz.DatabaseServer)
|
||||
if err := r.orm.Where("id = ?", serverID).First(server).Error; err != nil {
|
||||
return nil, errors.New(r.t.Get("server not found"))
|
||||
}
|
||||
if server.Type != biz.DatabaseTypeElasticsearch {
|
||||
return nil, errors.New(r.t.Get("server is not Elasticsearch type"))
|
||||
}
|
||||
|
||||
client, err := db.NewElasticsearch(fmt.Sprintf("%s:%d", server.Host, server.Port), server.Username, server.Password)
|
||||
if err != nil {
|
||||
return nil, errors.New(r.t.Get("failed to connect to Elasticsearch: %v", err))
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -140,6 +140,12 @@ func (r *databaseServerRepo) Sync(id uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 非 Operator 类型不支持用户同步
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeRedis, biz.DatabaseTypeMongoDB, biz.DatabaseTypeSQLite, biz.DatabaseTypeElasticsearch:
|
||||
return fmt.Errorf("sync is not supported for %s", server.Type)
|
||||
}
|
||||
|
||||
users := make([]*biz.DatabaseUser, 0)
|
||||
if err = r.db.Where("server_id = ?", id).Find(&users).Error; err != nil {
|
||||
return err
|
||||
@@ -191,6 +197,25 @@ func (r *databaseServerRepo) Sync(id uint) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
case biz.DatabaseTypeClickHouse:
|
||||
allUsers, err := operator.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for user := range slices.Values(allUsers) {
|
||||
if !slices.ContainsFunc(users, func(a *biz.DatabaseUser) bool {
|
||||
return a.Username == user.User
|
||||
}) && !slices.Contains([]string{"default"}, user.User) {
|
||||
newUser := &biz.DatabaseUser{
|
||||
ServerID: id,
|
||||
Username: user.User,
|
||||
Remark: r.t.Get("sync from server %s", server.Name),
|
||||
}
|
||||
if err = r.db.Create(newUser).Error; err != nil {
|
||||
r.log.Warn("sync clickhouse database user failed", slog.String("type", biz.OperationTypeDatabaseServer), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -199,7 +224,7 @@ func (r *databaseServerRepo) Sync(id uint) error {
|
||||
// checkServer 检查服务器连接
|
||||
func (r *databaseServerRepo) checkServer(server *biz.DatabaseServer) bool {
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql, biz.DatabaseTypePostgresql:
|
||||
case biz.DatabaseTypeMysql, biz.DatabaseTypePostgresql, biz.DatabaseTypeClickHouse:
|
||||
operator, err := r.getOperator(server)
|
||||
if err == nil {
|
||||
operator.Close()
|
||||
@@ -213,6 +238,27 @@ func (r *databaseServerRepo) checkServer(server *biz.DatabaseServer) bool {
|
||||
server.Status = biz.DatabaseServerStatusValid
|
||||
return true
|
||||
}
|
||||
case biz.DatabaseTypeMongoDB:
|
||||
mongo, err := db.NewMongoDB(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err == nil {
|
||||
mongo.Close()
|
||||
server.Status = biz.DatabaseServerStatusValid
|
||||
return true
|
||||
}
|
||||
case biz.DatabaseTypeSQLite:
|
||||
sqlite, err := db.NewSQLite(server.Host)
|
||||
if err == nil {
|
||||
sqlite.Close()
|
||||
server.Status = biz.DatabaseServerStatusValid
|
||||
return true
|
||||
}
|
||||
case biz.DatabaseTypeElasticsearch:
|
||||
es, err := db.NewElasticsearch(fmt.Sprintf("%s:%d", server.Host, server.Port), server.Username, server.Password)
|
||||
if err == nil {
|
||||
es.Close()
|
||||
server.Status = biz.DatabaseServerStatusValid
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
server.Status = biz.DatabaseServerStatusInvalid
|
||||
@@ -233,6 +279,12 @@ func (r *databaseServerRepo) getOperator(server *biz.DatabaseServer) (db.Operato
|
||||
return nil, err
|
||||
}
|
||||
return postgres, nil
|
||||
case biz.DatabaseTypeClickHouse:
|
||||
clickhouse, err := db.NewClickHouse(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clickhouse, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database type: %s", server.Type)
|
||||
}
|
||||
|
||||
@@ -140,6 +140,14 @@ func (r *databaseUserRepo) Update(req *request.DatabaseUserUpdate) error {
|
||||
user.Password = req.Password
|
||||
}
|
||||
|
||||
// 撤销被移除的权限
|
||||
currentPrivileges, _ := operator.UserPrivileges(user.Username, user.Host)
|
||||
for name := range slices.Values(currentPrivileges) {
|
||||
if !slices.Contains(req.Privileges, name) {
|
||||
_ = operator.PrivilegesRevoke(user.Username, name, user.Host)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建数据库并授权
|
||||
for name := range slices.Values(req.Privileges) {
|
||||
if err = operator.DatabaseCreate(name); err != nil {
|
||||
@@ -223,7 +231,7 @@ func (r *databaseUserRepo) DeleteByNames(serverID uint, names []string) error {
|
||||
}
|
||||
_ = operator.UserDrop(name, host)
|
||||
}
|
||||
case biz.DatabaseTypePostgresql:
|
||||
case biz.DatabaseTypePostgresql, biz.DatabaseTypeClickHouse:
|
||||
for name := range slices.Values(names) {
|
||||
_ = operator.UserDrop(name)
|
||||
}
|
||||
@@ -257,6 +265,15 @@ func (r *databaseUserRepo) fillUser(user *biz.DatabaseUser) {
|
||||
} else {
|
||||
user.Status = biz.DatabaseUserStatusInvalid
|
||||
}
|
||||
case biz.DatabaseTypeClickHouse:
|
||||
privileges, _ := operator.UserPrivileges(user.Username)
|
||||
user.Privileges = privileges
|
||||
if ch2, err := db.NewClickHouse(user.Username, user.Password, fmt.Sprintf("%s:%d", server.Host, server.Port)); err == nil {
|
||||
ch2.Close()
|
||||
user.Status = biz.DatabaseUserStatusValid
|
||||
} else {
|
||||
user.Status = biz.DatabaseUserStatusInvalid
|
||||
}
|
||||
}
|
||||
} else {
|
||||
user.Status = biz.DatabaseUserStatusInvalid
|
||||
@@ -282,6 +299,12 @@ func (r *databaseUserRepo) getOperator(server *biz.DatabaseServer) (db.Operator,
|
||||
return nil, err
|
||||
}
|
||||
return postgres, nil
|
||||
case biz.DatabaseTypeClickHouse:
|
||||
clickhouse, err := db.NewClickHouse(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clickhouse, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database type: %s", server.Type)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -169,19 +170,15 @@ func (r *logRepo) fillOperatorNames(entries []biz.LogEntry) {
|
||||
}
|
||||
|
||||
// 批量查询用户名
|
||||
ids := make([]uint, 0, len(userIDs))
|
||||
for id := range userIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
ids := lo.Keys(userIDs)
|
||||
|
||||
var users []biz.User
|
||||
r.db.Select("id", "username").Where("id IN ?", ids).Find(&users)
|
||||
|
||||
// 构建ID到用户名的映射
|
||||
userMap := make(map[uint]string)
|
||||
for _, user := range users {
|
||||
userMap[user.ID] = user.Username
|
||||
}
|
||||
userMap := lo.SliceToMap(users, func(user biz.User) (uint, string) {
|
||||
return user.ID, user.Username
|
||||
})
|
||||
|
||||
// 填充用户名
|
||||
for i := range entries {
|
||||
|
||||
@@ -31,10 +31,15 @@ func (r monitorRepo) GetSetting() (*request.MonitorSetting, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
monitorInterval, err := r.setting.GetInt(biz.SettingKeyMonitorInterval, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setting := new(request.MonitorSetting)
|
||||
setting.Enabled = cast.ToBool(monitor)
|
||||
setting.Days = cast.ToUint(monitorDays)
|
||||
setting.Interval = uint(monitorInterval)
|
||||
|
||||
return setting, nil
|
||||
}
|
||||
@@ -46,6 +51,9 @@ func (r monitorRepo) UpdateSetting(setting *request.MonitorSetting) error {
|
||||
if err := r.setting.Set(biz.SettingKeyMonitorDays, cast.ToString(setting.Days)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.setting.Set(biz.SettingKeyMonitorInterval, cast.ToString(setting.Interval)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -61,19 +61,18 @@ func (r *projectRepo) List(typ types.ProjectType, page, limit uint) ([]*types.Pr
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
details := make([]*types.ProjectDetail, 0, len(projects))
|
||||
for _, p := range projects {
|
||||
details := lo.Map(projects, func(p *biz.Project, _ int) *types.ProjectDetail {
|
||||
detail, err := r.parseProjectDetail(p)
|
||||
if err != nil {
|
||||
// 如果解析失败,返回基本信息
|
||||
detail = &types.ProjectDetail{
|
||||
return &types.ProjectDetail{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
}
|
||||
}
|
||||
details = append(details, detail)
|
||||
}
|
||||
return detail
|
||||
})
|
||||
|
||||
return details, total, nil
|
||||
}
|
||||
|
||||
@@ -156,10 +156,37 @@ func (r *scanEventRepo) GetSetting() (*biz.ScanSetting, error) {
|
||||
_ = json.Unmarshal([]byte(interfacesStr), &interfaces)
|
||||
}
|
||||
|
||||
autoBlock, err := r.setting.GetBool(biz.SettingKeyScanAwareAutoBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockThreshold, err := r.setting.GetInt(biz.SettingKeyScanAwareBlockThreshold, 100)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockWindow, err := r.setting.GetInt(biz.SettingKeyScanAwareBlockWindow, 5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blockDuration, err := r.setting.GetInt(biz.SettingKeyScanAwareBlockDuration, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
whitelist, err := r.setting.GetSlice(biz.SettingKeyScanAwareWhitelist)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &biz.ScanSetting{
|
||||
Enabled: enabled,
|
||||
Days: uint(days),
|
||||
Interfaces: interfaces,
|
||||
Enabled: enabled,
|
||||
Days: uint(days),
|
||||
Interfaces: interfaces,
|
||||
AutoBlock: autoBlock,
|
||||
BlockThreshold: uint(blockThreshold),
|
||||
BlockWindow: uint(blockWindow),
|
||||
BlockDuration: uint(blockDuration),
|
||||
Whitelist: whitelist,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -175,7 +202,24 @@ func (r *scanEventRepo) UpdateSetting(setting *biz.ScanSetting) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.setting.Set(biz.SettingKeyScanAwareInterfaces, string(interfacesJSON))
|
||||
if err = r.setting.Set(biz.SettingKeyScanAwareInterfaces, string(interfacesJSON)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = r.setting.Set(biz.SettingKeyScanAwareAutoBlock, cast.ToString(setting.AutoBlock)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = r.setting.Set(biz.SettingKeyScanAwareBlockThreshold, cast.ToString(setting.BlockThreshold)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = r.setting.Set(biz.SettingKeyScanAwareBlockWindow, cast.ToString(setting.BlockWindow)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = r.setting.Set(biz.SettingKeyScanAwareBlockDuration, cast.ToString(setting.BlockDuration)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.setting.SetSlice(biz.SettingKeyScanAwareWhitelist, setting.Whitelist)
|
||||
}
|
||||
|
||||
func (r *scanEventRepo) Clear() error {
|
||||
|
||||
+47
-37
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/libtnb/cache"
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/firewall"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/os"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
)
|
||||
|
||||
const settingCacheTTL = 5 * time.Minute
|
||||
@@ -111,16 +113,10 @@ func (r *settingRepo) GetSlice(key biz.SettingKey, defaultValue ...[]string) ([]
|
||||
}
|
||||
|
||||
func (r *settingRepo) Set(key biz.SettingKey, value string) error {
|
||||
setting := new(biz.Setting)
|
||||
if err := r.db.Where("key = ?", key).First(setting).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
setting.Key = key
|
||||
setting.Value = value
|
||||
if err := r.db.Save(setting).Error; err != nil {
|
||||
if err := r.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "key"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"value"}),
|
||||
}).Create(&biz.Setting{Key: key, Value: value}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -129,30 +125,16 @@ func (r *settingRepo) Set(key biz.SettingKey, value string) error {
|
||||
}
|
||||
|
||||
func (r *settingRepo) SetSlice(key biz.SettingKey, value []string) error {
|
||||
setting := new(biz.Setting)
|
||||
if err := r.db.Where("key = ?", key).First(setting).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
setting.Key = key
|
||||
if len(value) == 0 {
|
||||
setting.Value = "[]"
|
||||
} else {
|
||||
v := "[]"
|
||||
if len(value) > 0 {
|
||||
b, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setting.Value = string(b)
|
||||
v = string(b)
|
||||
}
|
||||
|
||||
if err := r.db.Save(setting).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.cache.Forget(r.cacheKey(key))
|
||||
return nil
|
||||
return r.Set(key, v)
|
||||
}
|
||||
|
||||
func (r *settingRepo) Delete(key biz.SettingKey) error {
|
||||
@@ -190,6 +172,10 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backupFormat, err := r.Get(biz.SettingKeyBackupFormat, "tar.xz")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projectPath, err := r.Get(biz.SettingKeyProjectPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -246,6 +232,7 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
|
||||
BindUA: r.conf.HTTP.BindUA,
|
||||
WebsitePath: websitePath,
|
||||
BackupPath: backupPath,
|
||||
BackupFormat: backupFormat,
|
||||
ProjectPath: projectPath,
|
||||
ContainerSock: containerSock,
|
||||
HiddenMenu: hiddenMenu,
|
||||
@@ -254,8 +241,7 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
|
||||
IPDBURL: ipdbURL,
|
||||
IPDBPath: ipdbPath,
|
||||
Port: r.conf.HTTP.Port,
|
||||
HTTPS: r.conf.HTTP.TLS,
|
||||
ACME: r.conf.HTTP.ACME,
|
||||
TLS: r.conf.HTTP.TLS,
|
||||
PublicIP: publicIP,
|
||||
Cert: crt,
|
||||
Key: key,
|
||||
@@ -281,6 +267,9 @@ func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel
|
||||
if err := r.Set(biz.SettingKeyBackupPath, req.BackupPath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.Set(biz.SettingKeyBackupFormat, req.BackupFormat); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.Set(biz.SettingKeyProjectPath, req.ProjectPath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -326,6 +315,25 @@ func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel
|
||||
// 下面是需要需要重启的设置
|
||||
// 面板HTTPS
|
||||
restartFlag := false
|
||||
|
||||
// 自签模式
|
||||
if req.TLS == "self-signed" {
|
||||
needGen := req.Cert == "" || req.Key == ""
|
||||
if !needGen {
|
||||
if _, err := cert.ParseCert([]byte(req.Cert)); err != nil {
|
||||
needGen = true
|
||||
}
|
||||
}
|
||||
if needGen {
|
||||
crt, key, err := cert.GenerateSelfSigned(tools.CollectLocalNames())
|
||||
if err != nil {
|
||||
return false, errors.New(r.t.Get("failed to generate self-signed certificate: %v", err))
|
||||
}
|
||||
req.Cert = string(crt)
|
||||
req.Key = string(key)
|
||||
}
|
||||
}
|
||||
|
||||
oldCert, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
|
||||
oldKey, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.key"))
|
||||
if oldCert != req.Cert || oldKey != req.Key {
|
||||
@@ -334,11 +342,14 @@ func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel
|
||||
}
|
||||
restartFlag = true
|
||||
}
|
||||
if _, err := cert.ParseCert([]byte(req.Cert)); err != nil {
|
||||
return false, errors.New(r.t.Get("failed to parse certificate: %v", err))
|
||||
}
|
||||
if _, err := cert.ParseKey([]byte(req.Key)); err != nil {
|
||||
return false, errors.New(r.t.Get("failed to parse private key: %v", err))
|
||||
// custom 模式需要验证证书格式
|
||||
if req.TLS == "custom" {
|
||||
if _, err := cert.ParseCert([]byte(req.Cert)); err != nil {
|
||||
return false, errors.New(r.t.Get("failed to parse certificate: %v", err))
|
||||
}
|
||||
if _, err := cert.ParseKey([]byte(req.Key)); err != nil {
|
||||
return false, errors.New(r.t.Get("failed to parse private key: %v", err))
|
||||
}
|
||||
}
|
||||
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), req.Cert, 0600); err != nil {
|
||||
return false, err
|
||||
@@ -379,8 +390,7 @@ func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel
|
||||
conf.HTTP.Entrance = req.Entrance
|
||||
conf.HTTP.EntranceError = req.EntranceError
|
||||
conf.HTTP.LoginCaptcha = req.LoginCaptcha
|
||||
conf.HTTP.TLS = req.HTTPS
|
||||
conf.HTTP.ACME = req.ACME
|
||||
conf.HTTP.TLS = req.TLS
|
||||
conf.HTTP.IPHeader = req.IPHeader
|
||||
conf.HTTP.BindDomain = req.BindDomain
|
||||
conf.HTTP.BindIP = req.BindIP
|
||||
|
||||
+181
-18
@@ -1,14 +1,20 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
@@ -19,29 +25,44 @@ import (
|
||||
|
||||
type templateRepo struct {
|
||||
t *gotext.Locale
|
||||
log *slog.Logger
|
||||
cache biz.CacheRepo
|
||||
api *api.API
|
||||
firewall firewall.Firewall
|
||||
}
|
||||
|
||||
func NewTemplateRepo(t *gotext.Locale, cache biz.CacheRepo) biz.TemplateRepo {
|
||||
func NewTemplateRepo(t *gotext.Locale, log *slog.Logger, cache biz.CacheRepo) biz.TemplateRepo {
|
||||
return &templateRepo{
|
||||
t: t,
|
||||
log: log,
|
||||
cache: cache,
|
||||
api: api.NewAPI(app.Version, app.Locale),
|
||||
firewall: firewall.NewFirewall(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取所有模版
|
||||
// List 获取所有模版,包括本地模板
|
||||
func (r *templateRepo) List() api.Templates {
|
||||
cached, err := r.cache.Get(biz.CacheKeyTemplates)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
templates := make(api.Templates, 0)
|
||||
if err = json.Unmarshal([]byte(cached), &templates); err != nil {
|
||||
return nil
|
||||
cached, err := r.cache.Get(biz.CacheKeyTemplates)
|
||||
if err == nil {
|
||||
_ = json.Unmarshal([]byte(cached), &templates)
|
||||
}
|
||||
|
||||
// 加载本地模板并合并,本地模板覆盖同 slug 的远端模板
|
||||
localTemplates := r.loadLocalTemplates()
|
||||
if len(localTemplates) > 0 {
|
||||
slugMap := make(map[string]int, len(templates))
|
||||
for i, t := range templates {
|
||||
slugMap[t.Slug] = i
|
||||
}
|
||||
for _, lt := range localTemplates {
|
||||
if i, ok := slugMap[lt.Slug]; ok {
|
||||
templates[i] = lt
|
||||
} else {
|
||||
templates = append(templates, lt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return templates
|
||||
@@ -153,10 +174,10 @@ func (r *templateRepo) parsePortsFromCompose(compose string) []composePort {
|
||||
// 格式: host:container
|
||||
parts := strings.Split(portStr, ":")
|
||||
if len(parts) > 0 {
|
||||
port = parseUint(parts[0])
|
||||
port = cast.ToUint(parts[0])
|
||||
}
|
||||
} else {
|
||||
port = parseUint(portStr)
|
||||
port = cast.ToUint(portStr)
|
||||
}
|
||||
|
||||
if port > 0 && port <= 65535 {
|
||||
@@ -170,14 +191,156 @@ func (r *templateRepo) parsePortsFromCompose(compose string) []composePort {
|
||||
return ports
|
||||
}
|
||||
|
||||
func parseUint(s string) uint {
|
||||
var n uint
|
||||
for _, c := range s {
|
||||
if c >= '0' && c <= '9' {
|
||||
n = n*10 + uint(c-'0')
|
||||
} else {
|
||||
break
|
||||
// loadLocalTemplates 从本地目录加载模板
|
||||
func (r *templateRepo) loadLocalTemplates() api.Templates {
|
||||
dir := filepath.Join(app.Root, "panel/storage/templates")
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
r.log.Warn("failed to read templates directory", "path", dir, "err", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var templates api.Templates
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
slug := entry.Name()
|
||||
tplDir := filepath.Join(dir, slug)
|
||||
|
||||
// 读取 data.yml
|
||||
dataPath := filepath.Join(tplDir, "data.yml")
|
||||
dataBytes, err := os.ReadFile(dataPath)
|
||||
if err != nil {
|
||||
r.log.Warn("failed to read template data.yml", "path", dataPath, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var data types.TemplateData
|
||||
if err = yaml.Unmarshal(dataBytes, &data); err != nil {
|
||||
r.log.Warn("failed to parse template data.yml", "path", dataPath, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 读取 docker-compose.yml
|
||||
composePath := filepath.Join(tplDir, "docker-compose.yml")
|
||||
composeBytes, err := os.ReadFile(composePath)
|
||||
if err != nil {
|
||||
r.log.Warn("failed to read template docker-compose.yml", "path", composePath, "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 构建模板
|
||||
t := &api.Template{
|
||||
Slug: slug,
|
||||
Name: r.resolveLocale(data.Name),
|
||||
Description: r.resolveLocale(data.Description),
|
||||
Website: data.Website,
|
||||
Categories: data.Categories,
|
||||
Architectures: data.Architectures,
|
||||
Compose: string(composeBytes),
|
||||
Local: true,
|
||||
}
|
||||
|
||||
// 转换环境变量,从 map 格式转为数组格式,保留 YAML 定义顺序
|
||||
names := r.extractEnvKeyOrder(dataBytes, "environments")
|
||||
for _, name := range names {
|
||||
env := data.Environments[name]
|
||||
t.Environments = append(t.Environments, struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
Options map[string]string `json:"options,omitempty"`
|
||||
Default any `json:"default,omitempty"`
|
||||
}{
|
||||
Name: name,
|
||||
Description: r.resolveLocale(env.Description),
|
||||
Type: env.Type,
|
||||
Options: env.Options,
|
||||
Default: env.Default,
|
||||
})
|
||||
}
|
||||
|
||||
// 读取 logo
|
||||
if icon := r.readLogo(tplDir); icon != "" {
|
||||
t.Icon = icon
|
||||
}
|
||||
|
||||
templates = append(templates, t)
|
||||
}
|
||||
|
||||
return templates
|
||||
}
|
||||
|
||||
// resolveLocale 根据当前语言环境解析国际化字段
|
||||
func (r *templateRepo) resolveLocale(m map[string]string) string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
if v, ok := m[app.Locale]; ok {
|
||||
return v
|
||||
}
|
||||
if v, ok := m["en"]; ok {
|
||||
return v
|
||||
}
|
||||
// 按 key 排序取第一个,保证结果稳定
|
||||
keys := slices.Sorted(maps.Keys(m))
|
||||
if len(keys) > 0 {
|
||||
return m[keys[0]]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractEnvKeyOrder 从原始 YAML 数据中提取指定 mapping 字段的键定义顺序
|
||||
func (r *templateRepo) extractEnvKeyOrder(data []byte, field string) []string {
|
||||
var node yaml.Node
|
||||
if err := yaml.Unmarshal(data, &node); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.Kind != yaml.DocumentNode || len(node.Content) == 0 {
|
||||
return nil
|
||||
}
|
||||
root := node.Content[0]
|
||||
if root.Kind != yaml.MappingNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i+1 < len(root.Content); i += 2 {
|
||||
if root.Content[i].Value == field {
|
||||
m := root.Content[i+1]
|
||||
if m.Kind != yaml.MappingNode {
|
||||
return nil
|
||||
}
|
||||
keys := make([]string, 0, len(m.Content)/2)
|
||||
for j := 0; j+1 < len(m.Content); j += 2 {
|
||||
keys = append(keys, m.Content[j].Value)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
}
|
||||
return n
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readLogo 读取模板目录中的 logo 文件并返回 base64 data URI
|
||||
func (r *templateRepo) readLogo(dir string) string {
|
||||
candidates := []struct {
|
||||
name string
|
||||
mime string
|
||||
}{
|
||||
{"logo.svg", "image/svg+xml"},
|
||||
{"logo.png", "image/png"},
|
||||
}
|
||||
for _, c := range candidates {
|
||||
data, err := os.ReadFile(filepath.Join(dir, c.name))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return "data:" + c.mime + ";base64," + base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
)
|
||||
|
||||
type userPasskeyRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserPasskeyRepo(db *gorm.DB) biz.UserPasskeyRepo {
|
||||
return &userPasskeyRepo{db: db}
|
||||
}
|
||||
|
||||
func (r userPasskeyRepo) List(userID uint) ([]*biz.UserPasskey, error) {
|
||||
var passkeys []*biz.UserPasskey
|
||||
if err := r.db.Where("user_id = ?", userID).Order("id desc").Find(&passkeys).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return passkeys, nil
|
||||
}
|
||||
|
||||
func (r userPasskeyRepo) Create(passkey *biz.UserPasskey) error {
|
||||
return r.db.Create(passkey).Error
|
||||
}
|
||||
|
||||
func (r userPasskeyRepo) UpdateSignCount(credentialID []byte, signCount uint32) error {
|
||||
return r.db.Model(&biz.UserPasskey{}).Where("credential_id = ?", credentialID).Update("sign_count", signCount).Error
|
||||
}
|
||||
|
||||
func (r userPasskeyRepo) UpdateLastUsed(credentialID []byte) error {
|
||||
now := time.Now()
|
||||
return r.db.Model(&biz.UserPasskey{}).Where("credential_id = ?", credentialID).Update("last_used_at", &now).Error
|
||||
}
|
||||
|
||||
func (r userPasskeyRepo) Delete(userID, id uint) error {
|
||||
return r.db.Where("user_id = ? AND id = ?", userID, id).Delete(&biz.UserPasskey{}).Error
|
||||
}
|
||||
|
||||
func (r userPasskeyRepo) GetByCredentialID(credentialID []byte) (*biz.UserPasskey, *biz.User, error) {
|
||||
passkey := new(biz.UserPasskey)
|
||||
if err := r.db.Where("credential_id = ?", credentialID).First(passkey).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
user := new(biz.User)
|
||||
if err := r.db.First(user, passkey.UserID).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return passkey, user, nil
|
||||
}
|
||||
|
||||
func (r userPasskeyRepo) HasPasskey(userID uint) (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.Model(&biz.UserPasskey{}).Where("user_id = ?", userID).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r userPasskeyRepo) HasAny() (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.Model(&biz.UserPasskey{}).Limit(1).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r userPasskeyRepo) DeleteAllByUserID(userID uint) error {
|
||||
return r.db.Where("user_id = ?", userID).Delete(&biz.UserPasskey{}).Error
|
||||
}
|
||||
+17
-22
@@ -114,9 +114,6 @@ func (r *websiteRepo) UpdateDefaultConfig(req *request.WebsiteDefaultConfig) err
|
||||
if err = r.setting.SetSlice(biz.SettingKeyWebsiteTLSVersions, req.TLSVersions); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = r.setting.Set(biz.SettingKeyWebsiteCipherSuites, req.CipherSuites); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.reloadWebServer()
|
||||
}
|
||||
@@ -173,7 +170,6 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
|
||||
setting.HSTS = sslConfig.HSTS
|
||||
setting.OCSP = sslConfig.OCSP
|
||||
setting.SSLProtocols = sslConfig.Protocols
|
||||
setting.SSLCiphers = sslConfig.Ciphers
|
||||
}
|
||||
// 证书
|
||||
crt, _ := os.ReadFile(filepath.Join(app.Root, "sites", website.Name, "config", "fullchain.pem"))
|
||||
@@ -372,7 +368,7 @@ func (r *websiteRepo) Create(ctx context.Context, req *request.WebsiteCreate) (*
|
||||
if err = phpVhost.SetIndex([]string{"index.php", "index.html"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = phpVhost.SetConfig("010-rewrite.conf", "site", ""); err != nil {
|
||||
if err = phpVhost.SetConfig("010-rewrite.conf", "site", "", true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cacheConfig string
|
||||
@@ -646,12 +642,10 @@ func (r *websiteRepo) Update(ctx context.Context, req *request.WebsiteUpdate) er
|
||||
}
|
||||
}
|
||||
defaultTLSVersions, _ := r.setting.GetSlice(biz.SettingKeyWebsiteTLSVersions)
|
||||
defaultCipherSuites, _ := r.setting.Get(biz.SettingKeyWebsiteCipherSuites)
|
||||
if err = vhost.SetSSLConfig(&webservertypes.SSLConfig{
|
||||
Cert: certPath,
|
||||
Key: keyPath,
|
||||
Protocols: lo.If(len(req.SSLProtocols) > 0, req.SSLProtocols).Else(defaultTLSVersions),
|
||||
Ciphers: lo.If(req.SSLCiphers != "", req.SSLCiphers).Else(defaultCipherSuites),
|
||||
HSTS: req.HSTS,
|
||||
OCSP: req.OCSP,
|
||||
HTTPRedirect: req.HTTPRedirect,
|
||||
@@ -671,7 +665,7 @@ func (r *websiteRepo) Update(ctx context.Context, req *request.WebsiteUpdate) er
|
||||
return err
|
||||
}
|
||||
// 伪静态
|
||||
if err = phpVhost.SetConfig("010-rewrite.conf", "site", req.Rewrite); err != nil {
|
||||
if err = phpVhost.SetConfig("010-rewrite.conf", "site", req.Rewrite, true); err != nil {
|
||||
return err
|
||||
}
|
||||
// 防跨站
|
||||
@@ -834,16 +828,6 @@ func (r *websiteRepo) Delete(ctx context.Context, req *request.WebsiteDelete) er
|
||||
return r.reloadWebServer()
|
||||
}
|
||||
|
||||
func (r *websiteRepo) ClearLog(id uint) error {
|
||||
website := new(biz.Website)
|
||||
if err := r.db.Where("id", id).First(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := shell.Execf(`cat /dev/null > %s/sites/%s/log/access.log`, app.Root, website.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *websiteRepo) UpdateRemark(id uint, remark string) error {
|
||||
website := new(biz.Website)
|
||||
if err := r.db.Where("id", id).First(website).Error; err != nil {
|
||||
@@ -959,6 +943,10 @@ func (r *websiteRepo) UpdateStatus(id uint, status bool) error {
|
||||
return r.reloadWebServer()
|
||||
}
|
||||
|
||||
func (r *websiteRepo) UpdateExpireAt(id uint, expireAt *time.Time) error {
|
||||
return r.db.Model(&biz.Website{}).Where("id = ?", id).Update("expire_at", expireAt).Error
|
||||
}
|
||||
|
||||
func (r *websiteRepo) UpdateCert(req *request.WebsiteUpdateCert) error {
|
||||
website := new(biz.Website)
|
||||
if err := r.db.Where("name", req.Name).First(&website).Error; err != nil {
|
||||
@@ -988,13 +976,18 @@ func (r *websiteRepo) UpdateCert(req *request.WebsiteUpdateCert) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
|
||||
func (r *websiteRepo) ObtainCert(ctx context.Context, id uint, dnsID uint) error {
|
||||
website, err := r.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if slices.Contains(website.Domains, "*") {
|
||||
return errors.New(r.t.Get("not support one-key obtain wildcard certificate, please use Cert menu to obtain it with DNS method"))
|
||||
|
||||
// 泛域名必须使用 DNS 验证
|
||||
hasWildcard := slices.ContainsFunc(website.Domains, func(d string) bool {
|
||||
return strings.Contains(d, "*")
|
||||
})
|
||||
if hasWildcard && dnsID == 0 {
|
||||
return errors.New(r.t.Get("wildcard domains require DNS verification, please select a DNS provider"))
|
||||
}
|
||||
|
||||
account, err := r.certAccount.GetDefault(cast.ToUint(ctx.Value("user_id")))
|
||||
@@ -1010,6 +1003,7 @@ func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
|
||||
Domains: website.Domains,
|
||||
AutoRenewal: true,
|
||||
AccountID: account.ID,
|
||||
DNSID: dnsID,
|
||||
WebsiteID: website.ID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -1020,6 +1014,7 @@ func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
|
||||
}
|
||||
}
|
||||
newCert.Domains = website.Domains
|
||||
newCert.DNSID = dnsID
|
||||
if err = r.db.Save(newCert).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1029,7 +1024,7 @@ func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.cert.Deploy(newCert.ID, website.ID)
|
||||
return r.cert.Deploy(newCert.ID, website.ID, false)
|
||||
}
|
||||
|
||||
// customConfigStartNum 自定义配置起始序号
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
)
|
||||
@@ -27,13 +28,10 @@ func MustInstall(t *gotext.Locale, app biz.AppRepo) func(next http.Handler) http
|
||||
slugs = append(slugs, pathArr[3])
|
||||
}
|
||||
|
||||
flag := false
|
||||
for _, s := range slugs {
|
||||
if installed, _ := app.IsInstalled("slug = ?", s); installed {
|
||||
flag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
flag := lo.SomeBy(slugs, func(s string) bool {
|
||||
installed, _ := app.IsInstalled("slug = ?", s)
|
||||
return installed
|
||||
})
|
||||
if !flag && len(slugs) > 0 {
|
||||
Abort(w, http.StatusForbidden, t.Get("app %s not installed", slugs))
|
||||
return
|
||||
|
||||
@@ -26,6 +26,8 @@ func MustLogin(t *gotext.Locale, conf *config.Config, session *sessions.Manager,
|
||||
"/api/user/captcha",
|
||||
"/api/user/login",
|
||||
"/api/user/logout",
|
||||
"/api/user/passkey/enabled",
|
||||
"/api/user/passkey/login",
|
||||
"/api/user/is_login",
|
||||
"/api/user/is_2fa",
|
||||
"/api/home/panel",
|
||||
@@ -95,7 +97,7 @@ func MustLogin(t *gotext.Locale, conf *config.Config, session *sessions.Manager,
|
||||
Expires: time.Now().Add(time.Duration(session.Lifetime) * time.Minute),
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: conf.HTTP.TLS,
|
||||
Secure: conf.HTTP.IsHTTPS(),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ package request
|
||||
import "mime/multipart"
|
||||
|
||||
type BackupList struct {
|
||||
Type string `uri:"type" form:"type" validate:"required|in:path,website,mysql,postgres,redis,panel"`
|
||||
Type string `uri:"type" form:"type" validate:"required|in:path,website,mysql,postgresql,redis,panel"`
|
||||
}
|
||||
|
||||
type BackupCreate struct {
|
||||
Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"`
|
||||
Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgresql,redis,panel"`
|
||||
Target string `json:"target" form:"target" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Storage uint `form:"storage" json:"storage"`
|
||||
}
|
||||
@@ -18,7 +18,7 @@ type BackupUpload struct {
|
||||
}
|
||||
|
||||
type BackupFile struct {
|
||||
Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"`
|
||||
Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgresql,redis,panel"`
|
||||
File string `json:"file" form:"file" validate:"required"`
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ type CertUpload struct {
|
||||
}
|
||||
|
||||
type CertCreate struct {
|
||||
Type string `form:"type" json:"type" validate:"required|in:P256,P384,2048,3072,4096"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"required|isSlice"`
|
||||
AutoRenewal bool `form:"auto_renewal" json:"auto_renewal"`
|
||||
AccountID uint `form:"account_id" json:"account_id"`
|
||||
DNSID uint `form:"dns_id" json:"dns_id"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id"`
|
||||
Type string `form:"type" json:"type" validate:"required|in:P256,P384,2048,3072,4096"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"required|isSlice"`
|
||||
Alias map[string]string `form:"alias" json:"alias"`
|
||||
AutoRenewal bool `form:"auto_renewal" json:"auto_renewal"`
|
||||
AccountID uint `form:"account_id" json:"account_id"`
|
||||
DNSID uint `form:"dns_id" json:"dns_id"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id"`
|
||||
}
|
||||
|
||||
func (r *CertCreate) Rules(_ *http.Request) map[string]string {
|
||||
@@ -23,16 +24,17 @@ func (r *CertCreate) Rules(_ *http.Request) map[string]string {
|
||||
}
|
||||
|
||||
type CertUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:certs,id"`
|
||||
Type string `form:"type" json:"type" validate:"required|in:P256,P384,2048,3072,4096,upload"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"required|isSlice"`
|
||||
Cert string `form:"cert" json:"cert"`
|
||||
Key string `form:"key" json:"key"`
|
||||
Script string `form:"script" json:"script"`
|
||||
AutoRenewal bool `form:"auto_renewal" json:"auto_renewal"`
|
||||
AccountID uint `form:"account_id" json:"account_id"`
|
||||
DNSID uint `form:"dns_id" json:"dns_id"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id"`
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:certs,id"`
|
||||
Type string `form:"type" json:"type" validate:"required|in:P256,P384,2048,3072,4096,upload"`
|
||||
Domains []string `form:"domains" json:"domains" validate:"required|isSlice"`
|
||||
Alias map[string]string `form:"alias" json:"alias"`
|
||||
Cert string `form:"cert" json:"cert"`
|
||||
Key string `form:"key" json:"key"`
|
||||
Script string `form:"script" json:"script"`
|
||||
AutoRenewal bool `form:"auto_renewal" json:"auto_renewal"`
|
||||
AccountID uint `form:"account_id" json:"account_id"`
|
||||
DNSID uint `form:"dns_id" json:"dns_id"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id"`
|
||||
}
|
||||
|
||||
func (r *CertUpdate) Rules(_ *http.Request) map[string]string {
|
||||
@@ -42,6 +44,7 @@ func (r *CertUpdate) Rules(_ *http.Request) map[string]string {
|
||||
}
|
||||
|
||||
type CertDeploy struct {
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:certs,id"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id" validate:"required|exists:websites,id"`
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:certs,id"`
|
||||
WebsiteID uint `form:"website_id" json:"website_id" validate:"required|exists:websites,id"`
|
||||
EnableHTTPS bool `form:"enable_https" json:"enable_https"`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package request
|
||||
|
||||
type CertAccountCreate struct {
|
||||
CA string `form:"ca" json:"ca" validate:"required|in:googlecn,google,letsencrypt,litessl,buypass,zerossl,sslcom"`
|
||||
CA string `form:"ca" json:"ca" validate:"required|in:googlecn,google,letsencrypt,litessl,zerossl,sslcom"`
|
||||
Email string `form:"email" json:"email" validate:"required"`
|
||||
Kid string `form:"kid" json:"kid"`
|
||||
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
|
||||
@@ -10,7 +10,7 @@ type CertAccountCreate struct {
|
||||
|
||||
type CertAccountUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:cert_accounts,id"`
|
||||
CA string `form:"ca" json:"ca" validate:"required|in:googlecn,google,letsencrypt,litessl,buypass,zerossl,sslcom"`
|
||||
CA string `form:"ca" json:"ca" validate:"required|in:googlecn,google,letsencrypt,litessl,zerossl,sslcom"`
|
||||
Email string `form:"email" json:"email" validate:"required"`
|
||||
Kid string `form:"kid" json:"kid"`
|
||||
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
|
||||
|
||||
@@ -7,9 +7,9 @@ type DatabaseList struct {
|
||||
|
||||
type DatabaseCreate struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z_-][a-zA-Z0-9_-]*$"`
|
||||
CreateUser bool `form:"create_user" json:"create_user"`
|
||||
Username string `form:"username" json:"username" validate:"requiredIf:CreateUser,true|not_in:root,admin|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Username string `form:"username" json:"username" validate:"requiredIf:CreateUser,true|not_in:root,admin|regex:^[a-zA-Z_-][a-zA-Z0-9_-]*$"`
|
||||
Password string `form:"password" json:"password" validate:"requiredIf:CreateUser,true"`
|
||||
Host string `form:"host" json:"host"`
|
||||
Comment string `form:"comment" json:"comment"`
|
||||
@@ -17,11 +17,11 @@ type DatabaseCreate struct {
|
||||
|
||||
type DatabaseDelete struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z_-][a-zA-Z0-9_-]*$"`
|
||||
}
|
||||
|
||||
type DatabaseComment struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z_-][a-zA-Z0-9_-]*$"`
|
||||
Comment string `form:"comment" json:"comment"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package request
|
||||
|
||||
// DatabaseESIndices 获取索引列表
|
||||
type DatabaseESIndices struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required|exists:database_servers,id"`
|
||||
}
|
||||
|
||||
// DatabaseESIndexCreate 创建索引
|
||||
type DatabaseESIndexCreate struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
// DatabaseESIndexDelete 删除索引
|
||||
type DatabaseESIndexDelete struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
// DatabaseESData 搜索文档列表
|
||||
type DatabaseESData struct {
|
||||
Paginate
|
||||
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required|exists:database_servers,id"`
|
||||
Index string `form:"index" json:"index" query:"index" validate:"required"`
|
||||
Search string `form:"search" json:"search" query:"search"`
|
||||
}
|
||||
|
||||
// DatabaseESDocumentGet 获取文档
|
||||
type DatabaseESDocumentGet struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required|exists:database_servers,id"`
|
||||
Index string `form:"index" json:"index" query:"index" validate:"required"`
|
||||
ID string `form:"id" json:"id" query:"id" validate:"required"`
|
||||
}
|
||||
|
||||
// DatabaseESDocumentSet 创建/更新文档
|
||||
type DatabaseESDocumentSet struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
|
||||
Index string `form:"index" json:"index" validate:"required"`
|
||||
ID string `form:"id" json:"id"`
|
||||
Body string `form:"body" json:"body" validate:"required"`
|
||||
}
|
||||
|
||||
// DatabaseESDocumentDelete 删除文档
|
||||
type DatabaseESDocumentDelete struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
|
||||
Index string `form:"index" json:"index" validate:"required"`
|
||||
ID string `form:"id" json:"id" validate:"required"`
|
||||
}
|
||||
@@ -2,9 +2,9 @@ package request
|
||||
|
||||
type DatabaseServerCreate struct {
|
||||
Name string `form:"name" json:"name" validate:"required|notExists:database_servers,name|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Type string `form:"type" json:"type" validate:"required|in:mysql,postgresql,redis"`
|
||||
Type string `form:"type" json:"type" validate:"required|in:mysql,postgresql,redis,clickhouse,mongodb,sqlite,elasticsearch"`
|
||||
Host string `form:"host" json:"host" validate:"required"`
|
||||
Port uint `form:"port" json:"port" validate:"required|min:1|max:65535"`
|
||||
Port uint `form:"port" json:"port" validate:"max:65535"`
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
Remark string `form:"remark" json:"remark"`
|
||||
@@ -14,7 +14,7 @@ type DatabaseServerUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:database_servers,id"`
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Host string `form:"host" json:"host" validate:"required"`
|
||||
Port uint `form:"port" json:"port" validate:"required|min:1|max:65535"`
|
||||
Port uint `form:"port" json:"port" validate:"max:65535"`
|
||||
Username string `form:"username" json:"username"`
|
||||
Password string `form:"password" json:"password"`
|
||||
Remark string `form:"remark" json:"remark"`
|
||||
|
||||
@@ -2,7 +2,7 @@ package request
|
||||
|
||||
type DatabaseUserCreate struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
|
||||
Username string `form:"username" json:"username" validate:"required|not_in:root,admin|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Username string `form:"username" json:"username" validate:"required|not_in:root,admin|regex:^[a-zA-Z_-][a-zA-Z0-9_-]*$"`
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Host string `form:"host" json:"host"`
|
||||
Privileges []string `form:"privileges" json:"privileges"`
|
||||
|
||||
@@ -22,6 +22,18 @@ type FilePath struct {
|
||||
Path string `json:"path" form:"path" validate:"required|isUnixPath"`
|
||||
}
|
||||
|
||||
type FileTail struct {
|
||||
Path string `json:"path" form:"path"`
|
||||
Service string `json:"service" form:"service"`
|
||||
Offset int `json:"offset" form:"offset"`
|
||||
Limit int `json:"limit" form:"limit"`
|
||||
}
|
||||
|
||||
type FileFollow struct {
|
||||
Path string `json:"path" form:"path"`
|
||||
Service string `json:"service" form:"service"`
|
||||
}
|
||||
|
||||
type FileCreate struct {
|
||||
Dir bool `json:"dir" form:"dir"`
|
||||
Path string `json:"path" form:"path" validate:"required|isUnixPath"`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user