mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 20:05:34 +08:00
Compare commits
228 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da7c93308b | |||
| 017c0ed9b7 | |||
| 0930007d24 | |||
| 8285b8dee8 | |||
| c52d42a2de | |||
| 5d07932226 | |||
| 89ad4d83f3 | |||
| cce1102c90 | |||
| f67a3e3a6f | |||
| 7e09e20254 | |||
| 6fd6c958cf | |||
| 7fe0c3ce3c | |||
| 086d0eb601 | |||
| d7b7cf0b57 | |||
| 78a1a43f2f | |||
| f9c1510d8a | |||
| 0ea2117d96 | |||
| 2b055d6e88 | |||
| 227a7b19f3 | |||
| eda6d92d66 | |||
| c2441d2081 | |||
| 47123317a2 | |||
| 68125fb1e6 | |||
| acecdf7f48 | |||
| 021646727b | |||
| 4e6d46d1e9 | |||
| 1265ac5b34 | |||
| 94704deb1b | |||
| 932f48044b | |||
| 0c89d77221 | |||
| e0ccab8eb7 | |||
| 3aef5140e2 | |||
| 10ca20f3aa | |||
| 89a55ca518 | |||
| d743bef697 | |||
| 50fe450d64 | |||
| 74259f833c | |||
| ca8e36351e | |||
| d01a2374f5 | |||
| f4885c55f9 | |||
| 7e8de43776 | |||
| 4ded231b1c | |||
| 20a0080b1e | |||
| d2b30f211f | |||
| 83311409ae | |||
| 515ed686fb | |||
| 975c0d7321 | |||
| 0bdf1d6058 | |||
| c5d62a46ab | |||
| 2dd6f0d9f0 | |||
| de5d76bff6 | |||
| e3742e61d8 | |||
| 72c31a14b2 | |||
| 205d95a591 | |||
| 4df5038f91 | |||
| da023c5faa | |||
| e6c13330d8 | |||
| bda95465f8 | |||
| c3074249f0 | |||
| 6388bcfa37 | |||
| 281f35dfa1 | |||
| 5d189172ca | |||
| aa23066fe5 | |||
| c3313845a6 | |||
| 85f86f51db | |||
| 374996498f | |||
| a17b9d4936 | |||
| c226091c6e | |||
| 8902dade39 | |||
| 5f9696f214 | |||
| 7155e7c7cb | |||
| c56b4e00d9 | |||
| f7bf8b01cc | |||
| b7e1bd8824 | |||
| d9b6813060 | |||
| 10a816f7f7 | |||
| 68e4016538 | |||
| 338b9cc73b | |||
| b27ced4b92 | |||
| 36fb9e76fc | |||
| 5f7df1ef9a | |||
| 7da411b2f3 | |||
| 3c6c5aa31e | |||
| 5c05d0d81b | |||
| 0724a042ef | |||
| 261d87d6df | |||
| f043f65f95 | |||
| 0e56569dab | |||
| 8ccefa6bff | |||
| f3b8069710 | |||
| 8bbddef429 | |||
| 296930d36c | |||
| ae8e1458ea | |||
| 9625b28ee8 | |||
| bcff241c95 | |||
| b4df083a19 | |||
| ab756ed2bd | |||
| 5cd1af9c2a | |||
| dc8bd635d4 | |||
| 9cec03a69a | |||
| 5096339ab4 | |||
| d20b940d24 | |||
| 76abe1769b | |||
| 0d0c38a225 | |||
| 5ede0161c1 | |||
| 9378546cc4 | |||
| 33b0ee9d1e | |||
| 5994595198 | |||
| c84ab39f16 | |||
| 4760c89a06 | |||
| a10a06a544 | |||
| f1a9f5a4bc | |||
| 6466868e08 | |||
| fd65c267b3 | |||
| 5ee2409b87 | |||
| fca72a7e35 | |||
| 7b971c7920 | |||
| dbeb6b5fa9 | |||
| 402388c2d3 | |||
| 58c5a87262 | |||
| c4cd926622 | |||
| debc34f236 | |||
| 2c06ae9293 | |||
| bd7cb16c3a | |||
| 9b36edd1a9 | |||
| d5c0e03f16 | |||
| 50a5b42b85 | |||
| 57accdb5cf | |||
| f6cf1daae5 | |||
| 9cbf7b3f9a | |||
| 9700e432fd | |||
| efb427ff9c | |||
| 7ac05c5e29 | |||
| 181b262a61 | |||
| f7b63e2f6e | |||
| 5fc8fb1c1f | |||
| 805a7ea773 | |||
| c8bfe92c5e | |||
| 4b26e190f5 | |||
| 78be22b3dc | |||
| 1909865f04 | |||
| d7e8301148 | |||
| 54b1ddb809 | |||
| 30bf14c40c | |||
| 9ab01dc64b | |||
| 58ddd55b7c | |||
| 4f19a58bba | |||
| 15f096191e | |||
| 6f1b177642 | |||
| f0ae6278f1 | |||
| 2fdb23a568 | |||
| ba12def9f2 | |||
| c5667d8ce4 | |||
| 6b1789421f | |||
| cef8d7caa7 | |||
| 2d3250bc51 | |||
| 37f50eebd2 | |||
| 676bed3a41 | |||
| bee7da8a7e | |||
| 9cfa4738cf | |||
| 39d015b1cc | |||
| b9a9bf7904 | |||
| c35c26b237 | |||
| d25c419462 | |||
| 309f6b7ee2 | |||
| 7de7eabca3 | |||
| ce83fdd4db | |||
| 7ef6a5df0e | |||
| 624bac6ead | |||
| 2e4a5d3b43 | |||
| a6e4c17b83 | |||
| f3e31ab518 | |||
| 42facd50c3 | |||
| 706bee0e43 | |||
| d0c4a106fb | |||
| 571a63325b | |||
| 4ae47f03ef | |||
| 9e24a953c1 | |||
| 64ef88c28e | |||
| 43b92ceb4e | |||
| c2c3aa48f1 | |||
| be7996969a | |||
| eeb4aaee90 | |||
| fb7c56dc01 | |||
| 15abaa875d | |||
| 0ab2bbe92a | |||
| dd7d1ed490 | |||
| c9783118f5 | |||
| 626342d13b | |||
| 15287b5bce | |||
| f0f4b4f692 | |||
| ea0dccb6c1 | |||
| 6a73f6e333 | |||
| b5c9f3bfb9 | |||
| 6458957650 | |||
| bf8ca47c62 | |||
| e7fa6b6fb0 | |||
| 6f0b10653b | |||
| c9bc8aa193 | |||
| 6870388d07 | |||
| 8f9a32dfb0 | |||
| f87658f175 | |||
| 369ef07f19 | |||
| 6a3e0e547a | |||
| 3ba9b8433d | |||
| bd03013392 | |||
| bd746fe766 | |||
| eca6ae07be | |||
| ab6d6c6ba2 | |||
| 3547157850 | |||
| 01bda75ce5 | |||
| 2cfd5a26a9 | |||
| fd583ec208 | |||
| af2d856576 | |||
| d4a6b5e207 | |||
| ef6b83f425 | |||
| 73d3a5d86b | |||
| 8f9ff80fbf | |||
| 3ef55c5241 | |||
| 6db0035c43 | |||
| fd27f198b9 | |||
| e735d63c56 | |||
| c23d13437d | |||
| 4424d8ba18 | |||
| 74a1fd2258 | |||
| 84f5ea815b | |||
| 0fc6104d89 | |||
| 05ee59c51c |
@@ -1,6 +1,164 @@
|
||||
## 角色定义
|
||||
|
||||
你是 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
|
||||
|
||||
@@ -13,11 +171,13 @@ AcePanel 是基于 Go 语言开发的新一代 Linux 服务器运维管理面板
|
||||
### 后端构建
|
||||
|
||||
构建主程序:
|
||||
|
||||
```bash
|
||||
go build -o ace ./cmd/ace
|
||||
```
|
||||
|
||||
构建 CLI 工具:
|
||||
|
||||
```bash
|
||||
go build -o cli ./cmd/cli
|
||||
```
|
||||
@@ -25,21 +185,25 @@ go build -o cli ./cmd/cli
|
||||
### 前端开发
|
||||
|
||||
进入前端目录:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
```
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
开发模式(带热重载):
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
构建生产版本:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
@@ -148,6 +312,7 @@ pnpm build
|
||||
### 助手函数(service 层)
|
||||
|
||||
在 service 层使用以下助手函数:
|
||||
|
||||
- `Success(w, data)`: 返回成功响应
|
||||
- `Error(w, statusCode, format, args...)`: 返回错误响应
|
||||
- `ErrorSystem(w, format, args...)`: 返回系统严重错误(500)
|
||||
@@ -170,6 +335,8 @@ pnpm build
|
||||
- 使用 `gofmt` 格式化代码
|
||||
- 复杂逻辑添加注释说明
|
||||
- 导出的函数和类型必须有注释
|
||||
- service 层以及 data 层方法对外返回的字符串需要使用 gotext 进行国际化处理
|
||||
- 禁止手动编辑国际化文件,项目使用外部 Crowdin 自动化管理
|
||||
|
||||
## Wire 依赖注入
|
||||
|
||||
@@ -177,6 +344,7 @@ pnpm build
|
||||
|
||||
1. 在 `cmd/ace/wire.go` 或 `cmd/cli/wire.go` 中添加 provider
|
||||
2. 运行生成命令:
|
||||
|
||||
```bash
|
||||
go generate ./...
|
||||
```
|
||||
@@ -190,17 +358,31 @@ go generate ./...
|
||||
- 图标:@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 上的实际使用案例
|
||||
|
||||
@@ -2,9 +2,167 @@
|
||||
|
||||
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
|
||||
|
||||
@@ -17,11 +175,13 @@ AcePanel 是基于 Go 语言开发的新一代 Linux 服务器运维管理面板
|
||||
### 后端构建
|
||||
|
||||
构建主程序:
|
||||
|
||||
```bash
|
||||
go build -o ace ./cmd/ace
|
||||
```
|
||||
|
||||
构建 CLI 工具:
|
||||
|
||||
```bash
|
||||
go build -o cli ./cmd/cli
|
||||
```
|
||||
@@ -29,21 +189,25 @@ go build -o cli ./cmd/cli
|
||||
### 前端开发
|
||||
|
||||
进入前端目录:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
```
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
开发模式(带热重载):
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
构建生产版本:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
@@ -55,35 +219,35 @@ pnpm build
|
||||
### 核心目录结构
|
||||
|
||||
- **`cmd/`**: 程序入口
|
||||
- `ace/`: 面板主程序
|
||||
- `cli/`: 命令行工具
|
||||
- `ace/`: 面板主程序
|
||||
- `cli/`: 命令行工具
|
||||
|
||||
- **`internal/app/`**: 应用入口和配置
|
||||
|
||||
- **`internal/route/`**: HTTP 路由定义
|
||||
- 定义路由规则
|
||||
- 注入所需的 service 依赖
|
||||
- 定义路由规则
|
||||
- 注入所需的 service 依赖
|
||||
|
||||
- **`internal/service/`**: 服务层(类似 DDD 的 application 层)
|
||||
- 处理 HTTP 请求/响应
|
||||
- DTO 到 DO 的转换
|
||||
- 协调多个 biz 接口完成业务流程
|
||||
- **不应处理复杂业务逻辑**
|
||||
- 处理 HTTP 请求/响应
|
||||
- DTO 到 DO 的转换
|
||||
- 协调多个 biz 接口完成业务流程
|
||||
- **不应处理复杂业务逻辑**
|
||||
|
||||
- **`internal/biz/`**: 业务逻辑层(类似 DDD 的 domain 层)
|
||||
- 定义业务接口(Repository 模式)
|
||||
- 定义领域模型和数据结构
|
||||
- 使用依赖倒置原则:biz 定义接口,data 实现接口
|
||||
- 定义业务接口(Repository 模式)
|
||||
- 定义领域模型和数据结构
|
||||
- 使用依赖倒置原则:biz 定义接口,data 实现接口
|
||||
|
||||
- **`internal/data/`**: 数据访问层(类似 DDD 的 repository 层)
|
||||
- 实现 biz 中定义的业务接口
|
||||
- 封装数据库、缓存等操作
|
||||
- 处理数据持久化逻辑
|
||||
- 实现 biz 中定义的业务接口
|
||||
- 封装数据库、缓存等操作
|
||||
- 处理数据持久化逻辑
|
||||
|
||||
- **`internal/http/`**: HTTP 相关
|
||||
- `middleware/`: 自定义中间件
|
||||
- `request/`: 请求结构体定义
|
||||
- `rule/`: 自定义验证规则
|
||||
- `middleware/`: 自定义中间件
|
||||
- `request/`: 请求结构体定义
|
||||
- `rule/`: 自定义验证规则
|
||||
|
||||
- **`internal/apps/`**: 面板子应用实现
|
||||
|
||||
@@ -96,42 +260,42 @@ pnpm build
|
||||
- **`internal/queuejob/`**: 任务队列
|
||||
|
||||
- **`pkg/`**: 工具函数和通用包
|
||||
- 包含各种独立的工具模块
|
||||
- 可被项目任何部分引用
|
||||
- 包含各种独立的工具模块
|
||||
- 可被项目任何部分引用
|
||||
|
||||
- **`web/`**: Vue 3 前端项目
|
||||
|
||||
## 开发新功能的标准流程
|
||||
|
||||
1. **在 `internal/route/` 中添加路由**
|
||||
- 参考已有路由文件(如 `http.go`)
|
||||
- 注入需要的 service 依赖
|
||||
- 定义路由规则和 handler 映射
|
||||
- 参考已有路由文件(如 `http.go`)
|
||||
- 注入需要的 service 依赖
|
||||
- 定义路由规则和 handler 映射
|
||||
|
||||
2. **在 `internal/service/` 中实现服务方法**
|
||||
- **先阅读已有的类似服务**以了解代码风格
|
||||
- 处理请求验证和响应格式化
|
||||
- 使用 `Success()` 返回成功响应
|
||||
- 使用 `Error()` 返回错误响应
|
||||
- 使用 `ErrorSystem()` 返回系统严重错误
|
||||
- 调用 biz 层接口完成业务逻辑
|
||||
- **先阅读已有的类似服务**以了解代码风格
|
||||
- 处理请求验证和响应格式化
|
||||
- 使用 `Success()` 返回成功响应
|
||||
- 使用 `Error()` 返回错误响应
|
||||
- 使用 `ErrorSystem()` 返回系统严重错误
|
||||
- 调用 biz 层接口完成业务逻辑
|
||||
|
||||
3. **在 `internal/biz/` 中定义业务接口**
|
||||
- **先阅读已有的类似接口定义**
|
||||
- 定义 Repository 接口(如 `WebsiteRepo`)
|
||||
- 定义领域模型结构体(如 `Website`)
|
||||
- 保持接口简洁明确
|
||||
- **先阅读已有的类似接口定义**
|
||||
- 定义 Repository 接口(如 `WebsiteRepo`)
|
||||
- 定义领域模型结构体(如 `Website`)
|
||||
- 保持接口简洁明确
|
||||
|
||||
4. **在 `internal/data/` 中实现 biz 接口**
|
||||
- **先阅读已有的类似实现**
|
||||
- 创建 repo 结构体(如 `websiteRepo`)
|
||||
- 实现构造函数(如 `NewWebsiteRepo`)
|
||||
- 实现所有接口方法
|
||||
- 处理数据库操作和缓存逻辑
|
||||
- **先阅读已有的类似实现**
|
||||
- 创建 repo 结构体(如 `websiteRepo`)
|
||||
- 实现构造函数(如 `NewWebsiteRepo`)
|
||||
- 实现所有接口方法
|
||||
- 处理数据库操作和缓存逻辑
|
||||
|
||||
5. **使用 Wire 进行依赖注入**
|
||||
- 在对应的 wire.go 文件中添加 provider
|
||||
- 运行 `go generate` 生成依赖注入代码
|
||||
- 在对应的 wire.go 文件中添加 provider
|
||||
- 运行 `go generate` 生成依赖注入代码
|
||||
|
||||
## 技术栈特定注意事项
|
||||
|
||||
@@ -152,6 +316,7 @@ pnpm build
|
||||
### 助手函数(service 层)
|
||||
|
||||
在 service 层使用以下助手函数:
|
||||
|
||||
- `Success(w, data)`: 返回成功响应
|
||||
- `Error(w, statusCode, format, args...)`: 返回错误响应
|
||||
- `ErrorSystem(w, format, args...)`: 返回系统严重错误(500)
|
||||
@@ -174,6 +339,8 @@ pnpm build
|
||||
- 使用 `gofmt` 格式化代码
|
||||
- 复杂逻辑添加注释说明
|
||||
- 导出的函数和类型必须有注释
|
||||
- service 层以及 data 层方法对外返回的字符串需要使用 gotext 进行国际化处理
|
||||
- 禁止手动编辑国际化文件,项目使用外部 Crowdin 自动化管理
|
||||
|
||||
## Wire 依赖注入
|
||||
|
||||
@@ -181,6 +348,7 @@ pnpm build
|
||||
|
||||
1. 在 `cmd/ace/wire.go` 或 `cmd/cli/wire.go` 中添加 provider
|
||||
2. 运行生成命令:
|
||||
|
||||
```bash
|
||||
go generate ./...
|
||||
```
|
||||
@@ -194,17 +362,31 @@ go generate ./...
|
||||
- 图标:@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 上的实际使用案例
|
||||
|
||||
+12
-5
@@ -103,6 +103,7 @@ func initAce() (*app.Ace, error) {
|
||||
environmentNodejsService := service.NewEnvironmentNodejsService(locale, environmentRepo)
|
||||
environmentPHPService := service.NewEnvironmentPHPService(locale, config, environmentRepo, taskRepo)
|
||||
environmentPythonService := service.NewEnvironmentPythonService(locale, environmentRepo)
|
||||
environmentDotnetService := service.NewEnvironmentDotnetService(locale, environmentRepo)
|
||||
cronService := service.NewCronService(cronRepo)
|
||||
processService := service.NewProcessService()
|
||||
safeRepo := data.NewSafeRepo(logger)
|
||||
@@ -122,16 +123,18 @@ func initAce() (*app.Ace, error) {
|
||||
containerVolumeService := service.NewContainerVolumeService(containerVolumeRepo)
|
||||
fileService := service.NewFileService(locale, taskRepo)
|
||||
logRepo := data.NewLogRepo(db)
|
||||
logService := service.NewLogService(logRepo)
|
||||
logService := service.NewLogService(locale, logRepo)
|
||||
monitorRepo := data.NewMonitorRepo(db, settingRepo)
|
||||
monitorService := service.NewMonitorService(settingRepo, monitorRepo)
|
||||
settingService := service.NewSettingService(locale, db, settingRepo, certRepo, certAccountRepo)
|
||||
systemctlService := service.NewSystemctlService(locale)
|
||||
toolboxNetworkService := service.NewToolboxNetworkService()
|
||||
toolboxSystemService := service.NewToolboxSystemService(locale)
|
||||
toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale)
|
||||
toolboxSSHService := service.NewToolboxSSHService(locale)
|
||||
toolboxDiskService := service.NewToolboxDiskService(locale)
|
||||
toolboxLogService := service.NewToolboxLogService(locale, db, containerImageRepo, settingRepo)
|
||||
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)
|
||||
@@ -158,14 +161,18 @@ func initAce() (*app.Ace, error) {
|
||||
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, databaseUserService, backupService, backupStorageService, certService, certDNSService, certAccountService, appService, environmentService, environmentGoService, environmentJavaService, environmentNodejsService, environmentPHPService, environmentPythonService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, webHookService, templateService, loader)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, backupStorageService, certService, certDNSService, certAccountService, appService, environmentService, environmentGoService, environmentJavaService, environmentNodejsService, environmentPHPService, environmentPythonService, environmentDotnetService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxNetworkService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, toolboxMigrationService, webHookService, templateService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo)
|
||||
ws := route.NewWs(wsService)
|
||||
ws := route.NewWs(wsService, toolboxMigrationService)
|
||||
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server, err := bootstrap.NewHttp(config, mux)
|
||||
reloader, err := bootstrap.NewTLSReloader(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server, err := bootstrap.NewHttp(config, mux, reloader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -176,6 +183,6 @@ func initAce() (*app.Ace, error) {
|
||||
return nil, err
|
||||
}
|
||||
validation := bootstrap.NewValidator(config, db)
|
||||
ace := app.NewAce(config, mux, server, gormigrate, cron, queue, validation)
|
||||
ace := app.NewAce(config, mux, server, reloader, gormigrate, cron, queue, validation)
|
||||
return ace, nil
|
||||
}
|
||||
|
||||
@@ -4,32 +4,32 @@ go 1.25.6
|
||||
|
||||
require (
|
||||
github.com/DeRuina/timberjack v1.3.9
|
||||
github.com/andybalholm/brotli v1.2.0
|
||||
github.com/bddjr/hlfhr v1.4.2
|
||||
github.com/beevik/ntp v1.5.0
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/coreos/go-systemd/v22 v22.6.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.7
|
||||
github.com/go-chi/chi/v5 v5.2.4
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
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-resty/resty/v2 v2.17.1
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
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.6
|
||||
github.com/gookit/validate v1.5.7-0.20260207022141-dc32dc1276cd
|
||||
github.com/hashicorp/go-version v1.8.0
|
||||
github.com/klauspost/compress v1.18.3
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/libdns/alidns v1.0.6-beta.3
|
||||
github.com/lib/pq v1.11.2
|
||||
github.com/libdns/alidns v1.0.6-beta.3.0.20260121104221-597b1e580e21
|
||||
github.com/libdns/cloudflare v0.2.2
|
||||
github.com/libdns/cloudns v1.1.0
|
||||
github.com/libdns/gcore v0.0.0-20250427050847-9964da923833
|
||||
github.com/libdns/huaweicloud v1.0.0
|
||||
github.com/libdns/huaweicloud v1.0.1
|
||||
github.com/libdns/libdns v1.1.1
|
||||
github.com/libdns/namesilo v1.0.0
|
||||
github.com/libdns/porkbun v1.1.0
|
||||
@@ -38,12 +38,11 @@ require (
|
||||
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 v0.1.0
|
||||
github.com/libtnb/utils v1.2.1
|
||||
github.com/mholt/acmez/v3 v3.1.4
|
||||
github.com/moby/moby/api v1.53.0-rc.2
|
||||
github.com/moby/moby/client v0.2.1
|
||||
github.com/ncruces/go-sqlite3 v0.30.5
|
||||
github.com/ncruces/go-sqlite3/gormlite v0.30.2
|
||||
github.com/moby/moby/api v1.53.0
|
||||
github.com/moby/moby/client v0.2.2
|
||||
github.com/orandin/slog-gorm v1.4.0
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/pquerna/otp v1.5.0
|
||||
@@ -51,16 +50,17 @@ require (
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samber/lo v1.52.0
|
||||
github.com/sethvargo/go-limiter v1.1.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.12
|
||||
github.com/shirou/gopsutil/v4 v4.26.1
|
||||
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-20250620092546-c3e307e36701
|
||||
github.com/urfave/cli/v3 v3.6.2
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/net v0.49.0
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/net v0.50.0
|
||||
gorm.io/gorm v1.31.1
|
||||
resty.dev/v3 v3.0.0-beta.6
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -72,27 +72,31 @@ require (
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
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/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gofiber/schema v1.6.0 // 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/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/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/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // 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/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/tetratelabs/wazero v1.11.0 // indirect
|
||||
github.com/timtadh/data-structures v0.6.2 // indirect
|
||||
github.com/timtadh/lexmachine v0.2.3 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
@@ -101,13 +105,17 @@ require (
|
||||
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.40.0 // indirect
|
||||
golang.org/x/text v0.33.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
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.45.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/mholt/acmez/v3 => github.com/libtnb/acmez/v3 v3.0.0-20260103184942-a835890fc93e
|
||||
github.com/moby/moby/client => github.com/libtnb/moby/client v0.0.0-20260119133723-7d7dd88cf643
|
||||
github.com/mholt/acmez/v3 => github.com/libtnb/acmez/v3 v3.0.0-20260211214515-0d13ab154236
|
||||
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
|
||||
)
|
||||
|
||||
@@ -24,6 +24,8 @@ 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/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=
|
||||
@@ -50,8 +52,8 @@ github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkE
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
@@ -69,6 +71,8 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
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/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
|
||||
@@ -79,11 +83,13 @@ 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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
|
||||
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
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=
|
||||
github.com/go-chi/httplog/v3 v3.3.0 h1:Gr6Y7nSzbpyCyRwKPOVKjDH3BH6TH5uvRNDsTZWDpvU=
|
||||
github.com/go-chi/httplog/v3 v3.3.0/go.mod h1:N/J1l5l1fozUrqIVuT8Z/HzNeSy8TF2EFyokPLe6y2w=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
@@ -95,8 +101,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
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-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
|
||||
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
@@ -125,7 +129,11 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
@@ -138,8 +146,8 @@ 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.6 h1:D6vbSZzreuKYpeeXm5FDDEJy3K5E4lcWsQE4saSMZbU=
|
||||
github.com/gookit/validate v1.5.6/go.mod h1:WYEHndRNepIIkM+6CtgEX9MQ9ToIQRhXxmz5oLHF/fc=
|
||||
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/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=
|
||||
@@ -161,7 +169,10 @@ github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bP
|
||||
github.com/hashicorp/go-version v1.8.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=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
@@ -185,8 +196,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.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
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/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=
|
||||
@@ -200,18 +211,18 @@ 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.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/libdns/alidns v1.0.6-beta.3 h1:KAmb7FQ1tRzKsaAUGa7ZpGKAMRANwg7+1c7tUbSELq8=
|
||||
github.com/libdns/alidns v1.0.6-beta.3/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec=
|
||||
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-beta.3.0.20260121104221-597b1e580e21 h1:BGtptgrEJGjDQz63r4xkcQ/2ERXpna9WVUtrTtmW8d0=
|
||||
github.com/libdns/alidns v1.0.6-beta.3.0.20260121104221-597b1e580e21/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=
|
||||
github.com/libdns/cloudns v1.1.0/go.mod h1:/22V6tYYDALDpM4pw/RGGJ+X2F1Luibty9kKpKvkqBM=
|
||||
github.com/libdns/gcore v0.0.0-20250427050847-9964da923833 h1:/gsawtsq03cI7qgK65v16tYHIh40omL9YEzHWqnc63I=
|
||||
github.com/libdns/gcore v0.0.0-20250427050847-9964da923833/go.mod h1:jZJEV7pCTOJFlaUhHty+YwR05dzoSmInXK/vT3wOeVg=
|
||||
github.com/libdns/huaweicloud v1.0.0 h1:BQUIkOAjF++ouiANRIE3jdMlCfeiAAr6tGqb+8hM1jo=
|
||||
github.com/libdns/huaweicloud v1.0.0/go.mod h1:W+XywkW+C93fM50Ayklf6KuoCHZTqbrmFUEtpQnVvcU=
|
||||
github.com/libdns/huaweicloud v1.0.1 h1:TdfpPY//3/QPhEGCyvR8u2DTlGxzpyaIsqofH5kYFyA=
|
||||
github.com/libdns/huaweicloud v1.0.1/go.mod h1:lMHNSEqLZeuE4N28ytaqLZia3NPuaaz1ldAH/73bOQE=
|
||||
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/libdns/namesilo v1.0.0 h1:Shwbj9YnSp4NR617sBOSfDkFozEnWInWCxNwrDvPkCg=
|
||||
@@ -222,18 +233,20 @@ 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-20260103184942-a835890fc93e h1:HPHmtFcR5VpTJTpDqG4+l16uJfSlpB/X0r3qq22znrk=
|
||||
github.com/libtnb/acmez/v3 v3.0.0-20260103184942-a835890fc93e/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
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/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/moby/client v0.0.0-20260119133723-7d7dd88cf643 h1:B7uVTSsIh/9b+gpVOHWcg+8xmYf1g3E9v4OQYcPCkt8=
|
||||
github.com/libtnb/moby/client v0.0.0-20260119133723-7d7dd88cf643/go.mod h1:y8zxhZOdysHyNZgn9osm6SBDNmissvoai5HLhAdSIFo=
|
||||
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 v0.1.0 h1:OUbrE2R6I4ofqW+0u07B2npLX5x1/lzrh2t04SUX+QE=
|
||||
github.com/libtnb/sqlite v0.1.0/go.mod h1:1IAnfRmeQMfYDApi9uoNE6KLQYiSb0bZUjVZcz4hFPw=
|
||||
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/utils v1.2.1 h1:LJmReRREnpqfHyy9PZtNgBh3ZaIGct81b8ZaAsolMkM=
|
||||
@@ -243,6 +256,8 @@ 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/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
@@ -255,17 +270,13 @@ 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-rc.2 h1:Ajc2MnWJRC2AAqhIGNaqvwaA2mMYKyuso5PjxPP2KM4=
|
||||
github.com/moby/moby/api v1.53.0-rc.2/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
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/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=
|
||||
github.com/ncruces/go-sqlite3 v0.30.5 h1:6usmTQ6khriL8oWilkAZSJM/AIpAlVL2zFrlcpDldCE=
|
||||
github.com/ncruces/go-sqlite3 v0.30.5/go.mod h1:0I0JFflTKzfs3Ogfv8erP7CCoV/Z8uxigVDNOR0AQ5E=
|
||||
github.com/ncruces/go-sqlite3/gormlite v0.30.2 h1:FZ8mic14xTatssTkHCrelh9nPeFdXuzgMoNGkfuFbBU=
|
||||
github.com/ncruces/go-sqlite3/gormlite v0.30.2/go.mod h1:W9WLBbqrrOIh2dqFZkeC/xKALG2LDIHY91jowahOdtI=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
@@ -295,6 +306,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
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=
|
||||
@@ -308,8 +321,8 @@ github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRo
|
||||
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.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
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/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=
|
||||
@@ -332,8 +345,6 @@ github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+Q
|
||||
github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM=
|
||||
github.com/studio-b12/gowebdav v0.12.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
|
||||
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
|
||||
github.com/timtadh/data-structures v0.5.3/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU=
|
||||
github.com/timtadh/data-structures v0.6.1/go.mod h1:uYUnI1cQi/5yMCc7s23I+x8Mn8BCMf4WgK+7/4QSEk4=
|
||||
github.com/timtadh/data-structures v0.6.2 h1:zybDnU5NLjJ7WKMDJpvVwczQuf1wSLBgdRHZ9O4AqJ0=
|
||||
@@ -354,6 +365,8 @@ github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMz
|
||||
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=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
@@ -370,8 +383,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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
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=
|
||||
@@ -392,6 +405,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/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=
|
||||
@@ -406,8 +421,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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
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/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=
|
||||
@@ -434,20 +449,19 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
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/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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
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/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/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -465,6 +479,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/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=
|
||||
@@ -506,6 +522,36 @@ 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/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/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/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=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
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.45.0 h1:r51cSGzKpbptxnby+EIIz5fop4VuE4qFoVEjNvWoObs=
|
||||
modernc.org/sqlite v1.45.0/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
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=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
resty.dev/v3 v3.0.0-beta.6 h1:ghRdNpoE8/wBCv+kTKIOauW1aCrSIeTq7GxtfYgtevU=
|
||||
resty.dev/v3 v3.0.0-beta.6/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
|
||||
+14
-8
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -19,22 +18,25 @@ import (
|
||||
|
||||
"github.com/acepanel/panel/pkg/config"
|
||||
"github.com/acepanel/panel/pkg/queue"
|
||||
"github.com/acepanel/panel/pkg/tlscert"
|
||||
)
|
||||
|
||||
type Ace struct {
|
||||
conf *config.Config
|
||||
router *chi.Mux
|
||||
server *hlfhr.Server
|
||||
reloader *tlscert.Reloader
|
||||
migrator *gormigrate.Gormigrate
|
||||
cron *cron.Cron
|
||||
queue *queue.Queue
|
||||
}
|
||||
|
||||
func NewAce(conf *config.Config, router *chi.Mux, server *hlfhr.Server, migrator *gormigrate.Gormigrate, cron *cron.Cron, queue *queue.Queue, _ *validate.Validation) *Ace {
|
||||
func NewAce(conf *config.Config, router *chi.Mux, server *hlfhr.Server, reloader *tlscert.Reloader, migrator *gormigrate.Gormigrate, cron *cron.Cron, queue *queue.Queue, _ *validate.Validation) *Ace {
|
||||
return &Ace{
|
||||
conf: conf,
|
||||
router: router,
|
||||
server: server,
|
||||
reloader: reloader,
|
||||
migrator: migrator,
|
||||
cron: cron,
|
||||
queue: queue,
|
||||
@@ -66,15 +68,12 @@ func (r *Ace) Run() error {
|
||||
// run http server in goroutine
|
||||
serverErr := make(chan error, 1)
|
||||
go func() {
|
||||
fmt.Println("[HTTP] listening and serving on port", r.conf.HTTP.Port)
|
||||
if r.conf.HTTP.TLS {
|
||||
cert := filepath.Join(Root, "panel/storage/cert.pem")
|
||||
key := filepath.Join(Root, "panel/storage/cert.key")
|
||||
fmt.Println("[HTTP] listening and serving on port", r.conf.HTTP.Port, "with tls")
|
||||
if err := r.server.ListenAndServeTLS(cert, key); !errors.Is(err, http.ErrServerClosed) {
|
||||
if err := r.server.ListenAndServeTLS("", ""); !errors.Is(err, http.ErrServerClosed) {
|
||||
serverErr <- err
|
||||
}
|
||||
} else {
|
||||
fmt.Println("[HTTP] listening and serving on port", r.conf.HTTP.Port)
|
||||
if err := r.server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||
serverErr <- err
|
||||
}
|
||||
@@ -89,7 +88,7 @@ func (r *Ace) Run() error {
|
||||
return err
|
||||
}
|
||||
case sig := <-quit:
|
||||
fmt.Println("\n[APP] received signal:", sig)
|
||||
fmt.Println("[APP] received signal:", sig)
|
||||
}
|
||||
|
||||
// graceful shutdown
|
||||
@@ -104,6 +103,13 @@ func (r *Ace) Run() error {
|
||||
queueCancel()
|
||||
fmt.Println("[QUEUE] queue stopped")
|
||||
|
||||
// close certificate reloader
|
||||
if r.reloader != nil {
|
||||
if err := r.reloader.Close(); err != nil {
|
||||
fmt.Println("[TLS] certificate reloader close error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// shutdown http server
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer shutdownCancel()
|
||||
|
||||
@@ -60,10 +60,8 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *App) GetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
configPath := "/etc/docker/daemon.json"
|
||||
|
||||
// 读取配置文件
|
||||
content, err := io.Read(configPath)
|
||||
if err != nil {
|
||||
// 如果文件不存在,返回默认设置
|
||||
if os.IsNotExist(err) {
|
||||
service.Success(w, Settings{})
|
||||
return
|
||||
@@ -72,14 +70,12 @@ func (s *App) GetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 JSON
|
||||
var daemonConfig DaemonConfig
|
||||
if err = json.Unmarshal([]byte(content), &daemonConfig); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
service.Success(w, Settings{}) // 配置文件可能为空或格式错误,返回默认设置
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为 Settings 结构
|
||||
settings := Settings{
|
||||
RegistryMirrors: daemonConfig.RegistryMirrors,
|
||||
InsecureRegistries: daemonConfig.InsecureRegistries,
|
||||
@@ -127,7 +123,7 @@ func (s *App) UpdateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
configPath := "/etc/docker/daemon.json"
|
||||
settings := req.Settings
|
||||
|
||||
// 读取现有配置(保留其他字段)
|
||||
// 读取现有配置
|
||||
var existingConfig map[string]any
|
||||
content, err := io.Read(configPath)
|
||||
if err == nil && content != "" {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
@@ -29,6 +30,8 @@ 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) Load(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -110,3 +113,111 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Memcached 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/systemd/system/memcached.service")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
Port: s.getExecStartArg(config, "-p"),
|
||||
UDPPort: s.getExecStartArg(config, "-U"),
|
||||
ListenAddress: s.getExecStartArg(config, "-l"),
|
||||
Memory: s.getExecStartArg(config, "-m"),
|
||||
MaxConnections: s.getExecStartArg(config, "-c"),
|
||||
Threads: s.getExecStartArg(config, "-t"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Memcached 配置调整参数
|
||||
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, err := io.Read("/etc/systemd/system/memcached.service")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config = s.setExecStartArg(config, "-p", req.Port)
|
||||
config = s.setExecStartArg(config, "-U", req.UDPPort)
|
||||
config = s.setExecStartArg(config, "-l", req.ListenAddress)
|
||||
config = s.setExecStartArg(config, "-m", req.Memory)
|
||||
config = s.setExecStartArg(config, "-c", req.MaxConnections)
|
||||
config = s.setExecStartArg(config, "-t", req.Threads)
|
||||
|
||||
if err = io.Write("/etc/systemd/system/memcached.service", config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("memcached"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getExecStartArg 从 systemd service 文件的 ExecStart 行中获取指定参数值
|
||||
func (s *App) getExecStartArg(content string, flag string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "ExecStart=") {
|
||||
continue
|
||||
}
|
||||
args := strings.Fields(trimmed)
|
||||
for i, arg := range args {
|
||||
if arg == flag && i+1 < len(args) {
|
||||
return args[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setExecStartArg 在 systemd service 文件的 ExecStart 行中设置指定参数值
|
||||
func (s *App) setExecStartArg(content string, flag 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))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "ExecStart=") {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
args := strings.Fields(trimmed)
|
||||
newArgs := make([]string, 0, len(args))
|
||||
found := false
|
||||
for i := 0; i < len(args); i++ {
|
||||
if args[i] == flag && i+1 < len(args) {
|
||||
i++ // 跳过旧值
|
||||
found = true
|
||||
// 值为空时删除该参数
|
||||
if value != "" {
|
||||
newArgs = append(newArgs, flag, value)
|
||||
}
|
||||
} else {
|
||||
newArgs = append(newArgs, args[i])
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
newArgs = append(newArgs, flag, value)
|
||||
}
|
||||
result = append(result, strings.Join(newArgs, " "))
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
@@ -3,3 +3,13 @@ package memcached
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Memcached 配置调整
|
||||
type ConfigTune struct {
|
||||
Port string `form:"port" json:"port"`
|
||||
UDPPort string `form:"udp_port" json:"udp_port"`
|
||||
ListenAddress string `form:"listen_address" json:"listen_address"`
|
||||
Memory string `form:"memory" json:"memory"`
|
||||
MaxConnections string `form:"max_connections" json:"max_connections"`
|
||||
Threads string `form:"threads" json:"threads"`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/acepanel/panel/pkg/types"
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -44,6 +45,8 @@ func (s *App) Route(r chi.Router) {
|
||||
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)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
@@ -284,3 +287,175 @@ func (s *App) getSock() string {
|
||||
|
||||
return "/tmp/mysql.sock"
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 MySQL 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(app.Root + "/server/mysql/conf/my.cnf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
// 常规设置
|
||||
Port: s.getINIValue(config, "port"),
|
||||
MaxConnections: s.getINIValue(config, "max_connections"),
|
||||
MaxConnectErrors: s.getINIValue(config, "max_connect_errors"),
|
||||
DefaultStorageEngine: s.getINIValue(config, "default_storage_engine"),
|
||||
TableOpenCache: s.getINIValue(config, "table_open_cache"),
|
||||
MaxAllowedPacket: s.getINIValue(config, "max_allowed_packet"),
|
||||
OpenFilesLimit: s.getINIValue(config, "open_files_limit"),
|
||||
// 性能调整
|
||||
KeyBufferSize: s.getINIValue(config, "key_buffer_size"),
|
||||
SortBufferSize: s.getINIValue(config, "sort_buffer_size"),
|
||||
ReadBufferSize: s.getINIValue(config, "read_buffer_size"),
|
||||
ReadRndBufferSize: s.getINIValue(config, "read_rnd_buffer_size"),
|
||||
JoinBufferSize: s.getINIValue(config, "join_buffer_size"),
|
||||
ThreadCacheSize: s.getINIValue(config, "thread_cache_size"),
|
||||
ThreadStack: s.getINIValue(config, "thread_stack"),
|
||||
TmpTableSize: s.getINIValue(config, "tmp_table_size"),
|
||||
MaxHeapTableSize: s.getINIValue(config, "max_heap_table_size"),
|
||||
MyisamSortBufferSize: s.getINIValue(config, "myisam_sort_buffer_size"),
|
||||
// InnoDB
|
||||
InnodbBufferPoolSize: s.getINIValue(config, "innodb_buffer_pool_size"),
|
||||
InnodbLogBufferSize: s.getINIValue(config, "innodb_log_buffer_size"),
|
||||
InnodbFlushLogAtTrxCommit: s.getINIValue(config, "innodb_flush_log_at_trx_commit"),
|
||||
InnodbLockWaitTimeout: s.getINIValue(config, "innodb_lock_wait_timeout"),
|
||||
InnodbMaxDirtyPagesPct: s.getINIValue(config, "innodb_max_dirty_pages_pct"),
|
||||
InnodbReadIoThreads: s.getINIValue(config, "innodb_read_io_threads"),
|
||||
InnodbWriteIoThreads: s.getINIValue(config, "innodb_write_io_threads"),
|
||||
// 日志
|
||||
SlowQueryLog: s.getINIValue(config, "slow_query_log"),
|
||||
LongQueryTime: s.getINIValue(config, "long_query_time"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 MySQL 配置调整参数
|
||||
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 := app.Root + "/server/mysql/conf/my.cnf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新常规设置
|
||||
config = s.setINIValue(config, "port", req.Port)
|
||||
config = s.setINIValue(config, "max_connections", req.MaxConnections)
|
||||
config = s.setINIValue(config, "max_connect_errors", req.MaxConnectErrors)
|
||||
config = s.setINIValue(config, "default_storage_engine", req.DefaultStorageEngine)
|
||||
config = s.setINIValue(config, "table_open_cache", req.TableOpenCache)
|
||||
config = s.setINIValue(config, "max_allowed_packet", req.MaxAllowedPacket)
|
||||
config = s.setINIValue(config, "open_files_limit", req.OpenFilesLimit)
|
||||
// 更新性能调整
|
||||
config = s.setINIValue(config, "key_buffer_size", req.KeyBufferSize)
|
||||
config = s.setINIValue(config, "sort_buffer_size", req.SortBufferSize)
|
||||
config = s.setINIValue(config, "read_buffer_size", req.ReadBufferSize)
|
||||
config = s.setINIValue(config, "read_rnd_buffer_size", req.ReadRndBufferSize)
|
||||
config = s.setINIValue(config, "join_buffer_size", req.JoinBufferSize)
|
||||
config = s.setINIValue(config, "thread_cache_size", req.ThreadCacheSize)
|
||||
config = s.setINIValue(config, "thread_stack", req.ThreadStack)
|
||||
config = s.setINIValue(config, "tmp_table_size", req.TmpTableSize)
|
||||
config = s.setINIValue(config, "max_heap_table_size", req.MaxHeapTableSize)
|
||||
config = s.setINIValue(config, "myisam_sort_buffer_size", req.MyisamSortBufferSize)
|
||||
// 更新 InnoDB
|
||||
config = s.setINIValue(config, "innodb_buffer_pool_size", req.InnodbBufferPoolSize)
|
||||
config = s.setINIValue(config, "innodb_log_buffer_size", req.InnodbLogBufferSize)
|
||||
config = s.setINIValue(config, "innodb_flush_log_at_trx_commit", req.InnodbFlushLogAtTrxCommit)
|
||||
config = s.setINIValue(config, "innodb_lock_wait_timeout", req.InnodbLockWaitTimeout)
|
||||
config = s.setINIValue(config, "innodb_max_dirty_pages_pct", req.InnodbMaxDirtyPagesPct)
|
||||
config = s.setINIValue(config, "innodb_read_io_threads", req.InnodbReadIoThreads)
|
||||
config = s.setINIValue(config, "innodb_write_io_threads", req.InnodbWriteIoThreads)
|
||||
// 更新日志
|
||||
config = s.setINIValue(config, "slow_query_log", req.SlowQueryLog)
|
||||
config = s.setINIValue(config, "long_query_time", req.LongQueryTime)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getINIValue 从 INI 格式内容中获取指定键的值
|
||||
func (s *App) getINIValue(content string, key string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, ";") || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "[") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setINIValue 在 INI 格式内容中设置指定键的值
|
||||
func (s *App) setINIValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "[") {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, ";") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
} else if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.SplitN(checkLine, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, ";") && !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+line)
|
||||
} 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")
|
||||
}
|
||||
|
||||
@@ -7,3 +7,37 @@ type UpdateConfig struct {
|
||||
type SetRootPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
}
|
||||
|
||||
// ConfigTune MySQL 配置调整
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
Port string `form:"port" json:"port"`
|
||||
MaxConnections string `form:"max_connections" json:"max_connections"`
|
||||
MaxConnectErrors string `form:"max_connect_errors" json:"max_connect_errors"`
|
||||
DefaultStorageEngine string `form:"default_storage_engine" json:"default_storage_engine"`
|
||||
TableOpenCache string `form:"table_open_cache" json:"table_open_cache"`
|
||||
MaxAllowedPacket string `form:"max_allowed_packet" json:"max_allowed_packet"`
|
||||
OpenFilesLimit string `form:"open_files_limit" json:"open_files_limit"`
|
||||
// 性能调整
|
||||
KeyBufferSize string `form:"key_buffer_size" json:"key_buffer_size"`
|
||||
SortBufferSize string `form:"sort_buffer_size" json:"sort_buffer_size"`
|
||||
ReadBufferSize string `form:"read_buffer_size" json:"read_buffer_size"`
|
||||
ReadRndBufferSize string `form:"read_rnd_buffer_size" json:"read_rnd_buffer_size"`
|
||||
JoinBufferSize string `form:"join_buffer_size" json:"join_buffer_size"`
|
||||
ThreadCacheSize string `form:"thread_cache_size" json:"thread_cache_size"`
|
||||
ThreadStack string `form:"thread_stack" json:"thread_stack"`
|
||||
TmpTableSize string `form:"tmp_table_size" json:"tmp_table_size"`
|
||||
MaxHeapTableSize string `form:"max_heap_table_size" json:"max_heap_table_size"`
|
||||
MyisamSortBufferSize string `form:"myisam_sort_buffer_size" json:"myisam_sort_buffer_size"`
|
||||
// InnoDB
|
||||
InnodbBufferPoolSize string `form:"innodb_buffer_pool_size" json:"innodb_buffer_pool_size"`
|
||||
InnodbLogBufferSize string `form:"innodb_log_buffer_size" json:"innodb_log_buffer_size"`
|
||||
InnodbFlushLogAtTrxCommit string `form:"innodb_flush_log_at_trx_commit" json:"innodb_flush_log_at_trx_commit"`
|
||||
InnodbLockWaitTimeout string `form:"innodb_lock_wait_timeout" json:"innodb_lock_wait_timeout"`
|
||||
InnodbMaxDirtyPagesPct string `form:"innodb_max_dirty_pages_pct" json:"innodb_max_dirty_pages_pct"`
|
||||
InnodbReadIoThreads string `form:"innodb_read_io_threads" json:"innodb_read_io_threads"`
|
||||
InnodbWriteIoThreads string `form:"innodb_write_io_threads" json:"innodb_write_io_threads"`
|
||||
// 日志
|
||||
SlowQueryLog string `form:"slow_query_log" json:"slow_query_log"`
|
||||
LongQueryTime string `form:"long_query_time" json:"long_query_time"`
|
||||
}
|
||||
|
||||
+182
-1
@@ -4,12 +4,13 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
@@ -36,6 +37,8 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.SaveConfig)
|
||||
r.Get("/error_log", s.ErrorLog)
|
||||
r.Post("/clear_error_log", s.ClearErrorLog)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
|
||||
r.Get("/stream/servers", s.ListStreamServers)
|
||||
r.Post("/stream/servers", s.CreateStreamServer)
|
||||
@@ -93,6 +96,7 @@ func (s *App) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
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/nginx_status")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
@@ -165,3 +169,180 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Nginx 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
// 常规设置
|
||||
WorkerProcesses: s.getNginxValue(config, "worker_processes"),
|
||||
WorkerConnections: s.getNginxValue(config, "worker_connections"),
|
||||
KeepaliveTimeout: s.getNginxValue(config, "keepalive_timeout"),
|
||||
ClientMaxBodySize: s.getNginxValue(config, "client_max_body_size"),
|
||||
ClientBodyBufferSize: s.getNginxValue(config, "client_body_buffer_size"),
|
||||
ClientHeaderBufferSize: s.getNginxValue(config, "client_header_buffer_size"),
|
||||
ServerNamesHashBucketSize: s.getNginxValue(config, "server_names_hash_bucket_size"),
|
||||
ServerTokens: s.getNginxValue(config, "server_tokens"),
|
||||
// Gzip 压缩
|
||||
Gzip: s.getNginxValue(config, "gzip"),
|
||||
GzipMinLength: s.getNginxValue(config, "gzip_min_length"),
|
||||
GzipCompLevel: s.getNginxValue(config, "gzip_comp_level"),
|
||||
GzipTypes: s.getNginxValue(config, "gzip_types"),
|
||||
GzipVary: s.getNginxValue(config, "gzip_vary"),
|
||||
GzipProxied: s.getNginxValue(config, "gzip_proxied"),
|
||||
// Brotli 压缩
|
||||
Brotli: s.getNginxValue(config, "brotli"),
|
||||
BrotliMinLength: s.getNginxValue(config, "brotli_min_length"),
|
||||
BrotliCompLevel: s.getNginxValue(config, "brotli_comp_level"),
|
||||
BrotliTypes: s.getNginxValue(config, "brotli_types"),
|
||||
BrotliStatic: s.getNginxValue(config, "brotli_static"),
|
||||
// Zstd 压缩
|
||||
Zstd: s.getNginxValue(config, "zstd"),
|
||||
ZstdMinLength: s.getNginxValue(config, "zstd_min_length"),
|
||||
ZstdCompLevel: s.getNginxValue(config, "zstd_comp_level"),
|
||||
ZstdTypes: s.getNginxValue(config, "zstd_types"),
|
||||
ZstdStatic: s.getNginxValue(config, "zstd_static"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Nginx 配置调整参数
|
||||
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/nginx/conf/nginx.conf", app.Root)
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新常规设置
|
||||
config = s.setNginxValue(config, "worker_processes", req.WorkerProcesses)
|
||||
config = s.setNginxValue(config, "worker_connections", req.WorkerConnections)
|
||||
config = s.setNginxValue(config, "keepalive_timeout", req.KeepaliveTimeout)
|
||||
config = s.setNginxValue(config, "client_max_body_size", req.ClientMaxBodySize)
|
||||
config = s.setNginxValue(config, "client_body_buffer_size", req.ClientBodyBufferSize)
|
||||
config = s.setNginxValue(config, "client_header_buffer_size", req.ClientHeaderBufferSize)
|
||||
config = s.setNginxValue(config, "server_names_hash_bucket_size", req.ServerNamesHashBucketSize)
|
||||
config = s.setNginxValue(config, "server_tokens", req.ServerTokens)
|
||||
// 更新 Gzip 压缩
|
||||
config = s.setNginxValue(config, "gzip", req.Gzip)
|
||||
config = s.setNginxValue(config, "gzip_min_length", req.GzipMinLength)
|
||||
config = s.setNginxValue(config, "gzip_comp_level", req.GzipCompLevel)
|
||||
config = s.setNginxValue(config, "gzip_types", req.GzipTypes)
|
||||
config = s.setNginxValue(config, "gzip_vary", req.GzipVary)
|
||||
config = s.setNginxValue(config, "gzip_proxied", req.GzipProxied)
|
||||
// 更新 Brotli 压缩
|
||||
config = s.setNginxValue(config, "brotli", req.Brotli)
|
||||
config = s.setNginxValue(config, "brotli_min_length", req.BrotliMinLength)
|
||||
config = s.setNginxValue(config, "brotli_comp_level", req.BrotliCompLevel)
|
||||
config = s.setNginxValue(config, "brotli_types", req.BrotliTypes)
|
||||
config = s.setNginxValue(config, "brotli_static", req.BrotliStatic)
|
||||
// 更新 Zstd 压缩
|
||||
config = s.setNginxValue(config, "zstd", req.Zstd)
|
||||
config = s.setNginxValue(config, "zstd_min_length", req.ZstdMinLength)
|
||||
config = s.setNginxValue(config, "zstd_comp_level", req.ZstdCompLevel)
|
||||
config = s.setNginxValue(config, "zstd_types", req.ZstdTypes)
|
||||
config = s.setNginxValue(config, "zstd_static", req.ZstdStatic)
|
||||
|
||||
if err = io.Write(confPath, config, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload nginx: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getNginxValue 从 Nginx 配置内容中获取指定指令的值
|
||||
func (s *App) getNginxValue(content string, key string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(trimmed, ";") {
|
||||
continue
|
||||
}
|
||||
trimmed = strings.TrimSuffix(trimmed, ";")
|
||||
trimmed = strings.TrimSpace(trimmed)
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
return strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setNginxValue 在 Nginx 配置内容中设置指定指令的值
|
||||
func (s *App) setNginxValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || trimmed == "{" || trimmed == "}" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查指令(可能被注释)
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(checkLine, ";") {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine = strings.TrimSuffix(checkLine, ";")
|
||||
checkLine = strings.TrimSpace(checkLine)
|
||||
parts := strings.Fields(checkLine)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
result = append(result, indent+"#"+strings.TrimLeft(line, " \t"))
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 保留原行缩进
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
result = append(result, indent+key+" "+value+";")
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, " "+key+" "+value+";")
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,38 @@ type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Nginx 配置调整
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
WorkerProcesses string `form:"worker_processes" json:"worker_processes"`
|
||||
WorkerConnections string `form:"worker_connections" json:"worker_connections"`
|
||||
KeepaliveTimeout string `form:"keepalive_timeout" json:"keepalive_timeout"`
|
||||
ClientMaxBodySize string `form:"client_max_body_size" json:"client_max_body_size"`
|
||||
ClientBodyBufferSize string `form:"client_body_buffer_size" json:"client_body_buffer_size"`
|
||||
ClientHeaderBufferSize string `form:"client_header_buffer_size" json:"client_header_buffer_size"`
|
||||
ServerNamesHashBucketSize string `form:"server_names_hash_bucket_size" json:"server_names_hash_bucket_size"`
|
||||
ServerTokens string `form:"server_tokens" json:"server_tokens"`
|
||||
// Gzip 压缩
|
||||
Gzip string `form:"gzip" json:"gzip"`
|
||||
GzipMinLength string `form:"gzip_min_length" json:"gzip_min_length"`
|
||||
GzipCompLevel string `form:"gzip_comp_level" json:"gzip_comp_level"`
|
||||
GzipTypes string `form:"gzip_types" json:"gzip_types"`
|
||||
GzipVary string `form:"gzip_vary" json:"gzip_vary"`
|
||||
GzipProxied string `form:"gzip_proxied" json:"gzip_proxied"`
|
||||
// Brotli 压缩
|
||||
Brotli string `form:"brotli" json:"brotli"`
|
||||
BrotliMinLength string `form:"brotli_min_length" json:"brotli_min_length"`
|
||||
BrotliCompLevel string `form:"brotli_comp_level" json:"brotli_comp_level"`
|
||||
BrotliTypes string `form:"brotli_types" json:"brotli_types"`
|
||||
BrotliStatic string `form:"brotli_static" json:"brotli_static"`
|
||||
// Zstd 压缩
|
||||
Zstd string `form:"zstd" json:"zstd"`
|
||||
ZstdMinLength string `form:"zstd_min_length" json:"zstd_min_length"`
|
||||
ZstdCompLevel string `form:"zstd_comp_level" json:"zstd_comp_level"`
|
||||
ZstdTypes string `form:"zstd_types" json:"zstd_types"`
|
||||
ZstdStatic string `form:"zstd_static" json:"zstd_static"`
|
||||
}
|
||||
|
||||
type StreamServer struct {
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` // 配置名称,用于文件命名
|
||||
Listen string `form:"listen" json:"listen" validate:"required"` // 监听地址,如: "12345", "0.0.0.0:12345", "[::]:12345"
|
||||
|
||||
@@ -95,8 +95,8 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: req.Port,
|
||||
PortEnd: req.Port,
|
||||
Direction: firewall.DirectionIn,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
Direction: firewall.DirectionIn,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/pkg/db"
|
||||
@@ -43,6 +44,8 @@ func (s *App) Route(r chi.Router) {
|
||||
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)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
@@ -231,3 +234,176 @@ func (s *App) SetPostgresPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 PostgreSQL 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
// 连接设置
|
||||
ListenAddresses: s.getPGValue(config, "listen_addresses"),
|
||||
Port: s.getPGValue(config, "port"),
|
||||
MaxConnections: s.getPGValue(config, "max_connections"),
|
||||
SuperuserReservedConnections: s.getPGValue(config, "superuser_reserved_connections"),
|
||||
// 内存设置
|
||||
SharedBuffers: s.getPGValue(config, "shared_buffers"),
|
||||
WorkMem: s.getPGValue(config, "work_mem"),
|
||||
MaintenanceWorkMem: s.getPGValue(config, "maintenance_work_mem"),
|
||||
EffectiveCacheSize: s.getPGValue(config, "effective_cache_size"),
|
||||
HugePages: s.getPGValue(config, "huge_pages"),
|
||||
// WAL 设置
|
||||
WalLevel: s.getPGValue(config, "wal_level"),
|
||||
WalBuffers: s.getPGValue(config, "wal_buffers"),
|
||||
MaxWalSize: s.getPGValue(config, "max_wal_size"),
|
||||
MinWalSize: s.getPGValue(config, "min_wal_size"),
|
||||
CheckpointCompletionTarget: s.getPGValue(config, "checkpoint_completion_target"),
|
||||
// 查询优化
|
||||
DefaultStatisticsTarget: s.getPGValue(config, "default_statistics_target"),
|
||||
RandomPageCost: s.getPGValue(config, "random_page_cost"),
|
||||
EffectiveIoConcurrency: s.getPGValue(config, "effective_io_concurrency"),
|
||||
// 日志设置
|
||||
LogDestination: s.getPGValue(config, "log_destination"),
|
||||
LogMinDurationStatement: s.getPGValue(config, "log_min_duration_statement"),
|
||||
LogTimezone: s.getPGValue(config, "log_timezone"),
|
||||
// IO 设置
|
||||
IoMethod: s.getPGValue(config, "io_method"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 PostgreSQL 配置调整参数
|
||||
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/postgresql/data/postgresql.conf", app.Root)
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新连接设置
|
||||
config = s.setPGValue(config, "listen_addresses", req.ListenAddresses)
|
||||
config = s.setPGValue(config, "port", req.Port)
|
||||
config = s.setPGValue(config, "max_connections", req.MaxConnections)
|
||||
config = s.setPGValue(config, "superuser_reserved_connections", req.SuperuserReservedConnections)
|
||||
// 更新内存设置
|
||||
config = s.setPGValue(config, "shared_buffers", req.SharedBuffers)
|
||||
config = s.setPGValue(config, "work_mem", req.WorkMem)
|
||||
config = s.setPGValue(config, "maintenance_work_mem", req.MaintenanceWorkMem)
|
||||
config = s.setPGValue(config, "effective_cache_size", req.EffectiveCacheSize)
|
||||
config = s.setPGValue(config, "huge_pages", req.HugePages)
|
||||
// 更新 WAL 设置
|
||||
config = s.setPGValue(config, "wal_level", req.WalLevel)
|
||||
config = s.setPGValue(config, "wal_buffers", req.WalBuffers)
|
||||
config = s.setPGValue(config, "max_wal_size", req.MaxWalSize)
|
||||
config = s.setPGValue(config, "min_wal_size", req.MinWalSize)
|
||||
config = s.setPGValue(config, "checkpoint_completion_target", req.CheckpointCompletionTarget)
|
||||
// 更新查询优化
|
||||
config = s.setPGValue(config, "default_statistics_target", req.DefaultStatisticsTarget)
|
||||
config = s.setPGValue(config, "random_page_cost", req.RandomPageCost)
|
||||
config = s.setPGValue(config, "effective_io_concurrency", req.EffectiveIoConcurrency)
|
||||
// 更新日志设置
|
||||
config = s.setPGValue(config, "log_destination", req.LogDestination)
|
||||
config = s.setPGValue(config, "log_min_duration_statement", req.LogMinDurationStatement)
|
||||
config = s.setPGValue(config, "log_timezone", req.LogTimezone)
|
||||
// 更新 IO 设置
|
||||
config = s.setPGValue(config, "io_method", req.IoMethod)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("postgresql"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload PostgreSQL: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getPGValue 从 PostgreSQL 配置内容中获取指定键的值
|
||||
func (s *App) getPGValue(content string, key string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
v := strings.TrimSpace(parts[1])
|
||||
// 去除行尾注释
|
||||
if idx := strings.Index(v, "#"); idx >= 0 {
|
||||
v = strings.TrimSpace(v[:idx])
|
||||
}
|
||||
// 去除引号
|
||||
v = strings.Trim(v, "'\"")
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setPGValue 在 PostgreSQL 配置内容中设置指定键的值
|
||||
func (s *App) setPGValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
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.SplitN(checkLine, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+line)
|
||||
} 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")
|
||||
}
|
||||
|
||||
@@ -7,3 +7,34 @@ type UpdateConfig struct {
|
||||
type SetPostgresPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
}
|
||||
|
||||
// ConfigTune PostgreSQL 配置调整
|
||||
type ConfigTune struct {
|
||||
// 连接设置
|
||||
ListenAddresses string `form:"listen_addresses" json:"listen_addresses"`
|
||||
Port string `form:"port" json:"port"`
|
||||
MaxConnections string `form:"max_connections" json:"max_connections"`
|
||||
SuperuserReservedConnections string `form:"superuser_reserved_connections" json:"superuser_reserved_connections"`
|
||||
// 内存设置
|
||||
SharedBuffers string `form:"shared_buffers" json:"shared_buffers"`
|
||||
WorkMem string `form:"work_mem" json:"work_mem"`
|
||||
MaintenanceWorkMem string `form:"maintenance_work_mem" json:"maintenance_work_mem"`
|
||||
EffectiveCacheSize string `form:"effective_cache_size" json:"effective_cache_size"`
|
||||
HugePages string `form:"huge_pages" json:"huge_pages"`
|
||||
// WAL 设置
|
||||
WalLevel string `form:"wal_level" json:"wal_level"`
|
||||
WalBuffers string `form:"wal_buffers" json:"wal_buffers"`
|
||||
MaxWalSize string `form:"max_wal_size" json:"max_wal_size"`
|
||||
MinWalSize string `form:"min_wal_size" json:"min_wal_size"`
|
||||
CheckpointCompletionTarget string `form:"checkpoint_completion_target" json:"checkpoint_completion_target"`
|
||||
// 查询优化
|
||||
DefaultStatisticsTarget string `form:"default_statistics_target" json:"default_statistics_target"`
|
||||
RandomPageCost string `form:"random_page_cost" json:"random_page_cost"`
|
||||
EffectiveIoConcurrency string `form:"effective_io_concurrency" json:"effective_io_concurrency"`
|
||||
// 日志设置
|
||||
LogDestination string `form:"log_destination" json:"log_destination"`
|
||||
LogMinDurationStatement string `form:"log_min_duration_statement" json:"log_min_duration_statement"`
|
||||
LogTimezone string `form:"log_timezone" json:"log_timezone"`
|
||||
// IO 设置
|
||||
IoMethod string `form:"io_method" json:"io_method"`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package pureftpd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -35,6 +36,8 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/users/{username}/password", s.ChangePassword)
|
||||
r.Get("/port", s.GetPort)
|
||||
r.Post("/port", s.UpdatePort)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
@@ -139,13 +142,19 @@ func (s *App) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetPort 获取端口
|
||||
func (s *App) GetPort(w http.ResponseWriter, r *http.Request) {
|
||||
port, err := shell.Execf(`cat %s/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`, app.Root)
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/pure-ftpd/etc/pure-ftpd.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get port: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, cast.ToInt(port))
|
||||
bind := strings.Trim(s.getFTPValue(config, "Bind"), `"'`)
|
||||
port := 21 // 默认端口
|
||||
if parts := strings.SplitN(bind, ",", 2); len(parts) == 2 {
|
||||
port = cast.ToInt(strings.TrimSpace(parts[1]))
|
||||
}
|
||||
|
||||
service.Success(w, port)
|
||||
}
|
||||
|
||||
// UpdatePort 设置端口
|
||||
@@ -156,7 +165,14 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(`sed -i "s/Bind.*/Bind 0.0.0.0,%d/g" %s/server/pure-ftpd/etc/pure-ftpd.conf`, req.Port, app.Root); err != nil {
|
||||
confPath := fmt.Sprintf("%s/server/pure-ftpd/etc/pure-ftpd.conf", app.Root)
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
config = s.setFTPValue(config, "Bind", fmt.Sprintf(`"0.0.0.0,%d"`, req.Port))
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -166,8 +182,8 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: req.Port,
|
||||
PortEnd: req.Port,
|
||||
Direction: firewall.DirectionIn,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
Direction: firewall.DirectionIn,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -181,3 +197,124 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Pure-FTPd 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/pure-ftpd/etc/pure-ftpd.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
MaxClientsNumber: s.getFTPValue(config, "MaxClientsNumber"),
|
||||
MaxClientsPerIP: s.getFTPValue(config, "MaxClientsPerIP"),
|
||||
MaxIdleTime: s.getFTPValue(config, "MaxIdleTime"),
|
||||
MaxLoad: s.getFTPValue(config, "MaxLoad"),
|
||||
PassivePortRange: s.getFTPValue(config, "PassivePortRange"),
|
||||
AnonymousOnly: s.getFTPValue(config, "AnonymousOnly"),
|
||||
NoAnonymous: s.getFTPValue(config, "NoAnonymous"),
|
||||
MaxDiskUsage: s.getFTPValue(config, "MaxDiskUsage"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Pure-FTPd 配置调整参数
|
||||
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/pure-ftpd/etc/pure-ftpd.conf", app.Root)
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config = s.setFTPValue(config, "MaxClientsNumber", req.MaxClientsNumber)
|
||||
config = s.setFTPValue(config, "MaxClientsPerIP", req.MaxClientsPerIP)
|
||||
config = s.setFTPValue(config, "MaxIdleTime", req.MaxIdleTime)
|
||||
config = s.setFTPValue(config, "MaxLoad", req.MaxLoad)
|
||||
config = s.setFTPValue(config, "PassivePortRange", req.PassivePortRange)
|
||||
config = s.setFTPValue(config, "AnonymousOnly", req.AnonymousOnly)
|
||||
config = s.setFTPValue(config, "NoAnonymous", req.NoAnonymous)
|
||||
config = s.setFTPValue(config, "MaxDiskUsage", req.MaxDiskUsage)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("pure-ftpd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getFTPValue 从 Pure-FTPd 配置内容中获取指定键的值
|
||||
func (s *App) getFTPValue(content string, key string) string {
|
||||
lines := strings.Split(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 ""
|
||||
}
|
||||
|
||||
// setFTPValue 在 Pure-FTPd 配置内容中设置指定键的值
|
||||
func (s *App) setFTPValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
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) >= 2 && parts[0] == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+line)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 保留原行格式
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
result = append(result, indent+key+" "+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" "+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
@@ -18,3 +18,15 @@ type ChangePassword struct {
|
||||
type UpdatePort struct {
|
||||
Port uint `form:"port" json:"port" validate:"required|number|min:1|max:65535"`
|
||||
}
|
||||
|
||||
// ConfigTune Pure-FTPd 配置调整
|
||||
type ConfigTune struct {
|
||||
MaxClientsNumber string `form:"max_clients_number" json:"max_clients_number"`
|
||||
MaxClientsPerIP string `form:"max_clients_per_ip" json:"max_clients_per_ip"`
|
||||
MaxIdleTime string `form:"max_idle_time" json:"max_idle_time"`
|
||||
MaxLoad string `form:"max_load" json:"max_load"`
|
||||
PassivePortRange string `form:"passive_port_range" json:"passive_port_range"`
|
||||
AnonymousOnly string `form:"anonymous_only" json:"anonymous_only"`
|
||||
NoAnonymous string `form:"no_anonymous" json:"no_anonymous"`
|
||||
MaxDiskUsage string `form:"max_disk_usage" json:"max_disk_usage"`
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ 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) Load(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -121,3 +123,126 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Redis 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/redis/redis.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
Bind: s.getRedisValue(config, "bind"),
|
||||
Port: s.getRedisValue(config, "port"),
|
||||
Databases: s.getRedisValue(config, "databases"),
|
||||
Requirepass: s.getRedisValue(config, "requirepass"),
|
||||
Timeout: s.getRedisValue(config, "timeout"),
|
||||
TCPKeepalive: s.getRedisValue(config, "tcp-keepalive"),
|
||||
Maxmemory: s.getRedisValue(config, "maxmemory"),
|
||||
MaxmemoryPolicy: s.getRedisValue(config, "maxmemory-policy"),
|
||||
Appendonly: s.getRedisValue(config, "appendonly"),
|
||||
Appendfsync: s.getRedisValue(config, "appendfsync"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Redis 配置调整参数
|
||||
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/redis/redis.conf", app.Root)
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config = s.setRedisValue(config, "bind", req.Bind)
|
||||
config = s.setRedisValue(config, "port", req.Port)
|
||||
config = s.setRedisValue(config, "databases", req.Databases)
|
||||
config = s.setRedisValue(config, "requirepass", req.Requirepass)
|
||||
config = s.setRedisValue(config, "timeout", req.Timeout)
|
||||
config = s.setRedisValue(config, "tcp-keepalive", req.TCPKeepalive)
|
||||
config = s.setRedisValue(config, "maxmemory", req.Maxmemory)
|
||||
config = s.setRedisValue(config, "maxmemory-policy", req.MaxmemoryPolicy)
|
||||
config = s.setRedisValue(config, "appendonly", req.Appendonly)
|
||||
config = s.setRedisValue(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("redis"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getRedisValue 从 Redis 配置内容中获取指定键的值
|
||||
func (s *App) getRedisValue(content string, key string) string {
|
||||
lines := strings.Split(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 ""
|
||||
}
|
||||
|
||||
// setRedisValue 在 Redis 配置内容中设置指定键的值
|
||||
func (s *App) setRedisValue(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")
|
||||
}
|
||||
|
||||
@@ -3,3 +3,20 @@ package redis
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Redis 配置调整
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -94,17 +94,19 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to create passwd file: %v", err))
|
||||
return
|
||||
}
|
||||
if _, err = shell.Execf(`echo 's3fs#%s %s fuse _netdev,allow_other,nonempty,url=%s,passwd_file=/etc/passwd-s3fs-%s 0 0' >> /etc/fstab`, req.Bucket, req.Path, req.URL, cast.ToString(id)); err != nil {
|
||||
if _, err = shell.Execf(`echo 's3fs#%s %s fuse3 _netdev,allow_other,url=%s,passwd_file=/etc/passwd-s3fs-%s 0 0' >> /etc/fstab`, req.Bucket, req.Path, req.URL, cast.ToString(id)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if _, err = shell.Execf("mount -a"); err != nil {
|
||||
_, _ = shell.Execf(`sed -i 's@^s3fs#%s\s%s.*$@@g' /etc/fstab`, req.Bucket, req.Path)
|
||||
_ = os.Remove("/etc/passwd-s3fs-" + cast.ToString(id))
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if _, err = shell.Execf(`df -h | grep '%s'`, req.Path); err != nil {
|
||||
_, _ = shell.Execf(`sed -i 's@^s3fs#%s\s%s.*$@@g' /etc/fstab`, req.Bucket, req.Path)
|
||||
_ = os.Remove("/etc/passwd-s3fs-" + cast.ToString(id))
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("mount failed: %v", err))
|
||||
return
|
||||
}
|
||||
@@ -138,7 +140,7 @@ func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`fusermount -uz '%s'`, mount.Path)
|
||||
_, _ = shell.Execf(`fusermount3 -uz '%s'`, mount.Path)
|
||||
_, err2 := shell.Execf(`umount -lf '%s'`, mount.Path)
|
||||
// 卸载之后再检查下是否还有挂载
|
||||
if _, err = shell.Execf(`df -h | grep '%s'`, mount.Path); err == nil {
|
||||
|
||||
@@ -41,11 +41,9 @@ type CertRepo interface {
|
||||
Update(ctx context.Context, req *request.CertUpdate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
ObtainAuto(id uint) (*acme.Certificate, error)
|
||||
ObtainManual(id uint) (*acme.Certificate, error)
|
||||
ObtainPanel(account *CertAccount, ips []string) ([]byte, []byte, error)
|
||||
ObtainSelfSigned(id uint) error
|
||||
Renew(id uint) (*acme.Certificate, error)
|
||||
RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error)
|
||||
ManualDNS(id uint) ([]acme.DNSRecord, error)
|
||||
Deploy(ID, WebsiteID uint) error
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ type Database struct {
|
||||
}
|
||||
|
||||
type DatabaseRepo interface {
|
||||
List(page, limit uint) ([]*Database, int64, error)
|
||||
List(page, limit uint, typ string) ([]*Database, int64, error)
|
||||
Create(ctx context.Context, req *request.DatabaseCreate) error
|
||||
Delete(ctx context.Context, serverID uint, name string) error
|
||||
Comment(req *request.DatabaseComment) error
|
||||
|
||||
@@ -62,7 +62,7 @@ func (r *DatabaseServer) AfterFind(tx *gorm.DB) error {
|
||||
|
||||
type DatabaseServerRepo interface {
|
||||
Count() (int64, error)
|
||||
List(page, limit uint) ([]*DatabaseServer, int64, error)
|
||||
List(page, limit uint, typ string) ([]*DatabaseServer, int64, error)
|
||||
Get(id uint) (*DatabaseServer, error)
|
||||
GetByName(name string) (*DatabaseServer, error)
|
||||
Create(req *request.DatabaseServerCreate) error
|
||||
|
||||
@@ -64,7 +64,7 @@ func (r *DatabaseUser) AfterFind(tx *gorm.DB) error {
|
||||
|
||||
type DatabaseUserRepo interface {
|
||||
Count() (int64, error)
|
||||
List(page, limit uint) ([]*DatabaseUser, int64, error)
|
||||
List(page, limit uint, typ string) ([]*DatabaseUser, int64, error)
|
||||
Get(id uint) (*DatabaseUser, error)
|
||||
Create(ctx context.Context, req *request.DatabaseUserCreate) error
|
||||
Update(req *request.DatabaseUserUpdate) error
|
||||
|
||||
+4
-1
@@ -47,5 +47,8 @@ type LogEntry struct {
|
||||
// LogRepo 日志仓库接口
|
||||
type LogRepo interface {
|
||||
// List 获取日志列表
|
||||
List(logType string, limit int) ([]LogEntry, error)
|
||||
// date 格式为 YYYY-MM-DD,空字符串表示当天日志
|
||||
List(logType string, limit int, date string) ([]LogEntry, error)
|
||||
// ListDates 获取可用的日志日期列表
|
||||
ListDates(logType string) ([]string, error)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ const (
|
||||
SettingKeyPublicIPs SettingKey = "public_ips"
|
||||
SettingHiddenMenu SettingKey = "hidden_menu"
|
||||
SettingKeyCustomLogo SettingKey = "custom_logo"
|
||||
SettingKeyMemo SettingKey = "memo"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
|
||||
@@ -3,4 +3,4 @@ package bootstrap
|
||||
import "github.com/google/wire"
|
||||
|
||||
// ProviderSet is bootstrap providers.
|
||||
var ProviderSet = wire.NewSet(NewConf, NewT, NewLog, NewCli, NewValidator, NewRouter, NewHttp, NewDB, NewMigrate, NewLoader, NewSession, NewCron, NewQueue)
|
||||
var ProviderSet = wire.NewSet(NewConf, NewT, NewLog, NewCli, NewValidator, NewRouter, NewTLSReloader, NewHttp, NewDB, NewMigrate, NewLoader, NewSession, NewCron, NewQueue)
|
||||
|
||||
@@ -6,8 +6,7 @@ import (
|
||||
|
||||
"github.com/DeRuina/timberjack"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/ncruces/go-sqlite3/gormlite"
|
||||
"github.com/libtnb/sqlite"
|
||||
sloggorm "github.com/orandin/slog-gorm"
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -33,11 +32,24 @@ func NewDB(conf *config.Config) (*gorm.DB, error) {
|
||||
options = append(options, sloggorm.WithTraceAll())
|
||||
}
|
||||
|
||||
return gorm.Open(gormlite.Open("file:"+filepath.Join(app.Root, "panel/storage/panel.db?_txlock=immediate")), &gorm.Config{
|
||||
Logger: sloggorm.New(options...),
|
||||
SkipDefaultTransaction: true,
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
db, err := gorm.Open(sqlite.Open("file:"+filepath.Join(app.Root, "panel/storage/panel.db")+"?_txlock=immediate&_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)"),
|
||||
&gorm.Config{
|
||||
Logger: sloggorm.New(options...),
|
||||
SkipDefaultTransaction: true,
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
sqlDB.SetMaxIdleConns(1)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func NewMigrate(db *gorm.DB) *gormigrate.Gormigrate {
|
||||
|
||||
@@ -4,14 +4,17 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bddjr/hlfhr"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/http/middleware"
|
||||
"github.com/acepanel/panel/internal/route"
|
||||
"github.com/acepanel/panel/pkg/config"
|
||||
"github.com/acepanel/panel/pkg/tlscert"
|
||||
)
|
||||
|
||||
func NewRouter(t *gotext.Locale, middlewares *middleware.Middlewares, http *route.Http, ws *route.Ws) (*chi.Mux, error) {
|
||||
@@ -27,7 +30,21 @@ func NewRouter(t *gotext.Locale, middlewares *middleware.Middlewares, http *rout
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func NewHttp(conf *config.Config, mux *chi.Mux) (*hlfhr.Server, error) {
|
||||
func NewTLSReloader(conf *config.Config) (*tlscert.Reloader, error) {
|
||||
if !conf.HTTP.TLS {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
certFile := filepath.Join(app.Root, "panel/storage/cert.pem")
|
||||
keyFile := filepath.Join(app.Root, "panel/storage/cert.key")
|
||||
reloader, err := tlscert.NewReloader(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load certificate: %w", err)
|
||||
}
|
||||
return reloader, nil
|
||||
}
|
||||
|
||||
func NewHttp(conf *config.Config, mux *chi.Mux, reloader *tlscert.Reloader) (*hlfhr.Server, error) {
|
||||
srv := hlfhr.New(&http.Server{
|
||||
Addr: fmt.Sprintf(":%d", conf.HTTP.Port),
|
||||
Handler: mux,
|
||||
@@ -35,9 +52,10 @@ func NewHttp(conf *config.Config, mux *chi.Mux) (*hlfhr.Server, error) {
|
||||
})
|
||||
srv.Listen80RedirectTo443 = true
|
||||
|
||||
if conf.HTTP.TLS {
|
||||
if conf.HTTP.TLS && reloader != nil {
|
||||
srv.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
GetCertificate: reloader.GetCertificate,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -501,7 +501,7 @@ func (r *backupRepo) createMySQL(name string, storage storage.Storage, target st
|
||||
// 导出数据库
|
||||
name = name + ".sql"
|
||||
_ = os.Setenv("MYSQL_PWD", rootPassword)
|
||||
if _, err = shell.Execf(`mysqldump -u root '%s' > '%s'`, target, filepath.Join(tmpDir, name)); err != nil {
|
||||
if _, err = shell.Execf(`mysqldump -u root --single-transaction --quick '%s' > '%s'`, target, filepath.Join(tmpDir, name)); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Unsetenv("MYSQL_PWD")
|
||||
@@ -873,10 +873,10 @@ func (r *backupRepo) UpdatePanel(version, url, checksum string) error {
|
||||
if app.IsCli {
|
||||
fmt.Println(r.t.Get("|-Downloading..."))
|
||||
}
|
||||
if _, err := shell.Execf("wget -T 120 -t 3 -O /tmp/%s %s", name, url); err != nil {
|
||||
if _, err := shell.Execf("aria2c -c --file-allocation=falloc --allow-overwrite=true --auto-file-renaming=false --retry-wait=5 --max-tries=5 -x 16 -s 16 -k 1M -d /tmp -o %s %s", name, url); err != nil {
|
||||
return errors.New(r.t.Get("Download failed: %v", err))
|
||||
}
|
||||
if _, err := shell.Execf("wget -T 20 -t 3 -O /tmp/%s %s", name+".sha256", checksum); err != nil {
|
||||
if _, err := shell.Execf("aria2c -c --file-allocation=falloc --allow-overwrite=true --auto-file-renaming=false --retry-wait=5 --max-tries=5 -x 1 -s 1 -k 1M -d /tmp -o %s %s", name+".sha256", checksum); err != nil {
|
||||
return errors.New(r.t.Get("Download failed: %v", err))
|
||||
}
|
||||
if !io.Exists(filepath.Join("/tmp", name)) || !io.Exists(filepath.Join("/tmp", name+".sha256")) {
|
||||
@@ -971,6 +971,8 @@ func (r *backupRepo) UpdatePanel(version, url, checksum string) error {
|
||||
fmt.Println(r.t.Get("|-Update completed"))
|
||||
}
|
||||
|
||||
r.log.Info("panel updated", slog.String("version", version))
|
||||
|
||||
_, _ = shell.Execf("systemctl daemon-reload")
|
||||
_ = io.Remove("/tmp/panel-storage.zip")
|
||||
_ = io.Remove(filepath.Join(app.Root, "panel/config.example.yml"))
|
||||
|
||||
+9
-75
@@ -31,7 +31,6 @@ type certRepo struct {
|
||||
db *gorm.DB
|
||||
log *slog.Logger
|
||||
settingRepo biz.SettingRepo
|
||||
client *acme.Client
|
||||
}
|
||||
|
||||
func NewCertRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger, settingRepo biz.SettingRepo) biz.CertRepo {
|
||||
@@ -66,7 +65,7 @@ func (r *certRepo) List(page, limit uint) ([]*types.CertList, int64, error) {
|
||||
CreatedAt: cert.CreatedAt,
|
||||
UpdatedAt: cert.UpdatedAt,
|
||||
}
|
||||
if decode, err := pkgcert.ParseCert(cert.Cert); err == nil {
|
||||
if decode, err := pkgcert.ParseCert([]byte(cert.Cert)); err == nil {
|
||||
item.NotBefore = decode.NotBefore
|
||||
item.NotAfter = decode.NotAfter
|
||||
item.Issuer = decode.Issuer.CommonName
|
||||
@@ -96,11 +95,11 @@ func (r *certRepo) GetByWebsite(WebsiteID uint) (*biz.Cert, error) {
|
||||
}
|
||||
|
||||
func (r *certRepo) Upload(ctx context.Context, req *request.CertUpload) (*biz.Cert, error) {
|
||||
info, err := pkgcert.ParseCert(req.Cert)
|
||||
info, err := pkgcert.ParseCert([]byte(req.Cert))
|
||||
if err != nil {
|
||||
return nil, errors.New(r.t.Get("failed to parse certificate: %v", err))
|
||||
}
|
||||
if _, err = pkgcert.ParseKey(req.Key); err != nil {
|
||||
if _, err = pkgcert.ParseKey([]byte(req.Key)); err != nil {
|
||||
return nil, errors.New(r.t.Get("failed to parse private key: %v", err))
|
||||
}
|
||||
|
||||
@@ -146,7 +145,7 @@ func (r *certRepo) Create(ctx context.Context, req *request.CertCreate) (*biz.Ce
|
||||
}
|
||||
|
||||
func (r *certRepo) Update(ctx context.Context, req *request.CertUpdate) error {
|
||||
info, err := pkgcert.ParseCert(req.Cert)
|
||||
info, err := pkgcert.ParseCert([]byte(req.Cert))
|
||||
if err == nil && req.Type == "upload" {
|
||||
// 合并 DNSNames 和 IPAddresses
|
||||
req.Domains = info.DNSNames
|
||||
@@ -219,7 +218,7 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
ssl, err := client.ObtainCertificate(ctx, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
@@ -245,40 +244,6 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
return &ssl, nil
|
||||
}
|
||||
|
||||
func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
|
||||
cert, err := r.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.client == nil {
|
||||
return nil, errors.New(r.t.Get("please retry the manual obtain operation"))
|
||||
}
|
||||
|
||||
ssl, err := r.client.ObtainCertificateManual()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert.RenewalInfo = *ssl.RenewalInfo
|
||||
cert.CertURL = ssl.URL
|
||||
cert.Cert = string(ssl.ChainPEM)
|
||||
cert.Key = string(ssl.PrivateKey)
|
||||
if err = r.db.Save(cert).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
return &ssl, r.Deploy(cert.ID, cert.WebsiteID)
|
||||
}
|
||||
|
||||
if err = r.runScript(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ssl, nil
|
||||
}
|
||||
|
||||
func (r *certRepo) ObtainPanel(account *biz.CertAccount, ips []string) ([]byte, []byte, error) {
|
||||
client, err := acme.NewPrivateKeyAccount(account.Email, account.PrivateKey, acme.CALetsEncrypt, nil, r.log)
|
||||
if err != nil {
|
||||
@@ -399,7 +364,7 @@ func (r *certRepo) RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error) {
|
||||
return mholtacme.RenewalInfo{}, err
|
||||
}
|
||||
|
||||
crt, err := pkgcert.ParseCert(cert.Cert)
|
||||
crt, err := pkgcert.ParseCert([]byte(cert.Cert))
|
||||
if err != nil {
|
||||
return mholtacme.RenewalInfo{}, err
|
||||
}
|
||||
@@ -419,34 +384,6 @@ func (r *certRepo) RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error) {
|
||||
return renewInfo, nil
|
||||
}
|
||||
|
||||
func (r *certRepo) ManualDNS(id uint) ([]acme.DNSRecord, error) {
|
||||
cert, err := r.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := r.getClient(cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.UseManualDns()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
defer cancel()
|
||||
records, err := client.GetDNSRecords(ctx, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 15 分钟后清理客户端
|
||||
r.client = client
|
||||
time.AfterFunc(15*time.Minute, func() {
|
||||
r.client = nil
|
||||
})
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
cert, err := r.Get(ID)
|
||||
if err != nil {
|
||||
@@ -501,12 +438,9 @@ func (r *certRepo) runScript(cert *biz.Cert) error {
|
||||
if _, err = f.WriteString(cert.Script); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.Chmod(0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = f.Chmod(0755)
|
||||
_ = f.Close()
|
||||
defer func(name string) { _ = os.Remove(name) }(f.Name())
|
||||
|
||||
_, err = shell.Execf("bash " + f.Name())
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"gorm.io/gorm"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
@@ -211,6 +211,7 @@ func (r certAccountRepo) getGoogleEAB() (*acme.EAB, error) {
|
||||
} `json:"data"`
|
||||
}
|
||||
client := resty.New()
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
client.SetTimeout(5 * time.Second)
|
||||
client.SetRetryCount(3)
|
||||
|
||||
@@ -234,6 +235,7 @@ func (r certAccountRepo) getZeroSSLEAB(email string) (*acme.EAB, error) {
|
||||
EabHmacKey string `json:"eab_hmac_key"`
|
||||
}
|
||||
client := resty.New()
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
client.SetTimeout(5 * time.Second)
|
||||
client.SetRetryCount(3)
|
||||
|
||||
|
||||
@@ -100,15 +100,19 @@ func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// 拉取镜像
|
||||
out, err := apiClient.ImagePull(ctx, req.Name, client.ImagePullOptions{})
|
||||
// 获取镜像信息
|
||||
image, err := apiClient.ImageInspect(ctx, req.Image)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", fmt.Errorf("failed to inspect image: %v", err)
|
||||
}
|
||||
defer func(out client.ImagePullResponse) { _ = out.Close() }(out)
|
||||
|
||||
if err = out.Wait(ctx); err != nil {
|
||||
return "", err
|
||||
// 兼容一些没有指定命令和入口点的镜像
|
||||
if image.Config != nil {
|
||||
if len(req.Command) == 0 && len(image.Config.Cmd) > 0 {
|
||||
req.Command = image.Config.Cmd
|
||||
}
|
||||
if len(req.Entrypoint) == 0 && len(image.Config.Entrypoint) > 0 {
|
||||
req.Entrypoint = image.Config.Entrypoint
|
||||
}
|
||||
}
|
||||
|
||||
// 构建容器配置
|
||||
@@ -147,10 +151,10 @@ func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) {
|
||||
portMap := make(network.PortMap)
|
||||
for _, port := range req.Ports {
|
||||
if port.ContainerStart-port.ContainerEnd != port.HostStart-port.HostEnd {
|
||||
return "", fmt.Errorf("容器端口和主机端口数量不匹配(容器: %d 主机: %d)", port.ContainerStart-port.ContainerEnd, port.HostStart-port.HostEnd)
|
||||
return "", fmt.Errorf("container port and host port count do not match (container: %d host: %d)", port.ContainerStart-port.ContainerEnd, port.HostStart-port.HostEnd)
|
||||
}
|
||||
if port.ContainerStart > port.ContainerEnd || port.HostStart > port.HostEnd || port.ContainerStart < 1 || port.HostStart < 1 {
|
||||
return "", fmt.Errorf("端口范围不正确")
|
||||
return "", fmt.Errorf("port range is invalid")
|
||||
}
|
||||
|
||||
count := uint(0)
|
||||
|
||||
@@ -36,6 +36,12 @@ func (r *containerVolumeRepo) List() ([]types.ContainerVolume, error) {
|
||||
var volumes []types.ContainerVolume
|
||||
for _, item := range resp.Items {
|
||||
createdAt, _ := time.Parse(time.RFC3339Nano, item.CreatedAt)
|
||||
var refCount int64
|
||||
var size string
|
||||
if item.UsageData != nil {
|
||||
refCount = item.UsageData.RefCount
|
||||
size = tools.FormatBytes(float64(item.UsageData.Size))
|
||||
}
|
||||
volumes = append(volumes, types.ContainerVolume{
|
||||
Name: item.Name,
|
||||
Driver: item.Driver,
|
||||
@@ -44,8 +50,8 @@ func (r *containerVolumeRepo) List() ([]types.ContainerVolume, error) {
|
||||
CreatedAt: createdAt,
|
||||
Labels: types.MapToKV(item.Labels),
|
||||
Options: types.MapToKV(item.Options),
|
||||
RefCount: item.UsageData.RefCount,
|
||||
Size: tools.FormatBytes(float64(item.UsageData.Size)),
|
||||
RefCount: refCount,
|
||||
Size: size,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,13 @@ func NewDatabaseRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger, server biz
|
||||
}
|
||||
}
|
||||
|
||||
func (r *databaseRepo) List(page, limit uint) ([]*biz.Database, int64, error) {
|
||||
func (r *databaseRepo) List(page, limit uint, typ string) ([]*biz.Database, int64, error) {
|
||||
var databaseServer []*biz.DatabaseServer
|
||||
if err := r.db.Model(&biz.DatabaseServer{}).Order("id desc").Find(&databaseServer).Error; err != nil {
|
||||
query := r.db.Model(&biz.DatabaseServer{}).Order("id desc")
|
||||
if typ != "" {
|
||||
query = query.Where("type = ?", typ)
|
||||
}
|
||||
if err := query.Find(&databaseServer).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
|
||||
@@ -37,10 +37,14 @@ func (r *databaseServerRepo) Count() (int64, error) {
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *databaseServerRepo) List(page, limit uint) ([]*biz.DatabaseServer, int64, error) {
|
||||
func (r *databaseServerRepo) List(page, limit uint, typ string) ([]*biz.DatabaseServer, int64, error) {
|
||||
databaseServer := make([]*biz.DatabaseServer, 0)
|
||||
var total int64
|
||||
err := r.db.Model(&biz.DatabaseServer{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&databaseServer).Error
|
||||
query := r.db.Model(&biz.DatabaseServer{}).Order("id desc")
|
||||
if typ != "" {
|
||||
query = query.Where("type = ?", typ)
|
||||
}
|
||||
err := query.Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&databaseServer).Error
|
||||
|
||||
for server := range slices.Values(databaseServer) {
|
||||
r.checkServer(server)
|
||||
|
||||
@@ -39,10 +39,14 @@ func (r *databaseUserRepo) Count() (int64, error) {
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r *databaseUserRepo) List(page, limit uint) ([]*biz.DatabaseUser, int64, error) {
|
||||
func (r *databaseUserRepo) List(page, limit uint, typ string) ([]*biz.DatabaseUser, int64, error) {
|
||||
user := make([]*biz.DatabaseUser, 0)
|
||||
var total int64
|
||||
err := r.db.Model(&biz.DatabaseUser{}).Preload("Server").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&user).Error
|
||||
query := r.db.Model(&biz.DatabaseUser{}).Preload("Server").Order("id desc")
|
||||
if typ != "" {
|
||||
query = query.Joins("JOIN database_servers ON database_servers.id = database_users.server_id").Where("database_servers.type = ?", typ)
|
||||
}
|
||||
err := query.Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&user).Error
|
||||
|
||||
for u := range slices.Values(user) {
|
||||
r.fillUser(u)
|
||||
|
||||
@@ -41,6 +41,7 @@ func (r *environmentRepo) Types() []types.LV {
|
||||
{Label: "Node.js", Value: "nodejs"},
|
||||
{Label: "PHP", Value: "php"},
|
||||
{Label: "Python", Value: "python"},
|
||||
{Label: ".NET", Value: "dotnet"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +87,8 @@ func (r *environmentRepo) IsInstalled(typ, slug string) bool {
|
||||
binFile = filepath.Join(path, "bin", "php")
|
||||
case "python":
|
||||
binFile = filepath.Join(path, "bin", "python3")
|
||||
case "dotnet":
|
||||
binFile = filepath.Join(path, "dotnet")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -133,6 +136,9 @@ func (r *environmentRepo) InstalledVersion(typ, slug string) string {
|
||||
case "python":
|
||||
// Python 3.11.5 -> 3.11.5
|
||||
version, err = shell.Exec(filepath.Join(basePath, "bin", "python3") + " --version | awk '{print $2}'")
|
||||
case "dotnet":
|
||||
// 8.0.100
|
||||
version, err = shell.Exec(filepath.Join(basePath, "dotnet") + " --version")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
+89
-13
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,6 +16,12 @@ import (
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
)
|
||||
|
||||
var (
|
||||
logArchivePatternApp = regexp.MustCompile(`^app-(\d{4}-\d{2}-\d{2})T.*\.log$`)
|
||||
logArchivePatternDB = regexp.MustCompile(`^db-(\d{4}-\d{2}-\d{2})T.*\.log$`)
|
||||
logArchivePatternHTTP = regexp.MustCompile(`^http-(\d{4}-\d{2}-\d{2})T.*\.log$`)
|
||||
)
|
||||
|
||||
type logRepo struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
@@ -25,20 +33,43 @@ func NewLogRepo(db *gorm.DB) biz.LogRepo {
|
||||
}
|
||||
|
||||
// List 获取日志列表
|
||||
func (r *logRepo) List(logType string, limit int) ([]biz.LogEntry, error) {
|
||||
var filename string
|
||||
switch logType {
|
||||
case biz.LogTypeApp:
|
||||
filename = "app.log"
|
||||
case biz.LogTypeDB:
|
||||
filename = "db.log"
|
||||
case biz.LogTypeHTTP:
|
||||
filename = "http.log"
|
||||
default:
|
||||
filename = "app.log"
|
||||
}
|
||||
// date 格式为 YYYY-MM-DD,空字符串表示当天日志
|
||||
func (r *logRepo) List(logType string, limit int, date string) ([]biz.LogEntry, error) {
|
||||
logDir := filepath.Join(app.Root, "panel/storage/logs")
|
||||
|
||||
logPath := filepath.Join(app.Root, "panel/storage/logs", filename)
|
||||
var logPath string
|
||||
if date == "" {
|
||||
// 无日期参数,读取当前日志文件
|
||||
logPath = filepath.Join(logDir, logType+".log")
|
||||
} else {
|
||||
// 有日期参数,查找对应的归档日志文件
|
||||
pattern := getLogArchivePattern(logType)
|
||||
|
||||
entries, err := os.ReadDir(logDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []biz.LogEntry{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
matches := pattern.FindStringSubmatch(entry.Name())
|
||||
if len(matches) == 2 && matches[1] == date {
|
||||
logPath = filepath.Join(logDir, entry.Name())
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return []biz.LogEntry{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.Open(logPath)
|
||||
if err != nil {
|
||||
@@ -92,6 +123,37 @@ func (r *logRepo) List(logType string, limit int) ([]biz.LogEntry, error) {
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// ListDates 获取可用的日志日期列表
|
||||
func (r *logRepo) ListDates(logType string) ([]string, error) {
|
||||
logDir := filepath.Join(app.Root, "panel/storage/logs")
|
||||
|
||||
entries, err := os.ReadDir(logDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []string{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pattern := getLogArchivePattern(logType)
|
||||
|
||||
dates := make([]string, 0)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
matches := pattern.FindStringSubmatch(entry.Name())
|
||||
if len(matches) == 2 {
|
||||
dates = append(dates, matches[1])
|
||||
}
|
||||
}
|
||||
|
||||
// 按日期倒序排列,最新的在前面
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(dates)))
|
||||
|
||||
return dates, nil
|
||||
}
|
||||
|
||||
// fillOperatorNames 填充操作员用户名
|
||||
func (r *logRepo) fillOperatorNames(entries []biz.LogEntry) {
|
||||
// 收集所有用户ID
|
||||
@@ -177,3 +239,17 @@ func (r *logRepo) parseLine(line string, logType string) (biz.LogEntry, error) {
|
||||
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// getLogArchivePattern 获取归档日志文件名匹配正则表达式
|
||||
func getLogArchivePattern(logType string) *regexp.Regexp {
|
||||
switch logType {
|
||||
case biz.LogTypeApp:
|
||||
return logArchivePatternApp
|
||||
case biz.LogTypeDB:
|
||||
return logArchivePatternDB
|
||||
case biz.LogTypeHTTP:
|
||||
return logArchivePatternHTTP
|
||||
default:
|
||||
return logArchivePatternApp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ func (r *projectRepo) Create(ctx context.Context, req *request.ProjectCreate) (*
|
||||
project := &biz.Project{
|
||||
Name: req.Name,
|
||||
Type: req.Type,
|
||||
Path: req.RootDir,
|
||||
Path: lo.If(!strings.HasPrefix(req.RootDir, "/"), filepath.Join("/", req.RootDir)).Else(req.RootDir),
|
||||
}
|
||||
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
@@ -108,8 +108,13 @@ func (r *projectRepo) Create(ctx context.Context, req *request.ProjectCreate) (*
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建项目目录
|
||||
if err := os.MkdirAll(project.Path, 0755); err != nil {
|
||||
return fmt.Errorf("%s: %w", r.t.Get("failed to create project directory"), err)
|
||||
}
|
||||
|
||||
// 生成 systemd unit 文件
|
||||
if err := r.generateUnitFile(project.ID, req); err != nil {
|
||||
if err := r.generateUnitFile(req); err != nil {
|
||||
return fmt.Errorf("%s: %w", r.t.Get("failed to generate systemd config"), err)
|
||||
}
|
||||
|
||||
@@ -141,7 +146,7 @@ func (r *projectRepo) Update(ctx context.Context, req *request.ProjectUpdate) er
|
||||
project.Name = req.Name
|
||||
}
|
||||
|
||||
project.Path = req.RootDir
|
||||
project.Path = lo.If(!strings.HasPrefix(req.RootDir, "/"), filepath.Join("/", req.RootDir)).Else(req.RootDir)
|
||||
if err := r.db.Save(project).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -368,7 +373,10 @@ func (r *projectRepo) parsePercent(value string) (float64, error) {
|
||||
}
|
||||
|
||||
// generateUnitFile 生成 systemd unit 文件
|
||||
func (r *projectRepo) generateUnitFile(id uint, req *request.ProjectCreate) error {
|
||||
func (r *projectRepo) generateUnitFile(req *request.ProjectCreate) error {
|
||||
req.RootDir = lo.If(!strings.HasPrefix(req.RootDir, "/"), filepath.Join("/", req.RootDir)).Else(req.RootDir)
|
||||
req.WorkingDir = lo.If(req.WorkingDir != "", req.WorkingDir).Else(req.RootDir)
|
||||
req.WorkingDir = lo.If(!strings.HasPrefix(req.WorkingDir, "/"), filepath.Join("/", req.WorkingDir)).Else(req.WorkingDir)
|
||||
options := []*unit.UnitOption{
|
||||
// [Unit] section
|
||||
unit.NewUnitOption("Unit", "Description", req.Description),
|
||||
@@ -376,7 +384,7 @@ func (r *projectRepo) generateUnitFile(id uint, req *request.ProjectCreate) erro
|
||||
|
||||
// [Service] section
|
||||
unit.NewUnitOption("Service", "Type", "simple"),
|
||||
unit.NewUnitOption("Service", "WorkingDirectory", lo.If(req.WorkingDir != "", req.WorkingDir).Else(req.RootDir)),
|
||||
unit.NewUnitOption("Service", "WorkingDirectory", req.WorkingDir),
|
||||
}
|
||||
|
||||
if req.ExecStart != "" {
|
||||
@@ -407,11 +415,19 @@ func (r *projectRepo) generateUnitFile(id uint, req *request.ProjectCreate) erro
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(unitPath, content, 0644)
|
||||
if err = os.WriteFile(unitPath, content, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return systemctl.DaemonReload()
|
||||
}
|
||||
|
||||
// updateUnitFile 更新 systemd unit 文件
|
||||
func (r *projectRepo) updateUnitFile(name string, req *request.ProjectUpdate) error {
|
||||
req.RootDir = lo.If(!strings.HasPrefix(req.RootDir, "/"), filepath.Join("/", req.RootDir)).Else(req.RootDir)
|
||||
req.WorkingDir = lo.If(req.WorkingDir != "", req.WorkingDir).Else(req.RootDir)
|
||||
req.WorkingDir = lo.If(!strings.HasPrefix(req.WorkingDir, "/"), filepath.Join("/", req.WorkingDir)).Else(req.WorkingDir)
|
||||
|
||||
options := []*unit.UnitOption{
|
||||
// [Unit] section
|
||||
unit.NewUnitOption("Unit", "Description", req.Description),
|
||||
@@ -436,7 +452,7 @@ func (r *projectRepo) updateUnitFile(name string, req *request.ProjectUpdate) er
|
||||
|
||||
// [Service] section
|
||||
options = append(options, unit.NewUnitOption("Service", "Type", "simple"))
|
||||
options = append(options, unit.NewUnitOption("Service", "WorkingDirectory", lo.If(req.WorkingDir != "", req.WorkingDir).Else(req.RootDir)))
|
||||
options = append(options, unit.NewUnitOption("Service", "WorkingDirectory", req.WorkingDir))
|
||||
|
||||
if req.ExecStartPre != "" {
|
||||
options = append(options, unit.NewUnitOption("Service", "ExecStartPre", req.ExecStartPre))
|
||||
@@ -526,5 +542,9 @@ func (r *projectRepo) updateUnitFile(name string, req *request.ProjectUpdate) er
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(unitPath, content, 0644)
|
||||
if err = os.WriteFile(unitPath, content, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return systemctl.DaemonReload()
|
||||
}
|
||||
|
||||
+7
-26
@@ -4,12 +4,10 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/pkg/firewall"
|
||||
"github.com/acepanel/panel/pkg/os"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
)
|
||||
|
||||
type safeRepo struct {
|
||||
@@ -31,38 +29,21 @@ func NewSafeRepo(log *slog.Logger) biz.SafeRepo {
|
||||
}
|
||||
|
||||
func (r *safeRepo) GetPingStatus() (bool, error) {
|
||||
out, err := shell.Execf(`firewall-cmd --list-rich-rules`)
|
||||
if err != nil { // 可能防火墙已关闭等
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !strings.Contains(out, `rule protocol value="icmp" drop`) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
fw := firewall.NewFirewall()
|
||||
return fw.PingStatus()
|
||||
}
|
||||
|
||||
func (r *safeRepo) UpdatePingStatus(ctx context.Context, status bool) error {
|
||||
fw, err := firewall.NewFirewall().Status()
|
||||
fw := firewall.NewFirewall()
|
||||
running, err := fw.Status()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fw {
|
||||
return fmt.Errorf("failed to update ping status: firewalld is not running")
|
||||
if !running {
|
||||
return fmt.Errorf("failed to update ping status: firewall is not running")
|
||||
}
|
||||
|
||||
if status {
|
||||
_, err = shell.Execf(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`)
|
||||
} else {
|
||||
_, err = shell.Execf(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = shell.Execf(`firewall-cmd --reload`)
|
||||
if err != nil {
|
||||
if err = fw.UpdatePingStatus(status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"github.com/acepanel/panel/pkg/firewall"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/os"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type settingRepo struct {
|
||||
@@ -284,10 +283,10 @@ func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel
|
||||
}
|
||||
restartFlag = true
|
||||
}
|
||||
if _, err := cert.ParseCert(req.Cert); err != nil {
|
||||
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(req.Key); err != nil {
|
||||
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 {
|
||||
@@ -308,14 +307,15 @@ func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel
|
||||
return false, errors.New(r.t.Get("port is already in use"))
|
||||
}
|
||||
// 放行端口
|
||||
if ok, _ := systemctl.IsEnabled("firewalld"); ok {
|
||||
fw := firewall.NewFirewall()
|
||||
fw := firewall.NewFirewall()
|
||||
if ok, _ := fw.Status(); ok {
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: req.Port,
|
||||
PortEnd: req.Port,
|
||||
Direction: firewall.DirectionIn,
|
||||
Protocol: firewall.ProtocolTCPUDP,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
Direction: firewall.DirectionIn,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -357,10 +357,10 @@ func (r *settingRepo) UpdateCert(req *request.SettingCert) error {
|
||||
if r.task.HasRunningTask() {
|
||||
return errors.New(r.t.Get("background task is running, modifying some settings is prohibited, please try again later"))
|
||||
}
|
||||
if _, err := cert.ParseCert(req.Cert); err != nil {
|
||||
if _, err := cert.ParseCert([]byte(req.Cert)); err != nil {
|
||||
return errors.New(r.t.Get("failed to parse certificate: %v", err))
|
||||
}
|
||||
if _, err := cert.ParseKey(req.Key); err != nil {
|
||||
if _, err := cert.ParseKey([]byte(req.Key)); err != nil {
|
||||
return errors.New(r.t.Get("failed to parse private key: %v", err))
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ type templateRepo struct {
|
||||
t *gotext.Locale
|
||||
cache biz.CacheRepo
|
||||
api *api.API
|
||||
firewall *firewall.Firewall
|
||||
firewall firewall.Firewall
|
||||
}
|
||||
|
||||
func NewTemplateRepo(t *gotext.Locale, cache biz.CacheRepo) biz.TemplateRepo {
|
||||
|
||||
+42
-17
@@ -176,10 +176,10 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
|
||||
setting.SSLCiphers = sslConfig.Ciphers
|
||||
}
|
||||
// 证书
|
||||
crt, _ := io.Read(filepath.Join(app.Root, "sites", website.Name, "config", "fullchain.pem"))
|
||||
setting.SSLCert = crt
|
||||
key, _ := io.Read(filepath.Join(app.Root, "sites", website.Name, "config", "private.key"))
|
||||
setting.SSLKey = key
|
||||
crt, _ := os.ReadFile(filepath.Join(app.Root, "sites", website.Name, "config", "fullchain.pem"))
|
||||
setting.SSLCert = string(crt)
|
||||
key, _ := os.ReadFile(filepath.Join(app.Root, "sites", website.Name, "config", "private.key"))
|
||||
setting.SSLKey = string(key)
|
||||
// 解析证书信息
|
||||
if decode, err := cert.ParseCert(crt); err == nil {
|
||||
setting.SSLNotBefore = decode.NotBefore.Format(time.DateTime)
|
||||
@@ -259,7 +259,7 @@ func (r *websiteRepo) List(typ string, page, limit uint) ([]*biz.Website, int64,
|
||||
|
||||
// 取证书剩余有效时间和PHP版本
|
||||
for _, website := range websites {
|
||||
crt, _ := io.Read(filepath.Join(app.Root, "sites", website.Name, "config", "fullchain.pem"))
|
||||
crt, _ := os.ReadFile(filepath.Join(app.Root, "sites", website.Name, "config", "fullchain.pem"))
|
||||
if decode, err := cert.ParseCert(crt); err == nil {
|
||||
hours := time.Until(decode.NotAfter).Hours()
|
||||
website.CertExpire = fmt.Sprintf("%.2f", hours/24)
|
||||
@@ -605,12 +605,28 @@ func (r *websiteRepo) Update(ctx context.Context, req *request.WebsiteUpdate) er
|
||||
}
|
||||
website.SSL = req.SSL
|
||||
if req.SSL {
|
||||
if _, err = cert.ParseCert(req.SSLCert); err != nil {
|
||||
if _, err = cert.ParseCert([]byte(req.SSLCert)); err != nil {
|
||||
return errors.New(r.t.Get("failed to parse certificate: %v", err))
|
||||
}
|
||||
if _, err = cert.ParseKey(req.SSLKey); err != nil {
|
||||
if _, err = cert.ParseKey([]byte(req.SSLKey)); err != nil {
|
||||
return errors.New(r.t.Get("failed to parse private key: %v", err))
|
||||
}
|
||||
// 检查证书是否已存在于面板的证书管理中,如果不存在则作为本地证书上传
|
||||
var certCount int64
|
||||
r.db.Model(&biz.Cert{}).Where("cert = ?", strings.TrimSpace(req.SSLCert)).Count(&certCount)
|
||||
if certCount == 0 {
|
||||
certInfo, _ := cert.ParseCert([]byte(req.SSLCert))
|
||||
sans := certInfo.DNSNames
|
||||
for _, ip := range certInfo.IPAddresses {
|
||||
sans = append(sans, ip.String())
|
||||
}
|
||||
r.db.Create(&biz.Cert{
|
||||
Type: "upload",
|
||||
Domains: sans,
|
||||
Cert: req.SSLCert,
|
||||
Key: req.SSLKey,
|
||||
})
|
||||
}
|
||||
quic := false
|
||||
for _, listen := range req.Listens {
|
||||
if slices.Contains(listen.Args, "quic") {
|
||||
@@ -925,10 +941,10 @@ func (r *websiteRepo) UpdateCert(req *request.WebsiteUpdateCert) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := cert.ParseCert(req.Cert); err != nil {
|
||||
if _, err := cert.ParseCert([]byte(req.Cert)); err != nil {
|
||||
return errors.New(r.t.Get("failed to parse certificate: %v", err))
|
||||
}
|
||||
if _, err := cert.ParseKey(req.Key); err != nil {
|
||||
if _, err := cert.ParseKey([]byte(req.Key)); err != nil {
|
||||
return errors.New(r.t.Get("failed to parse private key: %v", err))
|
||||
}
|
||||
|
||||
@@ -1186,12 +1202,12 @@ func (r *websiteRepo) reloadWebServer() error {
|
||||
func (r *websiteRepo) readBasicAuthUsers(siteName string) map[string]string {
|
||||
htpasswdPath := filepath.Join(app.Root, "sites", siteName, "htpasswd")
|
||||
if !io.Exists(htpasswdPath) {
|
||||
return nil
|
||||
return make(map[string]string)
|
||||
}
|
||||
|
||||
file, err := os.Open(htpasswdPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
return make(map[string]string)
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
@@ -1202,27 +1218,36 @@ func (r *websiteRepo) readBasicAuthUsers(siteName string) map[string]string {
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
// htpasswd 格式: username:password
|
||||
// htpasswd 格式: username:{PLAIN}password或直接username:password
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
users[parts[0]] = parts[1]
|
||||
users[parts[0]] = strings.TrimPrefix(parts[1], "{PLAIN}")
|
||||
}
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
return nil
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
// writeBasicAuthUsers 将用户凭证写入 htpasswd 文件
|
||||
func (r *websiteRepo) writeBasicAuthUsers(htpasswdPath string, users map[string]string) error {
|
||||
webServer, err := r.setting.Get(biz.SettingKeyWebserver, "unknown")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var lines []string
|
||||
for username, password := range users {
|
||||
if username == "" || password == "" {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("%s:%s", username, password))
|
||||
switch webServer {
|
||||
case "nginx":
|
||||
lines = append(lines, fmt.Sprintf("%s:%s", username, "{PLAIN}"+password))
|
||||
case "apache":
|
||||
lines = append(lines, fmt.Sprintf("%s:%s", username, password))
|
||||
default:
|
||||
return errors.New(r.t.Get("unsupported web server: %s", webServer))
|
||||
}
|
||||
}
|
||||
|
||||
content := strings.Join(lines, "\n")
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/DeRuina/timberjack"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/httplog/v3"
|
||||
"github.com/google/wire"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/sessions"
|
||||
sessionmiddleware "github.com/libtnb/sessions/middleware"
|
||||
@@ -54,18 +51,10 @@ func NewMiddlewares(conf *config.Config, session *sessions.Manager, appRepo biz.
|
||||
|
||||
// Globals is a collection of global middleware that will be applied to every request.
|
||||
func (r *Middlewares) Globals(t *gotext.Locale, mux *chi.Mux) []func(http.Handler) http.Handler {
|
||||
compressor := middleware.NewCompressor(6)
|
||||
compressor.SetEncoder("gzip", func(w io.Writer, level int) io.Writer {
|
||||
writer, _ := gzip.NewWriterLevel(w, level)
|
||||
return writer
|
||||
})
|
||||
compressor.SetEncoder("zstd", func(w io.Writer, level int) io.Writer {
|
||||
writer, _ := zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.SpeedBetterCompression))
|
||||
return writer
|
||||
})
|
||||
compressor := chimiddleware.NewCompressor(6)
|
||||
|
||||
return []func(http.Handler) http.Handler{
|
||||
middleware.Recoverer,
|
||||
Recoverer,
|
||||
httplog.RequestLogger(r.log, &httplog.Options{
|
||||
Level: slog.LevelInfo,
|
||||
LogRequestHeaders: []string{"User-Agent"},
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// Recoverer 捕获 panic,记录日志并返回 JSON 错误响应
|
||||
func Recoverer(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if rvr := recover(); rvr != nil {
|
||||
if rvr == http.ErrAbortHandler {
|
||||
panic(rvr)
|
||||
}
|
||||
|
||||
slog.Error(fmt.Sprintf("%v", rvr), slog.String("stack", string(debug.Stack())))
|
||||
|
||||
// WebSocket 等升级连接无法写入 HTTP 响应
|
||||
if r.Header.Get("Connection") == "Upgrade" {
|
||||
return
|
||||
}
|
||||
|
||||
Abort(w, http.StatusInternalServerError, fmt.Sprintf("%v", rvr))
|
||||
}
|
||||
}()
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package request
|
||||
|
||||
import "net/http"
|
||||
|
||||
type CertUpload struct {
|
||||
Cert string `form:"cert" json:"cert" validate:"required"`
|
||||
Key string `form:"key" json:"key" validate:"required"`
|
||||
@@ -14,6 +16,12 @@ type CertCreate struct {
|
||||
WebsiteID uint `form:"website_id" json:"website_id"`
|
||||
}
|
||||
|
||||
func (r *CertCreate) Rules(_ *http.Request) map[string]string {
|
||||
return map[string]string{
|
||||
"Domains.*": "required",
|
||||
}
|
||||
}
|
||||
|
||||
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"`
|
||||
@@ -27,6 +35,12 @@ type CertUpdate struct {
|
||||
WebsiteID uint `form:"website_id" json:"website_id"`
|
||||
}
|
||||
|
||||
func (r *CertUpdate) Rules(_ *http.Request) map[string]string {
|
||||
return map[string]string{
|
||||
"Domains.*": "required",
|
||||
}
|
||||
}
|
||||
|
||||
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"`
|
||||
|
||||
@@ -12,7 +12,7 @@ type ContainerRename struct {
|
||||
}
|
||||
|
||||
type ContainerCreate struct {
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Name string `form:"name" json:"name"`
|
||||
Image string `form:"image" json:"image" validate:"required"`
|
||||
Ports []types.ContainerPort `form:"ports" json:"ports"`
|
||||
Network string `form:"network" json:"network"`
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package request
|
||||
|
||||
type DatabaseList struct {
|
||||
Paginate
|
||||
Type string `form:"type" json:"type" query:"type"`
|
||||
}
|
||||
|
||||
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_-]+$"`
|
||||
|
||||
@@ -13,3 +13,35 @@ type EnvironmentPHPUpdateConfig struct {
|
||||
Version uint `json:"version"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// EnvironmentPHPConfigTune PHP 配置调整
|
||||
type EnvironmentPHPConfigTune struct {
|
||||
Version uint `json:"version"`
|
||||
// php.ini 常规设置
|
||||
ShortOpenTag string `form:"short_open_tag" json:"short_open_tag"`
|
||||
DateTimezone string `form:"date_timezone" json:"date_timezone"`
|
||||
DisplayErrors string `form:"display_errors" json:"display_errors"`
|
||||
ErrorReporting string `form:"error_reporting" json:"error_reporting"`
|
||||
// php.ini 禁用函数
|
||||
DisableFunctions string `form:"disable_functions" json:"disable_functions"`
|
||||
// php.ini 上传限制
|
||||
UploadMaxFilesize string `form:"upload_max_filesize" json:"upload_max_filesize"`
|
||||
PostMaxSize string `form:"post_max_size" json:"post_max_size"`
|
||||
MaxFileUploads string `form:"max_file_uploads" json:"max_file_uploads"`
|
||||
MemoryLimit string `form:"memory_limit" json:"memory_limit"`
|
||||
// php.ini 超时限制
|
||||
MaxExecutionTime string `form:"max_execution_time" json:"max_execution_time"`
|
||||
MaxInputTime string `form:"max_input_time" json:"max_input_time"`
|
||||
MaxInputVars string `form:"max_input_vars" json:"max_input_vars"`
|
||||
// php.ini Session 相关
|
||||
SessionSaveHandler string `form:"session_save_handler" json:"session_save_handler"`
|
||||
SessionSavePath string `form:"session_save_path" json:"session_save_path"`
|
||||
SessionGcMaxlifetime string `form:"session_gc_maxlifetime" json:"session_gc_maxlifetime"`
|
||||
SessionCookieLifetime string `form:"session_cookie_lifetime" json:"session_cookie_lifetime"`
|
||||
// php-fpm.conf 相关
|
||||
Pm string `form:"pm" json:"pm"`
|
||||
PmMaxChildren string `form:"pm_max_children" json:"pm_max_children"`
|
||||
PmStartServers string `form:"pm_start_servers" json:"pm_start_servers"`
|
||||
PmMinSpareServers string `form:"pm_min_spare_servers" json:"pm_min_spare_servers"`
|
||||
PmMaxSpareServers string `form:"pm_max_spare_servers" json:"pm_max_spare_servers"`
|
||||
}
|
||||
|
||||
@@ -56,6 +56,12 @@ type FileCompress struct {
|
||||
File string `form:"file" json:"file" validate:"required|isUnixPath"`
|
||||
}
|
||||
|
||||
func (r *FileCompress) Rules(_ *http.Request) map[string]string {
|
||||
return map[string]string{
|
||||
"Paths.*": "required",
|
||||
}
|
||||
}
|
||||
|
||||
type FileUnCompress struct {
|
||||
File string `form:"file" json:"file" validate:"required|isUnixPath"`
|
||||
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
|
||||
@@ -67,6 +73,7 @@ type ChunkUploadStart struct {
|
||||
FileName string `json:"file_name" validate:"required"` // 文件名
|
||||
FileHash string `json:"file_hash" validate:"required|len:64"` // 文件SHA256
|
||||
ChunkCount int `json:"chunk_count" validate:"required|min:1"` // 分块总数
|
||||
Force bool `json:"force"` // 是否覆盖已存在文件
|
||||
}
|
||||
|
||||
// ChunkUploadFinish 分块上传完成请求
|
||||
@@ -75,4 +82,5 @@ type ChunkUploadFinish struct {
|
||||
FileName string `json:"file_name" validate:"required"` // 文件名
|
||||
FileHash string `json:"file_hash" validate:"required|len:64"` // 文件SHA256
|
||||
ChunkCount int `json:"chunk_count" validate:"required|min:1"` // 分块总数
|
||||
Force bool `json:"force"` // 是否覆盖已存在文件
|
||||
}
|
||||
|
||||
@@ -4,3 +4,7 @@ type HomeCurrent struct {
|
||||
Nets []string `json:"nets" form:"nets"`
|
||||
Disks []string `json:"disks" form:"disks"`
|
||||
}
|
||||
|
||||
type HomeTopProcesses struct {
|
||||
Type string `json:"type" form:"type" query:"type" validate:"in:cpu,memory,disk_io"`
|
||||
}
|
||||
|
||||
@@ -4,4 +4,10 @@ package request
|
||||
type LogList struct {
|
||||
Type string `json:"type" form:"type" query:"type" validate:"required|in:app,db,http"`
|
||||
Limit int `json:"limit" form:"limit" query:"limit" validate:"min:1|max:1000"`
|
||||
Date string `json:"date" form:"date" query:"date"` // 日期,格式为 YYYY-MM-DD,空表示当天
|
||||
}
|
||||
|
||||
// LogDates 日志日期列表请求
|
||||
type LogDates struct {
|
||||
Type string `json:"type" form:"type" query:"type" validate:"required|in:app,db,http"`
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import "github.com/acepanel/panel/pkg/types"
|
||||
|
||||
type ProjectCreate struct {
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Type types.ProjectType `form:"type" json:"type" validate:"required|in:general,php,java,go,python,nodejs"`
|
||||
Type types.ProjectType `form:"type" json:"type" validate:"required|in:general,php,java,go,python,nodejs,dotnet"`
|
||||
Description string `form:"description" json:"description"`
|
||||
RootDir string `form:"root_dir" json:"root_dir"`
|
||||
WorkingDir string `form:"working_dir" json:"working_dir"`
|
||||
|
||||
@@ -32,9 +32,7 @@ type SettingPanel struct {
|
||||
|
||||
func (r *SettingPanel) Rules(_ *http.Request) map[string]string {
|
||||
return map[string]string{
|
||||
"BindDomain.*": "required",
|
||||
"BindIP.*": "required|ipcidr",
|
||||
"BindUA.*": "required",
|
||||
"BindIP.*": "ipcidr",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,3 +40,7 @@ type SettingCert struct {
|
||||
Cert string `json:"cert" validate:"required"`
|
||||
Key string `json:"key" validate:"required"`
|
||||
}
|
||||
|
||||
type SettingMemo struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ type ToolboxDiskDevice struct {
|
||||
// ToolboxDiskMount 挂载请求
|
||||
type ToolboxDiskMount struct {
|
||||
Device string `form:"device" json:"device" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
|
||||
WriteFstab bool `form:"write_fstab" json:"write_fstab"`
|
||||
MountOption string `form:"mount_option" json:"mount_option"`
|
||||
}
|
||||
|
||||
// ToolboxDiskUmount 卸载请求
|
||||
type ToolboxDiskUmount struct {
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
|
||||
}
|
||||
|
||||
// ToolboxDiskFormat 格式化请求
|
||||
@@ -44,12 +44,12 @@ type ToolboxDiskVGName struct {
|
||||
|
||||
// ToolboxDiskLVPath 逻辑卷路径请求
|
||||
type ToolboxDiskLVPath struct {
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
|
||||
}
|
||||
|
||||
// ToolboxDiskExtendLV 扩容逻辑卷请求
|
||||
type ToolboxDiskExtendLV struct {
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
|
||||
Size int `form:"size" json:"size" validate:"required|min:1"`
|
||||
Resize bool `form:"resize" json:"resize"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package request
|
||||
|
||||
// ToolboxMigrationConnection 迁移连接信息
|
||||
type ToolboxMigrationConnection struct {
|
||||
URL string `json:"url" validate:"required"`
|
||||
TokenID uint `json:"token_id" validate:"required"`
|
||||
Token string `json:"token" validate:"required"`
|
||||
}
|
||||
|
||||
// ToolboxMigrationItems 迁移选择项
|
||||
type ToolboxMigrationItems struct {
|
||||
Websites []ToolboxMigrationWebsite `json:"websites"`
|
||||
Databases []ToolboxMigrationDatabase `json:"databases"`
|
||||
DatabaseUsers []ToolboxMigrationDatabaseUser `json:"database_users"`
|
||||
Projects []ToolboxMigrationProject `json:"projects"`
|
||||
StopOnMig bool `json:"stop_on_mig"` // 迁移中是否停止服务
|
||||
}
|
||||
|
||||
// ToolboxMigrationWebsite 迁移网站项
|
||||
type ToolboxMigrationWebsite struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"` // 网站目录
|
||||
}
|
||||
|
||||
// ToolboxMigrationDatabase 迁移数据库项
|
||||
type ToolboxMigrationDatabase struct {
|
||||
Type string `json:"type"` // mysql / postgresql
|
||||
Name string `json:"name"`
|
||||
ServerID uint `json:"server_id"`
|
||||
Server string `json:"server"` // 服务器名称
|
||||
}
|
||||
|
||||
// ToolboxMigrationDatabaseUser 迁移数据库用户项
|
||||
type ToolboxMigrationDatabaseUser struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Host string `json:"host"` // 仅 mysql
|
||||
ServerID uint `json:"server_id"`
|
||||
Server string `json:"server"` // 服务器名称
|
||||
Type string `json:"type"` // mysql / postgresql
|
||||
}
|
||||
|
||||
// ToolboxMigrationProject 迁移项目项
|
||||
type ToolboxMigrationProject struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"` // 项目目录
|
||||
}
|
||||
|
||||
// ToolboxMigrationExec 远程执行命令请求
|
||||
type ToolboxMigrationExec struct {
|
||||
Command string `json:"command" validate:"required"`
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package request
|
||||
|
||||
// ToolboxNetworkList 网络连接列表请求
|
||||
type ToolboxNetworkList struct {
|
||||
Page uint `json:"page" form:"page" query:"page"`
|
||||
Limit uint `json:"limit" form:"limit" query:"limit"`
|
||||
Sort string `json:"sort" form:"sort" query:"sort" validate:"in:type,pid,process"`
|
||||
Order string `json:"order" form:"order" query:"order" validate:"in:asc,desc"`
|
||||
State string `json:"state" form:"state" query:"state"` // 逗号分隔多选: LISTEN,ESTABLISHED
|
||||
PID string `json:"pid" form:"pid" query:"pid"` // PID 搜索
|
||||
Process string `json:"process" form:"process" query:"process"` // 进程名称搜索
|
||||
Port string `json:"port" form:"port" query:"port"` // 端口搜索
|
||||
}
|
||||
@@ -15,7 +15,7 @@ type UserTokenCreate struct {
|
||||
|
||||
func (r *UserTokenCreate) Rules(_ *http.Request) map[string]string {
|
||||
return map[string]string{
|
||||
"IPs.*": "required|ipcidr",
|
||||
"IPs.*": "ipcidr",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,6 @@ type UserTokenUpdate struct {
|
||||
|
||||
func (r *UserTokenUpdate) Rules(_ *http.Request) map[string]string {
|
||||
return map[string]string{
|
||||
"IPs.*": "required|ipcidr",
|
||||
"IPs.*": "ipcidr",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/acepanel/panel/pkg/webserver/types"
|
||||
)
|
||||
|
||||
@@ -12,6 +14,12 @@ type WebsiteDefaultConfig struct {
|
||||
CipherSuites string `json:"cipher_suites" form:"cipher_suites" validate:"required"`
|
||||
}
|
||||
|
||||
func (r *WebsiteDefaultConfig) Rules(_ *http.Request) map[string]string {
|
||||
return map[string]string{
|
||||
"TLSVersions.*": "required",
|
||||
}
|
||||
}
|
||||
|
||||
type WebsiteList struct {
|
||||
Type string `json:"type" form:"type" validate:"required|in:all,proxy,static,php"`
|
||||
Paginate
|
||||
@@ -34,6 +42,13 @@ type WebsiteCreate struct {
|
||||
Proxy string `form:"proxy" json:"proxy" validate:"requiredIf:Type,proxy"` // 仅反向代理网站需要
|
||||
}
|
||||
|
||||
func (r *WebsiteCreate) Rules(_ *http.Request) map[string]string {
|
||||
return map[string]string{
|
||||
"Listens.*": "required",
|
||||
"Domains.*": "required",
|
||||
}
|
||||
}
|
||||
|
||||
type WebsiteDelete struct {
|
||||
ID uint `form:"id" json:"id" validate:"required|exists:websites,id"`
|
||||
Path bool `form:"path" json:"path"`
|
||||
@@ -81,6 +96,14 @@ type WebsiteUpdate struct {
|
||||
CustomConfigs []WebsiteCustomConfig `json:"custom_configs"`
|
||||
}
|
||||
|
||||
func (r *WebsiteUpdate) Rules(_ *http.Request) map[string]string {
|
||||
return map[string]string{
|
||||
"Listens.*": "required",
|
||||
"Domains.*": "required",
|
||||
"Index.*": "required",
|
||||
}
|
||||
}
|
||||
|
||||
// WebsiteCustomConfig 网站自定义配置请求
|
||||
type WebsiteCustomConfig struct {
|
||||
Name string `json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` // 配置名称
|
||||
|
||||
@@ -3,6 +3,7 @@ package job
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
@@ -13,7 +14,6 @@ import (
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
pkgcert "github.com/acepanel/panel/pkg/cert"
|
||||
"github.com/acepanel/panel/pkg/config"
|
||||
"github.com/acepanel/panel/pkg/tools"
|
||||
)
|
||||
|
||||
// CertRenew 证书续签
|
||||
@@ -74,14 +74,16 @@ func (r *CertRenew) Run() {
|
||||
|
||||
// 面板证书续签
|
||||
if r.conf.HTTP.ACME {
|
||||
decode, err := pkgcert.ParseCert(filepath.Join(app.Root, "panel/storage/cert.pem"))
|
||||
if err != nil {
|
||||
crt, _ := os.ReadFile(filepath.Join(app.Root, "panel/storage/cert.pem"))
|
||||
decode, err := pkgcert.ParseCert(crt)
|
||||
if err == nil {
|
||||
// 结束时间大于 2 天不续签
|
||||
if time.Until(decode.NotAfter) > 24*2*time.Hour {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 解析失败则继续续签流程,可能是证书格式不对或者文件不存在
|
||||
r.log.Warn("failed to parse panel certificate", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
// 结束时间大于 2 天不续签
|
||||
if time.Until(decode.NotAfter) > 24*2*time.Hour {
|
||||
return
|
||||
}
|
||||
|
||||
ip, err := r.settingRepo.Get(biz.SettingKeyPublicIPs)
|
||||
@@ -120,7 +122,6 @@ func (r *CertRenew) Run() {
|
||||
}
|
||||
|
||||
r.log.Info("panel certificate renewed successfully", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0))
|
||||
tools.RestartPanel()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ func (r *Monitoring) Run() {
|
||||
}
|
||||
|
||||
info := tools.CurrentInfo(nil, nil)
|
||||
info.TopProcesses = tools.CollectTopProcesses()
|
||||
|
||||
// 去除部分数据以减少数据库存储
|
||||
info.Disk = nil
|
||||
|
||||
+104
-33
@@ -1,11 +1,16 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/acepanel/panel/internal/http/middleware"
|
||||
@@ -38,6 +43,7 @@ type Http struct {
|
||||
environmentNodejs *service.EnvironmentNodejsService
|
||||
environmentPHP *service.EnvironmentPHPService
|
||||
environmentPython *service.EnvironmentPythonService
|
||||
environmentDotnet *service.EnvironmentDotnetService
|
||||
cron *service.CronService
|
||||
process *service.ProcessService
|
||||
safe *service.SafeService
|
||||
@@ -53,11 +59,13 @@ type Http struct {
|
||||
monitor *service.MonitorService
|
||||
setting *service.SettingService
|
||||
systemctl *service.SystemctlService
|
||||
toolboxNetwork *service.ToolboxNetworkService
|
||||
toolboxSystem *service.ToolboxSystemService
|
||||
toolboxBenchmark *service.ToolboxBenchmarkService
|
||||
toolboxSSH *service.ToolboxSSHService
|
||||
toolboxDisk *service.ToolboxDiskService
|
||||
toolboxLog *service.ToolboxLogService
|
||||
toolboxMigration *service.ToolboxMigrationService
|
||||
webhook *service.WebHookService
|
||||
template *service.TemplateService
|
||||
apps *apploader.Loader
|
||||
@@ -86,6 +94,7 @@ func NewHttp(
|
||||
environmentNodejs *service.EnvironmentNodejsService,
|
||||
environmentPHP *service.EnvironmentPHPService,
|
||||
environmentPython *service.EnvironmentPythonService,
|
||||
environmentDotnet *service.EnvironmentDotnetService,
|
||||
cron *service.CronService,
|
||||
process *service.ProcessService,
|
||||
safe *service.SafeService,
|
||||
@@ -101,11 +110,13 @@ func NewHttp(
|
||||
monitor *service.MonitorService,
|
||||
setting *service.SettingService,
|
||||
systemctl *service.SystemctlService,
|
||||
toolboxNetwork *service.ToolboxNetworkService,
|
||||
toolboxSystem *service.ToolboxSystemService,
|
||||
toolboxBenchmark *service.ToolboxBenchmarkService,
|
||||
toolboxSSH *service.ToolboxSSHService,
|
||||
toolboxDisk *service.ToolboxDiskService,
|
||||
toolboxLog *service.ToolboxLogService,
|
||||
toolboxMigration *service.ToolboxMigrationService,
|
||||
webhook *service.WebHookService,
|
||||
template *service.TemplateService,
|
||||
apps *apploader.Loader,
|
||||
@@ -133,6 +144,7 @@ func NewHttp(
|
||||
environmentNodejs: environmentNodejs,
|
||||
environmentPHP: environmentPHP,
|
||||
environmentPython: environmentPython,
|
||||
environmentDotnet: environmentDotnet,
|
||||
cron: cron,
|
||||
process: process,
|
||||
safe: safe,
|
||||
@@ -148,11 +160,13 @@ func NewHttp(
|
||||
monitor: monitor,
|
||||
setting: setting,
|
||||
systemctl: systemctl,
|
||||
toolboxNetwork: toolboxNetwork,
|
||||
toolboxSystem: toolboxSystem,
|
||||
toolboxBenchmark: toolboxBenchmark,
|
||||
toolboxSSH: toolboxSSH,
|
||||
toolboxDisk: toolboxDisk,
|
||||
toolboxLog: toolboxLog,
|
||||
toolboxMigration: toolboxMigration,
|
||||
webhook: webhook,
|
||||
template: template,
|
||||
apps: apps,
|
||||
@@ -200,6 +214,8 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Get("/update_info", route.home.UpdateInfo)
|
||||
r.Post("/update", route.home.Update)
|
||||
r.Post("/restart", route.home.Restart)
|
||||
r.Get("/top_processes", route.home.TopProcesses)
|
||||
r.Post("/restart_server", route.home.RestartServer)
|
||||
})
|
||||
|
||||
r.Route("/task", func(r chi.Router) {
|
||||
@@ -288,10 +304,8 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Get("/{id}", route.cert.Get)
|
||||
r.Delete("/{id}", route.cert.Delete)
|
||||
r.Post("/{id}/obtain_auto", route.cert.ObtainAuto)
|
||||
r.Post("/{id}/obtain_manual", route.cert.ObtainManual)
|
||||
r.Post("/{id}/obtain_self_signed", route.cert.ObtainSelfSigned)
|
||||
r.Post("/{id}/renew", route.cert.Renew)
|
||||
r.Post("/{id}/manual_dns", route.cert.ManualDNS)
|
||||
r.Post("/{id}/deploy", route.cert.Deploy)
|
||||
})
|
||||
r.Route("/dns", func(r chi.Router) {
|
||||
@@ -357,12 +371,18 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Get("/{version}/modules", route.environmentPHP.ModuleList)
|
||||
r.Post("/{version}/modules", route.environmentPHP.InstallModule)
|
||||
r.Delete("/{version}/modules", route.environmentPHP.UninstallModule)
|
||||
r.Get("/{version}/config_tune", route.environmentPHP.GetConfigTune)
|
||||
r.Post("/{version}/config_tune", route.environmentPHP.UpdateConfigTune)
|
||||
r.Post("/{version}/clean_session", route.environmentPHP.CleanSession)
|
||||
})
|
||||
r.Route("/python", func(r chi.Router) {
|
||||
r.Post("/{slug}/set_cli", route.environmentPython.SetCli)
|
||||
r.Get("/{slug}/mirror", route.environmentPython.GetMirror)
|
||||
r.Post("/{slug}/mirror", route.environmentPython.SetMirror)
|
||||
})
|
||||
r.Route("/dotnet", func(r chi.Router) {
|
||||
r.Post("/{slug}/set_cli", route.environmentDotnet.SetCli)
|
||||
})
|
||||
})
|
||||
|
||||
r.Route("/cron", func(r chi.Router) {
|
||||
@@ -479,6 +499,8 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
|
||||
r.Route("/log", func(r chi.Router) {
|
||||
r.Get("/list", route.log.List)
|
||||
r.Get("/dates", route.log.Dates)
|
||||
r.Get("/ssh", route.log.SSH)
|
||||
})
|
||||
|
||||
r.Route("/monitor", func(r chi.Router) {
|
||||
@@ -492,6 +514,9 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Get("/", route.setting.Get)
|
||||
r.Post("/", route.setting.Update)
|
||||
r.Post("/cert", route.setting.UpdateCert)
|
||||
r.Post("/obtain_cert", route.setting.ObtainCert)
|
||||
r.Get("/memo", route.setting.GetMemo)
|
||||
r.Post("/memo", route.setting.UpdateMemo)
|
||||
})
|
||||
|
||||
r.Route("/systemctl", func(r chi.Router) {
|
||||
@@ -505,6 +530,10 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Post("/stop", route.systemctl.Stop)
|
||||
})
|
||||
|
||||
r.Route("/toolbox_network", func(r chi.Router) {
|
||||
r.Get("/list", route.toolboxNetwork.List)
|
||||
})
|
||||
|
||||
r.Route("/toolbox_system", func(r chi.Router) {
|
||||
r.Get("/dns", route.toolboxSystem.GetDNS)
|
||||
r.Post("/dns", route.toolboxSystem.UpdateDNS)
|
||||
@@ -561,6 +590,17 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Post("/clean", route.toolboxLog.Clean)
|
||||
})
|
||||
|
||||
r.Route("/toolbox_migration", func(r chi.Router) {
|
||||
r.Get("/status", route.toolboxMigration.GetStatus)
|
||||
r.Post("/precheck", route.toolboxMigration.PreCheck)
|
||||
r.Get("/items", route.toolboxMigration.GetItems)
|
||||
r.Post("/start", route.toolboxMigration.Start)
|
||||
r.Post("/reset", route.toolboxMigration.Reset)
|
||||
r.Get("/results", route.toolboxMigration.GetResults)
|
||||
r.Get("/log", route.toolboxMigration.DownloadLog)
|
||||
r.Post("/exec", route.toolboxMigration.Exec)
|
||||
})
|
||||
|
||||
r.Route("/webhook", func(r chi.Router) {
|
||||
r.Get("/", route.webhook.List)
|
||||
r.Post("/", route.webhook.Create)
|
||||
@@ -593,36 +633,67 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
}
|
||||
// 其他返回前端页面
|
||||
frontend, _ := fs.Sub(embed.PublicFS, "frontend")
|
||||
spaHandler := func(fs http.FileSystem) http.HandlerFunc {
|
||||
fileServer := http.FileServer(fs)
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
f, err := fs.Open(path)
|
||||
if err != nil {
|
||||
indexFile, err := fs.Open("index.html")
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer func(indexFile http.File) {
|
||||
_ = indexFile.Close()
|
||||
}(indexFile)
|
||||
|
||||
fi, err := indexFile.Stat()
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, "index.html", fi.ModTime(), indexFile)
|
||||
return
|
||||
}
|
||||
defer func(f http.File) {
|
||||
_ = f.Close()
|
||||
}(f)
|
||||
fileServer.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
spaHandler(http.FS(frontend)).ServeHTTP(writer, request)
|
||||
spaHandler := newPrecompressedSPAHandler(http.FS(frontend))
|
||||
spaHandler.ServeHTTP(writer, request)
|
||||
})
|
||||
}
|
||||
|
||||
func newPrecompressedSPAHandler(fsys http.FileSystem) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
acceptBr := strings.Contains(r.Header.Get("Accept-Encoding"), "br")
|
||||
|
||||
if served := serveFileWithBr(w, r, fsys, path, acceptBr); served {
|
||||
return
|
||||
}
|
||||
|
||||
// 文件不存在,SPA fallback 到 index.html
|
||||
serveFileWithBr(w, r, fsys, "/index.html", acceptBr)
|
||||
}
|
||||
}
|
||||
|
||||
func serveFileWithBr(w http.ResponseWriter, r *http.Request, fsys http.FileSystem, path string, acceptBr bool) bool {
|
||||
name := filepath.Base(path)
|
||||
// 尝试打开 .br 版本
|
||||
if f, err := fsys.Open(path + ".br"); err == nil {
|
||||
defer func(f http.File) { _ = f.Close() }(f)
|
||||
fi, err := f.Stat()
|
||||
if err != nil || fi.IsDir() {
|
||||
return false
|
||||
}
|
||||
|
||||
ct := mime.TypeByExtension(filepath.Ext(path))
|
||||
if ct == "" {
|
||||
ct = "application/octet-stream"
|
||||
}
|
||||
w.Header().Set("Content-Type", ct)
|
||||
w.Header().Set("Vary", "Accept-Encoding")
|
||||
|
||||
if acceptBr {
|
||||
// 客户端支持 br,直接透传
|
||||
w.Header().Set("Content-Encoding", "br")
|
||||
http.ServeContent(w, r, name, fi.ModTime(), f)
|
||||
} else {
|
||||
// 客户端不支持 br,解压后返回(由中间件处理 gzip)
|
||||
decoded, err := io.ReadAll(brotli.NewReader(f))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
http.ServeContent(w, r, name, fi.ModTime(), bytes.NewReader(decoded))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 回退到原始文件(字体、图片等未压缩的资源)
|
||||
f, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer func(f http.File) { _ = f.Close() }(f)
|
||||
fi, err := f.Stat()
|
||||
if err != nil || fi.IsDir() {
|
||||
return false
|
||||
}
|
||||
http.ServeContent(w, r, name, fi.ModTime(), f)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
)
|
||||
|
||||
type Ws struct {
|
||||
ws *service.WsService
|
||||
ws *service.WsService
|
||||
toolboxMigration *service.ToolboxMigrationService
|
||||
}
|
||||
|
||||
func NewWs(ws *service.WsService) *Ws {
|
||||
func NewWs(ws *service.WsService, toolboxMigration *service.ToolboxMigrationService) *Ws {
|
||||
return &Ws{
|
||||
ws: ws,
|
||||
ws: ws,
|
||||
toolboxMigration: toolboxMigration,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,5 +25,6 @@ func (route *Ws) Register(r *chi.Mux) {
|
||||
r.Get("/ssh", route.ws.Session)
|
||||
r.Get("/container/{id}", route.ws.ContainerTerminal)
|
||||
r.Get("/container/image/pull", route.ws.ContainerImagePull)
|
||||
r.Get("/migration/progress", route.toolboxMigration.Progress)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -234,21 +234,6 @@ func (s *CertService) ObtainAuto(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *CertService) ObtainManual(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = s.certRepo.ObtainManual(req.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *CertService) ObtainSelfSigned(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
@@ -280,22 +265,6 @@ func (s *CertService) Renew(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *CertService) ManualDNS(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
dns, err := s.certRepo.ManualDNS(req.ID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, dns)
|
||||
}
|
||||
|
||||
func (s *CertService) Deploy(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.CertDeploy](r)
|
||||
if err != nil {
|
||||
|
||||
@@ -534,14 +534,15 @@ func (s *CliService) Port(ctx context.Context, cmd *cli.Command) error {
|
||||
conf.HTTP.Port = port
|
||||
|
||||
// 放行端口
|
||||
if ok, _ := systemctl.IsEnabled("firewalld"); ok {
|
||||
fw := firewall.NewFirewall()
|
||||
fw := firewall.NewFirewall()
|
||||
if ok, _ := fw.Status(); ok {
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: port,
|
||||
PortEnd: port,
|
||||
Direction: firewall.DirectionIn,
|
||||
Protocol: firewall.ProtocolTCPUDP,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
Direction: firewall.DirectionIn,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -996,8 +997,9 @@ checkPort:
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: port,
|
||||
PortEnd: port,
|
||||
Direction: firewall.DirectionIn,
|
||||
Protocol: firewall.ProtocolTCPUDP,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
Direction: firewall.DirectionIn,
|
||||
}, firewall.OperationAdd)
|
||||
|
||||
if err = config.Save(conf); err != nil {
|
||||
|
||||
@@ -20,13 +20,13 @@ func NewDatabaseService(database biz.DatabaseRepo) *DatabaseService {
|
||||
}
|
||||
|
||||
func (s *DatabaseService) List(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.Paginate](r)
|
||||
req, err := Bind[request.DatabaseList](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
databases, total, err := s.databaseRepo.List(req.Page, req.Limit)
|
||||
databases, total, err := s.databaseRepo.List(req.Page, req.Limit, req.Type)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
|
||||
@@ -20,13 +20,13 @@ func NewDatabaseServerService(databaseServer biz.DatabaseServerRepo) *DatabaseSe
|
||||
}
|
||||
|
||||
func (s *DatabaseServerService) List(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.Paginate](r)
|
||||
req, err := Bind[request.DatabaseList](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
servers, total, err := s.databaseServerRepo.List(req.Page, req.Limit)
|
||||
servers, total, err := s.databaseServerRepo.List(req.Page, req.Limit, req.Type)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
|
||||
@@ -20,13 +20,13 @@ func NewDatabaseUserService(databaseUser biz.DatabaseUserRepo) *DatabaseUserServ
|
||||
}
|
||||
|
||||
func (s *DatabaseUserService) List(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.Paginate](r)
|
||||
req, err := Bind[request.DatabaseList](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
users, total, err := s.databaseUserRepo.List(req.Page, req.Limit)
|
||||
users, total, err := s.databaseUserRepo.List(req.Page, req.Limit, req.Type)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
)
|
||||
|
||||
type EnvironmentDotnetService struct {
|
||||
t *gotext.Locale
|
||||
environmentRepo biz.EnvironmentRepo
|
||||
}
|
||||
|
||||
func NewEnvironmentDotnetService(t *gotext.Locale, environmentRepo biz.EnvironmentRepo) *EnvironmentDotnetService {
|
||||
return &EnvironmentDotnetService{
|
||||
t: t,
|
||||
environmentRepo: environmentRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EnvironmentDotnetService) SetCli(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentSlug](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("dotnet", req.Slug) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get(".NET-%s is not installed", req.Slug))
|
||||
return
|
||||
}
|
||||
|
||||
binPath := fmt.Sprintf("%s/server/dotnet/%s", app.Root, req.Slug)
|
||||
if _, err = shell.Execf("ln -sf %s/dotnet /usr/local/bin/dotnet", binPath); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
@@ -168,6 +168,7 @@ func (s *EnvironmentPHPService) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var raw map[string]any
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
_, err = client.R().SetResult(&raw).Get(fmt.Sprintf("http://127.0.0.1/phpfpm_status/%d?json", req.Version))
|
||||
if err != nil {
|
||||
Success(w, []types.NV{})
|
||||
@@ -635,3 +636,243 @@ func (s *EnvironmentPHPService) checkModule(version uint, slug string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 PHP 配置调整参数
|
||||
func (s *EnvironmentPHPService) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPVersion](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
|
||||
return
|
||||
}
|
||||
|
||||
iniPath := fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, req.Version)
|
||||
fpmPath := fmt.Sprintf("%s/server/php/%d/etc/php-fpm.conf", app.Root, req.Version)
|
||||
|
||||
ini, err := io.Read(iniPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
fpm, err := io.Read(fpmPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := request.EnvironmentPHPConfigTune{
|
||||
// php.ini 常规设置
|
||||
ShortOpenTag: s.getINIValue(ini, "short_open_tag"),
|
||||
DateTimezone: s.getINIValue(ini, "date.timezone"),
|
||||
DisplayErrors: s.getINIValue(ini, "display_errors"),
|
||||
ErrorReporting: s.getINIValue(ini, "error_reporting"),
|
||||
// php.ini 禁用函数
|
||||
DisableFunctions: s.getINIValue(ini, "disable_functions"),
|
||||
// php.ini 上传限制
|
||||
UploadMaxFilesize: s.getINIValue(ini, "upload_max_filesize"),
|
||||
PostMaxSize: s.getINIValue(ini, "post_max_size"),
|
||||
MaxFileUploads: s.getINIValue(ini, "max_file_uploads"),
|
||||
MemoryLimit: s.getINIValue(ini, "memory_limit"),
|
||||
// php.ini 超时限制
|
||||
MaxExecutionTime: s.getINIValue(ini, "max_execution_time"),
|
||||
MaxInputTime: s.getINIValue(ini, "max_input_time"),
|
||||
MaxInputVars: s.getINIValue(ini, "max_input_vars"),
|
||||
// Session 相关
|
||||
SessionSaveHandler: s.getINIValue(ini, "session.save_handler"),
|
||||
SessionSavePath: s.getINIValue(ini, "session.save_path"),
|
||||
SessionGcMaxlifetime: s.getINIValue(ini, "session.gc_maxlifetime"),
|
||||
SessionCookieLifetime: s.getINIValue(ini, "session.cookie_lifetime"),
|
||||
// php-fpm.conf 配置
|
||||
Pm: s.getINIValue(fpm, "pm"),
|
||||
PmMaxChildren: s.getINIValue(fpm, "pm.max_children"),
|
||||
PmStartServers: s.getINIValue(fpm, "pm.start_servers"),
|
||||
PmMinSpareServers: s.getINIValue(fpm, "pm.min_spare_servers"),
|
||||
PmMaxSpareServers: s.getINIValue(fpm, "pm.max_spare_servers"),
|
||||
}
|
||||
|
||||
Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 PHP 配置调整参数
|
||||
func (s *EnvironmentPHPService) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPConfigTune](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
|
||||
return
|
||||
}
|
||||
|
||||
iniPath := fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, req.Version)
|
||||
fpmPath := fmt.Sprintf("%s/server/php/%d/etc/php-fpm.conf", app.Root, req.Version)
|
||||
|
||||
ini, err := io.Read(iniPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
fpm, err := io.Read(fpmPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 php.ini 配置
|
||||
ini = s.setINIValue(ini, "short_open_tag", req.ShortOpenTag)
|
||||
ini = s.setINIValue(ini, "date.timezone", req.DateTimezone)
|
||||
ini = s.setINIValue(ini, "display_errors", req.DisplayErrors)
|
||||
ini = s.setINIValue(ini, "error_reporting", req.ErrorReporting)
|
||||
ini = s.setINIValue(ini, "disable_functions", req.DisableFunctions)
|
||||
ini = s.setINIValue(ini, "upload_max_filesize", req.UploadMaxFilesize)
|
||||
ini = s.setINIValue(ini, "post_max_size", req.PostMaxSize)
|
||||
ini = s.setINIValue(ini, "max_execution_time", req.MaxExecutionTime)
|
||||
ini = s.setINIValue(ini, "max_input_time", req.MaxInputTime)
|
||||
ini = s.setINIValue(ini, "memory_limit", req.MemoryLimit)
|
||||
ini = s.setINIValue(ini, "max_input_vars", req.MaxInputVars)
|
||||
ini = s.setINIValue(ini, "max_file_uploads", req.MaxFileUploads)
|
||||
ini = s.setINIValue(ini, "session.save_handler", req.SessionSaveHandler)
|
||||
ini = s.setINIValue(ini, "session.save_path", req.SessionSavePath)
|
||||
ini = s.setINIValue(ini, "session.gc_maxlifetime", req.SessionGcMaxlifetime)
|
||||
ini = s.setINIValue(ini, "session.cookie_lifetime", req.SessionCookieLifetime)
|
||||
|
||||
// 更新 php-fpm.conf 配置
|
||||
fpm = s.setINIValue(fpm, "pm", req.Pm)
|
||||
fpm = s.setINIValue(fpm, "pm.max_children", req.PmMaxChildren)
|
||||
fpm = s.setINIValue(fpm, "pm.start_servers", req.PmStartServers)
|
||||
fpm = s.setINIValue(fpm, "pm.min_spare_servers", req.PmMinSpareServers)
|
||||
fpm = s.setINIValue(fpm, "pm.max_spare_servers", req.PmMaxSpareServers)
|
||||
|
||||
if err = io.Write(iniPath, ini, 0644); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Write(fpmPath, fpm, 0644); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// CleanSession 清理 PHP Session 文件
|
||||
func (s *EnvironmentPHPService) CleanSession(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPVersion](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !s.environmentRepo.IsInstalled("php", fmt.Sprintf("%d", req.Version)) {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("PHP-%d is not installed", req.Version))
|
||||
return
|
||||
}
|
||||
|
||||
iniPath := fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, req.Version)
|
||||
ini, err := io.Read(iniPath)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
handler := s.getINIValue(ini, "session.save_handler")
|
||||
if handler != "files" {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("Session save handler is not files, cannot clean"))
|
||||
return
|
||||
}
|
||||
|
||||
savePath := s.getINIValue(ini, "session.save_path")
|
||||
if savePath == "" {
|
||||
savePath = "/tmp"
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("find '%s' -name 'sess_*' -type f -delete", savePath); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// getINIValue 从 INI 格式内容中获取指定键的值
|
||||
func (s *EnvironmentPHPService) getINIValue(content string, key string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
// 跳过注释行和空行
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, ";") || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
// 跳过 section 行
|
||||
if strings.HasPrefix(trimmed, "[") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setINIValue 在 INI 格式内容中设置指定键的值
|
||||
func (s *EnvironmentPHPService) setINIValue(content string, key string, value string) string {
|
||||
// 过滤值中的换行符,防止破坏 INI 文件结构
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
// 跳过空行和 section 行
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "[") {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
// 处理注释行(可能是被注释掉的配置)
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, ";") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
} else if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.SplitN(checkLine, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
if found {
|
||||
// 如果已经找到并替换过,跳过重复行
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, ";") && !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, ";"+line)
|
||||
} 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")
|
||||
}
|
||||
|
||||
@@ -155,12 +155,13 @@ func (s *FileService) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
path := r.FormValue("path")
|
||||
force := r.FormValue("force") == "true"
|
||||
_, handler, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("upload file error: %v", err))
|
||||
return
|
||||
}
|
||||
if io.Exists(path) {
|
||||
if io.Exists(path) && !force {
|
||||
Error(w, http.StatusForbidden, s.t.Get("target path %s already exists", path))
|
||||
return
|
||||
}
|
||||
@@ -298,7 +299,7 @@ func (s *FileService) RemoteDownload(w http.ResponseWriter, r *http.Request) {
|
||||
task := new(biz.Task)
|
||||
task.Name = s.t.Get("Download remote file %v", filepath.Base(req.Path))
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf(`wget -o /tmp/remote-download-%s.log -O '%s' '%s' && chmod 0755 '%s' && chown www:www '%s'`, timestamp, req.Path, req.URL, req.Path, req.Path)
|
||||
task.Shell = fmt.Sprintf(`aria2c -c --file-allocation=falloc --allow-overwrite=true --auto-file-renaming=false --retry-wait=5 --max-tries=5 -x 16 -s 16 -k 1M -d '%s' -o '%s' '%s' > /tmp/remote-download-%s.log 2>&1 && chmod 0755 '%s' && chown www:www '%s'`, filepath.Dir(req.Path), filepath.Base(req.Path), req.URL, timestamp, req.Path, req.Path)
|
||||
task.Log = fmt.Sprintf("/tmp/remote-download-%s.log", timestamp)
|
||||
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
@@ -565,7 +566,7 @@ func (s *FileService) ChunkUploadStart(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(req.Path, req.FileName)
|
||||
if io.Exists(targetPath) {
|
||||
if io.Exists(targetPath) && !req.Force {
|
||||
Error(w, http.StatusForbidden, s.t.Get("target path %s already exists", targetPath))
|
||||
return
|
||||
}
|
||||
@@ -680,7 +681,7 @@ func (s *FileService) ChunkUploadFinish(w http.ResponseWriter, r *http.Request)
|
||||
targetPath := filepath.Join(req.Path, req.FileName)
|
||||
|
||||
// 检查目标文件是否已存在
|
||||
if io.Exists(targetPath) {
|
||||
if io.Exists(targetPath) && !req.Force {
|
||||
Error(w, http.StatusForbidden, s.t.Get("target path %s already exists", targetPath))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,11 +9,10 @@ import (
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
"github.com/acepanel/panel/pkg/firewall"
|
||||
"github.com/acepanel/panel/pkg/os"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type FirewallService struct {
|
||||
firewall *firewall.Firewall
|
||||
firewall firewall.Firewall
|
||||
}
|
||||
|
||||
func NewFirewallService() *FirewallService {
|
||||
@@ -40,15 +39,9 @@ func (s *FirewallService) UpdateStatus(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if req.Status {
|
||||
err = systemctl.Start("firewalld")
|
||||
if err == nil {
|
||||
err = systemctl.Enable("firewalld")
|
||||
}
|
||||
err = s.firewall.Enable()
|
||||
} else {
|
||||
err = systemctl.Stop("firewalld")
|
||||
if err == nil {
|
||||
err = systemctl.Disable("firewalld")
|
||||
}
|
||||
err = s.firewall.Disable()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -220,6 +220,7 @@ func (s *HomeService) CountInfo(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *HomeService) InstalledEnvironment(w http.ResponseWriter, r *http.Request) {
|
||||
mysqlInstalled, _ := s.appRepo.IsInstalled("slug IN ?", []string{"mysql", "mariadb", "percona"})
|
||||
postgresqlInstalled, _ := s.appRepo.IsInstalled("slug = ?", "postgresql")
|
||||
rsyncInstalled, _ := s.appRepo.IsInstalled("slug = ?", "rsync")
|
||||
|
||||
// Go 版本
|
||||
var goData []types.LV
|
||||
@@ -256,6 +257,13 @@ func (s *HomeService) InstalledEnvironment(w http.ResponseWriter, r *http.Reques
|
||||
pythonData = append(pythonData, types.LV{Value: slug, Label: fmt.Sprintf("Python %s", ver)})
|
||||
}
|
||||
|
||||
// .NET 版本
|
||||
var dotnetData []types.LV
|
||||
for _, slug := range s.environmentRepo.InstalledSlugs("dotnet") {
|
||||
ver := s.environmentRepo.InstalledVersion("dotnet", slug)
|
||||
dotnetData = append(dotnetData, types.LV{Value: slug, Label: fmt.Sprintf(".NET %s", ver)})
|
||||
}
|
||||
|
||||
// 数据库
|
||||
var dbData []types.LV
|
||||
dbData = append(dbData, types.LV{Value: "0", Label: s.t.Get("Not used")})
|
||||
@@ -274,7 +282,9 @@ func (s *HomeService) InstalledEnvironment(w http.ResponseWriter, r *http.Reques
|
||||
"nodejs": nodejsData,
|
||||
"php": phpData,
|
||||
"python": pythonData,
|
||||
"dotnet": dotnetData,
|
||||
"db": dbData,
|
||||
"rsync": rsyncInstalled,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -400,3 +410,33 @@ func (s *HomeService) Restart(w http.ResponseWriter, r *http.Request) {
|
||||
tools.RestartPanel()
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *HomeService) RestartServer(w http.ResponseWriter, r *http.Request) {
|
||||
if s.taskRepo.HasRunningTask() {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("background task is running, restart is prohibited, please try again later"))
|
||||
return
|
||||
}
|
||||
|
||||
tools.RestartServer()
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *HomeService) TopProcesses(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.HomeTopProcesses](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tp := tools.CollectTopProcesses()
|
||||
switch req.Type {
|
||||
case "cpu":
|
||||
Success(w, tp.CPU)
|
||||
case "memory":
|
||||
Success(w, tp.Memory)
|
||||
case "disk_io":
|
||||
Success(w, tp.DiskIO)
|
||||
default:
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid type"))
|
||||
}
|
||||
}
|
||||
|
||||
+207
-2
@@ -1,18 +1,40 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/types"
|
||||
)
|
||||
|
||||
// SSH 日志正则
|
||||
var (
|
||||
sshAccepted = regexp.MustCompile(`Accepted\s+(\S+)\s+for\s+(\S+)\s+from\s+(\S+)\s+port\s+(\d+)`)
|
||||
sshFailed = regexp.MustCompile(`Failed\s+(\S+)\s+for\s+(?:invalid user\s+)?(\S+)\s+from\s+(\S+)\s+port\s+(\d+)`)
|
||||
sshInvalidUser = regexp.MustCompile(`Invalid user\s+(\S+)\s+from\s+(\S+)\s+port\s+(\d+)`)
|
||||
sshDisconnect = regexp.MustCompile(`Disconnected from\s+(?:authenticating\s+)?user\s+(\S+)\s+(\S+)\s+port\s+(\d+)`)
|
||||
)
|
||||
|
||||
type LogService struct {
|
||||
t *gotext.Locale
|
||||
logRepo biz.LogRepo
|
||||
}
|
||||
|
||||
func NewLogService(logRepo biz.LogRepo) *LogService {
|
||||
func NewLogService(t *gotext.Locale, logRepo biz.LogRepo) *LogService {
|
||||
return &LogService{
|
||||
t: t,
|
||||
logRepo: logRepo,
|
||||
}
|
||||
}
|
||||
@@ -30,7 +52,7 @@ func (s *LogService) List(w http.ResponseWriter, r *http.Request) {
|
||||
req.Limit = 100
|
||||
}
|
||||
|
||||
entries, err := s.logRepo.List(req.Type, req.Limit)
|
||||
entries, err := s.logRepo.List(req.Type, req.Limit, req.Date)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -38,3 +60,186 @@ func (s *LogService) List(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Success(w, entries)
|
||||
}
|
||||
|
||||
// Dates 获取日志日期列表
|
||||
func (s *LogService) Dates(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.LogDates](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
dates, err := s.logRepo.ListDates(req.Type)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, dates)
|
||||
}
|
||||
|
||||
// SSH 获取 SSH 登录日志
|
||||
func (s *LogService) SSH(w http.ResponseWriter, r *http.Request) {
|
||||
limit := 100
|
||||
if v := r.URL.Query().Get("limit"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 1000 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
|
||||
logs, err := s.sshFromJournalctl(limit)
|
||||
if err != nil {
|
||||
logs, err = s.sshFromLogFile(limit)
|
||||
}
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, logs)
|
||||
}
|
||||
|
||||
// sshFromJournalctl 通过 journalctl 获取 SSH 日志
|
||||
func (s *LogService) sshFromJournalctl(limit int) ([]types.SSHLoginLog, error) {
|
||||
raw, err := shell.Execf("journalctl -u sshd -u ssh --no-pager -o json -n %d 2>/dev/null", limit*5)
|
||||
if err != nil || raw == "" {
|
||||
return nil, errors.New(s.t.Get("journalctl is not available"))
|
||||
}
|
||||
|
||||
var logs []types.SSHLoginLog
|
||||
scanner := bufio.NewScanner(strings.NewReader(raw))
|
||||
for scanner.Scan() {
|
||||
var entry struct {
|
||||
RealtimeTimestamp string `json:"__REALTIME_TIMESTAMP"`
|
||||
Message string `json:"MESSAGE"`
|
||||
}
|
||||
if json.Unmarshal(scanner.Bytes(), &entry) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
record := parseSSHMessage(entry.Message)
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析 journalctl 微秒时间戳
|
||||
if us, err := strconv.ParseInt(entry.RealtimeTimestamp, 10, 64); err == nil {
|
||||
record.Time = time.Unix(0, us*int64(time.Microsecond)).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
logs = append(logs, *record)
|
||||
if len(logs) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(logs) == 0 {
|
||||
return nil, errors.New(s.t.Get("no SSH log entries found"))
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// sshFromLogFile 从日志文件中解析 SSH 日志
|
||||
func (s *LogService) sshFromLogFile(limit int) ([]types.SSHLoginLog, error) {
|
||||
paths := []string{"/var/log/auth.log", "/var/log/secure"}
|
||||
var file *os.File
|
||||
for _, p := range paths {
|
||||
if f, err := os.Open(p); err == nil {
|
||||
file = f
|
||||
break
|
||||
}
|
||||
}
|
||||
if file == nil {
|
||||
return nil, errors.New(s.t.Get("SSH log file not found"))
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
// 读取所有匹配行后取最后 limit 条(日志文件按时间正序)
|
||||
var logs []types.SSHLoginLog
|
||||
scanner := bufio.NewScanner(file)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.Contains(line, "sshd[") {
|
||||
continue
|
||||
}
|
||||
|
||||
record := parseSSHMessage(line)
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 从行首解析 syslog 时间戳(如 "Feb 11 08:30:01")
|
||||
record.Time = parseSSHLogTime(line)
|
||||
|
||||
logs = append(logs, *record)
|
||||
}
|
||||
|
||||
// 取最后 limit 条
|
||||
if len(logs) > limit {
|
||||
logs = logs[len(logs)-limit:]
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// parseSSHMessage 从日志消息中提取 SSH 登录信息
|
||||
func parseSSHMessage(msg string) *types.SSHLoginLog {
|
||||
if m := sshAccepted.FindStringSubmatch(msg); m != nil {
|
||||
return &types.SSHLoginLog{
|
||||
Method: m[1],
|
||||
User: m[2],
|
||||
IP: m[3],
|
||||
Port: m[4],
|
||||
Status: "accepted",
|
||||
}
|
||||
}
|
||||
if m := sshFailed.FindStringSubmatch(msg); m != nil {
|
||||
return &types.SSHLoginLog{
|
||||
Method: m[1],
|
||||
User: m[2],
|
||||
IP: m[3],
|
||||
Port: m[4],
|
||||
Status: "failed",
|
||||
}
|
||||
}
|
||||
if m := sshInvalidUser.FindStringSubmatch(msg); m != nil {
|
||||
return &types.SSHLoginLog{
|
||||
User: m[1],
|
||||
IP: m[2],
|
||||
Port: m[3],
|
||||
Method: "-",
|
||||
Status: "invalid_user",
|
||||
}
|
||||
}
|
||||
if m := sshDisconnect.FindStringSubmatch(msg); m != nil {
|
||||
return &types.SSHLoginLog{
|
||||
User: m[1],
|
||||
IP: m[2],
|
||||
Port: m[3],
|
||||
Method: "-",
|
||||
Status: "disconnected",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseSSHLogTime 从 syslog 格式行中解析时间
|
||||
func parseSSHLogTime(line string) string {
|
||||
// syslog 格式:Mon DD HH:MM:SS(前 15 个字符)
|
||||
if len(line) < 15 {
|
||||
return "-"
|
||||
}
|
||||
ts := line[:15]
|
||||
// 使用当前年份补全
|
||||
t, err := time.Parse("Jan 2 15:04:05", ts)
|
||||
if err != nil {
|
||||
t, err = time.Parse("Jan 2 15:04:05", ts)
|
||||
if err != nil {
|
||||
return "-"
|
||||
}
|
||||
}
|
||||
t = t.AddDate(time.Now().Year(), 0, 0)
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
@@ -175,6 +175,9 @@ func (s *MonitorService) List(w http.ResponseWriter, r *http.Request) {
|
||||
list.Mem.Used = append(list.Mem.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Used)/1024/1024))
|
||||
list.SWAP.Used = append(list.SWAP.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024))
|
||||
list.SWAP.Free = append(list.SWAP.Free, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Free)/1024/1024))
|
||||
|
||||
// Top 5 进程数据
|
||||
list.TopProcesses = append(list.TopProcesses, monitor.Info.TopProcesses)
|
||||
}
|
||||
|
||||
// 将 map 转换为 slice
|
||||
|
||||
@@ -26,6 +26,7 @@ var ProviderSet = wire.NewSet(
|
||||
NewEnvironmentNodejsService,
|
||||
NewEnvironmentPHPService,
|
||||
NewEnvironmentPythonService,
|
||||
NewEnvironmentDotnetService,
|
||||
NewFileService,
|
||||
NewFirewallService,
|
||||
NewHomeService,
|
||||
@@ -43,10 +44,12 @@ var ProviderSet = wire.NewSet(
|
||||
NewUserTokenService,
|
||||
NewWebHookService,
|
||||
NewWebsiteService,
|
||||
NewToolboxNetworkService,
|
||||
NewToolboxSystemService,
|
||||
NewToolboxBenchmarkService,
|
||||
NewToolboxSSHService,
|
||||
NewToolboxDiskService,
|
||||
NewToolboxLogService,
|
||||
NewToolboxMigrationService,
|
||||
NewWsService,
|
||||
)
|
||||
|
||||
@@ -104,8 +104,6 @@ func (s *SettingService) ObtainCert(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
tools.RestartPanel()
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -122,7 +120,32 @@ func (s *SettingService) UpdateCert(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
tools.RestartPanel()
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// GetMemo 获取便签内容
|
||||
func (s *SettingService) GetMemo(w http.ResponseWriter, r *http.Request) {
|
||||
memo, err := s.settingRepo.Get(biz.SettingKeyMemo)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, memo)
|
||||
}
|
||||
|
||||
// UpdateMemo 更新便签内容
|
||||
func (s *SettingService) UpdateMemo(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.SettingMemo](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.settingRepo.Set(biz.SettingKeyMemo, req.Content); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,190 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type ToolboxNetworkService struct{}
|
||||
|
||||
func NewToolboxNetworkService() *ToolboxNetworkService {
|
||||
return &ToolboxNetworkService{}
|
||||
}
|
||||
|
||||
type networkConnection struct {
|
||||
Type string `json:"type"` // tcp, tcp6, udp, udp6
|
||||
PID int32 `json:"pid"`
|
||||
Process string `json:"process"` // 进程名称
|
||||
Local string `json:"local"` // 本地地址:端口
|
||||
Remote string `json:"remote"` // 远程地址:端口
|
||||
State string `json:"state"` // LISTEN, ESTABLISHED 等
|
||||
}
|
||||
|
||||
// List 获取网络连接列表
|
||||
func (s *ToolboxNetworkService) List(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ToolboxNetworkList](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Page == 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.Limit == 0 {
|
||||
req.Limit = 50
|
||||
}
|
||||
if req.Order == "" {
|
||||
req.Order = "asc"
|
||||
}
|
||||
|
||||
conns, err := net.ConnectionsPid("all", 0)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 缓存 PID → 进程名
|
||||
nameCache := make(map[int32]string)
|
||||
getName := func(pid int32) string {
|
||||
if pid <= 0 {
|
||||
return ""
|
||||
}
|
||||
if name, ok := nameCache[pid]; ok {
|
||||
return name
|
||||
}
|
||||
proc, err := process.NewProcess(pid)
|
||||
if err != nil {
|
||||
nameCache[pid] = ""
|
||||
return ""
|
||||
}
|
||||
name, err := proc.Name()
|
||||
if err != nil {
|
||||
nameCache[pid] = ""
|
||||
return ""
|
||||
}
|
||||
nameCache[pid] = name
|
||||
return name
|
||||
}
|
||||
|
||||
// 状态过滤集合
|
||||
stateSet := make(map[string]struct{})
|
||||
if req.State != "" {
|
||||
for st := range strings.SplitSeq(req.State, ",") {
|
||||
st = strings.TrimSpace(st)
|
||||
if st != "" {
|
||||
stateSet[strings.ToUpper(st)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data := make([]networkConnection, 0, len(conns))
|
||||
for conn := range slices.Values(conns) {
|
||||
typ := resolveConnType(conn.Family, conn.Type)
|
||||
if typ == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
state := strings.ToUpper(conn.Status)
|
||||
procName := getName(conn.Pid)
|
||||
local := fmt.Sprintf("%s:%d", conn.Laddr.IP, conn.Laddr.Port)
|
||||
remote := fmt.Sprintf("%s:%d", conn.Raddr.IP, conn.Raddr.Port)
|
||||
|
||||
// 状态过滤
|
||||
if len(stateSet) > 0 {
|
||||
if _, ok := stateSet[state]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// PID 搜索
|
||||
if req.PID != "" {
|
||||
pidStr := strconv.FormatInt(int64(conn.Pid), 10)
|
||||
if !strings.Contains(pidStr, req.PID) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 进程名称搜索
|
||||
if req.Process != "" {
|
||||
if !strings.Contains(strings.ToLower(procName), strings.ToLower(req.Process)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 端口搜索
|
||||
if req.Port != "" {
|
||||
localPort := strconv.FormatUint(uint64(conn.Laddr.Port), 10)
|
||||
remotePort := strconv.FormatUint(uint64(conn.Raddr.Port), 10)
|
||||
if !strings.Contains(localPort, req.Port) && !strings.Contains(remotePort, req.Port) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
data = append(data, networkConnection{
|
||||
Type: typ,
|
||||
PID: conn.Pid,
|
||||
Process: procName,
|
||||
Local: local,
|
||||
Remote: remote,
|
||||
State: state,
|
||||
})
|
||||
}
|
||||
|
||||
// 排序
|
||||
s.sortConnections(data, req.Sort, req.Order)
|
||||
|
||||
paged, total := Paginate(r, data)
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// resolveConnType 将 Family + Type 转为可读字符串
|
||||
func resolveConnType(family, typ uint32) string {
|
||||
switch {
|
||||
case family == 2 && typ == 1:
|
||||
return "tcp"
|
||||
case family == 10 && typ == 1:
|
||||
return "tcp6"
|
||||
case family == 2 && typ == 2:
|
||||
return "udp"
|
||||
case family == 10 && typ == 2:
|
||||
return "udp6"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// sortConnections 对连接列表排序
|
||||
func (s *ToolboxNetworkService) sortConnections(data []networkConnection, sortBy, order string) {
|
||||
sort.Slice(data, func(i, j int) bool {
|
||||
var less bool
|
||||
switch sortBy {
|
||||
case "type":
|
||||
less = data[i].Type < data[j].Type
|
||||
case "pid":
|
||||
less = data[i].PID < data[j].PID
|
||||
case "process":
|
||||
less = strings.ToLower(data[i].Process) < strings.ToLower(data[j].Process)
|
||||
default:
|
||||
less = data[i].PID < data[j].PID
|
||||
}
|
||||
if order == "desc" {
|
||||
return !less
|
||||
}
|
||||
return less
|
||||
})
|
||||
}
|
||||
@@ -5,10 +5,8 @@ package biz
|
||||
import (
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
api "github.com/acepanel/panel/pkg/api"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
types "github.com/acepanel/panel/pkg/types"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// AppRepo is an autogenerated mock type for the AppRepo type
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
context "context"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// BackupAccountRepo is an autogenerated mock type for the BackupAccountRepo type
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
context "context"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
types "github.com/acepanel/panel/pkg/types"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// BackupRepo is an autogenerated mock type for the BackupRepo type
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
context "context"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// CertAccountRepo is an autogenerated mock type for the CertAccountRepo type
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
context "context"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// CertDNSRepo is an autogenerated mock type for the CertDNSRepo type
|
||||
|
||||
+3
-123
@@ -3,18 +3,14 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
acme "github.com/acepanel/panel/pkg/acme"
|
||||
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
|
||||
acme "github.com/acepanel/panel/pkg/acme"
|
||||
types "github.com/acepanel/panel/pkg/types"
|
||||
|
||||
v3acme "github.com/mholt/acmez/v3/acme"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// CertRepo is an autogenerated mock type for the CertRepo type
|
||||
@@ -365,64 +361,6 @@ func (_c *CertRepo_List_Call) RunAndReturn(run func(uint, uint) ([]*types.CertLi
|
||||
return _c
|
||||
}
|
||||
|
||||
// ManualDNS provides a mock function with given fields: id
|
||||
func (_m *CertRepo) ManualDNS(id uint) ([]acme.DNSRecord, error) {
|
||||
ret := _m.Called(id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ManualDNS")
|
||||
}
|
||||
|
||||
var r0 []acme.DNSRecord
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(uint) ([]acme.DNSRecord, error)); ok {
|
||||
return rf(id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(uint) []acme.DNSRecord); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]acme.DNSRecord)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(uint) error); ok {
|
||||
r1 = rf(id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CertRepo_ManualDNS_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ManualDNS'
|
||||
type CertRepo_ManualDNS_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ManualDNS is a helper method to define mock.On call
|
||||
// - id uint
|
||||
func (_e *CertRepo_Expecter) ManualDNS(id interface{}) *CertRepo_ManualDNS_Call {
|
||||
return &CertRepo_ManualDNS_Call{Call: _e.mock.On("ManualDNS", id)}
|
||||
}
|
||||
|
||||
func (_c *CertRepo_ManualDNS_Call) Run(run func(id uint)) *CertRepo_ManualDNS_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(uint))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CertRepo_ManualDNS_Call) Return(_a0 []acme.DNSRecord, _a1 error) *CertRepo_ManualDNS_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CertRepo_ManualDNS_Call) RunAndReturn(run func(uint) ([]acme.DNSRecord, error)) *CertRepo_ManualDNS_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ObtainAuto provides a mock function with given fields: id
|
||||
func (_m *CertRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
ret := _m.Called(id)
|
||||
@@ -481,64 +419,6 @@ func (_c *CertRepo_ObtainAuto_Call) RunAndReturn(run func(uint) (*acme.Certifica
|
||||
return _c
|
||||
}
|
||||
|
||||
// ObtainManual provides a mock function with given fields: id
|
||||
func (_m *CertRepo) ObtainManual(id uint) (*acme.Certificate, error) {
|
||||
ret := _m.Called(id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ObtainManual")
|
||||
}
|
||||
|
||||
var r0 *acme.Certificate
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(uint) (*acme.Certificate, error)); ok {
|
||||
return rf(id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(uint) *acme.Certificate); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*acme.Certificate)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(uint) error); ok {
|
||||
r1 = rf(id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CertRepo_ObtainManual_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ObtainManual'
|
||||
type CertRepo_ObtainManual_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ObtainManual is a helper method to define mock.On call
|
||||
// - id uint
|
||||
func (_e *CertRepo_Expecter) ObtainManual(id interface{}) *CertRepo_ObtainManual_Call {
|
||||
return &CertRepo_ObtainManual_Call{Call: _e.mock.On("ObtainManual", id)}
|
||||
}
|
||||
|
||||
func (_c *CertRepo_ObtainManual_Call) Run(run func(id uint)) *CertRepo_ObtainManual_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(uint))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CertRepo_ObtainManual_Call) Return(_a0 *acme.Certificate, _a1 error) *CertRepo_ObtainManual_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *CertRepo_ObtainManual_Call) RunAndReturn(run func(uint) (*acme.Certificate, error)) *CertRepo_ObtainManual_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ObtainPanel provides a mock function with given fields: account, ips
|
||||
func (_m *CertRepo) ObtainPanel(account *biz.CertAccount, ips []string) ([]byte, []byte, error) {
|
||||
ret := _m.Called(account, ips)
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
context "context"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// CronRepo is an autogenerated mock type for the CronRepo type
|
||||
|
||||
+18
-19
@@ -6,10 +6,8 @@ import (
|
||||
context "context"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// DatabaseRepo is an autogenerated mock type for the DatabaseRepo type
|
||||
@@ -166,9 +164,9 @@ func (_c *DatabaseRepo_Delete_Call) RunAndReturn(run func(context.Context, uint,
|
||||
return _c
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: page, limit
|
||||
func (_m *DatabaseRepo) List(page uint, limit uint) ([]*biz.Database, int64, error) {
|
||||
ret := _m.Called(page, limit)
|
||||
// List provides a mock function with given fields: page, limit, typ
|
||||
func (_m *DatabaseRepo) List(page uint, limit uint, typ string) ([]*biz.Database, int64, error) {
|
||||
ret := _m.Called(page, limit, typ)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
@@ -177,25 +175,25 @@ func (_m *DatabaseRepo) List(page uint, limit uint) ([]*biz.Database, int64, err
|
||||
var r0 []*biz.Database
|
||||
var r1 int64
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(0).(func(uint, uint) ([]*biz.Database, int64, error)); ok {
|
||||
return rf(page, limit)
|
||||
if rf, ok := ret.Get(0).(func(uint, uint, string) ([]*biz.Database, int64, error)); ok {
|
||||
return rf(page, limit, typ)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(uint, uint) []*biz.Database); ok {
|
||||
r0 = rf(page, limit)
|
||||
if rf, ok := ret.Get(0).(func(uint, uint, string) []*biz.Database); ok {
|
||||
r0 = rf(page, limit, typ)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*biz.Database)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(uint, uint) int64); ok {
|
||||
r1 = rf(page, limit)
|
||||
if rf, ok := ret.Get(1).(func(uint, uint, string) int64); ok {
|
||||
r1 = rf(page, limit, typ)
|
||||
} else {
|
||||
r1 = ret.Get(1).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(2).(func(uint, uint) error); ok {
|
||||
r2 = rf(page, limit)
|
||||
if rf, ok := ret.Get(2).(func(uint, uint, string) error); ok {
|
||||
r2 = rf(page, limit, typ)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
@@ -211,13 +209,14 @@ type DatabaseRepo_List_Call struct {
|
||||
// List is a helper method to define mock.On call
|
||||
// - page uint
|
||||
// - limit uint
|
||||
func (_e *DatabaseRepo_Expecter) List(page interface{}, limit interface{}) *DatabaseRepo_List_Call {
|
||||
return &DatabaseRepo_List_Call{Call: _e.mock.On("List", page, limit)}
|
||||
// - typ string
|
||||
func (_e *DatabaseRepo_Expecter) List(page interface{}, limit interface{}, typ interface{}) *DatabaseRepo_List_Call {
|
||||
return &DatabaseRepo_List_Call{Call: _e.mock.On("List", page, limit, typ)}
|
||||
}
|
||||
|
||||
func (_c *DatabaseRepo_List_Call) Run(run func(page uint, limit uint)) *DatabaseRepo_List_Call {
|
||||
func (_c *DatabaseRepo_List_Call) Run(run func(page uint, limit uint, typ string)) *DatabaseRepo_List_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(uint), args[1].(uint))
|
||||
run(args[0].(uint), args[1].(uint), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
@@ -227,7 +226,7 @@ func (_c *DatabaseRepo_List_Call) Return(_a0 []*biz.Database, _a1 int64, _a2 err
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *DatabaseRepo_List_Call) RunAndReturn(run func(uint, uint) ([]*biz.Database, int64, error)) *DatabaseRepo_List_Call {
|
||||
func (_c *DatabaseRepo_List_Call) RunAndReturn(run func(uint, uint, string) ([]*biz.Database, int64, error)) *DatabaseRepo_List_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ package biz
|
||||
|
||||
import (
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// DatabaseServerRepo is an autogenerated mock type for the DatabaseServerRepo type
|
||||
@@ -331,9 +330,9 @@ func (_c *DatabaseServerRepo_GetByName_Call) RunAndReturn(run func(string) (*biz
|
||||
return _c
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: page, limit
|
||||
func (_m *DatabaseServerRepo) List(page uint, limit uint) ([]*biz.DatabaseServer, int64, error) {
|
||||
ret := _m.Called(page, limit)
|
||||
// List provides a mock function with given fields: page, limit, typ
|
||||
func (_m *DatabaseServerRepo) List(page uint, limit uint, typ string) ([]*biz.DatabaseServer, int64, error) {
|
||||
ret := _m.Called(page, limit, typ)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
@@ -342,25 +341,25 @@ func (_m *DatabaseServerRepo) List(page uint, limit uint) ([]*biz.DatabaseServer
|
||||
var r0 []*biz.DatabaseServer
|
||||
var r1 int64
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(0).(func(uint, uint) ([]*biz.DatabaseServer, int64, error)); ok {
|
||||
return rf(page, limit)
|
||||
if rf, ok := ret.Get(0).(func(uint, uint, string) ([]*biz.DatabaseServer, int64, error)); ok {
|
||||
return rf(page, limit, typ)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(uint, uint) []*biz.DatabaseServer); ok {
|
||||
r0 = rf(page, limit)
|
||||
if rf, ok := ret.Get(0).(func(uint, uint, string) []*biz.DatabaseServer); ok {
|
||||
r0 = rf(page, limit, typ)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*biz.DatabaseServer)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(uint, uint) int64); ok {
|
||||
r1 = rf(page, limit)
|
||||
if rf, ok := ret.Get(1).(func(uint, uint, string) int64); ok {
|
||||
r1 = rf(page, limit, typ)
|
||||
} else {
|
||||
r1 = ret.Get(1).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(2).(func(uint, uint) error); ok {
|
||||
r2 = rf(page, limit)
|
||||
if rf, ok := ret.Get(2).(func(uint, uint, string) error); ok {
|
||||
r2 = rf(page, limit, typ)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
@@ -376,13 +375,14 @@ type DatabaseServerRepo_List_Call struct {
|
||||
// List is a helper method to define mock.On call
|
||||
// - page uint
|
||||
// - limit uint
|
||||
func (_e *DatabaseServerRepo_Expecter) List(page interface{}, limit interface{}) *DatabaseServerRepo_List_Call {
|
||||
return &DatabaseServerRepo_List_Call{Call: _e.mock.On("List", page, limit)}
|
||||
// - typ string
|
||||
func (_e *DatabaseServerRepo_Expecter) List(page interface{}, limit interface{}, typ interface{}) *DatabaseServerRepo_List_Call {
|
||||
return &DatabaseServerRepo_List_Call{Call: _e.mock.On("List", page, limit, typ)}
|
||||
}
|
||||
|
||||
func (_c *DatabaseServerRepo_List_Call) Run(run func(page uint, limit uint)) *DatabaseServerRepo_List_Call {
|
||||
func (_c *DatabaseServerRepo_List_Call) Run(run func(page uint, limit uint, typ string)) *DatabaseServerRepo_List_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(uint), args[1].(uint))
|
||||
run(args[0].(uint), args[1].(uint), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
@@ -392,7 +392,7 @@ func (_c *DatabaseServerRepo_List_Call) Return(_a0 []*biz.DatabaseServer, _a1 in
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *DatabaseServerRepo_List_Call) RunAndReturn(run func(uint, uint) ([]*biz.DatabaseServer, int64, error)) *DatabaseServerRepo_List_Call {
|
||||
func (_c *DatabaseServerRepo_List_Call) RunAndReturn(run func(uint, uint, string) ([]*biz.DatabaseServer, int64, error)) *DatabaseServerRepo_List_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
context "context"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// DatabaseUserRepo is an autogenerated mock type for the DatabaseUserRepo type
|
||||
@@ -279,9 +277,9 @@ func (_c *DatabaseUserRepo_Get_Call) RunAndReturn(run func(uint) (*biz.DatabaseU
|
||||
return _c
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: page, limit
|
||||
func (_m *DatabaseUserRepo) List(page uint, limit uint) ([]*biz.DatabaseUser, int64, error) {
|
||||
ret := _m.Called(page, limit)
|
||||
// List provides a mock function with given fields: page, limit, typ
|
||||
func (_m *DatabaseUserRepo) List(page uint, limit uint, typ string) ([]*biz.DatabaseUser, int64, error) {
|
||||
ret := _m.Called(page, limit, typ)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
@@ -290,25 +288,25 @@ func (_m *DatabaseUserRepo) List(page uint, limit uint) ([]*biz.DatabaseUser, in
|
||||
var r0 []*biz.DatabaseUser
|
||||
var r1 int64
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(0).(func(uint, uint) ([]*biz.DatabaseUser, int64, error)); ok {
|
||||
return rf(page, limit)
|
||||
if rf, ok := ret.Get(0).(func(uint, uint, string) ([]*biz.DatabaseUser, int64, error)); ok {
|
||||
return rf(page, limit, typ)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(uint, uint) []*biz.DatabaseUser); ok {
|
||||
r0 = rf(page, limit)
|
||||
if rf, ok := ret.Get(0).(func(uint, uint, string) []*biz.DatabaseUser); ok {
|
||||
r0 = rf(page, limit, typ)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*biz.DatabaseUser)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(uint, uint) int64); ok {
|
||||
r1 = rf(page, limit)
|
||||
if rf, ok := ret.Get(1).(func(uint, uint, string) int64); ok {
|
||||
r1 = rf(page, limit, typ)
|
||||
} else {
|
||||
r1 = ret.Get(1).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(2).(func(uint, uint) error); ok {
|
||||
r2 = rf(page, limit)
|
||||
if rf, ok := ret.Get(2).(func(uint, uint, string) error); ok {
|
||||
r2 = rf(page, limit, typ)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
@@ -324,13 +322,14 @@ type DatabaseUserRepo_List_Call struct {
|
||||
// List is a helper method to define mock.On call
|
||||
// - page uint
|
||||
// - limit uint
|
||||
func (_e *DatabaseUserRepo_Expecter) List(page interface{}, limit interface{}) *DatabaseUserRepo_List_Call {
|
||||
return &DatabaseUserRepo_List_Call{Call: _e.mock.On("List", page, limit)}
|
||||
// - typ string
|
||||
func (_e *DatabaseUserRepo_Expecter) List(page interface{}, limit interface{}, typ interface{}) *DatabaseUserRepo_List_Call {
|
||||
return &DatabaseUserRepo_List_Call{Call: _e.mock.On("List", page, limit, typ)}
|
||||
}
|
||||
|
||||
func (_c *DatabaseUserRepo_List_Call) Run(run func(page uint, limit uint)) *DatabaseUserRepo_List_Call {
|
||||
func (_c *DatabaseUserRepo_List_Call) Run(run func(page uint, limit uint, typ string)) *DatabaseUserRepo_List_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(uint), args[1].(uint))
|
||||
run(args[0].(uint), args[1].(uint), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
@@ -340,7 +339,7 @@ func (_c *DatabaseUserRepo_List_Call) Return(_a0 []*biz.DatabaseUser, _a1 int64,
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *DatabaseUserRepo_List_Call) RunAndReturn(run func(uint, uint) ([]*biz.DatabaseUser, int64, error)) *DatabaseUserRepo_List_Call {
|
||||
func (_c *DatabaseUserRepo_List_Call) RunAndReturn(run func(uint, uint, string) ([]*biz.DatabaseUser, int64, error)) *DatabaseUserRepo_List_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ package biz
|
||||
import (
|
||||
api "github.com/acepanel/panel/pkg/api"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
types "github.com/acepanel/panel/pkg/types"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// EnvironmentRepo is an autogenerated mock type for the EnvironmentRepo type
|
||||
|
||||
+73
-14
@@ -20,9 +20,9 @@ func (_m *LogRepo) EXPECT() *LogRepo_Expecter {
|
||||
return &LogRepo_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: logType, limit
|
||||
func (_m *LogRepo) List(logType string, limit int) ([]biz.LogEntry, error) {
|
||||
ret := _m.Called(logType, limit)
|
||||
// List provides a mock function with given fields: logType, limit, date
|
||||
func (_m *LogRepo) List(logType string, limit int, date string) ([]biz.LogEntry, error) {
|
||||
ret := _m.Called(logType, limit, date)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
@@ -30,19 +30,19 @@ func (_m *LogRepo) List(logType string, limit int) ([]biz.LogEntry, error) {
|
||||
|
||||
var r0 []biz.LogEntry
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, int) ([]biz.LogEntry, error)); ok {
|
||||
return rf(logType, limit)
|
||||
if rf, ok := ret.Get(0).(func(string, int, string) ([]biz.LogEntry, error)); ok {
|
||||
return rf(logType, limit, date)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, int) []biz.LogEntry); ok {
|
||||
r0 = rf(logType, limit)
|
||||
if rf, ok := ret.Get(0).(func(string, int, string) []biz.LogEntry); ok {
|
||||
r0 = rf(logType, limit, date)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]biz.LogEntry)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, int) error); ok {
|
||||
r1 = rf(logType, limit)
|
||||
if rf, ok := ret.Get(1).(func(string, int, string) error); ok {
|
||||
r1 = rf(logType, limit, date)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
@@ -58,13 +58,14 @@ type LogRepo_List_Call struct {
|
||||
// List is a helper method to define mock.On call
|
||||
// - logType string
|
||||
// - limit int
|
||||
func (_e *LogRepo_Expecter) List(logType interface{}, limit interface{}) *LogRepo_List_Call {
|
||||
return &LogRepo_List_Call{Call: _e.mock.On("List", logType, limit)}
|
||||
// - date string
|
||||
func (_e *LogRepo_Expecter) List(logType interface{}, limit interface{}, date interface{}) *LogRepo_List_Call {
|
||||
return &LogRepo_List_Call{Call: _e.mock.On("List", logType, limit, date)}
|
||||
}
|
||||
|
||||
func (_c *LogRepo_List_Call) Run(run func(logType string, limit int)) *LogRepo_List_Call {
|
||||
func (_c *LogRepo_List_Call) Run(run func(logType string, limit int, date string)) *LogRepo_List_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(int))
|
||||
run(args[0].(string), args[1].(int), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
@@ -74,7 +75,65 @@ func (_c *LogRepo_List_Call) Return(_a0 []biz.LogEntry, _a1 error) *LogRepo_List
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *LogRepo_List_Call) RunAndReturn(run func(string, int) ([]biz.LogEntry, error)) *LogRepo_List_Call {
|
||||
func (_c *LogRepo_List_Call) RunAndReturn(run func(string, int, string) ([]biz.LogEntry, error)) *LogRepo_List_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListDates provides a mock function with given fields: logType
|
||||
func (_m *LogRepo) ListDates(logType string) ([]string, error) {
|
||||
ret := _m.Called(logType)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListDates")
|
||||
}
|
||||
|
||||
var r0 []string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) ([]string, error)); ok {
|
||||
return rf(logType)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) []string); ok {
|
||||
r0 = rf(logType)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(logType)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// LogRepo_ListDates_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListDates'
|
||||
type LogRepo_ListDates_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListDates is a helper method to define mock.On call
|
||||
// - logType string
|
||||
func (_e *LogRepo_Expecter) ListDates(logType interface{}) *LogRepo_ListDates_Call {
|
||||
return &LogRepo_ListDates_Call{Call: _e.mock.On("ListDates", logType)}
|
||||
}
|
||||
|
||||
func (_c *LogRepo_ListDates_Call) Run(run func(logType string)) *LogRepo_ListDates_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *LogRepo_ListDates_Call) Return(_a0 []string, _a1 error) *LogRepo_ListDates_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *LogRepo_ListDates_Call) RunAndReturn(run func(string) ([]string, error)) *LogRepo_ListDates_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
|
||||
time "time"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MonitorRepo is an autogenerated mock type for the MonitorRepo type
|
||||
|
||||
@@ -6,9 +6,8 @@ import (
|
||||
context "context"
|
||||
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
types "github.com/acepanel/panel/pkg/types"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ProjectRepo is an autogenerated mock type for the ProjectRepo type
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
context "context"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// SSHRepo is an autogenerated mock type for the SSHRepo type
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
context "context"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
request "github.com/acepanel/panel/internal/http/request"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// SettingRepo is an autogenerated mock type for the SettingRepo type
|
||||
|
||||
@@ -5,9 +5,8 @@ package biz
|
||||
import (
|
||||
api "github.com/acepanel/panel/pkg/api"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
types "github.com/acepanel/panel/pkg/types"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// TemplateRepo is an autogenerated mock type for the TemplateRepo type
|
||||
|
||||
@@ -4,11 +4,9 @@ package biz
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
|
||||
image "image"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,12 +4,10 @@ package biz
|
||||
|
||||
import (
|
||||
http "net/http"
|
||||
time "time"
|
||||
|
||||
biz "github.com/acepanel/panel/internal/biz"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
time "time"
|
||||
)
|
||||
|
||||
// UserTokenRepo is an autogenerated mock type for the UserTokenRepo type
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user