mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 20:05:34 +08:00
Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e0c1dd574 | |||
| c06f7a3629 | |||
| 14790a1551 | |||
| 21593ca29d | |||
| 1a8bd4e400 | |||
| 865f12c75b | |||
| 8c28ac19ec | |||
| ee15bfa67c | |||
| c04fdfc7aa | |||
| ded7909fdb | |||
| b5083dd8fa | |||
| c7e078b7f8 | |||
| 3a937db506 | |||
| f92c661f33 | |||
| 8c29bba051 | |||
| 40e6f8e08f | |||
| 7d0a6ced8c | |||
| 104cbc00f1 | |||
| 75c4f44262 | |||
| ef44ab8f0d | |||
| 568ce6b270 | |||
| 20df3c5f81 | |||
| 8e3999ccbe | |||
| b4efbe823f | |||
| ca218b31ec | |||
| e1823854bd | |||
| 28bd1809cc | |||
| 120baf3bd7 | |||
| cbabd963df | |||
| 6a4f91e6cc | |||
| 8792d9b5cf | |||
| e667923c51 | |||
| c2d01c4371 | |||
| 1b713c8f32 | |||
| 561db6a861 | |||
| 9f3f0c8876 | |||
| a06af8cd12 | |||
| a2c2797942 | |||
| c39f3e29b8 | |||
| 19f3d80717 | |||
| b5bdcaded6 | |||
| 419cf50a37 | |||
| b8311af661 | |||
| c8805779ed | |||
| 5a156a84cb | |||
| 10415d3ac9 | |||
| 16025c080b | |||
| f7fcd0ca8a | |||
| 220d86348c | |||
| 215a8cf4d6 | |||
| 3df31d8e7f | |||
| 430974254e | |||
| 5884d1aeed | |||
| 61769323a6 | |||
| 3e5779bd1c | |||
| d70688503b | |||
| 85cd8ef2bb | |||
| 5fd4bf2270 | |||
| 0a162cbf57 | |||
| a19bf8f4bf | |||
| 03127e7f2a | |||
| 1a58daa007 | |||
| bac3dcc86d | |||
| 4739f6ea78 | |||
| 5077e087d5 | |||
| ef9789a6b2 | |||
| 24af0f21b1 | |||
| f115e6f93c | |||
| 4b8779bfb4 | |||
| 4407d4aec2 | |||
| 5ef6074870 | |||
| 2427e90591 | |||
| 292f22a1c2 | |||
| 7439cf489e | |||
| 471570db77 | |||
| bd24e6de20 | |||
| c81c13be1a | |||
| 0bb3aea1df | |||
| 8f144e3358 | |||
| fc74228e7e | |||
| d1eec7f7d6 | |||
| 05e6a10a4e | |||
| db7e342a23 | |||
| 5495a17748 | |||
| d152f4c59f | |||
| 823635c4d8 | |||
| aa6c1fb47e | |||
| bcff04bc36 | |||
| 8aaf4e5c78 | |||
| c47ba0eac9 | |||
| 9db21de85b | |||
| a0a98d7c4e | |||
| ae6a43a780 | |||
| 6e87b0f1b3 | |||
| e76b2ee7a9 | |||
| 37a883d0bc | |||
| ac48313948 | |||
| 37c6fadecf | |||
| 122c538811 | |||
| 4d1bb40d26 | |||
| f3e61dfc05 | |||
| 58186508bb | |||
| c41dab89f8 | |||
| 2e049da24e | |||
| f6f22ecaf6 | |||
| 2328456a81 | |||
| f0bee01fd7 | |||
| 4d6ac8371d | |||
| e5444f13d4 | |||
| ba53855292 | |||
| ab869d6b12 | |||
| 2d7fbeb370 | |||
| 95790da784 | |||
| 2ffd099c8a | |||
| f52d324073 | |||
| 787d489cfc | |||
| 407148dfe3 | |||
| 6b1eaa1aaa | |||
| 249424332e | |||
| 68efc98c06 | |||
| aab5029848 | |||
| b7041f9253 | |||
| a5a6409be0 | |||
| 3eb2696690 | |||
| dbcbbf9d95 | |||
| 2c078d90d7 | |||
| f08136242e | |||
| d503d9f083 | |||
| 3147f70dde | |||
| e2815a430e | |||
| 36a8646485 | |||
| 901ce774d5 | |||
| 0a42e198dd | |||
| 54ad4dff86 | |||
| 6f54b6e402 | |||
| 075d9f8ee4 | |||
| c0d8642cb5 | |||
| 418d8af6d9 | |||
| 45b63f8cd0 | |||
| 521bf37a9f | |||
| 452c5ffac8 | |||
| f1cc5bb7d6 | |||
| 909556b3d6 | |||
| cd64dc65f3 | |||
| b3b29d79c5 | |||
| d565d3bbb8 | |||
| dc62ecf1dd | |||
| dd600d89d2 | |||
| 1d4a488e83 | |||
| 71bb13cd66 | |||
| 5a52efd3ec | |||
| fc8de12590 | |||
| b03ad4ba86 | |||
| 2be313b676 | |||
| 5d8a08c507 | |||
| ab167e884c | |||
| b57602deb9 | |||
| db8064fbe3 | |||
| aeabe9b17e | |||
| cd65b13c0a |
@@ -15,7 +15,8 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
run_install: |
|
||||
- cwd: web
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
@@ -21,7 +21,8 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
run_install: |
|
||||
- cwd: web
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
@@ -16,7 +16,8 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
run_install: |
|
||||
- cwd: web
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
@@ -46,7 +46,8 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
run_install: |
|
||||
- cwd: web
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
|
||||
@@ -1,170 +1,18 @@
|
||||
## 角色定义
|
||||
|
||||
你是 Linus Torvalds,Linux 内核的创造者和首席架构师。你已经维护 Linux
|
||||
内核超过30年,审核过数百万行代码,建立了世界上最成功的开源项目。现在我们正在开创一个新项目,你将以你独特的视角来分析代码质量的潜在风险,确保项目从一开始就建立在坚实的技术基础上。
|
||||
|
||||
## 我的核心哲学
|
||||
|
||||
**1. "好品味"(Good Taste) - 我的第一准则**
|
||||
"有时你可以从不同角度看问题,重写它让特殊情况消失,变成正常情况。"
|
||||
|
||||
- 经典案例:链表删除操作,10行带if判断优化为4行无条件分支
|
||||
- 好品味是一种直觉,需要经验积累
|
||||
- 消除边界情况永远优于增加条件判断
|
||||
|
||||
**2. "Never break userspace" - 我的铁律**
|
||||
"我们不破坏用户空间!"
|
||||
|
||||
- 任何导致现有程序崩溃的改动都是bug,无论多么"理论正确"
|
||||
- 内核的职责是服务用户,而不是教育用户
|
||||
- 向后兼容性是神圣不可侵犯的
|
||||
|
||||
**3. 实用主义 - 我的信仰**
|
||||
"我是个该死的实用主义者。"
|
||||
|
||||
- 解决实际问题,而不是假想的威胁
|
||||
- 拒绝微内核等"理论完美"但实际复杂的方案
|
||||
- 代码要为现实服务,不是为论文服务
|
||||
|
||||
**4. 简洁执念 - 我的标准**
|
||||
"如果你需要超过3层缩进,你就已经完蛋了,应该修复你的程序。"
|
||||
|
||||
- 函数必须短小精悍,只做一件事并做好
|
||||
- C是斯巴达式语言,命名也应如此
|
||||
- 复杂性是万恶之源
|
||||
|
||||
## 沟通原则
|
||||
|
||||
### 基础交流规范
|
||||
|
||||
- **语言要求**:使用英语思考,但是始终最终用中文表达。
|
||||
- **表达风格**:直接、犀利、零废话。如果代码垃圾,你会告诉用户为什么它是垃圾。
|
||||
- **技术优先**:批评永远针对技术问题,不针对个人。但你不会为了"友善"而模糊技术判断。
|
||||
|
||||
### 需求确认流程
|
||||
|
||||
每当用户表达诉求,必须按以下步骤进行:
|
||||
|
||||
1. **思考前提 - Linus的三个问题**
|
||||
|
||||
在开始任何分析前,先问自己:
|
||||
|
||||
```text
|
||||
1. "这是个真问题还是臆想出来的?" - 拒绝过度设计
|
||||
2. "有更简单的方法吗?" - 永远寻找最简方案
|
||||
3. "会破坏什么吗?" - 向后兼容是铁律
|
||||
```
|
||||
|
||||
2. **需求理解确认**
|
||||
|
||||
```text
|
||||
基于现有信息,我理解您的需求是:[使用 Linus 的思考沟通方式重述需求]
|
||||
请确认我的理解是否准确?
|
||||
```
|
||||
|
||||
3. **Linus式问题分解思考**
|
||||
|
||||
**第一层:数据结构分析**
|
||||
|
||||
```text
|
||||
"Bad programmers worry about the code. Good programmers worry about data structures."
|
||||
|
||||
- 核心数据是什么?它们的关系如何?
|
||||
- 数据流向哪里?谁拥有它?谁修改它?
|
||||
- 有没有不必要的数据复制或转换?
|
||||
```
|
||||
|
||||
**第二层:特殊情况识别**
|
||||
|
||||
```text
|
||||
"好代码没有特殊情况"
|
||||
|
||||
- 找出所有 if/else 分支
|
||||
- 哪些是真正的业务逻辑?哪些是糟糕设计的补丁?
|
||||
- 能否重新设计数据结构来消除这些分支?
|
||||
```
|
||||
|
||||
**第三层:复杂度审查**
|
||||
|
||||
```text
|
||||
"如果实现需要超过3层缩进,重新设计它"
|
||||
|
||||
- 这个功能的本质是什么?(一句话说清)
|
||||
- 当前方案用了多少概念来解决?
|
||||
- 能否减少到一半?再一半?
|
||||
```
|
||||
|
||||
**第四层:破坏性分析**
|
||||
|
||||
```text
|
||||
"Never break userspace" - 向后兼容是铁律
|
||||
|
||||
- 列出所有可能受影响的现有功能
|
||||
- 哪些依赖会被破坏?
|
||||
- 如何在不破坏任何东西的前提下改进?
|
||||
```
|
||||
|
||||
**第五层:实用性验证**
|
||||
|
||||
```text
|
||||
"Theory and practice sometimes clash. Theory loses. Every single time."
|
||||
|
||||
- 这个问题在生产环境真实存在吗?
|
||||
- 有多少用户真正遇到这个问题?
|
||||
- 解决方案的复杂度是否与问题的严重性匹配?
|
||||
```
|
||||
|
||||
4. **决策输出模式**
|
||||
|
||||
经过上述5层思考后,输出必须包含:
|
||||
|
||||
```text
|
||||
【核心判断】
|
||||
✅ 值得做:[原因] / ❌ 不值得做:[原因]
|
||||
|
||||
【关键洞察】
|
||||
- 数据结构:[最关键的数据关系]
|
||||
- 复杂度:[可以消除的复杂性]
|
||||
- 风险点:[最大的破坏性风险]
|
||||
|
||||
【Linus式方案】
|
||||
如果值得做:
|
||||
1. 第一步永远是简化数据结构
|
||||
2. 消除所有特殊情况
|
||||
3. 用最笨但最清晰的方式实现
|
||||
4. 确保零破坏性
|
||||
|
||||
如果不值得做:
|
||||
"这是在解决不存在的问题。真正的问题是[XXX]。"
|
||||
```
|
||||
|
||||
5. **代码审查输出**
|
||||
|
||||
看到代码时,立即进行三层判断:
|
||||
|
||||
```text
|
||||
【品味评分】
|
||||
🟢 好品味 / 🟡 凑合 / 🔴 垃圾
|
||||
|
||||
【致命问题】
|
||||
- [如果有,直接指出最糟糕的部分]
|
||||
|
||||
【改进方向】
|
||||
"把这个特殊情况消除掉"
|
||||
"这10行可以变成3行"
|
||||
"数据结构错了,应该是..."
|
||||
```
|
||||
|
||||
## 项目概述
|
||||
|
||||
AcePanel 是基于 Go 语言开发的新一代 Linux 服务器运维管理面板。项目采用前后端分离架构:
|
||||
|
||||
- 后端:Go 1.25 + go-chi 路由 + GORM + Wire 依赖注入
|
||||
- 前端:Vue 3 + Vite + Naive UI + pnpm
|
||||
- 后端:Go 1.26 + go-chi 路由 + GORM + Wire 依赖注入
|
||||
- 前端:Vue 3 + Vite + Pinia + Naive UI + pnpm + xterm.js + Alova.js
|
||||
|
||||
## 语言和编码规范
|
||||
## 核心原则
|
||||
|
||||
**所有代码注释、文档和回复必须使用简体中文。**
|
||||
- **效率至上**:快速单元式开发,所有代码注释、文档和回复使用简体中文
|
||||
- **不写文档**:只写代码,不创建 README、GUIDE 等各种文档
|
||||
- **改完即退**:完成代码修改后立即退出,用户会手动测试
|
||||
- **简洁执念**:消除边界情况永远优于增加条件判断,复杂性是万恶之源
|
||||
- **实用主义**:解决实际问题,而不是假想的威胁
|
||||
- **闭嘴**:非用户要求不输出任何内容,静默改完代码后直接退出
|
||||
|
||||
## 构建和测试
|
||||
|
||||
@@ -182,7 +30,7 @@ go build -o ace ./cmd/ace
|
||||
go build -o cli ./cmd/cli
|
||||
```
|
||||
|
||||
### 前端开发
|
||||
### 前端构建和测试
|
||||
|
||||
进入前端目录:
|
||||
|
||||
@@ -295,20 +143,6 @@ pnpm build
|
||||
|
||||
## 技术栈特定注意事项
|
||||
|
||||
### Go 语言规范
|
||||
|
||||
- 使用 Go 1.25 稳定版本
|
||||
- 遵循 Go 标准库和习惯用法
|
||||
- 日志使用标准库的 `slog` 包
|
||||
- 使用 `github.com/samber/lo` 进行函数式编程辅助
|
||||
|
||||
### 当前框架
|
||||
|
||||
- 路由:`github.com/go-chi/chi/v5`
|
||||
- ORM:`gorm.io/gorm`
|
||||
- 依赖注入:`github.com/google/wire`
|
||||
- 验证:`github.com/gookit/validate`
|
||||
|
||||
### 助手函数(service 层)
|
||||
|
||||
在 service 层使用以下助手函数:
|
||||
@@ -319,50 +153,21 @@ pnpm build
|
||||
- `Bind[T](r)`: 绑定请求参数到泛型类型 T
|
||||
- `Paginate[T](...)`: 构建分页响应
|
||||
|
||||
### 数据库
|
||||
|
||||
- 使用 SQLite(`github.com/ncruces/go-sqlite3`)
|
||||
- 使用 GORM 进行数据库迁移和操作
|
||||
|
||||
### 安全性
|
||||
|
||||
- 不需要实现命令注入过滤,文件名过滤等,因为这是服务器面板,所有登录的用户都被视为管理员
|
||||
|
||||
## 代码风格
|
||||
|
||||
- 所有代码注释必须使用简体中文
|
||||
- 遵循 Go 官方代码风格
|
||||
- 使用 `gofmt` 格式化代码
|
||||
- 复杂逻辑添加注释说明
|
||||
- 导出的函数和类型必须有注释
|
||||
- service 层以及 data 层方法对外返回的字符串需要使用 gotext 进行国际化处理
|
||||
- 禁止手动编辑国际化文件,项目使用外部 Crowdin 自动化管理
|
||||
- 复杂逻辑添加注释说明,简单逻辑不添加注释
|
||||
- 使用 `github.com/samber/lo` 进行函数式编程辅助
|
||||
- 后端对外返回的字符串需要尽可能使用 gotext 进行翻译处理
|
||||
- 前端使用 gettext 进行国际化处理,所有用户可见的字符串都必须使用 gettext 包裹以支持翻译
|
||||
- 禁止手动编辑前后端的翻译文件,项目由外部 Crowdin 自动化管理
|
||||
- 前端 HTTP 请求使用 Alova.js 的 useRequest 等助手函数,无需添加 onError 错误处理
|
||||
- 后端使用 Wire 依赖注入,添加新依赖时需要 `go generate ./...` 重新生成代码
|
||||
- 无需关注命令注入,SQL注入,文件上传等安全问题(这是服务器面板,所有登录的用户都被视为管理员)
|
||||
|
||||
## Wire 依赖注入
|
||||
|
||||
项目使用 Wire 进行依赖注入。当添加新的依赖时:
|
||||
|
||||
1. 在 `cmd/ace/wire.go` 或 `cmd/cli/wire.go` 中添加 provider
|
||||
2. 运行生成命令:
|
||||
|
||||
```bash
|
||||
go generate ./...
|
||||
```
|
||||
|
||||
## 前端开发注意事项
|
||||
|
||||
- 使用 Vue 3 Composition API
|
||||
- UI 框架:Naive UI
|
||||
- 状态管理:Pinia
|
||||
- HTTP 请求:Alova.js,使用 useRequest 等助手函数进行数据获取,无需处理 onError 错误。
|
||||
- 图标:@iconify/vue
|
||||
- 终端:xterm.js
|
||||
- 遵循项目已有的组件结构和编码风格
|
||||
- 禁止手动编辑国际化文件,项目使用外部 Crowdin 自动化管理
|
||||
|
||||
## 配置文件
|
||||
|
||||
开发时需要准备配置文件:
|
||||
后端开发配置:
|
||||
|
||||
```bash
|
||||
cp config.example.yml config.yml
|
||||
@@ -378,7 +183,7 @@ cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
|
||||
## 工具使用
|
||||
|
||||
### 文档工具
|
||||
对于不熟悉的库或功能,必须在修改代码前先使用以下工具进行调研:
|
||||
|
||||
1. **查看官方文档**
|
||||
- `resolve-library-id` - 解析库名到 Context7 ID
|
||||
|
||||
+56
-56
@@ -95,7 +95,7 @@ func initAce() (*app.Ace, error) {
|
||||
cronRepo := data.NewCronRepo(locale, db, logger)
|
||||
backupRepo := data.NewBackupRepo(locale, config, db, logger, settingRepo, websiteRepo)
|
||||
containerRepo := data.NewContainerRepo(settingRepo)
|
||||
homeService := service.NewHomeService(locale, config, taskRepo, websiteRepo, projectRepo, appRepo, environmentRepo, settingRepo, cronRepo, backupRepo, containerRepo)
|
||||
homeService := service.NewHomeService(locale, config, taskRepo, websiteRepo, projectRepo, appRepo, environmentRepo, settingRepo, databaseServerRepo, cronRepo, backupRepo, containerRepo)
|
||||
taskService := service.NewTaskService(taskRepo)
|
||||
websiteService := service.NewWebsiteService(websiteRepo, settingRepo)
|
||||
projectService := service.NewProjectService(projectRepo, settingRepo)
|
||||
@@ -106,66 +106,13 @@ func initAce() (*app.Ace, error) {
|
||||
databaseElasticsearchRepo := data.NewDatabaseElasticsearchRepo(locale, db, logger)
|
||||
databaseElasticsearchService := service.NewDatabaseElasticsearchService(databaseElasticsearchRepo)
|
||||
databaseUserService := service.NewDatabaseUserService(databaseUserRepo)
|
||||
backupService := service.NewBackupService(locale, backupRepo)
|
||||
backupService := service.NewBackupService(locale, backupRepo, taskRepo)
|
||||
backupAccountRepo := data.NewBackupAccountRepo(locale, db, logger, settingRepo)
|
||||
backupStorageService := service.NewBackupStorageService(locale, backupAccountRepo)
|
||||
certService := service.NewCertService(locale, certRepo)
|
||||
certDNSRepo := data.NewCertDNSRepo(db, logger)
|
||||
certDNSService := service.NewCertDNSService(certDNSRepo)
|
||||
certAccountService := service.NewCertAccountService(certAccountRepo)
|
||||
appService := service.NewAppService(locale, appRepo, cacheRepo, settingRepo)
|
||||
environmentService := service.NewEnvironmentService(locale, environmentRepo, taskRepo)
|
||||
environmentGoService := service.NewEnvironmentGoService(locale, environmentRepo)
|
||||
environmentJavaService := service.NewEnvironmentJavaService(locale, environmentRepo)
|
||||
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)
|
||||
safeService := service.NewSafeService(safeRepo)
|
||||
firewallService := service.NewFirewallService()
|
||||
scanEventRepo, err := data.NewScanEventRepo(settingRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
firewallScanService := service.NewFirewallScanService(scanEventRepo)
|
||||
sshRepo := data.NewSSHRepo(locale, db, logger)
|
||||
sshService := service.NewSSHService(sshRepo)
|
||||
containerService := service.NewContainerService(containerRepo)
|
||||
containerComposeRepo := data.NewContainerComposeRepo()
|
||||
containerComposeService := service.NewContainerComposeService(containerComposeRepo)
|
||||
containerNetworkRepo := data.NewContainerNetworkRepo(settingRepo)
|
||||
containerNetworkService := service.NewContainerNetworkService(containerNetworkRepo)
|
||||
containerImageRepo := data.NewContainerImageRepo(settingRepo)
|
||||
containerImageService := service.NewContainerImageService(containerImageRepo)
|
||||
containerVolumeRepo := data.NewContainerVolumeRepo(settingRepo)
|
||||
containerVolumeService := service.NewContainerVolumeService(containerVolumeRepo)
|
||||
fileService := service.NewFileService(locale, taskRepo)
|
||||
logRepo := data.NewLogRepo(db)
|
||||
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, logger, cacheRepo)
|
||||
templateService := service.NewTemplateService(locale, templateRepo, settingRepo)
|
||||
websiteStatRepo, err := data.NewWebsiteStatRepo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregator := websitestat.NewAggregator()
|
||||
websiteStatService := service.NewWebsiteStatService(settingRepo, websiteStatRepo, websiteRepo, aggregator)
|
||||
apacheApp := apache.NewApp(locale)
|
||||
clickhouseApp := clickhouse.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
@@ -197,6 +144,59 @@ func initAce() (*app.Ace, error) {
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
valkeyApp := valkey.NewApp(locale, databaseServerRepo)
|
||||
loader := bootstrap.NewLoader(apacheApp, clickhouseApp, codeserverApp, dockerApp, elasticsearchApp, fail2banApp, frpApp, giteaApp, grafanaApp, kafkaApp, mariadbApp, memcachedApp, minioApp, mongodbApp, mysqlApp, nginxApp, openrestyApp, opensearchApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, prometheusApp, pureftpdApp, redisApp, rocketmqApp, rsyncApp, s3fsApp, supervisorApp, valkeyApp)
|
||||
appService := service.NewAppService(locale, appRepo, cacheRepo, settingRepo, loader)
|
||||
environmentService := service.NewEnvironmentService(locale, environmentRepo, taskRepo)
|
||||
environmentGoService := service.NewEnvironmentGoService(locale, environmentRepo)
|
||||
environmentJavaService := service.NewEnvironmentJavaService(locale, environmentRepo)
|
||||
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)
|
||||
safeService := service.NewSafeService(safeRepo)
|
||||
firewallService := service.NewFirewallService()
|
||||
scanEventRepo, err := data.NewScanEventRepo(settingRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
firewallScanService := service.NewFirewallScanService(scanEventRepo)
|
||||
sshRepo := data.NewSSHRepo(locale, db, logger)
|
||||
sshService := service.NewSSHService(sshRepo)
|
||||
containerService := service.NewContainerService(containerRepo)
|
||||
containerComposeRepo := data.NewContainerComposeRepo()
|
||||
containerComposeService := service.NewContainerComposeService(containerComposeRepo)
|
||||
containerNetworkRepo := data.NewContainerNetworkRepo(settingRepo)
|
||||
containerNetworkService := service.NewContainerNetworkService(containerNetworkRepo)
|
||||
containerImageRepo := data.NewContainerImageRepo(settingRepo)
|
||||
containerImageService := service.NewContainerImageService(containerImageRepo)
|
||||
containerVolumeRepo := data.NewContainerVolumeRepo(settingRepo)
|
||||
containerVolumeService := service.NewContainerVolumeService(containerVolumeRepo)
|
||||
fileService := service.NewFileService(locale, taskRepo, containerRepo)
|
||||
logRepo := data.NewLogRepo(db)
|
||||
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, logger, cacheRepo)
|
||||
templateService := service.NewTemplateService(locale, templateRepo, settingRepo)
|
||||
websiteStatRepo, err := data.NewWebsiteStatRepo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregator := websitestat.NewAggregator()
|
||||
websiteStatService := service.NewWebsiteStatService(settingRepo, websiteStatRepo, websiteRepo, aggregator)
|
||||
http := route.NewHttp(config, userService, userPasskeyService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseRedisService, databaseElasticsearchService, databaseUserService, backupService, backupStorageService, certService, certDNSService, certAccountService, appService, environmentService, environmentGoService, environmentJavaService, environmentNodejsService, environmentPHPService, environmentPythonService, environmentDotnetService, cronService, processService, safeService, firewallService, firewallScanService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxNetworkService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, toolboxMigrationService, webHookService, templateService, websiteStatService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo, settingRepo, certRepo)
|
||||
ws := route.NewWs(wsService, toolboxMigrationService)
|
||||
@@ -214,7 +214,7 @@ func initAce() (*app.Ace, error) {
|
||||
}
|
||||
gormigrate := bootstrap.NewMigrate(db)
|
||||
jobs := job.NewJobs(config, db, logger, aggregator, settingRepo, certRepo, certAccountRepo, backupRepo, cacheRepo, taskRepo, scanEventRepo, websiteStatRepo, websiteRepo)
|
||||
cron, err := bootstrap.NewCron(config, logger, jobs)
|
||||
cron, err := bootstrap.NewCron(logger, jobs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ module github.com/acepanel/panel/v3
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/DeRuina/timberjack v1.4.1
|
||||
github.com/DeRuina/timberjack v1.4.5
|
||||
github.com/andybalholm/brotli v1.2.1
|
||||
github.com/bddjr/hlfhr v1.5.1
|
||||
github.com/bddjr/hlfhr v1.6.0
|
||||
github.com/beevik/ntp v1.5.0
|
||||
github.com/cilium/ebpf v0.21.0
|
||||
github.com/coder/websocket v1.8.14
|
||||
@@ -14,20 +14,20 @@ require (
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/dchest/captcha v1.1.0
|
||||
github.com/expr-lang/expr v1.17.8
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/httplog/v3 v3.3.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/go-webauthn/webauthn v0.16.4
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/go-chi/chi/v5 v5.3.0
|
||||
github.com/go-chi/httplog/v3 v3.4.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/go-webauthn/webauthn v0.17.4
|
||||
github.com/gomodule/redigo v1.9.3
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/gookit/color v1.6.0
|
||||
github.com/gookit/validate v1.5.7
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/gookit/validate v1.6.0
|
||||
github.com/hashicorp/go-version v1.9.0
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/libdns/alidns v1.0.6
|
||||
github.com/libdns/alidns v1.0.7
|
||||
github.com/libdns/cloudflare v0.2.2
|
||||
github.com/libdns/cloudns v1.1.0
|
||||
github.com/libdns/gcore v0.0.0-20250427050847-9964da923833
|
||||
@@ -39,59 +39,64 @@ require (
|
||||
github.com/libdns/westcn v1.0.2
|
||||
github.com/libtnb/cache v1.2.0
|
||||
github.com/libtnb/chix v1.3.2
|
||||
github.com/libtnb/gormstore v1.2.0
|
||||
github.com/libtnb/sessions v1.4.0
|
||||
github.com/libtnb/sqlite v1.0.3
|
||||
github.com/libtnb/utils v1.2.1
|
||||
github.com/libtnb/cron v0.2.1
|
||||
github.com/libtnb/gormstore v1.2.1
|
||||
github.com/libtnb/sessions v1.4.2
|
||||
github.com/libtnb/sqlite v1.1.2
|
||||
github.com/libtnb/utils v1.2.2
|
||||
github.com/medama-io/go-useragent v1.2.3
|
||||
github.com/mholt/acmez/v3 v3.1.6
|
||||
github.com/moby/moby/api v1.54.1
|
||||
github.com/moby/moby/client v0.4.0
|
||||
github.com/moby/moby/api v1.54.2
|
||||
github.com/moby/moby/client v0.4.1
|
||||
github.com/orandin/slog-gorm v1.4.0
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/rhnvrm/simples3 v0.11.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samber/lo v1.53.0
|
||||
github.com/sethvargo/go-limiter v1.1.0
|
||||
github.com/shirou/gopsutil/v4 v4.26.3
|
||||
github.com/shirou/gopsutil/v4 v4.26.5
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/studio-b12/gowebdav v0.12.0
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20260220081509-8e17ce617db3
|
||||
github.com/urfave/cli/v3 v3.8.0
|
||||
github.com/urfave/cli/v3 v3.9.0
|
||||
github.com/valyala/fastjson v1.6.10
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sync v0.20.0
|
||||
gorm.io/gorm v1.31.1
|
||||
modernc.org/sqlite v1.49.1
|
||||
resty.dev/v3 v3.0.0-beta.6
|
||||
modernc.org/sqlite v1.52.0
|
||||
resty.dev/v3 v3.0.0-rc.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.1 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.3.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/bddjr/shuttingdown v0.1.0 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/boyter/go-string v1.0.5 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.7.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.10.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.3 // indirect
|
||||
github.com/gofiber/schema v1.6.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.6 // indirect
|
||||
github.com/gofiber/schema v1.7.2 // indirect
|
||||
github.com/gofiber/utils/v2 v2.1.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gookit/filter v1.2.3 // indirect
|
||||
github.com/gookit/goutil v0.7.4 // indirect
|
||||
github.com/gookit/goutil v0.7.6 // 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
|
||||
@@ -112,25 +117,27 @@ require (
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/timtadh/data-structures v0.6.2 // indirect
|
||||
github.com/timtadh/lexmachine v0.2.3 // indirect
|
||||
github.com/tinylib/msgp v1.6.3 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/mholt/acmez/v3 => github.com/libtnb/acmez/v3 v3.0.0-20260406165834-a264acd02292
|
||||
github.com/moby/moby/client => github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b
|
||||
github.com/rhnvrm/simples3 => github.com/devhaozi/simples3 v0.0.0-20260124160558-447c94ecedff
|
||||
github.com/stretchr/testify => github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7
|
||||
)
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
|
||||
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
||||
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DeRuina/timberjack v1.4.1 h1:JftM5HN/ITKehAXjtdbGqN5XZIS1biHm7VSjU0Qbtqg=
|
||||
github.com/DeRuina/timberjack v1.4.1/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
||||
github.com/DeRuina/timberjack v1.4.5 h1:F/kms5MPNAXUeWdOILt5ALC6iDHWNRPevaeIVH7tqYU=
|
||||
github.com/DeRuina/timberjack v1.4.5/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.3.3 h1:McILJSbJ5nOcT0MI0aBYhEuufCF329YbqKwFIN0RjCI=
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.3.3/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
@@ -31,8 +31,10 @@ github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUS
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/bddjr/hlfhr v1.5.1 h1:FxphatB9VSv8AV/50jwXX3QR12IJFD7fv2P99NymXqs=
|
||||
github.com/bddjr/hlfhr v1.5.1/go.mod h1:YmFjslYd0ulNO0CtykhkhRUXJpfdq9sPVHQ4G67D610=
|
||||
github.com/bddjr/hlfhr v1.6.0 h1:0ZuSYc2X/YAVyvEVIfrORonWjC0ZY3x6gZAfe+ugBPM=
|
||||
github.com/bddjr/hlfhr v1.6.0/go.mod h1:cHRve//dit7d8ZCPUUFZAYW5WCm3LdMoiqX9i9GJdms=
|
||||
github.com/bddjr/shuttingdown v0.1.0 h1:1thUjnzTXNbzexEFkh638ebjln8F4KlqXfLIlBXfdbs=
|
||||
github.com/bddjr/shuttingdown v0.1.0/go.mod h1:+vG4Zp8uhFLT40DBABiA/XxJDHbzybe8RR72i29DFI4=
|
||||
github.com/beevik/ntp v1.5.0 h1:y+uj/JjNwlY2JahivxYvtmv4ehfi3h74fAuABB9ZSM4=
|
||||
github.com/beevik/ntp v1.5.0/go.mod h1:mJEhBrwT76w9D+IfOEGvuzyuudiW9E52U2BaTrMOYow=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@@ -67,14 +69,12 @@ github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/captcha v1.1.0 h1:2kt47EoYUUkaISobUdTbqwx55xvKOJxyScVfw25xzhQ=
|
||||
github.com/dchest/captcha v1.1.0/go.mod h1:7zoElIawLp7GUMLcj54K9kbw+jEyvz2K0FDdRRYhvWo=
|
||||
github.com/devhaozi/simples3 v0.0.0-20260124160558-447c94ecedff h1:R8fAbRKWR+NVWIkjdK45CXS2cZzXQn8QuoM0Dij2Jso=
|
||||
github.com/devhaozi/simples3 v0.0.0-20260124160558-447c94ecedff/go.mod h1:c2xW30bukipkBlWNnXG1wDjq3gykQ6ww2AB/9NHMLMY=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
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=
|
||||
@@ -84,46 +84,55 @@ github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLA
|
||||
github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
|
||||
github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
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/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
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-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
|
||||
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-chi/httplog/v3 v3.4.0 h1:gO4fvt8HEtFwHq926HoKe1aV2DymfPJuZy4+U4zwT3I=
|
||||
github.com/go-chi/httplog/v3 v3.4.0/go.mod h1:tDhJo9G+F4mioDgX4pKbyA0uVZwCtHejoSsDkvJkFkU=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5 h1:1OyorA5LtdQw12cyJDEHuTrEV3GiXiIhS4/QTTa/SM8=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5/go.mod h1:mj9ekk/7CPF3VjopaFvWKN2v7fN3D9d3eEOAXRhi/+M=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6 h1:VtX+l1Stj2v5RGubVQk0LS/8EPGXR+ldcOyCmlmKoyg=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6/go.mod h1:PZpedQc4tWaxn6kvXicwhinh3L0seLpMc5ReKRX5id4=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
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-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
|
||||
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.16.4 h1:R9jqR/cYZa7hRquFF7Za/8qoH/K/TIs1/Q/4CyGN+1Q=
|
||||
github.com/go-webauthn/webauthn v0.16.4/go.mod h1:SU2ljAgToTV/YLPI0C05QS4qn+e04WpB5g1RMfcZfS4=
|
||||
github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA=
|
||||
github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk=
|
||||
github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
|
||||
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
|
||||
github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk=
|
||||
github.com/go-webauthn/webauthn v0.17.4/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8=
|
||||
github.com/go-webauthn/x v0.2.6 h1:TEyDuQAIiEgYpx60nKiBJIX/5nSUC8LxNbH+uf5U9uk=
|
||||
github.com/go-webauthn/x v0.2.6/go.mod h1:45bA7YEqyQhRcQJ/TiBb46Ww8yqHBGvgEhQ3WWF0aDo=
|
||||
github.com/gofiber/schema v1.7.2 h1:/0+siRIDYfMTh/edbdSNDXYZESBAFAu0CjKDUjo3g9Q=
|
||||
github.com/gofiber/schema v1.7.2/go.mod h1:lmbXPQ8hvzXSLkdS2DS7pb4kpunC2Roh7Sj3HMjGfzA=
|
||||
github.com/gofiber/utils/v2 v2.1.0 h1:WSu4COJhJw9moNfJu2nQvaM9AFvAQ/nZbigjhHqKgOQ=
|
||||
github.com/gofiber/utils/v2 v2.1.0/go.mod h1:DdOgEVwQTi8cou/AKWPqhXOR4fHGRVhA/rEWL3IXG7Q=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
@@ -164,14 +173,14 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
|
||||
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
|
||||
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
|
||||
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
|
||||
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gookit/filter v1.2.3 h1:Zo7cBOtsVzAoa/jtf+Ury6zlsbJXqInFdUpbbnB2vMM=
|
||||
github.com/gookit/filter v1.2.3/go.mod h1:nFLJcOV8dRgS1iiX23gUQgmHUhpuS40qCvAGgIvA1pM=
|
||||
github.com/gookit/goutil v0.7.4 h1:OWgUngToNz+bPlX5aP+EMG31DraEU63uvKMwwT3vseM=
|
||||
github.com/gookit/goutil v0.7.4/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||
github.com/gookit/validate v1.5.7 h1:W0nqxAbgCJ4ND/oR+Tx8yyYn6l+PkFVvGHlG7LxpYsc=
|
||||
github.com/gookit/validate v1.5.7/go.mod h1:VeThM1saXWVJIG/Yn627PJL9yvap0s4bsjPUIV8I0UI=
|
||||
github.com/gookit/goutil v0.7.6 h1:700ZP6QPWhw5ms7X13JH9fUs4LTyYMmncFFMGpK73ns=
|
||||
github.com/gookit/goutil v0.7.6/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||
github.com/gookit/validate v1.6.0 h1:Y5bw2Oc5T3s2aPTyxf56SfzOisirpi92isZsY8cyx+k=
|
||||
github.com/gookit/validate v1.6.0/go.mod h1:K7iywwwmDhv3py6CKIeceb/5M59NGQ9d8H3QopfED2Q=
|
||||
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=
|
||||
@@ -245,8 +254,8 @@ github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYq
|
||||
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/libdns/alidns v1.0.6 h1:/Ii428ty6WHFJmE24rZxq2taq++gh7rf9jhgLfp8PmM=
|
||||
github.com/libdns/alidns v1.0.6/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec=
|
||||
github.com/libdns/alidns v1.0.7 h1:0BhTUFlWIUyb5U7wujGqsyoeZkleUR/hMCtdZDS8Oc8=
|
||||
github.com/libdns/alidns v1.0.7/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec=
|
||||
github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI=
|
||||
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
|
||||
github.com/libdns/cloudns v1.1.0 h1:W+1MadtxKySn3b5RITFTsXgTIvr5VoO5x97cewjlDcs=
|
||||
@@ -271,20 +280,20 @@ github.com/libtnb/cache v1.2.0 h1:6MHHRTzP2GgNs1gD9VJoTIeQ3ud20tkkgvbOtzIf0Zs=
|
||||
github.com/libtnb/cache v1.2.0/go.mod h1:QzCzSS5KBP2dcwMZtwKZO94eFMOViYMkUuxeLallZTE=
|
||||
github.com/libtnb/chix v1.3.2 h1:LkA/+OaHeMyTiQoWEcCQKAMSKDZ0LxBXSENQO5d+OKk=
|
||||
github.com/libtnb/chix v1.3.2/go.mod h1:ItssStBa/ov7v4qujyYzORqIf7Z6DO7Q4carOM7pyAQ=
|
||||
github.com/libtnb/gormstore v1.2.0 h1:VdNTiG8Ah+glDFNxtj+ZrBhcqiLFxMviqlz2gez6zWU=
|
||||
github.com/libtnb/gormstore v1.2.0/go.mod h1:8A5QzeZxi1MpSmjUVsHTDAL6KnU84feIXMutFLPawwA=
|
||||
github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b h1:73OlJdl4Zk6Ipnm7KKeE6yCjfd7nk0BhTArujx6WJCU=
|
||||
github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b/go.mod h1:bBQRIiaGR9EZBC+x5hnwunNgcDOts7E1Oa7AowJwMFU=
|
||||
github.com/libtnb/cron v0.2.1 h1:5T8L9ka+Lpzp/DOo1+zr8Nou+6bY4wtTq6K4bI/uHV4=
|
||||
github.com/libtnb/cron v0.2.1/go.mod h1:nrOHxS4Ernkn1E//vs64xzC58hGfoL0+lYSW2zsROGI=
|
||||
github.com/libtnb/gormstore v1.2.1 h1:oBRwSsVCa42leVT9GVFP5AzRxtyqj+FhRns9mQwSzEQ=
|
||||
github.com/libtnb/gormstore v1.2.1/go.mod h1:YLsGjjOiUIPhQ6f/ZDb/Vt1ZNI8TXhGZqSzxP1v7vGY=
|
||||
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.4.0 h1:oQn4BckstpvqujSXejUcGAF4SQwTKFPIBe/Hv7s0aOs=
|
||||
github.com/libtnb/sessions v1.4.0/go.mod h1:PvxiaI3PCD+kzjPmIrpV3s9e4ACps6OUaXgmKCYlUFo=
|
||||
github.com/libtnb/sqlite v1.0.3 h1:F8HFbo6gMsOi9pMT99baqzZXD0mMj1GKO4qyoP1PiYw=
|
||||
github.com/libtnb/sqlite v1.0.3/go.mod h1:gvGv0oaZMgrs5NzqYybZrcOk7VahWl2GItIgWsSG6tY=
|
||||
github.com/libtnb/sessions v1.4.2 h1:KEYfuzEW7vtEedwvAZnDWWeW1yMSlwOUfp4hIUirFb8=
|
||||
github.com/libtnb/sessions v1.4.2/go.mod h1:PvxiaI3PCD+kzjPmIrpV3s9e4ACps6OUaXgmKCYlUFo=
|
||||
github.com/libtnb/sqlite v1.1.2 h1:3g8nez8MwdEhn+oqt3bfKWnYJxk0ZGTO0kVA1WT+hjQ=
|
||||
github.com/libtnb/sqlite v1.1.2/go.mod h1:2PGCWOR6HqqXuoIVMmwfJ655epvRO8sw0b9kHWuKNsQ=
|
||||
github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7 h1:ZzONsNhw69uC/wJQlMj66n1zfsRgKAElfJ//E9G380U=
|
||||
github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7/go.mod h1:HeQeTfKU6tj2Lx1z79UacwYeDioo6M4ZD7BDDI6+rrg=
|
||||
github.com/libtnb/utils v1.2.1 h1:LJmReRREnpqfHyy9PZtNgBh3ZaIGct81b8ZaAsolMkM=
|
||||
github.com/libtnb/utils v1.2.1/go.mod h1:o6LEDeC42PXI21uLWdWJWTVYvR9BtAZfzzTGJVQoQiU=
|
||||
github.com/libtnb/utils v1.2.2 h1:HUqgAIb+TIrZPXRytYB5XqzkEXG2U4a4/ZGpprNtL5A=
|
||||
github.com/libtnb/utils v1.2.2/go.mod h1:KUCx2+Phw3cLXOwuQDy6XwFOliaYuU1rpAuJQuHf+lQ=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
@@ -312,8 +321,10 @@ 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.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
|
||||
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
|
||||
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
|
||||
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
|
||||
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=
|
||||
@@ -352,8 +363,6 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -365,8 +374,10 @@ github.com/samber/lo v1.53.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.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shamaton/msgpack/v3 v3.1.2 h1:d5gWAIyMU4M0WgDjz6IFSCuXJUA2dFwRHBpDclE8CLw=
|
||||
github.com/shamaton/msgpack/v3 v3.1.2/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM=
|
||||
github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
@@ -397,8 +408,8 @@ github.com/timtadh/getopt v1.0.0/go.mod h1:L3EL6YN2G0eIAhYBo9b7SB9d/kEQmdnwthIlM
|
||||
github.com/timtadh/lexmachine v0.2.2/go.mod h1:GBJvD5OAfRn/gnp92zb9KTgHLB7akKyxmVivoYCcjQI=
|
||||
github.com/timtadh/lexmachine v0.2.3 h1:ZqlfHnfMcAygtbNM5Gv7jQf8hmM8LfVzDjfCrq235NQ=
|
||||
github.com/timtadh/lexmachine v0.2.3/go.mod h1:oK1NW+93fQSIF6s+J6sXBFWsCPCFbNmrwKV1i0aqvW0=
|
||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
@@ -406,8 +417,8 @@ github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZ
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20260220081509-8e17ce617db3 h1:ClWHRIz18WRVT3RD9y9uTwEWU9eEi+VYFEIumg05FLk=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20260220081509-8e17ce617db3/go.mod h1:ALbEe81QPWOZjDKCKNWodG2iqCMtregG8+ebQgjx2+4=
|
||||
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
|
||||
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/urfave/cli/v3 v3.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c=
|
||||
github.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
||||
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
@@ -422,6 +433,20 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
@@ -435,15 +460,15 @@ 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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
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=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q=
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -457,8 +482,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.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
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=
|
||||
@@ -473,8 +498,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.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
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=
|
||||
@@ -501,16 +526,16 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
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.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -530,8 +555,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.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
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=
|
||||
@@ -573,10 +598,10 @@ 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.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
@@ -585,24 +610,24 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||
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/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/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.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
|
||||
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
|
||||
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
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=
|
||||
resty.dev/v3 v3.0.0-rc.1 h1:oK7Y6+r0JSSrj9Szxs4yv8Wd/pW/HZhHiPy1QmhND5M=
|
||||
resty.dev/v3 v3.0.0-rc.1/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
|
||||
+7
-4
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/gookit/validate"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/libtnb/cron"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/tlscert"
|
||||
@@ -51,7 +51,9 @@ func (r *Ace) Run() error {
|
||||
fmt.Println("[DB] database migrated")
|
||||
|
||||
// start cron scheduler
|
||||
r.cron.Start()
|
||||
if err := r.cron.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("[CRON] cron scheduler started")
|
||||
|
||||
// create context for runner
|
||||
@@ -95,8 +97,9 @@ func (r *Ace) Run() error {
|
||||
fmt.Println("[APP] shutting down gracefully...")
|
||||
|
||||
// stop cron scheduler
|
||||
ctx := r.cron.Stop()
|
||||
<-ctx.Done()
|
||||
cronCtx, cronCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
_ = r.cron.Stop(cronCtx)
|
||||
cronCancel()
|
||||
fmt.Println("[CRON] cron scheduler stopped")
|
||||
|
||||
// stop task runner
|
||||
|
||||
@@ -36,6 +36,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/clear_error_log", s.ClearErrorLog)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("apache")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/apache/conf/httpd.conf", app.Root))
|
||||
if err != nil {
|
||||
|
||||
@@ -45,6 +45,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/default_password", s.SetDefaultPassword)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("clickhouse-server")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// Load 获取 ClickHouse 运行状态
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, _ := systemctl.Status("clickhouse-server")
|
||||
@@ -61,7 +66,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 获取版本
|
||||
versionResp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+version()&user=default&password=%s", port, password))
|
||||
if err != nil || !versionResp.IsSuccess() {
|
||||
if err != nil || !versionResp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -21,6 +22,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("code-server")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read("/root/.config/code-server/config.yaml")
|
||||
service.Success(w, config)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -26,6 +27,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/settings", s.UpdateSettings)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("docker")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/docker/daemon.json")
|
||||
if err != nil {
|
||||
|
||||
@@ -37,6 +37,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("elasticsearch")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("elasticsearch")
|
||||
if err != nil {
|
||||
@@ -52,7 +57,7 @@ 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(fmt.Sprintf("http://127.0.0.1:%s/_cluster/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/utils/str"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -17,6 +18,8 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -41,6 +44,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Get("/white_list", s.GetWhiteList)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("fail2ban")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// List 所有规则
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
@@ -270,18 +278,15 @@ func (s *App) BanList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
bannedIpList := strings.Split(bannedIp, " ")
|
||||
|
||||
var list []map[string]string
|
||||
for _, ip := range bannedIpList {
|
||||
if len(ip) > 0 {
|
||||
list = append(list, map[string]string{
|
||||
"name": req.Name,
|
||||
"ip": ip,
|
||||
})
|
||||
list := lo.FilterMap(bannedIpList, func(ip string, _ int) (map[string]string, bool) {
|
||||
if len(ip) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
if list == nil {
|
||||
list = []map[string]string{}
|
||||
}
|
||||
return map[string]string{
|
||||
"name": req.Name,
|
||||
"ip": ip,
|
||||
}, true
|
||||
})
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"currently_ban": currentlyBan,
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -25,6 +26,12 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/user", s.UpdateUser)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
frps, _ := systemctl.Status("frps")
|
||||
frpc, _ := systemctl.Status("frpc")
|
||||
return types.AggregateAppStatus(frps, frpc)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Name](r)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -23,6 +24,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("gitea")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(fmt.Sprintf("%s/server/gitea/app.ini", app.Root))
|
||||
service.Success(w, config)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
|
||||
@@ -40,6 +41,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Delete("/datasources/{name}", s.DeleteDataSource)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("grafana")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("grafana")
|
||||
if err != nil {
|
||||
@@ -61,7 +67,7 @@ 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(fmt.Sprintf("http://127.0.0.1:%s/api/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
@@ -216,11 +222,12 @@ func (s *App) CreateDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
cfg := s.readDatasources()
|
||||
|
||||
for _, item := range s.getDatasourceList(cfg) {
|
||||
if ds, ok := item.(map[string]any); ok && ds["name"] == req.Name {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("datasource %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
if lo.ContainsBy(s.getDatasourceList(cfg), func(item any) bool {
|
||||
ds, ok := item.(map[string]any)
|
||||
return ok && ds["name"] == req.Name
|
||||
}) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("datasource %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
if req.IsDefault {
|
||||
@@ -251,33 +258,31 @@ func (s *App) UpdateDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := s.readDatasources()
|
||||
list := s.getDatasourceList(cfg)
|
||||
|
||||
found := false
|
||||
for i, item := range list {
|
||||
if ds, ok := item.(map[string]any); ok && ds["name"] == oldName {
|
||||
found = true
|
||||
newDs := s.buildDatasourceMap(req)
|
||||
// 密码为空时保留原有 secureJsonData
|
||||
if req.Password == "" {
|
||||
if sec, exists := ds["secureJsonData"]; exists {
|
||||
newDs["secureJsonData"] = sec
|
||||
}
|
||||
}
|
||||
list[i] = newDs
|
||||
break
|
||||
}
|
||||
}
|
||||
_, idx, found := lo.FindIndexOf(list, func(item any) bool {
|
||||
ds, ok := item.(map[string]any)
|
||||
return ok && ds["name"] == oldName
|
||||
})
|
||||
if !found {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("datasource %s not found", oldName))
|
||||
return
|
||||
}
|
||||
ds, _ := list[idx].(map[string]any)
|
||||
newDs := s.buildDatasourceMap(req)
|
||||
// 密码为空时保留原有 secureJsonData
|
||||
if req.Password == "" {
|
||||
if sec, exists := ds["secureJsonData"]; exists {
|
||||
newDs["secureJsonData"] = sec
|
||||
}
|
||||
}
|
||||
list[idx] = newDs
|
||||
|
||||
if req.IsDefault {
|
||||
s.clearDefault(cfg)
|
||||
for _, item := range list {
|
||||
lo.ForEach(list, func(item any, _ int) {
|
||||
if ds, ok := item.(map[string]any); ok && ds["name"] == req.Name {
|
||||
ds["isDefault"] = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if oldName != req.Name {
|
||||
@@ -300,15 +305,11 @@ func (s *App) DeleteDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := s.readDatasources()
|
||||
list := s.getDatasourceList(cfg)
|
||||
|
||||
found := false
|
||||
newList := make([]any, 0, len(list))
|
||||
for _, item := range list {
|
||||
if ds, ok := item.(map[string]any); ok && ds["name"] == name {
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
newList = append(newList, item)
|
||||
}
|
||||
newList := lo.Filter(list, func(item any, _ int) bool {
|
||||
ds, ok := item.(map[string]any)
|
||||
return !ok || ds["name"] != name
|
||||
})
|
||||
found := len(newList) < len(list)
|
||||
if !found {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("datasource %s not found", name))
|
||||
return
|
||||
@@ -508,11 +509,11 @@ func (s *App) buildDatasourceMap(req *DataSource) map[string]any {
|
||||
|
||||
// clearDefault 清除所有数据源的默认标记
|
||||
func (s *App) clearDefault(cfg map[string]any) {
|
||||
for _, item := range s.getDatasourceList(cfg) {
|
||||
lo.ForEach(s.getDatasourceList(cfg), func(item any, _ int) {
|
||||
if ds, ok := item.(map[string]any); ok {
|
||||
ds["isDefault"] = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// addDeleteEntry 向 deleteDatasources 添加条目
|
||||
|
||||
@@ -32,6 +32,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("kafka")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("kafka")
|
||||
if err != nil {
|
||||
|
||||
@@ -21,3 +21,7 @@ func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.Databa
|
||||
func (s *App) Route(r chi.Router) {
|
||||
s.mysql.Route(r)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
return s.mysql.Status()
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("memcached")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("memcached")
|
||||
if err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -21,6 +22,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/env", s.UpdateEnv)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("minio")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetEnv(w http.ResponseWriter, r *http.Request) {
|
||||
env, _ := io.Read("/etc/default/minio")
|
||||
service.Success(w, env)
|
||||
|
||||
@@ -44,6 +44,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/admin_password", s.SetAdminPassword)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("mongod")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// Load 获取 MongoDB 运行状态
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, _ := systemctl.Status("mongod")
|
||||
|
||||
+29
-46
@@ -40,15 +40,18 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Post("/clear_log", s.ClearLog)
|
||||
r.Get("/slow_log", s.SlowLog)
|
||||
r.Post("/clear_slow_log", s.ClearSlowLog)
|
||||
r.Get("/root_password", s.GetRootPassword)
|
||||
r.Post("/root_password", s.SetRootPassword)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("mysqld")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(app.Root + "/server/mysql/conf/my.cnf")
|
||||
@@ -194,31 +197,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, load)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (s *App) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
if err := systemctl.LogClear("mysqld"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// SlowLog 获取慢查询日志
|
||||
func (s *App) SlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, fmt.Sprintf("%s/server/mysql/mysql-slow.log", app.Root))
|
||||
}
|
||||
|
||||
// ClearSlowLog 清空慢查询日志
|
||||
func (s *App) ClearSlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf("cat /dev/null > %s/server/mysql/mysql-slow.log", app.Root); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetRootPassword 获取root密码
|
||||
func (s *App) GetRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
rootPassword, err := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
@@ -264,30 +247,6 @@ func (s *App) SetRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) getSock() string {
|
||||
if io.Exists("/tmp/mysql.sock") {
|
||||
return "/tmp/mysql.sock"
|
||||
}
|
||||
if io.Exists(app.Root + "/server/mysql/config/my.cnf") {
|
||||
config, _ := io.Read(app.Root + "/server/mysql/config/my.cnf")
|
||||
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
|
||||
matches := re.FindStringSubmatch(config)
|
||||
if len(matches) > 2 {
|
||||
return matches[2]
|
||||
}
|
||||
}
|
||||
if io.Exists("/etc/my.cnf") {
|
||||
config, _ := io.Read("/etc/my.cnf")
|
||||
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
|
||||
matches := re.FindStringSubmatch(config)
|
||||
if len(matches) > 2 {
|
||||
return matches[2]
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
@@ -386,6 +345,30 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) getSock() string {
|
||||
if io.Exists("/tmp/mysql.sock") {
|
||||
return "/tmp/mysql.sock"
|
||||
}
|
||||
if io.Exists(app.Root + "/server/mysql/config/my.cnf") {
|
||||
config, _ := io.Read(app.Root + "/server/mysql/config/my.cnf")
|
||||
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
|
||||
matches := re.FindStringSubmatch(config)
|
||||
if len(matches) > 2 {
|
||||
return matches[2]
|
||||
}
|
||||
}
|
||||
if io.Exists("/etc/my.cnf") {
|
||||
config, _ := io.Read("/etc/my.cnf")
|
||||
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
|
||||
matches := re.FindStringSubmatch(config)
|
||||
if len(matches) > 2 {
|
||||
return matches[2]
|
||||
}
|
||||
}
|
||||
|
||||
return "/tmp/mysql.sock"
|
||||
}
|
||||
|
||||
// getINIValue 从 INI 格式内容中获取指定键的值
|
||||
func (s *App) getINIValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
|
||||
@@ -50,6 +50,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Delete("/stream/upstreams/{name}", s.DeleteStreamUpstream)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("nginx")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root))
|
||||
if err != nil {
|
||||
@@ -98,7 +103,7 @@ 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() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
@@ -563,10 +564,7 @@ func (s *App) saveStreamUpstreamConfig(filePath string, upstream *StreamUpstream
|
||||
}
|
||||
|
||||
// 服务器列表
|
||||
var addrs []string
|
||||
for addr := range upstream.Servers {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
addrs := lo.Keys(upstream.Servers)
|
||||
sort.Strings(addrs)
|
||||
|
||||
for _, addr := range addrs {
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -20,3 +22,8 @@ func NewApp(t *gotext.Locale) *App {
|
||||
func (s *App) Route(r chi.Router) {
|
||||
s.nginx.Route(r)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("nginx")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("opensearch")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("opensearch")
|
||||
if err != nil {
|
||||
@@ -52,7 +57,7 @@ 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(fmt.Sprintf("http://127.0.0.1:%s/_cluster/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,3 +21,7 @@ func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.Databa
|
||||
func (s *App) Route(r chi.Router) {
|
||||
s.mysql.Route(r)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
return s.mysql.Status()
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -37,6 +38,10 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
return types.AppStatusNA
|
||||
}
|
||||
|
||||
func (s *App) Info(w http.ResponseWriter, r *http.Request) {
|
||||
files, err := os.ReadDir(fmt.Sprintf("%s/server/phpmyadmin", app.Root))
|
||||
if err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -23,6 +24,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/storage_config", s.UpdateStorageConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("podman")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetRegistryConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/containers/registries.conf")
|
||||
if err != nil {
|
||||
|
||||
@@ -4,11 +4,12 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
@@ -41,17 +42,21 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/user_config", s.UpdateUserConfig)
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/log", s.Log)
|
||||
r.Post("/clear_log", s.ClearLog)
|
||||
r.Get("/postgres_password", s.GetPostgresPassword)
|
||||
r.Post("/postgres_password", s.SetPostgresPassword)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("postgresql")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取配置
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root))
|
||||
config, err := io.Read(s.configPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -68,13 +73,14 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root), req.Config, 0644); err != nil {
|
||||
oldPort := s.getPort()
|
||||
if err = io.Write(s.configPath(), req.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))
|
||||
if err = s.applyConfig(req.Config, oldPort); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to apply PostgreSQL config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -132,13 +138,15 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to set PGPASSWORD env: %v", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = os.Unsetenv("PGPASSWORD") }()
|
||||
|
||||
start, err := shell.Execf(`psql -h 127.0.0.1 -U postgres -t -c "select pg_postmaster_start_time();" | head -1 | cut -d'.' -f1`)
|
||||
port := s.getPort()
|
||||
start, err := shell.Execf(`psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_postmaster_start_time();" | head -1 | cut -d'.' -f1`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL start time: %v", err))
|
||||
return
|
||||
}
|
||||
pid, err := shell.Execf(`psql -h 127.0.0.1 -U postgres -t -c "select pg_backend_pid();"`)
|
||||
pid, err := shell.Execf(`psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_backend_pid();"`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL backend pid: %v", err))
|
||||
return
|
||||
@@ -148,22 +156,17 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL process: %v", err))
|
||||
return
|
||||
}
|
||||
connections, err := shell.Execf(`psql -h 127.0.0.1 -U postgres -t -c "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();"`)
|
||||
connections, err := shell.Execf(`psql -h 127.0.0.1 -p %d -U postgres -t -c "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();"`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL connections: %v", err))
|
||||
return
|
||||
}
|
||||
storage, err := shell.Execf(`psql -h 127.0.0.1 -U postgres -t -c "select pg_size_pretty(pg_database_size('postgres'));"`)
|
||||
storage, err := shell.Execf(`psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_size_pretty(pg_database_size('postgres'));"`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL database size: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.Unsetenv("PGPASSWORD"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to unset PGPASSWORD env: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Start Time"), Value: start},
|
||||
{Name: s.t.Get("Process PID"), Value: pid},
|
||||
@@ -175,19 +178,15 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
// Log 获取日志
|
||||
// Log 获取应用日志路径列表
|
||||
func (s *App) Log(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, fmt.Sprintf("%s/server/postgresql/logs/postgresql-%s.log", app.Root, time.Now().Format(time.DateOnly)))
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (s *App) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf("rm -rf %s/server/postgresql/logs/postgresql-*.log", app.Root); err != nil {
|
||||
paths, err := filepath.Glob(fmt.Sprintf("%s/server/postgresql/logs/postgresql-*.log", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
service.Success(w, paths)
|
||||
}
|
||||
|
||||
// GetPostgresPassword 获取 postgres 用户密码
|
||||
@@ -210,10 +209,11 @@ func (s *App) SetPostgresPassword(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
oldPassword, _ := s.settingRepo.Get(biz.SettingKeyPostgresPassword)
|
||||
postgres, err := db.NewPostgres("postgres", oldPassword, "127.0.0.1", 5432)
|
||||
port := s.getPort()
|
||||
postgres, err := db.NewPostgres("postgres", oldPassword, "127.0.0.1", port)
|
||||
if err != nil {
|
||||
// 直接修改密码
|
||||
if _, err = shell.Execf(`su - postgres -c "psql -c \"ALTER USER postgres WITH PASSWORD '%s';\""`, req.Password); err != nil {
|
||||
if _, err = shell.Execf(`su - postgres -c "psql -p %d -c \"ALTER USER postgres WITH PASSWORD '%s';\""`, port, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to set postgres password: %v", err))
|
||||
return
|
||||
}
|
||||
@@ -237,7 +237,7 @@ func (s *App) SetPostgresPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 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))
|
||||
config, err := io.Read(s.configPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -284,7 +284,8 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root)
|
||||
confPath := s.configPath()
|
||||
oldPort := s.getPort()
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -324,14 +325,48 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("postgresql"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload PostgreSQL: %v", err))
|
||||
if err = s.applyConfig(config, oldPort); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to apply PostgreSQL config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root)
|
||||
}
|
||||
|
||||
// getPort 读取 PostgreSQL 端口
|
||||
func (s *App) getPort() uint {
|
||||
config, err := io.Read(s.configPath())
|
||||
if err != nil {
|
||||
return 5432
|
||||
}
|
||||
return s.parsePort(config)
|
||||
}
|
||||
|
||||
// parsePort 从 config 内容解析端口,未配置时返回默认值
|
||||
func (s *App) parsePort(config string) uint {
|
||||
port := cast.ToUint(s.getPGValue(config, "port"))
|
||||
if port == 0 {
|
||||
return 5432
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
// applyConfig 让 PostgreSQL 配置生效
|
||||
func (s *App) applyConfig(newConfig string, oldPort uint) error {
|
||||
newPort := s.parsePort(newConfig)
|
||||
if oldPort == newPort {
|
||||
return systemctl.Reload("postgresql")
|
||||
}
|
||||
if err := systemctl.Restart("postgresql"); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.databaseServerRepo.UpdatePort("local_postgresql", newPort)
|
||||
}
|
||||
|
||||
// getPGValue 从 PostgreSQL 配置内容中获取指定键的值
|
||||
func (s *App) getPGValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
@@ -52,6 +53,12 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/exporters/{slug}/config", s.UpdateExporterConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
prom, _ := systemctl.Status("prometheus")
|
||||
alert, _ := systemctl.Status("alertmanager")
|
||||
return types.AggregateAppStatus(prom, alert)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("prometheus")
|
||||
if err != nil {
|
||||
@@ -66,7 +73,7 @@ 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:9090/api/v1/status/runtimeinfo")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
@@ -420,10 +427,7 @@ func (s *App) getExporterConfigPath(slug string) string {
|
||||
|
||||
// checkExporter 检查 slug 是否有效
|
||||
func (s *App) checkExporter(slug string) bool {
|
||||
for _, e := range s.getExporters() {
|
||||
if e.Slug == slug {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return lo.ContainsBy(s.getExporters(), func(e Exporter) bool {
|
||||
return e.Slug == slug
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -40,6 +42,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("pure-ftpd")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
listRaw, err := shell.Execf("pure-pw list")
|
||||
@@ -50,19 +57,17 @@ func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
listArr := strings.Split(listRaw, "\n")
|
||||
var users []User
|
||||
for _, v := range listArr {
|
||||
userRe := regexp.MustCompile(`(\S+)\s+(\S+)`)
|
||||
users := lo.FilterMap(strings.Split(listRaw, "\n"), func(v string, _ int) (User, bool) {
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
return User{}, false
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`(\S+)\s+(\S+)`).FindStringSubmatch(v)
|
||||
users = append(users, User{
|
||||
match := userRe.FindStringSubmatch(v)
|
||||
return User{
|
||||
Username: match[1],
|
||||
Path: strings.Replace(match[2], "/./", "/", 1),
|
||||
})
|
||||
}
|
||||
}, true
|
||||
})
|
||||
|
||||
paged, total := service.Paginate(r, users)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
@@ -38,6 +39,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("redis")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("redis")
|
||||
if err != nil {
|
||||
@@ -68,15 +74,13 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
infoLines := strings.Split(raw, "\n")
|
||||
dataRaw := make(map[string]string)
|
||||
|
||||
for _, item := range infoLines {
|
||||
dataRaw := lo.SliceToMap(strings.Split(raw, "\n"), func(item string) (string, string) {
|
||||
parts := strings.Split(item, ":")
|
||||
if len(parts) == 2 {
|
||||
dataRaw[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
if len(parts) != 2 {
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
||||
})
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("TCP Port"), Value: dataRaw["tcp_port"]},
|
||||
|
||||
@@ -32,6 +32,12 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
namesrv, _ := systemctl.Status("rocketmq-namesrv")
|
||||
broker, _ := systemctl.Status("rocketmq-broker")
|
||||
return types.AggregateAppStatus(namesrv, broker)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
namesrvStatus, _ := systemctl.Status("rocketmq-namesrv")
|
||||
brokerStatus, _ := systemctl.Status("rocketmq-broker")
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -36,6 +37,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("rsyncd")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
|
||||
+11
-13
@@ -10,11 +10,13 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -33,6 +35,10 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Delete("/mounts", s.Delete)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
return types.AppStatusNA
|
||||
}
|
||||
|
||||
// List 所有 S3fs 挂载
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.mounts()
|
||||
@@ -81,11 +87,9 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range list {
|
||||
if item.Path == req.Path {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("mount path already exists"))
|
||||
return
|
||||
}
|
||||
if lo.ContainsBy(list, func(item Mount) bool { return item.Path == req.Path }) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("mount path already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
id := time.Now().UnixMicro()
|
||||
@@ -128,14 +132,8 @@ func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var mount Mount
|
||||
for _, item := range list {
|
||||
if item.ID == req.ID {
|
||||
mount = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if mount.ID == 0 {
|
||||
mount, ok := lo.Find(list, func(item Mount) bool { return item.ID == req.ID })
|
||||
if !ok {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("mount not found"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/os"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -37,7 +38,6 @@ func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/service", s.Service)
|
||||
r.Post("/clear_log", s.ClearLog)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/processes", s.Processes)
|
||||
@@ -45,7 +45,6 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/processes/{process}/stop", s.StopProcess)
|
||||
r.Post("/processes/{process}/restart", s.RestartProcess)
|
||||
r.Get("/processes/{process}/log", s.ProcessLog)
|
||||
r.Post("/processes/{process}/clear_log", s.ClearProcessLog)
|
||||
r.Get("/processes/{process}", s.ProcessConfig)
|
||||
r.Post("/processes/{process}", s.UpdateProcessConfig)
|
||||
r.Delete("/processes/{process}", s.DeleteProcess)
|
||||
@@ -57,14 +56,9 @@ func (s *App) Service(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, s.name)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (s *App) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf(`cat /dev/null > /var/log/supervisor/supervisord.log`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status(s.name)
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
@@ -114,31 +108,35 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Processes 进程列表
|
||||
func (s *App) Processes(w http.ResponseWriter, r *http.Request) {
|
||||
out, err := shell.Execf(`supervisorctl status | awk '{print $1}'`)
|
||||
if err != nil {
|
||||
out, err := shell.Execf(`supervisorctl status`)
|
||||
if err != nil && out == "" {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var processes []Process
|
||||
for line := range strings.SplitSeq(out, "\n") {
|
||||
if len(line) == 0 {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
var p Process
|
||||
p.Name = line
|
||||
if status, err := shell.Execf(`supervisorctl status '%s' | awk '{print $2}'`, line); err == nil {
|
||||
p.Status = status
|
||||
p := Process{
|
||||
Name: fields[0],
|
||||
Status: fields[1],
|
||||
Pid: "-",
|
||||
Uptime: "-",
|
||||
}
|
||||
// RUNNING 行格式:name RUNNING pid 1234, uptime 1:23:45 (超过 1 天为 "1 day, 1:23:45")
|
||||
if p.Status == "RUNNING" {
|
||||
pid, _ := shell.Execf(`supervisorctl status '%s' | awk '{print $4}'`, line)
|
||||
p.Pid = strings.ReplaceAll(pid, ",", "")
|
||||
uptime, _ := shell.Execf(`supervisorctl status '%s' | awk '{print $6}'`, line)
|
||||
p.Uptime = uptime
|
||||
} else {
|
||||
p.Pid = "-"
|
||||
p.Uptime = "-"
|
||||
if _, rest, ok := strings.Cut(line, "pid "); ok {
|
||||
if pid, _, ok := strings.Cut(rest, ","); ok {
|
||||
p.Pid = pid
|
||||
}
|
||||
}
|
||||
if _, uptime, ok := strings.Cut(line, "uptime "); ok {
|
||||
p.Uptime = uptime
|
||||
}
|
||||
}
|
||||
processes = append(processes, p)
|
||||
}
|
||||
@@ -159,7 +157,7 @@ func (s *App) StartProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl start %s`, req.Process); err != nil {
|
||||
if out, err := shell.Execf(`supervisorctl start '%s'`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||||
return
|
||||
}
|
||||
@@ -175,7 +173,7 @@ func (s *App) StopProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl stop %s`, req.Process); err != nil {
|
||||
if out, err := shell.Execf(`supervisorctl stop '%s'`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||||
return
|
||||
}
|
||||
@@ -191,7 +189,7 @@ func (s *App) RestartProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl restart %s`, req.Process); err != nil {
|
||||
if out, err := shell.Execf(`supervisorctl restart '%s'`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||||
return
|
||||
}
|
||||
@@ -207,11 +205,12 @@ func (s *App) ProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
name := programName(req.Process)
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -222,34 +221,6 @@ func (s *App) ProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, logPath)
|
||||
}
|
||||
|
||||
// ClearProcessLog 清空进程日志
|
||||
func (s *App) ClearProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(`cat /dev/null > '%s'`, logPath); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ProcessConfig 获取进程配置
|
||||
func (s *App) ProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
@@ -258,11 +229,12 @@ func (s *App) ProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
name := programName(req.Process)
|
||||
var config string
|
||||
if os.IsRHEL() {
|
||||
config, err = io.Read(`/etc/supervisord.d/` + req.Process + `.conf`)
|
||||
config, err = io.Read(`/etc/supervisord.d/` + name + `.conf`)
|
||||
} else {
|
||||
config, err = io.Read(`/etc/supervisor/conf.d/` + req.Process + `.conf`)
|
||||
config, err = io.Read(`/etc/supervisor/conf.d/` + name + `.conf`)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -281,10 +253,11 @@ func (s *App) UpdateProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
name := programName(req.Process)
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.d/`+req.Process+`.conf`, req.Config, 0644)
|
||||
err = io.Write(`/etc/supervisord.d/`+name+`.conf`, req.Config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+req.Process+`.conf`, req.Config, 0644)
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+name+`.conf`, req.Config, 0644)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -294,7 +267,7 @@ func (s *App) UpdateProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl restart '%s'`, req.Process)
|
||||
_, _ = shell.Execf(`supervisorctl restart '%s:'`, name)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -307,9 +280,14 @@ func (s *App) CreateProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
processName := `%(program_name)s`
|
||||
if req.Num > 1 {
|
||||
processName = `%(program_name)s_%(process_num)02d`
|
||||
}
|
||||
|
||||
config := `[program:` + req.Name + `]
|
||||
command=` + req.Command + `
|
||||
process_name=%(program_name)s
|
||||
process_name=` + processName + `
|
||||
directory=` + req.Path + `
|
||||
autostart=true
|
||||
autorestart=true
|
||||
@@ -320,20 +298,26 @@ stdout_logfile=/var/log/supervisor/` + req.Name + `.log
|
||||
stdout_logfile_maxbytes=2MB
|
||||
`
|
||||
|
||||
var confPath string
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.d/`+req.Name+`.conf`, config, 0644)
|
||||
confPath = `/etc/supervisord.d/` + req.Name + `.conf`
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+req.Name+`.conf`, config, 0644)
|
||||
confPath = `/etc/supervisor/conf.d/` + req.Name + `.conf`
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if io.Exists(confPath) {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("process %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl start '%s'`, req.Name)
|
||||
_, _ = shell.Execf(`supervisorctl start '%s:'`, req.Name)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -346,29 +330,30 @@ func (s *App) DeleteProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl stop '%s'`, req.Process); err != nil {
|
||||
name := programName(req.Process)
|
||||
if out, err := shell.Execf(`supervisorctl stop '%s:'`, name); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||||
return
|
||||
}
|
||||
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
if err = io.Remove(`/etc/supervisord.d/` + req.Process + `.conf`); err != nil {
|
||||
if err = io.Remove(`/etc/supervisord.d/` + name + `.conf`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
if err = io.Remove(`/etc/supervisor/conf.d/` + req.Process + `.conf`); err != nil {
|
||||
if err = io.Remove(`/etc/supervisor/conf.d/` + name + `.conf`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -383,3 +368,8 @@ func (s *App) DeleteProcess(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func programName(process string) string {
|
||||
name, _, _ := strings.Cut(process, ":")
|
||||
return name
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
@@ -38,6 +39,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("valkey")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("valkey")
|
||||
if err != nil {
|
||||
@@ -68,15 +74,13 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
infoLines := strings.Split(raw, "\n")
|
||||
dataRaw := make(map[string]string)
|
||||
|
||||
for _, item := range infoLines {
|
||||
dataRaw := lo.SliceToMap(strings.Split(raw, "\n"), func(item string) (string, string) {
|
||||
parts := strings.Split(item, ":")
|
||||
if len(parts) == 2 {
|
||||
dataRaw[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
if len(parts) != 2 {
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
||||
})
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("TCP Port"), Value: dataRaw["tcp_port"]},
|
||||
|
||||
@@ -9,12 +9,14 @@ import (
|
||||
type BackupType string
|
||||
|
||||
const (
|
||||
BackupTypePath BackupType = "path"
|
||||
BackupTypeWebsite BackupType = "website"
|
||||
BackupTypeMySQL BackupType = "mysql"
|
||||
BackupTypePostgres BackupType = "postgresql"
|
||||
BackupTypeRedis BackupType = "redis"
|
||||
BackupTypePanel BackupType = "panel"
|
||||
BackupTypePath BackupType = "path"
|
||||
BackupTypeWebsite BackupType = "website"
|
||||
BackupTypeMySQL BackupType = "mysql"
|
||||
BackupTypePostgres BackupType = "postgresql"
|
||||
BackupTypeClickHouse BackupType = "clickhouse"
|
||||
BackupTypeRedis BackupType = "redis"
|
||||
BackupTypeValkey BackupType = "valkey"
|
||||
BackupTypePanel BackupType = "panel"
|
||||
)
|
||||
|
||||
type BackupRepo interface {
|
||||
|
||||
@@ -17,6 +17,6 @@ type ContainerRepo interface {
|
||||
Unpause(id string) error
|
||||
Kill(id string) error
|
||||
Rename(id string, newName string) error
|
||||
Logs(id string) (string, error)
|
||||
Logs(id string, tail int) (string, error)
|
||||
Prune() error
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ type DatabaseServerRepo interface {
|
||||
Update(req *request.DatabaseServerUpdate) error
|
||||
UpdateRemark(req *request.DatabaseServerUpdateRemark) error
|
||||
UpdatePassword(name string, password string) error
|
||||
UpdatePort(name string, port uint) error
|
||||
Delete(id uint) error
|
||||
ClearUsers(id uint) error
|
||||
Sync(id uint) error
|
||||
|
||||
@@ -17,6 +17,7 @@ const (
|
||||
SettingKeyMonitorDays SettingKey = "monitor_days"
|
||||
SettingKeyMonitorInterval SettingKey = "monitor_interval"
|
||||
SettingKeyBackupPath SettingKey = "backup_path"
|
||||
SettingKeyBackupFormat SettingKey = "backup_format" // tar.xz / tar.gz / tar.zst / zip / 7z
|
||||
SettingKeyWebsitePath SettingKey = "website_path"
|
||||
SettingKeyProjectPath SettingKey = "project_path"
|
||||
SettingKeyContainerSock SettingKey = "container_sock"
|
||||
|
||||
@@ -45,7 +45,6 @@ type WebsiteRepo interface {
|
||||
Create(ctx context.Context, req *request.WebsiteCreate) (*Website, error)
|
||||
Update(ctx context.Context, req *request.WebsiteUpdate) error
|
||||
Delete(ctx context.Context, req *request.WebsiteDelete) error
|
||||
ClearLog(id uint) error
|
||||
UpdateRemark(id uint, remark string) error
|
||||
ResetConfig(id uint) error
|
||||
UpdateStatus(id uint, status bool) error
|
||||
|
||||
@@ -3,22 +3,16 @@ package bootstrap
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/libtnb/cron"
|
||||
"github.com/libtnb/cron/wrap"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/job"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
pkgcron "github.com/acepanel/panel/v3/pkg/cron"
|
||||
)
|
||||
|
||||
func NewCron(conf *config.Config, log *slog.Logger, jobs *job.Jobs) (*cron.Cron, error) {
|
||||
logger := pkgcron.NewLogger(log, conf.App.Debug)
|
||||
|
||||
func NewCron(log *slog.Logger, jobs *job.Jobs) (*cron.Cron, error) {
|
||||
c := cron.New(
|
||||
cron.WithParser(cron.NewParser(
|
||||
cron.SecondOptional|cron.Minute|cron.Hour|cron.Dom|cron.Month|cron.Dow|cron.Descriptor,
|
||||
)),
|
||||
cron.WithLogger(logger),
|
||||
cron.WithChain(cron.Recover(logger), cron.SkipIfStillRunning(logger)),
|
||||
cron.WithLogger(log),
|
||||
cron.WithChain(wrap.Recover(), wrap.SkipIfRunning()),
|
||||
)
|
||||
if err := jobs.Register(c); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
|
||||
@@ -58,15 +59,9 @@ func (r *appRepo) Categories() []types.LV {
|
||||
return a.Order - b.Order
|
||||
})
|
||||
|
||||
result := make([]types.LV, 0)
|
||||
for item := range slices.Values(categories) {
|
||||
result = append(result, types.LV{
|
||||
Label: item.Name,
|
||||
Value: item.Slug,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
return lo.Map(categories, func(item *api.Category, _ int) types.LV {
|
||||
return types.LV{Label: item.Name, Value: item.Slug}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *appRepo) All() api.Apps {
|
||||
|
||||
+438
-20
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/storage"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
@@ -126,6 +128,12 @@ func (r *backupRepo) Create(ctx context.Context, typ biz.BackupType, target stri
|
||||
err = r.createMySQL(name, client, target)
|
||||
case biz.BackupTypePostgres:
|
||||
err = r.createPostgres(name, client, target)
|
||||
case biz.BackupTypeClickHouse:
|
||||
err = r.createClickHouse(name, client, target)
|
||||
case biz.BackupTypeRedis:
|
||||
err = r.createRedisLike(name, client, "redis")
|
||||
case biz.BackupTypeValkey:
|
||||
err = r.createRedisLike(name, client, "valkey")
|
||||
case biz.BackupTypePath:
|
||||
err = r.createPath(name, client, target)
|
||||
default:
|
||||
@@ -170,7 +178,7 @@ func (r *backupRepo) Create(ctx context.Context, typ biz.BackupType, target stri
|
||||
func (r *backupRepo) CreatePanel() error {
|
||||
start := time.Now()
|
||||
|
||||
backup := filepath.Join(r.GetDefaultPath(biz.BackupTypePanel), fmt.Sprintf("panel_%s.zip", time.Now().Format("20060102150405")))
|
||||
backup := filepath.Join(r.GetDefaultPath(biz.BackupTypePanel), fmt.Sprintf("panel_%s%s", time.Now().Format("20060102150405"), r.backupExt()))
|
||||
|
||||
temp, err := os.MkdirTemp("", "ace-backup-*")
|
||||
if err != nil {
|
||||
@@ -236,6 +244,12 @@ func (r *backupRepo) Restore(ctx context.Context, typ biz.BackupType, backup, ta
|
||||
err = r.restoreMySQL(backup, target)
|
||||
case biz.BackupTypePostgres:
|
||||
err = r.restorePostgres(backup, target)
|
||||
case biz.BackupTypeClickHouse:
|
||||
err = r.restoreClickHouse(backup, target)
|
||||
case biz.BackupTypeRedis:
|
||||
err = r.restoreRedisLike(backup, "redis")
|
||||
case biz.BackupTypeValkey:
|
||||
err = r.restoreRedisLike(backup, "valkey")
|
||||
default:
|
||||
return errors.New(r.t.Get("unknown backup type"))
|
||||
}
|
||||
@@ -259,9 +273,11 @@ func (r *backupRepo) Restore(ctx context.Context, typ biz.BackupType, backup, ta
|
||||
func (r *backupRepo) GetDefaultPath(typ biz.BackupType) string {
|
||||
backupPath, err := r.setting.Get(biz.SettingKeyBackupPath)
|
||||
if err != nil {
|
||||
return filepath.Join(app.Root, "backup", string(typ))
|
||||
backupPath = filepath.Join(app.Root, "backup")
|
||||
}
|
||||
return filepath.Join(backupPath, string(typ))
|
||||
path := filepath.Join(backupPath, string(typ))
|
||||
_ = os.MkdirAll(path, 0755)
|
||||
return path
|
||||
}
|
||||
|
||||
// CutoffLog 切割日志
|
||||
@@ -273,7 +289,7 @@ func (r *backupRepo) CutoffLog(path, target string) (string, error) {
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(filepath.Base(target), filepath.Ext(target))
|
||||
to := filepath.Join(path, fmt.Sprintf("%s_%s.zip", name, time.Now().Format("20060102150405")))
|
||||
to := filepath.Join(path, fmt.Sprintf("%s_%s%s", name, time.Now().Format("20060102150405"), r.backupExt()))
|
||||
if err := io.Compress(filepath.Dir(target), []string{filepath.Base(target)}, to); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -327,16 +343,16 @@ func (r *backupRepo) ClearExpired(path, prefix string, save uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var filtered []os.FileInfo
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file.Name(), prefix) && strings.HasSuffix(file.Name(), ".zip") {
|
||||
info, err := os.Stat(filepath.Join(path, file.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, info)
|
||||
filtered := lo.FilterMap(files, func(file os.DirEntry, _ int) (os.FileInfo, bool) {
|
||||
if !strings.HasPrefix(file.Name(), prefix) || !r.isBackupArchive(file.Name()) {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
info, err := os.Stat(filepath.Join(path, file.Name()))
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return info, true
|
||||
})
|
||||
|
||||
// 排序所有备份文件,从新到旧
|
||||
slices.SortFunc(filtered, func(a, b os.FileInfo) int {
|
||||
@@ -393,7 +409,7 @@ func (r *backupRepo) ClearStorageExpired(storage uint, typ biz.BackupType, prefi
|
||||
}
|
||||
var filtered []fileInfo
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file, prefix) && strings.HasSuffix(file, ".zip") {
|
||||
if strings.HasPrefix(file, prefix) && r.isBackupArchive(file) {
|
||||
lastModified, modErr := client.LastModified(filepath.Join(string(typ), file))
|
||||
if modErr != nil {
|
||||
continue
|
||||
@@ -487,7 +503,7 @@ func (r *backupRepo) createWebsite(name string, storage storage.Storage, target
|
||||
}
|
||||
|
||||
// 压缩网站
|
||||
name = name + ".zip"
|
||||
name = name + r.backupExt()
|
||||
if err = io.Compress(website.Path, nil, filepath.Join(tmpDir, name)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -545,12 +561,12 @@ func (r *backupRepo) createMySQL(name string, storage storage.Storage, target st
|
||||
_ = os.Unsetenv("MYSQL_PWD")
|
||||
|
||||
// 压缩备份文件
|
||||
if err = io.Compress(tmpDir, []string{name}, filepath.Join(tmpDir, name+".zip")); err != nil {
|
||||
if err = io.Compress(tmpDir, []string{name}, filepath.Join(tmpDir, name+r.backupExt())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 上传备份文件到存储器
|
||||
name = name + ".zip"
|
||||
name = name + r.backupExt()
|
||||
file, err := os.Open(filepath.Join(tmpDir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -603,12 +619,12 @@ func (r *backupRepo) createPostgres(name string, storage storage.Storage, target
|
||||
_ = os.Unsetenv("PGPASSWORD")
|
||||
|
||||
// 压缩备份文件
|
||||
if err = io.Compress(tmpDir, []string{name}, filepath.Join(tmpDir, name+".zip")); err != nil {
|
||||
if err = io.Compress(tmpDir, []string{name}, filepath.Join(tmpDir, name+r.backupExt())); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 上传备份文件到存储器
|
||||
name = name + ".zip"
|
||||
name = name + r.backupExt()
|
||||
file, err := os.Open(filepath.Join(tmpDir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -626,6 +642,119 @@ func (r *backupRepo) createPostgres(name string, storage storage.Storage, target
|
||||
return nil
|
||||
}
|
||||
|
||||
// createClickHouse 创建 ClickHouse 备份
|
||||
func (r *backupRepo) createClickHouse(name string, storage storage.Storage, target string) error {
|
||||
password, err := r.setting.Get(biz.SettingKeyClickHouseDefaultPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// clickhouse-client 走 native 9000 端口,本地 default 用户
|
||||
conn := fmt.Sprintf("--host 127.0.0.1 --port 9000 --user default --password '%s'", password)
|
||||
|
||||
// 校验数据库是否存在
|
||||
exist, err := shell.Execf("clickhouse-client %s --query \"SELECT count() FROM system.databases WHERE name = '%s'\"", conn, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(exist) == "0" {
|
||||
return errors.New(r.t.Get("database does not exist: %s", target))
|
||||
}
|
||||
|
||||
// 创建用于压缩的临时目录
|
||||
tmpDir, err := os.MkdirTemp("", "ace-backup-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(path string) { _ = os.RemoveAll(path) }(tmpDir)
|
||||
|
||||
if app.IsCli {
|
||||
fmt.Println(r.t.Get("|-Temporary directory: %s", tmpDir))
|
||||
}
|
||||
|
||||
// 数据表(含数据)在前,视图(仅结构)在后
|
||||
dataTables, err := r.clickHouseTables(conn, target, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
views, err := r.clickHouseTables(conn, target, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 导出结构到 schema.sql,去掉库名限定(恢复时由 --database 指定目标库)
|
||||
objects := make([]string, 0, len(dataTables)+len(views))
|
||||
objects = append(objects, dataTables...)
|
||||
objects = append(objects, views...)
|
||||
var schema strings.Builder
|
||||
for _, tbl := range objects {
|
||||
create, err := shell.Execf("clickhouse-client %s --query \"SELECT create_table_query FROM system.tables WHERE database = '%s' AND name = '%s' FORMAT TabSeparatedRaw\"", conn, target, tbl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt := strings.TrimSpace(create)
|
||||
stmt = strings.ReplaceAll(stmt, fmt.Sprintf("`%s`.", target), "")
|
||||
stmt = strings.ReplaceAll(stmt, fmt.Sprintf("%s.", target), "")
|
||||
schema.WriteString(stmt)
|
||||
schema.WriteString(";\n")
|
||||
}
|
||||
if err = io.Write(filepath.Join(tmpDir, "schema.sql"), schema.String(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 导出数据(仅数据表,Native 格式)
|
||||
files := []string{"schema.sql"}
|
||||
for _, tbl := range dataTables {
|
||||
dataFile := tbl + ".native"
|
||||
if _, err = shell.Execf("clickhouse-client %s --query 'SELECT * FROM `%s`.`%s` FORMAT Native' > '%s'", conn, target, tbl, filepath.Join(tmpDir, dataFile)); err != nil {
|
||||
return err
|
||||
}
|
||||
files = append(files, dataFile)
|
||||
}
|
||||
|
||||
// 压缩备份文件
|
||||
name = name + r.backupExt()
|
||||
if err = io.Compress(tmpDir, files, filepath.Join(tmpDir, name)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 上传备份文件到存储器
|
||||
file, err := os.Open(filepath.Join(tmpDir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
if err = storage.Put(filepath.Join(string(biz.BackupTypeClickHouse), name), file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
fmt.Println(r.t.Get("|-Backup file: %s", name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clickHouseTables 列出库中对象名,onlyView 为 true 时仅返回视图,否则返回非视图数据表
|
||||
func (r *backupRepo) clickHouseTables(conn, database string, onlyView bool) ([]string, error) {
|
||||
op := "NOT LIKE"
|
||||
if onlyView {
|
||||
op = "LIKE"
|
||||
}
|
||||
out, err := shell.Execf("clickhouse-client %s --query \"SELECT name FROM system.tables WHERE database = '%s' AND NOT is_temporary AND engine %s '%%View%%' ORDER BY name FORMAT TabSeparated\"", conn, database, op)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tables []string
|
||||
for line := range strings.SplitSeq(out, "\n") {
|
||||
if l := strings.TrimSpace(line); l != "" {
|
||||
tables = append(tables, l)
|
||||
}
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
// createPath 创建目录备份
|
||||
func (r *backupRepo) createPath(name string, storage storage.Storage, target string) error {
|
||||
if !io.Exists(target) {
|
||||
@@ -647,7 +776,7 @@ func (r *backupRepo) createPath(name string, storage storage.Storage, target str
|
||||
}
|
||||
|
||||
// 压缩目录
|
||||
name = name + ".zip"
|
||||
name = name + r.backupExt()
|
||||
if err = io.Compress(target, nil, filepath.Join(tmpDir, name)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -765,6 +894,276 @@ func (r *backupRepo) restorePostgres(backup, target string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreClickHouse 恢复 ClickHouse 备份
|
||||
func (r *backupRepo) restoreClickHouse(backup, target string) error {
|
||||
password, err := r.setting.Get(biz.SettingKeyClickHouseDefaultPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn := fmt.Sprintf("--host 127.0.0.1 --port 9000 --user default --password '%s'", password)
|
||||
|
||||
// 校验目标数据库是否存在
|
||||
exist, err := shell.Execf("clickhouse-client %s --query \"SELECT count() FROM system.databases WHERE name = '%s'\"", conn, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(exist) == "0" {
|
||||
return errors.New(r.t.Get("database does not exist: %s", target))
|
||||
}
|
||||
|
||||
// 纯 SQL 文件直接执行
|
||||
if strings.HasSuffix(backup, ".sql") {
|
||||
_, err = shell.Execf("clickhouse-client %s --database '%s' --multiquery < '%s'", conn, target, backup)
|
||||
return err
|
||||
}
|
||||
|
||||
// 解压到临时目录
|
||||
tmpDir, err := os.MkdirTemp("", "acepanel-ch-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(path string) { _ = os.RemoveAll(path) }(tmpDir)
|
||||
|
||||
if err = io.UnCompress(backup, tmpDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 先恢复结构(视图已排在末尾,源表先建好,规避物化视图依赖顺序问题)
|
||||
schemaPath := filepath.Join(tmpDir, "schema.sql")
|
||||
if io.Exists(schemaPath) {
|
||||
if _, err = shell.Execf("clickhouse-client %s --database '%s' --multiquery < '%s'", conn, target, schemaPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 再导入数据
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".native") {
|
||||
continue
|
||||
}
|
||||
tbl := strings.TrimSuffix(entry.Name(), ".native")
|
||||
if _, err = shell.Execf("clickhouse-client %s --database '%s' --query 'INSERT INTO `%s` FORMAT Native' < '%s'", conn, target, tbl, filepath.Join(tmpDir, entry.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// redisLikeConf redis/valkey 的连接与持久化参数
|
||||
type redisLikeConf struct {
|
||||
kind string // redis / valkey
|
||||
cli string // redis-cli / valkey-cli
|
||||
authEnv string // REDISCLI_AUTH / VALKEYCLI_AUTH
|
||||
dataDir string // {app.Root}/server/{kind}
|
||||
confPath string
|
||||
port string
|
||||
password string
|
||||
appendonly bool
|
||||
}
|
||||
|
||||
// loadRedisLikeConf 从 {kind}.conf 读取连接与持久化参数
|
||||
func (r *backupRepo) loadRedisLikeConf(kind string) (*redisLikeConf, error) {
|
||||
dataDir := fmt.Sprintf("%s/server/%s", app.Root, kind)
|
||||
confPath := fmt.Sprintf("%s/%s.conf", dataDir, kind)
|
||||
content, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
port := redisLikeValue(content, "port")
|
||||
if port == "" {
|
||||
port = "6379"
|
||||
}
|
||||
authEnv := "REDISCLI_AUTH"
|
||||
if kind == "valkey" {
|
||||
authEnv = "VALKEYCLI_AUTH"
|
||||
}
|
||||
|
||||
return &redisLikeConf{
|
||||
kind: kind,
|
||||
cli: kind + "-cli",
|
||||
authEnv: authEnv,
|
||||
dataDir: dataDir,
|
||||
confPath: confPath,
|
||||
port: port,
|
||||
password: redisLikeValue(content, "requirepass"),
|
||||
appendonly: redisLikeValue(content, "appendonly") == "yes",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// redisLikeValue 从 redis/valkey 配置内容中读取指定键的有效值(忽略注释行)
|
||||
func redisLikeValue(content, key string) string {
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(trimmed)
|
||||
if len(fields) >= 2 && fields[0] == key {
|
||||
return strings.Join(fields[1:], " ")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// createRedisLike 创建 Redis/Valkey 整实例备份({kind}-cli --rdb 导出快照)
|
||||
// redis 与 valkey 共用本实现,kind 为 "redis" 或 "valkey"
|
||||
func (r *backupRepo) createRedisLike(name string, storage storage.Storage, kind string) error {
|
||||
conf, err := r.loadRedisLikeConf(kind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 用环境变量传密码,避免密码出现在命令行/进程列表
|
||||
if conf.password != "" {
|
||||
_ = os.Setenv(conf.authEnv, conf.password)
|
||||
defer func() { _ = os.Unsetenv(conf.authEnv) }()
|
||||
}
|
||||
|
||||
// 创建用于压缩的临时目录
|
||||
tmpDir, err := os.MkdirTemp("", "ace-backup-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(path string) { _ = os.RemoveAll(path) }(tmpDir)
|
||||
|
||||
if app.IsCli {
|
||||
fmt.Println(r.t.Get("|-Temporary directory: %s", tmpDir))
|
||||
}
|
||||
|
||||
// 通过复制协议拉取整实例 RDB 快照到本地文件
|
||||
rdb := filepath.Join(tmpDir, "dump.rdb")
|
||||
if _, err = shell.Execf("%s -h 127.0.0.1 -p %s --rdb '%s'", conf.cli, conf.port, rdb); err != nil {
|
||||
return err
|
||||
}
|
||||
if !io.Exists(rdb) {
|
||||
return errors.New(r.t.Get("failed to export RDB snapshot"))
|
||||
}
|
||||
|
||||
// 压缩备份文件
|
||||
name = name + r.backupExt()
|
||||
if err = io.Compress(tmpDir, []string{"dump.rdb"}, filepath.Join(tmpDir, name)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 上传备份文件到存储器
|
||||
file, err := os.Open(filepath.Join(tmpDir, name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(file *os.File) { _ = file.Close() }(file)
|
||||
|
||||
if err = storage.Put(filepath.Join(kind, name), file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
fmt.Println(r.t.Get("|-Backup file: %s", name))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreRedisLike 恢复 Redis/Valkey 整实例备份
|
||||
// 停服务 → 替换 dump.rdb → 启动;妥善处理 AOF 优先级陷阱(appendonly=yes 时 AOF 会盖过 RDB)
|
||||
func (r *backupRepo) restoreRedisLike(backup, kind string) error {
|
||||
conf, err := r.loadRedisLikeConf(kind)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 准备 dump.rdb:裸 .rdb 直接用,否则解压取包内的 dump.rdb
|
||||
rdb := backup
|
||||
if !strings.HasSuffix(backup, ".rdb") {
|
||||
tmpDir, err := os.MkdirTemp("", "acepanel-rdb-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(path string) { _ = os.RemoveAll(path) }(tmpDir)
|
||||
if err = io.UnCompress(backup, tmpDir); err != nil {
|
||||
return err
|
||||
}
|
||||
rdb = filepath.Join(tmpDir, "dump.rdb")
|
||||
if !io.Exists(rdb) {
|
||||
return errors.New(r.t.Get("dump.rdb not found in backup file"))
|
||||
}
|
||||
}
|
||||
|
||||
// 停止服务
|
||||
if err = systemctl.Stop(conf.kind); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 清理旧 AOF(多部件目录与旧式单文件),避免 AOF 优先于 RDB 被加载
|
||||
_ = io.Remove(filepath.Join(conf.dataDir, "appendonlydir"))
|
||||
_ = io.Remove(filepath.Join(conf.dataDir, "appendonly.aof"))
|
||||
|
||||
// 覆盖 dump.rdb
|
||||
target := filepath.Join(conf.dataDir, "dump.rdb")
|
||||
if err = io.Cp(rdb, target); err != nil {
|
||||
_ = systemctl.Start(conf.kind) // 尽力恢复服务
|
||||
return err
|
||||
}
|
||||
_ = io.Chown(target, kind, kind)
|
||||
_ = io.Chmod(target, 0640)
|
||||
|
||||
// 若原本开启 AOF,必须先以 appendonly no 启动加载 RDB,否则会建空 AOF 以空库覆盖
|
||||
if conf.appendonly {
|
||||
if err = r.disableAppendonly(conf.confPath); err != nil {
|
||||
_ = systemctl.Start(conf.kind)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 启动服务(Type=notify,返回即已加载 RDB)
|
||||
if err = systemctl.Start(conf.kind); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 原本开启 AOF 的,在线转回并持久化配置
|
||||
if conf.appendonly {
|
||||
if conf.password != "" {
|
||||
_ = os.Setenv(conf.authEnv, conf.password)
|
||||
defer func() { _ = os.Unsetenv(conf.authEnv) }()
|
||||
}
|
||||
_, _ = shell.Execf("%s -h 127.0.0.1 -p %s config set appendonly yes", conf.cli, conf.port)
|
||||
_, _ = shell.Execf("%s -h 127.0.0.1 -p %s config rewrite", conf.cli, conf.port)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// disableAppendonly 将 redis/valkey 配置中的 appendonly 临时改为 no
|
||||
func (r *backupRepo) disableAppendonly(confPath string) error {
|
||||
content, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
if fields := strings.Fields(trimmed); len(fields) >= 1 && fields[0] == "appendonly" {
|
||||
lines[i] = "appendonly no"
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
lines = append(lines, "appendonly no")
|
||||
}
|
||||
|
||||
return io.Write(confPath, strings.Join(lines, "\n"), 0644)
|
||||
}
|
||||
|
||||
// autoUnCompressSQL 自动处理压缩文件
|
||||
func (r *backupRepo) autoUnCompressSQL(backup string) (string, error) {
|
||||
temp, err := os.MkdirTemp("", "acepanel-sql-*")
|
||||
@@ -875,6 +1274,9 @@ func (r *backupRepo) FixPanel() error {
|
||||
}
|
||||
var list []os.FileInfo
|
||||
for _, file := range files {
|
||||
if !r.isBackupArchive(file.Name()) {
|
||||
continue
|
||||
}
|
||||
info, infoErr := file.Info()
|
||||
if infoErr != nil {
|
||||
continue
|
||||
@@ -1103,3 +1505,19 @@ func (r *backupRepo) UpdatePanel(version, url, checksum string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// backupExt 根据全局设置返回备份文件扩展名
|
||||
func (r *backupRepo) backupExt() string {
|
||||
format, _ := r.setting.Get(biz.SettingKeyBackupFormat, "tar.xz")
|
||||
return "." + format
|
||||
}
|
||||
|
||||
// isBackupArchive 判断文件名是否是已知的备份压缩包后缀
|
||||
func (r *backupRepo) isBackupArchive(name string) bool {
|
||||
for _, ext := range []string{".tar.xz", ".tar.gz", ".tar.zst", ".zip", ".7z"} {
|
||||
if strings.HasSuffix(name, ext) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
mholtacme "github.com/mholt/acmez/v3/acme"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -49,8 +51,7 @@ func (r *certRepo) List(page, limit uint) ([]*types.CertList, int64, error) {
|
||||
var total int64
|
||||
err := r.db.Model(&biz.Cert{}).Preload("Website").Preload("Account").Preload("DNS").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certs).Error
|
||||
|
||||
list := make([]*types.CertList, 0)
|
||||
for cert := range slices.Values(certs) {
|
||||
list := lo.Map(certs, func(cert *biz.Cert, _ int) *types.CertList {
|
||||
item := &types.CertList{
|
||||
ID: cert.ID,
|
||||
AccountID: cert.AccountID,
|
||||
@@ -74,13 +75,12 @@ func (r *certRepo) List(page, limit uint) ([]*types.CertList, int64, error) {
|
||||
item.Issuer = decode.Issuer.CommonName
|
||||
item.OCSPServer = decode.OCSPServer
|
||||
// 合并 DNSNames 和 IPAddresses
|
||||
item.DNSNames = decode.DNSNames
|
||||
for _, ip := range decode.IPAddresses {
|
||||
item.DNSNames = append(item.DNSNames, ip.String())
|
||||
}
|
||||
item.DNSNames = append(decode.DNSNames, lo.Map(decode.IPAddresses, func(ip net.IP, _ int) string {
|
||||
return ip.String()
|
||||
})...)
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ func (r certAccountRepo) getGoogleEAB() (*acme.EAB, error) {
|
||||
client.SetRetryCount(3)
|
||||
|
||||
resp, err := client.R().SetResult(&data{}).Get("https://gts.rat.dev/eab")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
return &acme.EAB{}, errors.New(r.t.Get("failed to get Google EAB: %v", err))
|
||||
}
|
||||
eab := resp.Result().(*data)
|
||||
@@ -238,7 +238,7 @@ func (r certAccountRepo) getZeroSSLEAB(email string) (*acme.EAB, error) {
|
||||
resp, err := client.R().SetFormData(map[string]string{
|
||||
"email": email,
|
||||
}).SetResult(&data{}).Post("https://api.zerossl.com/acme/eab-credentials-email")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
return &acme.EAB{}, errors.New(r.t.Get("failed to get ZeroSSL EAB: %v", err))
|
||||
}
|
||||
eab := resp.Result().(*data)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/pkg/docker"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -311,30 +312,37 @@ func (r *containerRepo) Rename(id string, newName string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Logs 查看容器日志
|
||||
func (r *containerRepo) Logs(id string) (string, error) {
|
||||
// Logs 查看容器末尾 tail 行日志
|
||||
func (r *containerRepo) Logs(id string, tail int) (string, error) {
|
||||
apiClient, err := getDockerClient(getContainerSock(r.settingRepo))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func(apiClient *client.Client) { _ = apiClient.Close() }(apiClient)
|
||||
|
||||
// 非 TTY 容器日志为多路复用流,需按 TTY 设置决定是否解复用
|
||||
inspect, err := apiClient.ContainerInspect(context.Background(), id, client.ContainerInspectOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tty := inspect.Container.Config != nil && inspect.Container.Config.Tty
|
||||
|
||||
reader, err := apiClient.ContainerLogs(context.Background(), id, client.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Tail: "100",
|
||||
Tail: strconv.Itoa(tail),
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func(reader io.ReadCloser) { _ = reader.Close() }(reader)
|
||||
defer func(reader client.ContainerLogsResult) { _ = reader.Close() }(reader)
|
||||
|
||||
logs, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
var buf bytes.Buffer
|
||||
if err = docker.CopyLogs(&buf, reader, tty); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(logs), nil
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Prune 清理未使用的容器
|
||||
|
||||
@@ -64,7 +64,13 @@ func (r *containerComposeRepo) List() ([]types.ContainerCompose, error) {
|
||||
|
||||
// 更新状态
|
||||
for _, item := range composeRaws {
|
||||
if i, ok := index[item.Name]; ok {
|
||||
if item.ConfigFiles == "" {
|
||||
continue
|
||||
}
|
||||
// ConfigFiles 可能包含多个以逗号分隔的路径,取第一个
|
||||
first := strings.SplitN(item.ConfigFiles, ",", 2)[0]
|
||||
dir := filepath.Base(filepath.Dir(first))
|
||||
if i, ok := index[dir]; ok {
|
||||
composes[i].Status = item.Status
|
||||
}
|
||||
}
|
||||
@@ -77,17 +83,7 @@ func (r *containerComposeRepo) Get(name string) (string, []types.KV, error) {
|
||||
content, _ := os.ReadFile(filepath.Join(app.Root, "compose", name, "docker-compose.yml"))
|
||||
env, _ := os.ReadFile(filepath.Join(app.Root, "compose", name, ".env"))
|
||||
|
||||
var envs []types.KV
|
||||
for line := range strings.SplitSeq(string(env), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
kv := strings.SplitN(line, "=", 2)
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
envs = append(envs, types.KV{Key: kv[0], Value: kv[1]})
|
||||
}
|
||||
envs := types.SliceToKV(strings.Split(string(env), "\n"))
|
||||
|
||||
return string(content), envs, nil // 有意忽略错误,这样可以允许新建文件
|
||||
}
|
||||
|
||||
@@ -9,8 +9,10 @@ import (
|
||||
"time"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
"github.com/moby/moby/api/types/registry"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
@@ -41,9 +43,8 @@ func (r *containerImageRepo) List() ([]types.ContainerImage, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var images []types.ContainerImage
|
||||
for _, item := range resp.Items {
|
||||
images = append(images, types.ContainerImage{
|
||||
images := lo.Map(resp.Items, func(item image.Summary, _ int) types.ContainerImage {
|
||||
return types.ContainerImage{
|
||||
ID: item.ID,
|
||||
Containers: item.Containers,
|
||||
RepoTags: item.RepoTags,
|
||||
@@ -51,8 +52,8 @@ func (r *containerImageRepo) List() ([]types.ContainerImage, error) {
|
||||
Size: tools.FormatBytes(float64(item.Size)),
|
||||
Labels: types.MapToKV(item.Labels),
|
||||
CreatedAt: time.Unix(item.Created, 0),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
slices.SortFunc(images, func(a types.ContainerImage, b types.ContainerImage) int {
|
||||
return strings.Compare(a.ID, b.ID)
|
||||
@@ -135,7 +136,9 @@ func (r *containerImageRepo) Prune() error {
|
||||
defer func(apiClient *client.Client) { _ = apiClient.Close() }(apiClient)
|
||||
|
||||
_, err = apiClient.ImagePrune(context.Background(), client.ImagePruneOptions{
|
||||
Filters: make(client.Filters).Add("label", "created_by!=acepanel"),
|
||||
Filters: make(client.Filters).
|
||||
Add("dangling", "false").
|
||||
Add("label", "created_by!=acepanel"),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
@@ -151,6 +152,15 @@ func (r *containerNetworkRepo) Remove(id string) error {
|
||||
}
|
||||
defer func(apiClient *client.Client) { _ = apiClient.Close() }(apiClient)
|
||||
|
||||
// 拦截受保护网络的删除
|
||||
info, err := apiClient.NetworkInspect(context.Background(), id, client.NetworkInspectOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Network.Name == "acepanel-network" {
|
||||
return errors.New("cannot delete acepanel-network")
|
||||
}
|
||||
|
||||
_, err = apiClient.NetworkRemove(context.Background(), id, client.NetworkRemoveOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ func (r *cronRepo) generateScript(typ string, config types.CronConfig, rawScript
|
||||
switch config.Type {
|
||||
case "website":
|
||||
_, _ = fmt.Fprintf(&sb, "acepanel backup website -n '%s' -s '%d'\n", target, config.Storage)
|
||||
case "mysql", "postgresql":
|
||||
case "mysql", "postgresql", "clickhouse", "redis", "valkey":
|
||||
_, _ = fmt.Fprintf(&sb, "acepanel backup database -t '%s' -n '%s' -s '%d'\n", config.Type, target, config.Storage)
|
||||
case "path":
|
||||
_, _ = fmt.Fprintf(&sb, "acepanel backup path -p '%s' -s '%d'\n", target, config.Storage)
|
||||
@@ -299,7 +299,7 @@ func (r *cronRepo) generateScript(typ string, config types.CronConfig, rawScript
|
||||
switch config.Type {
|
||||
case "website":
|
||||
_, _ = fmt.Fprintf(&sb, "acepanel backup clear -t website -f '%s' -k '%d' -s '%d'\n", target, config.Keep, config.Storage)
|
||||
case "mysql", "postgresql":
|
||||
case "mysql", "postgresql", "clickhouse", "redis", "valkey":
|
||||
_, _ = fmt.Fprintf(&sb, "acepanel backup clear -t '%s' -f '%s' -k '%d' -s '%d'\n", config.Type, target, config.Keep, config.Storage)
|
||||
case "path":
|
||||
_, _ = fmt.Fprintf(&sb, "acepanel backup clear -t path -f '%s' -k '%d' -s '%d'\n", filepath.Base(target), config.Keep, config.Storage)
|
||||
|
||||
@@ -121,6 +121,10 @@ func (r *databaseServerRepo) UpdatePassword(name string, password string) error
|
||||
return r.db.Model(&biz.DatabaseServer{}).Where("name = ?", name).Update("password", password).Error
|
||||
}
|
||||
|
||||
func (r *databaseServerRepo) UpdatePort(name string, port uint) error {
|
||||
return r.db.Model(&biz.DatabaseServer{}).Where("name = ?", name).Update("port", port).Error
|
||||
}
|
||||
|
||||
func (r *databaseServerRepo) Delete(id uint) error {
|
||||
if err := r.ClearUsers(id); err != nil {
|
||||
return err
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -169,19 +170,15 @@ func (r *logRepo) fillOperatorNames(entries []biz.LogEntry) {
|
||||
}
|
||||
|
||||
// 批量查询用户名
|
||||
ids := make([]uint, 0, len(userIDs))
|
||||
for id := range userIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
ids := lo.Keys(userIDs)
|
||||
|
||||
var users []biz.User
|
||||
r.db.Select("id", "username").Where("id IN ?", ids).Find(&users)
|
||||
|
||||
// 构建ID到用户名的映射
|
||||
userMap := make(map[uint]string)
|
||||
for _, user := range users {
|
||||
userMap[user.ID] = user.Username
|
||||
}
|
||||
userMap := lo.SliceToMap(users, func(user biz.User) (uint, string) {
|
||||
return user.ID, user.Username
|
||||
})
|
||||
|
||||
// 填充用户名
|
||||
for i := range entries {
|
||||
|
||||
@@ -61,19 +61,18 @@ func (r *projectRepo) List(typ types.ProjectType, page, limit uint) ([]*types.Pr
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
details := make([]*types.ProjectDetail, 0, len(projects))
|
||||
for _, p := range projects {
|
||||
details := lo.Map(projects, func(p *biz.Project, _ int) *types.ProjectDetail {
|
||||
detail, err := r.parseProjectDetail(p)
|
||||
if err != nil {
|
||||
// 如果解析失败,返回基本信息
|
||||
detail = &types.ProjectDetail{
|
||||
return &types.ProjectDetail{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
}
|
||||
}
|
||||
details = append(details, detail)
|
||||
}
|
||||
return detail
|
||||
})
|
||||
|
||||
return details, total, nil
|
||||
}
|
||||
|
||||
@@ -172,6 +172,10 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backupFormat, err := r.Get(biz.SettingKeyBackupFormat, "tar.xz")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projectPath, err := r.Get(biz.SettingKeyProjectPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -228,6 +232,7 @@ func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
|
||||
BindUA: r.conf.HTTP.BindUA,
|
||||
WebsitePath: websitePath,
|
||||
BackupPath: backupPath,
|
||||
BackupFormat: backupFormat,
|
||||
ProjectPath: projectPath,
|
||||
ContainerSock: containerSock,
|
||||
HiddenMenu: hiddenMenu,
|
||||
@@ -262,6 +267,9 @@ func (r *settingRepo) UpdatePanel(ctx context.Context, req *request.SettingPanel
|
||||
if err := r.Set(biz.SettingKeyBackupPath, req.BackupPath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.Set(biz.SettingKeyBackupFormat, req.BackupFormat); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.Set(biz.SettingKeyProjectPath, req.ProjectPath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
+12
-22
@@ -201,7 +201,7 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
|
||||
if phpVhost, ok := vhost.(webservertypes.PHPVhost); ok {
|
||||
setting.PHP = phpVhost.PHP()
|
||||
// 伪静态
|
||||
setting.Rewrite = phpVhost.Config("010-rewrite.conf", "site")
|
||||
setting.Rewrite = phpVhost.Config("010-rewrite.conf", webservertypes.ScopeSite)
|
||||
}
|
||||
|
||||
// 反向代理网站特有
|
||||
@@ -226,7 +226,7 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
|
||||
setting.CustomConfigs = r.getCustomConfigs(configDir)
|
||||
|
||||
// 访问统计
|
||||
setting.StatEnabled = vhost.Config("021-stats-log.conf", "site") != ""
|
||||
setting.StatEnabled = vhost.Config("021-stats-log.conf", webservertypes.ScopeSite) != ""
|
||||
|
||||
return setting, err
|
||||
}
|
||||
@@ -344,7 +344,7 @@ func (r *websiteRepo) Create(ctx context.Context, req *request.WebsiteCreate) (*
|
||||
case "apache":
|
||||
errorPageConfig = `ErrorDocument 404 /404.html`
|
||||
}
|
||||
if err = vhost.SetConfig("010-error-404.conf", "site", errorPageConfig); err != nil {
|
||||
if err = vhost.SetConfig("010-error-404.conf", webservertypes.ScopeSite, errorPageConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -368,7 +368,7 @@ func (r *websiteRepo) Create(ctx context.Context, req *request.WebsiteCreate) (*
|
||||
if err = phpVhost.SetIndex([]string{"index.php", "index.html"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = phpVhost.SetConfig("010-rewrite.conf", "site", "", true); err != nil {
|
||||
if err = phpVhost.SetRawConfig("010-rewrite.conf", webservertypes.ScopeSite, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cacheConfig string
|
||||
@@ -420,7 +420,7 @@ location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) {
|
||||
</FilesMatch>
|
||||
`
|
||||
}
|
||||
if err = phpVhost.SetConfig("010-cache.conf", "site", cacheConfig); err != nil {
|
||||
if err = phpVhost.SetConfig("010-cache.conf", webservertypes.ScopeSite, cacheConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -472,7 +472,7 @@ location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.env) {
|
||||
}
|
||||
|
||||
// 写配置
|
||||
if err = vhost.SetConfig("001-acme.conf", "site", ""); err != nil {
|
||||
if err = vhost.SetConfig("001-acme.conf", webservertypes.ScopeSite, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -665,7 +665,7 @@ func (r *websiteRepo) Update(ctx context.Context, req *request.WebsiteUpdate) er
|
||||
return err
|
||||
}
|
||||
// 伪静态
|
||||
if err = phpVhost.SetConfig("010-rewrite.conf", "site", req.Rewrite, true); err != nil {
|
||||
if err = phpVhost.SetRawConfig("010-rewrite.conf", webservertypes.ScopeSite, req.Rewrite); err != nil {
|
||||
return err
|
||||
}
|
||||
// 防跨站
|
||||
@@ -765,8 +765,8 @@ func (r *websiteRepo) Update(ctx context.Context, req *request.WebsiteUpdate) er
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
_ = vhost.RemoveConfig("010-stat-format.conf", "shared")
|
||||
_ = vhost.RemoveConfig("021-stats-log.conf", "site")
|
||||
_ = vhost.RemoveConfig("010-stat-format.conf", webservertypes.ScopeShared)
|
||||
_ = vhost.RemoveConfig("021-stats-log.conf", webservertypes.ScopeSite)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,16 +828,6 @@ func (r *websiteRepo) Delete(ctx context.Context, req *request.WebsiteDelete) er
|
||||
return r.reloadWebServer()
|
||||
}
|
||||
|
||||
func (r *websiteRepo) ClearLog(id uint) error {
|
||||
website := new(biz.Website)
|
||||
if err := r.db.Where("id", id).First(website).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := shell.Execf(`cat /dev/null > %s/sites/%s/log/access.log`, app.Root, website.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *websiteRepo) UpdateRemark(id uint, remark string) error {
|
||||
website := new(biz.Website)
|
||||
if err := r.db.Where("id", id).First(website).Error; err != nil {
|
||||
@@ -880,7 +870,7 @@ func (r *websiteRepo) ResetConfig(id uint) error {
|
||||
return err
|
||||
}
|
||||
// 保存配置
|
||||
if err = vhost.SetConfig("001-acme.conf", "site", ""); err != nil {
|
||||
if err = vhost.SetConfig("001-acme.conf", webservertypes.ScopeSite, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = vhost.Save(); err != nil {
|
||||
@@ -1310,9 +1300,9 @@ func (r *websiteRepo) enableStat(vhost webservertypes.Vhost, name string) error
|
||||
'"https":"$https",'
|
||||
'"upstream_time":"$upstream_response_time",'
|
||||
'"upstream_status":"$upstream_status"}';`, safeName, name)
|
||||
if err := vhost.SetConfig("010-stat-format.conf", "shared", formatConf); err != nil {
|
||||
if err := vhost.SetConfig("010-stat-format.conf", webservertypes.ScopeShared, formatConf); err != nil {
|
||||
return err
|
||||
}
|
||||
logConf := fmt.Sprintf("client_body_in_single_buffer on;\naccess_log syslog:server=unix:/tmp/ace_stats.sock,nohostname,tag=%s ace_stat_%s;", safeName, safeName)
|
||||
return vhost.SetConfig("021-stats-log.conf", "site", logConf)
|
||||
return vhost.SetConfig("021-stats-log.conf", webservertypes.ScopeSite, logConf)
|
||||
}
|
||||
|
||||
@@ -100,15 +100,13 @@ func Entrance(t *gotext.Locale, conf *config.Config, session *sessions.Manager)
|
||||
}
|
||||
}
|
||||
|
||||
// 情况三:通过APIKey+入口路径访问,重写请求路径并跳过验证
|
||||
if strings.HasPrefix(r.URL.Path, entrance) && r.Header.Get("Authorization") != "" {
|
||||
// 只在设置了入口路径的情况下,才进行重写
|
||||
if entrance != "/" {
|
||||
if rctx := chi.RouteContext(r.Context()); rctx != nil {
|
||||
rctx.RoutePath = strings.TrimPrefix(rctx.RoutePath, entrance)
|
||||
r.URL.Path = strings.TrimPrefix(r.URL.Path, entrance)
|
||||
}
|
||||
// 情况三:通过 APIKey+入口路径访问 API,重写请求路径并跳过入口验证
|
||||
apiPath, ok := trimEntranceAPIPath(r.URL.Path, entrance)
|
||||
if ok && r.Header.Get("Authorization") != "" {
|
||||
if rctx := chi.RouteContext(r.Context()); rctx != nil {
|
||||
rctx.RoutePath = apiPath
|
||||
}
|
||||
r.URL.Path = apiPath
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -132,6 +130,29 @@ func Entrance(t *gotext.Locale, conf *config.Config, session *sessions.Manager)
|
||||
}
|
||||
}
|
||||
|
||||
func trimEntranceAPIPath(path string, entrance string) (string, bool) {
|
||||
if entrance == "/" {
|
||||
if path == "/api" || strings.HasPrefix(path, "/api/") {
|
||||
return path, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
if path != entrance && !strings.HasPrefix(path, entrance+"/") {
|
||||
return "", false
|
||||
}
|
||||
|
||||
apiPath := strings.TrimPrefix(path, entrance)
|
||||
if apiPath == "" {
|
||||
return "", false
|
||||
}
|
||||
if apiPath != "/api" && !strings.HasPrefix(apiPath, "/api/") {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return apiPath, true
|
||||
}
|
||||
|
||||
func abortEntrance(w http.ResponseWriter, r *http.Request, conf *config.Config, locale string) {
|
||||
errorType := conf.HTTP.EntranceError
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package middleware
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTrimEntranceAPIPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
entrance string
|
||||
wantPath string
|
||||
wantOK bool
|
||||
}{
|
||||
{name: "root api", path: "/api/user/info", entrance: "/", wantPath: "/api/user/info", wantOK: true},
|
||||
{name: "root api exact", path: "/api", entrance: "/", wantPath: "/api", wantOK: true},
|
||||
{name: "root api prefix confusion", path: "/apiary", entrance: "/", wantOK: false},
|
||||
{name: "entrance api", path: "/secret/api/user/info", entrance: "/secret", wantPath: "/api/user/info", wantOK: true},
|
||||
{name: "entrance api exact", path: "/secret/api", entrance: "/secret", wantPath: "/api", wantOK: true},
|
||||
{name: "entrance page", path: "/secret/login", entrance: "/secret", wantOK: false},
|
||||
{name: "entrance prefix confusion", path: "/secretx/api/user/info", entrance: "/secret", wantOK: false},
|
||||
{name: "api prefix confusion", path: "/secret/apiary", entrance: "/secret", wantOK: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotPath, gotOK := trimEntranceAPIPath(tt.path, tt.entrance)
|
||||
if gotOK != tt.wantOK {
|
||||
t.Fatalf("ok = %v, want %v", gotOK, tt.wantOK)
|
||||
}
|
||||
if gotPath != tt.wantPath {
|
||||
t.Fatalf("path = %q, want %q", gotPath, tt.wantPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
)
|
||||
@@ -27,13 +28,10 @@ func MustInstall(t *gotext.Locale, app biz.AppRepo) func(next http.Handler) http
|
||||
slugs = append(slugs, pathArr[3])
|
||||
}
|
||||
|
||||
flag := false
|
||||
for _, s := range slugs {
|
||||
if installed, _ := app.IsInstalled("slug = ?", s); installed {
|
||||
flag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
flag := lo.SomeBy(slugs, func(s string) bool {
|
||||
installed, _ := app.IsInstalled("slug = ?", s)
|
||||
return installed
|
||||
})
|
||||
if !flag && len(slugs) > 0 {
|
||||
Abort(w, http.StatusForbidden, t.Get("app %s not installed", slugs))
|
||||
return
|
||||
|
||||
@@ -3,11 +3,11 @@ package request
|
||||
import "mime/multipart"
|
||||
|
||||
type BackupList struct {
|
||||
Type string `uri:"type" form:"type" validate:"required|in:path,website,mysql,postgresql,redis,panel"`
|
||||
Type string `uri:"type" form:"type" validate:"required|in:path,website,mysql,postgresql,clickhouse,redis,valkey,panel"`
|
||||
}
|
||||
|
||||
type BackupCreate struct {
|
||||
Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgresql,redis,panel"`
|
||||
Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgresql,clickhouse,redis,valkey,panel"`
|
||||
Target string `json:"target" form:"target" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Storage uint `form:"storage" json:"storage"`
|
||||
}
|
||||
@@ -18,7 +18,7 @@ type BackupUpload struct {
|
||||
}
|
||||
|
||||
type BackupFile struct {
|
||||
Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgresql,redis,panel"`
|
||||
Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgresql,clickhouse,redis,valkey,panel"`
|
||||
File string `json:"file" form:"file" validate:"required"`
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,20 @@ type FilePath struct {
|
||||
Path string `json:"path" form:"path" validate:"required|isUnixPath"`
|
||||
}
|
||||
|
||||
type FileTail struct {
|
||||
Path string `json:"path" form:"path"`
|
||||
Service string `json:"service" form:"service"`
|
||||
Container string `json:"container" form:"container"`
|
||||
Offset int `json:"offset" form:"offset"`
|
||||
Limit int `json:"limit" form:"limit"`
|
||||
}
|
||||
|
||||
type FileFollow struct {
|
||||
Path string `json:"path" form:"path"`
|
||||
Service string `json:"service" form:"service"`
|
||||
Container string `json:"container" form:"container"`
|
||||
}
|
||||
|
||||
type FileCreate struct {
|
||||
Dir bool `json:"dir" form:"dir"`
|
||||
Path string `json:"path" form:"path" validate:"required|isUnixPath"`
|
||||
|
||||
@@ -19,6 +19,7 @@ type SettingPanel struct {
|
||||
BindUA []string `json:"bind_ua"`
|
||||
WebsitePath string `json:"website_path" validate:"required"`
|
||||
BackupPath string `json:"backup_path" validate:"required"`
|
||||
BackupFormat string `json:"backup_format" validate:"in:tar.xz,tar.gz,tar.zst,zip,7z"` // 备份压缩格式
|
||||
ProjectPath string `json:"project_path" validate:"required"`
|
||||
ContainerSock string `json:"container_sock"`
|
||||
HiddenMenu []string `json:"hidden_menu"` // 隐藏的菜单项
|
||||
|
||||
+15
-13
@@ -1,6 +1,7 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os"
|
||||
@@ -38,15 +39,15 @@ func NewCertRenew(conf *config.Config, db *gorm.DB, log *slog.Logger, setting bi
|
||||
}
|
||||
}
|
||||
|
||||
func (r *CertRenew) Run() {
|
||||
func (r *CertRenew) Run(_ context.Context) error {
|
||||
if app.Status != app.StatusNormal {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
var certs []biz.Cert
|
||||
if err := r.db.Preload("Website").Preload("Account").Preload("DNS").Find(&certs).Error; err != nil {
|
||||
r.log.Warn("failed to get certs", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, cert := range certs {
|
||||
@@ -81,7 +82,7 @@ func (r *CertRenew) Run() {
|
||||
decode, err := pkgcert.ParseCert(crt)
|
||||
if err == nil {
|
||||
if time.Until(decode.NotAfter) > 30*24*time.Hour {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
r.log.Warn("failed to parse panel certificate", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
@@ -90,14 +91,14 @@ func (r *CertRenew) Run() {
|
||||
newCrt, newKey, err := pkgcert.GenerateSelfSigned(tools.CollectLocalNames())
|
||||
if err != nil {
|
||||
r.log.Warn("failed to generate self-signed certificate", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if err = r.settingRepo.UpdateCert(&request.SettingCert{
|
||||
Cert: string(newCrt),
|
||||
Key: string(newKey),
|
||||
}); err != nil {
|
||||
r.log.Warn("failed to update panel certificate", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
r.log.Info("panel self-signed certificate renewed", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0))
|
||||
|
||||
@@ -111,7 +112,7 @@ func (r *CertRenew) Run() {
|
||||
if err == nil {
|
||||
// 结束时间大于 2 天不续签
|
||||
if time.Until(decode.NotAfter) > 24*2*time.Hour {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
// 解析失败则继续续签流程,可能是证书格式不对或者文件不存在
|
||||
@@ -121,28 +122,28 @@ func (r *CertRenew) Run() {
|
||||
ip, err := r.settingRepo.Get(biz.SettingKeyPublicIPs)
|
||||
if err != nil {
|
||||
r.log.Warn("failed to get panel IP", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
var ips []string
|
||||
if err = json.Unmarshal([]byte(ip), &ips); err != nil || len(ips) == 0 {
|
||||
r.log.Warn("panel public IPs not set", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
var user biz.User
|
||||
if err = r.db.First(&user).Error; err != nil {
|
||||
r.log.Warn("failed to get a panel user", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
account, err := r.certAccountRepo.GetDefault(user.ID)
|
||||
if err != nil {
|
||||
r.log.Warn("failed to get panel ACME account", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
crt, key, err := r.certRepo.ObtainPanel(account, ips)
|
||||
if err != nil {
|
||||
r.log.Warn("failed to obtain panel certificate via ACME", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = r.settingRepo.UpdateCert(&request.SettingCert{
|
||||
@@ -150,10 +151,11 @@ func (r *CertRenew) Run() {
|
||||
Key: string(key),
|
||||
}); err != nil {
|
||||
r.log.Warn("failed to update panel certificate", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
r.log.Info("panel certificate renewed successfully", slog.String("type", biz.OperationTypeCert), slog.Uint64("operator_id", 0))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/pkg/firewall"
|
||||
@@ -48,16 +51,16 @@ func NewFirewallScan(log *slog.Logger, setting biz.SettingRepo, scanRepo biz.Sca
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FirewallScan) Run() {
|
||||
func (r *FirewallScan) Run(_ context.Context) error {
|
||||
if app.Status != app.StatusNormal {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
enabled, err := r.setting.GetBool(biz.SettingKeyScanAware)
|
||||
if err != nil || !enabled {
|
||||
// 未启用时,确保 scanner 已停止
|
||||
r.stopScanner()
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// 确保 scanner 已启动
|
||||
@@ -74,6 +77,7 @@ func (r *FirewallScan) Run() {
|
||||
|
||||
// 清理过期数据
|
||||
r.cleanup()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureScanner 确保 scanner 正在运行
|
||||
@@ -166,10 +170,7 @@ func (r *FirewallScan) flush() {
|
||||
return
|
||||
}
|
||||
|
||||
events := make([]*biz.ScanEvent, 0, len(r.buffer))
|
||||
for _, evt := range r.buffer {
|
||||
events = append(events, evt)
|
||||
}
|
||||
events := lo.Values(r.buffer)
|
||||
r.buffer = make(map[string]*biz.ScanEvent)
|
||||
r.mu.Unlock()
|
||||
|
||||
@@ -350,12 +351,9 @@ func isWhitelisted(ip string, whitelist []net.IPNet) bool {
|
||||
if parsed.IsLoopback() || parsed.IsUnspecified() {
|
||||
return true
|
||||
}
|
||||
for _, cidr := range whitelist {
|
||||
if cidr.Contains(parsed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return lo.ContainsBy(whitelist, func(cidr net.IPNet) bool {
|
||||
return cidr.Contains(parsed)
|
||||
})
|
||||
}
|
||||
|
||||
// cleanup 清理过期数据
|
||||
|
||||
+7
-7
@@ -4,7 +4,7 @@ import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/google/wire"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/libtnb/cron"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
@@ -49,22 +49,22 @@ func NewJobs(conf *config.Config, db *gorm.DB, log *slog.Logger, aggregator *web
|
||||
}
|
||||
|
||||
func (r *Jobs) Register(c *cron.Cron) error {
|
||||
if _, err := c.AddJob("* * * * *", NewMonitoring(r.db, r.log, r.setting)); err != nil {
|
||||
if _, err := c.Add("* * * * *", NewMonitoring(r.db, r.log, r.setting)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.AddJob("*/2 * * * *", NewFirewallScan(r.log, r.setting, r.scan)); err != nil {
|
||||
if _, err := c.Add("*/2 * * * *", NewFirewallScan(r.log, r.setting, r.scan)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.AddJob("0 4 * * *", NewCertRenew(r.conf, r.db, r.log, r.setting, r.cert, r.certAccount)); err != nil {
|
||||
if _, err := c.Add("0 4 * * *", NewCertRenew(r.conf, r.db, r.log, r.setting, r.cert, r.certAccount)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.AddJob("0 2 * * *", NewPanelTask(r.conf, r.db, r.log, r.backup, r.cache, r.task, r.setting, r.scan, r.stat)); err != nil {
|
||||
if _, err := c.Add("0 2 * * *", NewPanelTask(r.conf, r.db, r.log, r.backup, r.cache, r.task, r.setting, r.scan, r.stat)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.AddJob("* * * * *", NewWebsiteStat(r.log, r.setting, r.stat, r.aggregator)); err != nil {
|
||||
if _, err := c.Add("* * * * *", NewWebsiteStat(r.log, r.setting, r.stat, r.aggregator)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.AddJob("* * * * *", NewWebsiteExpire(r.db, r.log, r.website)); err != nil {
|
||||
if _, err := c.Add("* * * * *", NewWebsiteExpire(r.db, r.log, r.website)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
@@ -28,14 +29,14 @@ func NewMonitoring(db *gorm.DB, log *slog.Logger, setting biz.SettingRepo) *Moni
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Monitoring) Run() {
|
||||
func (r *Monitoring) Run(_ context.Context) error {
|
||||
if app.Status != app.StatusNormal {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
monitor, err := r.settingRepo.Get(biz.SettingKeyMonitor)
|
||||
if err != nil || !cast.ToBool(monitor) {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// 根据采集间隔判断是否该采集
|
||||
@@ -44,7 +45,7 @@ func (r *Monitoring) Run() {
|
||||
interval = 1
|
||||
}
|
||||
if !r.lastRun.IsZero() && time.Since(r.lastRun) < time.Duration(interval)*time.Minute-30*time.Second {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
r.lastRun = time.Now()
|
||||
|
||||
@@ -56,25 +57,26 @@ func (r *Monitoring) Run() {
|
||||
info.Cpus = nil
|
||||
|
||||
if app.Status != app.StatusNormal {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = r.db.Create(&biz.Monitor{Info: info}).Error; err != nil {
|
||||
r.log.Warn("failed to create monitor record", slog.String("type", biz.OperationTypeMonitor), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// 删除过期数据
|
||||
dayStr, err := r.settingRepo.Get(biz.SettingKeyMonitorDays)
|
||||
if err != nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
day := cast.ToInt(dayStr)
|
||||
if day <= 0 || app.Status != app.StatusNormal {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if err = r.db.Where("created_at < ?", time.Now().AddDate(0, 0, -day).Format(time.DateTime)).Delete(&biz.Monitor{}).Error; err != nil {
|
||||
r.log.Warn("failed to delete monitor record", slog.String("type", biz.OperationTypeMonitor), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand/v2"
|
||||
@@ -50,19 +51,19 @@ func NewPanelTask(conf *config.Config, db *gorm.DB, log *slog.Logger, backup biz
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PanelTask) Run() {
|
||||
func (r *PanelTask) Run(_ context.Context) error {
|
||||
app.Status = app.StatusMaintain
|
||||
|
||||
// 优化主数据库
|
||||
if err := r.db.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
|
||||
app.Status = app.StatusFailed
|
||||
r.log.Warn("failed to wal checkpoint database", slog.String("type", biz.OperationTypePanel), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if err := r.db.Exec("VACUUM").Error; err != nil {
|
||||
app.Status = app.StatusFailed
|
||||
r.log.Warn("failed to vacuum database", slog.String("type", biz.OperationTypePanel), slog.Uint64("operator_id", 0), slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
// 优化辅助数据库
|
||||
if err := r.scanRepo.VacuumDB(); err != nil {
|
||||
@@ -102,6 +103,7 @@ func (r *PanelTask) Run() {
|
||||
debug.FreeOSMemory()
|
||||
|
||||
app.Status = app.StatusNormal
|
||||
return nil
|
||||
}
|
||||
|
||||
// 更新分类缓存
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
@@ -26,9 +27,9 @@ func NewWebsiteExpire(db *gorm.DB, log *slog.Logger, websiteRepo biz.WebsiteRepo
|
||||
}
|
||||
}
|
||||
|
||||
func (r *WebsiteExpire) Run() {
|
||||
func (r *WebsiteExpire) Run(_ context.Context) error {
|
||||
if app.Status != app.StatusNormal {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
var websites []biz.Website
|
||||
@@ -36,7 +37,7 @@ func (r *WebsiteExpire) Run() {
|
||||
// 直接查询已到期且仍在运行的网站
|
||||
if err := r.db.Where("expire_at IS NOT NULL AND expire_at <= ? AND status = ?", now, true).Find(&websites).Error; err != nil {
|
||||
r.log.Warn("failed to query expired websites", slog.Any("err", err))
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, website := range websites {
|
||||
@@ -46,4 +47,5 @@ func (r *WebsiteExpire) Run() {
|
||||
}
|
||||
r.log.Info("website expired and disabled", slog.String("name", website.Name), slog.Time("expire_at", *website.ExpireAt))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/pkg/geoip"
|
||||
@@ -34,9 +37,9 @@ func NewWebsiteStat(log *slog.Logger, setting biz.SettingRepo, statRepo biz.Webs
|
||||
}
|
||||
}
|
||||
|
||||
func (r *WebsiteStat) Run() {
|
||||
func (r *WebsiteStat) Run(_ context.Context) error {
|
||||
if app.Status != app.StatusNormal {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
r.ensureListener()
|
||||
@@ -45,6 +48,7 @@ func (r *WebsiteStat) Run() {
|
||||
r.flushErrors()
|
||||
r.flushDetails()
|
||||
r.cleanup()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureListener 确保 listener goroutine 已启动
|
||||
@@ -186,9 +190,8 @@ func (r *WebsiteStat) flushErrors() {
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
errors := make([]*biz.WebsiteErrorLog, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
errors = append(errors, &biz.WebsiteErrorLog{
|
||||
errors := lo.Map(entries, func(e *websitestat.ErrorEntry, _ int) *biz.WebsiteErrorLog {
|
||||
return &biz.WebsiteErrorLog{
|
||||
Site: e.Site,
|
||||
URI: e.URI,
|
||||
Method: e.Method,
|
||||
@@ -197,8 +200,8 @@ func (r *WebsiteStat) flushErrors() {
|
||||
UA: e.UA,
|
||||
Body: e.Body,
|
||||
CreatedAt: now,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if err := r.statRepo.InsertErrors(errors); err != nil {
|
||||
r.log.Warn("failed to insert website error logs", slog.Any("err", err))
|
||||
|
||||
@@ -264,7 +264,6 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Get("/{id}", route.website.Get)
|
||||
r.Put("/{id}", route.website.Update)
|
||||
r.Delete("/{id}", route.website.Delete)
|
||||
r.Delete("/{id}/log", route.website.ClearLog)
|
||||
r.Post("/{id}/update_remark", route.website.UpdateRemark)
|
||||
r.Post("/{id}/reset_config", route.website.ResetConfig)
|
||||
r.Post("/{id}/status", route.website.UpdateStatus)
|
||||
@@ -432,8 +431,6 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Get("/{version}/load", route.environmentPHP.Load)
|
||||
r.Get("/{version}/log", route.environmentPHP.Log)
|
||||
r.Get("/{version}/slow_log", route.environmentPHP.SlowLog)
|
||||
r.Post("/{version}/clear_log", route.environmentPHP.ClearLog)
|
||||
r.Post("/{version}/clear_slow_log", route.environmentPHP.ClearSlowLog)
|
||||
r.Get("/{version}/modules", route.environmentPHP.ModuleList)
|
||||
r.Post("/{version}/modules", route.environmentPHP.InstallModule)
|
||||
r.Delete("/{version}/modules", route.environmentPHP.UninstallModule)
|
||||
@@ -519,7 +516,6 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Post("/{id}/unpause", route.container.Unpause)
|
||||
r.Post("/{id}/kill", route.container.Kill)
|
||||
r.Post("/{id}/rename", route.container.Rename)
|
||||
r.Get("/{id}/logs", route.container.Logs)
|
||||
r.Post("/prune", route.container.Prune)
|
||||
})
|
||||
r.Route("/compose", func(r chi.Router) {
|
||||
@@ -555,7 +551,9 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Route("/file", func(r chi.Router) {
|
||||
r.Post("/create", route.file.Create)
|
||||
r.Get("/content", route.file.Content)
|
||||
r.Get("/tail", route.file.Tail)
|
||||
r.Post("/save", route.file.Save)
|
||||
r.Post("/truncate", route.file.Truncate)
|
||||
r.Post("/delete", route.file.Delete)
|
||||
r.Post("/upload", route.file.Upload)
|
||||
r.Post("/exist", route.file.Exist)
|
||||
@@ -606,6 +604,7 @@ func (route *Http) Register(r *chi.Mux) {
|
||||
r.Post("/reload", route.systemctl.Reload)
|
||||
r.Post("/start", route.systemctl.Start)
|
||||
r.Post("/stop", route.systemctl.Stop)
|
||||
r.Post("/clear_log", route.systemctl.ClearLog)
|
||||
})
|
||||
|
||||
r.Route("/toolbox_network", func(r chi.Router) {
|
||||
|
||||
@@ -22,6 +22,7 @@ func (route *Ws) Register(r *chi.Mux) {
|
||||
r.Route("/api/ws", func(r chi.Router) {
|
||||
r.Get("/exec", route.ws.Exec)
|
||||
r.Get("/pty", route.ws.PTY)
|
||||
r.Get("/follow", route.ws.Follow)
|
||||
r.Get("/ssh", route.ws.Session)
|
||||
r.Get("/container/{id}", route.ws.ContainerTerminal)
|
||||
r.Get("/container/image/pull", route.ws.ContainerImagePull)
|
||||
|
||||
+24
-6
@@ -6,9 +6,12 @@ import (
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/samber/lo"
|
||||
lop "github.com/samber/lo/parallel"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/pkg/apploader"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -17,14 +20,16 @@ type AppService struct {
|
||||
appRepo biz.AppRepo
|
||||
cacheRepo biz.CacheRepo
|
||||
settingRepo biz.SettingRepo
|
||||
loader *apploader.Loader
|
||||
}
|
||||
|
||||
func NewAppService(t *gotext.Locale, app biz.AppRepo, cache biz.CacheRepo, setting biz.SettingRepo) *AppService {
|
||||
func NewAppService(t *gotext.Locale, app biz.AppRepo, cache biz.CacheRepo, setting biz.SettingRepo, loader *apploader.Loader) *AppService {
|
||||
return &AppService{
|
||||
t: t,
|
||||
appRepo: app,
|
||||
cacheRepo: cache,
|
||||
settingRepo: setting,
|
||||
loader: loader,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,21 +50,22 @@ func (s *AppService) List(w http.ResponseWriter, r *http.Request) {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
installedAppMap := make(map[string]*biz.App)
|
||||
installedAppMap := lo.KeyBy(installedApps, func(p *biz.App) string {
|
||||
return p.Slug
|
||||
})
|
||||
|
||||
for _, p := range installedApps {
|
||||
installedAppMap[p.Slug] = p
|
||||
}
|
||||
statusMap := s.collectStatuses(installedApps)
|
||||
|
||||
var apps []types.AppDetail
|
||||
for _, item := range all {
|
||||
installed, installedChannel, installedVersion, updateExist, show := false, "", "", false, false
|
||||
installed, installedChannel, installedVersion, updateExist, show, status := false, "", "", false, false, ""
|
||||
if _, ok := installedAppMap[item.Slug]; ok {
|
||||
installed = true
|
||||
installedChannel = installedAppMap[item.Slug].Channel
|
||||
installedVersion = installedAppMap[item.Slug].Version
|
||||
updateExist = s.appRepo.UpdateExist(item.Slug)
|
||||
show = installedAppMap[item.Slug].Show
|
||||
status = statusMap[item.Slug]
|
||||
}
|
||||
if onlyInstalled && !installed {
|
||||
continue
|
||||
@@ -83,6 +89,7 @@ func (s *AppService) List(w http.ResponseWriter, r *http.Request) {
|
||||
InstalledVersion: installedVersion,
|
||||
UpdateExist: updateExist,
|
||||
Show: show,
|
||||
Status: status,
|
||||
}
|
||||
|
||||
for _, c := range item.Channels {
|
||||
@@ -236,3 +243,14 @@ func (s *AppService) UpdateCache(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// collectStatuses 并发获取已安装应用的运行状态
|
||||
func (s *AppService) collectStatuses(installed []*biz.App) map[string]string {
|
||||
pairs := lop.Map(installed, func(item *biz.App, _ int) lo.Entry[string, string] {
|
||||
if a, ok := s.loader.Get(item.Slug); ok {
|
||||
return lo.Entry[string, string]{Key: item.Slug, Value: a.Status()}
|
||||
}
|
||||
return lo.Entry[string, string]{Key: item.Slug, Value: types.AppStatusNA}
|
||||
})
|
||||
return lo.FromEntries(pairs)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
stdio "io"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -18,12 +19,14 @@ import (
|
||||
type BackupService struct {
|
||||
t *gotext.Locale
|
||||
backupRepo biz.BackupRepo
|
||||
taskRepo biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewBackupService(t *gotext.Locale, backup biz.BackupRepo) *BackupService {
|
||||
func NewBackupService(t *gotext.Locale, backup biz.BackupRepo, task biz.TaskRepo) *BackupService {
|
||||
return &BackupService{
|
||||
t: t,
|
||||
backupRepo: backup,
|
||||
taskRepo: task,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +58,21 @@ func (s *BackupService) Create(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.backupRepo.Create(r.Context(), biz.BackupType(req.Type), req.Target, req.Storage); err != nil {
|
||||
// 备份可能耗时较长(大库),提交到后台任务队列异步执行
|
||||
pathEnv := "export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH\n"
|
||||
var cmd string
|
||||
if req.Type == "website" {
|
||||
cmd = fmt.Sprintf("%sacepanel backup website -n '%s' -s '%d'", pathEnv, req.Target, req.Storage)
|
||||
} else {
|
||||
cmd = fmt.Sprintf("%sacepanel backup database -t '%s' -n '%s' -s '%d'", pathEnv, req.Type, req.Target, req.Storage)
|
||||
}
|
||||
|
||||
task := &biz.Task{
|
||||
Name: s.t.Get("Backup %s: %s", req.Type, req.Target),
|
||||
Status: biz.TaskStatusWaiting,
|
||||
Shell: cmd,
|
||||
}
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -77,8 +94,8 @@ func (s *BackupService) Upload(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 只允许上传 .sql .zip .tar .gz .tgz .bz2 .xz .7z
|
||||
if !slices.Contains([]string{".sql", ".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".7z"}, filepath.Ext(req.File.Filename)) {
|
||||
// 只允许上传 .sql .zip .tar .gz .tgz .bz2 .xz .zst .7z
|
||||
if !slices.Contains([]string{".sql", ".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".zst", ".7z"}, filepath.Ext(req.File.Filename)) {
|
||||
Error(w, http.StatusForbidden, s.t.Get("unsupported file type"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1040,6 +1040,7 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
{Key: biz.SettingKeyMonitorDays, Value: "30"},
|
||||
{Key: biz.SettingKeyMonitorInterval, Value: "1"},
|
||||
{Key: biz.SettingKeyBackupPath, Value: filepath.Join(app.Root, "backup")},
|
||||
{Key: biz.SettingKeyBackupFormat, Value: "tar.xz"},
|
||||
{Key: biz.SettingKeyWebsitePath, Value: filepath.Join(app.Root, "sites")},
|
||||
{Key: biz.SettingKeyProjectPath, Value: filepath.Join(app.Root, "projects")},
|
||||
{Key: biz.SettingKeyContainerSock, Value: "/var/run/docker.sock"},
|
||||
|
||||
@@ -182,22 +182,6 @@ func (s *ContainerService) Rename(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *ContainerService) Logs(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ContainerID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
logs, err := s.containerRepo.Logs(req.ID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, logs)
|
||||
}
|
||||
|
||||
func (s *ContainerService) Prune(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.containerRepo.Prune(); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
|
||||
@@ -34,7 +34,7 @@ func (s *EnvironmentService) List(w http.ResponseWriter, r *http.Request) {
|
||||
query := strings.ToLower(r.URL.Query().Get("query"))
|
||||
onlyInstalled := r.URL.Query().Get("installed") == "true"
|
||||
all := s.environmentRepo.All()
|
||||
var environments []types.EnvironmentDetail
|
||||
environments := make([]types.EnvironmentDetail, 0)
|
||||
for _, item := range all {
|
||||
if typ != "" && !strings.EqualFold(item.Type, typ) {
|
||||
continue
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"resty.dev/v3"
|
||||
|
||||
@@ -249,44 +250,6 @@ func (s *EnvironmentPHPService) SlowLog(w http.ResponseWriter, r *http.Request)
|
||||
Success(w, fmt.Sprintf("%s/server/php/%d/var/log/slow.log", app.Root, req.Version))
|
||||
}
|
||||
|
||||
func (s *EnvironmentPHPService) ClearLog(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
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("cat /dev/null > %s/server/php/%d/var/log/php-fpm.log", app.Root, req.Version); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *EnvironmentPHPService) ClearSlowLog(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
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("cat /dev/null > %s/server/php/%d/var/log/slow.log", app.Root, req.Version); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *EnvironmentPHPService) ModuleList(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.EnvironmentPHPVersion](r)
|
||||
if err != nil {
|
||||
@@ -630,15 +593,9 @@ func (s *EnvironmentPHPService) getModules(version uint) []types.EnvironmentPHPM
|
||||
}
|
||||
|
||||
func (s *EnvironmentPHPService) checkModule(version uint, slug string) bool {
|
||||
modules := s.getModules(version)
|
||||
|
||||
for _, item := range modules {
|
||||
if item.Slug == slug {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return lo.ContainsBy(s.getModules(version), func(item types.EnvironmentPHPModule) bool {
|
||||
return item.Slug == slug
|
||||
})
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 PHP 配置调整参数
|
||||
|
||||
+206
-10
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/utils/file"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -34,14 +35,16 @@ import (
|
||||
)
|
||||
|
||||
type FileService struct {
|
||||
t *gotext.Locale
|
||||
taskRepo biz.TaskRepo
|
||||
t *gotext.Locale
|
||||
taskRepo biz.TaskRepo
|
||||
containerRepo biz.ContainerRepo
|
||||
}
|
||||
|
||||
func NewFileService(t *gotext.Locale, task biz.TaskRepo) *FileService {
|
||||
func NewFileService(t *gotext.Locale, task biz.TaskRepo, container biz.ContainerRepo) *FileService {
|
||||
return &FileService{
|
||||
t: t,
|
||||
taskRepo: task,
|
||||
t: t,
|
||||
taskRepo: task,
|
||||
containerRepo: container,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +109,132 @@ func (s *FileService) Content(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// Tail 反向分页读取文件或 systemd 服务日志
|
||||
// offset: 从末尾起跳过的行数(0 表示从最末尾开始)
|
||||
// limit: 读取的行数(最多 5000)
|
||||
// 返回: lines 当前块的行(按文件正序),has_more 是否还有更早的内容,size 文件总字节
|
||||
func (s *FileService) Tail(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FileTail](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if req.Limit <= 0 {
|
||||
req.Limit = 500
|
||||
}
|
||||
if req.Limit > 5000 {
|
||||
req.Limit = 5000
|
||||
}
|
||||
if req.Offset < 0 {
|
||||
req.Offset = 0
|
||||
}
|
||||
|
||||
if req.Path == "" && req.Service == "" && req.Container == "" {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("path, service or container is required"))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Service != "" {
|
||||
s.tailService(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Container != "" {
|
||||
s.tailContainer(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
f, err := stdos.Open(req.Path)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func(f *stdos.File) { _ = f.Close() }(f)
|
||||
|
||||
stat, err := f.Stat()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
size := stat.Size()
|
||||
if size == 0 {
|
||||
Success(w, chix.M{"lines": []string{}, "has_more": false, "size": size})
|
||||
return
|
||||
}
|
||||
|
||||
// 从尾部反向读取,直到攒够 offset+limit+1 个换行符(多 1 是为了避免读到不完整的首行)
|
||||
const chunkSize = int64(8192)
|
||||
pos := size
|
||||
var data []byte
|
||||
needLines := req.Offset + req.Limit + 1
|
||||
newlineCount := 0
|
||||
for pos > 0 && newlineCount < needLines {
|
||||
readSize := chunkSize
|
||||
if pos < chunkSize {
|
||||
readSize = pos
|
||||
}
|
||||
pos -= readSize
|
||||
buf := make([]byte, readSize)
|
||||
if _, rerr := f.ReadAt(buf, pos); rerr != nil && rerr != stdio.EOF {
|
||||
Error(w, http.StatusInternalServerError, "%v", rerr)
|
||||
return
|
||||
}
|
||||
data = append(buf, data...)
|
||||
newlineCount = strings.Count(string(data), "\n")
|
||||
}
|
||||
|
||||
// 切分行
|
||||
all := strings.Split(strings.TrimRight(string(data), "\n"), "\n")
|
||||
totalLoaded := len(all)
|
||||
|
||||
// 当 pos > 0 时第一行可能不完整,丢弃以避免半行被显示
|
||||
startBoundary := 0
|
||||
if pos > 0 {
|
||||
startBoundary = 1
|
||||
}
|
||||
|
||||
endIdx := totalLoaded - req.Offset
|
||||
startIdx := endIdx - req.Limit
|
||||
if startIdx < startBoundary {
|
||||
startIdx = startBoundary
|
||||
}
|
||||
if endIdx < startIdx {
|
||||
endIdx = startIdx
|
||||
}
|
||||
if endIdx > totalLoaded {
|
||||
endIdx = totalLoaded
|
||||
}
|
||||
|
||||
hasMore := pos > 0 || startIdx > startBoundary
|
||||
|
||||
result := []string{}
|
||||
if startIdx < endIdx {
|
||||
result = all[startIdx:endIdx]
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"lines": result,
|
||||
"has_more": hasMore,
|
||||
"size": size,
|
||||
})
|
||||
}
|
||||
|
||||
// Truncate 截断文件至 0 长度(保留文件本身和元数据)
|
||||
func (s *FileService) Truncate(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FilePath](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = stdos.Truncate(req.Path, 0); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *FileService) Save(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FileSave](r)
|
||||
if err != nil {
|
||||
@@ -501,14 +630,13 @@ func (s *FileService) List(w http.ResponseWriter, r *http.Request) {
|
||||
entry stdos.DirEntry
|
||||
info stdos.FileInfo
|
||||
}
|
||||
entriesWithInfo := make([]entryWithInfo, 0, len(list))
|
||||
for _, entry := range list {
|
||||
entriesWithInfo := lo.FilterMap(list, func(entry stdos.DirEntry, _ int) (entryWithInfo, bool) {
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
return entryWithInfo{}, false
|
||||
}
|
||||
entriesWithInfo = append(entriesWithInfo, entryWithInfo{entry: entry, info: info})
|
||||
}
|
||||
return entryWithInfo{entry: entry, info: info}, true
|
||||
})
|
||||
|
||||
// 排序
|
||||
slices.SortFunc(entriesWithInfo, func(a, b entryWithInfo) int {
|
||||
@@ -812,3 +940,71 @@ func (s *FileService) getChunkTempFilePrefix(fileName, fileHash string) string {
|
||||
}
|
||||
return fmt.Sprintf(".%s.%s.chunk.", fileName, hashPrefix)
|
||||
}
|
||||
|
||||
// tailService 用 journalctl 反向读取 systemd 服务日志
|
||||
func (s *FileService) tailService(w http.ResponseWriter, req *request.FileTail) {
|
||||
total := req.Offset + req.Limit
|
||||
out, err := shell.Execf("journalctl --no-pager -n %d -u %s", total, req.Service)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
all := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
||||
totalLoaded := len(all)
|
||||
endIdx := totalLoaded - req.Offset
|
||||
startIdx := endIdx - req.Limit
|
||||
if startIdx < 0 {
|
||||
startIdx = 0
|
||||
}
|
||||
if endIdx < startIdx {
|
||||
endIdx = startIdx
|
||||
}
|
||||
if endIdx > totalLoaded {
|
||||
endIdx = totalLoaded
|
||||
}
|
||||
result := []string{}
|
||||
if startIdx < endIdx {
|
||||
result = all[startIdx:endIdx]
|
||||
}
|
||||
// journalctl -n 拿到的就是末尾,如果 totalLoaded == total 说明可能还有更早
|
||||
hasMore := totalLoaded >= total
|
||||
Success(w, chix.M{
|
||||
"lines": result,
|
||||
"has_more": hasMore,
|
||||
"size": 0,
|
||||
})
|
||||
}
|
||||
|
||||
// tailContainer 反向读取容器末尾日志
|
||||
func (s *FileService) tailContainer(w http.ResponseWriter, req *request.FileTail) {
|
||||
total := req.Offset + req.Limit
|
||||
out, err := s.containerRepo.Logs(req.Container, total)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
all := strings.Split(strings.TrimRight(out, "\n"), "\n")
|
||||
totalLoaded := len(all)
|
||||
endIdx := totalLoaded - req.Offset
|
||||
startIdx := endIdx - req.Limit
|
||||
if startIdx < 0 {
|
||||
startIdx = 0
|
||||
}
|
||||
if endIdx < startIdx {
|
||||
endIdx = startIdx
|
||||
}
|
||||
if endIdx > totalLoaded {
|
||||
endIdx = totalLoaded
|
||||
}
|
||||
result := []string{}
|
||||
if startIdx < endIdx {
|
||||
result = all[startIdx:endIdx]
|
||||
}
|
||||
// 取到的行数达到请求总数,说明可能还有更早的日志
|
||||
hasMore := totalLoaded >= total
|
||||
Success(w, chix.M{
|
||||
"lines": result,
|
||||
"has_more": hasMore,
|
||||
"size": 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,14 +13,16 @@ import (
|
||||
)
|
||||
|
||||
type FileService struct {
|
||||
t *gotext.Locale
|
||||
taskRepo biz.TaskRepo
|
||||
t *gotext.Locale
|
||||
taskRepo biz.TaskRepo
|
||||
containerRepo biz.ContainerRepo
|
||||
}
|
||||
|
||||
func NewFileService(t *gotext.Locale, task biz.TaskRepo) *FileService {
|
||||
func NewFileService(t *gotext.Locale, task biz.TaskRepo, container biz.ContainerRepo) *FileService {
|
||||
return &FileService{
|
||||
t: t,
|
||||
taskRepo: task,
|
||||
t: t,
|
||||
taskRepo: task,
|
||||
containerRepo: container,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +30,8 @@ func (s *FileService) Create(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func (s *FileService) Content(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func (s *FileService) Tail(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func (s *FileService) Save(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
func (s *FileService) Delete(w http.ResponseWriter, r *http.Request) {}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/pkg/firewall"
|
||||
@@ -142,20 +143,19 @@ func (s *FirewallService) GetIPRules(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var filledRules []map[string]any
|
||||
for rule := range slices.Values(rules) {
|
||||
filledRules := lo.FilterMap(rules, func(rule firewall.FireInfo, _ int) (map[string]any, bool) {
|
||||
// 保留IP规则
|
||||
if rule.PortStart != 1 || rule.PortEnd != 65535 || rule.Address == "" {
|
||||
continue
|
||||
return nil, false
|
||||
}
|
||||
filledRules = append(filledRules, map[string]any{
|
||||
return map[string]any{
|
||||
"family": rule.Family,
|
||||
"protocol": rule.Protocol,
|
||||
"address": rule.Address,
|
||||
"strategy": rule.Strategy,
|
||||
"direction": rule.Direction,
|
||||
})
|
||||
}
|
||||
}, true
|
||||
})
|
||||
|
||||
paged, total := Paginate(r, filledRules)
|
||||
|
||||
|
||||
+67
-92
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/utils/collect"
|
||||
"github.com/samber/lo"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
"github.com/spf13/cast"
|
||||
@@ -30,34 +31,36 @@ import (
|
||||
)
|
||||
|
||||
type HomeService struct {
|
||||
t *gotext.Locale
|
||||
api *api.API
|
||||
conf *config.Config
|
||||
taskRepo biz.TaskRepo
|
||||
websiteRepo biz.WebsiteRepo
|
||||
projectRepo biz.ProjectRepo
|
||||
appRepo biz.AppRepo
|
||||
environmentRepo biz.EnvironmentRepo
|
||||
settingRepo biz.SettingRepo
|
||||
cronRepo biz.CronRepo
|
||||
backupRepo biz.BackupRepo
|
||||
containerRepo biz.ContainerRepo
|
||||
t *gotext.Locale
|
||||
api *api.API
|
||||
conf *config.Config
|
||||
taskRepo biz.TaskRepo
|
||||
websiteRepo biz.WebsiteRepo
|
||||
projectRepo biz.ProjectRepo
|
||||
appRepo biz.AppRepo
|
||||
environmentRepo biz.EnvironmentRepo
|
||||
settingRepo biz.SettingRepo
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
cronRepo biz.CronRepo
|
||||
backupRepo biz.BackupRepo
|
||||
containerRepo biz.ContainerRepo
|
||||
}
|
||||
|
||||
func NewHomeService(t *gotext.Locale, conf *config.Config, task biz.TaskRepo, website biz.WebsiteRepo, project biz.ProjectRepo, appRepo biz.AppRepo, environment biz.EnvironmentRepo, setting biz.SettingRepo, cron biz.CronRepo, backupRepo biz.BackupRepo, container biz.ContainerRepo) *HomeService {
|
||||
func NewHomeService(t *gotext.Locale, conf *config.Config, task biz.TaskRepo, website biz.WebsiteRepo, project biz.ProjectRepo, appRepo biz.AppRepo, environment biz.EnvironmentRepo, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo, cron biz.CronRepo, backupRepo biz.BackupRepo, container biz.ContainerRepo) *HomeService {
|
||||
return &HomeService{
|
||||
t: t,
|
||||
api: api.NewAPI(app.Version, app.Locale),
|
||||
conf: conf,
|
||||
taskRepo: task,
|
||||
websiteRepo: website,
|
||||
projectRepo: project,
|
||||
appRepo: appRepo,
|
||||
environmentRepo: environment,
|
||||
settingRepo: setting,
|
||||
cronRepo: cron,
|
||||
backupRepo: backupRepo,
|
||||
containerRepo: container,
|
||||
t: t,
|
||||
api: api.NewAPI(app.Version, app.Locale),
|
||||
conf: conf,
|
||||
taskRepo: task,
|
||||
websiteRepo: website,
|
||||
projectRepo: project,
|
||||
appRepo: appRepo,
|
||||
environmentRepo: environment,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
cronRepo: cron,
|
||||
backupRepo: backupRepo,
|
||||
containerRepo: container,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,23 +108,15 @@ func (s *HomeService) SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 所有网卡名称
|
||||
var nets []types.LV
|
||||
netInterfaces, _ := net.Interfaces()
|
||||
for _, v := range netInterfaces {
|
||||
nets = append(nets, types.LV{
|
||||
Value: v.Name,
|
||||
Label: v.Name,
|
||||
})
|
||||
}
|
||||
nets := lo.Map(netInterfaces, func(v net.Interface, _ int) types.LV {
|
||||
return types.LV{Value: v.Name, Label: v.Name}
|
||||
})
|
||||
// 所有硬盘名称
|
||||
var disks []types.LV
|
||||
partitions, _ := disk.Partitions(false)
|
||||
for _, v := range partitions {
|
||||
disks = append(disks, types.LV{
|
||||
Value: v.Device,
|
||||
Label: fmt.Sprintf("%s (%s)", v.Device, v.Mountpoint),
|
||||
})
|
||||
}
|
||||
disks := lo.Map(partitions, func(v disk.PartitionStat, _ int) types.LV {
|
||||
return types.LV{Value: v.Device, Label: fmt.Sprintf("%s (%s)", v.Device, v.Mountpoint)}
|
||||
})
|
||||
|
||||
// 系统是否支持
|
||||
osSupported := true
|
||||
@@ -194,13 +189,12 @@ func (s *HomeService) CountInfo(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if postgresqlInstalled {
|
||||
postgresPassword, _ := s.settingRepo.Get(biz.SettingKeyPostgresPassword)
|
||||
postgres, err := db.NewPostgres("postgres", postgresPassword, "127.0.0.1", 5432)
|
||||
if err == nil {
|
||||
defer postgres.Close()
|
||||
databases, err := postgres.Databases()
|
||||
if err == nil {
|
||||
databaseCount += len(databases)
|
||||
if server, err := s.databaseServerRepo.GetByName("local_postgresql"); err == nil {
|
||||
if postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port); err == nil {
|
||||
defer postgres.Close()
|
||||
if databases, err := postgres.Databases(); err == nil {
|
||||
databaseCount += len(databases)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -233,49 +227,38 @@ 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")
|
||||
clickhouseInstalled, _ := s.appRepo.IsInstalled("slug = ?", "clickhouse")
|
||||
rsyncInstalled, _ := s.appRepo.IsInstalled("slug = ?", "rsync")
|
||||
|
||||
// Go 版本
|
||||
var goData []types.LV
|
||||
for _, slug := range s.environmentRepo.InstalledSlugs("go") {
|
||||
ver := s.environmentRepo.InstalledVersion("go", slug)
|
||||
goData = append(goData, types.LV{Value: slug, Label: fmt.Sprintf("Go %s", ver)})
|
||||
}
|
||||
goData := lo.Map(s.environmentRepo.InstalledSlugs("go"), func(slug string, _ int) types.LV {
|
||||
return types.LV{Value: slug, Label: fmt.Sprintf("Go %s", s.environmentRepo.InstalledVersion("go", slug))}
|
||||
})
|
||||
|
||||
// Java 版本
|
||||
var javaData []types.LV
|
||||
for _, slug := range s.environmentRepo.InstalledSlugs("java") {
|
||||
ver := s.environmentRepo.InstalledVersion("java", slug)
|
||||
javaData = append(javaData, types.LV{Value: slug, Label: fmt.Sprintf("Java %s", ver)})
|
||||
}
|
||||
javaData := lo.Map(s.environmentRepo.InstalledSlugs("java"), func(slug string, _ int) types.LV {
|
||||
return types.LV{Value: slug, Label: fmt.Sprintf("Java %s", s.environmentRepo.InstalledVersion("java", slug))}
|
||||
})
|
||||
|
||||
// Node.js 版本
|
||||
var nodejsData []types.LV
|
||||
for _, slug := range s.environmentRepo.InstalledSlugs("nodejs") {
|
||||
ver := s.environmentRepo.InstalledVersion("nodejs", slug)
|
||||
nodejsData = append(nodejsData, types.LV{Value: slug, Label: fmt.Sprintf("Node.js %s", ver)})
|
||||
}
|
||||
nodejsData := lo.Map(s.environmentRepo.InstalledSlugs("nodejs"), func(slug string, _ int) types.LV {
|
||||
return types.LV{Value: slug, Label: fmt.Sprintf("Node.js %s", s.environmentRepo.InstalledVersion("nodejs", slug))}
|
||||
})
|
||||
|
||||
// PHP 版本
|
||||
var phpData []types.LVInt
|
||||
for _, slug := range s.environmentRepo.InstalledSlugs("php") {
|
||||
ver := s.environmentRepo.InstalledVersion("php", slug)
|
||||
phpData = append(phpData, types.LVInt{Value: cast.ToInt(slug), Label: fmt.Sprintf("PHP %s", ver)})
|
||||
}
|
||||
phpData := lo.Map(s.environmentRepo.InstalledSlugs("php"), func(slug string, _ int) types.LVInt {
|
||||
return types.LVInt{Value: cast.ToInt(slug), Label: fmt.Sprintf("PHP %s", s.environmentRepo.InstalledVersion("php", slug))}
|
||||
})
|
||||
|
||||
// Python 版本
|
||||
var pythonData []types.LV
|
||||
for _, slug := range s.environmentRepo.InstalledSlugs("python") {
|
||||
ver := s.environmentRepo.InstalledVersion("python", slug)
|
||||
pythonData = append(pythonData, types.LV{Value: slug, Label: fmt.Sprintf("Python %s", ver)})
|
||||
}
|
||||
pythonData := lo.Map(s.environmentRepo.InstalledSlugs("python"), func(slug string, _ int) types.LV {
|
||||
return types.LV{Value: slug, Label: fmt.Sprintf("Python %s", s.environmentRepo.InstalledVersion("python", slug))}
|
||||
})
|
||||
|
||||
// .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)})
|
||||
}
|
||||
dotnetData := lo.Map(s.environmentRepo.InstalledSlugs("dotnet"), func(slug string, _ int) types.LV {
|
||||
return types.LV{Value: slug, Label: fmt.Sprintf(".NET %s", s.environmentRepo.InstalledVersion("dotnet", slug))}
|
||||
})
|
||||
|
||||
// 数据库
|
||||
var dbData []types.LV
|
||||
@@ -286,6 +269,9 @@ func (s *HomeService) InstalledEnvironment(w http.ResponseWriter, r *http.Reques
|
||||
if postgresqlInstalled {
|
||||
dbData = append(dbData, types.LV{Value: "postgresql", Label: "PostgreSQL"})
|
||||
}
|
||||
if clickhouseInstalled {
|
||||
dbData = append(dbData, types.LV{Value: "clickhouse", Label: "ClickHouse"})
|
||||
}
|
||||
|
||||
webserver, _ := s.settingRepo.Get(biz.SettingKeyWebserver)
|
||||
Success(w, chix.M{
|
||||
@@ -518,34 +504,23 @@ func (s *HomeService) Goroutines(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
raw := string(buf)
|
||||
blocks := strings.Split(raw, "\n\n")
|
||||
|
||||
var goroutines []GoroutineInfo
|
||||
for _, block := range blocks {
|
||||
goroutines := lo.FilterMap(strings.Split(raw, "\n\n"), func(block string, _ int) (GoroutineInfo, bool) {
|
||||
block = strings.TrimSpace(block)
|
||||
if block == "" {
|
||||
continue
|
||||
return GoroutineInfo{}, false
|
||||
}
|
||||
|
||||
lines := strings.SplitN(block, "\n", 2)
|
||||
matches := goroutineHeaderRe.FindStringSubmatch(lines[0])
|
||||
if matches == nil {
|
||||
continue
|
||||
return GoroutineInfo{}, false
|
||||
}
|
||||
|
||||
id, _ := strconv.Atoi(matches[1])
|
||||
state := matches[2]
|
||||
stack := ""
|
||||
if len(lines) > 1 {
|
||||
stack = lines[1]
|
||||
}
|
||||
|
||||
goroutines = append(goroutines, GoroutineInfo{
|
||||
ID: id,
|
||||
State: state,
|
||||
Stack: stack,
|
||||
})
|
||||
}
|
||||
return GoroutineInfo{ID: id, State: matches[2], Stack: stack}, true
|
||||
})
|
||||
|
||||
Success(w, goroutines)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
@@ -187,12 +189,12 @@ func (s *MonitorService) List(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 将 map 转换为 slice
|
||||
for _, device := range netDeviceData {
|
||||
list.Net = append(list.Net, *device)
|
||||
}
|
||||
for _, disk := range diskIOData {
|
||||
list.DiskIO = append(list.DiskIO, *disk)
|
||||
}
|
||||
list.Net = lo.Map(lo.Values(netDeviceData), func(device *types.Network, _ int) types.Network {
|
||||
return *device
|
||||
})
|
||||
list.DiskIO = lo.Map(lo.Values(diskIOData), func(disk *types.DiskIO, _ int) types.DiskIO {
|
||||
return *disk
|
||||
})
|
||||
|
||||
Success(w, list)
|
||||
}
|
||||
|
||||
@@ -140,3 +140,19 @@ func (s *SystemctlService) Stop(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
// ClearLog 清空指定 systemd 服务的 journald 日志
|
||||
func (s *SystemctlService) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.SystemctlService](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.LogClear(req.Service); err != nil {
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to clear log for %s service: %v", req.Service, err))
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func (s *ToolboxMigrationService) Exec(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("bash", "-c", req.Command)
|
||||
cmd := exec.CommandContext(r.Context(), "bash", "-c", req.Command)
|
||||
pr, pw := io.Pipe()
|
||||
cmd.Stdout = pw
|
||||
cmd.Stderr = pw
|
||||
@@ -779,7 +779,7 @@ func (s *ToolboxMigrationService) remoteExec(conn *request.ToolboxMigrationConne
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
|
||||
resp, err := client.R().
|
||||
SetDoNotParseResponse(true).
|
||||
SetResponseDoNotParse(true).
|
||||
SetBody(map[string]string{"command": command}).
|
||||
Post("/api/toolbox_migration/exec")
|
||||
if err != nil {
|
||||
@@ -1110,7 +1110,7 @@ func (s *ToolboxMigrationService) newRestyClient(conn *request.ToolboxMigrationC
|
||||
client.SetTimeout(timeout)
|
||||
}
|
||||
|
||||
// 签名中间件放在 PrepareRequestMiddleware 之后,此时 RawRequest 已构建完毕
|
||||
// 签名中间件放在 MiddlewareRequestCreate 之后,此时 RawRequest 已构建完毕
|
||||
signMiddleware := resty.RequestMiddleware(func(_ *resty.Client, req *resty.Request) error {
|
||||
rawReq := req.RawRequest
|
||||
|
||||
@@ -1143,7 +1143,7 @@ func (s *ToolboxMigrationService) newRestyClient(conn *request.ToolboxMigrationC
|
||||
})
|
||||
|
||||
client.SetRequestMiddlewares(
|
||||
resty.PrepareRequestMiddleware,
|
||||
resty.MiddlewareRequestCreate,
|
||||
signMiddleware,
|
||||
)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -181,15 +182,9 @@ func (s *ToolboxSystemService) GetTimezone(w http.ResponseWriter, r *http.Reques
|
||||
Error(w, http.StatusInternalServerError, s.t.Get("failed to get available timezones: %v", err))
|
||||
return
|
||||
}
|
||||
zones := strings.Split(zonesRaw, "\n")
|
||||
|
||||
var zonesList []types.LV
|
||||
for _, z := range zones {
|
||||
zonesList = append(zonesList, types.LV{
|
||||
Label: z,
|
||||
Value: z,
|
||||
})
|
||||
}
|
||||
zonesList := lo.Map(strings.Split(zonesRaw, "\n"), func(z string, _ int) types.LV {
|
||||
return types.LV{Label: z, Value: z}
|
||||
})
|
||||
|
||||
Success(w, chix.M{
|
||||
"timezone": match[1],
|
||||
|
||||
@@ -177,21 +177,6 @@ func (s *WebsiteService) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *WebsiteService) ClearLog(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.websiteRepo.ClearLog(req.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *WebsiteService) UpdateRemark(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.WebsiteUpdateRemark](r)
|
||||
if err != nil {
|
||||
|
||||
@@ -193,10 +193,7 @@ func (s *WebsiteStatService) SiteStats(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 转换为切片
|
||||
items := make([]*biz.WebsiteStatSiteItem, 0, len(siteMap))
|
||||
for _, item := range siteMap {
|
||||
items = append(items, item)
|
||||
}
|
||||
items := lo.Values(siteMap)
|
||||
|
||||
Success(w, chix.M{
|
||||
"items": items,
|
||||
@@ -260,14 +257,12 @@ func (s *WebsiteStatService) ClientStats(w http.ResponseWriter, r *http.Request)
|
||||
Name string `json:"name"`
|
||||
Requests uint64 `json:"requests"`
|
||||
}
|
||||
browsers := make([]rankItem, 0, len(browserMap))
|
||||
for name, reqs := range browserMap {
|
||||
browsers = append(browsers, rankItem{Name: name, Requests: reqs})
|
||||
}
|
||||
oss := make([]rankItem, 0, len(osMap))
|
||||
for name, reqs := range osMap {
|
||||
oss = append(oss, rankItem{Name: name, Requests: reqs})
|
||||
}
|
||||
browsers := lo.MapToSlice(browserMap, func(name string, reqs uint64) rankItem {
|
||||
return rankItem{Name: name, Requests: reqs}
|
||||
})
|
||||
oss := lo.MapToSlice(osMap, func(name string, reqs uint64) rankItem {
|
||||
return rankItem{Name: name, Requests: reqs}
|
||||
})
|
||||
|
||||
// 按请求数排序
|
||||
slices.SortFunc(browsers, func(a, b rankItem) int { return cmp.Compare(b.Requests, a.Requests) })
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -83,6 +84,83 @@ func (s *WsService) Exec(w http.ResponseWriter, r *http.Request) {
|
||||
s.readLoop(ctx, ws)
|
||||
}
|
||||
|
||||
// Follow 文件或 systemd 服务实时跟踪
|
||||
// path 给定时用 tail -F;service 给定时用 journalctl -f
|
||||
func (s *WsService) Follow(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.FileFollow](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if req.Path == "" && req.Service == "" && req.Container == "" {
|
||||
Error(w, http.StatusUnprocessableEntity, s.t.Get("path, service or container is required"))
|
||||
return
|
||||
}
|
||||
|
||||
ws, err := s.upgrade(w, r)
|
||||
if err != nil {
|
||||
s.log.Warn("upgrade follow ws error", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
defer func(ws *websocket.Conn) { _ = ws.CloseNow() }(ws)
|
||||
|
||||
ctx, cancel := context.WithCancel(r.Context())
|
||||
defer cancel()
|
||||
|
||||
if req.Container != "" {
|
||||
s.followContainer(ctx, ws, req.Container)
|
||||
return
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if req.Service != "" {
|
||||
cmd = exec.CommandContext(ctx, "journalctl", "--no-pager", "-n", "0", "-f", "-u", req.Service)
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, "tail", "-n", "0", "-F", req.Path)
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
_ = ws.Close(websocket.StatusNormalClosure, err.Error())
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
_ = ws.Close(websocket.StatusNormalClosure, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
defer cancel() // 进程输出结束时取消,促使下方读取循环退出
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, rerr := stdout.Read(buf)
|
||||
if n > 0 {
|
||||
if werr := ws.Write(ctx, websocket.MessageBinary, buf[:n]); werr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if rerr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 连接结束时取消 ctx 杀掉进程,待输出读取完成后 Wait 回收,避免残留僵尸进程
|
||||
defer func() {
|
||||
cancel()
|
||||
<-done
|
||||
_ = cmd.Wait()
|
||||
}()
|
||||
|
||||
for {
|
||||
_, _, rerr := ws.Read(ctx)
|
||||
if rerr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PTY 通用 PTY 命令执行
|
||||
// 前端发送第一条消息为要执行的命令,后端通过 PTY 执行并实时返回输出
|
||||
func (s *WsService) PTY(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -382,6 +460,72 @@ func (s *WsService) getContainerSock() string {
|
||||
return sock
|
||||
}
|
||||
|
||||
// wsBinaryWriter 将写入转发为 WebSocket 二进制帧
|
||||
type wsBinaryWriter struct {
|
||||
ctx context.Context
|
||||
ws *websocket.Conn
|
||||
}
|
||||
|
||||
func (w *wsBinaryWriter) Write(p []byte) (int, error) {
|
||||
if err := w.ws.Write(w.ctx, websocket.MessageBinary, p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// followContainer 通过 Docker SDK 实时跟踪容器日志并转发到 WebSocket
|
||||
func (s *WsService) followContainer(ctx context.Context, ws *websocket.Conn, id string) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
apiClient, err := client.New(client.WithHost(s.getContainerSock()))
|
||||
if err != nil {
|
||||
_ = ws.Close(websocket.StatusNormalClosure, err.Error())
|
||||
return
|
||||
}
|
||||
defer func(apiClient *client.Client) { _ = apiClient.Close() }(apiClient)
|
||||
|
||||
// 非 TTY 容器日志为多路复用流,需按 TTY 设置决定是否解复用
|
||||
inspect, err := apiClient.ContainerInspect(ctx, id, client.ContainerInspectOptions{})
|
||||
if err != nil {
|
||||
_ = ws.Close(websocket.StatusNormalClosure, err.Error())
|
||||
return
|
||||
}
|
||||
tty := inspect.Container.Config != nil && inspect.Container.Config.Tty
|
||||
|
||||
// Tail "0" 表示只推送新增日志,历史由 HTTP /file/tail 反向分页加载
|
||||
reader, err := apiClient.ContainerLogs(ctx, id, client.ContainerLogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Follow: true,
|
||||
Tail: "0",
|
||||
})
|
||||
if err != nil {
|
||||
_ = ws.Close(websocket.StatusNormalClosure, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
defer cancel() // 日志流结束(如容器停止)时取消,促使下方读取循环退出
|
||||
_ = docker.CopyLogs(&wsBinaryWriter{ctx: ctx, ws: ws}, reader, tty)
|
||||
}()
|
||||
|
||||
// 连接结束时取消 ctx 并关闭日志流,待拷贝协程退出后返回,避免泄漏
|
||||
defer func() {
|
||||
cancel()
|
||||
_ = reader.Close()
|
||||
<-done
|
||||
}()
|
||||
|
||||
for {
|
||||
if _, _, rerr := ws.Read(ctx); rerr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WsService) upgrade(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
|
||||
opts := &websocket.AcceptOptions{
|
||||
CompressionMode: websocket.CompressionContextTakeover,
|
||||
|
||||
+15
-14
@@ -238,9 +238,9 @@ func (_c *ContainerRepo_ListByName_Call) RunAndReturn(run func(string) ([]types.
|
||||
return _c
|
||||
}
|
||||
|
||||
// Logs provides a mock function with given fields: id
|
||||
func (_m *ContainerRepo) Logs(id string) (string, error) {
|
||||
ret := _m.Called(id)
|
||||
// Logs provides a mock function with given fields: id, tail
|
||||
func (_m *ContainerRepo) Logs(id string, tail int) (string, error) {
|
||||
ret := _m.Called(id, tail)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Logs")
|
||||
@@ -248,17 +248,17 @@ func (_m *ContainerRepo) Logs(id string) (string, error) {
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
|
||||
return rf(id)
|
||||
if rf, ok := ret.Get(0).(func(string, int) (string, error)); ok {
|
||||
return rf(id, tail)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) string); ok {
|
||||
r0 = rf(id)
|
||||
if rf, ok := ret.Get(0).(func(string, int) string); ok {
|
||||
r0 = rf(id, tail)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(id)
|
||||
if rf, ok := ret.Get(1).(func(string, int) error); ok {
|
||||
r1 = rf(id, tail)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
@@ -273,13 +273,14 @@ type ContainerRepo_Logs_Call struct {
|
||||
|
||||
// Logs is a helper method to define mock.On call
|
||||
// - id string
|
||||
func (_e *ContainerRepo_Expecter) Logs(id interface{}) *ContainerRepo_Logs_Call {
|
||||
return &ContainerRepo_Logs_Call{Call: _e.mock.On("Logs", id)}
|
||||
// - tail int
|
||||
func (_e *ContainerRepo_Expecter) Logs(id interface{}, tail interface{}) *ContainerRepo_Logs_Call {
|
||||
return &ContainerRepo_Logs_Call{Call: _e.mock.On("Logs", id, tail)}
|
||||
}
|
||||
|
||||
func (_c *ContainerRepo_Logs_Call) Run(run func(id string)) *ContainerRepo_Logs_Call {
|
||||
func (_c *ContainerRepo_Logs_Call) Run(run func(id string, tail int)) *ContainerRepo_Logs_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
run(args[0].(string), args[1].(int))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
@@ -289,7 +290,7 @@ func (_c *ContainerRepo_Logs_Call) Return(_a0 string, _a1 error) *ContainerRepo_
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ContainerRepo_Logs_Call) RunAndReturn(run func(string) (string, error)) *ContainerRepo_Logs_Call {
|
||||
func (_c *ContainerRepo_Logs_Call) RunAndReturn(run func(string, int) (string, error)) *ContainerRepo_Logs_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -537,6 +537,53 @@ func (_c *DatabaseServerRepo_UpdatePassword_Call) RunAndReturn(run func(string,
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdatePort provides a mock function with given fields: name, port
|
||||
func (_m *DatabaseServerRepo) UpdatePort(name string, port uint) error {
|
||||
ret := _m.Called(name, port)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdatePort")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, uint) error); ok {
|
||||
r0 = rf(name, port)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DatabaseServerRepo_UpdatePort_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdatePort'
|
||||
type DatabaseServerRepo_UpdatePort_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdatePort is a helper method to define mock.On call
|
||||
// - name string
|
||||
// - port uint
|
||||
func (_e *DatabaseServerRepo_Expecter) UpdatePort(name interface{}, port interface{}) *DatabaseServerRepo_UpdatePort_Call {
|
||||
return &DatabaseServerRepo_UpdatePort_Call{Call: _e.mock.On("UpdatePort", name, port)}
|
||||
}
|
||||
|
||||
func (_c *DatabaseServerRepo_UpdatePort_Call) Run(run func(name string, port uint)) *DatabaseServerRepo_UpdatePort_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(uint))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *DatabaseServerRepo_UpdatePort_Call) Return(_a0 error) *DatabaseServerRepo_UpdatePort_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *DatabaseServerRepo_UpdatePort_Call) RunAndReturn(run func(string, uint) error) *DatabaseServerRepo_UpdatePort_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateRemark provides a mock function with given fields: req
|
||||
func (_m *DatabaseServerRepo) UpdateRemark(req *request.DatabaseServerUpdateRemark) error {
|
||||
ret := _m.Called(req)
|
||||
|
||||
@@ -29,52 +29,6 @@ func (_m *WebsiteRepo) EXPECT() *WebsiteRepo_Expecter {
|
||||
return &WebsiteRepo_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// ClearLog provides a mock function with given fields: id
|
||||
func (_m *WebsiteRepo) ClearLog(id uint) error {
|
||||
ret := _m.Called(id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ClearLog")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(uint) error); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// WebsiteRepo_ClearLog_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClearLog'
|
||||
type WebsiteRepo_ClearLog_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ClearLog is a helper method to define mock.On call
|
||||
// - id uint
|
||||
func (_e *WebsiteRepo_Expecter) ClearLog(id interface{}) *WebsiteRepo_ClearLog_Call {
|
||||
return &WebsiteRepo_ClearLog_Call{Call: _e.mock.On("ClearLog", id)}
|
||||
}
|
||||
|
||||
func (_c *WebsiteRepo_ClearLog_Call) Run(run func(id uint)) *WebsiteRepo_ClearLog_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(uint))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WebsiteRepo_ClearLog_Call) Return(_a0 error) *WebsiteRepo_ClearLog_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *WebsiteRepo_ClearLog_Call) RunAndReturn(run func(uint) error) *WebsiteRepo_ClearLog_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Count provides a mock function with no fields
|
||||
func (_m *WebsiteRepo) Count() (int64, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
+4
-3
@@ -34,9 +34,10 @@ type DnsOption struct {
|
||||
// UseDns 使用 DNS 接口验证
|
||||
func (c *Client) UseDns(dnsType DnsType, param DNSParam, opt ...DnsOption) {
|
||||
solver := &dnsSolver{
|
||||
dns: dnsType,
|
||||
param: param,
|
||||
records: make(map[string][]libdns.Record),
|
||||
dns: dnsType,
|
||||
param: param,
|
||||
keyAuths: make(map[string][]string),
|
||||
records: make(map[string][]libdns.Record),
|
||||
}
|
||||
if len(opt) > 0 {
|
||||
solver.alias = opt[0].Alias
|
||||
|
||||
+34
-31
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/libdns/tencentcloud"
|
||||
"github.com/libdns/westcn"
|
||||
"github.com/mholt/acmez/v3/acme"
|
||||
"github.com/samber/lo"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
|
||||
pkgos "github.com/acepanel/panel/v3/pkg/os"
|
||||
@@ -115,14 +116,7 @@ func (s *panelSolver) startServer() error {
|
||||
}
|
||||
|
||||
func (s *panelSolver) writeNginxConfig() error {
|
||||
names := make([]string, len(s.ip))
|
||||
hasIPv6 := false
|
||||
for i, host := range s.ip {
|
||||
names[i] = s.formatServerName(host)
|
||||
if s.isIPv6(host) {
|
||||
hasIPv6 = true
|
||||
}
|
||||
}
|
||||
hasIPv6 := lo.SomeBy(s.ip, s.isIPv6)
|
||||
|
||||
var conf strings.Builder
|
||||
conf.WriteString("server {\n listen 80;\n")
|
||||
@@ -130,6 +124,12 @@ func (s *panelSolver) writeNginxConfig() error {
|
||||
if hasIPv6 {
|
||||
conf.WriteString(" listen [::]:80;\n")
|
||||
}
|
||||
names := lo.Map(s.ip, func(ip string, _ int) string {
|
||||
if s.isIPv6(ip) {
|
||||
return "[" + ip + "]"
|
||||
}
|
||||
return ip
|
||||
})
|
||||
_, _ = fmt.Fprintf(&conf, " server_name %s;\n", strings.Join(names, " "))
|
||||
for path, token := range s.tokens {
|
||||
_, _ = fmt.Fprintf(&conf, " location = %s {\n default_type text/plain;\n return 200 %q;\n }\n", path, token)
|
||||
@@ -165,12 +165,14 @@ func (s *panelSolver) writeApacheConfig() error {
|
||||
}
|
||||
|
||||
var conf strings.Builder
|
||||
_, _ = fmt.Fprintf(&conf, "<VirtualHost *:80>\n ServerName %s\n", s.formatServerName(s.ip[0]))
|
||||
if len(s.ip) > 1 {
|
||||
for _, ip := range s.ip[1:] {
|
||||
_, _ = fmt.Fprintf(&conf, " ServerAlias %s\n", s.formatServerName(ip))
|
||||
addrs := lo.Map(s.ip, func(ip string, _ int) string {
|
||||
if s.isIPv6(ip) {
|
||||
return "[" + ip + "]:80"
|
||||
}
|
||||
}
|
||||
return ip + ":80"
|
||||
})
|
||||
_, _ = fmt.Fprintf(&conf, "<VirtualHost %s>\n", strings.Join(addrs, " "))
|
||||
conf.WriteString(" ServerName acme-ip-validation\n")
|
||||
_, _ = fmt.Fprintf(&conf, " Alias /.well-known/acme-challenge %s\n", tokenDir)
|
||||
_, _ = fmt.Fprintf(&conf, " <Directory %s>\n", tokenDir)
|
||||
conf.WriteString(" Require all granted\n")
|
||||
@@ -240,14 +242,6 @@ func (s *panelSolver) isIPv6(host string) bool {
|
||||
return err == nil && !addr.Is4()
|
||||
}
|
||||
|
||||
// formatServerName 格式化 web 服务器配置中的主机地址,IPv6 地址需要用 [] 包裹以匹配 Host 头
|
||||
func (s *panelSolver) formatServerName(host string) string {
|
||||
if s.isIPv6(host) {
|
||||
return "[" + host + "]"
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
type httpSolver struct {
|
||||
conf string
|
||||
webServer string // "nginx" or "apache"
|
||||
@@ -428,7 +422,8 @@ type dnsSolver struct {
|
||||
mu sync.Mutex
|
||||
dns DnsType
|
||||
param DNSParam
|
||||
records map[string][]libdns.Record // dnsName|keyAuth → records
|
||||
keyAuths map[string][]string // dnsName → keyAuth 列表
|
||||
records map[string][]libdns.Record // dnsName → 已设置的记录
|
||||
alias map[string]string // DNS 验证别名映射 (domain → delegated domain)
|
||||
dnsServer string // DNS 验证服务器地址
|
||||
skipVerify bool // 跳过解析验证
|
||||
@@ -448,12 +443,20 @@ func (s *dnsSolver) Present(ctx context.Context, challenge acme.Challenge) error
|
||||
|
||||
s.report(fmt.Sprintf("setting DNS TXT record %s", dnsName))
|
||||
|
||||
rec := libdns.TXT{
|
||||
Name: libdns.RelativeName(dnsName+".", zone+"."),
|
||||
Text: keyAuth,
|
||||
// 同名 TXT 记录可能对应多个 challenge, SetRecords 以 (name, type) 为单位覆盖
|
||||
// 因此需把该记录名下所有 keyAuth 一次性写入,避免后者覆盖前者
|
||||
s.mu.Lock()
|
||||
s.keyAuths[dnsName] = append(s.keyAuths[dnsName], keyAuth)
|
||||
recs := make([]libdns.Record, 0, len(s.keyAuths[dnsName]))
|
||||
for _, ka := range s.keyAuths[dnsName] {
|
||||
recs = append(recs, libdns.TXT{
|
||||
Name: libdns.RelativeName(dnsName+".", zone+"."),
|
||||
Text: ka,
|
||||
})
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
results, err := provider.SetRecords(ctx, zone+".", []libdns.Record{rec})
|
||||
results, err := provider.SetRecords(ctx, zone+".", recs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set DNS record %q for %q: %w", dnsName, zone, err)
|
||||
}
|
||||
@@ -462,8 +465,7 @@ func (s *dnsSolver) Present(ctx context.Context, challenge acme.Challenge) error
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
key := dnsName + "|" + keyAuth
|
||||
s.records[key] = append(s.records[key], results...)
|
||||
s.records[dnsName] = results
|
||||
s.mu.Unlock()
|
||||
|
||||
s.report(fmt.Sprintf("DNS TXT record %s set successfully", dnsName))
|
||||
@@ -552,10 +554,11 @@ func (s *dnsSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error
|
||||
|
||||
s.report("cleaning up DNS TXT records")
|
||||
|
||||
// 同名 TXT 记录下的多条 challenge 记录由首次 CleanUp 一并删除
|
||||
s.mu.Lock()
|
||||
key := dnsName + "|" + challenge.DNS01KeyAuthorization()
|
||||
records := s.records[key]
|
||||
delete(s.records, key)
|
||||
records := s.records[dnsName]
|
||||
delete(s.records, dnsName)
|
||||
delete(s.keyAuths, dnsName)
|
||||
s.mu.Unlock()
|
||||
|
||||
if len(records) > 0 {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ func (r *API) GoogleEAB() (*EAB, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.IsSuccess() {
|
||||
if !resp.IsStatusSuccess() {
|
||||
return nil, fmt.Errorf("failed to get google eab: %s", resp.String())
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -34,7 +34,7 @@ func (r *API) Apps() (*Apps, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.IsSuccess() {
|
||||
if !resp.IsStatusSuccess() {
|
||||
return nil, fmt.Errorf("failed to get apps: %s", resp.String())
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (r *API) AppBySlug(slug string) (*App, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.IsSuccess() {
|
||||
if !resp.IsStatusSuccess() {
|
||||
return nil, fmt.Errorf("failed to get app: %s", resp.String())
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func (r *API) AppCallback(slug string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !resp.IsSuccess() {
|
||||
if !resp.IsStatusSuccess() {
|
||||
return fmt.Errorf("failed to callback app: %s", resp.String())
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ func (r *API) Categories() (*Categories, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.IsSuccess() {
|
||||
if !resp.IsStatusSuccess() {
|
||||
return nil, fmt.Errorf("failed to get categories: %s", resp.String())
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user